diff --git a/extensions/spotify/build.gradle.kts b/extensions/spotify/build.gradle.kts
index eb963a007..fd346d93b 100644
--- a/extensions/spotify/build.gradle.kts
+++ b/extensions/spotify/build.gradle.kts
@@ -7,7 +7,6 @@ dependencies {
compileOnly(project(":extensions:spotify:stub"))
compileOnly(libs.annotation)
- implementation(project(":extensions:spotify:utils"))
implementation(libs.nanohttpd)
implementation(libs.protobuf.javalite)
}
diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/ClientTokenService.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/ClientTokenService.java
new file mode 100644
index 000000000..0345b19ef
--- /dev/null
+++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/ClientTokenService.java
@@ -0,0 +1,115 @@
+package app.revanced.extension.spotify.misc.fix;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.spotify.misc.fix.clienttoken.data.v0.ClienttokenHttp.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import static app.revanced.extension.spotify.misc.fix.Constants.*;
+
+class ClientTokenService {
+ private static final String IOS_CLIENT_ID = "58bd3c95768941ea9eb4350aaa033eb3";
+ private static final String IOS_USER_AGENT;
+
+ static {
+ String clientVersion = getClientVersion();
+ int commitHashIndex = clientVersion.lastIndexOf(".");
+ String version = clientVersion.substring(
+ clientVersion.indexOf("-") + 1,
+ clientVersion.lastIndexOf(".", commitHashIndex - 1)
+ );
+
+ IOS_USER_AGENT = "Spotify/" + version + " iOS/" + getSystemVersion() + " (" + getHardwareMachine() + ")";
+ }
+
+ private static final ConnectivitySdkData.Builder IOS_CONNECTIVITY_SDK_DATA =
+ ConnectivitySdkData.newBuilder()
+ .setPlatformSpecificData(PlatformSpecificData.newBuilder()
+ .setIos(NativeIOSData.newBuilder()
+ .setHwMachine(getHardwareMachine())
+ .setSystemVersion(getSystemVersion())
+ )
+ );
+
+ private static final ClientDataRequest.Builder IOS_CLIENT_DATA_REQUEST =
+ ClientDataRequest.newBuilder()
+ .setClientVersion(getClientVersion())
+ .setClientId(IOS_CLIENT_ID);
+
+ private static final ClientTokenRequest.Builder IOS_CLIENT_TOKEN_REQUEST =
+ ClientTokenRequest.newBuilder()
+ .setRequestType(ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST);
+
+
+ @NonNull
+ static ClientTokenRequest newIOSClientTokenRequest(String deviceId) {
+ Logger.printInfo(() -> "Creating new iOS client token request with device ID: " + deviceId);
+
+ return IOS_CLIENT_TOKEN_REQUEST
+ .setClientData(IOS_CLIENT_DATA_REQUEST
+ .setConnectivitySdkData(IOS_CONNECTIVITY_SDK_DATA
+ .setDeviceId(deviceId)
+ )
+ )
+ .build();
+ }
+
+ @Nullable
+ static ClientTokenResponse getClientTokenResponse(@NonNull ClientTokenRequest request) {
+ if (request.getRequestType() == ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST) {
+ Logger.printInfo(() -> "Requesting iOS client token");
+ String deviceId = request.getClientData().getConnectivitySdkData().getDeviceId();
+ request = newIOSClientTokenRequest(deviceId);
+ }
+
+ ClientTokenResponse response;
+ try {
+ response = requestClientToken(request);
+ } catch (IOException ex) {
+ Logger.printException(() -> "Failed to handle request", ex);
+ return null;
+ }
+
+ return response;
+ }
+
+ @NonNull
+ private static ClientTokenResponse requestClientToken(@NonNull ClientTokenRequest request) throws IOException {
+ HttpURLConnection urlConnection = (HttpURLConnection) new URL(CLIENT_TOKEN_API_URL).openConnection();
+ urlConnection.setRequestMethod("POST");
+ urlConnection.setDoOutput(true);
+ urlConnection.setRequestProperty("Content-Type", "application/x-protobuf");
+ urlConnection.setRequestProperty("Accept", "application/x-protobuf");
+ urlConnection.setRequestProperty("User-Agent", IOS_USER_AGENT);
+
+ byte[] requestArray = request.toByteArray();
+ urlConnection.setFixedLengthStreamingMode(requestArray.length);
+ urlConnection.getOutputStream().write(requestArray);
+
+ try (InputStream inputStream = urlConnection.getInputStream()) {
+ return ClientTokenResponse.parseFrom(inputStream);
+ }
+ }
+
+ @Nullable
+ static ClientTokenResponse serveClientTokenRequest(@NonNull InputStream inputStream) {
+ ClientTokenRequest request;
+ try {
+ request = ClientTokenRequest.parseFrom(inputStream);
+ } catch (IOException ex) {
+ Logger.printException(() -> "Failed to parse request from input stream", ex);
+ return null;
+ }
+ Logger.printInfo(() -> "Request of type: " + request.getRequestType());
+
+ ClientTokenResponse response = getClientTokenResponse(request);
+ if (response != null) Logger.printInfo(() -> "Response of type: " + response.getResponseType());
+
+ return response;
+ }
+}
diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/Constants.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/Constants.java
new file mode 100644
index 000000000..4928da7ad
--- /dev/null
+++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/Constants.java
@@ -0,0 +1,26 @@
+package app.revanced.extension.spotify.misc.fix;
+
+import androidx.annotation.NonNull;
+
+class Constants {
+ static final String CLIENT_TOKEN_API_PATH = "/v1/clienttoken";
+ static final String CLIENT_TOKEN_API_URL = "https://clienttoken.spotify.com" + CLIENT_TOKEN_API_PATH;
+
+ // Modified by a patch. Do not touch.
+ @NonNull
+ static String getClientVersion() {
+ return "";
+ }
+
+ // Modified by a patch. Do not touch.
+ @NonNull
+ static String getSystemVersion() {
+ return "";
+ }
+
+ // Modified by a patch. Do not touch.
+ @NonNull
+ static String getHardwareMachine() {
+ return "";
+ }
+}
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
deleted file mode 100644
index 2a6107c31..000000000
--- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/LoginRequestListener.java
+++ /dev/null
@@ -1,158 +0,0 @@
-package app.revanced.extension.spotify.misc.fix;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.spotify.login5.v4.proto.Login5.*;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.MessageLite;
-import fi.iki.elonen.NanoHTTPD;
-
-import java.io.ByteArrayInputStream;
-import java.io.FilterInputStream;
-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
- @Override
- public Response serve(IHTTPSession request) {
- Logger.printInfo(() -> "Serving request for URI: " + request.getUri());
-
- InputStream requestBodyInputStream = getRequestBodyInputStream(request);
-
- LoginRequest loginRequest;
- try {
- loginRequest = LoginRequest.parseFrom(requestBodyInputStream);
- } catch (IOException ex) {
- Logger.printException(() -> "Failed to parse LoginRequest", ex);
- return newResponse(INTERNAL_ERROR);
- }
-
- MessageLite loginResponse;
-
- // A request may be made concurrently by Spotify,
- // 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) {
- try {
- loginResponse = getLoginResponse(loginRequest);
- } catch (Exception ex) {
- Logger.printException(() -> "Failed to get login response", ex);
- return newResponse(INTERNAL_ERROR);
- }
- }
-
- return newResponse(Response.Status.OK, loginResponse);
- }
-
-
- private static LoginResponse getLoginResponse(@NonNull LoginRequest loginRequest) {
- Session session;
-
- if (!loginRequest.hasStoredCredential()) {
- Logger.printInfo(() -> "Received request for initial 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);
- }
-
- private static LoginResponse toLoginResponse(@Nullable Session session) {
- LoginResponse.Builder builder = LoginResponse.newBuilder();
-
- if (session == null) {
- 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 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);
- builder.setOk(LoginOk.newBuilder()
- .setUsername(session.username)
- .setAccessToken(session.accessToken)
- .setStoredCredential(ByteString.fromHex("00")) // Placeholder, as it cannot be null or empty.
- .setAccessTokenExpiresIn(session.accessTokenExpiresInSeconds())
- .build());
- }
-
- return builder.build();
- }
-
- @NonNull
- private static InputStream limitedInputStream(InputStream inputStream, long contentLength) {
- return new FilterInputStream(inputStream) {
- private long remaining = contentLength;
-
- @Override
- public int read() throws IOException {
- if (remaining <= 0) return -1;
- int result = super.read();
- if (result != -1) remaining--;
- return result;
- }
-
- @Override
- public int read(byte[] b, int off, int len) throws IOException {
- if (remaining <= 0) return -1;
- len = (int) Math.min(len, remaining);
- int result = super.read(b, off, len);
- if (result != -1) remaining -= result;
- return result;
- }
- };
- }
-
- @NonNull
- private static InputStream getRequestBodyInputStream(@NonNull IHTTPSession request) {
- long requestContentLength =
- Long.parseLong(Objects.requireNonNull(request.getHeaders().get("content-length")));
- return limitedInputStream(request.getInputStream(), requestContentLength);
- }
-
-
- @SuppressWarnings("SameParameterValue")
- @NonNull
- private static Response newResponse(Response.Status status) {
- return newResponse(status, null);
- }
-
- @NonNull
- private static Response newResponse(Response.IStatus status, MessageLite messageLite) {
- if (messageLite == null) {
- return newFixedLengthResponse(status, "application/x-protobuf", null);
- }
-
- byte[] messageBytes = messageLite.toByteArray();
- InputStream stream = new ByteArrayInputStream(messageBytes);
- return newFixedLengthResponse(status, "application/x-protobuf", stream, messageBytes.length);
- }
-}
diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/RequestListener.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/RequestListener.java
new file mode 100644
index 000000000..2de6caa3e
--- /dev/null
+++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/RequestListener.java
@@ -0,0 +1,94 @@
+package app.revanced.extension.spotify.misc.fix;
+
+import androidx.annotation.NonNull;
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.spotify.misc.fix.clienttoken.data.v0.ClienttokenHttp.ClientTokenResponse;
+import com.google.protobuf.MessageLite;
+import fi.iki.elonen.NanoHTTPD;
+
+import java.io.ByteArrayInputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+import static app.revanced.extension.spotify.misc.fix.ClientTokenService.serveClientTokenRequest;
+import static app.revanced.extension.spotify.misc.fix.Constants.CLIENT_TOKEN_API_PATH;
+import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
+
+class RequestListener extends NanoHTTPD {
+ RequestListener(int port) {
+ super(port);
+
+ try {
+ start();
+ } catch (IOException ex) {
+ Logger.printException(() -> "Failed to start request listener on port " + port, ex);
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @NonNull
+ @Override
+ public Response serve(@NonNull IHTTPSession session) {
+ String uri = session.getUri();
+ if (!uri.equals(CLIENT_TOKEN_API_PATH)) return INTERNAL_ERROR_RESPONSE;
+
+ Logger.printInfo(() -> "Serving request for URI: " + uri);
+
+ ClientTokenResponse response = serveClientTokenRequest(getInputStream(session));
+ if (response != null) return newResponse(Response.Status.OK, response);
+
+ Logger.printException(() -> "Failed to serve client token request");
+ return INTERNAL_ERROR_RESPONSE;
+ }
+
+ @NonNull
+ private static InputStream newLimitedInputStream(InputStream inputStream, long contentLength) {
+ return new FilterInputStream(inputStream) {
+ private long remaining = contentLength;
+
+ @Override
+ public int read() throws IOException {
+ if (remaining <= 0) return -1;
+ int result = super.read();
+ if (result != -1) remaining--;
+ return result;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (remaining <= 0) return -1;
+ len = (int) Math.min(len, remaining);
+ int result = super.read(b, off, len);
+ if (result != -1) remaining -= result;
+ return result;
+ }
+ };
+ }
+
+ @NonNull
+ private static InputStream getInputStream(@NonNull IHTTPSession session) {
+ long requestContentLength = Long.parseLong(Objects.requireNonNull(session.getHeaders().get("content-length")));
+ return newLimitedInputStream(session.getInputStream(), requestContentLength);
+ }
+
+ private static final Response INTERNAL_ERROR_RESPONSE = newResponse(INTERNAL_ERROR);
+
+ @SuppressWarnings("SameParameterValue")
+ @NonNull
+ private static Response newResponse(Response.Status status) {
+ return newResponse(status, null);
+ }
+
+ @NonNull
+ private static Response newResponse(Response.IStatus status, MessageLite messageLite) {
+ if (messageLite == null) {
+ return newFixedLengthResponse(status, "application/x-protobuf", null);
+ }
+
+ byte[] messageBytes = messageLite.toByteArray();
+ InputStream stream = new ByteArrayInputStream(messageBytes);
+ return newFixedLengthResponse(status, "application/x-protobuf", stream, messageBytes.length);
+ }
+}
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
deleted file mode 100644
index 0860253f6..000000000
--- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/Session.java
+++ /dev/null
@@ -1,136 +0,0 @@
-package app.revanced.extension.spotify.misc.fix;
-
-import android.content.SharedPreferences;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import static android.content.Context.MODE_PRIVATE;
-
-class Session {
- /**
- * Username of the account. Null if this session does not have an authenticated user.
- */
- @Nullable
- final String username;
- /**
- * Access token for this session.
- */
- final String accessToken;
- /**
- * Session expiration timestamp in milliseconds.
- */
- final Long expirationTime;
- /**
- * Authentication cookies for this 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.
- * @param cookies Authentication cookies for this session.
- */
- Session(@Nullable String username, String accessToken, String cookies) {
- this(username, accessToken, System.currentTimeMillis() + 60 * 60 * 1000, cookies);
- }
-
- private Session(@Nullable String username, String accessToken, long expirationTime, String cookies) {
- this.username = username;
- this.accessToken = accessToken;
- this.expirationTime = expirationTime;
- this.cookies = cookies;
- }
-
- /**
- * @return The number of milliseconds until the access token expires.
- */
- long accessTokenExpiresInMillis() {
- long currentTime = System.currentTimeMillis();
- return expirationTime - currentTime;
- }
-
- /**
- * @return The number of seconds until the access token expires.
- */
- int accessTokenExpiresInSeconds() {
- return (int) accessTokenExpiresInMillis() / 1000;
- }
-
- /**
- * @return True if the access token has expired, false otherwise.
- */
- boolean accessTokenExpired() {
- return accessTokenExpiresInMillis() <= 0;
- }
-
- void save() {
- Logger.printInfo(() -> "Saving session: " + this);
-
- SharedPreferences.Editor editor = Utils.getContext().getSharedPreferences("revanced", MODE_PRIVATE).edit();
-
- String json;
- try {
- json = new JSONObject()
- .put("accessToken", accessToken)
- .put("expirationTime", expirationTime)
- .put("cookies", cookies).toString();
- } catch (JSONException ex) {
- Logger.printException(() -> "Failed to convert session to stored credential", ex);
- return;
- }
-
- editor.putString("session_" + username, json);
- 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);
-
- SharedPreferences sharedPreferences = Utils.getContext().getSharedPreferences("revanced", MODE_PRIVATE);
- String savedJson = sharedPreferences.getString("session_" + username, null);
- if (savedJson == null) {
- Logger.printInfo(() -> "No session found in shared preferences");
- return null;
- }
-
- try {
- JSONObject json = new JSONObject(savedJson);
- String accessToken = json.getString("accessToken");
- long expirationTime = json.getLong("expirationTime");
- String cookies = json.getString("cookies");
-
- return new Session(username, accessToken, expirationTime, cookies);
- } catch (JSONException ex) {
- Logger.printException(() -> "Failed to read session from shared preferences", ex);
- return null;
- }
- }
-
- @NonNull
- @Override
- public String toString() {
- return "Session(" +
- "username=" + username +
- ", accessToken=" + accessToken +
- ", expirationTime=" + expirationTime +
- ", cookies=" + cookies +
- ')';
- }
-}
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 fb5e08113..fee58b26c 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,19 +1,15 @@
package app.revanced.extension.spotify.misc.fix;
-import android.view.LayoutInflater;
-import android.view.View;
import app.revanced.extension.shared.Logger;
@SuppressWarnings("unused")
public class SpoofClientPatch {
- private static LoginRequestListener listener;
+ private static RequestListener listener;
/**
- * Injection point.
- *
- * Launch login server.
+ * Injection point. Launch requests listener server.
*/
- public static void launchListener(int port) {
+ public synchronized static void launchListener(int port) {
if (listener != null) {
Logger.printInfo(() -> "Listener already running on port " + port);
return;
@@ -21,34 +17,9 @@ public class SpoofClientPatch {
try {
Logger.printInfo(() -> "Launching listener on port " + port);
- listener = new LoginRequestListener(port);
+ listener = new RequestListener(port);
} catch (Exception ex) {
Logger.printException(() -> "launchListener failure", ex);
}
}
-
- /**
- * Injection point.
- *
- * Launch login web view.
- */
- public static void launchLogin(LayoutInflater inflater) {
- try {
- WebApp.launchLogin(inflater.getContext());
- } catch (Exception ex) {
- 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
deleted file mode 100644
index fd11ae7a8..000000000
--- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/fix/WebApp.java
+++ /dev/null
@@ -1,297 +0,0 @@
-package app.revanced.extension.spotify.misc.fix;
-
-import android.annotation.SuppressLint;
-import android.app.Dialog;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.os.Build;
-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 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?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;
-
- 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 {
- CountDownLatch onLoggedInLatch = new CountDownLatch(1);
- CountDownLatch getSessionLatch = new CountDownLatch(1);
-
- // Can't use Utils.getContext() here, because autofill won't work.
- // See https://stackoverflow.com/a/79182053/11213244.
- launchWebView(context, ACCOUNTS_SPOTIFY_COM_LOGIN_URL, new WebViewCallback() {
- @Override
- void onInitialized(WebView webView) {
- super.onInitialized(webView);
-
- dialog.setContentView(webView);
- dialog.show();
- }
-
- @Override
- void onLoggedIn(String cookies) {
- onLoggedInLatch.countDown();
- }
-
- @Override
- void onReceivedSession(Session session) {
- super.onReceivedSession(session);
-
- getSessionLatch.countDown();
- dialog.dismiss();
-
- try {
- nativeLoginHandler.login();
- } catch (Exception ex) {
- Logger.printException(() -> "nativeLoginHandler failure", ex);
- }
- }
- });
-
- 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 renewSessionBlocking(String cookies) {
- Logger.printInfo(() -> "Renewing session with cookies: " + cookies);
-
- CountDownLatch getSessionLatch = new CountDownLatch(1);
-
- launchWebView(Utils.getContext(), OPEN_SPOTIFY_COM_PREFERENCES_URL, new WebViewCallback() {
- @Override
- public void onInitialized(WebView webView) {
- setCookies(cookies);
- super.onInitialized(webView);
- }
-
- 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();
- }
-
- if (!isAcquired) {
- Logger.printException(() -> "Failed to retrieve session within " + GET_SESSION_TIMEOUT_SECONDS + " seconds");
- currentSession = FAILED_TO_RENEW_SESSION;
- destructWebView();
- }
- }
-
- /**
- * All methods are called on the main thread.
- */
- abstract static class WebViewCallback {
- void onInitialized(WebView webView) {
- currentWebView = webView;
- currentSession = null; // Reset current session.
- }
-
- void onLoggedIn(String cookies) {
- }
-
- void onReceivedSession(Session session) {
- Logger.printInfo(() -> "Received session: " + session);
- currentSession = session;
-
- destructWebView();
- }
- }
-
- @SuppressLint("SetJavaScriptEnabled")
- private static void launchWebView(
- Context context,
- String initialUrl,
- WebViewCallback webViewCallback
- ) {
- Utils.runOnMainThreadNowOrLater(() -> {
- 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() {
- @Override
- public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
- if (OPEN_SPOTIFY_COM.equals(request.getUrl().getHost())) {
- Utils.runOnMainThread(() -> webViewCallback.onLoggedIn(getCurrentCookies()));
- }
-
- return super.shouldInterceptRequest(view, request);
- }
-
- @Override
- public void onPageStarted(WebView view, String url, Bitmap favicon) {
- Logger.printInfo(() -> "Page started loading: " + url);
-
- if (!url.startsWith(OPEN_SPOTIFY_COM_URL)) {
- return;
- }
-
- Logger.printInfo(() -> "Evaluating script to get session on url: " + url);
- String getSessionScript = "Object.defineProperty(Object.prototype, \"_username\", {" +
- " configurable: true," +
- " set(username) {" +
- " accessToken = this._builder?.accessToken;" +
- " if (accessToken) {" +
- " " + JAVASCRIPT_INTERFACE_NAME + ".getSession(username, accessToken);" +
- " delete Object.prototype._username;" +
- " }" +
- " " +
- " Object.defineProperty(this, \"_username\", {" +
- " configurable: true," +
- " enumerable: true," +
- " writable: true," +
- " value: username" +
- " })" +
- " " +
- " }" +
- "});" +
- "if (new URLSearchParams(window.location.search).get('_authfailed') != null) {" +
- " " + JAVASCRIPT_INTERFACE_NAME + ".getSession(null, null);" +
- "}";
-
- view.evaluateJavascript(getSessionScript, null);
- }
- });
-
- 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(session));
- }
- }, JAVASCRIPT_INTERFACE_NAME);
-
- CookieManager.getInstance().removeAllCookies((anyRemoved) -> {
- Logger.printInfo(() -> "Loading URL: " + initialUrl);
- webView.loadUrl(initialUrl);
-
- Logger.printInfo(() -> "WebView initialized with user agent: " + USER_AGENT);
- 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 {
- return new UserAgent(userAgentString)
- .withCommentReplaced("Android", "Windows NT 10.0; Win64; x64")
- .withoutProduct("Mobile")
- .toString();
- } 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, 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);
- }
-
- private static void setCookies(@NonNull String cookies) {
- CookieManager cookieManager = CookieManager.getInstance();
-
- String[] cookiesList = cookies.split(";");
- for (String cookie : cookiesList) {
- cookieManager.setCookie(OPEN_SPOTIFY_COM_URL, cookie);
- }
- }
-}
diff --git a/extensions/spotify/src/main/proto/app/revanced/extension/spotify/misc/fix/clienttoken_http.proto b/extensions/spotify/src/main/proto/app/revanced/extension/spotify/misc/fix/clienttoken_http.proto
new file mode 100644
index 000000000..8e7f242b7
--- /dev/null
+++ b/extensions/spotify/src/main/proto/app/revanced/extension/spotify/misc/fix/clienttoken_http.proto
@@ -0,0 +1,73 @@
+syntax = "proto3";
+
+package spotify.clienttoken.data.v0;
+
+option optimize_for = LITE_RUNTIME;
+option java_package = "app.revanced.extension.spotify.misc.fix.clienttoken.data.v0";
+
+message ClientTokenRequest {
+ ClientTokenRequestType request_type = 1;
+
+ oneof request {
+ ClientDataRequest client_data = 2;
+ }
+}
+
+enum ClientTokenRequestType {
+ REQUEST_UNKNOWN = 0;
+ REQUEST_CLIENT_DATA_REQUEST = 1;
+ REQUEST_CHALLENGE_ANSWERS_REQUEST = 2;
+}
+
+message ClientDataRequest {
+ string client_version = 1;
+ string client_id = 2;
+
+ oneof data {
+ ConnectivitySdkData connectivity_sdk_data = 3;
+ }
+}
+
+message ConnectivitySdkData {
+ PlatformSpecificData platform_specific_data = 1;
+ string device_id = 2;
+}
+
+message PlatformSpecificData {
+ oneof data {
+ NativeIOSData ios = 2;
+ }
+}
+
+message NativeIOSData {
+ int32 user_interface_idiom = 1;
+ bool target_iphone_simulator = 2;
+ string hw_machine = 3;
+ string system_version = 4;
+ string simulator_model_identifier = 5;
+}
+
+message ClientTokenResponse {
+ ClientTokenResponseType response_type = 1;
+
+ oneof response {
+ GrantedTokenResponse granted_token = 2;
+ }
+}
+
+enum ClientTokenResponseType {
+ RESPONSE_UNKNOWN = 0;
+ RESPONSE_GRANTED_TOKEN_RESPONSE = 1;
+ RESPONSE_CHALLENGES_RESPONSE = 2;
+}
+
+message GrantedTokenResponse {
+ string token = 1;
+ int32 expires_after_seconds = 2;
+ int32 refresh_after_seconds = 3;
+ repeated TokenDomain domains = 4;
+}
+
+message TokenDomain {
+ string domain = 1;
+}
diff --git a/extensions/spotify/src/main/proto/login5.proto b/extensions/spotify/src/main/proto/login5.proto
deleted file mode 100644
index 7d8e38d24..000000000
--- a/extensions/spotify/src/main/proto/login5.proto
+++ /dev/null
@@ -1,43 +0,0 @@
-syntax = "proto3";
-
-package spotify.login5.v4;
-
-option optimize_for = LITE_RUNTIME;
-option java_package = "app.revanced.extension.spotify.login5.v4.proto";
-
-message StoredCredential {
- string username = 1;
- bytes data = 2;
-}
-
-message LoginRequest {
- oneof login_method {
- StoredCredential stored_credential = 100;
- }
-}
-
-message LoginOk {
- string username = 1;
- string access_token = 2;
- bytes stored_credential = 3;
- int32 access_token_expires_in = 4;
-}
-
-message LoginResponse {
- oneof response {
- LoginOk ok = 1;
- LoginError error = 2;
- }
-}
-
-enum LoginError {
- UNKNOWN_ERROR = 0;
- INVALID_CREDENTIALS = 1;
- BAD_REQUEST = 2;
- UNSUPPORTED_LOGIN_PROTOCOL = 3;
- TIMEOUT = 4;
- UNKNOWN_IDENTIFIER = 5;
- TOO_MANY_ATTEMPTS = 6;
- INVALID_PHONENUMBER = 7;
- TRY_AGAIN_LATER = 8;
-}
diff --git a/extensions/spotify/utils/build.gradle.kts b/extensions/spotify/utils/build.gradle.kts
deleted file mode 100644
index 3cdce1cc0..000000000
--- a/extensions/spotify/utils/build.gradle.kts
+++ /dev/null
@@ -1,19 +0,0 @@
-plugins {
- java
- antlr
-}
-
-dependencies {
- antlr(libs.antlr4)
-}
-
-java {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
-}
-
-tasks {
- generateGrammarSource {
- arguments = listOf("-visitor")
- }
-}
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
deleted file mode 100644
index b46799359..000000000
--- a/extensions/spotify/utils/src/main/antlr/app/revanced/extension/spotify/UserAgent.g4
+++ /dev/null
@@ -1,35 +0,0 @@
-grammar UserAgent;
-
-@header { package app.revanced.extension.spotify; }
-
-userAgent
- : product (WS product)* EOF
- ;
-
-product
- : name ('/' version)? (WS comment)?
- ;
-
-name
- : STRING
- ;
-
-version
- : STRING ('.' STRING)*
- ;
-
-comment
- : COMMENT
- ;
-
-COMMENT
- : '(' ~ ')'* ')'
- ;
-
-STRING
- : [a-zA-Z0-9]+
- ;
-
-WS
- : [ \r\n]+
- ;
diff --git a/extensions/spotify/utils/src/main/java/app/revanced/extension/spotify/UserAgent.java b/extensions/spotify/utils/src/main/java/app/revanced/extension/spotify/UserAgent.java
deleted file mode 100644
index e25912f86..000000000
--- a/extensions/spotify/utils/src/main/java/app/revanced/extension/spotify/UserAgent.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package app.revanced.extension.spotify;
-
-import org.antlr.v4.runtime.CharStream;
-import org.antlr.v4.runtime.CharStreams;
-import org.antlr.v4.runtime.CommonTokenStream;
-import org.antlr.v4.runtime.TokenStreamRewriter;
-import org.antlr.v4.runtime.tree.ParseTreeWalker;
-
-public class UserAgent {
- private final UserAgentParser.UserAgentContext tree;
- private final TokenStreamRewriter rewriter;
- private final ParseTreeWalker walker;
-
- public UserAgent(String userAgentString) {
- CharStream input = CharStreams.fromString(userAgentString);
- UserAgentLexer lexer = new UserAgentLexer(input);
- CommonTokenStream tokens = new CommonTokenStream(lexer);
-
- tree = new UserAgentParser(tokens).userAgent();
- walker = new ParseTreeWalker();
- rewriter = new TokenStreamRewriter(tokens);
- }
-
- public UserAgent withoutProduct(String name) {
- walker.walk(new UserAgentBaseListener() {
- @Override
- public void exitProduct(UserAgentParser.ProductContext ctx) {
- if (!ctx.name().getText().contains(name)) return;
-
- int startIndex = ctx.getStart().getTokenIndex();
- if (startIndex != 0) startIndex -= 1; // Also remove the preceding whitespace.
-
- int stopIndex = ctx.getStop().getTokenIndex();
-
-
- rewriter.delete(startIndex, stopIndex);
- }
- }, tree);
-
- return new UserAgent(rewriter.getText().trim());
- }
-
- public UserAgent withCommentReplaced(String containing, String replacement) {
- walker.walk(new UserAgentBaseListener() {
- @Override
- public void exitComment(UserAgentParser.CommentContext ctx) {
- if (ctx.getText().contains(containing)) {
- rewriter.replace(ctx.getStart(), ctx.getStop(), "(" + replacement + ")");
- }
- }
- }, tree);
-
- return new UserAgent(rewriter.getText());
- }
-
- @Override
- public String toString() {
- return rewriter.getText();
- }
-}
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 3cc57181b..abe67ad15 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
@@ -7,37 +7,12 @@ 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(
- "Failed to get the application signatures"
- )
-}
-
internal val loadOrbitLibraryFingerprint = fingerprint {
strings("/liborbit-jni-spotify.so")
}
-internal val startupPageLayoutInflateFingerprint = fingerprint {
- accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
- returns("Landroid/view/View;")
- parameters("Landroid/view/LayoutInflater;", "Landroid/view/ViewGroup;", "Landroid/os/Bundle;")
- 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 extensionFixConstantsFingerprint = fingerprint {
+ custom { _, classDef -> classDef.type == "Lapp/revanced/extension/spotify/misc/fix/Constants;" }
}
internal val runIntegrityVerificationFingerprint = fingerprint {
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 c35dc1346..49bb19704 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
@@ -1,19 +1,13 @@
package app.revanced.patches.spotify.misc.fix
-import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
-import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
-import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.intOption
+import app.revanced.patcher.patch.stringOption
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.*
-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
+import app.revanced.util.returnEarly
internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/misc/fix/SpoofClientPatch;"
@@ -25,16 +19,40 @@ val spoofClientPatch = bytecodePatch(
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. " +
- "Port must be between 0 and 65535.",
- required = true,
+ title = "Request listener port",
+ description = "The port to use for the listener that intercepts and handles spoofed requests. " +
+ "Port must be between 0 and 65535. " +
+ "Do not change this option, if you do not know what you are doing.",
validator = {
it!!
!(it < 0 || it > 65535)
}
)
+ val clientVersion by stringOption(
+ key = "clientVersion",
+ default = "iphone-9.0.58.558.g200011c",
+ title = "Client version",
+ description = "The client version used for spoofing the client token. " +
+ "Do not change this option, if you do not know what you are doing."
+ )
+
+ val hardwareMachine by stringOption(
+ key = "hardwareMachine",
+ default = "iPhone16,1",
+ title = "Hardware machine",
+ description = "The hardware machine used for spoofing the client token. " +
+ "Do not change this option, if you do not know what you are doing."
+ )
+
+ val systemVersion by stringOption(
+ key = "systemVersion",
+ default = "17.7.2",
+ title = "System version",
+ description = "The system version used for spoofing the client token. " +
+ "Do not change this option, if you do not know what you are doing."
+ )
+
dependsOn(
sharedExtensionPatch,
hexPatch(ignoreMissingTargetFiles = true, block = fun HexPatchBuilder.() {
@@ -44,10 +62,8 @@ val spoofClientPatch = bytecodePatch(
"x86",
"x86_64"
).forEach { architecture ->
- "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:$requestListenerPort/v4/login" inFile
+ "https://clienttoken.spotify.com/v1/clienttoken" to
+ "http://127.0.0.1:$requestListenerPort/v1/clienttoken" inFile
"lib/$architecture/liborbit-jni-spotify.so"
}
})
@@ -56,51 +72,6 @@ val spoofClientPatch = bytecodePatch(
compatibleWith("com.spotify.music")
execute {
- // region Spoof package info.
-
- getPackageInfoFingerprint.method.apply {
- // region Spoof signature.
-
- val failedToGetSignaturesStringIndex =
- getPackageInfoFingerprint.stringMatches!!.first().index
-
- val concatSignaturesIndex = indexOfFirstInstructionReversedOrThrow(
- failedToGetSignaturesStringIndex,
- Opcode.MOVE_RESULT_OBJECT,
- )
-
- val signatureRegister = getInstruction(concatSignaturesIndex).registerA
- val expectedSignature = "d6a6dced4a85f24204bf9505ccc1fce114cadb32"
-
- replaceInstruction(concatSignaturesIndex, "const-string v$signatureRegister, \"$expectedSignature\"")
-
- // endregion
-
- // region Spoof installer name.
-
- val expectedInstallerName = "com.android.vending"
-
- findInstructionIndicesReversedOrThrow {
- val reference = getReference()
- reference?.name == "getInstallerPackageName" || reference?.name == "getInstallingPackageName"
- }.forEach { index ->
- val returnObjectIndex = index + 1
-
- val installerPackageNameRegister = getInstruction(
- returnObjectIndex
- ).registerA
-
- addInstruction(
- returnObjectIndex + 1,
- "const-string v$installerPackageNameRegister, \"$expectedInstallerName\""
- )
- }
-
- // endregion
- }
-
- // endregion
-
// region Spoof client.
loadOrbitLibraryFingerprint.method.addInstructions(
@@ -111,72 +82,12 @@ val spoofClientPatch = bytecodePatch(
"""
)
- startupPageLayoutInflateFingerprint.method.apply {
- val openLoginWebViewDescriptor =
- "$EXTENSION_CLASS_DESCRIPTOR->launchLogin(Landroid/view/LayoutInflater;)V"
-
- 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-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"
- """
- )
+ mapOf(
+ "getClientVersion" to clientVersion!!,
+ "getSystemVersion" to systemVersion!!,
+ "getHardwareMachine" to hardwareMachine!!
+ ).forEach { (methodName, value) ->
+ extensionFixConstantsFingerprint.classDef.methods.single { it.name == methodName }.returnEarly(value)
}
// endregion