diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideCategoryBarPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideCategoryBarPatch.java
new file mode 100644
index 000000000..f0433ccb1
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideCategoryBarPatch.java
@@ -0,0 +1,14 @@
+package app.revanced.extension.music.patches;
+
+import app.revanced.extension.music.settings.Settings;
+
+@SuppressWarnings("unused")
+public class HideCategoryBarPatch {
+
+ /**
+ * Injection point
+ */
+ public static boolean hideCategoryBar() {
+ return Settings.HIDE_CATEGORY_BAR.get();
+ }
+}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideGetPremiumPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideGetPremiumPatch.java
new file mode 100644
index 000000000..658c0e59a
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideGetPremiumPatch.java
@@ -0,0 +1,14 @@
+package app.revanced.extension.music.patches;
+
+import app.revanced.extension.music.settings.Settings;
+
+@SuppressWarnings("unused")
+public class HideGetPremiumPatch {
+
+ /**
+ * Injection point
+ */
+ public static boolean hideGetPremiumLabel() {
+ return Settings.HIDE_GET_PREMIUM_LABEL.get();
+ }
+}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideUpgradeButtonPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideUpgradeButtonPatch.java
new file mode 100644
index 000000000..8d3e022b1
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideUpgradeButtonPatch.java
@@ -0,0 +1,14 @@
+package app.revanced.extension.music.patches;
+
+import app.revanced.extension.music.settings.Settings;
+
+@SuppressWarnings("unused")
+public class HideUpgradeButtonPatch {
+
+ /**
+ * Injection point
+ */
+ public static boolean hideUpgradeButton() {
+ return Settings.HIDE_UPGRADE_BUTTON.get();
+ }
+}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideVideoAdsPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideVideoAdsPatch.java
new file mode 100644
index 000000000..9c4d51ee3
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideVideoAdsPatch.java
@@ -0,0 +1,17 @@
+package app.revanced.extension.music.patches;
+
+import app.revanced.extension.music.settings.Settings;
+
+@SuppressWarnings("unused")
+public class HideVideoAdsPatch {
+
+ /**
+ * Injection point
+ */
+ public static boolean showVideoAds(boolean original) {
+ if (Settings.HIDE_VIDEO_ADS.get()) {
+ return false;
+ }
+ return original;
+ }
+}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/PermanentRepeatPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/PermanentRepeatPatch.java
new file mode 100644
index 000000000..b44b0a3f1
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/PermanentRepeatPatch.java
@@ -0,0 +1,14 @@
+package app.revanced.extension.music.patches;
+
+import app.revanced.extension.music.settings.Settings;
+
+@SuppressWarnings("unused")
+public class PermanentRepeatPatch {
+
+ /**
+ * Injection point
+ */
+ public static boolean permanentRepeat() {
+ return Settings.PERMANENT_REPEAT.get();
+ }
+}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/GoogleApiActivityHook.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/GoogleApiActivityHook.java
new file mode 100644
index 000000000..8597113c6
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/GoogleApiActivityHook.java
@@ -0,0 +1,84 @@
+package app.revanced.extension.music.settings;
+
+import android.app.Activity;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.preference.PreferenceFragment;
+import android.view.View;
+
+import app.revanced.extension.music.settings.preference.ReVancedPreferenceFragment;
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.BaseActivityHook;
+
+/**
+ * Hooks GoogleApiActivity to inject a custom ReVancedPreferenceFragment with a toolbar.
+ */
+public class GoogleApiActivityHook extends BaseActivityHook {
+ /**
+ * Injection point
+ *
+ * Creates an instance of GoogleApiActivityHook for use in static initialization.
+ */
+ @SuppressWarnings("unused")
+ public static GoogleApiActivityHook createInstance() {
+ // Must touch the Music settings to ensure the class is loaded and
+ // the values can be found when setting the UI preferences.
+ // Logging anything under non debug ensures this is set.
+ Logger.printInfo(() -> "Permanent repeat enabled: " + Settings.PERMANENT_REPEAT.get());
+
+ return new GoogleApiActivityHook();
+ }
+
+ /**
+ * Sets the fixed theme for the activity.
+ */
+ @Override
+ protected void customizeActivityTheme(Activity activity) {
+ // Override the default YouTube Music theme to increase start padding of list items.
+ // Custom style located in resources/music/values/style.xml
+ activity.setTheme(Utils.getResourceIdentifier("Theme.ReVanced.YouTubeMusic.Settings", "style"));
+ }
+
+ /**
+ * Returns the resource ID for the YouTube Music settings layout.
+ */
+ @Override
+ protected int getContentViewResourceId() {
+ return Utils.getResourceIdentifier("revanced_music_settings_with_toolbar", "layout");
+ }
+
+ /**
+ * Returns the fixed background color for the toolbar.
+ */
+ @Override
+ protected int getToolbarBackgroundColor() {
+ return Utils.getResourceColor("ytm_color_black");
+ }
+
+ /**
+ * Returns the navigation icon with a color filter applied.
+ */
+ @Override
+ protected Drawable getNavigationIcon() {
+ Drawable navigationIcon = ReVancedPreferenceFragment.getBackButtonDrawable();
+ navigationIcon.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
+ return navigationIcon;
+ }
+
+ /**
+ * Returns the click listener that finishes the activity when the navigation icon is clicked.
+ */
+ @Override
+ protected View.OnClickListener getNavigationClickListener(Activity activity) {
+ return view -> activity.finish();
+ }
+
+ /**
+ * Creates a new ReVancedPreferenceFragment for the activity.
+ */
+ @Override
+ protected PreferenceFragment createPreferenceFragment() {
+ return new ReVancedPreferenceFragment();
+ }
+}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java
new file mode 100644
index 000000000..394cc7b3e
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java
@@ -0,0 +1,21 @@
+package app.revanced.extension.music.settings;
+
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+
+import app.revanced.extension.shared.settings.BaseSettings;
+import app.revanced.extension.shared.settings.BooleanSetting;
+
+public class Settings extends BaseSettings {
+
+ // Ads
+ public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true);
+ public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true);
+ public static final BooleanSetting HIDE_UPGRADE_BUTTON = new BooleanSetting("revanced_music_hide_upgrade_button", TRUE, true);
+
+ // General
+ public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_music_hide_category_bar", FALSE, true);
+
+ // Player
+ public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true);
+}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java
new file mode 100644
index 000000000..67ca69ba4
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java
@@ -0,0 +1,38 @@
+package app.revanced.extension.music.settings.preference;
+
+import android.widget.Toolbar;
+
+import app.revanced.extension.music.settings.GoogleApiActivityHook;
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
+
+/**
+ * Preference fragment for ReVanced settings.
+ */
+@SuppressWarnings({"deprecation", "NewApi"})
+public class ReVancedPreferenceFragment extends ToolbarPreferenceFragment {
+
+ /**
+ * Initializes the preference fragment.
+ */
+ @Override
+ protected void initialize() {
+ super.initialize();
+
+ try {
+ Utils.sortPreferenceGroups(getPreferenceScreen());
+ setPreferenceScreenToolbar(getPreferenceScreen());
+ } catch (Exception ex) {
+ Logger.printException(() -> "initialize failure", ex);
+ }
+ }
+
+ /**
+ * Sets toolbar for all nested preference screens.
+ */
+ @Override
+ protected void customizeToolbar(Toolbar toolbar) {
+ GoogleApiActivityHook.setToolbarLayoutParams(toolbar);
+ }
+}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java
new file mode 100644
index 000000000..a24897ca5
--- /dev/null
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java
@@ -0,0 +1,142 @@
+package app.revanced.extension.shared.settings;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.preference.PreferenceFragment;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toolbar;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
+
+/**
+ * Base class for hooking activities to inject a custom PreferenceFragment with a toolbar.
+ * Provides common logic for initializing the activity and setting up the toolbar.
+ */
+@SuppressWarnings({"deprecation", "NewApi"})
+public abstract class BaseActivityHook extends Activity {
+
+ /**
+ * Layout parameters for the toolbar, extracted from the dummy toolbar.
+ */
+ protected static ViewGroup.LayoutParams toolbarLayoutParams;
+
+ /**
+ * Sets the layout parameters for the toolbar.
+ */
+ public static void setToolbarLayoutParams(Toolbar toolbar) {
+ if (toolbarLayoutParams != null) {
+ toolbar.setLayoutParams(toolbarLayoutParams);
+ }
+ }
+
+ /**
+ * Initializes the activity by setting the theme, content view and injecting a PreferenceFragment.
+ */
+ public static void initialize(BaseActivityHook hook, Activity activity) {
+ try {
+ hook.customizeActivityTheme(activity);
+ activity.setContentView(hook.getContentViewResourceId());
+
+ // Sanity check.
+ String dataString = activity.getIntent().getDataString();
+ if (!"revanced_settings_intent".equals(dataString)) {
+ Logger.printException(() -> "Unknown intent: " + dataString);
+ return;
+ }
+
+ PreferenceFragment fragment = hook.createPreferenceFragment();
+ hook.createToolbar(activity, fragment);
+
+ activity.getFragmentManager()
+ .beginTransaction()
+ .replace(Utils.getResourceIdentifier("revanced_settings_fragments", "id"), fragment)
+ .commit();
+ } catch (Exception ex) {
+ Logger.printException(() -> "initialize failure", ex);
+ }
+ }
+
+ /**
+ * Creates and configures a toolbar for the activity, replacing a dummy placeholder.
+ */
+ @SuppressLint("UseCompatLoadingForDrawables")
+ protected void createToolbar(Activity activity, PreferenceFragment fragment) {
+ // Replace dummy placeholder toolbar.
+ // This is required to fix submenu title alignment issue with Android ASOP 15+
+ ViewGroup toolBarParent = activity.findViewById(
+ Utils.getResourceIdentifier("revanced_toolbar_parent", "id"));
+ ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolBarParent, "revanced_toolbar");
+ toolbarLayoutParams = dummyToolbar.getLayoutParams();
+ toolBarParent.removeView(dummyToolbar);
+
+ // Sets appropriate system navigation bar color for the activity.
+ ToolbarPreferenceFragment.setNavigationBarColor(activity.getWindow());
+
+ Toolbar toolbar = new Toolbar(toolBarParent.getContext());
+ toolbar.setBackgroundColor(getToolbarBackgroundColor());
+ toolbar.setNavigationIcon(getNavigationIcon());
+ toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
+ toolbar.setTitle(Utils.getResourceIdentifier("revanced_settings_title", "string"));
+
+ final int margin = Utils.dipToPixels(16);
+ toolbar.setTitleMarginStart(margin);
+ toolbar.setTitleMarginEnd(margin);
+ TextView toolbarTextView = Utils.getChildView(toolbar, false, view -> view instanceof TextView);
+ if (toolbarTextView != null) {
+ toolbarTextView.setTextColor(Utils.getAppForegroundColor());
+ toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
+ }
+ setToolbarLayoutParams(toolbar);
+
+ onPostToolbarSetup(activity, toolbar, fragment);
+
+ toolBarParent.addView(toolbar, 0);
+ }
+
+ /**
+ * Customizes the activity's theme.
+ */
+ protected abstract void customizeActivityTheme(Activity activity);
+
+ /**
+ * Returns the resource ID for the content view layout.
+ */
+ protected abstract int getContentViewResourceId();
+
+ /**
+ * Returns the background color for the toolbar.
+ */
+ protected abstract int getToolbarBackgroundColor();
+
+ /**
+ * Returns the navigation icon drawable for the toolbar.
+ */
+ protected abstract Drawable getNavigationIcon();
+
+ /**
+ * Returns the click listener for the toolbar's navigation icon.
+ */
+ protected abstract View.OnClickListener getNavigationClickListener(Activity activity);
+
+ /**
+ * Creates the PreferenceFragment to be injected into the activity.
+ */
+ protected PreferenceFragment createPreferenceFragment() {
+ return new ToolbarPreferenceFragment();
+ }
+
+ /**
+ * Performs additional setup after the toolbar is configured.
+ *
+ * @param activity The activity hosting the toolbar.
+ * @param toolbar The configured toolbar.
+ * @param fragment The PreferenceFragment associated with the activity.
+ */
+ protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {}
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ClearLogBufferPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ClearLogBufferPreference.java
similarity index 88%
rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ClearLogBufferPreference.java
rename to extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ClearLogBufferPreference.java
index 109c6c8e7..7dbf0dd38 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ClearLogBufferPreference.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ClearLogBufferPreference.java
@@ -1,9 +1,8 @@
-package app.revanced.extension.youtube.settings.preference;
+package app.revanced.extension.shared.settings.preference;
import android.content.Context;
import android.util.AttributeSet;
import android.preference.Preference;
-import app.revanced.extension.shared.settings.preference.LogBufferManager;
/**
* A custom preference that clears the ReVanced debug log buffer when clicked.
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExportLogToClipboardPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ExportLogToClipboardPreference.java
similarity index 88%
rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExportLogToClipboardPreference.java
rename to extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ExportLogToClipboardPreference.java
index fac1cfa79..57fb12823 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExportLogToClipboardPreference.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ExportLogToClipboardPreference.java
@@ -1,9 +1,8 @@
-package app.revanced.extension.youtube.settings.preference;
+package app.revanced.extension.shared.settings.preference;
import android.content.Context;
import android.util.AttributeSet;
import android.preference.Preference;
-import app.revanced.extension.shared.settings.preference.LogBufferManager;
/**
* A custom preference that triggers exporting ReVanced debug logs to the clipboard when clicked.
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ToolbarPreferenceFragment.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ToolbarPreferenceFragment.java
new file mode 100644
index 000000000..05a1fddcc
--- /dev/null
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ToolbarPreferenceFragment.java
@@ -0,0 +1,150 @@
+package app.revanced.extension.shared.settings.preference;
+
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.graphics.Insets;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.preference.Preference;
+import android.preference.PreferenceScreen;
+import android.util.TypedValue;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.widget.TextView;
+import android.widget.Toolbar;
+
+import androidx.annotation.Nullable;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.BaseActivityHook;
+
+@SuppressWarnings({"deprecation", "NewApi"})
+public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
+ /**
+ * Sets toolbar for all nested preference screens.
+ */
+ protected void setPreferenceScreenToolbar(PreferenceScreen parentScreen) {
+ for (int i = 0, count = parentScreen.getPreferenceCount(); i < count; i++) {
+ Preference childPreference = parentScreen.getPreference(i);
+ if (childPreference instanceof PreferenceScreen) {
+ // Recursively set sub preferences.
+ setPreferenceScreenToolbar((PreferenceScreen) childPreference);
+
+ childPreference.setOnPreferenceClickListener(
+ childScreen -> {
+ Dialog preferenceScreenDialog = ((PreferenceScreen) childScreen).getDialog();
+ ViewGroup rootView = (ViewGroup) preferenceScreenDialog
+ .findViewById(android.R.id.content)
+ .getParent();
+
+ // Allow package-specific background customization.
+ customizeDialogBackground(rootView);
+
+ // Fix the system navigation bar color for submenus.
+ setNavigationBarColor(preferenceScreenDialog.getWindow());
+
+ // Fix edge-to-edge screen with Android 15 and YT 19.45+
+ // https://developer.android.com/develop/ui/views/layout/edge-to-edge#system-bars-insets
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ rootView.setOnApplyWindowInsetsListener((v, insets) -> {
+ Insets statusInsets = insets.getInsets(WindowInsets.Type.statusBars());
+ Insets navInsets = insets.getInsets(WindowInsets.Type.navigationBars());
+ Insets cutoutInsets = insets.getInsets(WindowInsets.Type.displayCutout());
+
+ // Apply padding for display cutout in landscape.
+ int leftPadding = cutoutInsets.left;
+ int rightPadding = cutoutInsets.right;
+ int topPadding = statusInsets.top;
+ int bottomPadding = navInsets.bottom;
+
+ v.setPadding(leftPadding, topPadding, rightPadding, bottomPadding);
+ return insets;
+ });
+ }
+
+ Toolbar toolbar = new Toolbar(childScreen.getContext());
+ toolbar.setTitle(childScreen.getTitle());
+ toolbar.setNavigationIcon(getBackButtonDrawable());
+ toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
+
+ final int margin = Utils.dipToPixels(16);
+ toolbar.setTitleMargin(margin, 0, margin, 0);
+
+ TextView toolbarTextView = Utils.getChildView(toolbar,
+ true, TextView.class::isInstance);
+ if (toolbarTextView != null) {
+ toolbarTextView.setTextColor(Utils.getAppForegroundColor());
+ toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
+ }
+
+ // Allow package-specific toolbar customization.
+ customizeToolbar(toolbar);
+
+ // Allow package-specific post-toolbar setup.
+ onPostToolbarSetup(toolbar, preferenceScreenDialog);
+
+ rootView.addView(toolbar, 0);
+ return false;
+ }
+ );
+ }
+ }
+ }
+
+ /**
+ * Sets the system navigation bar color for the activity.
+ * Applies the background color obtained from {@link Utils#getAppBackgroundColor()} to the navigation bar.
+ * For Android 10 (API 29) and above, enforces navigation bar contrast to ensure visibility.
+ */
+ public static void setNavigationBarColor(@Nullable Window window) {
+ if (window == null) {
+ Logger.printDebug(() -> "Cannot set navigation bar color, window is null");
+ return;
+ }
+
+ window.setNavigationBarColor(Utils.getAppBackgroundColor());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ window.setNavigationBarContrastEnforced(true);
+ }
+ }
+
+ /**
+ * Returns the drawable for the back button.
+ */
+ @SuppressLint("UseCompatLoadingForDrawables")
+ public static Drawable getBackButtonDrawable() {
+ final int backButtonResource = Utils.getResourceIdentifier(
+ "revanced_settings_toolbar_arrow_left", "drawable");
+ Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
+ customizeBackButtonDrawable(drawable);
+ return drawable;
+ }
+
+ /**
+ * Customizes the back button drawable.
+ */
+ protected static void customizeBackButtonDrawable(Drawable drawable) {
+ drawable.setTint(Utils.getAppForegroundColor());
+ }
+
+ /**
+ * Allows subclasses to customize the dialog's root view background.
+ */
+ protected void customizeDialogBackground(ViewGroup rootView) {
+ rootView.setBackgroundColor(Utils.getAppBackgroundColor());
+ }
+
+ /**
+ * Allows subclasses to customize the toolbar.
+ */
+ protected void customizeToolbar(Toolbar toolbar) {
+ BaseActivityHook.setToolbarLayoutParams(toolbar);
+ }
+
+ /**
+ * Allows subclasses to perform actions after toolbar setup.
+ */
+ protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {}
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java
index 24d3a4f42..5c4d3ca77 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java
@@ -1,50 +1,120 @@
package app.revanced.extension.youtube.settings;
-import static app.revanced.extension.shared.Utils.getResourceIdentifier;
-
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
import android.preference.PreferenceFragment;
-import android.util.TypedValue;
-import android.view.ViewGroup;
-import android.widget.TextView;
+import android.view.View;
import android.widget.Toolbar;
-import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.AppLanguage;
+import app.revanced.extension.shared.settings.BaseActivityHook;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.youtube.patches.VersionCheckPatch;
import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch;
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
/**
- * Hooks LicenseActivity.
- *
- * This class is responsible for injecting our own fragment by replacing the LicenseActivity.
+ * Hooks LicenseActivity to inject a custom ReVancedPreferenceFragment with a toolbar and search functionality.
*/
-@SuppressWarnings("unused")
-public class LicenseActivityHook extends Activity {
+@SuppressWarnings("deprecation")
+public class LicenseActivityHook extends BaseActivityHook {
private static int currentThemeValueOrdinal = -1; // Must initially be a non-valid enum ordinal value.
- private static ViewGroup.LayoutParams toolbarLayoutParams;
-
+ /**
+ * Controller for managing search view components in the toolbar.
+ */
@SuppressLint("StaticFieldLeak")
public static SearchViewController searchViewController;
- public static void setToolbarLayoutParams(Toolbar toolbar) {
- if (toolbarLayoutParams != null) {
- toolbar.setLayoutParams(toolbarLayoutParams);
+ /**
+ * Injection point
+ *
+ * Creates an instance of LicenseActivityHook for use in static initialization.
+ */
+ @SuppressWarnings("unused")
+ public static LicenseActivityHook createInstance() {
+ return new LicenseActivityHook();
+ }
+
+ /**
+ * Customizes the activity theme based on dark/light mode.
+ */
+ @Override
+ protected void customizeActivityTheme(Activity activity) {
+ final var theme = Utils.isDarkModeEnabled()
+ ? "Theme.YouTube.Settings.Dark"
+ : "Theme.YouTube.Settings";
+ activity.setTheme(Utils.getResourceIdentifier(theme, "style"));
+ }
+
+ /**
+ * Returns the resource ID for the YouTube settings layout.
+ */
+ @Override
+ protected int getContentViewResourceId() {
+ return Utils.getResourceIdentifier("revanced_settings_with_toolbar", "layout");
+ }
+
+ /**
+ * Returns the toolbar background color based on dark/light mode.
+ */
+ @Override
+ protected int getToolbarBackgroundColor() {
+ final String colorName = Utils.isDarkModeEnabled()
+ ? "yt_black3"
+ : "yt_white1";
+ return Utils.getColorFromString(colorName);
+ }
+
+ /**
+ * Returns the navigation icon drawable for the toolbar.
+ */
+ @Override
+ protected Drawable getNavigationIcon() {
+ return ReVancedPreferenceFragment.getBackButtonDrawable();
+ }
+
+ /**
+ * Returns the click listener for the navigation icon.
+ */
+ @Override
+ protected View.OnClickListener getNavigationClickListener(Activity activity) {
+ return null;
+ }
+
+ /**
+ * Adds search view components to the toolbar for ReVancedPreferenceFragment.
+ *
+ * @param activity The activity hosting the toolbar.
+ * @param toolbar The configured toolbar.
+ * @param fragment The PreferenceFragment associated with the activity.
+ */
+ @Override
+ protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {
+ if (fragment instanceof ReVancedPreferenceFragment) {
+ searchViewController = SearchViewController.addSearchViewComponents(
+ activity, toolbar, (ReVancedPreferenceFragment) fragment);
}
}
+ /**
+ * Creates a new ReVancedPreferenceFragment for the activity.
+ */
+ @Override
+ protected PreferenceFragment createPreferenceFragment() {
+ return new ReVancedPreferenceFragment();
+ }
+
/**
* Injection point.
* Overrides the ReVanced settings language.
*/
+ @SuppressWarnings("unused")
public static Context getAttachBaseContext(Context original) {
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
if (language == AppLanguage.DEFAULT) {
@@ -57,6 +127,7 @@ public class LicenseActivityHook extends Activity {
/**
* Injection point.
*/
+ @SuppressWarnings("unused")
public static boolean useCairoSettingsFragment(boolean original) {
// Early targets have layout issues and it's better to always force off.
if (!VersionCheckPatch.IS_19_34_OR_GREATER) {
@@ -80,87 +151,6 @@ public class LicenseActivityHook extends Activity {
/**
* Injection point.
*
- * Hooks LicenseActivity#onCreate in order to inject our own fragment.
- */
- public static void initialize(Activity licenseActivity) {
- try {
- setActivityTheme(licenseActivity);
- ReVancedPreferenceFragment.setNavigationBarColor(licenseActivity.getWindow());
- licenseActivity.setContentView(getResourceIdentifier(
- "revanced_settings_with_toolbar", "layout"));
-
- // Sanity check.
- String dataString = licenseActivity.getIntent().getDataString();
- if (!"revanced_settings_intent".equals(dataString)) {
- Logger.printException(() -> "Unknown intent: " + dataString);
- return;
- }
-
- PreferenceFragment fragment = new ReVancedPreferenceFragment();
- createToolbar(licenseActivity, fragment);
-
- //noinspection deprecation
- licenseActivity.getFragmentManager()
- .beginTransaction()
- .replace(getResourceIdentifier("revanced_settings_fragments", "id"), fragment)
- .commit();
- } catch (Exception ex) {
- Logger.printException(() -> "initialize failure", ex);
- }
- }
-
- @SuppressLint("UseCompatLoadingForDrawables")
- private static void createToolbar(Activity activity, PreferenceFragment fragment) {
- // Replace dummy placeholder toolbar.
- // This is required to fix submenu title alignment issue with Android ASOP 15+
- ViewGroup toolBarParent = activity.findViewById(
- getResourceIdentifier("revanced_toolbar_parent", "id"));
- ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolBarParent, "revanced_toolbar");
- toolbarLayoutParams = dummyToolbar.getLayoutParams();
- toolBarParent.removeView(dummyToolbar);
-
- Toolbar toolbar = new Toolbar(toolBarParent.getContext());
- toolbar.setBackgroundColor(getToolbarBackgroundColor());
- toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable());
- toolbar.setTitle(getResourceIdentifier("revanced_settings_title", "string"));
-
- final int margin = Utils.dipToPixels(16);
- toolbar.setTitleMarginStart(margin);
- toolbar.setTitleMarginEnd(margin);
- TextView toolbarTextView = Utils.getChildView(toolbar, false,
- view -> view instanceof TextView);
- if (toolbarTextView != null) {
- toolbarTextView.setTextColor(Utils.getAppForegroundColor());
- toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
- }
- setToolbarLayoutParams(toolbar);
-
- // Add Search bar only for ReVancedPreferenceFragment.
- if (fragment instanceof ReVancedPreferenceFragment) {
- searchViewController = SearchViewController.addSearchViewComponents(activity, toolbar, (ReVancedPreferenceFragment) fragment);
- }
-
- toolBarParent.addView(toolbar, 0);
- }
-
- public static void setActivityTheme(Activity activity) {
- final var theme = Utils.isDarkModeEnabled()
- ? "Theme.YouTube.Settings.Dark"
- : "Theme.YouTube.Settings";
- activity.setTheme(getResourceIdentifier(theme, "style"));
- }
-
- public static int getToolbarBackgroundColor() {
- final String colorName = Utils.isDarkModeEnabled()
- ? "yt_black3"
- : "yt_white1";
-
- return Utils.getColorFromString(colorName);
- }
-
- /**
- * Injection point.
- *
* Updates dark/light mode since YT settings can force light/dark mode
* which can differ from the global device settings.
*/
@@ -173,6 +163,10 @@ public class LicenseActivityHook extends Activity {
}
}
+ /**
+ * Handles configuration changes, such as orientation, to update the search view.
+ */
+ @SuppressWarnings("unused")
public static void handleConfigurationChanged(Activity activity, Configuration newConfig) {
if (searchViewController != null) {
searchViewController.handleOrientationChange(newConfig.orientation);
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
index c4cb42313..c96ed26ed 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
@@ -3,11 +3,7 @@ package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
-import android.annotation.SuppressLint;
import android.app.Dialog;
-import android.graphics.Insets;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
@@ -17,11 +13,6 @@ import android.preference.SwitchPreference;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
-import android.util.TypedValue;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowInsets;
-import android.widget.TextView;
import android.widget.Toolbar;
import androidx.annotation.CallSuper;
@@ -40,16 +31,16 @@ import java.util.regex.Pattern;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
+import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
import app.revanced.extension.youtube.settings.LicenseActivityHook;
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
/**
* Preference fragment for ReVanced settings.
*/
-@SuppressWarnings("deprecation")
-public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
+@SuppressWarnings({"deprecation", "NewApi"})
+public class ReVancedPreferenceFragment extends ToolbarPreferenceFragment {
/**
* The main PreferenceScreen used to display the current set of preferences.
@@ -70,31 +61,6 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
*/
private final List> allPreferences = new ArrayList<>();
- @SuppressLint("UseCompatLoadingForDrawables")
- public static Drawable getBackButtonDrawable() {
- final int backButtonResource = getResourceIdentifier("revanced_settings_toolbar_arrow_left", "drawable");
- Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
- drawable.setTint(Utils.getAppForegroundColor());
- return drawable;
- }
-
- /**
- * Sets the system navigation bar color for the activity.
- * Applies the background color obtained from {@link Utils#getAppBackgroundColor()} to the navigation bar.
- * For Android 10 (API 29) and above, enforces navigation bar contrast to ensure visibility.
- */
- public static void setNavigationBarColor(@Nullable Window window) {
- if (window == null) {
- Logger.printDebug(() -> "Cannot set navigation bar color, window is null");
- return;
- }
-
- window.setNavigationBarColor(Utils.getAppBackgroundColor());
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- window.setNavigationBarContrastEnforced(true);
- }
- }
-
/**
* Initializes the preference fragment, copying the original screen to allow full restoration.
*/
@@ -139,8 +105,28 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
}
}
+ /**
+ * Sets toolbar for all nested preference screens.
+ */
+ @Override
+ protected void customizeToolbar(Toolbar toolbar) {
+ LicenseActivityHook.setToolbarLayoutParams(toolbar);
+ }
+
+ /**
+ * Perform actions after toolbar setup.
+ */
+ @Override
+ protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {
+ if (LicenseActivityHook.searchViewController != null
+ && LicenseActivityHook.searchViewController.isSearchActive()) {
+ toolbar.post(() -> LicenseActivityHook.searchViewController.closeSearch());
+ }
+ }
+
/**
* Recursively collects all preferences from the screen or group.
+ *
* @param includeDepth Menu depth to start including preferences.
* A value of 0 adds all preferences.
*/
@@ -222,75 +208,6 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
preferenceScreen.addPreference(noResultsPreference);
}
}
-
- /**
- * Sets toolbar for all nested preference screens.
- */
- private void setPreferenceScreenToolbar(PreferenceScreen parentScreen) {
- for (int i = 0, count = parentScreen.getPreferenceCount(); i < count; i++) {
- Preference childPreference = parentScreen.getPreference(i);
- if (childPreference instanceof PreferenceScreen) {
- // Recursively set sub preferences.
- setPreferenceScreenToolbar((PreferenceScreen) childPreference);
-
- childPreference.setOnPreferenceClickListener(
- childScreen -> {
- Dialog preferenceScreenDialog = ((PreferenceScreen) childScreen).getDialog();
- ViewGroup rootView = (ViewGroup) preferenceScreenDialog
- .findViewById(android.R.id.content)
- .getParent();
-
- // Fix the system navigation bar color for submenus.
- setNavigationBarColor(preferenceScreenDialog.getWindow());
-
- // Fix edge-to-edge screen with Android 15 and YT 19.45+
- // https://developer.android.com/develop/ui/views/layout/edge-to-edge#system-bars-insets
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- rootView.setOnApplyWindowInsetsListener((v, insets) -> {
- Insets statusInsets = insets.getInsets(WindowInsets.Type.statusBars());
- Insets navInsets = insets.getInsets(WindowInsets.Type.navigationBars());
- Insets cutoutInsets = insets.getInsets(WindowInsets.Type.displayCutout());
-
- // Apply padding for display cutout in landscape.
- int leftPadding = cutoutInsets.left;
- int rightPadding = cutoutInsets.right;
- int topPadding = statusInsets.top;
- int bottomPadding = navInsets.bottom;
-
- v.setPadding(leftPadding, topPadding, rightPadding, bottomPadding);
- return insets;
- });
- }
-
- Toolbar toolbar = new Toolbar(childScreen.getContext());
- toolbar.setTitle(childScreen.getTitle());
- toolbar.setNavigationIcon(getBackButtonDrawable());
- toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
-
- final int margin = Utils.dipToPixels(16);
- toolbar.setTitleMargin(margin, 0, margin, 0);
-
- TextView toolbarTextView = Utils.getChildView(toolbar,
- true, TextView.class::isInstance);
- if (toolbarTextView != null) {
- toolbarTextView.setTextColor(Utils.getAppForegroundColor());
- toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
- }
-
- LicenseActivityHook.setToolbarLayoutParams(toolbar);
-
- if (LicenseActivityHook.searchViewController != null
- && LicenseActivityHook.searchViewController.isSearchActive()) {
- toolbar.post(() -> LicenseActivityHook.searchViewController.closeSearch());
- }
-
- rootView.addView(toolbar, 0);
- return false;
- }
- );
- }
- }
- }
}
@SuppressWarnings("deprecation")
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 70df3fdcc..00b58e88e 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -372,8 +372,9 @@ public final class app/revanced/patches/music/layout/premium/HideGetPremiumPatch
public static final fun getHideGetPremiumPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
-public final class app/revanced/patches/music/layout/upgradebutton/RemoveUpgradeButtonPatchKt {
- public static final fun getRemoveUpgradeButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+public final class app/revanced/patches/music/layout/upgradebutton/HideUpgradeButtonPatchKt {
+ public static final fun getHideUpgradeButton ()Lapp/revanced/patcher/patch/BytecodePatch;
+ public static final fun getRemoveUpgradeButton ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatchKt {
@@ -396,7 +397,21 @@ public final class app/revanced/patches/music/misc/gms/GmsCoreSupportPatchKt {
public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
-public final class app/revanced/patches/music/misc/spoof/SpoofVideoStreamsKt {
+public final class app/revanced/patches/music/misc/settings/PreferenceScreen : app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen {
+ public static final field INSTANCE Lapp/revanced/patches/music/misc/settings/PreferenceScreen;
+ public fun commit (Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference;)V
+ public final fun getADS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
+ public final fun getGENERAL ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
+ public final fun getMISC ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
+ public final fun getPLAYER ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
+}
+
+public final class app/revanced/patches/music/misc/settings/SettingsPatchKt {
+ public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+ public static final fun newIntent (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent;
+}
+
+public final class app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatchKt {
public static final fun getSpoofVideoStreamsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -1652,7 +1667,6 @@ public final class app/revanced/patches/youtube/misc/settings/PreferenceScreen :
}
public final class app/revanced/patches/youtube/misc/settings/SettingsPatchKt {
- public static final fun addSettingPreference (Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)V
public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
public static final fun newIntent (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt b/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt
index 4c89bfb03..fd417bb5d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt
@@ -1,13 +1,27 @@
package app.revanced.patches.music.ad.video
-import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.all.misc.resources.addResources
+import app.revanced.patches.all.misc.resources.addResourcesPatch
+import app.revanced.patches.music.misc.extension.sharedExtensionPatch
+import app.revanced.patches.music.misc.settings.PreferenceScreen
+import app.revanced.patches.music.misc.settings.settingsPatch
+import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
+
+private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/HideVideoAdsPatch;"
@Suppress("unused")
val hideVideoAdsPatch = bytecodePatch(
name = "Hide music video ads",
- description = "Hides ads that appear while listening to or streaming music videos, podcasts, or songs.",
+ description = "Adds an option to hide ads that appear while listening to or streaming music videos, podcasts, or songs.",
) {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsPatch,
+ addResourcesPatch,
+ )
+
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52"
@@ -15,9 +29,21 @@ val hideVideoAdsPatch = bytecodePatch(
)
execute {
+ addResources("music", "ad.video.hideVideoAdsPatch")
+
+ PreferenceScreen.ADS.addPreferences(
+ SwitchPreference("revanced_music_hide_video_ads"),
+ )
+
navigate(showVideoAdsParentFingerprint.originalMethod)
.to(showVideoAdsParentFingerprint.patternMatch!!.startIndex + 1)
.stop()
- .addInstruction(0, "const/4 p1, 0x0")
+ .addInstructions(
+ 0,
+ """
+ invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->showVideoAds(Z)Z
+ move-result p1
+ """
+ )
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt b/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt
index 9834f649e..4d598e402 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt
@@ -1,6 +1,8 @@
package app.revanced.patches.music.audio.exclusiveaudio
import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.music.misc.extension.sharedExtensionPatch
+import app.revanced.patches.music.misc.settings.settingsPatch
import app.revanced.util.returnEarly
@Suppress("unused")
@@ -8,6 +10,11 @@ val enableExclusiveAudioPlaybackPatch = bytecodePatch(
name = "Enable exclusive audio playback",
description = "Enables the option to play audio without video.",
) {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsPatch,
+ )
+
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52"
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt
index bfcdaba38..559bacc39 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt
@@ -4,13 +4,27 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWith
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.smali.ExternalLabel
+import app.revanced.patches.all.misc.resources.addResources
+import app.revanced.patches.all.misc.resources.addResourcesPatch
+import app.revanced.patches.music.misc.extension.sharedExtensionPatch
+import app.revanced.patches.music.misc.settings.PreferenceScreen
+import app.revanced.patches.music.misc.settings.settingsPatch
+import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
+import app.revanced.util.findFreeRegister
+
+private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/PermanentRepeatPatch;"
@Suppress("unused")
val permanentRepeatPatch = bytecodePatch(
name = "Permanent repeat",
- description = "Permanently remember your repeating preference even if the playlist ends or another track is played.",
- use = false,
+ description = "Adds an option to always repeat even if the playlist ends or another track is played."
) {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsPatch,
+ addResourcesPatch,
+ )
+
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52"
@@ -18,13 +32,27 @@ val permanentRepeatPatch = bytecodePatch(
)
execute {
+ addResources("music", "interaction.permanentrepeat.permanentRepeatPatch")
+
+ PreferenceScreen.PLAYER.addPreferences(
+ SwitchPreference("revanced_music_play_permanent_repeat"),
+ )
+
val startIndex = repeatTrackFingerprint.patternMatch!!.endIndex
val repeatIndex = startIndex + 1
repeatTrackFingerprint.method.apply {
+ // Start index is at a branch, but the same
+ // register is clobbered in both branch paths.
+ val freeRegister = findFreeRegister(startIndex + 1)
+
addInstructionsWithLabels(
startIndex,
- "goto :repeat",
+ """
+ invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->permanentRepeat()Z
+ move-result v$freeRegister
+ if-nez v$freeRegister, :repeat
+ """,
ExternalLabel("repeat", instructions[repeatIndex]),
)
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt
index 78ef86f50..883be02a6 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt
@@ -1,17 +1,32 @@
package app.revanced.patches.music.layout.compactheader
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.all.misc.resources.addResources
+import app.revanced.patches.all.misc.resources.addResourcesPatch
+import app.revanced.patches.music.misc.extension.sharedExtensionPatch
+import app.revanced.patches.music.misc.settings.PreferenceScreen
+import app.revanced.patches.music.misc.settings.settingsPatch
+import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
+import app.revanced.util.addInstructionsAtControlFlowLabel
import app.revanced.util.findFreeRegister
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/HideCategoryBarPatch;"
+
@Suppress("unused")
val hideCategoryBar = bytecodePatch(
name = "Hide category bar",
- description = "Hides the category bar at the top of the homepage.",
- use = false,
+ description = "Adds an option to hide the category bar at the top of the homepage."
) {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsPatch,
+ addResourcesPatch,
+ )
+
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52"
@@ -19,16 +34,27 @@ val hideCategoryBar = bytecodePatch(
)
execute {
+ addResources("music", "layout.compactheader.hideCategoryBar")
+
+ PreferenceScreen.GENERAL.addPreferences(
+ SwitchPreference("revanced_music_hide_category_bar"),
+ )
+
constructCategoryBarFingerprint.method.apply {
val insertIndex = constructCategoryBarFingerprint.patternMatch!!.startIndex
val register = getInstruction(insertIndex - 1).registerA
val freeRegister = findFreeRegister(insertIndex, register)
- addInstructions(
+ addInstructionsWithLabels(
insertIndex,
"""
+ invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->hideCategoryBar()Z
+ move-result v$freeRegister
+ if-eqz v$freeRegister, :show
const/16 v$freeRegister, 0x8
invoke-virtual { v$register, v$freeRegister }, Landroid/view/View;->setVisibility(I)V
+ :show
+ nop
"""
)
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt
index 7ba3260ef..beea72673 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt
@@ -1,16 +1,31 @@
package app.revanced.patches.music.layout.premium
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
-import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.all.misc.resources.addResources
+import app.revanced.patches.all.misc.resources.addResourcesPatch
+import app.revanced.patches.music.misc.extension.sharedExtensionPatch
+import app.revanced.patches.music.misc.settings.PreferenceScreen
+import app.revanced.patches.music.misc.settings.settingsPatch
+import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
+private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/HideGetPremiumPatch;"
+
+@Suppress("unused")
val hideGetPremiumPatch = bytecodePatch(
- name = "Hide 'Get Music Premium' label",
- description = "Hides the \"Get Music Premium\" label from the account menu and settings.",
+ name = "Hide 'Get Music Premium'",
+ description = "Adds an option to hide the \"Get Music Premium\" label in the settings and account menu.",
) {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsPatch,
+ addResourcesPatch,
+ )
+
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52"
@@ -18,6 +33,12 @@ val hideGetPremiumPatch = bytecodePatch(
)
execute {
+ addResources("music", "layout.premium.hideGetPremiumPatch")
+
+ PreferenceScreen.ADS.addPreferences(
+ SwitchPreference("revanced_music_hide_get_premium_label"),
+ )
+
hideGetPremiumFingerprint.method.apply {
val insertIndex = hideGetPremiumFingerprint.patternMatch!!.endIndex
@@ -37,12 +58,17 @@ val hideGetPremiumPatch = bytecodePatch(
)
}
- membershipSettingsFingerprint.method.addInstructions(
+ membershipSettingsFingerprint.method.addInstructionsWithLabels(
0,
"""
- const/4 v0, 0x0
- return-object v0
- """,
+ invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->hideGetPremiumLabel()Z
+ move-result v0
+ if-eqz v0, :show
+ const/4 v0, 0x0
+ return-object v0
+ :show
+ nop
+ """
)
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/RemoveUpgradeButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/HideUpgradeButtonPatch.kt
similarity index 70%
rename from patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/RemoveUpgradeButtonPatch.kt
rename to patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/HideUpgradeButtonPatch.kt
index 426ab8046..b54e4f2b6 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/RemoveUpgradeButtonPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/HideUpgradeButtonPatch.kt
@@ -7,17 +7,31 @@ import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.extensions.newLabel
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.smali.toInstructions
+import app.revanced.patches.all.misc.resources.addResources
+import app.revanced.patches.all.misc.resources.addResourcesPatch
+import app.revanced.patches.music.misc.extension.sharedExtensionPatch
+import app.revanced.patches.music.misc.settings.PreferenceScreen
+import app.revanced.patches.music.misc.settings.settingsPatch
+import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.util.getReference
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction22t
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/HideUpgradeButtonPatch;"
+
@Suppress("unused")
-val removeUpgradeButtonPatch = bytecodePatch(
- name = "Remove upgrade button",
- description = "Removes the upgrade tab from the pivot bar.",
+val hideUpgradeButton = bytecodePatch(
+ name = "Hide upgrade button",
+ description = "Hides the upgrade tab from the pivot bar.",
) {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsPatch,
+ addResourcesPatch,
+ )
+
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52"
@@ -25,6 +39,15 @@ val removeUpgradeButtonPatch = bytecodePatch(
)
execute {
+ addResources("music", "layout.upgradebutton.hideUpgradeButtonPatch")
+
+ // TODO: Add an extension patch to allow this to be enabled/disabled in app.
+ if (false) {
+ PreferenceScreen.ADS.addPreferences(
+ SwitchPreference("revanced_music_hide_upgrade_button")
+ )
+ }
+
pivotBarConstructorFingerprint.method.apply {
val pivotBarElementFieldReference =
getInstruction(pivotBarConstructorFingerprint.patternMatch!!.endIndex - 1)
@@ -77,3 +100,9 @@ val removeUpgradeButtonPatch = bytecodePatch(
}
}
}
+
+@Deprecated("Patch was renamed", ReplaceWith("hideUpgradeButton"))
+@Suppress("unused")
+val removeUpgradeButton = bytecodePatch{
+ dependsOn(hideUpgradeButton)
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch.kt
index a08e1fceb..70e707fbb 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch.kt
@@ -1,6 +1,8 @@
package app.revanced.patches.music.misc.androidauto
import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.music.misc.extension.sharedExtensionPatch
+import app.revanced.patches.music.misc.settings.settingsPatch
import app.revanced.util.returnEarly
@Suppress("unused")
@@ -8,6 +10,11 @@ val bypassCertificateChecksPatch = bytecodePatch(
name = "Bypass certificate checks",
description = "Bypasses certificate checks which prevent YouTube Music from working on Android Auto.",
) {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsPatch
+ )
+
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52"
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt
index ab0fe132d..3d81296a7 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt
@@ -1,13 +1,20 @@
package app.revanced.patches.music.misc.backgroundplayback
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
-import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.music.misc.extension.sharedExtensionPatch
+import app.revanced.patches.music.misc.settings.settingsPatch
+import app.revanced.util.returnEarly
val backgroundPlaybackPatch = bytecodePatch(
name = "Remove background playback restrictions",
description = "Removes restrictions on background playback, including playing kids videos in the background.",
) {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsPatch
+ )
+
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52"
@@ -20,12 +27,6 @@ val backgroundPlaybackPatch = bytecodePatch(
"return-void",
)
- backgroundPlaybackDisableFingerprint.method.addInstructions(
- 0,
- """
- const/4 v0, 0x1
- return v0
- """,
- )
+ backgroundPlaybackDisableFingerprint.method.returnEarly(true)
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt
index 0fa223b23..b61258496 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt
@@ -1,12 +1,17 @@
package app.revanced.patches.music.misc.gms
import app.revanced.patcher.patch.Option
+import app.revanced.patches.all.misc.resources.addResources
+import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME
import app.revanced.patches.music.misc.gms.Constants.REVANCED_MUSIC_PACKAGE_NAME
+import app.revanced.patches.music.misc.settings.PreferenceScreen
+import app.revanced.patches.music.misc.settings.settingsPatch
import app.revanced.patches.music.misc.spoof.spoofVideoStreamsPatch
import app.revanced.patches.shared.castContextFetchFingerprint
import app.revanced.patches.shared.misc.gms.gmsCoreSupportPatch
+import app.revanced.patches.shared.misc.settings.preference.IntentPreference
import app.revanced.patches.shared.primeMethodFingerprint
@Suppress("unused")
@@ -33,4 +38,23 @@ private fun gmsCoreSupportResourcePatch(
toPackageName = REVANCED_MUSIC_PACKAGE_NAME,
gmsCoreVendorGroupIdOption = gmsCoreVendorGroupIdOption,
spoofedPackageSignature = "afb0fed5eeaebdd86f56a97742f4b6b33ef59875",
-)
+ executeBlock = {
+ addResources("shared", "misc.gms.gmsCoreSupportResourcePatch")
+
+ val gmsCoreVendorGroupId by gmsCoreVendorGroupIdOption
+
+ PreferenceScreen.MISC.addPreferences(
+ IntentPreference(
+ "microg_settings",
+ intent = IntentPreference.Intent("", "org.microg.gms.ui.SettingsActivity") {
+ "$gmsCoreVendorGroupId.android.gms"
+ }
+ )
+ )
+ }
+) {
+ dependsOn(
+ addResourcesPatch,
+ settingsPatch
+ )
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/Fingerprints.kt
new file mode 100644
index 000000000..580e6e767
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/Fingerprints.kt
@@ -0,0 +1,11 @@
+package app.revanced.patches.music.misc.settings
+
+import app.revanced.patcher.fingerprint
+
+internal val googleApiActivityFingerprint = fingerprint {
+ returns("V")
+ parameters("Landroid/os/Bundle;")
+ custom { method, classDef ->
+ classDef.endsWith("GoogleApiActivity;") && method.name == "onCreate"
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/SettingsPatch.kt
new file mode 100644
index 000000000..09fab446b
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/SettingsPatch.kt
@@ -0,0 +1,176 @@
+package app.revanced.patches.music.misc.settings
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.patch.resourcePatch
+import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName
+import app.revanced.patches.all.misc.resources.addResources
+import app.revanced.patches.all.misc.resources.addResourcesPatch
+import app.revanced.patches.music.misc.extension.sharedExtensionPatch
+import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
+import app.revanced.patches.shared.misc.settings.preference.*
+import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
+import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting
+import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
+import app.revanced.patches.shared.misc.settings.settingsPatch
+import app.revanced.util.*
+import com.android.tools.smali.dexlib2.util.MethodUtil
+
+private const val BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
+ "Lapp/revanced/extension/shared/settings/BaseActivityHook;"
+private const val GOOGLE_API_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
+ "Lapp/revanced/extension/music/settings/GoogleApiActivityHook;"
+
+private val preferences = mutableSetOf()
+
+
+private val settingsResourcePatch = resourcePatch {
+ dependsOn(
+ resourceMappingPatch,
+ settingsPatch(
+ IntentPreference(
+ titleKey = "revanced_settings_title",
+ summaryKey = null,
+ intent = newIntent("revanced_settings_intent"),
+ ) to "settings_headers",
+ preferences
+ )
+ )
+
+ execute {
+
+ // TODO: Remove this when search will be abstract.
+ copyResources(
+ "settings",
+ ResourceGroup(
+ "layout",
+ "revanced_music_settings_with_toolbar.xml"
+ )
+ )
+
+ val targetResource = "values/styles.xml"
+ inputStreamFromBundledResource(
+ "settings/music",
+ targetResource,
+ )!!.let { inputStream ->
+ "resources".copyXmlNode(
+ document(inputStream),
+ document("res/$targetResource"),
+ ).close()
+ }
+
+ // Remove horizontal divider from the settings Preferences.
+ val styleFile = get("res/values/styles.xml")
+ styleFile.writeText(
+ styleFile.readText()
+ .replace(
+ "allowDividerAbove\">true",
+ "allowDividerAbove\">false"
+ ).replace(
+ "allowDividerBelow\">true",
+ "allowDividerBelow\">false"
+ )
+ )
+ }
+}
+
+val settingsPatch = bytecodePatch(
+ description = "Adds settings for ReVanced to YouTube Music.",
+) {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsResourcePatch,
+ addResourcesPatch,
+ )
+
+ execute {
+ addResources("music", "misc.settings.settingsPatch")
+ addResources("shared", "misc.debugging.enableDebuggingPatch")
+
+ // Should make a separate debugging patch, but for now include it with all installations.
+ PreferenceScreen.MISC.addPreferences(
+ PreferenceScreenPreference(
+ key = "revanced_debug_screen",
+ sorting = Sorting.UNSORTED,
+ preferences = setOf(
+ SwitchPreference("revanced_debug"),
+ NonInteractivePreference(
+ "revanced_debug_export_logs_to_clipboard",
+ tag = "app.revanced.extension.shared.settings.preference.ExportLogToClipboardPreference",
+ selectable = true
+ ),
+ NonInteractivePreference(
+ "revanced_debug_logs_clear_buffer",
+ tag = "app.revanced.extension.shared.settings.preference.ClearLogBufferPreference",
+ selectable = true
+ )
+ )
+ )
+ )
+
+ // Add an "About" preference to the top.
+ preferences += NonInteractivePreference(
+ key = "revanced_settings_music_screen_0_about",
+ summaryKey = null,
+ tag = "app.revanced.extension.shared.settings.preference.ReVancedAboutPreference",
+ selectable = true,
+ )
+
+ // Modify GoogleApiActivity and remove all existing layout code.
+ // Must modify an existing activity and cannot add a new activity to the manifest,
+ // as that fails for root installations.
+
+ googleApiActivityFingerprint.method.addInstructions(
+ 1,
+ """
+ invoke-static { }, $GOOGLE_API_ACTIVITY_HOOK_CLASS_DESCRIPTOR->createInstance()Lapp/revanced/extension/music/settings/GoogleApiActivityHook;
+ move-result-object v0
+ invoke-static { v0, p0 }, $BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->initialize(Lapp/revanced/extension/shared/settings/BaseActivityHook;Landroid/app/Activity;)V
+ return-void
+ """
+ )
+
+ // Remove other methods as they will break as the onCreate method is modified above.
+ googleApiActivityFingerprint.classDef.apply {
+ methods.removeIf { it.name != "onCreate" && !MethodUtil.isConstructor(it) }
+ }
+ }
+
+ finalize {
+ PreferenceScreen.close()
+ }
+}
+
+/**
+ * Creates an intent to open ReVanced settings.
+ */
+fun newIntent(settingsName: String) = IntentPreference.Intent(
+ data = settingsName,
+ targetClass = "com.google.android.gms.common.api.GoogleApiActivity"
+) {
+ // The package name change has to be reflected in the intent.
+ setOrGetFallbackPackageName("com.google.android.apps.youtube.music")
+}
+
+object PreferenceScreen : BasePreferenceScreen() {
+ val ADS = Screen(
+ "revanced_settings_music_screen_1_ads",
+ summaryKey = null
+ )
+ val GENERAL = Screen(
+ "revanced_settings_music_screen_2_general",
+ summaryKey = null
+ )
+ val PLAYER = Screen(
+ "revanced_settings_music_screen_3_player",
+ summaryKey = null
+ )
+ val MISC = Screen(
+ "revanced_settings_music_screen_4_misc",
+ summaryKey = null
+ )
+
+ override fun commit(screen: PreferenceScreenPreference) {
+ preferences += screen
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreams.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt
similarity index 51%
rename from patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreams.kt
rename to patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt
index e50a70a49..7d1a4c648 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreams.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt
@@ -1,12 +1,19 @@
package app.revanced.patches.music.misc.spoof
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patches.all.misc.resources.addResources
+import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
import app.revanced.patches.music.misc.gms.musicActivityOnCreateFingerprint
+import app.revanced.patches.music.misc.settings.PreferenceScreen
+import app.revanced.patches.music.misc.settings.settingsPatch
import app.revanced.patches.music.playservice.is_7_33_or_greater
import app.revanced.patches.music.playservice.is_8_11_or_greater
import app.revanced.patches.music.playservice.is_8_15_or_greater
import app.revanced.patches.music.playservice.versionCheckPatch
+import app.revanced.patches.shared.misc.settings.preference.ListPreference
+import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
+import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.shared.misc.spoof.spoofVideoStreamsPatch
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch;"
@@ -16,17 +23,36 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch(
fixMediaFetchHotConfigAlternativeChanges = { is_8_11_or_greater && !is_8_15_or_greater },
fixParsePlaybackResponseFeatureFlag = { is_7_33_or_greater },
block = {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsPatch,
+ addResourcesPatch,
+ versionCheckPatch,
+ userAgentClientSpoofPatch
+ )
+
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52"
)
)
-
- dependsOn(sharedExtensionPatch, versionCheckPatch, userAgentClientSpoofPatch)
},
executeBlock = {
+ addResources("shared", "misc.spoof.spoofVideoStreamsPatch")
+
+ PreferenceScreen.MISC.addPreferences(
+ PreferenceScreenPreference(
+ key = "revanced_spoof_video_streams_screen",
+ sorting = PreferenceScreenPreference.Sorting.UNSORTED,
+ preferences = setOf(
+ SwitchPreference("revanced_spoof_video_streams"),
+ ListPreference("revanced_spoof_video_streams_client_type"),
+ )
+ )
+ )
+
musicActivityOnCreateFingerprint.method.addInstruction(
- 1, // Must use 1 index so context is set by extension patch.
+ 0,
"invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->setClientOrderToUse()V"
)
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt
index 923089d01..26b80a2f9 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt
@@ -9,8 +9,8 @@ import app.revanced.patcher.patch.BytecodePatchBuilder
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
+import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
-import app.revanced.patches.music.misc.extension.sharedExtensionPatch
import app.revanced.util.findFreeRegister
import app.revanced.util.findInstructionIndicesReversedOrThrow
import app.revanced.util.getReference
@@ -46,6 +46,8 @@ fun spoofVideoStreamsPatch(
dependsOn(addResourcesPatch)
execute {
+ addResources("shared", "misc.fix.playback.spoofVideoStreamsPatch")
+
// region Enable extension helper method used by other patches
patchIncludedExtensionMethodFingerprint.method.returnEarly(true)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt
index 6d5b70905..ab8c54afb 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt
@@ -22,6 +22,8 @@ import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/patches/EnableDebuggingPatch;"
+// TODO: Refactor this into a shared patch that can be used by both YT and YT Music.
+// Almost all of the feature flag hooks are the same between both apps.
val enableDebuggingPatch = bytecodePatch(
name = "Enable debugging",
description = "Adds options for debugging and exporting ReVanced logs to the clipboard.",
@@ -45,6 +47,7 @@ val enableDebuggingPatch = bytecodePatch(
)
execute {
+ addResources("shared", "misc.debugging.enableDebuggingPatch")
addResources("youtube", "misc.debugging.enableDebuggingPatch")
PreferenceScreen.MISC.addPreferences(
@@ -58,13 +61,13 @@ val enableDebuggingPatch = bytecodePatch(
SwitchPreference("revanced_debug_toast_on_error"),
NonInteractivePreference(
"revanced_debug_export_logs_to_clipboard",
- tag = "app.revanced.extension.youtube.settings.preference.ExportLogToClipboardPreference",
- selectable = true,
+ tag = "app.revanced.extension.shared.settings.preference.ExportLogToClipboardPreference",
+ selectable = true
),
NonInteractivePreference(
"revanced_debug_logs_clear_buffer",
- tag = "app.revanced.extension.youtube.settings.preference.ClearLogBufferPreference",
- selectable = true,
+ tag = "app.revanced.extension.shared.settings.preference.ClearLogBufferPreference",
+ selectable = true
),
),
),
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt
index 3c8b238cc..5ffd49cb4 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt
@@ -53,7 +53,7 @@ private fun gmsCoreSupportResourcePatch(
gmsCoreVendorGroupIdOption = gmsCoreVendorGroupIdOption,
spoofedPackageSignature = "24bb24c05e47e0aefa68a58a766179d9b613a600",
executeBlock = {
- addResources("youtube", "misc.gms.gmsCoreSupportResourcePatch")
+ addResources("shared", "misc.gms.gmsCoreSupportResourcePatch")
val gmsCoreVendorGroupId by gmsCoreVendorGroupIdOption
@@ -62,10 +62,14 @@ private fun gmsCoreSupportResourcePatch(
"microg_settings",
intent = IntentPreference.Intent("", "org.microg.gms.ui.SettingsActivity") {
"$gmsCoreVendorGroupId.android.gms"
- },
- ),
+ }
+ )
)
- },
+ }
) {
- dependsOn(settingsPatch, addResourcesPatch, accountCredentialsInvalidTextPatch)
+ dependsOn(
+ addResourcesPatch,
+ settingsPatch,
+ accountCredentialsInvalidTextPatch
+ )
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt
index 672999b6a..a90bb9163 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt
@@ -29,7 +29,9 @@ import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
import com.android.tools.smali.dexlib2.util.MethodUtil
-private const val EXTENSION_CLASS_DESCRIPTOR =
+private const val BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
+ "Lapp/revanced/extension/shared/settings/BaseActivityHook;"
+private const val LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/settings/LicenseActivityHook;"
internal var appearanceStringId = -1L
@@ -37,10 +39,6 @@ internal var appearanceStringId = -1L
private val preferences = mutableSetOf()
-fun addSettingPreference(screen: BasePreference) {
- preferences += screen
-}
-
private val settingsResourcePatch = resourcePatch {
dependsOn(
resourceMappingPatch,
@@ -225,7 +223,9 @@ val settingsPatch = bytecodePatch(
licenseActivityOnCreateFingerprint.method.addInstructions(
1,
"""
- invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->initialize(Landroid/app/Activity;)V
+ invoke-static {}, $LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->createInstance()Lapp/revanced/extension/youtube/settings/LicenseActivityHook;
+ move-result-object v0
+ invoke-static { v0, p0 }, $BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->initialize(Lapp/revanced/extension/shared/settings/BaseActivityHook;Landroid/app/Activity;)V
return-void
"""
)
@@ -249,7 +249,7 @@ val settingsPatch = bytecodePatch(
).toMutable().apply {
addInstructions(
"""
- invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->getAttachBaseContext(Landroid/content/Context;)Landroid/content/Context;
+ invoke-static { p1 }, $LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->getAttachBaseContext(Landroid/content/Context;)Landroid/content/Context;
move-result-object p1
invoke-super { p0, p1 }, $superclass->attachBaseContext(Landroid/content/Context;)V
return-void
@@ -294,7 +294,7 @@ val settingsPatch = bytecodePatch(
addInstructions(
"""
invoke-super { p0, p1 }, Landroid/app/Activity;->onConfigurationChanged(Landroid/content/res/Configuration;)V
- invoke-static { p0, p1 }, $EXTENSION_CLASS_DESCRIPTOR->handleConfigurationChanged(Landroid/app/Activity;Landroid/content/res/Configuration;)V
+ invoke-static { p0, p1 }, $LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->handleConfigurationChanged(Landroid/app/Activity;Landroid/content/res/Configuration;)V
return-void
"""
)
@@ -309,15 +309,15 @@ val settingsPatch = bytecodePatch(
val register = getInstruction(index).registerA
addInstructionsAtControlFlowLabel(
index,
- "invoke-static { v$register }, ${EXTENSION_CLASS_DESCRIPTOR}->updateLightDarkModeStatus(Ljava/lang/Enum;)V",
+ "invoke-static { v$register }, ${LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR}->updateLightDarkModeStatus(Ljava/lang/Enum;)V",
)
}
}
- // Add setting to force cairo settings fragment on/off.
+ // Add setting to force Cairo settings fragment on/off.
cairoFragmentConfigFingerprint.method.insertLiteralOverride(
CAIRO_CONFIG_LITERAL_VALUE,
- "$EXTENSION_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z"
+ "$LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z"
)
}
diff --git a/patches/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml
index ac8860273..c8796f57d 100644
--- a/patches/src/main/resources/addresources/values/arrays.xml
+++ b/patches/src/main/resources/addresources/values/arrays.xml
@@ -121,18 +121,18 @@
- ZH
-
-
- Android VR
- - VisionOS
+ - visionOS
- ANDROID_VR_1_61_48
- VISIONOS
+
+
- @string/revanced_swipe_overlay_style_entry_1
diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml
index a297dd20e..2f4840a5f 100644
--- a/patches/src/main/resources/addresources/values/strings.xml
+++ b/patches/src/main/resources/addresources/values/strings.xml
@@ -124,6 +124,8 @@ To translate new languages visit translate.revanced.app"
and changes made here must also be made there. -->
+ GmsCore Settings
+ Settings for GmsCore
MicroG GmsCore is not installed. Install it.
Action needed
@@ -140,6 +142,37 @@ Disabling battery optimizations for MicroG will not negatively affect battery us
Tap the continue button and allow optimization changes."
Continue
+
+ Spoof video streams
+ Spoof the client video streams to prevent playback issues
+ Spoof video streams
+ Spoof the client video streams to prevent playback issues
+ Spoof video streams
+ "Video streams are spoofed
+
+If you are a YouTube Premium user, this setting may not be required"
+ "Video streams are not spoofed
+
+Playback may not work"
+ Turning off this setting may cause playback issues.
+ Default client
+
+
+ Debugging
+ Enable or disable debugging options
+ Debug logging
+ Debug logs are enabled
+ Debug logs are disabled
+ Export debug logs
+ Copies ReVanced debug logs to the clipboard
+ Debug logging is disabled
+ No logs found
+ Logs copied
+ Failed to export logs: %s
+ Clear debug logs
+ Clears all stored ReVanced debug logs
+ Logs cleared
+
@@ -168,11 +201,6 @@ Tap the continue button and allow optimization changes."
Shorts background play is enabled
- Debugging
- Enable or disable debugging options
- Debug logging
- Debug logs are enabled
- Debug logs are disabled
Log protocol buffer
Debug logs include proto buffer
Debug logs do not include proto buffer
@@ -190,15 +218,6 @@ However, enabling this will also log some user data such as your IP address.""Turning off error toasts hides all ReVanced error notifications.
You will not be notified of any unexpected events."
- Export debug logs
- Copies ReVanced debug logs to the clipboard
- Debug logging is disabled
- No logs found
- Logs copied
- Failed to export logs: %s
- Clear debug logs
- Clears all stored ReVanced debug logs
- Logs cleared
Hide album cards
@@ -1493,10 +1512,6 @@ Higher video qualities might be unlocked but you may experience video playback s
Enabling this can unlock higher video qualities"
Enabling this can cause video playback stuttering, worse battery life, and unknown side effects.
-
- GmsCore Settings
- Settings for GmsCore
-
Haptic feedback
Change haptic feedback
@@ -1610,17 +1625,6 @@ Enabling this can unlock higher video qualities"
Slide to seek is not enabled
- Spoof video streams
- Spoof the client video streams to prevent playback issues
- Spoof video streams
- "Video streams are spoofed
-
-If you are a YouTube Premium user, this setting may not be required"
- "Video streams are not spoofed
-
-Video playback may not work"
- Turning off this setting may cause video playback issues.
- Default client
Spoofing side effects
Android spoofing side effects
"• Audio track menu is missing
@@ -1634,6 +1638,40 @@ Video playback may not work"
Audio stream language
+
+
+ About
+ Ads
+ General
+ Player
+ Miscellaneous
+
+
+ Hide video ads
+ Video ads are hidden
+ Video ads are shown
+
+
+ Enable permanent repeat
+ Permanent repeat is enabled
+ Permanent repeat is disabled
+
+
+
+ Hide \'Get Music Premium\' label
+ Label is hidden
+ Label is shown
+
+
+ Hide upgrade button
+ Button is hidden
+ Button is shown
+
+
Block audio ads
diff --git a/patches/src/main/resources/settings/layout/revanced_music_settings_with_toolbar.xml b/patches/src/main/resources/settings/layout/revanced_music_settings_with_toolbar.xml
new file mode 100644
index 000000000..09ce00ad4
--- /dev/null
+++ b/patches/src/main/resources/settings/layout/revanced_music_settings_with_toolbar.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/patches/src/main/resources/settings/music/values/styles.xml b/patches/src/main/resources/settings/music/values/styles.xml
new file mode 100644
index 000000000..4a692559a
--- /dev/null
+++ b/patches/src/main/resources/settings/music/values/styles.xml
@@ -0,0 +1,7 @@
+
+
+
+