diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f623d8a57..98b14a097 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -72,6 +72,7 @@ body: - **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Bug+report%22). - **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md). + - **Check the troubleshooting guide**: A solution to your issue might be found in the [FAQ](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/questions.md) or the [troubleshooting guide](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/troubleshooting.md). - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app). - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index f49436ec6..13d436ba2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -72,6 +72,7 @@ body: - **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Feature+request%22). - **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md). + - **Check the troubleshooting guide**: Information about your issue might be found in the [FAQ](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/questions.md) or the [troubleshooting guide](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/troubleshooting.md). - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app). - type: textarea attributes: diff --git a/CHANGELOG.md b/CHANGELOG.md index ef513ef33..902aa6cba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,61 @@ +## [5.40.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.40.0...v5.40.1-dev.1) (2025-09-21) + + +### Bug Fixes + +* **YouTube - Return YouTube Dislike:** Do not show error toast if API returns 401 status ([#5949](https://github.com/ReVanced/revanced-patches/issues/5949)) ([58d088a](https://github.com/ReVanced/revanced-patches/commit/58d088ab307440a6912a867246da799b7dd6499b)) +* **YouTube - Settings:** Use an overlay to show search results ([#5806](https://github.com/ReVanced/revanced-patches/issues/5806)) ([ece8076](https://github.com/ReVanced/revanced-patches/commit/ece8076f7cefd752b97515014bc50fe4fd80171e)) + +# [5.40.0](https://github.com/ReVanced/revanced-patches/compare/v5.39.0...v5.40.0) (2025-09-21) + + +### Bug Fixes + +* **Instagram - Limit feed to followed profiles:** Change patch to default off ([767f1e3](https://github.com/ReVanced/revanced-patches/commit/767f1e3695327bdbc4daea8b50a80d4c0a38456a)) +* **Spoof video streams:** Resolve occasional playback stuttering ([5c7c8b5](https://github.com/ReVanced/revanced-patches/commit/5c7c8b536416ec53cd98f7d59d11850aa1b70f11)) +* **YouTube - Force original audio:** Show UI setting summary if spoofing to Android Studio ([b7026b7](https://github.com/ReVanced/revanced-patches/commit/b7026b70865bc44de07b30f84ba8b8b608930d5b)) +* **YouTube - Spoof video streams:** Add "Force original audio" disclaimer for Android Studio client ([f97d332](https://github.com/ReVanced/revanced-patches/commit/f97d33206b4c97244f0bd0c672c4b91eaf477b0b)) +* **YouTube - Spoof video streams:** Add stream audio selector disclaimer for Android Studio client ([a8a4107](https://github.com/ReVanced/revanced-patches/commit/a8a410708d50f34ac4bd2ca29bbbc3cde00bbf93)) + + +### Features + +* **Instagram:** Add `Limit feed to followed profiles` patch ([#5908](https://github.com/ReVanced/revanced-patches/issues/5908)) ([8ba9a19](https://github.com/ReVanced/revanced-patches/commit/8ba9a19ade24c5fe9bd6d4e49772b7663522780e)) +* **Viber - Hide ads:** Support latest app target ([#5863](https://github.com/ReVanced/revanced-patches/issues/5863)) ([e6cce85](https://github.com/ReVanced/revanced-patches/commit/e6cce8554116df3c0ea6dbb7440c59c9e73d8334)) +* **YouTube - Hide video action buttons:** Add "Hide comments" button ([db796fb](https://github.com/ReVanced/revanced-patches/commit/db796fb8830b813e1ed626d491c4a797171e69e7)) +* **YouTube Music:** Add `Enable debugging` patch ([#5939](https://github.com/ReVanced/revanced-patches/issues/5939)) ([418f594](https://github.com/ReVanced/revanced-patches/commit/418f5945c213313f9a77cac9a5c326d89c754dfd)) +* **YouTube Music:** Add `Hide cast button` and `Navigation bar` patches ([#5934](https://github.com/ReVanced/revanced-patches/issues/5934)) ([651d358](https://github.com/ReVanced/revanced-patches/commit/651d3580967a252b57cbf4afbba02d6a4601ccfe)) +* **YouTube Music:** Support version `8.10.52` ([#5941](https://github.com/ReVanced/revanced-patches/issues/5941)) ([01c0f1b](https://github.com/ReVanced/revanced-patches/commit/01c0f1bd1ac6edb8aea758f88ffffcdea74a29b7)) +* **YouTube:** Support version `20.14.43` ([#5940](https://github.com/ReVanced/revanced-patches/issues/5940)) ([f7f4a1b](https://github.com/ReVanced/revanced-patches/commit/f7f4a1b0f0186598266b41a2c6a781fdee49e440)) + +# [5.40.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.10...v5.40.0-dev.11) (2025-09-20) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Add stream audio selector disclaimer for Android Studio client ([a8a4107](https://github.com/ReVanced/revanced-patches/commit/a8a410708d50f34ac4bd2ca29bbbc3cde00bbf93)) + +# [5.40.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.9...v5.40.0-dev.10) (2025-09-20) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Add "Force original audio" disclaimer for Android Studio client ([f97d332](https://github.com/ReVanced/revanced-patches/commit/f97d33206b4c97244f0bd0c672c4b91eaf477b0b)) + +# [5.40.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.8...v5.40.0-dev.9) (2025-09-20) + + +### Features + +* **YouTube Music:** Support version `8.10.52` ([#5941](https://github.com/ReVanced/revanced-patches/issues/5941)) ([01c0f1b](https://github.com/ReVanced/revanced-patches/commit/01c0f1bd1ac6edb8aea758f88ffffcdea74a29b7)) + +# [5.40.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.7...v5.40.0-dev.8) (2025-09-20) + + +### Features + +* **YouTube:** Support version `20.14.43` ([#5940](https://github.com/ReVanced/revanced-patches/issues/5940)) ([f7f4a1b](https://github.com/ReVanced/revanced-patches/commit/f7f4a1b0f0186598266b41a2c6a781fdee49e440)) + # [5.40.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.6...v5.40.0-dev.7) (2025-09-20) 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 deleted file mode 100644 index 8d3e022b1..000000000 --- a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideUpgradeButtonPatch.java +++ /dev/null @@ -1,14 +0,0 @@ -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/settings/GoogleApiActivityHook.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/GoogleApiActivityHook.java deleted file mode 100644 index 57395a0a2..000000000 --- a/extensions/music/src/main/java/app/revanced/extension/music/settings/GoogleApiActivityHook.java +++ /dev/null @@ -1,88 +0,0 @@ -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.ResourceType; -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()); - - // YT Music always uses dark mode. - Utils.setIsDarkModeEnabled(true); - - 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(ResourceType.STYLE, "Theme.ReVanced.YouTubeMusic.Settings")); - } - - /** - * Returns the resource ID for the YouTube Music settings layout. - */ - @Override - protected int getContentViewResourceId() { - return Utils.getResourceIdentifier(ResourceType.LAYOUT, "revanced_music_settings_with_toolbar"); - } - - /** - * 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/MusicActivityHook.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/MusicActivityHook.java new file mode 100644 index 000000000..bb19d2497 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/MusicActivityHook.java @@ -0,0 +1,126 @@ +package app.revanced.extension.music.settings; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.preference.PreferenceFragment; +import android.view.View; +import android.widget.Toolbar; + +import app.revanced.extension.music.settings.preference.MusicPreferenceFragment; +import app.revanced.extension.music.settings.search.MusicSearchViewController; +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 {@link MusicPreferenceFragment} with a toolbar and search. + */ +public class MusicActivityHook extends BaseActivityHook { + + @SuppressLint("StaticFieldLeak") + public static MusicSearchViewController searchViewController; + + /** + * Injection point. + */ + @SuppressWarnings("unused") + public static void initialize(Activity parentActivity) { + // 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()); + + // YT Music always uses dark mode. + Utils.setIsDarkModeEnabled(true); + + BaseActivityHook.initialize(new MusicActivityHook(), parentActivity); + } + + /** + * 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.getResourceIdentifierOrThrow( + "Theme.ReVanced.YouTubeMusic.Settings", "style")); + } + + /** + * Returns the resource ID for the YouTube Music settings layout. + */ + @Override + protected int getContentViewResourceId() { + return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR; + } + + /** + * 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 = MusicPreferenceFragment.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 -> { + if (searchViewController != null && searchViewController.isSearchActive()) { + searchViewController.closeSearch(); + } else { + activity.finish(); + } + }; + } + + /** + * Adds search view components to the toolbar for {@link MusicPreferenceFragment}. + * + * @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 MusicPreferenceFragment) { + searchViewController = MusicSearchViewController.addSearchViewComponents( + activity, toolbar, (MusicPreferenceFragment) fragment); + } + } + + /** + * Creates a new {@link MusicPreferenceFragment} for the activity. + */ + @Override + protected PreferenceFragment createPreferenceFragment() { + return new MusicPreferenceFragment(); + } + + /** + * Injection point. + *

