Compare commits

...

16 Commits

Author SHA1 Message Date
semantic-release-bot
5d2c21540c chore: Release v5.29.1-dev.1 [skip ci]
## [5.29.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.29.0...v5.29.1-dev.1) (2025-06-26)

### Bug Fixes

* **Spotify:** Add `Spoof client` patch to fix various issues by using a web platform access token ([#5173](https://github.com/ReVanced/revanced-patches/issues/5173)) ([1a8aacd](1a8aacdff6))
2025-06-26 17:56:10 +00:00
oSumAtrIX
1a8aacdff6 fix(Spotify): Add Spoof client patch to fix various issues by using a web platform access token (#5173)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: brosssh <tiabroch@gmail.com>
Co-authored-by: Dawid Krajcarz <80264606+drobotk@users.noreply.github.com>
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2025-06-26 19:51:18 +02:00
semantic-release-bot
1804bd9bfc chore: Release v5.29.0 [skip ci]
# [5.29.0](https://github.com/ReVanced/revanced-patches/compare/v5.28.0...v5.29.0) (2025-06-26)

### Bug Fixes

* Add scrollable content to modern style settings dialogs ([#5211](https://github.com/ReVanced/revanced-patches/issues/5211)) ([2b62fc2](2b62fc2224))
* **Google Photos:** Resolve startup crash for Android 5.0 devices ([7be3741](7be374100b))
* **YouTube - Hide ads:** Hide new type of product ad in video description ([#5225](https://github.com/ReVanced/revanced-patches/issues/5225)) ([b656976](b65697603d))
* **YouTube - Hide layout components:** Fix "Hide video description attributes" ([#5250](https://github.com/ReVanced/revanced-patches/issues/5250)) ([978c244](978c24458b))
* **YouTube - Hide Shorts components:** Fix "Hide Use this sound button" ([#5233](https://github.com/ReVanced/revanced-patches/issues/5233)) ([a678f17](a678f178e1))
* **YouTube - Hide Shorts components:** Fix "Hide Use this template button" ([#5249](https://github.com/ReVanced/revanced-patches/issues/5249)) ([957bece](957bece3e9))
* **YouTube:** Always use single threaded layout to resolve layout bugs in unpatched YouTube ([#5226](https://github.com/ReVanced/revanced-patches/issues/5226)) ([ccd1691](ccd169121a))
* **YouTube:** Fix refactoring app startup exception ([0dbd058](0dbd058099))

### Features

* Add `Spoof app signature` patch ([#5158](https://github.com/ReVanced/revanced-patches/issues/5158)) ([fb83e58](fb83e58f79))
* **Cricbuzz:** Add `Hide ads` patch ([#4998](https://github.com/ReVanced/revanced-patches/issues/4998)) ([558bf8b](558bf8bca8))
* **Crunchyroll:** Add `Hide ads` patch ([#5201](https://github.com/ReVanced/revanced-patches/issues/5201)) ([d338989](d338989cb4))
* **YouTube - Hide Shorts components:** Add `Hide Effects button` ([#5255](https://github.com/ReVanced/revanced-patches/issues/5255)) ([29c86ac](29c86ac6a3))
* **YouTube - Hide video action buttons:** Add `Hide Stop ads` ([#5245](https://github.com/ReVanced/revanced-patches/issues/5245)) ([0e63f49](0e63f49e13))
* **YouTube:** Add an option to disable toasts when changing default playback speed or quality ([#5230](https://github.com/ReVanced/revanced-patches/issues/5230)) ([6b719df](6b719dfcd7))
* **YouTube:** Support version `20.13.41` ([#5253](https://github.com/ReVanced/revanced-patches/issues/5253)) ([439ca37](439ca37e99))
2025-06-26 08:06:08 +00:00
LisoUseInAIKyrios
7eb4e62762 chore: Merge branch dev to main (#5227) 2025-06-26 12:02:33 +04:00
LisoUseInAIKyrios
b8e10b5c1f chore: Fix api dump 2025-06-26 11:51:54 +04:00
github-actions[bot]
a7c11b9b08 chore: Sync translations (#5258) 2025-06-26 11:50:50 +04:00
semantic-release-bot
443c0a74d5 chore: Release v5.29.0-dev.11 [skip ci]
# [5.29.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.10...v5.29.0-dev.11) (2025-06-26)

### Features

* **Cricbuzz:** Add `Hide ads` patch ([#4998](https://github.com/ReVanced/revanced-patches/issues/4998)) ([558bf8b](558bf8bca8))
2025-06-26 07:13:07 +00:00
github-actions[bot]
84a0f7f7d7 chore: Sync translations (#5257) 2025-06-26 11:10:05 +04:00
Sourav Agrawal
558bf8bca8 feat(Cricbuzz): Add Hide ads patch (#4998) 2025-06-26 11:08:22 +04:00
semantic-release-bot
e22d4e6a4b chore: Release v5.29.0-dev.10 [skip ci]
# [5.29.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.9...v5.29.0-dev.10) (2025-06-25)

### Features

* **YouTube - Hide Shorts components:** Add `Hide Effects button` ([#5255](https://github.com/ReVanced/revanced-patches/issues/5255)) ([29c86ac](29c86ac6a3))
2025-06-25 08:59:45 +00:00
LisoUseInAIKyrios
a07f946633 chore: Fix typo 2025-06-25 12:56:42 +04:00
LisoUseInAIKyrios
29c86ac6a3 feat(YouTube - Hide Shorts components): Add Hide Effects button (#5255) 2025-06-25 12:54:59 +04:00
semantic-release-bot
19cf5667d8 chore: Release v5.29.0-dev.9 [skip ci]
# [5.29.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.8...v5.29.0-dev.9) (2025-06-25)

### Features

* Add `Spoof app signature` patch ([#5158](https://github.com/ReVanced/revanced-patches/issues/5158)) ([fb83e58](fb83e58f79))
2025-06-25 07:16:33 +00:00
Markus Probst
fb83e58f79 feat: Add Spoof app signature patch (#5158) 2025-06-25 11:13:32 +04:00
semantic-release-bot
9844081d04 chore: Release v5.29.0-dev.8 [skip ci]
# [5.29.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.7...v5.29.0-dev.8) (2025-06-25)

### Features

* **YouTube:** Support version `20.13.41` ([#5253](https://github.com/ReVanced/revanced-patches/issues/5253)) ([439ca37](439ca37e99))
2025-06-25 07:06:01 +00:00
LisoUseInAIKyrios
439ca37e99 feat(YouTube): Support version 20.13.41 (#5253) 2025-06-25 11:03:25 +04:00
138 changed files with 1800 additions and 491 deletions

View File

@@ -1,3 +1,63 @@
## [5.29.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.29.0...v5.29.1-dev.1) (2025-06-26)
### Bug Fixes
* **Spotify:** Add `Spoof client` patch to fix various issues by using a web platform access token ([#5173](https://github.com/ReVanced/revanced-patches/issues/5173)) ([b7b75bb](https://github.com/ReVanced/revanced-patches/commit/b7b75bb9d8d5fd505121e752b8a20e61ff28d1b2))
# [5.29.0](https://github.com/ReVanced/revanced-patches/compare/v5.28.0...v5.29.0) (2025-06-26)
### Bug Fixes
* Add scrollable content to modern style settings dialogs ([#5211](https://github.com/ReVanced/revanced-patches/issues/5211)) ([e6876d5](https://github.com/ReVanced/revanced-patches/commit/e6876d510d28f6a3a41ec1722a033b3e30a22c65))
* **Google Photos:** Resolve startup crash for Android 5.0 devices ([0294533](https://github.com/ReVanced/revanced-patches/commit/0294533c4d9a321aea086eedb4e46385ae9a026e))
* **YouTube - Hide ads:** Hide new type of product ad in video description ([#5225](https://github.com/ReVanced/revanced-patches/issues/5225)) ([1e2efad](https://github.com/ReVanced/revanced-patches/commit/1e2efad7b2714c395ed6b0a77cbbf8a2265df520))
* **YouTube - Hide layout components:** Fix "Hide video description attributes" ([#5250](https://github.com/ReVanced/revanced-patches/issues/5250)) ([2f22d45](https://github.com/ReVanced/revanced-patches/commit/2f22d45eb80745ac64fbea44c8055ebe7925a586))
* **YouTube - Hide Shorts components:** Fix "Hide Use this sound button" ([#5233](https://github.com/ReVanced/revanced-patches/issues/5233)) ([5d6ec9e](https://github.com/ReVanced/revanced-patches/commit/5d6ec9e94a6221a0f32762d5bede893e9e7457fc))
* **YouTube - Hide Shorts components:** Fix "Hide Use this template button" ([#5249](https://github.com/ReVanced/revanced-patches/issues/5249)) ([b399ecb](https://github.com/ReVanced/revanced-patches/commit/b399ecbb6a222d82dd5e4b3417c9f7eff4324adb))
* **YouTube:** Always use single threaded layout to resolve layout bugs in unpatched YouTube ([#5226](https://github.com/ReVanced/revanced-patches/issues/5226)) ([1f539b1](https://github.com/ReVanced/revanced-patches/commit/1f539b1396526b2c767d77a804bd0308ee4a42ec))
* **YouTube:** Fix refactoring app startup exception ([1b00c90](https://github.com/ReVanced/revanced-patches/commit/1b00c907f4b90f4659afb4a54ba61ac2835b460d))
### Features
* Add `Spoof app signature` patch ([#5158](https://github.com/ReVanced/revanced-patches/issues/5158)) ([78b25aa](https://github.com/ReVanced/revanced-patches/commit/78b25aa4e87ec3f9df1d57831b48a39029969416))
* **Cricbuzz:** Add `Hide ads` patch ([#4998](https://github.com/ReVanced/revanced-patches/issues/4998)) ([83ccfa8](https://github.com/ReVanced/revanced-patches/commit/83ccfa8e1b5d5a44c55ef659484acf3cc08d3346))
* **Crunchyroll:** Add `Hide ads` patch ([#5201](https://github.com/ReVanced/revanced-patches/issues/5201)) ([46b4398](https://github.com/ReVanced/revanced-patches/commit/46b4398fd6ca223391ed8f497a8347c2313421b7))
* **YouTube - Hide Shorts components:** Add `Hide Effects button` ([#5255](https://github.com/ReVanced/revanced-patches/issues/5255)) ([240897a](https://github.com/ReVanced/revanced-patches/commit/240897a94008ce9a148c87bb41b978d553d5a6f5))
* **YouTube - Hide video action buttons:** Add `Hide Stop ads` ([#5245](https://github.com/ReVanced/revanced-patches/issues/5245)) ([274dcc6](https://github.com/ReVanced/revanced-patches/commit/274dcc676e009be63eb6970de33abacd34dc6560))
* **YouTube:** Add an option to disable toasts when changing default playback speed or quality ([#5230](https://github.com/ReVanced/revanced-patches/issues/5230)) ([c68cde3](https://github.com/ReVanced/revanced-patches/commit/c68cde3a896450874cc571be5c4723387db96032))
* **YouTube:** Support version `20.13.41` ([#5253](https://github.com/ReVanced/revanced-patches/issues/5253)) ([d284c3d](https://github.com/ReVanced/revanced-patches/commit/d284c3dd3277430b6885e7c27ee02d062dcefc85))
# [5.29.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.10...v5.29.0-dev.11) (2025-06-26)
### Features
* **Cricbuzz:** Add `Hide ads` patch ([#4998](https://github.com/ReVanced/revanced-patches/issues/4998)) ([83ccfa8](https://github.com/ReVanced/revanced-patches/commit/83ccfa8e1b5d5a44c55ef659484acf3cc08d3346))
# [5.29.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.9...v5.29.0-dev.10) (2025-06-25)
### Features
* **YouTube - Hide Shorts components:** Add `Hide Effects button` ([#5255](https://github.com/ReVanced/revanced-patches/issues/5255)) ([240897a](https://github.com/ReVanced/revanced-patches/commit/240897a94008ce9a148c87bb41b978d553d5a6f5))
# [5.29.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.8...v5.29.0-dev.9) (2025-06-25)
### Features
* Add `Spoof app signature` patch ([#5158](https://github.com/ReVanced/revanced-patches/issues/5158)) ([78b25aa](https://github.com/ReVanced/revanced-patches/commit/78b25aa4e87ec3f9df1d57831b48a39029969416))
# [5.29.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.7...v5.29.0-dev.8) (2025-06-25)
### Features
* **YouTube:** Support version `20.13.41` ([#5253](https://github.com/ReVanced/revanced-patches/issues/5253)) ([d284c3d](https://github.com/ReVanced/revanced-patches/commit/d284c3dd3277430b6885e7c27ee02d062dcefc85))
# [5.29.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.6...v5.29.0-dev.7) (2025-06-24)

3
build.gradle.kts Normal file
View File

@@ -0,0 +1,3 @@
plugins {
alias(libs.plugins.android.library) apply false
}

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,5 +1,5 @@
plugins {
id("com.android.library")
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,7 +1,15 @@
plugins {
alias(libs.plugins.protobuf)
}
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:spotify:stub"))
compileOnly(libs.annotation)
implementation(project(":extensions:spotify:utils"))
implementation(libs.nanohttpd)
implementation(libs.protobuf.javalite)
}
android {
@@ -14,3 +22,19 @@ android {
targetCompatibility = JavaVersion.VERSION_1_8
}
}
protobuf {
protoc {
artifact = libs.protobuf.protoc.get().toString()
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") {
option("lite")
}
}
}
}
}

View File

@@ -0,0 +1,153 @@
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 fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
class LoginRequestListener extends NanoHTTPD {
LoginRequestListener(int port) {
super(port);
}
@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 e) {
Logger.printException(() -> "Failed to parse LoginRequest", e);
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) {
loginResponse = getLoginResponse(loginRequest);
}
if (loginResponse != null) {
return newResponse(Response.Status.OK, loginResponse);
}
return newResponse(INTERNAL_ERROR);
}
@Nullable
private static LoginResponse getLoginResponse(@NonNull LoginRequest loginRequest) {
Session session;
boolean isInitialLogin = !loginRequest.hasStoredCredential();
if (isInitialLogin) {
Logger.printInfo(() -> "Received request for initial login");
session = WebApp.currentSession; // Session obtained from WebApp.login.
} else {
Logger.printInfo(() -> "Received request to restore saved session");
session = Session.read(loginRequest.getStoredCredential().getUsername());
}
return toLoginResponse(session, isInitialLogin);
}
private static LoginResponse toLoginResponse(Session session, boolean isInitialLogin) {
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);
} else if (session.accessTokenExpired()) {
Logger.printInfo(() -> "Access token has expired, renewing session");
WebApp.renewSession(session.cookies);
return toLoginResponse(WebApp.currentSession, isInitialLogin);
} 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);
}
}

View File

@@ -0,0 +1,124 @@
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;
/**
* @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();
}
@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 +
')';
}
}

View File

@@ -0,0 +1,42 @@
package app.revanced.extension.spotify.misc.fix;
import android.view.LayoutInflater;
import app.revanced.extension.shared.Logger;
@SuppressWarnings("unused")
public class SpoofClientPatch {
private static LoginRequestListener listener;
/**
* Injection point.
* <br>
* Start login server.
*/
public static void listen(int port) {
if (listener != null) {
Logger.printInfo(() -> "Listener already running on port " + port);
return;
}
try {
listener = new LoginRequestListener(port);
listener.start();
Logger.printInfo(() -> "Listener running on port " + port);
} catch (Exception ex) {
Logger.printException(() -> "listen failure", ex);
}
}
/**
* Injection point.
* <br>
* Launch login web view.
*/
public static void login(LayoutInflater inflater) {
try {
WebApp.login(inflater.getContext());
} catch (Exception ex) {
Logger.printException(() -> "login failure", ex);
}
}
}

View File

@@ -0,0 +1,267 @@
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.*;
import android.webkit.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
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 int GET_SESSION_TIMEOUT_SECONDS = 10;
private static final String JAVASCRIPT_INTERFACE_NAME = "androidInterface";
private static final String USER_AGENT = getWebUserAgent();
/**
* 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 login(Context context) {
Logger.printInfo(() -> "Starting 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);
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() {
@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);
});
dialog.setCancelable(false);
dialog.setContentView(webView);
dialog.show();
}
@Override
void onLoggedIn(String cookies) {
dialog.dismiss();
}
@Override
void onReceivedSession(WebView webView, Session session) {
Logger.printInfo(() -> "Received session from login: " + session);
currentSession = session;
currentWebView = null;
webView.stopLoading();
webView.destroy();
}
}
);
}
static void renewSession(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");
}
} catch (InterruptedException e) {
Logger.printException(() -> "Interrupted while waiting to retrieve session", e);
Thread.currentThread().interrupt();
}
// Cleanup.
currentWebView = null;
}
/**
* All methods are called on the main thread.
*/
abstract static class WebViewCallback {
void onInitialized(WebView webView) {
}
void onLoggedIn(String cookies) {
}
void onReceivedSession(WebView webView, Session session) {
}
}
@SuppressLint("SetJavaScriptEnabled")
private static void newWebView(
Context context,
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);
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" +
" })" +
" " +
" }" +
"});";
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));
}
}, JAVASCRIPT_INTERFACE_NAME);
currentWebView = webView;
CookieManager.getInstance().removeAllCookies((anyRemoved) -> {
Logger.printInfo(() -> "WebView initialized with user agent: " + USER_AGENT);
webViewCallback.onInitialized(currentWebView);
});
});
}
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 e) {
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);
}
return userAgentString;
}
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);
}
}
}

View File

@@ -0,0 +1,43 @@
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;
}

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -0,0 +1,19 @@
plugins {
java
antlr
}
dependencies {
antlr(libs.antlr4)
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
tasks {
generateGrammarSource {
arguments = listOf("-visitor")
}
}

View File

@@ -0,0 +1,35 @@
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]+
;

View File

@@ -0,0 +1,60 @@
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();
}
}

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,7 +1,7 @@
android.namespace = "app.revanced.extension"
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -274,6 +274,11 @@ public final class ShortsFilter extends Filter {
Settings.HIDE_SHORTS_UPCOMING_BUTTON,
"yt_outline_bell_"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_EFFECT_BUTTON,
// https://www.gstatic.com/youtube/effects/xeno/arcade/effects/icons/
"/arcade/effects/icons/"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_GREEN_SCREEN_BUTTON,
"greenscreen_temp"

View File

@@ -267,6 +267,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_SHORTS_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_shorts_comments_button", FALSE);
public static final BooleanSetting HIDE_SHORTS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_dislike_button", FALSE);
public static final BooleanSetting HIDE_SHORTS_FULL_VIDEO_LINK_LABEL = new BooleanSetting("revanced_hide_shorts_full_video_link_label", FALSE);
public static final BooleanSetting HIDE_SHORTS_EFFECT_BUTTON = new BooleanSetting("revanced_hide_shorts_effect_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_GREEN_SCREEN_BUTTON = new BooleanSetting("revanced_hide_shorts_green_screen_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_NEW_POSTS_BUTTON = new BooleanSetting("revanced_hide_shorts_new_posts_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_HASHTAG_BUTTON = new BooleanSetting("revanced_hide_shorts_hashtag_button", TRUE);

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
version = 5.29.0-dev.7
version = 5.29.1-dev.1

View File

@@ -11,14 +11,25 @@ appcompat = "1.7.0"
okhttp = "5.0.0-alpha.14"
retrofit = "2.11.0"
guava = "33.4.0-jre"
protobuf-javalite = "4.31.1"
protoc = "4.31.1"
protobuf = "0.9.5"
antlr4 = "4.13.2"
nanohttpd = "2.3.1"
apksig = "8.10.1"
[libraries]
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
antlr4 = { module = "org.antlr:antlr4", version.ref = "antlr4" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf-javalite" }
protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protoc" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
apksig = { group = "com.android.tools.build", name = "apksig", version.ref = "apksig" }
[plugins]
android-library = { id = "com.android.library", version.ref = "agp" }
android-library = { id = "com.android.library" }
protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }

View File

@@ -1,8 +1,6 @@
#Mon Jun 16 14:39:32 CEST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -116,6 +116,10 @@ public final class app/revanced/patches/all/misc/shortcut/sharetargets/RemoveSha
public static final fun getRemoveShareTargetsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
public final class app/revanced/patches/all/misc/spoof/SignatureSpoofPatchKt {
public static final fun getSignatureSpoofPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
public final class app/revanced/patches/all/misc/targetSdk/SetTargetSdkVersion34Kt {
public static final fun getSetTargetSdkVersion34 ()Lapp/revanced/patcher/patch/ResourcePatch;
}
@@ -160,6 +164,10 @@ public final class app/revanced/patches/cieid/restrictions/root/BypassRootChecks
public static final fun getBypassRootChecksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/cricbuzz/ads/DisableAdsPatchKt {
public static final fun getDisableAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/crunchyroll/ads/HideAdsPatchKt {
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -660,17 +668,39 @@ public final class app/revanced/patches/shared/misc/gms/GmsCoreSupportPatchKt {
public static synthetic fun gmsCoreSupportResourcePatch$default (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patcher/patch/Option;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/ResourcePatch;
}
public final class app/revanced/patches/shared/misc/hex/HexPatchKt {
public static final fun hexPatch (Lkotlin/jvm/functions/Function0;)Lapp/revanced/patcher/patch/RawResourcePatch;
public final class app/revanced/patches/shared/misc/hex/HexPatchBuilder : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> ()V
public fun add (Lapp/revanced/patches/shared/misc/hex/Replacement;)Z
public synthetic fun add (Ljava/lang/Object;)Z
public fun addAll (Ljava/util/Collection;)Z
public final fun asPatternTo (Ljava/lang/String;Ljava/lang/String;)Lkotlin/Pair;
public fun clear ()V
public fun contains (Lapp/revanced/patches/shared/misc/hex/Replacement;)Z
public final fun contains (Ljava/lang/Object;)Z
public fun containsAll (Ljava/util/Collection;)Z
public fun getSize ()I
public final fun inFile (Lkotlin/Pair;Ljava/lang/String;)V
public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator;
public fun remove (Ljava/lang/Object;)Z
public fun removeAll (Ljava/util/Collection;)Z
public fun retainAll (Ljava/util/Collection;)Z
public final fun size ()I
public fun toArray ()[Ljava/lang/Object;
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
}
public final class app/revanced/patches/shared/misc/hex/HexPatchBuilderKt {
public static final fun hexPatch (ZLkotlin/jvm/functions/Function0;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static final fun hexPatch (ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static synthetic fun hexPatch$default (ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static synthetic fun hexPatch$default (ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/RawResourcePatch;
}
public final class app/revanced/patches/shared/misc/hex/Replacement {
public static final field Companion Lapp/revanced/patches/shared/misc/hex/Replacement$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public final fun replacePattern ([B)V
}
public final class app/revanced/patches/shared/misc/hex/Replacement$Companion {
public fun <init> ([B[BLjava/lang/String;)V
public final fun getReplacementBytesPadded ()[B
}
public final class app/revanced/patches/shared/misc/mapping/ResourceElement {
@@ -914,6 +944,10 @@ public final class app/revanced/patches/spotify/misc/extension/ExtensionPatchKt
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/spotify/misc/fix/SpoofClientPatchKt {
public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatchKt {
public static final fun getSpoofPackageInfoPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}

View File

@@ -15,6 +15,9 @@ patches {
dependencies {
// Required due to smali, or build fails. Can be removed once smali is bumped.
implementation(libs.guava)
implementation(libs.apksig)
// Android API stubs defined here.
compileOnly(project(":patches:stub"))
}

View File

@@ -3,7 +3,7 @@ package app.revanced.patches.all.misc.hex
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.rawResourcePatch
import app.revanced.patcher.patch.stringsOption
import app.revanced.patches.shared.misc.hex.Replacement
import app.revanced.patches.shared.misc.hex.HexPatchBuilder
import app.revanced.patches.shared.misc.hex.hexPatch
import app.revanced.util.Utils.trimIndentMultiline
@@ -13,44 +13,42 @@ val hexPatch = rawResourcePatch(
description = "Replaces a hexadecimal patterns of bytes of files in an APK.",
use = false,
) {
// TODO: Instead of stringArrayOption, use a custom option type to work around
// https://github.com/ReVanced/revanced-library/issues/48.
// Replace the custom option type with a stringArrayOption once the issue is resolved.
val replacements by stringsOption(
key = "replacements",
title = "Replacements",
description = """
Hexadecimal patterns to search for and replace with another in a target file.
A pattern is a sequence of case insensitive strings, each representing hexadecimal bytes, separated by spaces.
An example pattern is 'aa 01 02 FF'.
Every pattern must be followed by a pipe ('|'), the replacement pattern,
another pipe ('|'), and the path to the file to make the changes in relative to the APK root.
The replacement pattern must have the same length as the original pattern.
The replacement pattern must be shorter or equal in length to the pattern.
Full example of a valid input:
'aa 01 02 FF|00 00 00 00|path/to/file'
Full example of a valid replacement:
'01 02 aa FF|03 04|path/to/file'
""".trimIndentMultiline(),
required = true,
)
dependsOn(
hexPatch {
replacements!!.map { from ->
val (pattern, replacementPattern, targetFilePath) = try {
from.split("|", limit = 3)
} catch (e: Exception) {
throw PatchException(
"Invalid input: $from.\n" +
"Every pattern must be followed by a pipe ('|'), " +
"the replacement pattern, another pipe ('|'), " +
"and the path to the file to make the changes in relative to the APK root. ",
)
hexPatch(
block = fun HexPatchBuilder.() {
replacements!!.forEach { replacement ->
try {
val (pattern, replacementPattern, targetFilePath) = replacement.split("|", limit = 3)
pattern asPatternTo replacementPattern inFile targetFilePath
} catch (e: Exception) {
throw PatchException(
"Invalid replacement: $replacement.\n" +
"Every pattern must be followed by a pipe ('|'), " +
"the replacement pattern, another pipe ('|'), " +
"and the path to the file to make the changes in relative to the APK root. ",
)
}
}
Replacement(pattern, replacementPattern, targetFilePath)
}.toSet()
},
},
)
)
}

View File

@@ -0,0 +1,95 @@
package app.revanced.patches.all.misc.spoof
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.util.getNode
import com.android.apksig.ApkVerifier
import com.android.apksig.apk.ApkFormatException
import org.w3c.dom.Element
import java.io.ByteArrayInputStream
import java.io.IOException
import java.nio.file.Files
import java.nio.file.InvalidPathException
import java.nio.file.attribute.BasicFileAttributes
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.util.*
import kotlin.io.path.Path
val signatureSpoofPatch = resourcePatch(
name = "Spoof app signature",
description = "Spoofs the app signature via the \"fake-signature\" meta key. " +
"This patch only works with patched device roms.",
use = false,
) {
val signature by stringOption(
key = "spoofedAppSignature",
title = "Signature",
validator = { signature ->
optionToSignature(signature) != null
},
description = "The hex-encoded signature or path to an apk file with the desired signature",
required = true,
)
execute {
document("AndroidManifest.xml").use { document ->
val manifest = document.getNode("manifest") as Element
val fakeSignaturePermission = document.createElement("uses-permission")
fakeSignaturePermission.setAttribute("android:name", "android.permission.FAKE_PACKAGE_SIGNATURE")
manifest.appendChild(fakeSignaturePermission)
val application = document.getNode("application") ?: {
val child = document.createElement("application")
manifest.appendChild(child)
child
} as Element;
val fakeSignatureMetadata = document.createElement("meta-data")
fakeSignatureMetadata.setAttribute("android:name", "fake-signature")
fakeSignatureMetadata.setAttribute("android:value", optionToSignature(signature))
application.appendChild(fakeSignatureMetadata)
}
}
}
internal fun optionToSignature(signature: String?): String? {
if (signature == null) {
return null;
}
try {
// TODO: Replace with signature.hexToByteArray when stable in kotlin
val signatureBytes = HexFormat.of()
.parseHex(signature)
val factory = CertificateFactory.getInstance("X.509")
factory.generateCertificate(ByteArrayInputStream(signatureBytes))
return signature;
} catch (_: IllegalArgumentException) {
} catch (_: CertificateException) {
}
try {
val signaturePath = Path(signature)
if (!Files.readAttributes(signaturePath, BasicFileAttributes::class.java).isRegularFile) {
return null;
}
val verifier = ApkVerifier.Builder(signaturePath.toFile())
.build()
val result = verifier.verify()
if (result.isVerifiedUsingV3Scheme) {
return HexFormat.of().formatHex(result.v3SchemeSigners[0].certificate.encoded)
} else if (result.isVerifiedUsingV2Scheme) {
return HexFormat.of().formatHex(result.v2SchemeSigners[0].certificate.encoded)
} else if (result.isVerifiedUsingV1Scheme) {
return HexFormat.of().formatHex(result.v1SchemeSigners[0].certificate.encoded)
}
return null;
} catch (_: IOException) {
} catch (_: InvalidPathException) {
} catch (_: ApkFormatException) {
} catch (_: NoSuchAlgorithmException) {
} catch (_: IllegalArgumentException) {}
return null;
}

View File

@@ -0,0 +1,27 @@
package app.revanced.patches.cricbuzz.ads
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@Suppress("unused")
val disableAdsPatch = bytecodePatch (
name = "Hide ads",
) {
compatibleWith("com.cricbuzz.android"("6.23.02"))
execute {
userStateSwitchFingerprint.method.apply {
val opcodeIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT_OBJECT)
val register = getInstruction<OneRegisterInstruction>(opcodeIndex).registerA
addInstruction(
opcodeIndex + 1,
"const-string v$register, \"ACTIVE\""
)
}
}
}

View File

@@ -0,0 +1,9 @@
package app.revanced.patches.cricbuzz.ads
import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.Opcode
internal val userStateSwitchFingerprint = fingerprint {
strings("key.user.state", "NA")
opcodes(Opcode.SPARSE_SWITCH)
}

View File

@@ -1,123 +0,0 @@
package app.revanced.patches.shared.misc.hex
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.rawResourcePatch
import kotlin.math.max
// The replacements being passed using a function is intended.
// Previously the replacements were a property of the patch. Getter were being delegated to that property.
// This late evaluation was being leveraged in app.revanced.patches.all.misc.hex.HexPatch.
// Without the function, the replacements would be evaluated at the time of patch creation.
// This isn't possible because the delegated property is not accessible at that time.
fun hexPatch(replacementsSupplier: () -> Set<Replacement>) = rawResourcePatch {
execute {
replacementsSupplier().groupBy { it.targetFilePath }.forEach { (targetFilePath, replacements) ->
val targetFile = try {
get(targetFilePath, true)
} catch (e: Exception) {
throw PatchException("Could not find target file: $targetFilePath")
}
// TODO: Use a file channel to read and write the file instead of reading the whole file into memory,
// in order to reduce memory usage.
val targetFileBytes = targetFile.readBytes()
replacements.forEach { replacement ->
replacement.replacePattern(targetFileBytes)
}
targetFile.writeBytes(targetFileBytes)
}
}
}
/**
* Represents a pattern to search for and its replacement pattern.
*
* @property pattern The pattern to search for.
* @property replacementPattern The pattern to replace the [pattern] with.
* @property targetFilePath The path to the file to make the changes in relative to the APK root.
*/
class Replacement(
private val pattern: String,
replacementPattern: String,
internal val targetFilePath: String,
) {
private val patternBytes = pattern.toByteArrayPattern()
private val replacementPattern = replacementPattern.toByteArrayPattern()
init {
if (this.patternBytes.size != this.replacementPattern.size) {
throw PatchException("Pattern and replacement pattern must have the same length: $pattern")
}
}
/**
* Replaces the [patternBytes] with the [replacementPattern] in the [targetFileBytes].
*
* @param targetFileBytes The bytes of the file to make the changes in.
*/
fun replacePattern(targetFileBytes: ByteArray) {
val startIndex = indexOfPatternIn(targetFileBytes)
if (startIndex == -1) {
throw PatchException("Pattern not found in target file: $pattern")
}
replacementPattern.copyInto(targetFileBytes, startIndex)
}
// TODO: Allow searching in a file channel instead of a byte array to reduce memory usage.
/**
* Returns the index of the first occurrence of [patternBytes] in the haystack
* using the Boyer-Moore algorithm.
*
* @param haystack The array to search in.
*
* @return The index of the first occurrence of the [patternBytes] in the haystack or -1
* if the [patternBytes] is not found.
*/
private fun indexOfPatternIn(haystack: ByteArray): Int {
val needle = patternBytes
val haystackLength = haystack.size - 1
val needleLength = needle.size - 1
val right = IntArray(256) { -1 }
for (i in 0 until needleLength) right[needle[i].toInt().and(0xFF)] = i
var skip: Int
for (i in 0..haystackLength - needleLength) {
skip = 0
for (j in needleLength - 1 downTo 0) {
if (needle[j] != haystack[i + j]) {
skip = max(1, j - right[haystack[i + j].toInt().and(0xFF)])
break
}
}
if (skip == 0) return i
}
return -1
}
companion object {
/**
* Convert a string representing a pattern of hexadecimal bytes to a byte array.
*
* @return The byte array representing the pattern.
* @throws PatchException If the pattern is invalid.
*/
private fun String.toByteArrayPattern() = try {
split(" ").map { it.toInt(16).toByte() }.toByteArray()
} catch (e: NumberFormatException) {
throw PatchException(
"Could not parse pattern: $this. A pattern is a sequence of case insensitive strings " +
"representing hexadecimal bytes separated by spaces",
e,
)
}
}
}

View File

@@ -0,0 +1,154 @@
package app.revanced.patches.shared.misc.hex
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.rawResourcePatch
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.math.max
fun hexPatch(ignoreMissingTargetFiles: Boolean = false, block: HexPatchBuilder.() -> Unit) =
hexPatch(ignoreMissingTargetFiles, fun(): Set<Replacement> = HexPatchBuilder().apply(block))
@Suppress("JavaDefaultMethodsNotOverriddenByDelegation")
class HexPatchBuilder internal constructor(
private val replacements: MutableSet<Replacement> = mutableSetOf(),
) : Set<Replacement> by replacements {
infix fun String.asPatternTo(replacementPattern: String) = byteArrayOf(this) to byteArrayOf(replacementPattern)
infix fun <T> Pair<T, T>.inFile(filePath: String) {
if (first is String && second is String) {
val first = first as String
val second = second as String
replacements += Replacement(
first.toByteArray(), second.toByteArray(),
filePath
)
} else if (first is ByteArray && second is ByteArray) {
val first = first as ByteArray
val second = second as ByteArray
replacements += Replacement(first, second, filePath)
} else {
throw PatchException("Unsupported types for pattern and replacement: $first, $second")
}
}
}
// The replacements being passed using a function is intended.
// Previously the replacements were a property of the patch. Getter were being delegated to that property.
// This late evaluation was being leveraged in app.revanced.patches.all.misc.hex.HexPatch.
// Without the function, the replacements would be evaluated at the time of patch creation.
// This isn't possible because the delegated property is not accessible at that time.
@Deprecated("Use the hexPatch function with the builder parameter instead.")
fun hexPatch(ignoreMissingTargetFiles: Boolean = false, replacementsSupplier: () -> Set<Replacement>) =
rawResourcePatch {
execute {
replacementsSupplier().groupBy { it.targetFilePath }.forEach { (targetFilePath, replacements) ->
val targetFile = get(targetFilePath, true)
if (ignoreMissingTargetFiles && !targetFile.exists()) return@forEach
// TODO: Use a file channel to read and write the file instead of reading the whole file into memory,
// in order to reduce memory usage.
val targetFileBytes = targetFile.readBytes()
replacements.forEach { it.replacePattern(targetFileBytes) }
targetFile.writeBytes(targetFileBytes)
}
}
}
/**
* Represents a pattern to search for and its replacement pattern in a file.
*
* @property bytes The bytes to search for.
* @property replacementBytes The bytes to replace the [bytes] with.
* @property targetFilePath The path to the file to make the changes in relative to the APK root.
*/
class Replacement(
private val bytes: ByteArray,
replacementBytes: ByteArray,
internal val targetFilePath: String,
) {
val replacementBytesPadded = replacementBytes + ByteArray(bytes.size - replacementBytes.size)
@Deprecated("Use the constructor with ByteArray parameters instead.")
constructor(
pattern: String,
replacementPattern: String,
targetFilePath: String,
) : this(
byteArrayOf(pattern),
byteArrayOf(replacementPattern),
targetFilePath
)
/**
* Replaces the [bytes] with the [replacementBytes] in the [targetFileBytes].
*
* @param targetFileBytes The bytes of the file to make the changes in.
*/
internal fun replacePattern(targetFileBytes: ByteArray) {
val startIndex = indexOfPatternIn(targetFileBytes)
if (startIndex == -1) {
throw PatchException(
"Pattern not found in target file: " +
bytes.joinToString(" ") { "%02x".format(it) }
)
}
replacementBytesPadded.copyInto(targetFileBytes, startIndex)
}
// TODO: Allow searching in a file channel instead of a byte array to reduce memory usage.
/**
* Returns the index of the first occurrence of [bytes] in the haystack
* using the Boyer-Moore algorithm.
*
* @param haystack The array to search in.
*
* @return The index of the first occurrence of the [bytes] in the haystack or -1
* if the [bytes] is not found.
*/
private fun indexOfPatternIn(haystack: ByteArray): Int {
val needle = bytes
val haystackLength = haystack.size - 1
val needleLength = needle.size - 1
val right = IntArray(256) { -1 }
for (i in 0 until needleLength) right[needle[i].toInt().and(0xFF)] = i
var skip: Int
for (i in 0..haystackLength - needleLength) {
skip = 0
for (j in needleLength - 1 downTo 0) {
if (needle[j] != haystack[i + j]) {
skip = max(1, j - right[haystack[i + j].toInt().and(0xFF)])
break
}
}
if (skip == 0) return i
}
return -1
}
}
/**
* Convert a string representing a pattern of hexadecimal bytes to a byte array.
*
* @return The byte array representing the pattern.
* @throws PatchException If the pattern is invalid.
*/
private fun byteArrayOf(pattern: String) = try {
pattern.split(" ").map { it.toInt(16).toByte() }.toByteArray()
} catch (e: NumberFormatException) {
throw PatchException(
"Could not parse pattern: $pattern. A pattern is a sequence of case insensitive strings " +
"representing hexadecimal bytes separated by spaces",
e,
)
}

View File

@@ -1,9 +1,21 @@
package app.revanced.patches.spotify.misc.fix
import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags
internal val getPackageInfoFingerprint = fingerprint {
strings(
"Failed to get the application signatures"
)
}
internal val startLiborbitFingerprint = 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")
}

View File

@@ -0,0 +1,122 @@
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.patches.shared.misc.hex.HexPatchBuilder
import app.revanced.patches.shared.misc.hex.hexPatch
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
import app.revanced.util.findInstructionIndicesReversedOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/misc/fix/SpoofClientPatch;"
@Suppress("unused")
val spoofClientPatch = bytecodePatch(
name = "Spoof client",
description = "Spoofs the client to fix various functions of the app.",
) {
val port by intOption(
key = "port",
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,
validator = {
it!!
!(it < 0 || it > 65535)
}
)
dependsOn(
sharedExtensionPatch,
hexPatch(ignoreMissingTargetFiles = true, block = fun HexPatchBuilder.() {
listOf(
"arm64-v8a",
"armeabi-v7a",
"x86",
"x86_64"
).forEach { architecture ->
"https://login5.spotify.com/v3/login" to "http://127.0.0.1:$port/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
"lib/$architecture/liborbit-jni-spotify.so"
}
})
)
compatibleWith("com.spotify.music")
execute {
getPackageInfoFingerprint.method.apply {
// region Spoof signature.
val failedToGetSignaturesStringIndex =
getPackageInfoFingerprint.stringMatches!!.first().index
val concatSignaturesIndex = indexOfFirstInstructionReversedOrThrow(
failedToGetSignaturesStringIndex,
Opcode.MOVE_RESULT_OBJECT,
)
val signatureRegister = getInstruction<OneRegisterInstruction>(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<MethodReference>()
reference?.name == "getInstallerPackageName" || reference?.name == "getInstallingPackageName"
}.forEach { index ->
val returnObjectIndex = index + 1
val installerPackageNameRegister = getInstruction<OneRegisterInstruction>(
returnObjectIndex
).registerA
addInstruction(
returnObjectIndex + 1,
"const-string v$installerPackageNameRegister, \"$expectedInstallerName\""
)
}
// endregion
}
startLiborbitFingerprint.method.addInstructions(
0,
"""
const/16 v0, $port
invoke-static { v0 }, $EXTENSION_CLASS_DESCRIPTOR->listen(I)V
"""
)
startupPageLayoutInflateFingerprint.method.apply {
val openLoginWebViewDescriptor =
"$EXTENSION_CLASS_DESCRIPTOR->login(Landroid/view/LayoutInflater;)V"
addInstructions(
0,
"""
move-object/from16 v3, p1
invoke-static { v3 }, $openLoginWebViewDescriptor
"""
)
}
}
}

View File

@@ -1,63 +1,11 @@
package app.revanced.patches.spotify.misc.fix
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.findInstructionIndicesReversedOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
@Deprecated("Superseded by spoofClientPatch", ReplaceWith("spoofClientPatch"))
@Suppress("unused")
val spoofPackageInfoPatch = bytecodePatch(
name = "Spoof package info",
description = "Spoofs the package info of the app to fix various functions of the app.",
) {
compatibleWith("com.spotify.music")
execute {
getPackageInfoFingerprint.method.apply {
// region Spoof signature.
val failedToGetSignaturesStringIndex =
getPackageInfoFingerprint.stringMatches!!.first().index
val concatSignaturesIndex = indexOfFirstInstructionReversedOrThrow(
failedToGetSignaturesStringIndex,
Opcode.MOVE_RESULT_OBJECT,
)
val signatureRegister = getInstruction<OneRegisterInstruction>(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<MethodReference>()
reference?.name == "getInstallerPackageName" || reference?.name == "getInstallingPackageName"
}.forEach { index ->
val returnObjectIndex = index + 1
val installerPackageNameRegister = getInstruction<OneRegisterInstruction>(
returnObjectIndex
).registerA
addInstruction(
returnObjectIndex + 1,
"const-string v$installerPackageNameRegister, \"$expectedInstallerName\""
)
}
// endregion
}
}
dependsOn(spoofClientPatch)
}

View File

@@ -2,10 +2,10 @@ package app.revanced.patches.spotify.misc.fix
import app.revanced.patcher.patch.bytecodePatch
@Deprecated("Superseded by spoofPackageInfoPatch", ReplaceWith("spoofPackageInfoPatch"))
@Deprecated("Superseded by spoofClientPatch", ReplaceWith("spoofClientPatch"))
@Suppress("unused")
val spoofSignaturePatch = bytecodePatch(
description = "Spoofs the signature of the app fix various functions of the app.",
) {
dependsOn(spoofPackageInfoPatch)
dependsOn(spoofClientPatch)
}

View File

@@ -82,6 +82,7 @@ val hideAdsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -30,6 +30,7 @@ val hideGetPremiumPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -28,6 +28,7 @@ val videoAdsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -58,6 +58,7 @@ val copyVideoUrlPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -29,6 +29,7 @@ val removeViewerDiscretionDialogPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -77,6 +77,7 @@ val downloadsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -25,6 +25,7 @@ val seekbarPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)
}

View File

@@ -93,6 +93,7 @@ val swipeControlsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -29,6 +29,7 @@ val autoCaptionsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -48,6 +48,7 @@ val customBrandingPatch = resourcePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -46,6 +46,7 @@ val changeHeaderPatch = resourcePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -27,6 +27,7 @@ val hideButtonsPatch = resourcePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -45,6 +45,7 @@ val navigationButtonsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -63,6 +63,7 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -38,6 +38,7 @@ val changeFormFactorPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -64,6 +64,7 @@ val hideEndscreenCardsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -36,6 +36,7 @@ val hideEndScreenSuggestedVideoPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -34,6 +34,7 @@ val disableFullscreenAmbientModePatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -130,6 +130,7 @@ val hideLayoutComponentsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -62,6 +62,7 @@ val hideInfoCardsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -29,6 +29,7 @@ val hidePlayerFlyoutMenuPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -53,6 +53,7 @@ val hideRelatedVideoOverlayPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -34,6 +34,7 @@ val disableRollingNumberAnimationPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -97,6 +97,7 @@ private val hideShortsComponentsResourcePatch = resourcePatch {
SwitchPreference("revanced_hide_shorts_use_sound_button"),
SwitchPreference("revanced_hide_shorts_use_template_button"),
SwitchPreference("revanced_hide_shorts_upcoming_button"),
SwitchPreference("revanced_hide_shorts_effect_button"),
SwitchPreference("revanced_hide_shorts_green_screen_button"),
SwitchPreference("revanced_hide_shorts_hashtag_button"),
SwitchPreference("revanced_hide_shorts_new_posts_button"),
@@ -176,6 +177,7 @@ val hideShortsComponentsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -26,6 +26,7 @@ val hideTimestampPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -158,6 +158,7 @@ val miniplayerPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -26,6 +26,7 @@ val playerPopupPanelsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -26,6 +26,7 @@ internal val exitFullscreenPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -28,6 +28,7 @@ val openVideosFullscreenPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -57,6 +57,7 @@ val customPlayerOverlayOpacityPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -66,6 +66,7 @@ val returnYouTubeDislikePatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -71,6 +71,7 @@ val wideSearchbarPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -49,6 +49,7 @@ val shortsAutoplayPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -69,6 +69,7 @@ val openShortsInRegularPlayerPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -131,6 +131,7 @@ val sponsorBlockPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -63,6 +63,7 @@ val spoofAppVersionPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -37,6 +37,7 @@ val changeStartPagePatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -39,6 +39,7 @@ val disableResumingShortsOnStartupPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -209,6 +209,7 @@ val themePatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -38,6 +38,7 @@ val alternativeThumbnailsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -32,6 +32,7 @@ val bypassImageRegionRestrictionsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -28,6 +28,7 @@ val announcementsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -29,6 +29,7 @@ val autoRepeatPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -56,6 +56,7 @@ val backgroundPlaybackPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -40,6 +40,7 @@ val enableDebuggingPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -29,6 +29,7 @@ val spoofDeviceDimensionsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -26,6 +26,7 @@ val checkWatchHistoryDomainNameResolutionPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -40,6 +40,7 @@ val gmsCoreSupportPatch = gmsCoreSupportPatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)
}

View File

@@ -31,6 +31,7 @@ val disableHapticFeedbackPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -37,6 +37,7 @@ val bypassURLRedirectsPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -46,6 +46,7 @@ val openLinksExternallyPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -35,6 +35,7 @@ val removeTrackingQueryParameterPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -22,6 +22,7 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch({
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -46,6 +46,7 @@ val forceOriginalAudioPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -33,6 +33,7 @@ val disableHdrPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -28,6 +28,7 @@ val videoQualityPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -33,6 +33,7 @@ val playbackSpeedPatch = bytecodePatch(
"19.47.53",
"20.07.39",
"20.12.46",
"20.13.41",
)
)

View File

@@ -773,6 +773,9 @@ Second \"item\" text"</string>
<string name="revanced_hide_shorts_upcoming_button_title">إخفاء زر القادم</string>
<string name="revanced_hide_shorts_upcoming_button_summary_on">تم إخفاء زر القادم</string>
<string name="revanced_hide_shorts_upcoming_button_summary_off">يتم عرض زر القادم</string>
<string name="revanced_hide_shorts_effect_button_title">إخفاء زر التأثير</string>
<string name="revanced_hide_shorts_effect_button_summary_on">زر التأثير مخفي</string>
<string name="revanced_hide_shorts_effect_button_summary_off">زر التأثير معروض</string>
<string name="revanced_hide_shorts_green_screen_button_title">إخفاء زر الشاشة الخضراء</string>
<string name="revanced_hide_shorts_green_screen_button_summary_on">تم إخفاء زر الشاشة الخضراء</string>
<string name="revanced_hide_shorts_green_screen_button_summary_off">يتم عرض زر الشاشة الخضراء</string>

View File

@@ -773,6 +773,9 @@ Audio trek seçimin göstərmək üçün \"Video axınları saxtalaşdır\"ı iO
<string name="revanced_hide_shorts_upcoming_button_title">Yaxınlaşan düyməsini gizlət</string>
<string name="revanced_hide_shorts_upcoming_button_summary_on">\"Yaxınlaşan\" düyməsi gizlidir</string>
<string name="revanced_hide_shorts_upcoming_button_summary_off">\"Yaxınlaşan\" düyməsi göstərilir</string>
<string name="revanced_hide_shorts_effect_button_title">Effekt düyməsini gizlət</string>
<string name="revanced_hide_shorts_effect_button_summary_on">Effekt düyməsi gizlidir</string>
<string name="revanced_hide_shorts_effect_button_summary_off">Effekt düyməsi görünür</string>
<string name="revanced_hide_shorts_green_screen_button_title">Yaşıl ekran düyməsini gizlət</string>
<string name="revanced_hide_shorts_green_screen_button_summary_on">\"Yaşıl ekran\" düyməsi gizlidir</string>
<string name="revanced_hide_shorts_green_screen_button_summary_off">\"Yaşıl ekran\" düyməsi göstərilir</string>

View File

@@ -773,6 +773,9 @@ Second \"item\" text"</string>
<string name="revanced_hide_shorts_upcoming_button_title">Схаваць кнопку «Наступныя»</string>
<string name="revanced_hide_shorts_upcoming_button_summary_on">Кнопка Будущие ролики скрыта</string>
<string name="revanced_hide_shorts_upcoming_button_summary_off">Кнопка Будущие ролики отображается</string>
<string name="revanced_hide_shorts_effect_button_title">Схаваць кнопку эфекту</string>
<string name="revanced_hide_shorts_effect_button_summary_on">Кнопка эфекту схавана</string>
<string name="revanced_hide_shorts_effect_button_summary_off">Кнопка эфекту паказана</string>
<string name="revanced_hide_shorts_green_screen_button_title">Схаваць кнопку «Зялёны экран»</string>
<string name="revanced_hide_shorts_green_screen_button_summary_on">Кнопка с зелёным экраном Shorts скрыта</string>
<string name="revanced_hide_shorts_green_screen_button_summary_off">Кнопка с зелёным экраном Shorts отображается</string>

View File

@@ -773,6 +773,9 @@ Second \"item\" text"</string>
<string name="revanced_hide_shorts_upcoming_button_title">Скриване на бутона Upcoming</string>
<string name="revanced_hide_shorts_upcoming_button_summary_on">Бутон \"Предстоящи събития\" е скрит</string>
<string name="revanced_hide_shorts_upcoming_button_summary_off">Бутон \"Предстоящи събития\" се показва</string>
<string name="revanced_hide_shorts_effect_button_title">Скрий бутона за ефект</string>
<string name="revanced_hide_shorts_effect_button_summary_on">Бутонът за ефекти е скрит</string>
<string name="revanced_hide_shorts_effect_button_summary_off">Бутонът за ефекти е видим</string>
<string name="revanced_hide_shorts_green_screen_button_title">Скриване на бутона Green screen</string>
<string name="revanced_hide_shorts_green_screen_button_summary_on">Бутон \"Зелен екран\" е скрит</string>
<string name="revanced_hide_shorts_green_screen_button_summary_off">Бутон \"Зелен екран\" се показва</string>

Some files were not shown because too many files have changed in this diff Show More