fix(Spotify - Spoof client): Handle remaining edge cases to obtain a session (#5285)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
This commit is contained in:
parent
d3ec219a29
commit
b2e601f0f0
13 changed files with 262 additions and 169 deletions
|
|
@ -14,11 +14,19 @@ import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Objects;
|
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;
|
import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
|
||||||
|
|
||||||
class LoginRequestListener extends NanoHTTPD {
|
class LoginRequestListener extends NanoHTTPD {
|
||||||
LoginRequestListener(int port) {
|
LoginRequestListener(int port) {
|
||||||
super(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
|
@NonNull
|
||||||
|
|
@ -31,8 +39,8 @@ class LoginRequestListener extends NanoHTTPD {
|
||||||
LoginRequest loginRequest;
|
LoginRequest loginRequest;
|
||||||
try {
|
try {
|
||||||
loginRequest = LoginRequest.parseFrom(requestBodyInputStream);
|
loginRequest = LoginRequest.parseFrom(requestBodyInputStream);
|
||||||
} catch (IOException e) {
|
} catch (IOException ex) {
|
||||||
Logger.printException(() -> "Failed to parse LoginRequest", e);
|
Logger.printException(() -> "Failed to parse LoginRequest", ex);
|
||||||
return newResponse(INTERNAL_ERROR);
|
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.
|
// 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.
|
// Therefore, synchronize to ensure that only one webview handles the request at a time.
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
|
try {
|
||||||
loginResponse = getLoginResponse(loginRequest);
|
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(Response.Status.OK, loginResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newResponse(INTERNAL_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static LoginResponse getLoginResponse(@NonNull LoginRequest loginRequest) {
|
private static LoginResponse getLoginResponse(@NonNull LoginRequest loginRequest) {
|
||||||
Session session;
|
Session session;
|
||||||
|
|
||||||
boolean isInitialLogin = !loginRequest.hasStoredCredential();
|
if (!loginRequest.hasStoredCredential()) {
|
||||||
if (isInitialLogin) {
|
|
||||||
Logger.printInfo(() -> "Received request for initial login");
|
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 {
|
} else {
|
||||||
Logger.printInfo(() -> "Received request to restore saved session");
|
Logger.printInfo(() -> "Received request to restore saved session");
|
||||||
session = Session.read(loginRequest.getStoredCredential().getUsername());
|
session = Session.read(loginRequest.getStoredCredential().getUsername());
|
||||||
}
|
}
|
||||||
|
|
||||||
return toLoginResponse(session, isInitialLogin);
|
return toLoginResponse(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LoginResponse toLoginResponse(@Nullable Session session) {
|
||||||
private static LoginResponse toLoginResponse(Session session, boolean isInitialLogin) {
|
|
||||||
LoginResponse.Builder builder = LoginResponse.newBuilder();
|
LoginResponse.Builder builder = LoginResponse.newBuilder();
|
||||||
|
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
if (isInitialLogin) {
|
Logger.printException(() -> "Session is null. An initial login may still be in progress, returning try again later error");
|
||||||
Logger.printInfo(() -> "Session is null, returning try again later error for initial login");
|
|
||||||
builder.setError(LoginError.TRY_AGAIN_LATER);
|
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);
|
|
||||||
} else if (session.accessTokenExpired()) {
|
} else if (session.accessTokenExpired()) {
|
||||||
Logger.printInfo(() -> "Access token has expired, renewing session");
|
Logger.printInfo(() -> "Access token expired, renewing session");
|
||||||
WebApp.renewSession(session.cookies);
|
WebApp.renewSessionBlocking(session.cookies);
|
||||||
return toLoginResponse(WebApp.currentSession, isInitialLogin);
|
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 {
|
} else {
|
||||||
session.save();
|
session.save();
|
||||||
Logger.printInfo(() -> "Returning session for username: " + session.username);
|
Logger.printInfo(() -> "Returning session for username: " + session.username);
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,11 @@ class Session {
|
||||||
*/
|
*/
|
||||||
final String cookies;
|
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 username Username of the account. Empty if this session does not have an authenticated user.
|
||||||
* @param accessToken Access token for this session.
|
* @param accessToken Access token for this session.
|
||||||
|
|
@ -87,6 +92,13 @@ class Session {
|
||||||
editor.apply();
|
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
|
@Nullable
|
||||||
static Session read(String username) {
|
static Session read(String username) {
|
||||||
Logger.printInfo(() -> "Reading saved session for username: " + username);
|
Logger.printInfo(() -> "Reading saved session for username: " + username);
|
||||||
|
|
|
||||||
|
|
@ -10,20 +10,19 @@ public class SpoofClientPatch {
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
* <br>
|
* <br>
|
||||||
* Start login server.
|
* Launch login server.
|
||||||
*/
|
*/
|
||||||
public static void listen(int port) {
|
public static void launchListener(int port) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
Logger.printInfo(() -> "Listener already running on port " + port);
|
Logger.printInfo(() -> "Listener already running on port " + port);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Logger.printInfo(() -> "Launching listener on port " + port);
|
||||||
listener = new LoginRequestListener(port);
|
listener = new LoginRequestListener(port);
|
||||||
listener.start();
|
|
||||||
Logger.printInfo(() -> "Listener running on port " + port);
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "listen failure", ex);
|
Logger.printException(() -> "launchListener failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,11 +31,11 @@ public class SpoofClientPatch {
|
||||||
* <br>
|
* <br>
|
||||||
* Launch login web view.
|
* Launch login web view.
|
||||||
*/
|
*/
|
||||||
public static void login(LayoutInflater inflater) {
|
public static void launchLogin(LayoutInflater inflater) {
|
||||||
try {
|
try {
|
||||||
WebApp.login(inflater.getContext());
|
WebApp.launchLogin(inflater.getContext());
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "login failure", ex);
|
Logger.printException(() -> "launchLogin failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,135 +5,125 @@ import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.view.*;
|
import android.view.Window;
|
||||||
|
import android.view.WindowInsets;
|
||||||
import android.webkit.*;
|
import android.webkit.*;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
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.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import static app.revanced.extension.spotify.misc.fix.Session.FAILED_TO_RENEW_SESSION;
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.spotify.UserAgent;
|
|
||||||
|
|
||||||
class WebApp {
|
class WebApp {
|
||||||
private static final String OPEN_SPOTIFY_COM = "open.spotify.com";
|
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_URL = "https://" + OPEN_SPOTIFY_COM;
|
||||||
private static final String OPEN_SPOTIFY_COM_PREFERENCES_URL = OPEN_SPOTIFY_COM_URL + "/preferences";
|
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=" +
|
private static final String ACCOUNTS_SPOTIFY_COM_LOGIN_URL = "https://accounts.spotify.com/login?allow_password=1"
|
||||||
"https%3A%2F%2Fopen.spotify.com%2Fpreferences";
|
+ "&continue=https%3A%2F%2Fopen.spotify.com%2Fpreferences";
|
||||||
|
|
||||||
private static final int GET_SESSION_TIMEOUT_SECONDS = 10;
|
private static final int GET_SESSION_TIMEOUT_SECONDS = 10;
|
||||||
private static final String JAVASCRIPT_INTERFACE_NAME = "androidInterface";
|
private static final String JAVASCRIPT_INTERFACE_NAME = "androidInterface";
|
||||||
private static final String USER_AGENT = getWebUserAgent();
|
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.
|
* Current webview in use. Any use of the object must be done on the main thread.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private static volatile WebView currentWebView;
|
private static volatile WebView currentWebView;
|
||||||
|
|
||||||
/**
|
static void launchLogin(Context context) {
|
||||||
* A session obtained from the webview after logging in or renewing the session.
|
final Dialog dialog = newDialog(context);
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
static volatile Session currentSession;
|
|
||||||
|
|
||||||
static void login(Context context) {
|
Utils.runOnBackgroundThread(() -> {
|
||||||
Logger.printInfo(() -> "Starting login");
|
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.
|
// A session must be obtained from a login. Repeat until a session is acquired.
|
||||||
Window window = dialog.getWindow();
|
boolean isAcquired = false;
|
||||||
//noinspection StatementWithEmptyBody
|
do {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
CountDownLatch onLoggedInLatch = new CountDownLatch(1);
|
||||||
window.getDecorView().setOnApplyWindowInsetsListener((v, insets) -> {
|
CountDownLatch getSessionLatch = new CountDownLatch(1);
|
||||||
v.setPadding(0, 0, 0, insets.getInsets(WindowInsets.Type.ime()).bottom);
|
|
||||||
|
|
||||||
return WindowInsets.CONSUMED;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// TODO: Implement for lower Android versions.
|
|
||||||
}
|
|
||||||
|
|
||||||
newWebView(
|
|
||||||
// Can't use Utils.getContext() here, because autofill won't work.
|
// Can't use Utils.getContext() here, because autofill won't work.
|
||||||
// See https://stackoverflow.com/a/79182053/11213244.
|
// See https://stackoverflow.com/a/79182053/11213244.
|
||||||
context,
|
launchWebView(context, ACCOUNTS_SPOTIFY_COM_LOGIN_URL, new WebViewCallback() {
|
||||||
new WebViewCallback() {
|
|
||||||
@Override
|
@Override
|
||||||
void onInitialized(WebView webView) {
|
void onInitialized(WebView webView) {
|
||||||
// Ensure that cookies are cleared before loading the login page.
|
super.onInitialized(webView);
|
||||||
CookieManager.getInstance().removeAllCookies((anyRemoved) -> {
|
|
||||||
Logger.printInfo(() -> "Loading URL: " + ACCOUNTS_SPOTIFY_COM_LOGIN_URL);
|
|
||||||
webView.loadUrl(ACCOUNTS_SPOTIFY_COM_LOGIN_URL);
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.setCancelable(false);
|
|
||||||
dialog.setContentView(webView);
|
dialog.setContentView(webView);
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void onLoggedIn(String cookies) {
|
void onLoggedIn(String cookies) {
|
||||||
dialog.dismiss();
|
onLoggedInLatch.countDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void onReceivedSession(WebView webView, Session session) {
|
void onReceivedSession(Session session) {
|
||||||
Logger.printInfo(() -> "Received session from login: " + session);
|
super.onReceivedSession(session);
|
||||||
currentSession = session;
|
|
||||||
currentWebView = null;
|
getSessionLatch.countDown();
|
||||||
webView.stopLoading();
|
dialog.dismiss();
|
||||||
webView.destroy();
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
Logger.printInfo(() -> "Renewing session with cookies: " + cookies);
|
||||||
|
|
||||||
CountDownLatch getSessionLatch = new CountDownLatch(1);
|
CountDownLatch getSessionLatch = new CountDownLatch(1);
|
||||||
|
|
||||||
newWebView(
|
launchWebView(Utils.getContext(), OPEN_SPOTIFY_COM_PREFERENCES_URL, new WebViewCallback() {
|
||||||
Utils.getContext(),
|
|
||||||
new WebViewCallback() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onInitialized(WebView webView) {
|
public void onInitialized(WebView webView) {
|
||||||
Logger.printInfo(() -> "Loading URL: " + OPEN_SPOTIFY_COM_PREFERENCES_URL +
|
|
||||||
" with cookies: " + cookies);
|
|
||||||
setCookies(cookies);
|
setCookies(cookies);
|
||||||
webView.loadUrl(OPEN_SPOTIFY_COM_PREFERENCES_URL);
|
super.onInitialized(webView);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void onReceivedSession(Session session) {
|
||||||
public void onReceivedSession(WebView webView, Session session) {
|
super.onReceivedSession(session);
|
||||||
Logger.printInfo(() -> "Received session: " + session);
|
|
||||||
currentSession = session;
|
|
||||||
getSessionLatch.countDown();
|
getSessionLatch.countDown();
|
||||||
currentWebView = null;
|
|
||||||
webView.stopLoading();
|
|
||||||
webView.destroy();
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
|
boolean isAcquired = false;
|
||||||
try {
|
try {
|
||||||
final boolean isAcquired = getSessionLatch.await(GET_SESSION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
isAcquired = getSessionLatch.await(GET_SESSION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
if (!isAcquired) {
|
} catch (InterruptedException ex) {
|
||||||
Logger.printException(() -> "Failed to retrieve session within " + GET_SESSION_TIMEOUT_SECONDS + " seconds");
|
Logger.printException(() -> "Session renewal interrupted", ex);
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Logger.printException(() -> "Interrupted while waiting to retrieve session", e);
|
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup.
|
if (!isAcquired) {
|
||||||
currentWebView = null;
|
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 {
|
abstract static class WebViewCallback {
|
||||||
void onInitialized(WebView webView) {
|
void onInitialized(WebView webView) {
|
||||||
|
currentWebView = webView;
|
||||||
|
currentSession = null; // Reset current session.
|
||||||
}
|
}
|
||||||
|
|
||||||
void onLoggedIn(String cookies) {
|
void onLoggedIn(String cookies) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void onReceivedSession(WebView webView, Session session) {
|
void onReceivedSession(Session session) {
|
||||||
|
Logger.printInfo(() -> "Received session: " + session);
|
||||||
|
currentSession = session;
|
||||||
|
|
||||||
|
destructWebView();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
private static void newWebView(
|
private static void launchWebView(
|
||||||
Context context,
|
Context context,
|
||||||
|
String initialUrl,
|
||||||
WebViewCallback webViewCallback
|
WebViewCallback webViewCallback
|
||||||
) {
|
) {
|
||||||
Utils.runOnMainThreadNowOrLater(() -> {
|
Utils.runOnMainThreadNowOrLater(() -> {
|
||||||
WebView webView = currentWebView;
|
WebView webView = new WebView(context);
|
||||||
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);
|
|
||||||
WebSettings settings = webView.getSettings();
|
WebSettings settings = webView.getSettings();
|
||||||
settings.setDomStorageEnabled(true);
|
settings.setDomStorageEnabled(true);
|
||||||
settings.setJavaScriptEnabled(true);
|
settings.setJavaScriptEnabled(true);
|
||||||
settings.setUserAgentString(USER_AGENT);
|
settings.setUserAgentString(USER_AGENT);
|
||||||
|
|
||||||
// WebViewClient is always called off the main thread,
|
// WebViewClient is always called off the main thread,
|
||||||
// but callback interface methods are called on the main thread.
|
// but callback interface methods are called on the main thread.
|
||||||
webView.setWebViewClient(new WebViewClient() {
|
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);
|
view.evaluateJavascript(getSessionScript, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final WebView callbackWebView = webView;
|
|
||||||
webView.addJavascriptInterface(new Object() {
|
webView.addJavascriptInterface(new Object() {
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
public void getSession(String username, String accessToken) {
|
public void getSession(String username, String accessToken) {
|
||||||
Session session = new Session(username, accessToken, getCurrentCookies());
|
Session session = new Session(username, accessToken, getCurrentCookies());
|
||||||
Utils.runOnMainThread(() -> webViewCallback.onReceivedSession(callbackWebView, session));
|
Utils.runOnMainThread(() -> webViewCallback.onReceivedSession(session));
|
||||||
}
|
}
|
||||||
}, JAVASCRIPT_INTERFACE_NAME);
|
}, JAVASCRIPT_INTERFACE_NAME);
|
||||||
|
|
||||||
currentWebView = webView;
|
|
||||||
|
|
||||||
CookieManager.getInstance().removeAllCookies((anyRemoved) -> {
|
CookieManager.getInstance().removeAllCookies((anyRemoved) -> {
|
||||||
|
Logger.printInfo(() -> "Loading URL: " + initialUrl);
|
||||||
|
webView.loadUrl(initialUrl);
|
||||||
|
|
||||||
Logger.printInfo(() -> "WebView initialized with user agent: " + USER_AGENT);
|
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() {
|
private static String getWebUserAgent() {
|
||||||
String userAgentString = WebSettings.getDefaultUserAgent(Utils.getContext());
|
String userAgentString = WebSettings.getDefaultUserAgent(Utils.getContext());
|
||||||
try {
|
try {
|
||||||
|
|
@ -241,16 +240,36 @@ class WebApp {
|
||||||
.withCommentReplaced("Android", "Windows NT 10.0; Win64; x64")
|
.withCommentReplaced("Android", "Windows NT 10.0; Win64; x64")
|
||||||
.withoutProduct("Mobile")
|
.withoutProduct("Mobile")
|
||||||
.toString();
|
.toString();
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException ex) {
|
||||||
userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
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";
|
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edge/137.0.0.0";
|
||||||
String fallback = userAgentString;
|
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;
|
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() {
|
private static String getCurrentCookies() {
|
||||||
CookieManager cookieManager = CookieManager.getInstance();
|
CookieManager cookieManager = CookieManager.getInstance();
|
||||||
return cookieManager.getCookie(OPEN_SPOTIFY_COM_URL);
|
return cookieManager.getCookie(OPEN_SPOTIFY_COM_URL);
|
||||||
|
|
|
||||||
|
|
@ -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 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/Function2;Lkotlin/jvm/functions/Function2;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 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/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/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/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;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 (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;
|
public static final fun sharedExtensionPatch ([Lapp/revanced/patches/shared/misc/extension/ExtensionHook;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,8 @@ fun sharedExtensionPatch(
|
||||||
|
|
||||||
class ExtensionHook internal constructor(
|
class ExtensionHook internal constructor(
|
||||||
internal val fingerprint: Fingerprint,
|
internal val fingerprint: Fingerprint,
|
||||||
private val insertIndexResolver: ((Method) -> Int),
|
private val insertIndexResolver: BytecodePatchContext.(Method) -> Int,
|
||||||
private val contextRegisterResolver: (Method) -> String,
|
private val contextRegisterResolver: BytecodePatchContext.(Method) -> String,
|
||||||
) {
|
) {
|
||||||
context(BytecodePatchContext)
|
context(BytecodePatchContext)
|
||||||
operator fun invoke(extensionClassDescriptor: String) {
|
operator fun invoke(extensionClassDescriptor: String) {
|
||||||
|
|
@ -104,13 +104,13 @@ class ExtensionHook internal constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun extensionHook(
|
fun extensionHook(
|
||||||
insertIndexResolver: ((Method) -> Int) = { 0 },
|
insertIndexResolver: BytecodePatchContext.(Method) -> Int = { 0 },
|
||||||
contextRegisterResolver: (Method) -> String = { "p0" },
|
contextRegisterResolver: BytecodePatchContext.(Method) -> String = { "p0" },
|
||||||
fingerprint: Fingerprint,
|
fingerprint: Fingerprint,
|
||||||
) = ExtensionHook(fingerprint, insertIndexResolver, contextRegisterResolver)
|
) = ExtensionHook(fingerprint, insertIndexResolver, contextRegisterResolver)
|
||||||
|
|
||||||
fun extensionHook(
|
fun extensionHook(
|
||||||
insertIndexResolver: ((Method) -> Int) = { 0 },
|
insertIndexResolver: BytecodePatchContext.(Method) -> Int = { 0 },
|
||||||
contextRegisterResolver: (Method) -> String = { "p0" },
|
contextRegisterResolver: BytecodePatchContext.(Method) -> String = { "p0" },
|
||||||
fingerprintBuilderBlock: FingerprintBuilder.() -> Unit,
|
fingerprintBuilderBlock: FingerprintBuilder.() -> Unit,
|
||||||
) = extensionHook(insertIndexResolver, contextRegisterResolver, fingerprint(block = fingerprintBuilderBlock))
|
) = extensionHook(insertIndexResolver, contextRegisterResolver, fingerprint(block = fingerprintBuilderBlock))
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,8 @@ package app.revanced.patches.spotify.misc.extension
|
||||||
|
|
||||||
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
||||||
|
|
||||||
val sharedExtensionPatch = sharedExtensionPatch("spotify", mainActivityOnCreateHook)
|
val sharedExtensionPatch = sharedExtensionPatch(
|
||||||
|
"spotify",
|
||||||
|
mainActivityOnCreateHook,
|
||||||
|
loadOrbitLibraryHook
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package app.revanced.patches.spotify.misc.extension
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint
|
||||||
|
|
||||||
|
internal val loadOrbitLibraryFingerprint = fingerprint {
|
||||||
|
strings("OrbitLibraryLoader", "cst")
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,26 @@
|
||||||
package app.revanced.patches.spotify.misc.extension
|
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.shared.misc.extension.extensionHook
|
||||||
import app.revanced.patches.spotify.shared.mainActivityOnCreateFingerprint
|
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 mainActivityOnCreateHook = extensionHook(fingerprint = mainActivityOnCreateFingerprint)
|
||||||
|
|
||||||
|
internal val loadOrbitLibraryHook = extensionHook(
|
||||||
|
insertIndexResolver = {
|
||||||
|
loadOrbitLibraryFingerprint.stringMatches!!.last().index
|
||||||
|
},
|
||||||
|
contextRegisterResolver = { method ->
|
||||||
|
val contextReferenceIndex = method.indexOfFirstInstruction {
|
||||||
|
getReference<FieldReference>()?.type == "Landroid/content/Context;"
|
||||||
|
}
|
||||||
|
val contextRegister = method.getInstruction<TwoRegisterInstruction>(contextReferenceIndex).registerA
|
||||||
|
|
||||||
|
"v$contextRegister"
|
||||||
|
},
|
||||||
|
fingerprint = loadOrbitLibraryFingerprint,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
package app.revanced.patches.spotify.misc.fix
|
package app.revanced.patches.spotify.misc.fix
|
||||||
|
|
||||||
import app.revanced.patcher.fingerprint
|
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.AccessFlags
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
|
|
||||||
internal val getPackageInfoFingerprint = fingerprint {
|
internal val getPackageInfoFingerprint = fingerprint {
|
||||||
strings(
|
strings(
|
||||||
|
|
@ -9,7 +13,7 @@ internal val getPackageInfoFingerprint = fingerprint {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val startLiborbitFingerprint = fingerprint {
|
internal val loadOrbitLibraryFingerprint = fingerprint {
|
||||||
strings("/liborbit-jni-spotify.so")
|
strings("/liborbit-jni-spotify.so")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,10 +24,22 @@ internal val startupPageLayoutInflateFingerprint = fingerprint {
|
||||||
strings("blueprintContainer", "gradient", "valuePropositionTextView")
|
strings("blueprintContainer", "gradient", "valuePropositionTextView")
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val standardIntegrityTokenProviderBuilderFingerprint = fingerprint {
|
internal val runIntegrityVerificationFingerprint = fingerprint {
|
||||||
strings(
|
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||||
"standard_pi_init",
|
returns("V")
|
||||||
"outcome",
|
opcodes(
|
||||||
"success"
|
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<MethodReference>()
|
||||||
|
reference?.definingClass == "Ljava/util/Calendar;" && reference.name == "get"
|
||||||
|
} >= 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ val spoofClientPatch = bytecodePatch(
|
||||||
name = "Spoof client",
|
name = "Spoof client",
|
||||||
description = "Spoofs the client to fix various functions of the app.",
|
description = "Spoofs the client to fix various functions of the app.",
|
||||||
) {
|
) {
|
||||||
val port by intOption(
|
val requestListenerPort by intOption(
|
||||||
key = "port",
|
key = "requestListenerPort",
|
||||||
default = 4345,
|
default = 4345,
|
||||||
title = " Login request listener port",
|
title = " Login request listener port",
|
||||||
description = "The port to use for the listener that intercepts and handles login requests. " +
|
description = "The port to use for the listener that intercepts and handles login requests. " +
|
||||||
|
|
@ -46,10 +46,10 @@ val spoofClientPatch = bytecodePatch(
|
||||||
"x86",
|
"x86",
|
||||||
"x86_64"
|
"x86_64"
|
||||||
).forEach { architecture ->
|
).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"
|
"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"
|
"lib/$architecture/liborbit-jni-spotify.so"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -58,6 +58,8 @@ val spoofClientPatch = bytecodePatch(
|
||||||
compatibleWith("com.spotify.music")
|
compatibleWith("com.spotify.music")
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
|
// region Spoof package info.
|
||||||
|
|
||||||
getPackageInfoFingerprint.method.apply {
|
getPackageInfoFingerprint.method.apply {
|
||||||
// region Spoof signature.
|
// region Spoof signature.
|
||||||
|
|
||||||
|
|
@ -99,28 +101,33 @@ val spoofClientPatch = bytecodePatch(
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
startLiborbitFingerprint.method.addInstructions(
|
// endregion
|
||||||
|
|
||||||
|
// region Spoof client.
|
||||||
|
|
||||||
|
loadOrbitLibraryFingerprint.method.addInstructions(
|
||||||
0,
|
0,
|
||||||
"""
|
"""
|
||||||
const/16 v0, $port
|
const/16 v0, $requestListenerPort
|
||||||
invoke-static { v0 }, $EXTENSION_CLASS_DESCRIPTOR->listen(I)V
|
invoke-static { v0 }, $EXTENSION_CLASS_DESCRIPTOR->launchListener(I)V
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
startupPageLayoutInflateFingerprint.method.apply {
|
startupPageLayoutInflateFingerprint.method.apply {
|
||||||
val openLoginWebViewDescriptor =
|
val openLoginWebViewDescriptor =
|
||||||
"$EXTENSION_CLASS_DESCRIPTOR->login(Landroid/view/LayoutInflater;)V"
|
"$EXTENSION_CLASS_DESCRIPTOR->launchLogin(Landroid/view/LayoutInflater;)V"
|
||||||
|
|
||||||
addInstructions(
|
addInstructions(
|
||||||
0,
|
0,
|
||||||
"""
|
"""
|
||||||
move-object/from16 v3, p1
|
invoke-static/range { p1 .. p1 }, $openLoginWebViewDescriptor
|
||||||
invoke-static { v3 }, $openLoginWebViewDescriptor
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Early return to block sending bad verdicts to the API.
|
// Early return to block sending bad verdicts to the API.
|
||||||
standardIntegrityTokenProviderBuilderFingerprint.method.returnEarly()
|
runIntegrityVerificationFingerprint.method.returnEarly()
|
||||||
|
|
||||||
|
// endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package app.revanced.patches.spotify.shared
|
||||||
|
|
||||||
import app.revanced.patcher.fingerprint
|
import app.revanced.patcher.fingerprint
|
||||||
import app.revanced.patcher.patch.BytecodePatchContext
|
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;"
|
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 const val SPOTIFY_MAIN_ACTIVITY_LEGACY = "Lcom/spotify/music/MainActivity;"
|
||||||
|
|
||||||
internal val mainActivityOnCreateFingerprint = fingerprint {
|
internal val mainActivityOnCreateFingerprint = fingerprint {
|
||||||
|
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||||
|
returns("V")
|
||||||
|
parameters("Landroid/os/Bundle;")
|
||||||
custom { method, classDef ->
|
custom { method, classDef ->
|
||||||
method.name == "onCreate" && (classDef.type == SPOTIFY_MAIN_ACTIVITY
|
method.name == "onCreate" && (classDef.type == SPOTIFY_MAIN_ACTIVITY
|
||||||
|| classDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY)
|
|| classDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY)
|
||||||
|
|
@ -26,9 +29,10 @@ private var isLegacyAppTarget: Boolean? = null
|
||||||
* supports Spotify integration on Kenwood/Pioneer car stereos.
|
* supports Spotify integration on Kenwood/Pioneer car stereos.
|
||||||
*/
|
*/
|
||||||
context(BytecodePatchContext)
|
context(BytecodePatchContext)
|
||||||
internal val IS_SPOTIFY_LEGACY_APP_TARGET get(): Boolean {
|
internal val IS_SPOTIFY_LEGACY_APP_TARGET
|
||||||
|
get(): Boolean {
|
||||||
if (isLegacyAppTarget == null) {
|
if (isLegacyAppTarget == null) {
|
||||||
isLegacyAppTarget = mainActivityOnCreateHook.fingerprint.originalClassDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY
|
isLegacyAppTarget = mainActivityOnCreateFingerprint.originalClassDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY
|
||||||
}
|
}
|
||||||
return isLegacyAppTarget!!
|
return isLegacyAppTarget!!
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue