From 8736b6a80b48cb1f4562c9f9919804006ddb18bd Mon Sep 17 00:00:00 2001 From: brosssh <44944126+brosssh@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:25:58 +0200 Subject: [PATCH 01/23] feat(Spotify): Add `Change lyrics provider` patch (#4937) --- .../spotify/shared/ComponentFilters.java | 8 +- patches/api/patches.api | 4 + .../createbutton/HideCreateButtonPatch.kt | 4 +- .../misc/fix/login/FixFacebookLoginPatch.kt | 2 +- .../misc/lyrics/ChangeLyricsProviderPatch.kt | 123 ++++++++++++++++++ .../spotify/misc/lyrics/Fingerprints.kt | 21 +++ 6 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/Fingerprints.kt diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/shared/ComponentFilters.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/shared/ComponentFilters.java index 0349c713b..21f1dd3e3 100644 --- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/shared/ComponentFilters.java +++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/shared/ComponentFilters.java @@ -1,11 +1,14 @@ package app.revanced.extension.spotify.shared; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; public final class ComponentFilters { public interface ComponentFilter { + @NonNull String getFilterValue(); String getFilterRepresentation(); default boolean filterUnavailable() { @@ -20,7 +23,8 @@ public final class ComponentFilters { // Android resources are always positive, so -1 is a valid sentinel value to indicate it has not been loaded. // 0 is returned when a resource has not been found. private int resourceId = -1; - private String stringfiedResourceId = null; + @Nullable + private String stringfiedResourceId; public ResourceIdComponentFilter(String resourceName, String resourceType) { this.resourceName = resourceName; @@ -34,6 +38,7 @@ public final class ComponentFilters { return resourceId; } + @NonNull @Override public String getFilterValue() { if (stringfiedResourceId == null) { @@ -66,6 +71,7 @@ public final class ComponentFilters { this.string = string; } + @NonNull @Override public String getFilterValue() { return string; diff --git a/patches/api/patches.api b/patches/api/patches.api index ba28dbbfa..5a6c50d0f 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -921,6 +921,10 @@ public final class app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginP public static final fun getFixFacebookLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatchKt { + public static final fun getChangeLyricsProviderPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt { public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt index 9bdd69be8..38581c8c9 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt @@ -94,10 +94,10 @@ val hideCreateButtonPatch = bytecodePatch( """ invoke-static { v$oldNavigationBarItemTitleResIdRegister }, $isOldCreateButtonDescriptor move-result v0 - + # If this navigation bar item is not the Create button, jump to the normal method logic. if-eqz v0, :normal-method-logic - + # Return null early because this method return value is a BottomNavigationItemView. const/4 v0, 0 return-object v0 diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginPatch.kt index 8c415c5b5..f8be93ac5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginPatch.kt @@ -19,7 +19,7 @@ val fixFacebookLoginPatch = bytecodePatch( // signature checks. val katanaProxyLoginMethodHandlerClass = katanaProxyLoginMethodHandlerClassFingerprint.originalClassDef - // Always return 0 (no Intent was launched) as the result of trying to authorize with the Facebook app to + // Always return 0 (no Intent was launched) as the result of trying to authorize with the Facebook app to // make the login fallback to a web browser window. katanaProxyLoginMethodTryAuthorizeFingerprint .match(katanaProxyLoginMethodHandlerClass) diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatch.kt new file mode 100644 index 000000000..99ec038c2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatch.kt @@ -0,0 +1,123 @@ +package app.revanced.patches.spotify.misc.lyrics + +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.stringOption +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction35c +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference +import java.net.InetAddress +import java.net.URI +import java.net.URISyntaxException +import java.net.UnknownHostException +import java.util.logging.Logger + +@Suppress("unused") +val changeLyricsProviderPatch = bytecodePatch( + name = "Change lyrics provider", + description = "Changes the lyrics provider to a custom one.", + use = false, +) { + compatibleWith("com.spotify.music") + + val lyricsProviderHost by stringOption( + key = "lyricsProviderHost", + default = "lyrics.natanchiodi.fr", + title = "Lyrics provider host", + description = "The domain name or IP address of a custom lyrics provider.", + required = false, + ) { + // Fix bad data if the user enters a URL (https://whatever.com/path). + val host = try { + URI(it!!).host ?: it + } catch (e: URISyntaxException) { + return@stringOption false + } + + // Do a courtesy check if the host can be resolved. + // If it does not resolve, then print a warning but use the host anyway. + // Unresolvable hosts should not be rejected, since the patching environment + // may not allow network connections or the network may be down. + try { + InetAddress.getByName(host) + } catch (e: UnknownHostException) { + Logger.getLogger(this::class.java.name).warning( + "Host \"$host\" did not resolve to any domain." + ) + } + true + } + + execute { + if (IS_SPOTIFY_LEGACY_APP_TARGET) { + Logger.getLogger(this::class.java.name).severe( + "Change lyrics provider patch is not supported for this target version." + ) + return@execute + } + + val httpClientBuilderMethod = httpClientBuilderFingerprint.originalMethod + + // region Create a modified copy of the HTTP client builder method with the custom lyrics provider host. + val patchedHttpClientBuilderMethod = with(httpClientBuilderMethod) { + val invokeBuildUrlIndex = indexOfFirstInstructionOrThrow { + getReference()?.returnType == "Lokhttp3/HttpUrl;" + } + val setUrlBuilderHostIndex = indexOfFirstInstructionReversedOrThrow(invokeBuildUrlIndex) { + val reference = getReference() + reference?.definingClass == "Lokhttp3/HttpUrl${"$"}Builder;" && + reference.parameterTypes.firstOrNull() == "Ljava/lang/String;" + } + val hostRegister = getInstruction(setUrlBuilderHostIndex).registerD + + MutableMethod(this).apply { + name = "rv_getCustomLyricsProviderHttpClient" + addInstruction( + setUrlBuilderHostIndex, + "const-string v$hostRegister, \"$lyricsProviderHost\"" + ) + + // Add the patched method to the class. + httpClientBuilderFingerprint.classDef.methods.add(this) + } + } + //endregion + + // region Replace the call to the HTTP client builder method used exclusively for lyrics by the modified one. + getLyricsHttpClientFingerprint(httpClientBuilderMethod).method.apply { + val getLyricsHttpClientIndex = indexOfFirstInstructionOrThrow { + getReference() == httpClientBuilderMethod + } + val getLyricsHttpClientInstruction = getInstruction(getLyricsHttpClientIndex) + + // Replace the original method call with a call to our patched method. + replaceInstruction( + getLyricsHttpClientIndex, + BuilderInstruction35c( + getLyricsHttpClientInstruction.opcode, + getLyricsHttpClientInstruction.registerCount, + getLyricsHttpClientInstruction.registerC, + getLyricsHttpClientInstruction.registerD, + getLyricsHttpClientInstruction.registerE, + getLyricsHttpClientInstruction.registerF, + getLyricsHttpClientInstruction.registerG, + ImmutableMethodReference( + patchedHttpClientBuilderMethod.definingClass, + patchedHttpClientBuilderMethod.name, // Only difference from the original method. + patchedHttpClientBuilderMethod.parameters, + patchedHttpClientBuilderMethod.returnType + ) + ) + ) + } + //endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/Fingerprints.kt new file mode 100644 index 000000000..f55fc349f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/lyrics/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.spotify.misc.lyrics + +import app.revanced.patcher.fingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val httpClientBuilderFingerprint = fingerprint { + strings("client == null", "scheduler == null") +} + +internal fun getLyricsHttpClientFingerprint(httpClientBuilderMethodReference: MethodReference) = + fingerprint { + returns(httpClientBuilderMethodReference.returnType) + parameters() + custom { method, _ -> + method.indexOfFirstInstruction { + getReference() == httpClientBuilderMethodReference + } >= 0 + } + } From be4a7ef241a3457524c9ecdc6b0735b099cec9f1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 11 Jun 2025 08:28:44 +0000 Subject: [PATCH 02/23] chore: Release v5.28.0-dev.1 [skip ci] # [5.28.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.27.0...v5.28.0-dev.1) (2025-06-11) ### Features * **Spotify:** Add `Change lyrics provider` patch ([#4937](https://github.com/ReVanced/revanced-patches/issues/4937)) ([8736b6a](https://github.com/ReVanced/revanced-patches/commit/8736b6a80b48cb1f4562c9f9919804006ddb18bd)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eab74e5b..8e7b46957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [5.28.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.27.0...v5.28.0-dev.1) (2025-06-11) + + +### Features + +* **Spotify:** Add `Change lyrics provider` patch ([#4937](https://github.com/ReVanced/revanced-patches/issues/4937)) ([8736b6a](https://github.com/ReVanced/revanced-patches/commit/8736b6a80b48cb1f4562c9f9919804006ddb18bd)) + # [5.27.0](https://github.com/ReVanced/revanced-patches/compare/v5.26.0...v5.27.0) (2025-06-09) diff --git a/gradle.properties b/gradle.properties index 7f8e95efc..89dee0fd4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M org.gradle.parallel = true android.useAndroidX = true kotlin.code.style = official -version = 5.27.0 +version = 5.28.0-dev.1 From a93d74d26e7ef87a3745df2b9fe82722d65a0e59 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:40:37 +0200 Subject: [PATCH 03/23] fix(Google Photos): Resolve startup crash if MicroG GmsCore does not already have granted permissions --- .../app/revanced/extension/shared/Logger.java | 33 ++++++++----------- .../app/revanced/extension/shared/Utils.java | 10 +++--- .../tiktok/spoof/sim/SpoofSimPatch.java | 2 +- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java index edf57e9c1..437a56420 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java @@ -19,7 +19,8 @@ import app.revanced.extension.shared.settings.preference.LogBufferManager; * ReVanced specific logger. Logging is done to standard device log (accessible thru ADB), * and additionally accessible thru {@link LogBufferManager}. * - * All methods are thread safe. + * All methods are thread safe, and are safe to call even + * if {@link Utils#getContext()} is not available. */ public class Logger { @@ -138,6 +139,14 @@ public class Logger { } } + private static boolean includeStackTrace() { + return Utils.context != null && DEBUG_STACKTRACE.get(); + } + + private static boolean shouldShowErrorToast() { + return Utils.context != null && DEBUG_TOAST_ON_ERROR.get(); + } + /** * Logs debug messages under the outer class name of the code calling this method. *

@@ -158,7 +167,7 @@ public class Logger { */ public static void printDebug(LogMessage message, @Nullable Exception ex) { if (DEBUG.get()) { - logInternal(LogLevel.DEBUG, message, ex, DEBUG_STACKTRACE.get(), false); + logInternal(LogLevel.DEBUG, message, ex, includeStackTrace(), false); } } @@ -173,7 +182,7 @@ public class Logger { * Logs information messages using the outer class name of the code calling this method. */ public static void printInfo(LogMessage message, @Nullable Exception ex) { - logInternal(LogLevel.INFO, message, ex, DEBUG_STACKTRACE.get(), false); + logInternal(LogLevel.INFO, message, ex, includeStackTrace(), false); } /** @@ -194,22 +203,6 @@ public class Logger { * @param ex exception (optional) */ public static void printException(LogMessage message, @Nullable Throwable ex) { - logInternal(LogLevel.ERROR, message, ex, DEBUG_STACKTRACE.get(), DEBUG_TOAST_ON_ERROR.get()); - } - - /** - * Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized. - * Normally this method should not be used. - */ - public static void initializationInfo(LogMessage message) { - logInternal(LogLevel.INFO, message, null, false, false); - } - - /** - * Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized. - * Normally this method should not be used. - */ - public static void initializationException(LogMessage message, @Nullable Exception ex) { - logInternal(LogLevel.ERROR, message, ex, false, false); + logInternal(LogLevel.ERROR, message, ex, includeStackTrace(), shouldShowErrorToast()); } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java index 111a0dd77..a0f482c7e 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java @@ -55,7 +55,7 @@ import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference public class Utils { @SuppressLint("StaticFieldLeak") - private static volatile Context context; + static volatile Context context; private static String versionName; private static String applicationLabel; @@ -363,15 +363,15 @@ public class Utils { public static Context getContext() { if (context == null) { - Logger.initializationException(() -> "Context is not set by extension hook, returning null", null); + Logger.printException(() -> "Context is not set by extension hook, returning null", null); } return context; } public static void setContext(Context appContext) { // Intentionally use logger before context is set, - // to expose any bugs in the 'no context available' logger method. - Logger.initializationInfo(() -> "Set context: " + appContext); + // to expose any bugs in the 'no context available' logger code. + Logger.printInfo(() -> "Set context: " + appContext); // Must initially set context to check the app language. context = appContext; @@ -554,7 +554,7 @@ public class Utils { Context currentContext = context; if (currentContext == null) { - Logger.initializationException(() -> "Cannot show toast (context is null): " + messageToToast, null); + Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast, null); } else { Logger.printDebug(() -> "Showing toast: " + messageToToast); Toast.makeText(currentContext, messageToToast, toastDuration).show(); diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/spoof/sim/SpoofSimPatch.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/spoof/sim/SpoofSimPatch.java index d3e1baf7b..7ad76c97b 100644 --- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/spoof/sim/SpoofSimPatch.java +++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/spoof/sim/SpoofSimPatch.java @@ -16,7 +16,7 @@ public class SpoofSimPatch { return false; } - Logger.initializationException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null); + Logger.printException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null); return true; } From d0d1667f0f8ba4c7791dfd2797f6410289acd953 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 11 Jun 2025 17:44:07 +0000 Subject: [PATCH 04/23] chore: Release v5.28.0-dev.2 [skip ci] # [5.28.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.1...v5.28.0-dev.2) (2025-06-11) ### Bug Fixes * **Google Photos:** Resolve startup crash if MicroG GmsCore does not already have granted permissions ([a93d74d](https://github.com/ReVanced/revanced-patches/commit/a93d74d26e7ef87a3745df2b9fe82722d65a0e59)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e7b46957..5a7d24e38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [5.28.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.1...v5.28.0-dev.2) (2025-06-11) + + +### Bug Fixes + +* **Google Photos:** Resolve startup crash if MicroG GmsCore does not already have granted permissions ([a93d74d](https://github.com/ReVanced/revanced-patches/commit/a93d74d26e7ef87a3745df2b9fe82722d65a0e59)) + # [5.28.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.27.0...v5.28.0-dev.1) (2025-06-11) diff --git a/gradle.properties b/gradle.properties index 89dee0fd4..c88f8e482 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M org.gradle.parallel = true android.useAndroidX = true kotlin.code.style = official -version = 5.28.0-dev.1 +version = 5.28.0-dev.2 From 756b28dca0dc86e38157cbf17cfafb3818b33412 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:57:37 +0200 Subject: [PATCH 05/23] chore: Fix debug logging if context is not set --- .../java/app/revanced/extension/shared/Logger.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java index 437a56420..47f6da3e3 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java @@ -139,14 +139,20 @@ public class Logger { } } - private static boolean includeStackTrace() { - return Utils.context != null && DEBUG_STACKTRACE.get(); + private static boolean shouldLogDebug() { + // If the app is still starting up and the context is not yet set, + // then allow debug logging regardless what the debug setting actually is. + return Utils.context == null || DEBUG.get(); } private static boolean shouldShowErrorToast() { return Utils.context != null && DEBUG_TOAST_ON_ERROR.get(); } + private static boolean includeStackTrace() { + return Utils.context != null && DEBUG_STACKTRACE.get(); + } + /** * Logs debug messages under the outer class name of the code calling this method. *

@@ -166,7 +172,7 @@ public class Logger { * building strings is paid only if {@link BaseSettings#DEBUG} is enabled. */ public static void printDebug(LogMessage message, @Nullable Exception ex) { - if (DEBUG.get()) { + if (shouldLogDebug()) { logInternal(LogLevel.DEBUG, message, ex, includeStackTrace(), false); } } From e7dd061c513af90861c0ab0d7adc6ee43be57ce2 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:20:19 -0300 Subject: [PATCH 06/23] fix(Spotify): Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets (#5159) --- .../createbutton/HideCreateButtonPatch.java | 26 ++++++---- .../spotify/misc/UnlockPremiumPatch.java | 52 ++++++++++--------- .../createbutton/HideCreateButtonPatch.kt | 16 ++++-- .../spotify/misc/privacy/Fingerprints.kt | 9 +++- .../misc/privacy/SanitizeSharingLinksPatch.kt | 11 +++- 5 files changed, 73 insertions(+), 41 deletions(-) diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/hide/createbutton/HideCreateButtonPatch.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/hide/createbutton/HideCreateButtonPatch.java index 19685e31a..c7d2a0754 100644 --- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/hide/createbutton/HideCreateButtonPatch.java +++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/hide/createbutton/HideCreateButtonPatch.java @@ -37,20 +37,24 @@ public final class HideCreateButtonPatch { return null; } - String stringifiedNavigationBarItem = navigationBarItem.toString(); + try { + String stringifiedNavigationBarItem = navigationBarItem.toString(); - for (ComponentFilter componentFilter : CREATE_BUTTON_COMPONENT_FILTERS) { - if (componentFilter.filterUnavailable()) { - Logger.printInfo(() -> "returnNullIfIsCreateButton: Filter " + - componentFilter.getFilterRepresentation() + " not available, skipping"); - continue; - } + for (ComponentFilter componentFilter : CREATE_BUTTON_COMPONENT_FILTERS) { + if (componentFilter.filterUnavailable()) { + Logger.printInfo(() -> "returnNullIfIsCreateButton: Filter " + + componentFilter.getFilterRepresentation() + " not available, skipping"); + continue; + } - if (stringifiedNavigationBarItem.contains(componentFilter.getFilterValue())) { - Logger.printInfo(() -> "Hiding Create button because the navigation bar item " + navigationBarItem + - " matched the filter " + componentFilter.getFilterRepresentation()); - return null; + if (stringifiedNavigationBarItem.contains(componentFilter.getFilterValue())) { + Logger.printInfo(() -> "Hiding Create button because the navigation bar item " + + navigationBarItem + " matched the filter " + componentFilter.getFilterRepresentation()); + return null; + } } + } catch (Exception ex) { + Logger.printException(() -> "returnNullIfIsCreateButton failure", ex); } return navigationBarItem; diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java index 1baf4af9f..c618ca7ba 100644 --- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java +++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/UnlockPremiumPatch.java @@ -202,37 +202,41 @@ public final class UnlockPremiumPatch { return false; } - String stringifiedContextMenuItem = contextMenuItem.toString(); + try { + String stringifiedContextMenuItem = contextMenuItem.toString(); - for (List componentFilters : CONTEXT_MENU_ITEMS_COMPONENT_FILTERS) { - boolean allMatch = true; - StringBuilder matchedFilterRepresentations = new StringBuilder(); + for (List componentFilters : CONTEXT_MENU_ITEMS_COMPONENT_FILTERS) { + boolean allMatch = true; + StringBuilder matchedFilterRepresentations = new StringBuilder(); - for (int i = 0, filterSize = componentFilters.size(); i < filterSize; i++) { - ComponentFilter componentFilter = componentFilters.get(i); + for (int i = 0, filterSize = componentFilters.size(); i < filterSize; i++) { + ComponentFilter componentFilter = componentFilters.get(i); - if (componentFilter.filterUnavailable()) { - Logger.printInfo(() -> "isFilteredContextMenuItem: Filter " + - componentFilter.getFilterRepresentation() + " not available, skipping"); - continue; + if (componentFilter.filterUnavailable()) { + Logger.printInfo(() -> "isFilteredContextMenuItem: Filter " + + componentFilter.getFilterRepresentation() + " not available, skipping"); + continue; + } + + if (!stringifiedContextMenuItem.contains(componentFilter.getFilterValue())) { + allMatch = false; + break; + } + + matchedFilterRepresentations.append(componentFilter.getFilterRepresentation()); + if (i < filterSize - 1) { + matchedFilterRepresentations.append(", "); + } } - if (!stringifiedContextMenuItem.contains(componentFilter.getFilterValue())) { - allMatch = false; - break; - } - - matchedFilterRepresentations.append(componentFilter.getFilterRepresentation()); - if (i < filterSize - 1) { - matchedFilterRepresentations.append(", "); + if (allMatch) { + Logger.printInfo(() -> "Filtering context menu item " + stringifiedContextMenuItem + + " because the following filters matched: " + matchedFilterRepresentations); + return true; } } - - if (allMatch) { - Logger.printInfo(() -> "Filtering context menu item " + stringifiedContextMenuItem + - " because the following filters matched: " + matchedFilterRepresentations); - return true; - } + } catch (Exception ex) { + Logger.printException(() -> "isFilteredContextMenuItem failure", ex); } return false; diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt index 38581c8c9..e01d05ecc 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatch.kt @@ -72,7 +72,7 @@ val hideCreateButtonPatch = bytecodePatch( if (oldNavigationBarAddItemMethod != null) { // In case an older version of the app is being patched, hook the old method which adds navigation bar items. - // Return null early if the navigation bar item title resource id is the old Create button title resource id. + // Return early if the navigation bar item title resource id is the old Create button title resource id. oldNavigationBarAddItemFingerprint.methodOrNull?.apply { val getNavigationBarItemTitleStringIndex = indexOfFirstInstructionOrThrow { val reference = getReference() @@ -89,6 +89,16 @@ val hideCreateButtonPatch = bytecodePatch( val isOldCreateButtonDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->isOldCreateButton(I)Z" + val returnEarlyInstruction = if (returnType == "V") { + // In older implementations the method return value is void. + "return-void" + } else { + // In newer implementations + // return null because the method return value is a BottomNavigationItemView. + "const/4 v0, 0\n" + + "return-object v0" + } + addInstructionsWithLabels( 0, """ @@ -98,9 +108,7 @@ val hideCreateButtonPatch = bytecodePatch( # If this navigation bar item is not the Create button, jump to the normal method logic. if-eqz v0, :normal-method-logic - # Return null early because this method return value is a BottomNavigationItemView. - const/4 v0, 0 - return-object v0 + $returnEarlyInstruction """, ExternalLabel("normal-method-logic", firstInstruction) ) diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt index a2d65561d..f8ce3a262 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt @@ -3,6 +3,7 @@ package app.revanced.patches.spotify.misc.privacy import app.revanced.patcher.fingerprint import app.revanced.util.literal import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode internal val shareCopyUrlFingerprint = fingerprint { returns("Ljava/lang/Object;") @@ -23,9 +24,15 @@ internal val shareCopyUrlLegacyFingerprint = fingerprint { } internal val formatAndroidShareSheetUrlFingerprint = fingerprint { - accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) returns("Ljava/lang/String;") parameters("L", "Ljava/lang/String;") + opcodes( + Opcode.GOTO, + Opcode.IF_EQZ, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.RETURN_OBJECT + ) literal { '\n'.code.toLong() } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt index 7fe59394d..707896bf5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt @@ -8,6 +8,7 @@ import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference @@ -56,7 +57,15 @@ val sanitizeSharingLinksPatch = bytecodePatch( shareUrlParameter = "p2" } else { shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint - shareUrlParameter = "p1" + val methodAccessFlags = formatAndroidShareSheetUrlFingerprint.originalMethod.accessFlags + shareUrlParameter = if (AccessFlags.STATIC.isSet(methodAccessFlags)) { + // In newer implementations the method is static, so p0 is not `this`. + "p1" + } else { + // In older implementations the method is not static, making it so p0 is `this`. + // For that reason, add one to the parameter register. + "p2" + } } shareSheetFingerprint.method.addInstructions( From 442f5f5aec32642793db15411f0fc493ef5ed5da Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 11 Jun 2025 20:23:04 +0000 Subject: [PATCH 07/23] chore: Release v5.28.0-dev.3 [skip ci] # [5.28.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.2...v5.28.0-dev.3) (2025-06-11) ### Bug Fixes * **Spotify:** Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets ([#5159](https://github.com/ReVanced/revanced-patches/issues/5159)) ([e7dd061](https://github.com/ReVanced/revanced-patches/commit/e7dd061c513af90861c0ab0d7adc6ee43be57ce2)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a7d24e38..4c85e5bd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [5.28.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.2...v5.28.0-dev.3) (2025-06-11) + + +### Bug Fixes + +* **Spotify:** Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets ([#5159](https://github.com/ReVanced/revanced-patches/issues/5159)) ([e7dd061](https://github.com/ReVanced/revanced-patches/commit/e7dd061c513af90861c0ab0d7adc6ee43be57ce2)) + # [5.28.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.1...v5.28.0-dev.2) (2025-06-11) diff --git a/gradle.properties b/gradle.properties index c88f8e482..b216f87fe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M org.gradle.parallel = true android.useAndroidX = true kotlin.code.style = official -version = 5.28.0-dev.2 +version = 5.28.0-dev.3 From 312b6dc04e01c2758cd304ca8606306027aa2f01 Mon Sep 17 00:00:00 2001 From: MarcaD <152095496+MarcaDian@users.noreply.github.com> Date: Fri, 13 Jun 2025 10:29:13 +0300 Subject: [PATCH 08/23] feat: Use modern style settings dialogs (#5109) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> --- .../extension/shared/GmsCoreSupport.java | 29 +- .../app/revanced/extension/shared/Utils.java | 734 ++++++++++++++++-- .../extension/shared/checks/Check.java | 131 +++- .../extension/shared/requests/Route.java | 2 +- .../AbstractPreferenceFragment.java | 79 +- .../preference/ColorPickerPreference.java | 267 ++++--- .../settings/preference/ColorPickerView.java | 64 +- .../CustomDialogListPreference.java | 197 +++++ .../preference/ImportExportPreference.java | 59 +- .../preference/ReVancedAboutPreference.java | 60 +- .../ResettableEditTextPreference.java | 100 ++- .../preference/SortedListPreference.java | 23 +- .../extension/youtube/ThemeHelper.java | 179 ----- ...WatchHistoryDomainNameResolutionPatch.java | 35 +- .../announcements/AnnouncementsPatch.java | 56 +- .../AdvancedVideoQualityMenuFilter.java | 2 +- .../AdvancedVideoQualityMenuPatch.java | 8 +- .../speed/CustomPlaybackSpeedPatch.java | 130 +++- .../youtube/patches/theme/ThemePatch.java | 3 +- .../ReturnYouTubeDislike.java | 10 +- .../youtube/settings/LicenseActivityHook.java | 41 +- .../settings/SearchViewController.java | 46 +- .../CustomVideoSpeedListPreference.java | 6 +- .../ReVancedPreferenceFragment.java | 44 +- .../ReVancedYouTubeAboutPreference.java | 32 - .../sponsorblock/SponsorBlockSettings.java | 32 +- .../SegmentCategoryListPreference.java | 167 ++-- .../ui/SponsorBlockPreferenceGroup.java | 201 +++-- .../SponsorBlockStatsPreferenceCategory.java | 26 +- patches/api/patches.api | 3 + .../signature/SpoofSignaturePatch.kt | 10 +- .../nunl/firebase/SpoofCertificatePatch.kt | 10 +- .../detection/deviceid/SpoofDeviceIdPatch.kt | 10 +- .../misc/SpoofAndroidDeviceIdPatch.kt | 9 +- .../boostforreddit/api/SpoofClientPatch.kt | 10 +- .../joeyforreddit/api/SpoofClientPatch.kt | 19 +- .../redditisfun/api/SpoofClientPatch.kt | 10 +- .../slide/api/SpoofClientPatch.kt | 10 +- .../syncforreddit/api/SpoofClientPatch.kt | 33 +- .../misc/checks/BaseCheckEnvironmentPatch.kt | 2 +- .../misc/extension/SharedExtensionPatch.kt | 11 +- .../shared/misc/settings/Fingerprints.kt | 23 + .../shared/misc/settings/SettingsPatch.kt | 44 +- .../settings/preference/ListPreference.kt | 8 +- .../twitch/ad/embedded/EmbeddedAdsPatch.kt | 2 +- .../antidelete/ShowDeletedMessagesPatch.kt | 5 +- .../firebasegetcert/FirebaseGetCertPatch.kt | 10 +- .../interaction/downloads/DownloadsPatch.kt | 18 +- .../swipecontrols/SwipeControlsPatch.kt | 9 +- .../formfactor/ChangeFormFactorPatch.kt | 5 +- .../layout/miniplayer/MiniplayerPatch.kt | 15 +- .../player/fullscreen/ExitFullscreenPatch.kt | 5 +- .../shortsautoplay/ShortsAutoplayPatch.kt | 3 +- .../OpenShortsInRegularPlayerPatch.kt | 6 +- .../spoofappversion/SpoofAppVersionPatch.kt | 2 +- .../layout/startpage/ChangeStartPagePatch.kt | 1 - .../youtube/layout/theme/Fingerprints.kt | 20 - .../youtube/layout/theme/ThemePatch.kt | 152 ++-- .../thumbnails/AlternativeThumbnailsPatch.kt | 27 +- .../fix/backtoexitgesture/Fingerprints.kt | 9 - .../FixBackToExitGesturePatch.kt | 11 +- .../youtube/misc/navigation/Fingerprints.kt | 9 - .../misc/navigation/NavigationBarHookPatch.kt | 1 + .../youtube/misc/settings/Fingerprints.kt | 5 - .../youtube/misc/settings/SettingsPatch.kt | 88 ++- .../misc/spoof/SpoofVideoStreamsPatch.kt | 6 +- .../patches/youtube/shared/Fingerprints.kt | 15 +- .../quality/RememberVideoQualityPatch.kt | 10 +- .../remember/RememberPlaybackSpeedPatch.kt | 1 - .../kotlin/app/revanced/util/BytecodeUtils.kt | 63 +- .../resources/addresources/values/strings.xml | 5 +- .../drawable/revanced_ic_dialog_alert.xml | 10 + .../drawable/revanced_settings_arrow_time.xml | 9 + .../drawable/revanced_settings_cursor.xml | 2 +- .../revanced_settings_custom_checkmark.xml | 9 + .../drawable/revanced_settings_icon.xml | 4 +- .../revanced_settings_screen_00_about.xml | 2 +- .../revanced_settings_screen_01_ads.xml | 2 +- ...nced_settings_screen_02_alt_thumbnails.xml | 4 +- .../revanced_settings_screen_03_feed.xml | 2 +- .../revanced_settings_screen_04_general.xml | 2 +- .../revanced_settings_screen_05_player.xml | 2 +- .../revanced_settings_screen_06_shorts.xml | 4 +- .../revanced_settings_screen_07_seekbar.xml | 2 +- ...nced_settings_screen_08_swipe_controls.xml | 2 +- ...tings_screen_09_return_youtube_dislike.xml | 2 +- ...vanced_settings_screen_10_sponsorblock.xml | 2 +- .../revanced_settings_screen_11_misc.xml | 2 +- .../revanced_settings_screen_12_video.xml | 2 +- .../revanced_settings_search_icon.xml | 9 + .../revanced_settings_toolbar_arrow_left.xml | 9 + .../resources/settings/host/values/styles.xml | 22 - .../layout/revanced_color_dot_widget.xml | 2 + .../settings/layout/revanced_color_picker.xml | 12 +- .../revanced_custom_list_item_checked.xml | 36 + ..._preference_with_icon_no_search_result.xml | 17 +- .../revanced_search_suggestion_item.xml | 2 +- .../layout/revanced_settings_with_toolbar.xml | 12 +- .../settings/menu/revanced_search_menu.xml | 4 +- 99 files changed, 2343 insertions(+), 1324 deletions(-) create mode 100644 extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/CustomDialogListPreference.java delete mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java delete mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedYouTubeAboutPreference.java create mode 100644 patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/Fingerprints.kt create mode 100644 patches/src/main/resources/settings/drawable/revanced_ic_dialog_alert.xml create mode 100644 patches/src/main/resources/settings/drawable/revanced_settings_arrow_time.xml create mode 100644 patches/src/main/resources/settings/drawable/revanced_settings_custom_checkmark.xml create mode 100644 patches/src/main/resources/settings/drawable/revanced_settings_search_icon.xml create mode 100644 patches/src/main/resources/settings/drawable/revanced_settings_toolbar_arrow_left.xml create mode 100644 patches/src/main/resources/settings/layout/revanced_custom_list_item_checked.xml diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java index 585721491..cff4e5a3c 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java @@ -5,7 +5,7 @@ import static app.revanced.extension.shared.requests.Route.Method.GET; import android.annotation.SuppressLint; import android.app.Activity; -import android.app.AlertDialog; +import android.app.Dialog; import android.app.SearchManager; import android.content.Context; import android.content.DialogInterface; @@ -15,6 +15,8 @@ import android.net.Uri; import android.os.Build; import android.os.PowerManager; import android.provider.Settings; +import android.util.Pair; +import android.widget.LinearLayout; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -26,6 +28,7 @@ import java.util.Locale; import app.revanced.extension.shared.requests.Requester; import app.revanced.extension.shared.requests.Route; +import app.revanced.extension.shared.Utils; @SuppressWarnings("unused") public class GmsCoreSupport { @@ -78,13 +81,27 @@ public class GmsCoreSupport { // Use a delay to allow the activity to finish initializing. // Otherwise, if device is in dark mode the dialog is shown with wrong color scheme. Utils.runOnMainThreadDelayed(() -> { + // Create the custom dialog. + Pair dialogPair = Utils.createCustomDialog( + context, + str("gms_core_dialog_title"), // Title. + str(dialogMessageRef), // Message. + null, // No EditText. + str(positiveButtonTextRef), // OK button text. + () -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable. + null, // No Cancel button action. + null, // No Neutral button text. + null, // No Neutral button action. + true // Dismiss dialog when onNeutralClick. + ); + + Dialog dialog = dialogPair.first; + // Do not set cancelable to false, to allow using back button to skip the action, // just in case the battery change can never be satisfied. - var dialog = new AlertDialog.Builder(context) - .setTitle(str("gms_core_dialog_title")) - .setMessage(str(dialogMessageRef)) - .setPositiveButton(str(positiveButtonTextRef), onPositiveClickListener) - .create(); + dialog.setCancelable(true); + + // Show the dialog Utils.showDialog(context, dialog); }, 100); } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java index a0f482c7e..1b1b4e280 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java @@ -2,10 +2,8 @@ package app.revanced.extension.shared; import android.annotation.SuppressLint; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; -import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -14,6 +12,9 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; import android.net.ConnectivityManager; import android.os.Build; import android.os.Bundle; @@ -22,30 +23,45 @@ import android.os.Looper; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.util.DisplayMetrics; import android.util.Pair; import android.util.TypedValue; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.Window; +import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.widget.Button; +import android.widget.EditText; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.RelativeLayout; +import android.widget.TextView; import android.widget.Toast; import android.widget.Toolbar; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.text.Bidi; -import java.util.*; -import java.util.regex.Pattern; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import app.revanced.extension.shared.settings.AppLanguage; import app.revanced.extension.shared.settings.BaseSettings; @@ -60,6 +76,14 @@ public class Utils { private static String versionName; private static String applicationLabel; + @ColorInt + private static int darkColor = Color.BLACK; + @ColorInt + private static int lightColor = Color.WHITE; + + @Nullable + private static Boolean isDarkModeEnabled; + private Utils() { } // utility class @@ -183,8 +207,8 @@ public class Utils { public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) { if (setting) { ViewParent parent = view.getParent(); - if (parent instanceof ViewGroup) { - ((ViewGroup) parent).removeView(view); + if (parent instanceof ViewGroup parentGroup) { + parentGroup.removeView(view); return true; } } @@ -197,23 +221,22 @@ public class Utils { * All tasks run at max thread priority. */ private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor( - 3, // 3 threads always ready to go + 3, // 3 threads always ready to go. Integer.MAX_VALUE, - 10, // For any threads over the minimum, keep them alive 10 seconds after they go idle + 10, // For any threads over the minimum, keep them alive 10 seconds after they go idle. TimeUnit.SECONDS, new SynchronousQueue<>(), r -> { // ThreadFactory Thread t = new Thread(r); - t.setPriority(Thread.MAX_PRIORITY); // run at max priority + t.setPriority(Thread.MAX_PRIORITY); // Run at max priority. return t; }); - public static void runOnBackgroundThread(@NonNull Runnable task) { + public static void runOnBackgroundThread(Runnable task) { backgroundThreadPool.execute(task); } - @NonNull - public static Future submitOnBackgroundThread(@NonNull Callable call) { + public static Future submitOnBackgroundThread(Callable call) { return backgroundThreadPool.submit(call); } @@ -228,20 +251,19 @@ public class Utils { long meaninglessValue = 0; while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) { - // could do a thread sleep, but that will trigger an exception if the thread is interrupted + // Could do a thread sleep, but that will trigger an exception if the thread is interrupted. meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random())); } - // return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work, - // leaving an empty loop that hammers on the System.currentTimeMillis native call + // Return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work, + // leaving an empty loop that hammers on the System.currentTimeMillis native call. return meaninglessValue; } - - public static boolean containsAny(@NonNull String value, @NonNull String... targets) { + public static boolean containsAny(String value, String... targets) { return indexOfFirstFound(value, targets) >= 0; } - public static int indexOfFirstFound(@NonNull String value, @NonNull String... targets) { + public static int indexOfFirstFound(String value, String... targets) { for (String string : targets) { if (!string.isEmpty()) { final int indexOf = value.indexOf(string); @@ -252,39 +274,39 @@ public class Utils { } /** - * @return zero, if the resource is not found + * @return zero, if the resource is not found. */ @SuppressLint("DiscouragedApi") - public static int getResourceIdentifier(@NonNull Context context, @NonNull String resourceIdentifierName, @NonNull String type) { + public static int getResourceIdentifier(Context context, String resourceIdentifierName, String type) { return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName()); } /** - * @return zero, if the resource is not found + * @return zero, if the resource is not found. */ - public static int getResourceIdentifier(@NonNull String resourceIdentifierName, @NonNull String type) { + public static int getResourceIdentifier(String resourceIdentifierName, String type) { return getResourceIdentifier(getContext(), resourceIdentifierName, type); } - public static int getResourceInteger(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException { return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer")); } - @NonNull - public static Animation getResourceAnimation(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException { return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim")); } - public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + @ColorInt + public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException { //noinspection deprecation return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color")); } - public static int getResourceDimensionPixelSize(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException { return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen")); } - public static float getResourceDimension(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException { return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen")); } @@ -294,12 +316,11 @@ public class Utils { /** * Includes sub children. - * - * @noinspection unchecked */ - public static R getChildViewByResourceName(@NonNull View view, @NonNull String str) { + public static R getChildViewByResourceName(View view, String str) { var child = view.findViewById(Utils.getResourceIdentifier(str, "id")); if (child != null) { + //noinspection unchecked return (R) child; } @@ -312,8 +333,8 @@ public class Utils { * @return The first child view that matches the filter. */ @Nullable - public static T getChildView(@NonNull ViewGroup viewGroup, boolean searchRecursively, - @NonNull MatchFilter filter) { + public static T getChildView(ViewGroup viewGroup, boolean searchRecursively, + MatchFilter filter) { for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) { View childAt = viewGroup.getChildAt(i); @@ -332,7 +353,7 @@ public class Utils { } @Nullable - public static ViewParent getParentView(@NonNull View view, int nthParent) { + public static ViewParent getParentView(View view, int nthParent) { ViewParent parent = view.getParent(); int currentDepth = 0; @@ -350,7 +371,7 @@ public class Utils { return null; } - public static void restartApp(@NonNull Context context) { + public static void restartApp(Context context) { String packageName = context.getPackageName(); Intent intent = Objects.requireNonNull(context.getPackageManager().getLaunchIntentForPackage(packageName)); Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent()); @@ -383,6 +404,9 @@ public class Utils { config.setLocale(language.getLocale()); context = appContext.createConfigurationContext(config); } + + setThemeLightColor(getThemeColor(getThemeLightColorResourceName(), Color.WHITE)); + setThemeDarkColor(getThemeColor(getThemeDarkColorResourceName(), Color.BLACK)); } public static void setClipboard(CharSequence text) { @@ -446,7 +470,7 @@ public class Utils { * including any unicode numbers such as Arabic. */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public static boolean containsNumber(@NonNull CharSequence text) { + public static boolean containsNumber(CharSequence text) { for (int index = 0, length = text.length(); index < length;) { final int codePoint = Character.codePointAt(text, index); if (Character.isDigit(codePoint)) { @@ -485,7 +509,7 @@ public class Utils { super.onStart(); if (onStartAction != null) { - onStartAction.onStart((AlertDialog) getDialog()); + onStartAction.onStart(dialog); } } catch (Exception ex) { Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex); @@ -494,34 +518,34 @@ public class Utils { } /** - * Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}. + * Interface for {@link #showDialog(Activity, Dialog, boolean, DialogFragmentOnStartAction)}. */ @FunctionalInterface public interface DialogFragmentOnStartAction { - void onStart(AlertDialog dialog); + void onStart(Dialog dialog); } - public static void showDialog(Activity activity, AlertDialog dialog) { + public static void showDialog(Activity activity, Dialog dialog) { showDialog(activity, dialog, true, null); } /** - * Utility method to allow showing an AlertDialog on top of other alert dialogs. + * Utility method to allow showing a Dialog on top of other dialogs. * Calling this will always display the dialog on top of all other dialogs * previously called using this method. - *
+ *

* Be aware the on start action can be called multiple times for some situations, * such as the user switching apps without dismissing the dialog then switching back to this app. - *
+ *

* This method is only useful during app startup and multiple patches may show their own dialog, * and the most important dialog can be called last (using a delay) so it's always on top. - *
+ *

* For all other situations it's better to not use this method and - * call {@link AlertDialog#show()} on the dialog. + * call {@link Dialog#show()} on the dialog. */ @SuppressWarnings("deprecation") public static void showDialog(Activity activity, - AlertDialog dialog, + Dialog dialog, boolean isCancelable, @Nullable DialogFragmentOnStartAction onStartAction) { verifyOnMainThread(); @@ -535,20 +559,20 @@ public class Utils { } /** - * Safe to call from any thread + * Safe to call from any thread. */ - public static void showToastShort(@NonNull String messageToToast) { + public static void showToastShort(String messageToToast) { showToast(messageToToast, Toast.LENGTH_SHORT); } /** - * Safe to call from any thread + * Safe to call from any thread. */ - public static void showToastLong(@NonNull String messageToToast) { + public static void showToastLong(String messageToToast) { showToast(messageToToast, Toast.LENGTH_LONG); } - private static void showToast(@NonNull String messageToToast, int toastDuration) { + private static void showToast(String messageToToast, int toastDuration) { Objects.requireNonNull(messageToToast); runOnMainThreadNowOrLater(() -> { Context currentContext = context; @@ -562,12 +586,29 @@ public class Utils { }); } + /** + * @return The current dark mode as set by any patch. + * Or if none is set, then the system dark mode status is returned. + */ public static boolean isDarkModeEnabled() { + Boolean isDarkMode = isDarkModeEnabled; + if (isDarkMode != null) { + return isDarkMode; + } + Configuration config = Resources.getSystem().getConfiguration(); final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; return currentNightMode == Configuration.UI_MODE_NIGHT_YES; } + /** + * Overrides dark mode status as returned by {@link #isDarkModeEnabled()}. + */ + public static void setIsDarkModeEnabled(boolean isDarkMode) { + isDarkModeEnabled = isDarkMode; + Logger.printDebug(() -> "Dark mode status: " + isDarkMode); + } + public static boolean isLandscapeOrientation() { final int orientation = Resources.getSystem().getConfiguration().orientation; return orientation == Configuration.ORIENTATION_LANDSCAPE; @@ -578,14 +619,14 @@ public class Utils { * * @see #runOnMainThreadNowOrLater(Runnable) */ - public static void runOnMainThread(@NonNull Runnable runnable) { + public static void runOnMainThread(Runnable runnable) { runOnMainThreadDelayed(runnable, 0); } /** * Automatically logs any exceptions the runnable throws. */ - public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) { + public static void runOnMainThreadDelayed(Runnable runnable, long delayMillis) { Runnable loggingRunnable = () -> { try { runnable.run(); @@ -597,10 +638,10 @@ public class Utils { } /** - * If called from the main thread, the code is run immediately.

+ * If called from the main thread, the code is run immediately. * If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}. */ - public static void runOnMainThreadNowOrLater(@NonNull Runnable runnable) { + public static void runOnMainThreadNowOrLater(Runnable runnable) { if (isCurrentlyOnMainThread()) { runnable.run(); } else { @@ -641,8 +682,8 @@ public class Utils { /** * Calling extension code must ensure the un-patched app has the permission - * android.permission.ACCESS_NETWORK_STATE, otherwise the app will crash - * if this method is used. + * android.permission.ACCESS_NETWORK_STATE, + * otherwise the app will crash if this method is used. */ public static boolean isNetworkConnected() { NetworkType networkType = getNetworkType(); @@ -652,10 +693,10 @@ public class Utils { /** * Calling extension code must ensure the un-patched app has the permission - * android.permission.ACCESS_NETWORK_STATE, otherwise the app will crash - * if this method is used. + * android.permission.ACCESS_NETWORK_STATE, + * otherwise the app will crash if this method is used. */ - @SuppressLint({"MissingPermission", "deprecation"}) + @SuppressWarnings({"MissingPermission", "deprecation"}) public static NetworkType getNetworkType() { Context networkContext = getContext(); if (networkContext == null) { @@ -700,6 +741,514 @@ public class Utils { } } + /** + * Creates a custom dialog with a styled layout, including a title, message, buttons, and an + * optional EditText. The dialog's appearance adapts to the app's dark mode setting, with + * rounded corners and customizable button actions. Buttons adjust dynamically to their text + * content and are arranged in a single row if they fit within 80% of the screen width, + * with the Neutral button aligned to the left and OK/Cancel buttons centered on the right. + * If buttons do not fit, each is placed on a separate row, all aligned to the right. + * + * @param context Context used to create the dialog. + * @param title Title text of the dialog. + * @param message Message text of the dialog (supports Spanned for HTML), or null if replaced by EditText. + * @param editText EditText to include in the dialog, or null if no EditText is needed. + * @param okButtonText OK button text, or null to use the default "OK" string. + * @param onOkClick Action to perform when the OK button is clicked. + * @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed. + * @param neutralButtonText Neutral button text, or null if no Neutral button is needed. + * @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed. + * @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked. + * @return The Dialog and its main LinearLayout container. + */ + @SuppressWarnings("ExtractMethodRecommender") + public static Pair createCustomDialog( + Context context, String title, CharSequence message, @Nullable EditText editText, + String okButtonText, Runnable onOkClick, Runnable onCancelClick, + @Nullable String neutralButtonText, @Nullable Runnable onNeutralClick, + boolean dismissDialogOnNeutralClick + ) { + Logger.printDebug(() -> "Creating custom dialog with title: " + title); + + Dialog dialog = new Dialog(context); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar. + + // Create main layout. + LinearLayout mainLayout = new LinearLayout(context); + mainLayout.setOrientation(LinearLayout.VERTICAL); + + // Preset size constants. + final int dip4 = dipToPixels(4); + final int dip8 = dipToPixels(8); + final int dip16 = dipToPixels(16); + final int dip24 = dipToPixels(24); + + mainLayout.setPadding(dip24, dip16, dip24, dip24); + // Set rounded rectangle background. + ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape( + createCornerRadii(28), null, null)); + mainBackground.getPaint().setColor(getDialogBackgroundColor()); // Dialog background. + mainLayout.setBackground(mainBackground); + + // Title. + if (!TextUtils.isEmpty(title)) { + TextView titleView = new TextView(context); + titleView.setText(title); + titleView.setTypeface(Typeface.DEFAULT_BOLD); + titleView.setTextSize(18); + titleView.setTextColor(getAppForegroundColor()); + titleView.setGravity(Gravity.CENTER); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + layoutParams.setMargins(0, 0, 0, dip8); + titleView.setLayoutParams(layoutParams); + mainLayout.addView(titleView); + } + + // Message (if not replaced by EditText). + if (editText == null && message != null) { + TextView messageView = new TextView(context); + messageView.setText(message); // Supports Spanned (HTML). + messageView.setTextSize(16); + messageView.setTextColor(getAppForegroundColor()); + // Enable HTML link clicking if the message contains links. + if (message instanceof Spanned) { + messageView.setMovementMethod(LinkMovementMethod.getInstance()); + } + LinearLayout.LayoutParams messageParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + messageParams.setMargins(0, dip8, 0, dip16); + messageView.setLayoutParams(messageParams); + mainLayout.addView(messageView); + } + + // EditText (if provided). + if (editText != null) { + // Remove EditText from its current parent, if any. + ViewGroup parent = (ViewGroup) editText.getParent(); + if (parent != null) { + parent.removeView(editText); + } + // Style the EditText to match the dialog theme. + editText.setTextColor(getAppForegroundColor()); + editText.setBackgroundColor(isDarkModeEnabled() ? Color.BLACK : Color.WHITE); + editText.setPadding(dip8, dip8, dip8, dip8); + ShapeDrawable editTextBackground = new ShapeDrawable(new RoundRectShape( + createCornerRadii(10), null, null)); + editTextBackground.getPaint().setColor(getEditTextBackground()); // Background color for EditText. + editText.setBackground(editTextBackground); + + LinearLayout.LayoutParams editTextParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + editTextParams.setMargins(0, dip8, 0, dip16); + // Prevent buttons from moving off the screen by fixing the height of the EditText. + final int maxHeight = (int) (context.getResources().getDisplayMetrics().heightPixels * 0.6); + editText.setMaxHeight(maxHeight); + mainLayout.addView(editText, 1, editTextParams); + } + + // Button container. + LinearLayout buttonContainer = new LinearLayout(context); + buttonContainer.setOrientation(LinearLayout.VERTICAL); + buttonContainer.removeAllViews(); + LinearLayout.LayoutParams buttonContainerParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + buttonContainerParams.setMargins(0, dip8, 0, 0); + buttonContainer.setLayoutParams(buttonContainerParams); + + // Lists to track buttons. + List