+ * Overrides {@link Activity#finish()} of the injection Activity. + * + * @return if the original activity finish method should be allowed to run. + */ + @SuppressWarnings("unused") + public static boolean handleFinish() { + return MusicSearchViewController.handleFinish(searchViewController); + } +} 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 index 431ec7de0..4feb13d9c 100644 --- 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 @@ -2,7 +2,6 @@ package app.revanced.extension.music.settings; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; - import static app.revanced.extension.shared.settings.Setting.parent; import app.revanced.extension.shared.settings.BaseSettings; @@ -15,7 +14,6 @@ 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_CAST_BUTTON = new BooleanSetting("revanced_music_hide_cast_button", TRUE, false); diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/MusicPreferenceFragment.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/MusicPreferenceFragment.java new file mode 100644 index 000000000..1ebae16df --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/MusicPreferenceFragment.java @@ -0,0 +1,80 @@ +package app.revanced.extension.music.settings.preference; + +import android.app.Dialog; +import android.preference.PreferenceScreen; +import android.widget.Toolbar; + +import app.revanced.extension.music.settings.MusicActivityHook; +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") +public class MusicPreferenceFragment extends ToolbarPreferenceFragment { + /** + * The main PreferenceScreen used to display the current set of preferences. + */ + private PreferenceScreen preferenceScreen; + + /** + * Initializes the preference fragment. + */ + @Override + protected void initialize() { + super.initialize(); + + try { + preferenceScreen = getPreferenceScreen(); + Utils.sortPreferenceGroups(preferenceScreen); + setPreferenceScreenToolbar(preferenceScreen); + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Called when the fragment starts. + */ + @Override + public void onStart() { + super.onStart(); + try { + // Initialize search controller if needed + if (MusicActivityHook.searchViewController != null) { + // Trigger search data collection after fragment is ready. + MusicActivityHook.searchViewController.initializeSearchData(); + } + } catch (Exception ex) { + Logger.printException(() -> "onStart failure", ex); + } + } + + /** + * Sets toolbar for all nested preference screens. + */ + @Override + protected void customizeToolbar(Toolbar toolbar) { + MusicActivityHook.setToolbarLayoutParams(toolbar); + } + + /** + * Perform actions after toolbar setup. + */ + @Override + protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) { + if (MusicActivityHook.searchViewController != null + && MusicActivityHook.searchViewController.isSearchActive()) { + toolbar.post(() -> MusicActivityHook.searchViewController.closeSearch()); + } + } + + /** + * Returns the preference screen for external access by SearchViewController. + */ + public PreferenceScreen getPreferenceScreenForSearch() { + return preferenceScreen; + } +} 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 deleted file mode 100644 index 67ca69ba4..000000000 --- a/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java +++ /dev/null @@ -1,38 +0,0 @@ -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/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchResultsAdapter.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchResultsAdapter.java new file mode 100644 index 000000000..65ccd4ea1 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchResultsAdapter.java @@ -0,0 +1,28 @@ +package app.revanced.extension.music.settings.search; + +import android.content.Context; +import android.preference.PreferenceScreen; + +import app.revanced.extension.shared.settings.search.BaseSearchResultsAdapter; +import app.revanced.extension.shared.settings.search.BaseSearchViewController; +import app.revanced.extension.shared.settings.search.BaseSearchResultItem; + +import java.util.List; + +/** + * Music-specific search results adapter. + */ +@SuppressWarnings("deprecation") +public class MusicSearchResultsAdapter extends BaseSearchResultsAdapter { + + public MusicSearchResultsAdapter(Context context, List items, + BaseSearchViewController.BasePreferenceFragment fragment, + BaseSearchViewController searchViewController) { + super(context, items, fragment, searchViewController); + } + + @Override + protected PreferenceScreen getMainPreferenceScreen() { + return fragment.getPreferenceScreenForSearch(); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchViewController.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchViewController.java new file mode 100644 index 000000000..6681a2f02 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchViewController.java @@ -0,0 +1,71 @@ +package app.revanced.extension.music.settings.search; + +import android.app.Activity; +import android.preference.Preference; +import android.preference.PreferenceScreen; +import android.view.View; +import android.widget.Toolbar; + +import app.revanced.extension.music.settings.preference.MusicPreferenceFragment; +import app.revanced.extension.shared.settings.search.*; + +/** + * Music-specific search view controller implementation. + */ +@SuppressWarnings("deprecation") +public class MusicSearchViewController extends BaseSearchViewController { + + public static MusicSearchViewController addSearchViewComponents(Activity activity, Toolbar toolbar, + MusicPreferenceFragment fragment) { + return new MusicSearchViewController(activity, toolbar, fragment); + } + + private MusicSearchViewController(Activity activity, Toolbar toolbar, MusicPreferenceFragment fragment) { + super(activity, toolbar, new PreferenceFragmentAdapter(fragment)); + } + + @Override + protected BaseSearchResultsAdapter createSearchResultsAdapter() { + return new MusicSearchResultsAdapter(activity, filteredSearchItems, fragment, this); + } + + @Override + protected boolean isSpecialPreferenceGroup(Preference preference) { + // Music doesn't have SponsorBlock, so no special groups. + return false; + } + + @Override + protected void setupSpecialPreferenceListeners(BaseSearchResultItem item) { + // Music doesn't have special preferences. + // This method can be empty or handle music-specific preferences if any. + } + + // Static method for handling Activity finish + public static boolean handleFinish(MusicSearchViewController searchViewController) { + if (searchViewController != null && searchViewController.isSearchActive()) { + searchViewController.closeSearch(); + return true; + } + return false; + } + + // Adapter to wrap MusicPreferenceFragment to BasePreferenceFragment interface. + private record PreferenceFragmentAdapter(MusicPreferenceFragment fragment) implements BasePreferenceFragment { + + @Override + public PreferenceScreen getPreferenceScreenForSearch() { + return fragment.getPreferenceScreenForSearch(); + } + + @Override + public View getView() { + return fragment.getView(); + } + + @Override + public Activity getActivity() { + return fragment.getActivity(); + } + } +} 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 3f9f0af11..978ee7131 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 @@ -27,6 +27,7 @@ import java.util.Locale; import app.revanced.extension.shared.requests.Requester; import app.revanced.extension.shared.requests.Route; +import app.revanced.extension.shared.ui.CustomDialog; @SuppressWarnings("unused") public class GmsCoreSupport { @@ -80,17 +81,17 @@ public class GmsCoreSupport { // 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( + Pair dialogPair = CustomDialog.create( context, str("gms_core_dialog_title"), // Title. - str(dialogMessageRef), // Message. - null, // No EditText. - str(positiveButtonTextRef), // OK button text. + 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. + null, // No Cancel button action. + null, // No Neutral button text. + null, // No Neutral button action. + true // Dismiss dialog when onNeutralClick. ); Dialog dialog = dialogPair.first; 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 a71037ec7..92903b9f9 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 @@ -4,6 +4,8 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.app.DialogFragment; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -12,9 +14,6 @@ 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; @@ -23,9 +22,6 @@ 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; @@ -37,13 +33,9 @@ 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.ScrollView; -import android.widget.TextView; import android.widget.Toast; import android.widget.Toolbar; @@ -71,6 +63,7 @@ import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference; +@SuppressWarnings("NewApi") public class Utils { @SuppressLint("StaticFieldLeak") @@ -304,42 +297,108 @@ public class Utils { /** * @return zero, if the resource is not found. */ - public static int getResourceIdentifier(ResourceType type, String resourceIdentifierName) { + @SuppressLint("DiscouragedApi") + public static int getResourceIdentifier(Context context, @Nullable ResourceType type, String resourceIdentifierName) { + return context.getResources().getIdentifier(resourceIdentifierName, + type == null ? null : type.value, context.getPackageName()); + } + + public static int getResourceIdentifierOrThrow(Context context, @Nullable ResourceType type, String resourceIdentifierName) { + final int resourceId = getResourceIdentifier(context, type, resourceIdentifierName); + if (resourceId == 0) { + throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName + + " type: " + type); + } + return resourceId; + } + + /** + * @return zero, if the resource is not found. + * @see #getResourceIdentifierOrThrow(ResourceType, String) + */ + public static int getResourceIdentifier(@Nullable ResourceType type, String resourceIdentifierName) { return getResourceIdentifier(getContext(), type, resourceIdentifierName); } /** * @return zero, if the resource is not found. + * @see #getResourceIdentifier(ResourceType, String) */ - @SuppressLint("DiscouragedApi") - public static int getResourceIdentifier(Context context, ResourceType type, String resourceIdentifierName) { - return context.getResources().getIdentifier(resourceIdentifierName, type.value, context.getPackageName()); + public static int getResourceIdentifierOrThrow(@Nullable ResourceType type, String resourceIdentifierName) { + return getResourceIdentifierOrThrow(getContext(), type, resourceIdentifierName); + } + + /** + * @return The resource identifier, or throws an exception if not found. + */ + @Deprecated + public static int getResourceIdentifierOrThrow(Context context, String resourceIdentifierName, @Nullable String type) { + final int resourceId = getResourceIdentifier(context, type, resourceIdentifierName); + if (resourceId == 0) { + throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName + + " type: " + type); + } + return resourceId; + } + + + /** + * Instead use {@link #getResourceIdentifierOrThrow(ResourceType, String)} + */ + @Deprecated + public static int getResourceIdentifierOrThrow(String resourceIdentifierName, @Nullable String stringType) { + return getResourceIdentifierOrThrow(getContext(), resourceIdentifierName, stringType); + } + + /** + * Instead use {@link #getResourceIdentifier(ResourceType, String)} + */ + @Deprecated + public static int getResourceIdentifier(String resourceIdentifierName, @Nullable String stringType) { + return getResourceIdentifier(getContext(), resourceIdentifierName, stringType); + } + + /** + * Instead use {@link #getResourceIdentifier(Context, ResourceType, String)} + */ + @Deprecated + public static int getResourceIdentifier(Context context, String resourceIdentifierName, @Nullable String stringType) { + // Find ResourceType with same name as type parameter string + ResourceType convertedType = null; + for (ResourceType type : ResourceType.values()) { + if (type.value.equals(stringType)) { + convertedType = type; + break; + } + } + + return getResourceIdentifierOrThrow(context, convertedType, resourceIdentifierName); } public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException { - return getContext().getResources().getInteger(getResourceIdentifier(ResourceType.INTEGER, resourceIdentifierName)); + return getContext().getResources().getInteger(getResourceIdentifierOrThrow(ResourceType.INTEGER, resourceIdentifierName)); } public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException { - return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(ResourceType.ANIM, resourceIdentifierName)); + return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(ResourceType.ANIM, resourceIdentifierName)); } @ColorInt public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException { //noinspection deprecation - return getContext().getResources().getColor(getResourceIdentifier(ResourceType.COLOR, resourceIdentifierName)); + return getContext().getResources().getColor(getResourceIdentifierOrThrow(ResourceType.COLOR, resourceIdentifierName)); } public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException { - return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(ResourceType.DIMEN, resourceIdentifierName)); + return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName)); } public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException { - return getContext().getResources().getDimension(getResourceIdentifier(ResourceType.DIMEN, resourceIdentifierName)); + return getContext().getResources().getDimension(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName)); } public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException { - return getContext().getResources().getStringArray(getResourceIdentifier(ResourceType.ARRAY, resourceIdentifierName)); + return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(ResourceType.ARRAY, resourceIdentifierName)); } public interface MatchFilter { @@ -350,13 +409,9 @@ public class Utils { * Includes sub children. */ public static R getChildViewByResourceName(View view, String str) { - var child = view.findViewById(Utils.getResourceIdentifier(ResourceType.ID, str)); - if (child != null) { - //noinspection unchecked - return (R) child; - } - - throw new IllegalArgumentException("View with resource name not found: " + str); + var child = view.findViewById(Utils.getResourceIdentifierOrThrow(ResourceType.ID, str)); + //noinspection unchecked + return (R) child; } /** @@ -442,9 +497,9 @@ public class Utils { } public static void setClipboard(CharSequence text) { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context + ClipboardManager clipboard = (ClipboardManager) context .getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text); + ClipData clip = ClipData.newPlainText("ReVanced", text); clipboard.setPrimaryClip(clip); } @@ -604,7 +659,13 @@ public class Utils { showToast(messageToToast, Toast.LENGTH_LONG); } - private static void showToast(String messageToToast, int toastDuration) { + /** + * Safe to call from any thread. + * + * @param messageToToast Message to show. + * @param toastDuration Either {@link Toast#LENGTH_SHORT} or {@link Toast#LENGTH_LONG}. + */ + public static void showToast(String messageToToast, int toastDuration) { Objects.requireNonNull(messageToToast); runOnMainThreadNowOrLater(() -> { Context currentContext = context; @@ -746,396 +807,32 @@ 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. + * Configures the parameters of a dialog window, including its width, gravity, vertical offset and background dimming. + * The width is calculated as a percentage of the screen's portrait width and the vertical offset is specified in DIP. + * The default dialog background is removed to allow for custom styling. * - * @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. + * @param window The {@link Window} object to configure. + * @param gravity The gravity for positioning the dialog (e.g., {@link Gravity#BOTTOM}). + * @param yOffsetDip The vertical offset from the gravity position in DIP. + * @param widthPercentage The width of the dialog as a percentage of the screen's portrait width (0-100). + * @param dimAmount If true, sets the background dim amount to 0 (no dimming); if false, leaves the default dim amount. */ - @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. - - // Preset size constants. - final int dip4 = dipToPixels(4); - final int dip8 = dipToPixels(8); - final int dip16 = dipToPixels(16); - final int dip24 = dipToPixels(24); - - // Create main layout. - LinearLayout mainLayout = new LinearLayout(context); - mainLayout.setOrientation(LinearLayout.VERTICAL); - 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, dip16); - titleView.setLayoutParams(layoutParams); - mainLayout.addView(titleView); - } - - // Create content container (message/EditText) inside a ScrollView only if message or editText is provided. - ScrollView contentScrollView = null; - LinearLayout contentContainer; - if (message != null || editText != null) { - contentScrollView = new ScrollView(context); - contentScrollView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar. - contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); - if (editText != null) { - ShapeDrawable scrollViewBackground = new ShapeDrawable(new RoundRectShape( - createCornerRadii(10), null, null)); - scrollViewBackground.getPaint().setColor(getEditTextBackground()); - contentScrollView.setPadding(dip8, dip8, dip8, dip8); - contentScrollView.setBackground(scrollViewBackground); - contentScrollView.setClipToOutline(true); - } - LinearLayout.LayoutParams contentParams = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - 0, - 1.0f // Weight to take available space. - ); - contentScrollView.setLayoutParams(contentParams); - contentContainer = new LinearLayout(context); - contentContainer.setOrientation(LinearLayout.VERTICAL); - contentScrollView.addView(contentContainer); - - // Message (if not replaced by EditText). - if (editText == 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 - ); - messageView.setLayoutParams(messageParams); - contentContainer.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(Color.TRANSPARENT); - editText.setPadding(0, 0, 0, 0); - LinearLayout.LayoutParams editTextParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ); - contentContainer.addView(editText, 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, dip16, 0, 0); - buttonContainer.setLayoutParams(buttonContainerParams); - - // Lists to track buttons. - List