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 336ea890f..000000000 --- a/extensions/music/src/main/java/app/revanced/extension/music/settings/GoogleApiActivityHook.java +++ /dev/null @@ -1,87 +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.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("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/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 832118829..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; 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 6fbdc233a..0f976ea10 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; @@ -69,6 +61,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") @@ -278,41 +271,63 @@ public class Utils { * @return zero, if the resource is not found. */ @SuppressLint("DiscouragedApi") - public static int getResourceIdentifier(Context context, String resourceIdentifierName, String type) { + public static int getResourceIdentifier(Context context, String resourceIdentifierName, @Nullable String type) { return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName()); } + public static int getResourceIdentifierOrThrow(Context context, String resourceIdentifierName, @Nullable String type) { + final int resourceId = getResourceIdentifier(context, resourceIdentifierName, type); + 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(String, String) */ - public static int getResourceIdentifier(String resourceIdentifierName, String type) { + public static int getResourceIdentifier(String resourceIdentifierName, @Nullable String type) { return getResourceIdentifier(getContext(), resourceIdentifierName, type); } + /** + * @return The resource identifier, or throws an exception if not found. + */ + public static int getResourceIdentifierOrThrow(String resourceIdentifierName, @Nullable String type) { + final int resourceId = getResourceIdentifier(getContext(), resourceIdentifierName, type); + if (resourceId == 0) { + throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName + + " type: " + type); + } + return resourceId; + } + public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException { - return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer")); + return getContext().getResources().getInteger(getResourceIdentifierOrThrow(resourceIdentifierName, "integer")); } public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException { - return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim")); + return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(resourceIdentifierName, "anim")); } @ColorInt public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException { //noinspection deprecation - return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color")); + return getContext().getResources().getColor(getResourceIdentifierOrThrow(resourceIdentifierName, "color")); } public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException { - return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen")); + return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen")); } public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException { - return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen")); + return getContext().getResources().getDimension(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen")); } public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException { - return getContext().getResources().getStringArray(getResourceIdentifier(resourceIdentifierName, "array")); + return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(resourceIdentifierName, "array")); } public interface MatchFilter { @@ -323,13 +338,9 @@ public class Utils { * Includes sub children. */ public static R getChildViewByResourceName(View view, String str) { - var child = view.findViewById(Utils.getResourceIdentifier(str, "id")); - if (child != null) { - //noinspection unchecked - return (R) child; - } - - throw new IllegalArgumentException("View with resource name not found: " + str); + var child = view.findViewById(Utils.getResourceIdentifierOrThrow(str, "id")); + //noinspection unchecked + return (R) child; } /** @@ -415,9 +426,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); } @@ -747,396 +758,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