mirror of
https://github.com/ReVanced/revanced-patches.git
synced 2026-01-21 18:03:56 +00:00
Compare commits
41 Commits
v5.29.0-de
...
v5.30.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e52ee41222 | ||
|
|
6ee94f8532 | ||
|
|
21688201af | ||
|
|
f08474369b | ||
|
|
ed617094ea | ||
|
|
9131c50f1b | ||
|
|
69600d08a4 | ||
|
|
5dba77612b | ||
|
|
92b588c866 | ||
|
|
da20e565cd | ||
|
|
ca694c78d2 | ||
|
|
e169056b70 | ||
|
|
b6bf1e026c | ||
|
|
9fa89d48c0 | ||
|
|
5d2c21540c | ||
|
|
1a8aacdff6 | ||
|
|
1804bd9bfc | ||
|
|
7eb4e62762 | ||
|
|
b8e10b5c1f | ||
|
|
a7c11b9b08 | ||
|
|
443c0a74d5 | ||
|
|
84a0f7f7d7 | ||
|
|
558bf8bca8 | ||
|
|
e22d4e6a4b | ||
|
|
a07f946633 | ||
|
|
29c86ac6a3 | ||
|
|
19cf5667d8 | ||
|
|
fb83e58f79 | ||
|
|
9844081d04 | ||
|
|
439ca37e99 | ||
|
|
113a3d9f19 | ||
|
|
978c24458b | ||
|
|
957bece3e9 | ||
|
|
d32c3ac51d | ||
|
|
26102a70a2 | ||
|
|
2b44bf4c23 | ||
|
|
0e63f49e13 | ||
|
|
674a5b8d29 | ||
|
|
7be374100b | ||
|
|
e48c152b95 | ||
|
|
a678f178e1 |
125
CHANGELOG.md
125
CHANGELOG.md
@@ -1,3 +1,128 @@
|
|||||||
|
# [5.30.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.3...v5.30.0-dev.4) (2025-06-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - SponsorBlock:** Add "Undo automatic skip toast" ([#5277](https://github.com/ReVanced/revanced-patches/issues/5277)) ([7fa169a](https://github.com/ReVanced/revanced-patches/commit/7fa169ae262c880019c5a069a2d6bdc7f94885f1))
|
||||||
|
|
||||||
|
# [5.30.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.2...v5.30.0-dev.3) (2025-06-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Hide layout components:** Fix "Hide AI Comments summary" in Comments ([#5284](https://github.com/ReVanced/revanced-patches/issues/5284)) ([d42370e](https://github.com/ReVanced/revanced-patches/commit/d42370ef71f4608abc64b6ef4a3fb0c5bd5e3eb6))
|
||||||
|
|
||||||
|
# [5.30.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.1...v5.30.0-dev.2) (2025-06-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Spotify - Spoof client patch:** Block sending bad integrity verdicts to potentially fix account suspensions ([#5274](https://github.com/ReVanced/revanced-patches/issues/5274)) ([f7b574c](https://github.com/ReVanced/revanced-patches/commit/f7b574ca79c5a616cfe33a3fc75bd8cf68571f7d))
|
||||||
|
|
||||||
|
# [5.30.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.29.1-dev.1...v5.30.0-dev.1) (2025-06-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Hide ads:** Fix "Hide shopping links" ([#5267](https://github.com/ReVanced/revanced-patches/issues/5267)) ([2fe4607](https://github.com/ReVanced/revanced-patches/commit/2fe46079d78ab98076d3a4cdf01c8bfdbdea45c0))
|
||||||
|
* **YouTube - Hide layout components:** Fix "Hide AI-generated video summary" in video description ([#5269](https://github.com/ReVanced/revanced-patches/issues/5269)) ([5203da0](https://github.com/ReVanced/revanced-patches/commit/5203da0ae58e467657bc915ab0af5b9904c4f492))
|
||||||
|
* **YouTube - Hide Shorts components:** Fix hiding of untoggled components ([#5266](https://github.com/ReVanced/revanced-patches/issues/5266)) ([008e192](https://github.com/ReVanced/revanced-patches/commit/008e192779a8658e894d5718baa732717bf96e40))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Spotify:** Remove ads section from browse ([#5193](https://github.com/ReVanced/revanced-patches/issues/5193)) ([ebd4dcc](https://github.com/ReVanced/revanced-patches/commit/ebd4dccf12a5fbd31d2d53c19a792c389a4641d7))
|
||||||
|
* **YouTube - Hide layout components:** Add `Hide in history` option to filter bar ([#5271](https://github.com/ReVanced/revanced-patches/issues/5271)) ([ba242a3](https://github.com/ReVanced/revanced-patches/commit/ba242a36b040b82e84870e5e240734637125a472))
|
||||||
|
|
||||||
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **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 template button" ([#5249](https://github.com/ReVanced/revanced-patches/issues/5249)) ([b399ecb](https://github.com/ReVanced/revanced-patches/commit/b399ecbb6a222d82dd5e4b3417c9f7eff4324adb))
|
||||||
|
|
||||||
|
# [5.29.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.5...v5.29.0-dev.6) (2025-06-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **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))
|
||||||
|
|
||||||
|
# [5.29.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.4...v5.29.0-dev.5) (2025-06-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Google Photos:** Resolve startup crash for Android 5.0 devices ([0294533](https://github.com/ReVanced/revanced-patches/commit/0294533c4d9a321aea086eedb4e46385ae9a026e))
|
||||||
|
|
||||||
|
# [5.29.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.3...v5.29.0-dev.4) (2025-06-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **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))
|
||||||
|
|
||||||
# [5.29.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.2...v5.29.0-dev.3) (2025-06-23)
|
# [5.29.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.2...v5.29.0-dev.3) (2025-06-23)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
3
build.gradle.kts
Normal file
3
build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.library) apply false
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.library")
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import android.util.Pair;
|
|||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
@@ -28,7 +27,6 @@ import java.util.Locale;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.requests.Requester;
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
import app.revanced.extension.shared.requests.Route;
|
import app.revanced.extension.shared.requests.Route;
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class GmsCoreSupport {
|
public class GmsCoreSupport {
|
||||||
@@ -109,7 +107,6 @@ public class GmsCoreSupport {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
||||||
public static void checkGmsCore(Activity context) {
|
public static void checkGmsCore(Activity context) {
|
||||||
try {
|
try {
|
||||||
// Verify the user has not included GmsCore for a root installation.
|
// Verify the user has not included GmsCore for a root installation.
|
||||||
@@ -157,7 +154,9 @@ public class GmsCoreSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if GmsCore is currently running in the background.
|
// Check if GmsCore is currently running in the background.
|
||||||
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
|
var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER);
|
||||||
|
//noinspection TryFinallyCanBeTryWithResources
|
||||||
|
try {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
Logger.printInfo(() -> "GmsCore is not running in the background");
|
Logger.printInfo(() -> "GmsCore is not running in the background");
|
||||||
checkIfDontKillMyAppSupportsManufacturer();
|
checkIfDontKillMyAppSupportsManufacturer();
|
||||||
@@ -167,6 +166,8 @@ public class GmsCoreSupport {
|
|||||||
"gms_core_dialog_open_website_text",
|
"gms_core_dialog_open_website_text",
|
||||||
(dialog, id) -> openDontKillMyApp());
|
(dialog, id) -> openDontKillMyApp());
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (client != null) client.close();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "checkGmsCore failure", ex);
|
Logger.printException(() -> "checkGmsCore failure", ex);
|
||||||
@@ -226,6 +227,11 @@ public class GmsCoreSupport {
|
|||||||
* @return If GmsCore is not whitelisted from battery optimizations.
|
* @return If GmsCore is not whitelisted from battery optimizations.
|
||||||
*/
|
*/
|
||||||
private static boolean batteryOptimizationsEnabled(Context context) {
|
private static boolean batteryOptimizationsEnabled(Context context) {
|
||||||
|
//noinspection ObsoleteSdkInt
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||||
|
// Android 5.0 does not have battery optimization settings.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||||
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
|
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,6 +311,10 @@ public class Utils {
|
|||||||
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
|
return getContext().getResources().getStringArray(getResourceIdentifier(resourceIdentifierName, "array"));
|
||||||
|
}
|
||||||
|
|
||||||
public interface MatchFilter<T> {
|
public interface MatchFilter<T> {
|
||||||
boolean matches(T object);
|
boolean matches(T object);
|
||||||
}
|
}
|
||||||
@@ -579,7 +583,7 @@ public class Utils {
|
|||||||
Context currentContext = context;
|
Context currentContext = context;
|
||||||
|
|
||||||
if (currentContext == null) {
|
if (currentContext == null) {
|
||||||
Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast, null);
|
Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast);
|
||||||
} else {
|
} else {
|
||||||
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
||||||
Toast.makeText(currentContext, messageToToast, toastDuration).show();
|
Toast.makeText(currentContext, messageToToast, toastDuration).show();
|
||||||
@@ -809,7 +813,7 @@ public class Utils {
|
|||||||
|
|
||||||
// Create content container (message/EditText) inside a ScrollView only if message or editText is provided.
|
// Create content container (message/EditText) inside a ScrollView only if message or editText is provided.
|
||||||
ScrollView contentScrollView = null;
|
ScrollView contentScrollView = null;
|
||||||
LinearLayout contentContainer = null;
|
LinearLayout contentContainer;
|
||||||
if (message != null || editText != null) {
|
if (message != null || editText != null) {
|
||||||
contentScrollView = new ScrollView(context);
|
contentScrollView = new ScrollView(context);
|
||||||
contentScrollView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar.
|
contentScrollView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar.
|
||||||
@@ -833,7 +837,7 @@ public class Utils {
|
|||||||
contentScrollView.addView(contentContainer);
|
contentScrollView.addView(contentContainer);
|
||||||
|
|
||||||
// Message (if not replaced by EditText).
|
// Message (if not replaced by EditText).
|
||||||
if (editText == null && message != null) {
|
if (editText == null) {
|
||||||
TextView messageView = new TextView(context);
|
TextView messageView = new TextView(context);
|
||||||
messageView.setText(message); // Supports Spanned (HTML).
|
messageView.setText(message); // Supports Spanned (HTML).
|
||||||
messageView.setTextSize(16);
|
messageView.setTextSize(16);
|
||||||
|
|||||||
@@ -71,15 +71,20 @@ public class EnumSetting<T extends Enum<?>> extends Setting<T> {
|
|||||||
json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH));
|
json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
/**
|
||||||
private T getEnumFromString(String enumName) {
|
* @param enumName Enum name. Casing does not matter.
|
||||||
|
* @return Enum of this type with the same declared name.
|
||||||
|
* @throws IllegalArgumentException if the name is not a valid enum of this type.
|
||||||
|
*/
|
||||||
|
protected T getEnumFromString(String enumName) {
|
||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
for (Enum<?> value : defaultValue.getClass().getEnumConstants()) {
|
for (Enum<?> value : defaultValue.getClass().getEnumConstants()) {
|
||||||
if (value.name().equalsIgnoreCase(enumName)) {
|
if (value.name().equalsIgnoreCase(enumName)) {
|
||||||
// noinspection unchecked
|
//noinspection unchecked
|
||||||
return (T) value;
|
return (T) value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException("Unknown enum value: " + enumName);
|
throw new IllegalArgumentException("Unknown enum value: " + enumName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +108,9 @@ public class EnumSetting<T extends Enum<?>> extends Setting<T> {
|
|||||||
* Availability based on if this setting is currently set to any of the provided types.
|
* Availability based on if this setting is currently set to any of the provided types.
|
||||||
*/
|
*/
|
||||||
@SafeVarargs
|
@SafeVarargs
|
||||||
public final Setting.Availability availability(@NonNull T... types) {
|
public final Setting.Availability availability(T... types) {
|
||||||
|
Objects.requireNonNull(types);
|
||||||
|
|
||||||
return () -> {
|
return () -> {
|
||||||
T currentEnumType = get();
|
T currentEnumType = get();
|
||||||
for (T enumType : types) {
|
for (T enumType : types) {
|
||||||
|
|||||||
@@ -28,16 +28,14 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Availability based on a single parent setting being enabled.
|
* Availability based on a single parent setting being enabled.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
public static Availability parent(BooleanSetting parent) {
|
||||||
public static Availability parent(@NonNull BooleanSetting parent) {
|
|
||||||
return parent::get;
|
return parent::get;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Availability based on all parents being enabled.
|
* Availability based on all parents being enabled.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
public static Availability parentsAll(BooleanSetting... parents) {
|
||||||
public static Availability parentsAll(@NonNull BooleanSetting... parents) {
|
|
||||||
return () -> {
|
return () -> {
|
||||||
for (BooleanSetting parent : parents) {
|
for (BooleanSetting parent : parents) {
|
||||||
if (!parent.get()) return false;
|
if (!parent.get()) return false;
|
||||||
@@ -49,8 +47,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Availability based on any parent being enabled.
|
* Availability based on any parent being enabled.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
public static Availability parentsAny(BooleanSetting... parents) {
|
||||||
public static Availability parentsAny(@NonNull BooleanSetting... parents) {
|
|
||||||
return () -> {
|
return () -> {
|
||||||
for (BooleanSetting parent : parents) {
|
for (BooleanSetting parent : parents) {
|
||||||
if (parent.get()) return true;
|
if (parent.get()) return true;
|
||||||
@@ -79,7 +76,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}.
|
* Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}.
|
||||||
*/
|
*/
|
||||||
public static void addImportExportCallback(@NonNull ImportExportCallback callback) {
|
public static void addImportExportCallback(ImportExportCallback callback) {
|
||||||
importExportCallbacks.add(Objects.requireNonNull(callback));
|
importExportCallbacks.add(Objects.requireNonNull(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,14 +97,13 @@ public abstract class Setting<T> {
|
|||||||
public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs");
|
public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs");
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static Setting<?> getSettingFromPath(@NonNull String str) {
|
public static Setting<?> getSettingFromPath(String str) {
|
||||||
return PATH_TO_SETTINGS.get(str);
|
return PATH_TO_SETTINGS.get(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return All settings that have been created.
|
* @return All settings that have been created.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public static List<Setting<?>> allLoadedSettings() {
|
public static List<Setting<?>> allLoadedSettings() {
|
||||||
return Collections.unmodifiableList(SETTINGS);
|
return Collections.unmodifiableList(SETTINGS);
|
||||||
}
|
}
|
||||||
@@ -115,7 +111,6 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* @return All settings that have been created, sorted by keys.
|
* @return All settings that have been created, sorted by keys.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
private static List<Setting<?>> allLoadedSettingsSorted() {
|
private static List<Setting<?>> allLoadedSettingsSorted() {
|
||||||
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
|
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
|
||||||
return allLoadedSettings();
|
return allLoadedSettings();
|
||||||
@@ -124,13 +119,11 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* The key used to store the value in the shared preferences.
|
* The key used to store the value in the shared preferences.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public final String key;
|
public final String key;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default value of the setting.
|
* The default value of the setting.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public final T defaultValue;
|
public final T defaultValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,7 +154,6 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* The value of the setting.
|
* The value of the setting.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
protected volatile T value;
|
protected volatile T value;
|
||||||
|
|
||||||
public Setting(String key, T defaultValue) {
|
public Setting(String key, T defaultValue) {
|
||||||
@@ -199,8 +191,8 @@ public abstract class Setting<T> {
|
|||||||
* @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value.
|
* @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value.
|
||||||
* @param availability Condition that must be true, for this setting to be available to configure.
|
* @param availability Condition that must be true, for this setting to be available to configure.
|
||||||
*/
|
*/
|
||||||
public Setting(@NonNull String key,
|
public Setting(String key,
|
||||||
@NonNull T defaultValue,
|
T defaultValue,
|
||||||
boolean rebootApp,
|
boolean rebootApp,
|
||||||
boolean includeWithImportExport,
|
boolean includeWithImportExport,
|
||||||
@Nullable String userDialogMessage,
|
@Nullable String userDialogMessage,
|
||||||
@@ -227,7 +219,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical.
|
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical.
|
||||||
*/
|
*/
|
||||||
public static <T> void migrateOldSettingToNew(@NonNull Setting<T> oldSetting, @NonNull Setting<T> newSetting) {
|
public static <T> void migrateOldSettingToNew(Setting<T> oldSetting, Setting<T> newSetting) {
|
||||||
if (oldSetting == newSetting) throw new IllegalArgumentException();
|
if (oldSetting == newSetting) throw new IllegalArgumentException();
|
||||||
|
|
||||||
if (!oldSetting.isSetToDefault()) {
|
if (!oldSetting.isSetToDefault()) {
|
||||||
@@ -243,7 +235,7 @@ public abstract class Setting<T> {
|
|||||||
* This method will be deleted in the future.
|
* This method will be deleted in the future.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) {
|
public static void migrateFromOldPreferences(SharedPrefCategory oldPrefs, Setting setting, String settingKey) {
|
||||||
if (!oldPrefs.preferences.contains(settingKey)) {
|
if (!oldPrefs.preferences.contains(settingKey)) {
|
||||||
return; // Nothing to do.
|
return; // Nothing to do.
|
||||||
}
|
}
|
||||||
@@ -285,7 +277,7 @@ public abstract class Setting<T> {
|
|||||||
* This intentionally is a static method to deter
|
* This intentionally is a static method to deter
|
||||||
* accidental usage when {@link #save(Object)} was intended.
|
* accidental usage when {@link #save(Object)} was intended.
|
||||||
*/
|
*/
|
||||||
public static void privateSetValueFromString(@NonNull Setting<?> setting, @NonNull String newValue) {
|
public static void privateSetValueFromString(Setting<?> setting, String newValue) {
|
||||||
setting.setValueFromString(newValue);
|
setting.setValueFromString(newValue);
|
||||||
|
|
||||||
// Clear the preference value since default is used, to allow changing
|
// Clear the preference value since default is used, to allow changing
|
||||||
@@ -299,7 +291,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Sets the value of {@link #value}, but do not save to {@link #preferences}.
|
* Sets the value of {@link #value}, but do not save to {@link #preferences}.
|
||||||
*/
|
*/
|
||||||
protected abstract void setValueFromString(@NonNull String newValue);
|
protected abstract void setValueFromString(String newValue);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load and set the value of {@link #value}.
|
* Load and set the value of {@link #value}.
|
||||||
@@ -309,7 +301,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Persistently saves the value.
|
* Persistently saves the value.
|
||||||
*/
|
*/
|
||||||
public final void save(@NonNull T newValue) {
|
public final void save(T newValue) {
|
||||||
if (value.equals(newValue)) {
|
if (value.equals(newValue)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -406,7 +398,6 @@ public abstract class Setting<T> {
|
|||||||
json.put(importExportKey, value);
|
json.put(importExportKey, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static String exportToJson(@Nullable Context alertDialogContext) {
|
public static String exportToJson(@Nullable Context alertDialogContext) {
|
||||||
try {
|
try {
|
||||||
JSONObject json = new JSONObject();
|
JSONObject json = new JSONObject();
|
||||||
@@ -445,7 +436,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* @return if any settings that require a reboot were changed.
|
* @return if any settings that require a reboot were changed.
|
||||||
*/
|
*/
|
||||||
public static boolean importFromJSON(@NonNull Context alertDialogContext, @NonNull String settingsJsonString) {
|
public static boolean importFromJSON(Context alertDialogContext, String settingsJsonString) {
|
||||||
try {
|
try {
|
||||||
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
|
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
|
||||||
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
|
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.protobuf)
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":extensions:shared:library"))
|
compileOnly(project(":extensions:shared:library"))
|
||||||
compileOnly(project(":extensions:spotify:stub"))
|
compileOnly(project(":extensions:spotify:stub"))
|
||||||
compileOnly(libs.annotation)
|
compileOnly(libs.annotation)
|
||||||
|
|
||||||
|
implementation(project(":extensions:spotify:utils"))
|
||||||
|
implementation(libs.nanohttpd)
|
||||||
|
implementation(libs.protobuf.javalite)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -14,3 +22,19 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = libs.protobuf.protoc.get().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
generateProtoTasks {
|
||||||
|
all().forEach { task ->
|
||||||
|
task.builtins {
|
||||||
|
create("java") {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 +
|
||||||
|
')';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
extensions/spotify/src/main/proto/login5.proto
Normal file
43
extensions/spotify/src/main/proto/login5.proto
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.spotify.browsita.v1.resolved;
|
||||||
|
|
||||||
|
public final class Section {
|
||||||
|
public static final int BRAND_ADS_FIELD_NUMBER = 6;
|
||||||
|
public int sectionTypeCase_;
|
||||||
|
}
|
||||||
19
extensions/spotify/utils/build.gradle.kts
Normal file
19
extensions/spotify/utils/build.gradle.kts
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]+
|
||||||
|
;
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
android.namespace = "app.revanced.extension"
|
android.namespace = "app.revanced.extension"
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -116,18 +116,17 @@ public final class AdsFilter extends Filter {
|
|||||||
|
|
||||||
shoppingLinks = new StringFilterGroup(
|
shoppingLinks = new StringFilterGroup(
|
||||||
Settings.HIDE_SHOPPING_LINKS,
|
Settings.HIDE_SHOPPING_LINKS,
|
||||||
"expandable_list"
|
"expandable_list",
|
||||||
|
"shopping_description_shelf.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
playerShoppingShelf = new StringFilterGroup(
|
playerShoppingShelf = new StringFilterGroup(
|
||||||
Settings.HIDE_PLAYER_STORE_SHELF,
|
Settings.HIDE_PLAYER_STORE_SHELF,
|
||||||
"expandable_list.eml",
|
|
||||||
"horizontal_shelf.eml"
|
"horizontal_shelf.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
|
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
|
||||||
null,
|
null,
|
||||||
"shopping_link_item",
|
|
||||||
"shopping_item_card_list"
|
"shopping_item_card_list"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ final class ButtonsFilter extends Filter {
|
|||||||
"|download_button.eml"
|
"|download_button.eml"
|
||||||
),
|
),
|
||||||
new StringFilterGroup(
|
new StringFilterGroup(
|
||||||
Settings.HIDE_PLAYLIST_BUTTON,
|
Settings.HIDE_SAVE_BUTTON,
|
||||||
"|save_to_playlist_button"
|
"|save_to_playlist_button"
|
||||||
),
|
),
|
||||||
new StringFilterGroup(
|
new StringFilterGroup(
|
||||||
@@ -76,6 +76,10 @@ final class ButtonsFilter extends Filter {
|
|||||||
Settings.HIDE_ASK_BUTTON,
|
Settings.HIDE_ASK_BUTTON,
|
||||||
"yt_fill_spark"
|
"yt_fill_spark"
|
||||||
),
|
),
|
||||||
|
new ByteArrayFilterGroup(
|
||||||
|
Settings.HIDE_STOP_ADS_BUTTON,
|
||||||
|
"yt_outline_slash_circle_left"
|
||||||
|
),
|
||||||
// Check for clip button both here and using a path filter,
|
// Check for clip button both here and using a path filter,
|
||||||
// as there's a chance the path is a generic action button and won't contain 'clip_button'
|
// as there's a chance the path is a generic action button and won't contain 'clip_button'
|
||||||
new ByteArrayFilterGroup(
|
new ByteArrayFilterGroup(
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ final class CommentsFilter extends Filter {
|
|||||||
|
|
||||||
filterChipBar = new StringFilterGroup(
|
filterChipBar = new StringFilterGroup(
|
||||||
Settings.HIDE_COMMENTS_AI_SUMMARY,
|
Settings.HIDE_COMMENTS_AI_SUMMARY,
|
||||||
"filter_chip_bar.eml"
|
"chip_bar.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
aiCommentsSummary = new ByteArrayFilterGroup(
|
aiCommentsSummary = new ByteArrayFilterGroup(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import app.revanced.extension.youtube.StringTrieSearch;
|
import app.revanced.extension.youtube.StringTrieSearch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
final class DescriptionComponentsFilter extends Filter {
|
final class DescriptionComponentsFilter extends Filter {
|
||||||
@@ -14,6 +15,11 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
|
|
||||||
private final StringFilterGroup macroMarkersCarousel;
|
private final StringFilterGroup macroMarkersCarousel;
|
||||||
|
|
||||||
|
private final StringFilterGroup horizontalShelf;
|
||||||
|
private final ByteArrayFilterGroup cellVideoAttribute;
|
||||||
|
|
||||||
|
private final StringFilterGroup aiGeneratedVideoSummarySection;
|
||||||
|
|
||||||
public DescriptionComponentsFilter() {
|
public DescriptionComponentsFilter() {
|
||||||
exceptions.addPatterns(
|
exceptions.addPatterns(
|
||||||
"compact_channel",
|
"compact_channel",
|
||||||
@@ -23,7 +29,7 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
"metadata"
|
"metadata"
|
||||||
);
|
);
|
||||||
|
|
||||||
final StringFilterGroup aiGeneratedVideoSummarySection = new StringFilterGroup(
|
aiGeneratedVideoSummarySection = new StringFilterGroup(
|
||||||
Settings.HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION,
|
Settings.HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION,
|
||||||
"cell_expandable_metadata.eml"
|
"cell_expandable_metadata.eml"
|
||||||
);
|
);
|
||||||
@@ -35,8 +41,7 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
|
|
||||||
final StringFilterGroup attributesSection = new StringFilterGroup(
|
final StringFilterGroup attributesSection = new StringFilterGroup(
|
||||||
Settings.HIDE_ATTRIBUTES_SECTION,
|
Settings.HIDE_ATTRIBUTES_SECTION,
|
||||||
"gaming_section",
|
// "gaming_section", "music_section"
|
||||||
"music_section",
|
|
||||||
"video_attributes_section"
|
"video_attributes_section"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -76,27 +81,48 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
horizontalShelf = new StringFilterGroup(
|
||||||
|
Settings.HIDE_ATTRIBUTES_SECTION,
|
||||||
|
"horizontal_shelf.eml"
|
||||||
|
);
|
||||||
|
|
||||||
|
cellVideoAttribute = new ByteArrayFilterGroup(
|
||||||
|
null,
|
||||||
|
"cell_video_attribute"
|
||||||
|
);
|
||||||
|
|
||||||
addPathCallbacks(
|
addPathCallbacks(
|
||||||
aiGeneratedVideoSummarySection,
|
aiGeneratedVideoSummarySection,
|
||||||
askSection,
|
askSection,
|
||||||
attributesSection,
|
attributesSection,
|
||||||
infoCardsSection,
|
infoCardsSection,
|
||||||
|
horizontalShelf,
|
||||||
howThisWasMadeSection,
|
howThisWasMadeSection,
|
||||||
|
macroMarkersCarousel,
|
||||||
podcastSection,
|
podcastSection,
|
||||||
transcriptSection,
|
transcriptSection
|
||||||
macroMarkersCarousel
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
|
|
||||||
|
if (matchedGroup == aiGeneratedVideoSummarySection) {
|
||||||
|
// Only hide if player is open, in case this component is used somewhere else.
|
||||||
|
return PlayerType.getCurrent().isMaximizedOrFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
if (exceptions.matches(path)) return false;
|
if (exceptions.matches(path)) return false;
|
||||||
|
|
||||||
if (matchedGroup == macroMarkersCarousel) {
|
if (matchedGroup == macroMarkersCarousel) {
|
||||||
return contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered();
|
return contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (matchedGroup == horizontalShelf) {
|
||||||
|
return cellVideoAttribute.check(protobufBufferArray).isFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
private final ByteArrayFilterGroup joinMembershipButton;
|
private final ByteArrayFilterGroup joinMembershipButton;
|
||||||
private final StringFilterGroup horizontalShelves;
|
private final StringFilterGroup horizontalShelves;
|
||||||
private final ByteArrayFilterGroup ticketShelf;
|
private final ByteArrayFilterGroup ticketShelf;
|
||||||
|
private final StringFilterGroup chipBar;
|
||||||
|
|
||||||
public LayoutComponentsFilter() {
|
public LayoutComponentsFilter() {
|
||||||
exceptions.addPatterns(
|
exceptions.addPatterns(
|
||||||
@@ -105,6 +106,11 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
"subscriptions_chip_bar"
|
"subscriptions_chip_bar"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
chipBar = new StringFilterGroup(
|
||||||
|
Settings.HIDE_FILTER_BAR_FEED_IN_HISTORY,
|
||||||
|
"chip_bar"
|
||||||
|
);
|
||||||
|
|
||||||
inFeedSurvey = new StringFilterGroup(
|
inFeedSurvey = new StringFilterGroup(
|
||||||
Settings.HIDE_FEED_SURVEY,
|
Settings.HIDE_FEED_SURVEY,
|
||||||
"in_feed_survey",
|
"in_feed_survey",
|
||||||
@@ -272,6 +278,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
emergencyBox,
|
emergencyBox,
|
||||||
subscribersCommunityGuidelines,
|
subscribersCommunityGuidelines,
|
||||||
subscriptionsChipBar,
|
subscriptionsChipBar,
|
||||||
|
chipBar,
|
||||||
channelGuidelines,
|
channelGuidelines,
|
||||||
audioTrackButton,
|
audioTrackButton,
|
||||||
artistCard,
|
artistCard,
|
||||||
@@ -314,6 +321,10 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
return contentIndex == 0 && (hideShelves() || ticketShelf.check(protobufBufferArray).isFiltered());
|
return contentIndex == 0 && (hideShelves() || ticketShelf.check(protobufBufferArray).isFiltered());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (matchedGroup == chipBar) {
|
||||||
|
return contentIndex == 0 && NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,7 +459,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do not hide if the navigation back button is visible,
|
// Do not hide if the navigation back button is visible,
|
||||||
// otherwise the content shelves in the explore/music/courses pages are hidde.
|
// otherwise the content shelves in the explore/music/courses pages are hidden.
|
||||||
if (NavigationBar.isBackButtonVisible()) {
|
if (NavigationBar.isBackButtonVisible()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public final class LithoFilterPatch {
|
|||||||
/**
|
/**
|
||||||
* Search through a byte array for all ASCII strings.
|
* Search through a byte array for all ASCII strings.
|
||||||
*/
|
*/
|
||||||
private static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
|
static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
|
||||||
// Valid ASCII values (ignore control characters).
|
// Valid ASCII values (ignore control characters).
|
||||||
final int minimumAscii = 32; // 32 = space character
|
final int minimumAscii = 32; // 32 = space character
|
||||||
final int maximumAscii = 126; // 127 = delete character
|
final int maximumAscii = 126; // 127 = delete character
|
||||||
@@ -96,7 +96,7 @@ public final class LithoFilterPatch {
|
|||||||
private static final class DummyFilter extends Filter { }
|
private static final class DummyFilter extends Filter { }
|
||||||
|
|
||||||
private static final Filter[] filters = new Filter[] {
|
private static final Filter[] filters = new Filter[] {
|
||||||
new DummyFilter() // Replaced by patch.
|
new DummyFilter() // Replaced patching, do not touch.
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
|
private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
|
||||||
@@ -108,11 +108,7 @@ public final class LithoFilterPatch {
|
|||||||
* Because litho filtering is multi-threaded and the buffer is passed in from a different injection point,
|
* Because litho filtering is multi-threaded and the buffer is passed in from a different injection point,
|
||||||
* the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
|
* the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
|
||||||
*/
|
*/
|
||||||
private static final ThreadLocal<ByteBuffer> bufferThreadLocal = new ThreadLocal<>();
|
private static final ThreadLocal<byte[]> bufferThreadLocal = new ThreadLocal<>();
|
||||||
/**
|
|
||||||
* Results of calling {@link #filter(String, StringBuilder)}.
|
|
||||||
*/
|
|
||||||
private static final ThreadLocal<Boolean> filterResult = new ThreadLocal<>();
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
for (Filter filter : filters) {
|
for (Filter filter : filters) {
|
||||||
@@ -168,57 +164,50 @@ public final class LithoFilterPatch {
|
|||||||
/**
|
/**
|
||||||
* Injection point. Called off the main thread.
|
* Injection point. Called off the main thread.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
public static void setProtoBuffer(byte[] buffer) {
|
||||||
public static void setProtoBuffer(@Nullable ByteBuffer protobufBuffer) {
|
|
||||||
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
|
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
|
||||||
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
|
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
|
||||||
// The buffer will be cleared from memory after a new buffer is set by the same thread,
|
// The buffer will be cleared from memory after a new buffer is set by the same thread,
|
||||||
// or when the calling thread eventually dies.
|
// or when the calling thread eventually dies.
|
||||||
if (protobufBuffer == null) {
|
bufferThreadLocal.set(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point. Called off the main thread.
|
||||||
|
* Targets 20.21 and lower.
|
||||||
|
*/
|
||||||
|
public static void setProtoBuffer(@Nullable ByteBuffer buffer) {
|
||||||
|
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
|
||||||
|
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
|
||||||
|
// The buffer will be cleared from memory after a new buffer is set by the same thread,
|
||||||
|
// or when the calling thread eventually dies.
|
||||||
|
if (buffer == null || !buffer.hasArray()) {
|
||||||
// It appears the buffer can be cleared out just before the call to #filter()
|
// It appears the buffer can be cleared out just before the call to #filter()
|
||||||
// Ignore this null value and retain the last buffer that was set.
|
// Ignore this null value and retain the last buffer that was set.
|
||||||
Logger.printDebug(() -> "Ignoring null protobuffer");
|
Logger.printDebug(() -> "Ignoring null or empty buffer: " + buffer);
|
||||||
} else {
|
} else {
|
||||||
bufferThreadLocal.set(protobufBuffer);
|
setProtoBuffer(buffer.array());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static boolean shouldFilter() {
|
public static boolean shouldFilter(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
|
||||||
Boolean shouldFilter = filterResult.get();
|
|
||||||
return shouldFilter != null && shouldFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point. Called off the main thread, and commonly called by multiple threads at the same time.
|
|
||||||
*/
|
|
||||||
public static void filter(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
|
|
||||||
filterResult.set(handleFiltering(lithoIdentifier, pathBuilder));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean handleFiltering(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
|
|
||||||
try {
|
try {
|
||||||
if (pathBuilder.length() == 0) {
|
if (pathBuilder.length() == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteBuffer protobufBuffer = bufferThreadLocal.get();
|
byte[] buffer = bufferThreadLocal.get();
|
||||||
final byte[] bufferArray;
|
|
||||||
// Potentially the buffer may have been null or never set up until now.
|
// Potentially the buffer may have been null or never set up until now.
|
||||||
// Use an empty buffer so the litho id/path filters still work correctly.
|
// Use an empty buffer so the litho id/path filters still work correctly.
|
||||||
if (protobufBuffer == null) {
|
if (buffer == null) {
|
||||||
bufferArray = EMPTY_BYTE_ARRAY;
|
buffer = EMPTY_BYTE_ARRAY;
|
||||||
} else if (!protobufBuffer.hasArray()) {
|
|
||||||
Logger.printDebug(() -> "Proto buffer does not have an array, using an empty buffer array");
|
|
||||||
bufferArray = EMPTY_BYTE_ARRAY;
|
|
||||||
} else {
|
|
||||||
bufferArray = protobufBuffer.array();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LithoFilterParameters parameter = new LithoFilterParameters(lithoIdentifier,
|
LithoFilterParameters parameter = new LithoFilterParameters(
|
||||||
pathBuilder.toString(), bufferArray);
|
lithoIdentifier, pathBuilder.toString(), buffer);
|
||||||
Logger.printDebug(() -> "Searching " + parameter);
|
Logger.printDebug(() -> "Searching " + parameter);
|
||||||
|
|
||||||
if (parameter.identifier != null && identifierSearchTree.matches(parameter.identifier, parameter)) {
|
if (parameter.identifier != null && identifierSearchTree.matches(parameter.identifier, parameter)) {
|
||||||
|
|||||||
@@ -40,8 +40,12 @@ public final class ShortsFilter extends Filter {
|
|||||||
|
|
||||||
private static WeakReference<PivotBar> pivotBarRef = new WeakReference<>(null);
|
private static WeakReference<PivotBar> pivotBarRef = new WeakReference<>(null);
|
||||||
|
|
||||||
private final StringFilterGroup shortsCompactFeedVideoPath;
|
private final StringFilterGroup shortsCompactFeedVideo;
|
||||||
private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer;
|
private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer;
|
||||||
|
private final StringFilterGroup useSoundButton;
|
||||||
|
private final ByteArrayFilterGroup useSoundButtonBuffer;
|
||||||
|
private final StringFilterGroup useTemplateButton;
|
||||||
|
private final ByteArrayFilterGroup useTemplateButtonBuffer;
|
||||||
|
|
||||||
private final StringFilterGroup subscribeButton;
|
private final StringFilterGroup subscribeButton;
|
||||||
private final StringFilterGroup joinButton;
|
private final StringFilterGroup joinButton;
|
||||||
@@ -49,11 +53,11 @@ public final class ShortsFilter extends Filter {
|
|||||||
private final StringFilterGroup shelfHeader;
|
private final StringFilterGroup shelfHeader;
|
||||||
|
|
||||||
private final StringFilterGroup suggestedAction;
|
private final StringFilterGroup suggestedAction;
|
||||||
private final ByteArrayFilterGroupList suggestedActionsGroupList = new ByteArrayFilterGroupList();
|
private final ByteArrayFilterGroupList suggestedActionsBuffer = new ByteArrayFilterGroupList();
|
||||||
|
|
||||||
private final StringFilterGroup shortsActionBar;
|
private final StringFilterGroup shortsActionBar;
|
||||||
private final StringFilterGroup actionButton;
|
private final StringFilterGroup videoActionButton;
|
||||||
private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList();
|
private final ByteArrayFilterGroupList videoActionButtonBuffer = new ByteArrayFilterGroupList();
|
||||||
|
|
||||||
public ShortsFilter() {
|
public ShortsFilter() {
|
||||||
//
|
//
|
||||||
@@ -82,7 +86,7 @@ public final class ShortsFilter extends Filter {
|
|||||||
// Path components.
|
// Path components.
|
||||||
//
|
//
|
||||||
|
|
||||||
shortsCompactFeedVideoPath = new StringFilterGroup(null,
|
shortsCompactFeedVideo = new StringFilterGroup(null,
|
||||||
// Shorts that appear in the feed/search when the device is using tablet layout.
|
// Shorts that appear in the feed/search when the device is using tablet layout.
|
||||||
"compact_video.eml",
|
"compact_video.eml",
|
||||||
// 'video_lockup_with_attachment.eml' is shown instead of 'compact_video.eml' for some users
|
// 'video_lockup_with_attachment.eml' is shown instead of 'compact_video.eml' for some users
|
||||||
@@ -174,7 +178,32 @@ public final class ShortsFilter extends Filter {
|
|||||||
"reel_action_bar.eml"
|
"reel_action_bar.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
actionButton = new StringFilterGroup(
|
useSoundButton = new StringFilterGroup(
|
||||||
|
Settings.HIDE_SHORTS_USE_SOUND_BUTTON,
|
||||||
|
// First filter needed for "Use this sound" that can appear when viewing Shorts
|
||||||
|
// through the "Short remixing this video" section.
|
||||||
|
"floating_action_button.eml",
|
||||||
|
// Second filter needed for "Use this sound" that can appear below the video title.
|
||||||
|
REEL_METAPANEL_PATH
|
||||||
|
);
|
||||||
|
|
||||||
|
useSoundButtonBuffer = new ByteArrayFilterGroup(
|
||||||
|
null,
|
||||||
|
"yt_outline_camera_"
|
||||||
|
);
|
||||||
|
|
||||||
|
useTemplateButton = new StringFilterGroup(
|
||||||
|
Settings.HIDE_SHORTS_USE_TEMPLATE_BUTTON,
|
||||||
|
// Second filter needed for "Use this template" that can appear below the video title.
|
||||||
|
REEL_METAPANEL_PATH
|
||||||
|
);
|
||||||
|
|
||||||
|
useTemplateButtonBuffer = new ByteArrayFilterGroup(
|
||||||
|
null,
|
||||||
|
"yt_outline_template_add_"
|
||||||
|
);
|
||||||
|
|
||||||
|
videoActionButton = new StringFilterGroup(
|
||||||
null,
|
null,
|
||||||
// Can be simply 'button.eml', 'shorts_video_action_button.eml' or 'reel_action_button.eml'
|
// Can be simply 'button.eml', 'shorts_video_action_button.eml' or 'reel_action_button.eml'
|
||||||
"button.eml"
|
"button.eml"
|
||||||
@@ -186,16 +215,16 @@ public final class ShortsFilter extends Filter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
addPathCallbacks(
|
addPathCallbacks(
|
||||||
shortsCompactFeedVideoPath, joinButton, subscribeButton, paidPromotionButton,
|
shortsCompactFeedVideo, joinButton, subscribeButton, paidPromotionButton,
|
||||||
shortsActionBar, suggestedAction, pausedOverlayButtons, channelBar,
|
shortsActionBar, suggestedAction, pausedOverlayButtons, channelBar,
|
||||||
fullVideoLinkLabel, videoTitle, reelSoundMetadata, soundButton, infoPanel,
|
fullVideoLinkLabel, videoTitle, useSoundButton, reelSoundMetadata, soundButton, infoPanel,
|
||||||
stickers, likeFountain, likeButton, dislikeButton
|
stickers, likeFountain, likeButton, dislikeButton
|
||||||
);
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// All other action buttons.
|
// All other action buttons.
|
||||||
//
|
//
|
||||||
videoActionButtonGroupList.addAll(
|
videoActionButtonBuffer.addAll(
|
||||||
new ByteArrayFilterGroup(
|
new ByteArrayFilterGroup(
|
||||||
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
|
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
|
||||||
"reel_comment_button",
|
"reel_comment_button",
|
||||||
@@ -216,7 +245,7 @@ public final class ShortsFilter extends Filter {
|
|||||||
//
|
//
|
||||||
// Suggested actions.
|
// Suggested actions.
|
||||||
//
|
//
|
||||||
suggestedActionsGroupList.addAll(
|
suggestedActionsBuffer.addAll(
|
||||||
new ByteArrayFilterGroup(
|
new ByteArrayFilterGroup(
|
||||||
Settings.HIDE_SHORTS_PREVIEW_COMMENT,
|
Settings.HIDE_SHORTS_PREVIEW_COMMENT,
|
||||||
// Preview comment that can popup while a Short is playing.
|
// Preview comment that can popup while a Short is playing.
|
||||||
@@ -242,10 +271,7 @@ public final class ShortsFilter extends Filter {
|
|||||||
"yt_outline_bookmark_",
|
"yt_outline_bookmark_",
|
||||||
// 'Save sound' button. It seems this has been removed and only 'Save music' is used.
|
// 'Save sound' button. It seems this has been removed and only 'Save music' is used.
|
||||||
// Still hide this in case it's still present.
|
// Still hide this in case it's still present.
|
||||||
"yt_outline_list_add_",
|
"yt_outline_list_add_"
|
||||||
// 'Use this sound' button. It seems this has been removed and only 'Save music' is used.
|
|
||||||
// Still hide this in case it's still present.
|
|
||||||
"yt_outline_camera_"
|
|
||||||
),
|
),
|
||||||
new ByteArrayFilterGroup(
|
new ByteArrayFilterGroup(
|
||||||
Settings.HIDE_SHORTS_SEARCH_SUGGESTIONS,
|
Settings.HIDE_SHORTS_SEARCH_SUGGESTIONS,
|
||||||
@@ -257,12 +283,18 @@ public final class ShortsFilter extends Filter {
|
|||||||
),
|
),
|
||||||
new ByteArrayFilterGroup(
|
new ByteArrayFilterGroup(
|
||||||
Settings.HIDE_SHORTS_USE_TEMPLATE_BUTTON,
|
Settings.HIDE_SHORTS_USE_TEMPLATE_BUTTON,
|
||||||
|
// "Use this template" can appear in two different places.
|
||||||
"yt_outline_template_add_"
|
"yt_outline_template_add_"
|
||||||
),
|
),
|
||||||
new ByteArrayFilterGroup(
|
new ByteArrayFilterGroup(
|
||||||
Settings.HIDE_SHORTS_UPCOMING_BUTTON,
|
Settings.HIDE_SHORTS_UPCOMING_BUTTON,
|
||||||
"yt_outline_bell_"
|
"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(
|
new ByteArrayFilterGroup(
|
||||||
Settings.HIDE_SHORTS_GREEN_SCREEN_BUTTON,
|
Settings.HIDE_SHORTS_GREEN_SCREEN_BUTTON,
|
||||||
"greenscreen_temp"
|
"greenscreen_temp"
|
||||||
@@ -279,7 +311,7 @@ public final class ShortsFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isEverySuggestedActionFilterEnabled() {
|
private boolean isEverySuggestedActionFilterEnabled() {
|
||||||
for (ByteArrayFilterGroup group : suggestedActionsGroupList) {
|
for (ByteArrayFilterGroup group : suggestedActionsBuffer) {
|
||||||
if (!group.isEnabled()) {
|
if (!group.isEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -297,15 +329,23 @@ public final class ShortsFilter extends Filter {
|
|||||||
return path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH);
|
return path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == shortsCompactFeedVideoPath) {
|
if (matchedGroup == useSoundButton) {
|
||||||
|
return useSoundButtonBuffer.check(protobufBufferArray).isFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedGroup == useTemplateButton) {
|
||||||
|
return useTemplateButtonBuffer.check(protobufBufferArray).isFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedGroup == shortsCompactFeedVideo) {
|
||||||
return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered();
|
return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video action buttons (comment, share, remix) have the same path.
|
// Video action buttons (comment, share, remix) have the same path.
|
||||||
// Like and dislike are separate path filters and don't require buffer searching.
|
// Like and dislike are separate path filters and don't require buffer searching.
|
||||||
if (matchedGroup == shortsActionBar) {
|
if (matchedGroup == shortsActionBar) {
|
||||||
return actionButton.check(path).isFiltered()
|
return videoActionButton.check(path).isFiltered()
|
||||||
&& videoActionButtonGroupList.check(protobufBufferArray).isFiltered();
|
&& videoActionButtonBuffer.check(protobufBufferArray).isFiltered();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == suggestedAction) {
|
if (matchedGroup == suggestedAction) {
|
||||||
@@ -316,7 +356,7 @@ public final class ShortsFilter extends Filter {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return suggestedActionsGroupList.check(protobufBufferArray).isFiltered();
|
return suggestedActionsBuffer.check(protobufBufferArray).isFiltered();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -382,17 +422,6 @@ public final class ShortsFilter extends Filter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point. Only used if patching older than 19.03.
|
|
||||||
* This hook may be obsolete even for old versions
|
|
||||||
* as they now use a litho layout like newer versions.
|
|
||||||
*/
|
|
||||||
public static void hideShortsShelf(final View shortsShelfView) {
|
|
||||||
if (shouldHideShortsFeedItems()) {
|
|
||||||
Utils.hideViewByLayoutParams(shortsShelfView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getSoundButtonSize(int original) {
|
public static int getSoundButtonSize(int original) {
|
||||||
if (Settings.HIDE_SHORTS_SOUND_BUTTON.get()) {
|
if (Settings.HIDE_SHORTS_SOUND_BUTTON.get()) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import static java.lang.Boolean.TRUE;
|
|||||||
import static app.revanced.extension.shared.settings.Setting.Availability;
|
import static app.revanced.extension.shared.settings.Setting.Availability;
|
||||||
import static app.revanced.extension.shared.settings.Setting.migrateOldSettingToNew;
|
import static app.revanced.extension.shared.settings.Setting.migrateOldSettingToNew;
|
||||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||||
|
import static app.revanced.extension.shared.settings.Setting.parentsAll;
|
||||||
import static app.revanced.extension.shared.settings.Setting.parentsAny;
|
import static app.revanced.extension.shared.settings.Setting.parentsAny;
|
||||||
import static app.revanced.extension.youtube.patches.ChangeFormFactorPatch.FormFactor;
|
import static app.revanced.extension.youtube.patches.ChangeFormFactorPatch.FormFactor;
|
||||||
import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.ChangeStartPageTypeAvailability;
|
import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.ChangeStartPageTypeAvailability;
|
||||||
@@ -22,6 +23,7 @@ import static app.revanced.extension.youtube.patches.OpenShortsInRegularPlayerPa
|
|||||||
import static app.revanced.extension.youtube.patches.SeekbarThumbnailsPatch.SeekbarThumbnailsHighQualityAvailability;
|
import static app.revanced.extension.youtube.patches.SeekbarThumbnailsPatch.SeekbarThumbnailsHighQualityAvailability;
|
||||||
import static app.revanced.extension.youtube.patches.components.PlayerFlyoutMenuItemsFilter.HideAudioFlyoutMenuAvailability;
|
import static app.revanced.extension.youtube.patches.components.PlayerFlyoutMenuItemsFilter.HideAudioFlyoutMenuAvailability;
|
||||||
import static app.revanced.extension.youtube.patches.theme.ThemePatch.SplashScreenAnimationStyle;
|
import static app.revanced.extension.youtube.patches.theme.ThemePatch.SplashScreenAnimationStyle;
|
||||||
|
import static app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController.SponsorBlockDuration;
|
||||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.IGNORE;
|
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.IGNORE;
|
||||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
|
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
|
||||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||||
@@ -98,6 +100,7 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_EXPANDABLE_CHIP = new BooleanSetting("revanced_hide_expandable_chip", TRUE);
|
public static final BooleanSetting HIDE_EXPANDABLE_CHIP = new BooleanSetting("revanced_hide_expandable_chip", TRUE);
|
||||||
public static final BooleanSetting HIDE_FEED_SURVEY = new BooleanSetting("revanced_hide_feed_survey", TRUE);
|
public static final BooleanSetting HIDE_FEED_SURVEY = new BooleanSetting("revanced_hide_feed_survey", TRUE);
|
||||||
public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_FEED = new BooleanSetting("revanced_hide_filter_bar_feed_in_feed", FALSE, true);
|
public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_FEED = new BooleanSetting("revanced_hide_filter_bar_feed_in_feed", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_HISTORY = new BooleanSetting("revanced_hide_filter_bar_feed_in_history", FALSE);
|
||||||
public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS = new BooleanSetting("revanced_hide_filter_bar_feed_in_related_videos", FALSE, true);
|
public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS = new BooleanSetting("revanced_hide_filter_bar_feed_in_related_videos", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_SEARCH = new BooleanSetting("revanced_hide_filter_bar_feed_in_search", FALSE, true);
|
public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_SEARCH = new BooleanSetting("revanced_hide_filter_bar_feed_in_search", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_FLOATING_MICROPHONE_BUTTON = new BooleanSetting("revanced_hide_floating_microphone_button", TRUE, true);
|
public static final BooleanSetting HIDE_FLOATING_MICROPHONE_BUTTON = new BooleanSetting("revanced_hide_floating_microphone_button", TRUE, true);
|
||||||
@@ -200,15 +203,16 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_TRANSCRIPT_SECTION = new BooleanSetting("revanced_hide_transcript_section", TRUE);
|
public static final BooleanSetting HIDE_TRANSCRIPT_SECTION = new BooleanSetting("revanced_hide_transcript_section", TRUE);
|
||||||
// Action buttons
|
// Action buttons
|
||||||
public static final BooleanSetting DISABLE_LIKE_SUBSCRIBE_GLOW = new BooleanSetting("revanced_disable_like_subscribe_glow", FALSE);
|
public static final BooleanSetting DISABLE_LIKE_SUBSCRIBE_GLOW = new BooleanSetting("revanced_disable_like_subscribe_glow", FALSE);
|
||||||
|
public static final BooleanSetting HIDE_ASK_BUTTON = new BooleanSetting("revanced_hide_ask_button", FALSE);
|
||||||
public static final BooleanSetting HIDE_CLIP_BUTTON = new BooleanSetting("revanced_hide_clip_button", TRUE);
|
public static final BooleanSetting HIDE_CLIP_BUTTON = new BooleanSetting("revanced_hide_clip_button", TRUE);
|
||||||
public static final BooleanSetting HIDE_DOWNLOAD_BUTTON = new BooleanSetting("revanced_hide_download_button", FALSE);
|
public static final BooleanSetting HIDE_DOWNLOAD_BUTTON = new BooleanSetting("revanced_hide_download_button", FALSE);
|
||||||
public static final BooleanSetting HIDE_LIKE_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_like_dislike_button", FALSE);
|
public static final BooleanSetting HIDE_LIKE_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_like_dislike_button", FALSE);
|
||||||
public static final BooleanSetting HIDE_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_playlist_button", FALSE);
|
|
||||||
public static final BooleanSetting HIDE_REMIX_BUTTON = new BooleanSetting("revanced_hide_remix_button", TRUE);
|
public static final BooleanSetting HIDE_REMIX_BUTTON = new BooleanSetting("revanced_hide_remix_button", TRUE);
|
||||||
public static final BooleanSetting HIDE_REPORT_BUTTON = new BooleanSetting("revanced_hide_report_button", FALSE);
|
public static final BooleanSetting HIDE_REPORT_BUTTON = new BooleanSetting("revanced_hide_report_button", FALSE);
|
||||||
|
public static final BooleanSetting HIDE_SAVE_BUTTON = new BooleanSetting("revanced_hide_save_button", FALSE);
|
||||||
public static final BooleanSetting HIDE_SHARE_BUTTON = new BooleanSetting("revanced_hide_share_button", FALSE);
|
public static final BooleanSetting HIDE_SHARE_BUTTON = new BooleanSetting("revanced_hide_share_button", FALSE);
|
||||||
|
public static final BooleanSetting HIDE_STOP_ADS_BUTTON = new BooleanSetting("revanced_hide_stop_ads_button", TRUE);
|
||||||
public static final BooleanSetting HIDE_THANKS_BUTTON = new BooleanSetting("revanced_hide_thanks_button", TRUE);
|
public static final BooleanSetting HIDE_THANKS_BUTTON = new BooleanSetting("revanced_hide_thanks_button", TRUE);
|
||||||
public static final BooleanSetting HIDE_ASK_BUTTON = new BooleanSetting("revanced_hide_ask_button", FALSE);
|
|
||||||
// Player flyout menu items
|
// Player flyout menu items
|
||||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_ADDITIONAL_SETTINGS = new BooleanSetting("revanced_hide_player_flyout_additional_settings", FALSE);
|
public static final BooleanSetting HIDE_PLAYER_FLYOUT_ADDITIONAL_SETTINGS = new BooleanSetting("revanced_hide_player_flyout_additional_settings", FALSE);
|
||||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_AMBIENT_MODE = new BooleanSetting("revanced_hide_player_flyout_ambient_mode", FALSE);
|
public static final BooleanSetting HIDE_PLAYER_FLYOUT_AMBIENT_MODE = new BooleanSetting("revanced_hide_player_flyout_ambient_mode", FALSE);
|
||||||
@@ -266,6 +270,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_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_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_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_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_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);
|
public static final BooleanSetting HIDE_SHORTS_HASHTAG_BUTTON = new BooleanSetting("revanced_hide_shorts_hashtag_button", TRUE);
|
||||||
@@ -293,6 +298,7 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_SHORTS_SUPER_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_super_thanks_button", TRUE);
|
public static final BooleanSetting HIDE_SHORTS_SUPER_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_super_thanks_button", TRUE);
|
||||||
public static final BooleanSetting HIDE_SHORTS_TAGGED_PRODUCTS = new BooleanSetting("revanced_hide_shorts_tagged_products", TRUE);
|
public static final BooleanSetting HIDE_SHORTS_TAGGED_PRODUCTS = new BooleanSetting("revanced_hide_shorts_tagged_products", TRUE);
|
||||||
public static final BooleanSetting HIDE_SHORTS_UPCOMING_BUTTON = new BooleanSetting("revanced_hide_shorts_upcoming_button", TRUE);
|
public static final BooleanSetting HIDE_SHORTS_UPCOMING_BUTTON = new BooleanSetting("revanced_hide_shorts_upcoming_button", TRUE);
|
||||||
|
public static final BooleanSetting HIDE_SHORTS_USE_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_use_sound_button", TRUE);
|
||||||
public static final BooleanSetting HIDE_SHORTS_USE_TEMPLATE_BUTTON = new BooleanSetting("revanced_hide_shorts_use_template_button", TRUE);
|
public static final BooleanSetting HIDE_SHORTS_USE_TEMPLATE_BUTTON = new BooleanSetting("revanced_hide_shorts_use_template_button", TRUE);
|
||||||
public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE);
|
public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE);
|
||||||
public static final BooleanSetting SHORTS_AUTOPLAY = new BooleanSetting("revanced_shorts_autoplay", FALSE);
|
public static final BooleanSetting SHORTS_AUTOPLAY = new BooleanSetting("revanced_shorts_autoplay", FALSE);
|
||||||
@@ -377,7 +383,11 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting SB_SQUARE_LAYOUT = new BooleanSetting("sb_square_layout", FALSE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_SQUARE_LAYOUT = new BooleanSetting("sb_square_layout", FALSE, parent(SB_ENABLED));
|
||||||
public static final BooleanSetting SB_COMPACT_SKIP_BUTTON = new BooleanSetting("sb_compact_skip_button", FALSE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_COMPACT_SKIP_BUTTON = new BooleanSetting("sb_compact_skip_button", FALSE, parent(SB_ENABLED));
|
||||||
public static final BooleanSetting SB_AUTO_HIDE_SKIP_BUTTON = new BooleanSetting("sb_auto_hide_skip_button", TRUE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_AUTO_HIDE_SKIP_BUTTON = new BooleanSetting("sb_auto_hide_skip_button", TRUE, parent(SB_ENABLED));
|
||||||
|
public static final EnumSetting<SponsorBlockDuration> SB_AUTO_HIDE_SKIP_BUTTON_DURATION = new EnumSetting<>("sb_auto_hide_skip_button_duration",
|
||||||
|
SponsorBlockDuration.FOUR_SECONDS, parent(SB_ENABLED));
|
||||||
public static final BooleanSetting SB_TOAST_ON_SKIP = new BooleanSetting("sb_toast_on_skip", TRUE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_TOAST_ON_SKIP = new BooleanSetting("sb_toast_on_skip", TRUE, parent(SB_ENABLED));
|
||||||
|
public static final EnumSetting<SponsorBlockDuration> SB_TOAST_ON_SKIP_DURATION = new EnumSetting<>("sb_toast_on_skip_duration",
|
||||||
|
SponsorBlockDuration.FOUR_SECONDS, parentsAll(SB_ENABLED, SB_TOAST_ON_SKIP));
|
||||||
public static final BooleanSetting SB_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("sb_toast_on_connection_error", TRUE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("sb_toast_on_connection_error", TRUE, parent(SB_ENABLED));
|
||||||
public static final BooleanSetting SB_TRACK_SKIP_COUNT = new BooleanSetting("sb_track_skip_count", TRUE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_TRACK_SKIP_COUNT = new BooleanSetting("sb_track_skip_count", TRUE, parent(SB_ENABLED));
|
||||||
public static final FloatSetting SB_SEGMENT_MIN_DURATION = new FloatSetting("sb_min_segment_duration", 0F, parent(SB_ENABLED));
|
public static final FloatSetting SB_SEGMENT_MIN_DURATION = new FloatSetting("sb_min_segment_duration", 0F, parent(SB_ENABLED));
|
||||||
|
|||||||
@@ -1,16 +1,37 @@
|
|||||||
package app.revanced.extension.youtube.sponsorblock;
|
package app.revanced.extension.youtube.sponsorblock;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.ShapeDrawable;
|
||||||
|
import android.graphics.drawable.shapes.RoundRectShape;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Range;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
@@ -30,20 +51,37 @@ import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController
|
|||||||
* Class is not thread safe. All methods must be called on the main thread unless otherwise specified.
|
* Class is not thread safe. All methods must be called on the main thread unless otherwise specified.
|
||||||
*/
|
*/
|
||||||
public class SegmentPlaybackController {
|
public class SegmentPlaybackController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Length of time to show a skip button for a highlight segment,
|
* Enum for configurable durations (1 to 10 seconds) for skip button and toast display.
|
||||||
* or a regular segment if {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is enabled.
|
|
||||||
*
|
|
||||||
* Effectively this value is rounded up to the next second.
|
|
||||||
*/
|
*/
|
||||||
private static final long DURATION_TO_SHOW_SKIP_BUTTON = 3800;
|
public enum SponsorBlockDuration {
|
||||||
|
ONE_SECOND(1),
|
||||||
|
TWO_SECONDS(2),
|
||||||
|
THREE_SECONDS(3),
|
||||||
|
FOUR_SECONDS(4),
|
||||||
|
FIVE_SECONDS(5),
|
||||||
|
SIX_SECONDS(6),
|
||||||
|
SEVEN_SECONDS(7),
|
||||||
|
EIGHT_SECONDS(8),
|
||||||
|
NINE_SECONDS(9),
|
||||||
|
TEN_SECONDS(10);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration, minus 200ms to adjust for exclusive end time checking in scheduled show/hides.
|
||||||
|
*/
|
||||||
|
private final long adjustedDuration;
|
||||||
|
|
||||||
|
SponsorBlockDuration(int seconds) {
|
||||||
|
adjustedDuration = seconds * 1000L - 200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Highlight segments have zero length as they are a point in time.
|
* Highlight segments have zero length as they are a point in time.
|
||||||
* Draw them on screen using a fixed width bar.
|
* Draw them on screen using a fixed width bar.
|
||||||
* Value is independent of device dpi.
|
|
||||||
*/
|
*/
|
||||||
private static final int HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH = 7;
|
private static final int HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH = dipToPixels(7);
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static String currentVideoId;
|
private static String currentVideoId;
|
||||||
@@ -59,7 +97,7 @@ public class SegmentPlaybackController {
|
|||||||
/**
|
/**
|
||||||
* Because loading can take time, show the skip to highlight for a few seconds after the segments load.
|
* Because loading can take time, show the skip to highlight for a few seconds after the segments load.
|
||||||
* This is the system time (in milliseconds) to no longer show the initial display skip to highlight.
|
* This is the system time (in milliseconds) to no longer show the initial display skip to highlight.
|
||||||
* Value will be zero if no highlight segment exists, or if the system time to show the highlight has passed.
|
* Value is zero if no highlight segment exists, or if the system time to show the highlight has passed.
|
||||||
*/
|
*/
|
||||||
private static long highlightSegmentInitialShowEndTime;
|
private static long highlightSegmentInitialShowEndTime;
|
||||||
|
|
||||||
@@ -70,7 +108,7 @@ public class SegmentPlaybackController {
|
|||||||
private static SponsorSegment segmentCurrentlyPlaying;
|
private static SponsorSegment segmentCurrentlyPlaying;
|
||||||
/**
|
/**
|
||||||
* Currently playing manual skip segment that is scheduled to hide.
|
* Currently playing manual skip segment that is scheduled to hide.
|
||||||
* This will always be NULL or equal to {@link #segmentCurrentlyPlaying}.
|
* This is always NULL or equal to {@link #segmentCurrentlyPlaying}.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
private static SponsorSegment scheduledHideSegment;
|
private static SponsorSegment scheduledHideSegment;
|
||||||
@@ -89,31 +127,71 @@ public class SegmentPlaybackController {
|
|||||||
*/
|
*/
|
||||||
private static final List<SponsorSegment> hiddenSkipSegmentsForCurrentVideoTime = new ArrayList<>();
|
private static final List<SponsorSegment> hiddenSkipSegmentsForCurrentVideoTime = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current segments that have been auto skipped.
|
||||||
|
* If field is non null then the range will always contain the current video time.
|
||||||
|
* Range is used to prevent auto-skipping after undo.
|
||||||
|
* Android Range object has inclusive end time, unlike {@link SponsorSegment}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static Range<Long> undoAutoSkipRange;
|
||||||
|
/**
|
||||||
|
* Range to undo if the toast is tapped.
|
||||||
|
* Is always null or identical to the last non null value of {@link #undoAutoSkipRange}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static Range<Long> undoAutoSkipRangeToast;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* System time (in milliseconds) of when to hide the skip button of {@link #segmentCurrentlyPlaying}.
|
* System time (in milliseconds) of when to hide the skip button of {@link #segmentCurrentlyPlaying}.
|
||||||
* Value is zero if playback is not inside a segment ({@link #segmentCurrentlyPlaying} is null),
|
* Value is zero if playback is not inside a segment ({@link #segmentCurrentlyPlaying} is null),
|
||||||
* or if {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is not enabled.
|
* or if {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is not enabled.
|
||||||
*/
|
*/
|
||||||
private static long skipSegmentButtonEndTime;
|
private static long skipSegmentButtonEndTime;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static String timeWithoutSegments;
|
private static String timeWithoutSegments;
|
||||||
|
|
||||||
private static int sponsorBarAbsoluteLeft;
|
private static int sponsorBarAbsoluteLeft;
|
||||||
private static int sponsorAbsoluteBarRight;
|
private static int sponsorAbsoluteBarRight;
|
||||||
private static int sponsorBarThickness;
|
private static int sponsorBarThickness;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static SponsorSegment lastSegmentSkipped;
|
||||||
|
private static long lastSegmentSkippedTime;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static SponsorSegment toastSegmentSkipped;
|
||||||
|
private static int toastNumberOfSegmentsSkipped;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last toast dialog showing on screen.
|
||||||
|
*/
|
||||||
|
private static WeakReference<Dialog> toastDialogRef = new WeakReference<>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The adjusted duration to show the skip button, in milliseconds.
|
||||||
|
*/
|
||||||
|
private static long getSkipButtonDuration() {
|
||||||
|
return Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.get().adjustedDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The adjusted duration to show the skipped toast, in milliseconds.
|
||||||
|
*/
|
||||||
|
private static long getToastDuration() {
|
||||||
|
return Settings.SB_TOAST_ON_SKIP_DURATION.get().adjustedDuration;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
static SponsorSegment[] getSegments() {
|
static SponsorSegment[] getSegments() {
|
||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setSegments(@NonNull SponsorSegment[] videoSegments) {
|
private static void setSegments(SponsorSegment[] videoSegments) {
|
||||||
Arrays.sort(videoSegments);
|
Arrays.sort(videoSegments);
|
||||||
segments = videoSegments;
|
segments = videoSegments;
|
||||||
calculateTimeWithoutSegments();
|
calculateTimeWithoutSegments();
|
||||||
|
|
||||||
if (SegmentCategory.HIGHLIGHT.behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY
|
if (SegmentCategory.HIGHLIGHT.behaviour == SKIP_AUTOMATICALLY
|
||||||
|| SegmentCategory.HIGHLIGHT.behaviour == CategoryBehaviour.MANUAL_SKIP) {
|
|| SegmentCategory.HIGHLIGHT.behaviour == CategoryBehaviour.MANUAL_SKIP) {
|
||||||
for (SponsorSegment segment : videoSegments) {
|
for (SponsorSegment segment : videoSegments) {
|
||||||
if (segment.category == SegmentCategory.HIGHLIGHT) {
|
if (segment.category == SegmentCategory.HIGHLIGHT) {
|
||||||
@@ -125,7 +203,7 @@ public class SegmentPlaybackController {
|
|||||||
highlightSegment = null;
|
highlightSegment = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void addUnsubmittedSegment(@NonNull SponsorSegment segment) {
|
static void addUnsubmittedSegment(SponsorSegment segment) {
|
||||||
Objects.requireNonNull(segment);
|
Objects.requireNonNull(segment);
|
||||||
if (segments == null) {
|
if (segments == null) {
|
||||||
segments = new SponsorSegment[1];
|
segments = new SponsorSegment[1];
|
||||||
@@ -140,6 +218,7 @@ public class SegmentPlaybackController {
|
|||||||
if (segments == null || segments.length == 0) {
|
if (segments == null || segments.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SponsorSegment> replacement = new ArrayList<>();
|
List<SponsorSegment> replacement = new ArrayList<>();
|
||||||
for (SponsorSegment segment : segments) {
|
for (SponsorSegment segment : segments) {
|
||||||
if (segment.category != SegmentCategory.UNSUBMITTED) {
|
if (segment.category != SegmentCategory.UNSUBMITTED) {
|
||||||
@@ -156,7 +235,7 @@ public class SegmentPlaybackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all downloaded data.
|
* Clear all data.
|
||||||
*/
|
*/
|
||||||
private static void clearData() {
|
private static void clearData() {
|
||||||
currentVideoId = null;
|
currentVideoId = null;
|
||||||
@@ -170,6 +249,8 @@ public class SegmentPlaybackController {
|
|||||||
skipSegmentButtonEndTime = 0;
|
skipSegmentButtonEndTime = 0;
|
||||||
toastSegmentSkipped = null;
|
toastSegmentSkipped = null;
|
||||||
toastNumberOfSegmentsSkipped = 0;
|
toastNumberOfSegmentsSkipped = 0;
|
||||||
|
undoAutoSkipRange = null;
|
||||||
|
undoAutoSkipRangeToast = null;
|
||||||
hiddenSkipSegmentsForCurrentVideoTime.clear();
|
hiddenSkipSegmentsForCurrentVideoTime.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +267,7 @@ public class SegmentPlaybackController {
|
|||||||
SponsorBlockUtils.clearUnsubmittedSegmentTimes();
|
SponsorBlockUtils.clearUnsubmittedSegmentTimes();
|
||||||
Logger.printDebug(() -> "Initialized SponsorBlock");
|
Logger.printDebug(() -> "Initialized SponsorBlock");
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Failed to initialize SponsorBlock", ex);
|
Logger.printException(() -> "initialize failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +284,7 @@ public class SegmentPlaybackController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (PlayerType.getCurrent().isNoneOrHidden()) {
|
if (PlayerType.getCurrent().isNoneOrHidden()) {
|
||||||
Logger.printDebug(() -> "ignoring Short");
|
Logger.printDebug(() -> "Ignoring Short");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!Utils.isNetworkConnected()) {
|
if (!Utils.isNetworkConnected()) {
|
||||||
@@ -212,7 +293,7 @@ public class SegmentPlaybackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentVideoId = videoId;
|
currentVideoId = videoId;
|
||||||
Logger.printDebug(() -> "setCurrentVideoId: " + videoId);
|
Logger.printDebug(() -> "New video ID: " + videoId);
|
||||||
|
|
||||||
Utils.runOnBackgroundThread(() -> {
|
Utils.runOnBackgroundThread(() -> {
|
||||||
try {
|
try {
|
||||||
@@ -227,42 +308,39 @@ public class SegmentPlaybackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must be called off main thread
|
* Must be called off main thread.
|
||||||
*/
|
*/
|
||||||
static void executeDownloadSegments(@NonNull String videoId) {
|
static void executeDownloadSegments(String videoId) {
|
||||||
Objects.requireNonNull(videoId);
|
Objects.requireNonNull(videoId);
|
||||||
try {
|
|
||||||
SponsorSegment[] segments = SBRequester.getSegments(videoId);
|
|
||||||
|
|
||||||
Utils.runOnMainThread(()-> {
|
SponsorSegment[] segments = SBRequester.getSegments(videoId);
|
||||||
if (!videoId.equals(currentVideoId)) {
|
|
||||||
// user changed videos before get segments network call could complete
|
|
||||||
Logger.printDebug(() -> "Ignoring segments for prior video: " + videoId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSegments(segments);
|
|
||||||
|
|
||||||
final long videoTime = VideoInformation.getVideoTime();
|
Utils.runOnMainThread(() -> {
|
||||||
if (highlightSegment != null) {
|
if (!videoId.equals(currentVideoId)) {
|
||||||
// If the current video time is before the highlight.
|
// user changed videos before get segments network call could complete
|
||||||
final long timeUntilHighlight = highlightSegment.start - videoTime;
|
Logger.printDebug(() -> "Ignoring segments for prior video: " + videoId);
|
||||||
if (timeUntilHighlight > 0) {
|
return;
|
||||||
if (highlightSegment.shouldAutoSkip()) {
|
}
|
||||||
skipSegment(highlightSegment, false);
|
setSegments(segments);
|
||||||
return;
|
|
||||||
}
|
final long videoTime = VideoInformation.getVideoTime();
|
||||||
highlightSegmentInitialShowEndTime = System.currentTimeMillis() + Math.min(
|
if (highlightSegment != null) {
|
||||||
(long) (timeUntilHighlight / VideoInformation.getPlaybackSpeed()),
|
// If the current video time is before the highlight.
|
||||||
DURATION_TO_SHOW_SKIP_BUTTON);
|
final long timeUntilHighlight = highlightSegment.start - videoTime;
|
||||||
|
if (timeUntilHighlight > 0) {
|
||||||
|
if (highlightSegment.shouldAutoSkip()) {
|
||||||
|
skipSegment(highlightSegment, false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
highlightSegmentInitialShowEndTime = System.currentTimeMillis() + Math.min(
|
||||||
|
(long) (timeUntilHighlight / VideoInformation.getPlaybackSpeed()),
|
||||||
|
getSkipButtonDuration());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check for any skips now, instead of waiting for the next update to setVideoTime()
|
// check for any skips now, instead of waiting for the next update to setVideoTime()
|
||||||
setVideoTime(videoTime);
|
setVideoTime(videoTime);
|
||||||
});
|
});
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "executeDownloadSegments failure", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,8 +351,8 @@ public class SegmentPlaybackController {
|
|||||||
public static void setVideoTime(long millis) {
|
public static void setVideoTime(long millis) {
|
||||||
try {
|
try {
|
||||||
if (!Settings.SB_ENABLED.get()
|
if (!Settings.SB_ENABLED.get()
|
||||||
|| PlayerType.getCurrent().isNoneOrHidden() // Shorts playback.
|
|| PlayerType.getCurrent().isNoneOrHidden() // Shorts playback.
|
||||||
|| segments == null || segments.length == 0) {
|
|| segments == null || segments.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Logger.printDebug(() -> "setVideoTime: " + millis);
|
Logger.printDebug(() -> "setVideoTime: " + millis);
|
||||||
@@ -290,7 +368,7 @@ public class SegmentPlaybackController {
|
|||||||
//
|
//
|
||||||
// To debug the stale skip logic, set this to a very large value (5000 or more)
|
// To debug the stale skip logic, set this to a very large value (5000 or more)
|
||||||
// then try manually seeking just before playback reaches a segment skip.
|
// then try manually seeking just before playback reaches a segment skip.
|
||||||
final long speedAdjustedTimeThreshold = (long)(playbackSpeed * 1200);
|
final long speedAdjustedTimeThreshold = (long) (playbackSpeed * 1200);
|
||||||
final long startTimerLookAheadThreshold = millis + speedAdjustedTimeThreshold;
|
final long startTimerLookAheadThreshold = millis + speedAdjustedTimeThreshold;
|
||||||
|
|
||||||
SponsorSegment foundSegmentCurrentlyPlaying = null;
|
SponsorSegment foundSegmentCurrentlyPlaying = null;
|
||||||
@@ -298,22 +376,24 @@ public class SegmentPlaybackController {
|
|||||||
|
|
||||||
for (final SponsorSegment segment : segments) {
|
for (final SponsorSegment segment : segments) {
|
||||||
if (segment.category.behaviour == CategoryBehaviour.SHOW_IN_SEEKBAR
|
if (segment.category.behaviour == CategoryBehaviour.SHOW_IN_SEEKBAR
|
||||||
|| segment.category.behaviour == CategoryBehaviour.IGNORE
|
|| segment.category.behaviour == CategoryBehaviour.IGNORE
|
||||||
|| segment.category == SegmentCategory.HIGHLIGHT) {
|
|| segment.category == SegmentCategory.HIGHLIGHT) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (segment.end <= millis) {
|
if (segment.end <= millis) {
|
||||||
continue; // past this segment
|
continue; // Past this segment.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final boolean segmentShouldAutoSkip = shouldAutoSkipAndUndoSkipNotActive(segment, millis);
|
||||||
|
|
||||||
if (segment.start <= millis) {
|
if (segment.start <= millis) {
|
||||||
// we are in the segment!
|
// We are in the segment!
|
||||||
if (segment.shouldAutoSkip()) {
|
if (segmentShouldAutoSkip) {
|
||||||
skipSegment(segment, false);
|
skipSegment(segment, false);
|
||||||
return; // must return, as skipping causes a recursive call back into this method
|
return; // Must return, as skipping causes a recursive call back into this method.
|
||||||
}
|
}
|
||||||
|
|
||||||
// first found segment, or it's an embedded segment and fully inside the outer segment
|
// First found segment, or it's an embedded segment and fully inside the outer segment.
|
||||||
if (foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment)) {
|
if (foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment)) {
|
||||||
// If the found segment is not currently displayed, then do not show if the segment is nearly over.
|
// If the found segment is not currently displayed, then do not show if the segment is nearly over.
|
||||||
// This check prevents the skip button text from rapidly changing when multiple segments end at nearly the same time.
|
// This check prevents the skip button text from rapidly changing when multiple segments end at nearly the same time.
|
||||||
@@ -327,25 +407,27 @@ public class SegmentPlaybackController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Keep iterating and looking. There may be an upcoming autoskip,
|
// Keep iterating and looking. There may be an upcoming autoskip,
|
||||||
// or there may be another smaller segment nested inside this segment
|
// or there may be another smaller segment nested inside this segment.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// segment is upcoming
|
// Segment is upcoming.
|
||||||
if (startTimerLookAheadThreshold < segment.start) {
|
if (startTimerLookAheadThreshold < segment.start) {
|
||||||
break; // segment is not close enough to schedule, and no segments after this are of interest
|
// Segment is not close enough to schedule, and no segments after this are of interest.
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (segment.shouldAutoSkip()) { // upcoming autoskip
|
|
||||||
|
if (segmentShouldAutoSkip) {
|
||||||
foundUpcomingSegment = segment;
|
foundUpcomingSegment = segment;
|
||||||
break; // must stop here
|
break; // Must stop here.
|
||||||
}
|
}
|
||||||
|
|
||||||
// upcoming manual skip
|
// Upcoming manual skip.
|
||||||
|
|
||||||
// do not schedule upcoming segment, if it is not fully contained inside the current segment
|
// Do not schedule upcoming segment, if it is not fully contained inside the current segment.
|
||||||
if ((foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment))
|
if ((foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment))
|
||||||
// use the most inner upcoming segment
|
// Use the most inner upcoming segment.
|
||||||
&& (foundUpcomingSegment == null || foundUpcomingSegment.containsSegment(segment))) {
|
&& (foundUpcomingSegment == null || foundUpcomingSegment.containsSegment(segment))) {
|
||||||
|
|
||||||
// Only schedule, if the segment start time is not near the end time of the current segment.
|
// Only schedule, if the segment start time is not near the end time of the current segment.
|
||||||
// This check is needed to prevent scheduled hide and show from clashing with each other.
|
// This check is needed to prevent scheduled hide and show from clashing with each other.
|
||||||
@@ -361,8 +443,8 @@ public class SegmentPlaybackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (highlightSegment != null) {
|
if (highlightSegment != null) {
|
||||||
if (millis < DURATION_TO_SHOW_SKIP_BUTTON || (highlightSegmentInitialShowEndTime != 0
|
if (millis < getSkipButtonDuration() || (highlightSegmentInitialShowEndTime != 0
|
||||||
&& System.currentTimeMillis() < highlightSegmentInitialShowEndTime)) {
|
&& System.currentTimeMillis() < highlightSegmentInitialShowEndTime)) {
|
||||||
SponsorBlockViewController.showSkipHighlightButton(highlightSegment);
|
SponsorBlockViewController.showSkipHighlightButton(highlightSegment);
|
||||||
} else {
|
} else {
|
||||||
highlightSegmentInitialShowEndTime = 0;
|
highlightSegmentInitialShowEndTime = 0;
|
||||||
@@ -373,16 +455,17 @@ public class SegmentPlaybackController {
|
|||||||
if (segmentCurrentlyPlaying != foundSegmentCurrentlyPlaying) {
|
if (segmentCurrentlyPlaying != foundSegmentCurrentlyPlaying) {
|
||||||
setSegmentCurrentlyPlaying(foundSegmentCurrentlyPlaying);
|
setSegmentCurrentlyPlaying(foundSegmentCurrentlyPlaying);
|
||||||
} else if (foundSegmentCurrentlyPlaying != null
|
} else if (foundSegmentCurrentlyPlaying != null
|
||||||
&& skipSegmentButtonEndTime != 0 && skipSegmentButtonEndTime <= System.currentTimeMillis()) {
|
&& skipSegmentButtonEndTime != 0
|
||||||
|
&& skipSegmentButtonEndTime <= System.currentTimeMillis()) {
|
||||||
Logger.printDebug(() -> "Auto hiding skip button for segment: " + segmentCurrentlyPlaying);
|
Logger.printDebug(() -> "Auto hiding skip button for segment: " + segmentCurrentlyPlaying);
|
||||||
skipSegmentButtonEndTime = 0;
|
skipSegmentButtonEndTime = 0;
|
||||||
hiddenSkipSegmentsForCurrentVideoTime.add(foundSegmentCurrentlyPlaying);
|
hiddenSkipSegmentsForCurrentVideoTime.add(foundSegmentCurrentlyPlaying);
|
||||||
SponsorBlockViewController.hideSkipSegmentButton();
|
SponsorBlockViewController.hideSkipSegmentButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
// schedule a hide, only if the segment end is near
|
// Schedule a hide, but only if the segment end is near.
|
||||||
final SponsorSegment segmentToHide =
|
final SponsorSegment segmentToHide = (foundSegmentCurrentlyPlaying != null &&
|
||||||
(foundSegmentCurrentlyPlaying != null && foundSegmentCurrentlyPlaying.endIsNear(millis, speedAdjustedTimeThreshold))
|
foundSegmentCurrentlyPlaying.endIsNear(millis, speedAdjustedTimeThreshold))
|
||||||
? foundSegmentCurrentlyPlaying
|
? foundSegmentCurrentlyPlaying
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -407,7 +490,7 @@ public class SegmentPlaybackController {
|
|||||||
|
|
||||||
final long videoTime = VideoInformation.getVideoTime();
|
final long videoTime = VideoInformation.getVideoTime();
|
||||||
if (!segmentToHide.endIsNear(videoTime, speedAdjustedTimeThreshold)) {
|
if (!segmentToHide.endIsNear(videoTime, speedAdjustedTimeThreshold)) {
|
||||||
// current video time is not what's expected. User paused playback
|
// Current video time is not what's expected. User paused playback.
|
||||||
Logger.printDebug(() -> "Ignoring outdated scheduled hide: " + segmentToHide
|
Logger.printDebug(() -> "Ignoring outdated scheduled hide: " + segmentToHide
|
||||||
+ " videoInformation time: " + videoTime);
|
+ " videoInformation time: " + videoTime);
|
||||||
return;
|
return;
|
||||||
@@ -416,7 +499,7 @@ public class SegmentPlaybackController {
|
|||||||
// Need more than just hide the skip button, as this may have been an embedded segment
|
// Need more than just hide the skip button, as this may have been an embedded segment
|
||||||
// Instead call back into setVideoTime to check everything again.
|
// Instead call back into setVideoTime to check everything again.
|
||||||
// Should not use VideoInformation time as it is less accurate,
|
// Should not use VideoInformation time as it is less accurate,
|
||||||
// but this scheduled handler was scheduled precisely so we can just use the segment end time
|
// but this scheduled handler was scheduled precisely so we can just use the segment end time.
|
||||||
setSegmentCurrentlyPlaying(null);
|
setSegmentCurrentlyPlaying(null);
|
||||||
setVideoTime(segmentToHide.end);
|
setVideoTime(segmentToHide.end);
|
||||||
}, delayUntilHide);
|
}, delayUntilHide);
|
||||||
@@ -446,12 +529,12 @@ public class SegmentPlaybackController {
|
|||||||
|
|
||||||
final long videoTime = VideoInformation.getVideoTime();
|
final long videoTime = VideoInformation.getVideoTime();
|
||||||
if (!segmentToSkip.startIsNear(videoTime, speedAdjustedTimeThreshold)) {
|
if (!segmentToSkip.startIsNear(videoTime, speedAdjustedTimeThreshold)) {
|
||||||
// current video time is not what's expected. User paused playback
|
// Current video time is not what's expected. User paused playback.
|
||||||
Logger.printDebug(() -> "Ignoring outdated scheduled segment: " + segmentToSkip
|
Logger.printDebug(() -> "Ignoring outdated scheduled segment: " + segmentToSkip
|
||||||
+ " videoInformation time: " + videoTime);
|
+ " videoInformation time: " + videoTime);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (segmentToSkip.shouldAutoSkip()) {
|
if (shouldAutoSkipAndUndoSkipNotActive(segmentToSkip, videoTime)) {
|
||||||
Logger.printDebug(() -> "Running scheduled skip segment: " + segmentToSkip);
|
Logger.printDebug(() -> "Running scheduled skip segment: " + segmentToSkip);
|
||||||
skipSegment(segmentToSkip, false);
|
skipSegment(segmentToSkip, false);
|
||||||
} else {
|
} else {
|
||||||
@@ -461,6 +544,12 @@ public class SegmentPlaybackController {
|
|||||||
}, delayUntilSkip);
|
}, delayUntilSkip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear undo range if video time is outside the segment. Must check last.
|
||||||
|
if (undoAutoSkipRange != null && !undoAutoSkipRange.contains(millis)) {
|
||||||
|
Logger.printDebug(() -> "Clearing undo range as current time is now outside range: " + undoAutoSkipRange);
|
||||||
|
undoAutoSkipRange = null;
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.printException(() -> "setVideoTime failure", e);
|
Logger.printException(() -> "setVideoTime failure", e);
|
||||||
}
|
}
|
||||||
@@ -470,14 +559,13 @@ public class SegmentPlaybackController {
|
|||||||
* Removes all previously hidden segments that are not longer contained in the given video time.
|
* Removes all previously hidden segments that are not longer contained in the given video time.
|
||||||
*/
|
*/
|
||||||
private static void updateHiddenSegments(long currentVideoTime) {
|
private static void updateHiddenSegments(long currentVideoTime) {
|
||||||
Iterator<SponsorSegment> i = hiddenSkipSegmentsForCurrentVideoTime.iterator();
|
hiddenSkipSegmentsForCurrentVideoTime.removeIf((hiddenSegment) -> {
|
||||||
while (i.hasNext()) {
|
|
||||||
SponsorSegment hiddenSegment = i.next();
|
|
||||||
if (!hiddenSegment.containsTime(currentVideoTime)) {
|
if (!hiddenSegment.containsTime(currentVideoTime)) {
|
||||||
Logger.printDebug(() -> "Resetting hide skip button: " + hiddenSegment);
|
Logger.printDebug(() -> "Resetting hide skip button: " + hiddenSegment);
|
||||||
i.remove();
|
return true;
|
||||||
}
|
}
|
||||||
}
|
return false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setSegmentCurrentlyPlaying(@Nullable SponsorSegment segment) {
|
private static void setSegmentCurrentlyPlaying(@Nullable SponsorSegment segment) {
|
||||||
@@ -488,8 +576,10 @@ public class SegmentPlaybackController {
|
|||||||
SponsorBlockViewController.hideSkipSegmentButton();
|
SponsorBlockViewController.hideSkipSegmentButton();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
segmentCurrentlyPlaying = segment;
|
segmentCurrentlyPlaying = segment;
|
||||||
skipSegmentButtonEndTime = 0;
|
skipSegmentButtonEndTime = 0;
|
||||||
|
|
||||||
if (Settings.SB_AUTO_HIDE_SKIP_BUTTON.get()) {
|
if (Settings.SB_AUTO_HIDE_SKIP_BUTTON.get()) {
|
||||||
if (hiddenSkipSegmentsForCurrentVideoTime.contains(segment)) {
|
if (hiddenSkipSegmentsForCurrentVideoTime.contains(segment)) {
|
||||||
// Playback exited a nested segment and the outer segment skip button was previously hidden.
|
// Playback exited a nested segment and the outer segment skip button was previously hidden.
|
||||||
@@ -497,16 +587,13 @@ public class SegmentPlaybackController {
|
|||||||
SponsorBlockViewController.hideSkipSegmentButton();
|
SponsorBlockViewController.hideSkipSegmentButton();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
skipSegmentButtonEndTime = System.currentTimeMillis() + DURATION_TO_SHOW_SKIP_BUTTON;
|
skipSegmentButtonEndTime = System.currentTimeMillis() + getSkipButtonDuration();
|
||||||
}
|
}
|
||||||
Logger.printDebug(() -> "Showing segment: " + segment);
|
Logger.printDebug(() -> "Showing segment: " + segment);
|
||||||
SponsorBlockViewController.showSkipSegmentButton(segment);
|
SponsorBlockViewController.showSkipSegmentButton(segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SponsorSegment lastSegmentSkipped;
|
private static void skipSegment(SponsorSegment segmentToSkip, boolean userManuallySkipped) {
|
||||||
private static long lastSegmentSkippedTime;
|
|
||||||
|
|
||||||
private static void skipSegment(@NonNull SponsorSegment segmentToSkip, boolean userManuallySkipped) {
|
|
||||||
try {
|
try {
|
||||||
SponsorBlockViewController.hideSkipHighlightButton();
|
SponsorBlockViewController.hideSkipHighlightButton();
|
||||||
SponsorBlockViewController.hideSkipSegmentButton();
|
SponsorBlockViewController.hideSkipSegmentButton();
|
||||||
@@ -525,7 +612,7 @@ public class SegmentPlaybackController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.printDebug(() -> "Skipping segment: " + segmentToSkip);
|
Logger.printDebug(() -> "Skipping segment: " + segmentToSkip + " videoState: " + VideoState.getCurrent());
|
||||||
lastSegmentSkipped = segmentToSkip;
|
lastSegmentSkipped = segmentToSkip;
|
||||||
lastSegmentSkippedTime = now;
|
lastSegmentSkippedTime = now;
|
||||||
setSegmentCurrentlyPlaying(null);
|
setSegmentCurrentlyPlaying(null);
|
||||||
@@ -535,29 +622,39 @@ public class SegmentPlaybackController {
|
|||||||
highlightSegmentInitialShowEndTime = 0;
|
highlightSegmentInitialShowEndTime = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set or update undo skip range.
|
||||||
|
Range<Long> range = segmentToSkip.getUndoRange();
|
||||||
|
if (undoAutoSkipRange == null) {
|
||||||
|
Logger.printDebug(() -> "Setting new undo range to: " + range);
|
||||||
|
undoAutoSkipRange = range;
|
||||||
|
} else {
|
||||||
|
Range<Long> extendedRange = undoAutoSkipRange.extend(range);
|
||||||
|
Logger.printDebug(() -> "Extending undo range from: " + undoAutoSkipRange +
|
||||||
|
" to: " + extendedRange);
|
||||||
|
undoAutoSkipRange = extendedRange;
|
||||||
|
}
|
||||||
|
undoAutoSkipRangeToast = undoAutoSkipRange;
|
||||||
|
|
||||||
// If the seek is successful, then the seek causes a recursive call back into this class.
|
// If the seek is successful, then the seek causes a recursive call back into this class.
|
||||||
final boolean seekSuccessful = VideoInformation.seekTo(segmentToSkip.end);
|
final boolean seekSuccessful = VideoInformation.seekTo(segmentToSkip.end);
|
||||||
if (!seekSuccessful) {
|
if (!seekSuccessful) {
|
||||||
// can happen when switching videos and is normal
|
// Can happen when switching videos and is normal.
|
||||||
Logger.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segmentToSkip);
|
Logger.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segmentToSkip);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean videoIsPaused = VideoState.getCurrent() == VideoState.PAUSED;
|
|
||||||
if (!userManuallySkipped) {
|
if (!userManuallySkipped) {
|
||||||
// check for any smaller embedded segments, and count those as autoskipped
|
// Check for any smaller embedded segments, and count those as auto-skipped.
|
||||||
final boolean showSkipToast = Settings.SB_TOAST_ON_SKIP.get();
|
final boolean showSkipToast = Settings.SB_TOAST_ON_SKIP.get();
|
||||||
for (final SponsorSegment otherSegment : Objects.requireNonNull(segments)) {
|
for (SponsorSegment otherSegment : Objects.requireNonNull(segments)) {
|
||||||
if (segmentToSkip.end < otherSegment.start) {
|
if (segmentToSkip.end < otherSegment.start) {
|
||||||
break; // no other segments can be contained
|
break; // No other segments can be contained.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otherSegment == segmentToSkip ||
|
if (otherSegment == segmentToSkip ||
|
||||||
(otherSegment.category != SegmentCategory.HIGHLIGHT && segmentToSkip.containsSegment(otherSegment))) {
|
(otherSegment.category != SegmentCategory.HIGHLIGHT && segmentToSkip.containsSegment(otherSegment))) {
|
||||||
otherSegment.didAutoSkipped = true;
|
otherSegment.didAutoSkipped = true;
|
||||||
// Do not show a toast if the user is scrubbing thru a paused video.
|
if (showSkipToast) {
|
||||||
// Cannot do this video state check in setTime or earlier in this method, as the video state may not be up to date.
|
|
||||||
// So instead, only hide toasts because all other skip logic done while paused causes no harm.
|
|
||||||
if (showSkipToast && !videoIsPaused) {
|
|
||||||
showSkippedSegmentToast(otherSegment);
|
showSkippedSegmentToast(otherSegment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,7 +664,7 @@ public class SegmentPlaybackController {
|
|||||||
if (segmentToSkip.category == SegmentCategory.UNSUBMITTED) {
|
if (segmentToSkip.category == SegmentCategory.UNSUBMITTED) {
|
||||||
removeUnsubmittedSegments();
|
removeUnsubmittedSegments();
|
||||||
SponsorBlockUtils.setNewSponsorSegmentPreviewed();
|
SponsorBlockUtils.setNewSponsorSegmentPreviewed();
|
||||||
} else if (!videoIsPaused) {
|
} else if (VideoState.getCurrent() != VideoState.PAUSED) {
|
||||||
SponsorBlockUtils.sendViewRequestAsync(segmentToSkip);
|
SponsorBlockUtils.sendViewRequestAsync(segmentToSkip);
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -575,29 +672,44 @@ public class SegmentPlaybackController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the segment should be auto-skipped _and_ if undo autoskip is not active.
|
||||||
|
*/
|
||||||
|
private static boolean shouldAutoSkipAndUndoSkipNotActive(SponsorSegment segment, long currentVideoTime) {
|
||||||
|
return segment.shouldAutoSkip() && (undoAutoSkipRange == null
|
||||||
|
|| !undoAutoSkipRange.contains(currentVideoTime));
|
||||||
|
}
|
||||||
|
|
||||||
private static int toastNumberOfSegmentsSkipped;
|
private static void showSkippedSegmentToast(SponsorSegment segment) {
|
||||||
@Nullable
|
|
||||||
private static SponsorSegment toastSegmentSkipped;
|
|
||||||
|
|
||||||
private static void showSkippedSegmentToast(@NonNull SponsorSegment segment) {
|
|
||||||
Utils.verifyOnMainThread();
|
Utils.verifyOnMainThread();
|
||||||
toastNumberOfSegmentsSkipped++;
|
|
||||||
if (toastNumberOfSegmentsSkipped > 1) {
|
|
||||||
return; // toast already scheduled
|
|
||||||
}
|
|
||||||
toastSegmentSkipped = segment;
|
toastSegmentSkipped = segment;
|
||||||
|
if (toastNumberOfSegmentsSkipped++ > 0) {
|
||||||
|
return; // Toast is already scheduled.
|
||||||
|
}
|
||||||
|
|
||||||
final long delayToToastMilliseconds = 250; // also the maximum time between skips to be considered skipping multiple segments
|
// Maximum time between skips to be considered skipping multiple segments.
|
||||||
|
final long delayToToastMilliseconds = 250;
|
||||||
Utils.runOnMainThreadDelayed(() -> {
|
Utils.runOnMainThreadDelayed(() -> {
|
||||||
try {
|
try {
|
||||||
if (toastSegmentSkipped == null) { // video was changed just after skipping segment
|
// Do not show a toast if the user is scrubbing thru a paused video.
|
||||||
|
// Cannot do this video state check in setTime or before calling this this method,
|
||||||
|
// as the video state may not be up to date. So instead, only ignore the toast
|
||||||
|
// just before it's about to show since the video state is up to date.
|
||||||
|
if (VideoState.getCurrent() == VideoState.PAUSED) {
|
||||||
|
Logger.printDebug(() -> "Ignoring scheduled toast as video state is paused");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toastSegmentSkipped == null || undoAutoSkipRangeToast == null) {
|
||||||
|
// Video was changed immediately after skipping segment.
|
||||||
Logger.printDebug(() -> "Ignoring old scheduled show toast");
|
Logger.printDebug(() -> "Ignoring old scheduled show toast");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Utils.showToastShort(toastNumberOfSegmentsSkipped == 1
|
String message = toastNumberOfSegmentsSkipped == 1
|
||||||
? toastSegmentSkipped.getSkippedToastText()
|
? toastSegmentSkipped.getSkippedToastText()
|
||||||
: str("revanced_sb_skipped_multiple_segments"));
|
: str("revanced_sb_skipped_multiple_segments");
|
||||||
|
|
||||||
|
showToastShortWithTapAction(message, undoAutoSkipRangeToast);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "showSkippedSegmentToast failure", ex);
|
Logger.printException(() -> "showSkippedSegmentToast failure", ex);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -607,13 +719,128 @@ public class SegmentPlaybackController {
|
|||||||
}, delayToToastMilliseconds);
|
}, delayToToastMilliseconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void showToastShortWithTapAction(String messageToToast, Range<Long> rangeToUndo) {
|
||||||
|
Objects.requireNonNull(messageToToast);
|
||||||
|
Utils.verifyOnMainThread();
|
||||||
|
|
||||||
|
Context currentContext = SponsorBlockViewController.getOverLaysViewGroupContext();
|
||||||
|
if (currentContext == null) {
|
||||||
|
Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
||||||
|
|
||||||
|
Dialog dialog = new Dialog(currentContext);
|
||||||
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
// Do not dismiss dialog if tapped outside the dialog bounds.
|
||||||
|
dialog.setCanceledOnTouchOutside(false);
|
||||||
|
|
||||||
|
LinearLayout mainLayout = new LinearLayout(currentContext);
|
||||||
|
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
final int dip8 = dipToPixels(8);
|
||||||
|
final int dip16 = dipToPixels(16);
|
||||||
|
mainLayout.setPadding(dip16, dip8, dip16, dip8);
|
||||||
|
mainLayout.setGravity(Gravity.CENTER);
|
||||||
|
mainLayout.setMinimumHeight(dipToPixels(48));
|
||||||
|
|
||||||
|
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||||
|
Utils.createCornerRadii(20), null, null));
|
||||||
|
background.getPaint().setColor(Utils.getDialogBackgroundColor());
|
||||||
|
mainLayout.setBackground(background);
|
||||||
|
|
||||||
|
TextView textView = new TextView(currentContext);
|
||||||
|
textView.setText(messageToToast);
|
||||||
|
textView.setTextSize(14);
|
||||||
|
textView.setTextColor(Utils.getAppForegroundColor());
|
||||||
|
textView.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
textParams.gravity = Gravity.CENTER;
|
||||||
|
textView.setLayoutParams(textParams);
|
||||||
|
mainLayout.addView(textView);
|
||||||
|
mainLayout.setAlpha(0.8f); // Opacity for the entire dialog.
|
||||||
|
|
||||||
|
final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
|
||||||
|
Animation fadeIn = Utils.getResourceAnimation("fade_in");
|
||||||
|
Animation fadeOut = Utils.getResourceAnimation("fade_out");
|
||||||
|
fadeIn.setDuration(fadeDurationFast);
|
||||||
|
fadeOut.setDuration(fadeDurationFast);
|
||||||
|
fadeOut.setAnimationListener(new Animation.AnimationListener() {
|
||||||
|
public void onAnimationStart(Animation animation) { }
|
||||||
|
public void onAnimationEnd(Animation animation) {
|
||||||
|
if (dialog.isShowing()) {
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void onAnimationRepeat(Animation animation) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
mainLayout.setOnClickListener(v -> {
|
||||||
|
try {
|
||||||
|
Logger.printDebug(() -> "Undoing autoskip using range: " + rangeToUndo);
|
||||||
|
// Restore undo autoskip range since it's already cleared by now.
|
||||||
|
undoAutoSkipRange = rangeToUndo;
|
||||||
|
VideoInformation.seekTo(rangeToUndo.getLower());
|
||||||
|
|
||||||
|
mainLayout.startAnimation(fadeOut);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "showToastShortWithTapAction setOnClickListener failure", ex);
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mainLayout.setClickable(true);
|
||||||
|
dialog.setContentView(mainLayout);
|
||||||
|
|
||||||
|
Window window = dialog.getWindow();
|
||||||
|
if (window != null) {
|
||||||
|
// Remove window animations and use custom fade animation.
|
||||||
|
window.setWindowAnimations(0);
|
||||||
|
|
||||||
|
WindowManager.LayoutParams params = window.getAttributes();
|
||||||
|
params.gravity = Gravity.BOTTOM;
|
||||||
|
params.y = dipToPixels(72);
|
||||||
|
DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
|
||||||
|
int portraitWidth = (int) (displayMetrics.widthPixels * 0.6);
|
||||||
|
|
||||||
|
if (Resources.getSystem().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
|
portraitWidth = (int) Math.min(portraitWidth, displayMetrics.heightPixels * 0.6);
|
||||||
|
}
|
||||||
|
params.width = portraitWidth;
|
||||||
|
params.dimAmount = 0.0f;
|
||||||
|
window.setAttributes(params);
|
||||||
|
window.setBackgroundDrawable(null);
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog priorDialog = toastDialogRef.get();
|
||||||
|
if (priorDialog != null && priorDialog.isShowing()) {
|
||||||
|
Logger.printDebug(() -> "Removing previous skip toast that is still on screen: " + priorDialog);
|
||||||
|
priorDialog.dismiss();
|
||||||
|
}
|
||||||
|
toastDialogRef = new WeakReference<>(dialog);
|
||||||
|
|
||||||
|
mainLayout.startAnimation(fadeIn);
|
||||||
|
dialog.show();
|
||||||
|
|
||||||
|
// Fade out and dismiss the dialog if the user does not undo the skip.
|
||||||
|
Utils.runOnMainThreadDelayed(() -> {
|
||||||
|
if (dialog.isShowing()) {
|
||||||
|
mainLayout.startAnimation(fadeOut);
|
||||||
|
}
|
||||||
|
}, getToastDuration());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param segment can be either a highlight or a regular manual skip segment.
|
* @param segment can be either a highlight or a regular manual skip segment.
|
||||||
*/
|
*/
|
||||||
public static void onSkipSegmentClicked(@NonNull SponsorSegment segment) {
|
public static void onSkipSegmentClicked(SponsorSegment segment) {
|
||||||
try {
|
try {
|
||||||
if (segment != highlightSegment && segment != segmentCurrentlyPlaying) {
|
if (segment != highlightSegment && segment != segmentCurrentlyPlaying) {
|
||||||
Logger.printException(() -> "error: segment not available to skip"); // should never happen
|
Logger.printException(() -> "error: segment not available to skip"); // Should never happen.
|
||||||
SponsorBlockViewController.hideSkipSegmentButton();
|
SponsorBlockViewController.hideSkipSegmentButton();
|
||||||
SponsorBlockViewController.hideSkipHighlightButton();
|
SponsorBlockViewController.hideSkipHighlightButton();
|
||||||
return;
|
return;
|
||||||
@@ -628,7 +855,7 @@ public class SegmentPlaybackController {
|
|||||||
* Injection point
|
* Injection point
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static void setSponsorBarRect(final Object self) {
|
public static void setSponsorBarRect(Object self) {
|
||||||
try {
|
try {
|
||||||
Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect");
|
Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect");
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
@@ -651,7 +878,7 @@ public class SegmentPlaybackController {
|
|||||||
private static void setSponsorBarAbsoluteRight(Rect rect) {
|
private static void setSponsorBarAbsoluteRight(Rect rect) {
|
||||||
final int right = rect.right;
|
final int right = rect.right;
|
||||||
if (sponsorAbsoluteBarRight != right) {
|
if (sponsorAbsoluteBarRight != right) {
|
||||||
Logger.printDebug(() -> "setSponsorBarAbsoluteRight: " + right);
|
Logger.printDebug(() -> "setSponsorBarAbsoluteRight: " + right);
|
||||||
sponsorAbsoluteBarRight = right;
|
sponsorAbsoluteBarRight = right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -726,12 +953,6 @@ public class SegmentPlaybackController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Actual screen pixel width to use for the highlight segment time bar.
|
|
||||||
*/
|
|
||||||
private static final int highlightSegmentTimeBarScreenWidth
|
|
||||||
= Utils.dipToPixels(HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@@ -752,9 +973,9 @@ public class SegmentPlaybackController {
|
|||||||
final float left = leftPadding + segment.start * videoMillisecondsToPixels;
|
final float left = leftPadding + segment.start * videoMillisecondsToPixels;
|
||||||
final float right;
|
final float right;
|
||||||
if (segment.category == SegmentCategory.HIGHLIGHT) {
|
if (segment.category == SegmentCategory.HIGHLIGHT) {
|
||||||
right = left + highlightSegmentTimeBarScreenWidth;
|
right = left + HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH;
|
||||||
} else {
|
} else {
|
||||||
right = leftPadding + segment.end * videoMillisecondsToPixels;
|
right = leftPadding + segment.end * videoMillisecondsToPixels;
|
||||||
}
|
}
|
||||||
canvas.drawRect(left, top, right, bottom, segment.category.paint);
|
canvas.drawRect(left, top, right, bottom, segment.category.paint);
|
||||||
}
|
}
|
||||||
@@ -762,5 +983,4 @@ public class SegmentPlaybackController {
|
|||||||
Logger.printException(() -> "drawSponsorTimeBars failure", ex);
|
Logger.printException(() -> "drawSponsorTimeBars failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,13 +223,18 @@ public class SponsorBlockUtils {
|
|||||||
Logger.printException(() -> "invalid parameters");
|
Logger.printException(() -> "invalid parameters");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearUnsubmittedSegmentTimes();
|
clearUnsubmittedSegmentTimes();
|
||||||
Utils.runOnBackgroundThread(() -> {
|
Utils.runOnBackgroundThread(() -> {
|
||||||
SBRequester.submitSegments(videoId, segmentCategory.keyValue, start, end, videoLength);
|
try {
|
||||||
SegmentPlaybackController.executeDownloadSegments(videoId);
|
SBRequester.submitSegments(videoId, segmentCategory.keyValue, start, end, videoLength);
|
||||||
|
SegmentPlaybackController.executeDownloadSegments(videoId);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "submitNewSegment failure", ex);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Unable to submit segment", e);
|
Logger.printException(() -> "submitNewSegment failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +371,7 @@ public class SponsorBlockUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void sendViewRequestAsync(@NonNull SponsorSegment segment) {
|
static void sendViewRequestAsync(SponsorSegment segment) {
|
||||||
if (segment.recordedAsSkipped || segment.category == SegmentCategory.UNSUBMITTED) {
|
if (segment.recordedAsSkipped || segment.category == SegmentCategory.UNSUBMITTED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -409,7 +414,7 @@ public class SponsorBlockUtils {
|
|||||||
return statsNumberFormatter.format(viewCount);
|
return statsNumberFormatter.format(viewCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long parseSegmentTime(@NonNull String time) {
|
private static long parseSegmentTime(String time) {
|
||||||
Matcher matcher = manualEditTimePattern.matcher(time);
|
Matcher matcher = manualEditTimePattern.matcher(time);
|
||||||
if (!matcher.matches()) {
|
if (!matcher.matches()) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -419,9 +424,12 @@ public class SponsorBlockUtils {
|
|||||||
String secondsStr = matcher.group(4);
|
String secondsStr = matcher.group(4);
|
||||||
String millisecondsStr = matcher.group(6); // Milliseconds is optional.
|
String millisecondsStr = matcher.group(6); // Milliseconds is optional.
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final int hours = (hoursStr != null) ? Integer.parseInt(hoursStr) : 0;
|
final int hours = (hoursStr != null) ? Integer.parseInt(hoursStr) : 0;
|
||||||
|
//noinspection ConstantConditions
|
||||||
final int minutes = Integer.parseInt(minutesStr);
|
final int minutes = Integer.parseInt(minutesStr);
|
||||||
|
//noinspection ConstantConditions
|
||||||
final int seconds = Integer.parseInt(secondsStr);
|
final int seconds = Integer.parseInt(secondsStr);
|
||||||
final int milliseconds;
|
final int milliseconds;
|
||||||
if (millisecondsStr != null) {
|
if (millisecondsStr != null) {
|
||||||
@@ -468,32 +476,29 @@ public class SponsorBlockUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getTimeSavedString(long totalSecondsSaved) {
|
public static String getTimeSavedString(long totalSecondsSaved) {
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
Duration duration = Duration.ofSeconds(totalSecondsSaved);
|
||||||
Duration duration = Duration.ofSeconds(totalSecondsSaved);
|
final long hours = duration.toHours();
|
||||||
final long hours = duration.toHours();
|
final long minutes = duration.toMinutes() % 60;
|
||||||
final long minutes = duration.toMinutes() % 60;
|
|
||||||
|
|
||||||
// Format all numbers so non-western numbers use a consistent appearance.
|
// Format all numbers so non-western numbers use a consistent appearance.
|
||||||
String minutesFormatted = statsNumberFormatter.format(minutes);
|
String minutesFormatted = statsNumberFormatter.format(minutes);
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
String hoursFormatted = statsNumberFormatter.format(hours);
|
String hoursFormatted = statsNumberFormatter.format(hours);
|
||||||
return str("revanced_sb_stats_saved_hour_format", hoursFormatted, minutesFormatted);
|
return str("revanced_sb_stats_saved_hour_format", hoursFormatted, minutesFormatted);
|
||||||
}
|
|
||||||
|
|
||||||
final long seconds = duration.getSeconds() % 60;
|
|
||||||
String secondsFormatted = statsNumberFormatter.format(seconds);
|
|
||||||
if (minutes > 0) {
|
|
||||||
return str("revanced_sb_stats_saved_minute_format", minutesFormatted, secondsFormatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
return str("revanced_sb_stats_saved_second_format", secondsFormatted);
|
|
||||||
}
|
}
|
||||||
return "error"; // will never be reached. YouTube requires Android O or greater
|
|
||||||
|
final long seconds = duration.getSeconds() % 60;
|
||||||
|
String secondsFormatted = statsNumberFormatter.format(seconds);
|
||||||
|
if (minutes > 0) {
|
||||||
|
return str("revanced_sb_stats_saved_minute_format", minutesFormatted, secondsFormatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return str("revanced_sb_stats_saved_second_format", secondsFormatted);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class EditByHandSaveDialogListener implements DialogInterface.OnClickListener {
|
private static class EditByHandSaveDialogListener implements DialogInterface.OnClickListener {
|
||||||
boolean settingStart;
|
private boolean settingStart;
|
||||||
WeakReference<EditText> editTextRef = new WeakReference<>(null);
|
private WeakReference<EditText> editTextRef = new WeakReference<>(null);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
@@ -512,10 +517,11 @@ public class SponsorBlockUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingStart)
|
if (settingStart) {
|
||||||
newSponsorSegmentStartMillis = Math.max(time, 0);
|
newSponsorSegmentStartMillis = Math.max(time, 0);
|
||||||
else
|
} else {
|
||||||
newSponsorSegmentEndMillis = time;
|
newSponsorSegmentEndMillis = time;
|
||||||
|
}
|
||||||
|
|
||||||
if (which == DialogInterface.BUTTON_NEUTRAL)
|
if (which == DialogInterface.BUTTON_NEUTRAL)
|
||||||
editByHandDialogListener.onClick(dialog, settingStart ?
|
editByHandDialogListener.onClick(dialog, settingStart ?
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import java.util.Objects;
|
|||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.sf;
|
import static app.revanced.extension.shared.StringRef.sf;
|
||||||
|
|
||||||
|
import android.util.Range;
|
||||||
|
|
||||||
public class SponsorSegment implements Comparable<SponsorSegment> {
|
public class SponsorSegment implements Comparable<SponsorSegment> {
|
||||||
|
|
||||||
public enum SegmentVote {
|
public enum SegmentVote {
|
||||||
UPVOTE(sf("revanced_sb_vote_upvote"), 1,false),
|
UPVOTE(sf("revanced_sb_vote_upvote"), 1,false),
|
||||||
DOWNVOTE(sf("revanced_sb_vote_downvote"), 0, true),
|
DOWNVOTE(sf("revanced_sb_vote_downvote"), 0, true),
|
||||||
@@ -38,7 +41,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
|||||||
@NonNull
|
@NonNull
|
||||||
public final SegmentCategory category;
|
public final SegmentCategory category;
|
||||||
/**
|
/**
|
||||||
* NULL if segment is unsubmitted
|
* NULL if segment is unsubmitted.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public final String UUID;
|
public final String UUID;
|
||||||
@@ -64,33 +67,54 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number
|
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number.
|
||||||
*/
|
*/
|
||||||
public boolean startIsNear(long videoTime, long nearThreshold) {
|
public boolean startIsNear(long videoTime, long nearThreshold) {
|
||||||
return Math.abs(start - videoTime) <= nearThreshold;
|
return Math.abs(start - videoTime) <= nearThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number
|
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number.
|
||||||
*/
|
*/
|
||||||
public boolean endIsNear(long videoTime, long nearThreshold) {
|
public boolean endIsNear(long videoTime, long nearThreshold) {
|
||||||
return Math.abs(end - videoTime) <= nearThreshold;
|
return Math.abs(end - videoTime) <= nearThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the time parameter is within this segment
|
* @return if the time parameter is within this segment.
|
||||||
*/
|
*/
|
||||||
public boolean containsTime(long videoTime) {
|
public boolean containsTime(long videoTime) {
|
||||||
return start <= videoTime && videoTime < end;
|
return start <= videoTime && videoTime < end;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the segment is completely contained inside this segment
|
* @return if the segment is completely contained inside this segment.
|
||||||
*/
|
*/
|
||||||
public boolean containsSegment(SponsorSegment other) {
|
public boolean containsSegment(SponsorSegment other) {
|
||||||
return start <= other.start && other.end <= end;
|
return start <= other.start && other.end <= end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return If the range has any overlap with this segment.
|
||||||
|
*/
|
||||||
|
public boolean intersectsRange(Range<Long> range) {
|
||||||
|
return range.getLower() < end && range.getUpper() >= start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The start/end time in range form.
|
||||||
|
* Range times are adjusted since it uses inclusive and Segments use exclusive.
|
||||||
|
*
|
||||||
|
* {@link SegmentCategory#HIGHLIGHT} is unique and
|
||||||
|
* returns a range from the start of the video until the highlight.
|
||||||
|
*/
|
||||||
|
public Range<Long> getUndoRange() {
|
||||||
|
final long undoStart = category == SegmentCategory.HIGHLIGHT
|
||||||
|
? 0
|
||||||
|
: start;
|
||||||
|
return Range.create(undoStart, end - 1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the length of this segment, in milliseconds. Always a positive number.
|
* @return the length of this segment, in milliseconds. Always a positive number.
|
||||||
*/
|
*/
|
||||||
@@ -99,7 +123,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return 'skip segment' UI overlay button text
|
* @return 'skip segment' UI overlay button text.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public String getSkipButtonText() {
|
public String getSkipButtonText() {
|
||||||
@@ -107,7 +131,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return 'skipped segment' toast message
|
* @return 'skipped segment' toast message.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public String getSkippedToastText() {
|
public String getSkippedToastText() {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class SBRequester {
|
|||||||
private SBRequester() {
|
private SBRequester() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) {
|
private static void handleConnectionError(String toastMessage, @Nullable Exception ex) {
|
||||||
if (Settings.SB_TOAST_ON_CONNECTION_ERROR.get()) {
|
if (Settings.SB_TOAST_ON_CONNECTION_ERROR.get()) {
|
||||||
Utils.showToastShort(toastMessage);
|
Utils.showToastShort(toastMessage);
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ public class SBRequester {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static SponsorSegment[] getSegments(@NonNull String videoId) {
|
public static SponsorSegment[] getSegments(String videoId) {
|
||||||
Utils.verifyOffMainThread();
|
Utils.verifyOffMainThread();
|
||||||
List<SponsorSegment> segments = new ArrayList<>();
|
List<SponsorSegment> segments = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
@@ -113,10 +113,10 @@ public class SBRequester {
|
|||||||
Logger.printException(() -> "getSegments failure", ex);
|
Logger.printException(() -> "getSegments failure", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crude debug tests to verify random features
|
// Crude debug tests to verify random features.
|
||||||
// Could benefit from:
|
// Could benefit from:
|
||||||
// 1) collection of YouTube videos with test segment times (verify client skip timing matches the video, verify seekbar draws correctly)
|
// 1) Collection of YouTube videos with test segment times (verify client skip timing matches the video, verify seekbar draws correctly).
|
||||||
// 2) unit tests (verify everything else)
|
// 2) Unit tests (verify everything else).
|
||||||
//noinspection ConstantValue
|
//noinspection ConstantValue
|
||||||
if (false) {
|
if (false) {
|
||||||
segments.clear();
|
segments.clear();
|
||||||
@@ -140,10 +140,30 @@ public class SBRequester {
|
|||||||
segments.add(new SponsorSegment(SegmentCategory.SELF_PROMO, "debug", 200000, 330000, false));
|
segments.add(new SponsorSegment(SegmentCategory.SELF_PROMO, "debug", 200000, 330000, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test undo skip functionality.
|
||||||
|
// To test enable 'Autoskip always' for intro and self promo.
|
||||||
|
//noinspection ConstantValue
|
||||||
|
if (false) {
|
||||||
|
// Should autoskip to 12 seconds.
|
||||||
|
// Undoing skip should seek to 2 seconds.
|
||||||
|
// Skip button should show at 2 seconds, and again at 8 seconds.
|
||||||
|
// Self promo at 8 second time should not autoskip.
|
||||||
|
segments.clear();
|
||||||
|
segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 2000, 12000, false));
|
||||||
|
segments.add(new SponsorSegment(SegmentCategory.SELF_PROMO, "debug", 8000, 15000, false));
|
||||||
|
|
||||||
|
// Test multiple autoskip dialogs rapidly showing.
|
||||||
|
// Only one toast should be shown at anytime.
|
||||||
|
segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 16000, 17000, false));
|
||||||
|
segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 18000, 19000, false));
|
||||||
|
segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 20000, 21000, false));
|
||||||
|
segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 22000, 23000, false));
|
||||||
|
}
|
||||||
|
|
||||||
return segments.toArray(new SponsorSegment[0]);
|
return segments.toArray(new SponsorSegment[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void submitSegments(@NonNull String videoId, @NonNull String category,
|
public static void submitSegments(String videoId, String category,
|
||||||
long startTime, long endTime, long videoLength) {
|
long startTime, long endTime, long videoLength) {
|
||||||
Utils.verifyOffMainThread();
|
Utils.verifyOffMainThread();
|
||||||
|
|
||||||
@@ -189,7 +209,7 @@ public class SBRequester {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void sendSegmentSkippedViewedRequest(@NonNull SponsorSegment segment) {
|
public static void sendSegmentSkippedViewedRequest(SponsorSegment segment) {
|
||||||
Utils.verifyOffMainThread();
|
Utils.verifyOffMainThread();
|
||||||
try {
|
try {
|
||||||
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.VIEWED_SEGMENT, segment.UUID);
|
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.VIEWED_SEGMENT, segment.UUID);
|
||||||
@@ -208,13 +228,13 @@ public class SBRequester {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void voteForSegmentOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption) {
|
public static void voteForSegmentOnBackgroundThread(SponsorSegment segment, SegmentVote voteOption) {
|
||||||
voteOrRequestCategoryChange(segment, voteOption, null);
|
voteOrRequestCategoryChange(segment, voteOption, null);
|
||||||
}
|
}
|
||||||
public static void voteToChangeCategoryOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentCategory categoryToVoteFor) {
|
public static void voteToChangeCategoryOnBackgroundThread(SponsorSegment segment, SegmentCategory categoryToVoteFor) {
|
||||||
voteOrRequestCategoryChange(segment, SegmentVote.CATEGORY_CHANGE, categoryToVoteFor);
|
voteOrRequestCategoryChange(segment, SegmentVote.CATEGORY_CHANGE, categoryToVoteFor);
|
||||||
}
|
}
|
||||||
private static void voteOrRequestCategoryChange(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption, SegmentCategory categoryToVoteFor) {
|
private static void voteOrRequestCategoryChange(SponsorSegment segment, SegmentVote voteOption, SegmentCategory categoryToVoteFor) {
|
||||||
Utils.runOnBackgroundThread(() -> {
|
Utils.runOnBackgroundThread(() -> {
|
||||||
try {
|
try {
|
||||||
String segmentUuid = segment.UUID;
|
String segmentUuid = segment.UUID;
|
||||||
@@ -280,7 +300,7 @@ public class SBRequester {
|
|||||||
* @return NULL if the call was successful. If unsuccessful, an error message is returned.
|
* @return NULL if the call was successful. If unsuccessful, an error message is returned.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public static String setUsername(@NonNull String username) {
|
public static String setUsername(String username) {
|
||||||
Utils.verifyOffMainThread();
|
Utils.verifyOffMainThread();
|
||||||
try {
|
try {
|
||||||
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SponsorBlockSettings.getSBPrivateUserID(), username);
|
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SponsorBlockSettings.getSBPrivateUserID(), username);
|
||||||
@@ -320,14 +340,14 @@ public class SBRequester {
|
|||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
|
|
||||||
private static HttpURLConnection getConnectionFromRoute(@NonNull Route route, String... params) throws IOException {
|
private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException {
|
||||||
HttpURLConnection connection = Requester.getConnectionFromRoute(Settings.SB_API_URL.get(), route, params);
|
HttpURLConnection connection = Requester.getConnectionFromRoute(Settings.SB_API_URL.get(), route, params);
|
||||||
connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS);
|
connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS);
|
||||||
connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS);
|
connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS);
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JSONObject getJSONObject(@NonNull Route route, String... params) throws IOException, JSONException {
|
private static JSONObject getJSONObject(Route route, String... params) throws IOException, JSONException {
|
||||||
return Requester.parseJSONObject(getConnectionFromRoute(route, params));
|
return Requester.parseJSONObject(getConnectionFromRoute(route, params));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.youtube.sponsorblock.ui;
|
package app.revanced.extension.youtube.sponsorblock.ui;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController.SponsorBlockDuration;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
@@ -28,6 +29,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
||||||
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
|
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
|
||||||
@@ -62,6 +64,8 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
private SwitchPreference trackSkips;
|
private SwitchPreference trackSkips;
|
||||||
private SwitchPreference showTimeWithoutSegments;
|
private SwitchPreference showTimeWithoutSegments;
|
||||||
private SwitchPreference toastOnConnectionError;
|
private SwitchPreference toastOnConnectionError;
|
||||||
|
private CustomDialogListPreference autoHideSkipSegmentButtonDuration;
|
||||||
|
private CustomDialogListPreference showSkipToastDuration;
|
||||||
|
|
||||||
private ResettableEditTextPreference newSegmentStep;
|
private ResettableEditTextPreference newSegmentStep;
|
||||||
private ResettableEditTextPreference minSegmentDuration;
|
private ResettableEditTextPreference minSegmentDuration;
|
||||||
@@ -69,8 +73,8 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
private EditTextPreference importExport;
|
private EditTextPreference importExport;
|
||||||
private Preference apiUrl;
|
private Preference apiUrl;
|
||||||
|
|
||||||
private final List<SegmentCategoryListPreference> segmentCategories = new ArrayList<>();
|
|
||||||
private PreferenceCategory segmentCategory;
|
private PreferenceCategory segmentCategory;
|
||||||
|
private final List<SegmentCategoryListPreference> segmentCategories = new ArrayList<>();
|
||||||
|
|
||||||
public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
@@ -114,17 +118,23 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get());
|
votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get());
|
||||||
votingEnabled.setEnabled(enabled);
|
votingEnabled.setEnabled(enabled);
|
||||||
|
|
||||||
autoHideSkipSegmentButton.setEnabled(enabled);
|
|
||||||
autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get());
|
autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get());
|
||||||
|
autoHideSkipSegmentButton.setEnabled(enabled);
|
||||||
|
|
||||||
|
autoHideSkipSegmentButtonDuration.setValue(Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.get().toString());
|
||||||
|
autoHideSkipSegmentButtonDuration.setEnabled(Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.isAvailable());
|
||||||
|
|
||||||
compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get());
|
compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get());
|
||||||
compactSkipButton.setEnabled(enabled);
|
compactSkipButton.setEnabled(enabled);
|
||||||
|
|
||||||
|
showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get());
|
||||||
|
showSkipToast.setEnabled(enabled);
|
||||||
|
|
||||||
squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get());
|
squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get());
|
||||||
squareLayout.setEnabled(enabled);
|
squareLayout.setEnabled(enabled);
|
||||||
|
|
||||||
showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get());
|
showSkipToastDuration.setValue(Settings.SB_TOAST_ON_SKIP_DURATION.get().toString());
|
||||||
showSkipToast.setEnabled(enabled);
|
showSkipToastDuration.setEnabled(Settings.SB_TOAST_ON_SKIP_DURATION.isAvailable());
|
||||||
|
|
||||||
toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get());
|
toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get());
|
||||||
toastOnConnectionError.setEnabled(enabled);
|
toastOnConnectionError.setEnabled(enabled);
|
||||||
@@ -166,7 +176,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
try {
|
try {
|
||||||
super.onAttachedToActivity();
|
super.onAttachedToActivity();
|
||||||
|
|
||||||
if (preferencesInitialized) {
|
if (preferencesInitialized) {
|
||||||
if (settingsImported) {
|
if (settingsImported) {
|
||||||
settingsImported = false;
|
settingsImported = false;
|
||||||
updateUI();
|
updateUI();
|
||||||
@@ -205,17 +215,6 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
});
|
});
|
||||||
appearanceCategory.addPreference(votingEnabled);
|
appearanceCategory.addPreference(votingEnabled);
|
||||||
|
|
||||||
autoHideSkipSegmentButton = new SwitchPreference(context);
|
|
||||||
autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button"));
|
|
||||||
autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on"));
|
|
||||||
autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off"));
|
|
||||||
autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
appearanceCategory.addPreference(autoHideSkipSegmentButton);
|
|
||||||
|
|
||||||
compactSkipButton = new SwitchPreference(context);
|
compactSkipButton = new SwitchPreference(context);
|
||||||
compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button"));
|
compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button"));
|
||||||
compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on"));
|
compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on"));
|
||||||
@@ -227,25 +226,38 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
});
|
});
|
||||||
appearanceCategory.addPreference(compactSkipButton);
|
appearanceCategory.addPreference(compactSkipButton);
|
||||||
|
|
||||||
squareLayout = new SwitchPreference(context);
|
autoHideSkipSegmentButton = new SwitchPreference(context);
|
||||||
squareLayout.setTitle(str("revanced_sb_square_layout"));
|
autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button"));
|
||||||
squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on"));
|
autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on"));
|
||||||
squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off"));
|
autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off"));
|
||||||
squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
|
autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
|
Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
|
||||||
updateUI();
|
updateUI();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
appearanceCategory.addPreference(squareLayout);
|
appearanceCategory.addPreference(autoHideSkipSegmentButton);
|
||||||
|
|
||||||
|
String[] durationEntries = Utils.getResourceStringArray("revanced_sb_duration_entries");
|
||||||
|
String[] durationEntryValues = Utils.getResourceStringArray("revanced_sb_duration_entry_values");
|
||||||
|
|
||||||
|
autoHideSkipSegmentButtonDuration = new CustomDialogListPreference(context);
|
||||||
|
autoHideSkipSegmentButtonDuration.setTitle(str("revanced_sb_auto_hide_skip_button_duration"));
|
||||||
|
autoHideSkipSegmentButtonDuration.setSummary(str("revanced_sb_auto_hide_skip_button_duration_sum"));
|
||||||
|
autoHideSkipSegmentButtonDuration.setEntries(durationEntries);
|
||||||
|
autoHideSkipSegmentButtonDuration.setEntryValues(durationEntryValues);
|
||||||
|
autoHideSkipSegmentButtonDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.save(
|
||||||
|
SponsorBlockDuration.valueOf((String) newValue)
|
||||||
|
);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(autoHideSkipSegmentButtonDuration);
|
||||||
|
|
||||||
showSkipToast = new SwitchPreference(context);
|
showSkipToast = new SwitchPreference(context);
|
||||||
showSkipToast.setTitle(str("revanced_sb_general_skiptoast"));
|
showSkipToast.setTitle(str("revanced_sb_general_skiptoast"));
|
||||||
showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on"));
|
showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on"));
|
||||||
showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off"));
|
showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off"));
|
||||||
showSkipToast.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
Utils.showToastShort(str("revanced_sb_skipped_sponsor"));
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
|
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue);
|
Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue);
|
||||||
updateUI();
|
updateUI();
|
||||||
@@ -253,6 +265,20 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
});
|
});
|
||||||
appearanceCategory.addPreference(showSkipToast);
|
appearanceCategory.addPreference(showSkipToast);
|
||||||
|
|
||||||
|
showSkipToastDuration = new CustomDialogListPreference(context);
|
||||||
|
showSkipToastDuration.setTitle(str("revanced_sb_toast_on_skip_duration"));
|
||||||
|
showSkipToastDuration.setSummary(str("revanced_sb_toast_on_skip_duration_sum"));
|
||||||
|
showSkipToastDuration.setEntries(durationEntries);
|
||||||
|
showSkipToastDuration.setEntryValues(durationEntryValues);
|
||||||
|
showSkipToastDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_TOAST_ON_SKIP_DURATION.save(
|
||||||
|
SponsorBlockDuration.valueOf((String) newValue)
|
||||||
|
);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(showSkipToastDuration);
|
||||||
|
|
||||||
showTimeWithoutSegments = new SwitchPreference(context);
|
showTimeWithoutSegments = new SwitchPreference(context);
|
||||||
showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without"));
|
showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without"));
|
||||||
showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on"));
|
showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on"));
|
||||||
@@ -264,6 +290,17 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
});
|
});
|
||||||
appearanceCategory.addPreference(showTimeWithoutSegments);
|
appearanceCategory.addPreference(showTimeWithoutSegments);
|
||||||
|
|
||||||
|
squareLayout = new SwitchPreference(context);
|
||||||
|
squareLayout.setTitle(str("revanced_sb_square_layout"));
|
||||||
|
squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on"));
|
||||||
|
squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off"));
|
||||||
|
squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(squareLayout);
|
||||||
|
|
||||||
segmentCategory = new PreferenceCategory(context);
|
segmentCategory = new PreferenceCategory(context);
|
||||||
segmentCategory.setTitle(str("revanced_sb_diff_segments"));
|
segmentCategory.setTitle(str("revanced_sb_diff_segments"));
|
||||||
addPreference(segmentCategory);
|
addPreference(segmentCategory);
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ public class SponsorBlockViewController {
|
|||||||
setSkipButtonMargins(skipSponsorButton, isWatchFullScreen);
|
setSkipButtonMargins(skipSponsorButton, isWatchFullScreen);
|
||||||
setViewVisibility(skipSponsorButton, skipSegment != null);
|
setViewVisibility(skipSponsorButton, skipSegment != null);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Player type changed failure", ex);
|
Logger.printException(() -> "playerTypeChanged failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
alias(libs.plugins.android.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
|
|||||||
org.gradle.parallel = true
|
org.gradle.parallel = true
|
||||||
android.useAndroidX = true
|
android.useAndroidX = true
|
||||||
kotlin.code.style = official
|
kotlin.code.style = official
|
||||||
version = 5.29.0-dev.3
|
version = 5.30.0-dev.4
|
||||||
|
|||||||
@@ -11,14 +11,25 @@ appcompat = "1.7.0"
|
|||||||
okhttp = "5.0.0-alpha.14"
|
okhttp = "5.0.0-alpha.14"
|
||||||
retrofit = "2.11.0"
|
retrofit = "2.11.0"
|
||||||
guava = "33.4.0-jre"
|
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]
|
[libraries]
|
||||||
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
|
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" }
|
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" }
|
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" }
|
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||||
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
||||||
|
apksig = { group = "com.android.tools.build", name = "apksig", version.ref = "apksig" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
android-library = { id = "com.android.library" }
|
||||||
|
protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }
|
||||||
|
|||||||
6
gradle/wrapper/gradle-wrapper.properties
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,8 +1,6 @@
|
|||||||
|
#Mon Jun 16 14:39:32 CEST 2025
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -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 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 final class app/revanced/patches/all/misc/targetSdk/SetTargetSdkVersion34Kt {
|
||||||
public static final fun getSetTargetSdkVersion34 ()Lapp/revanced/patcher/patch/ResourcePatch;
|
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 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 final class app/revanced/patches/crunchyroll/ads/HideAdsPatchKt {
|
||||||
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
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 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 final class app/revanced/patches/shared/misc/hex/HexPatchBuilder : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker {
|
||||||
public static final fun hexPatch (Lkotlin/jvm/functions/Function0;)Lapp/revanced/patcher/patch/RawResourcePatch;
|
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 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 fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
|
||||||
public final fun replacePattern ([B)V
|
public fun <init> ([B[BLjava/lang/String;)V
|
||||||
}
|
public final fun getReplacementBytesPadded ()[B
|
||||||
|
|
||||||
public final class app/revanced/patches/shared/misc/hex/Replacement$Companion {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/shared/misc/mapping/ResourceElement {
|
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 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 final class app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatchKt {
|
||||||
public static final fun getSpoofPackageInfoPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getSpoofPackageInfoPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ patches {
|
|||||||
dependencies {
|
dependencies {
|
||||||
// Required due to smali, or build fails. Can be removed once smali is bumped.
|
// Required due to smali, or build fails. Can be removed once smali is bumped.
|
||||||
implementation(libs.guava)
|
implementation(libs.guava)
|
||||||
|
|
||||||
|
implementation(libs.apksig)
|
||||||
|
|
||||||
// Android API stubs defined here.
|
// Android API stubs defined here.
|
||||||
compileOnly(project(":patches:stub"))
|
compileOnly(project(":patches:stub"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package app.revanced.patches.all.misc.hex
|
|||||||
import app.revanced.patcher.patch.PatchException
|
import app.revanced.patcher.patch.PatchException
|
||||||
import app.revanced.patcher.patch.rawResourcePatch
|
import app.revanced.patcher.patch.rawResourcePatch
|
||||||
import app.revanced.patcher.patch.stringsOption
|
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.patches.shared.misc.hex.hexPatch
|
||||||
import app.revanced.util.Utils.trimIndentMultiline
|
import app.revanced.util.Utils.trimIndentMultiline
|
||||||
|
|
||||||
@@ -13,9 +13,6 @@ val hexPatch = rawResourcePatch(
|
|||||||
description = "Replaces a hexadecimal patterns of bytes of files in an APK.",
|
description = "Replaces a hexadecimal patterns of bytes of files in an APK.",
|
||||||
use = false,
|
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(
|
val replacements by stringsOption(
|
||||||
key = "replacements",
|
key = "replacements",
|
||||||
title = "Replacements",
|
title = "Replacements",
|
||||||
@@ -27,30 +24,31 @@ val hexPatch = rawResourcePatch(
|
|||||||
|
|
||||||
Every pattern must be followed by a pipe ('|'), the replacement pattern,
|
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.
|
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:
|
Full example of a valid replacement:
|
||||||
'aa 01 02 FF|00 00 00 00|path/to/file'
|
'01 02 aa FF|03 04|path/to/file'
|
||||||
""".trimIndentMultiline(),
|
""".trimIndentMultiline(),
|
||||||
required = true,
|
required = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
dependsOn(
|
dependsOn(
|
||||||
hexPatch {
|
hexPatch(
|
||||||
replacements!!.map { from ->
|
block = fun HexPatchBuilder.() {
|
||||||
val (pattern, replacementPattern, targetFilePath) = try {
|
replacements!!.forEach { replacement ->
|
||||||
from.split("|", limit = 3)
|
try {
|
||||||
} catch (e: Exception) {
|
val (pattern, replacementPattern, targetFilePath) = replacement.split("|", limit = 3)
|
||||||
throw PatchException(
|
pattern asPatternTo replacementPattern inFile targetFilePath
|
||||||
"Invalid input: $from.\n" +
|
} catch (e: Exception) {
|
||||||
"Every pattern must be followed by a pipe ('|'), " +
|
throw PatchException(
|
||||||
"the replacement pattern, another pipe ('|'), " +
|
"Invalid replacement: $replacement.\n" +
|
||||||
"and the path to the file to make the changes in relative to the APK root. ",
|
"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()
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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\""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -93,18 +93,28 @@ internal val abstractProtobufListEnsureIsMutableFingerprint = fingerprint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val homeSectionFingerprint = fingerprint {
|
private fun structureGetSectionsFingerprint(className: String) = fingerprint {
|
||||||
custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val homeStructureGetSectionsFingerprint = fingerprint {
|
|
||||||
custom { method, classDef ->
|
custom { method, classDef ->
|
||||||
classDef.endsWith("homeapi/proto/HomeStructure;") && method.indexOfFirstInstruction {
|
classDef.endsWith(className) && method.indexOfFirstInstruction {
|
||||||
opcode == Opcode.IGET_OBJECT && getReference<FieldReference>()?.name == "sections_"
|
opcode == Opcode.IGET_OBJECT && getReference<FieldReference>()?.name == "sections_"
|
||||||
} >= 0
|
} >= 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal val homeSectionFingerprint = fingerprint {
|
||||||
|
custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val homeStructureGetSectionsFingerprint =
|
||||||
|
structureGetSectionsFingerprint("homeapi/proto/HomeStructure;")
|
||||||
|
|
||||||
|
internal val browseSectionFingerprint = fingerprint {
|
||||||
|
custom { _, classDef -> classDef.endsWith("browsita/v1/resolved/Section;") }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val browseStructureGetSectionsFingerprint =
|
||||||
|
structureGetSectionsFingerprint("browsita/v1/resolved/BrowseStructure;")
|
||||||
|
|
||||||
internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint {
|
internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint {
|
||||||
returns("Ljava/lang/Object;")
|
returns("Ljava/lang/Object;")
|
||||||
parameters("Ljava/lang/Object;")
|
parameters("Ljava/lang/Object;")
|
||||||
|
|||||||
@@ -1,9 +1,29 @@
|
|||||||
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 com.android.tools.smali.dexlib2.AccessFlags
|
||||||
|
|
||||||
internal val getPackageInfoFingerprint = fingerprint {
|
internal val getPackageInfoFingerprint = fingerprint {
|
||||||
strings(
|
strings(
|
||||||
"Failed to get the application signatures"
|
"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")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val standardIntegrityTokenProviderBuilderFingerprint = fingerprint {
|
||||||
|
strings(
|
||||||
|
"standard_pi_init",
|
||||||
|
"outcome",
|
||||||
|
"success"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
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 app.revanced.util.returnEarly
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early return to block sending bad verdicts to the API.
|
||||||
|
standardIntegrityTokenProviderBuilderFingerprint.method.returnEarly()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,63 +1,11 @@
|
|||||||
package app.revanced.patches.spotify.misc.fix
|
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.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")
|
@Suppress("unused")
|
||||||
val spoofPackageInfoPatch = bytecodePatch(
|
val spoofPackageInfoPatch = bytecodePatch(
|
||||||
name = "Spoof package info",
|
|
||||||
description = "Spoofs the package info of the app to fix various functions of the app.",
|
description = "Spoofs the package info of the app to fix various functions of the app.",
|
||||||
) {
|
) {
|
||||||
compatibleWith("com.spotify.music")
|
dependsOn(spoofClientPatch)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package app.revanced.patches.spotify.misc.fix
|
|||||||
|
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
|
||||||
@Deprecated("Superseded by spoofPackageInfoPatch", ReplaceWith("spoofPackageInfoPatch"))
|
@Deprecated("Superseded by spoofClientPatch", ReplaceWith("spoofClientPatch"))
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val spoofSignaturePatch = bytecodePatch(
|
val spoofSignaturePatch = bytecodePatch(
|
||||||
description = "Spoofs the signature of the app fix various functions of the app.",
|
description = "Spoofs the signature of the app fix various functions of the app.",
|
||||||
) {
|
) {
|
||||||
dependsOn(spoofPackageInfoPatch)
|
dependsOn(spoofClientPatch)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ val hideAdsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ val hideGetPremiumPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ val videoAdsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ val copyVideoUrlPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ val removeViewerDiscretionDialogPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ val downloadsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ val seekbarPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ val swipeControlsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ val autoCaptionsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ val customBrandingPatch = resourcePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ val changeHeaderPatch = resourcePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ val hideButtonsPatch = resourcePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,15 +39,16 @@ val hideButtonsPatch = resourcePatch(
|
|||||||
"revanced_hide_buttons_screen",
|
"revanced_hide_buttons_screen",
|
||||||
preferences = setOf(
|
preferences = setOf(
|
||||||
SwitchPreference("revanced_disable_like_subscribe_glow"),
|
SwitchPreference("revanced_disable_like_subscribe_glow"),
|
||||||
SwitchPreference("revanced_hide_like_dislike_button"),
|
|
||||||
SwitchPreference("revanced_hide_share_button"),
|
|
||||||
SwitchPreference("revanced_hide_report_button"),
|
|
||||||
SwitchPreference("revanced_hide_remix_button"),
|
|
||||||
SwitchPreference("revanced_hide_download_button"),
|
|
||||||
SwitchPreference("revanced_hide_thanks_button"),
|
|
||||||
SwitchPreference("revanced_hide_ask_button"),
|
SwitchPreference("revanced_hide_ask_button"),
|
||||||
SwitchPreference("revanced_hide_clip_button"),
|
SwitchPreference("revanced_hide_clip_button"),
|
||||||
SwitchPreference("revanced_hide_playlist_button"),
|
SwitchPreference("revanced_hide_download_button"),
|
||||||
|
SwitchPreference("revanced_hide_like_dislike_button"),
|
||||||
|
SwitchPreference("revanced_hide_remix_button"),
|
||||||
|
SwitchPreference("revanced_hide_report_button"),
|
||||||
|
SwitchPreference("revanced_hide_save_button"),
|
||||||
|
SwitchPreference("revanced_hide_share_button"),
|
||||||
|
SwitchPreference("revanced_hide_stop_ads_button"),
|
||||||
|
SwitchPreference("revanced_hide_thanks_button"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ val navigationButtonsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ val changeFormFactorPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ val hideEndscreenCardsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ val hideEndScreenSuggestedVideoPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ val disableFullscreenAmbientModePatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ val hideLayoutComponentsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -200,6 +201,7 @@ val hideLayoutComponentsPatch = bytecodePatch(
|
|||||||
key = "revanced_hide_filter_bar_screen",
|
key = "revanced_hide_filter_bar_screen",
|
||||||
preferences = setOf(
|
preferences = setOf(
|
||||||
SwitchPreference("revanced_hide_filter_bar_feed_in_feed"),
|
SwitchPreference("revanced_hide_filter_bar_feed_in_feed"),
|
||||||
|
SwitchPreference("revanced_hide_filter_bar_feed_in_history"),
|
||||||
SwitchPreference("revanced_hide_filter_bar_feed_in_search"),
|
SwitchPreference("revanced_hide_filter_bar_feed_in_search"),
|
||||||
SwitchPreference("revanced_hide_filter_bar_feed_in_related_videos"),
|
SwitchPreference("revanced_hide_filter_bar_feed_in_related_videos"),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ val hideInfoCardsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ val hidePlayerFlyoutMenuPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ val hideRelatedVideoOverlayPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ val disableRollingNumberAnimationPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -94,8 +94,10 @@ private val hideShortsComponentsResourcePatch = resourcePatch {
|
|||||||
// Suggested actions.
|
// Suggested actions.
|
||||||
SwitchPreference("revanced_hide_shorts_preview_comment"),
|
SwitchPreference("revanced_hide_shorts_preview_comment"),
|
||||||
SwitchPreference("revanced_hide_shorts_save_sound_button"),
|
SwitchPreference("revanced_hide_shorts_save_sound_button"),
|
||||||
|
SwitchPreference("revanced_hide_shorts_use_sound_button"),
|
||||||
SwitchPreference("revanced_hide_shorts_use_template_button"),
|
SwitchPreference("revanced_hide_shorts_use_template_button"),
|
||||||
SwitchPreference("revanced_hide_shorts_upcoming_button"),
|
SwitchPreference("revanced_hide_shorts_upcoming_button"),
|
||||||
|
SwitchPreference("revanced_hide_shorts_effect_button"),
|
||||||
SwitchPreference("revanced_hide_shorts_green_screen_button"),
|
SwitchPreference("revanced_hide_shorts_green_screen_button"),
|
||||||
SwitchPreference("revanced_hide_shorts_hashtag_button"),
|
SwitchPreference("revanced_hide_shorts_hashtag_button"),
|
||||||
SwitchPreference("revanced_hide_shorts_new_posts_button"),
|
SwitchPreference("revanced_hide_shorts_new_posts_button"),
|
||||||
@@ -175,6 +177,7 @@ val hideShortsComponentsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ val hideTimestampPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ val miniplayerPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ val playerPopupPanelsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ internal val exitFullscreenPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ val openVideosFullscreenPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ val customPlayerOverlayOpacityPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ val returnYouTubeDislikePatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ val wideSearchbarPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ val shortsAutoplayPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ val openShortsInRegularPlayerPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ val sponsorBlockPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ val spoofAppVersionPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ val changeStartPagePatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ val disableResumingShortsOnStartupPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ val themePatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ val alternativeThumbnailsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ val bypassImageRegionRestrictionsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ val announcementsPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ val autoRepeatPatch = bytecodePatch(
|
|||||||
"19.47.53",
|
"19.47.53",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.12.46",
|
"20.12.46",
|
||||||
|
"20.13.41",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user