diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 3e2f365f7..bb66d8526 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -29,7 +29,7 @@ jobs: run: ./gradlew :patches:buildAndroid --no-daemon - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: revanced-patches path: patches/build/libs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a87fdd40..946dc9380 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: run: ./gradlew :patches:buildAndroid clean - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'npm' diff --git a/CHANGELOG.md b/CHANGELOG.md index 693096982..ecc27ca64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,76 @@ +# [5.46.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.9...v5.46.0-dev.10) (2025-11-09) + + +### Features + +* **YouTube - Hide layout components:** Add video description "Hide Featured content" and "Hide Subscribe button" ([#6253](https://github.com/ReVanced/revanced-patches/issues/6253)) ([da4cf94](https://github.com/ReVanced/revanced-patches/commit/da4cf940911a4406e2c9dd558b60305385a80c61)) + +# [5.46.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.8...v5.46.0-dev.9) (2025-11-09) + + +### Features + +* **YouTube Music:** Add `Change miniplayer color` patch ([#6259](https://github.com/ReVanced/revanced-patches/issues/6259)) ([ab808ae](https://github.com/ReVanced/revanced-patches/commit/ab808aeb773592cb26c848d8456478a346ec3bad)) + +# [5.46.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.7...v5.46.0-dev.8) (2025-11-09) + + +### Features + +* **YouTube Music:** Add `Hide buttons` patch ([#6255](https://github.com/ReVanced/revanced-patches/issues/6255)) ([7a18ebc](https://github.com/ReVanced/revanced-patches/commit/7a18ebc7ab74ba30c5d5284a4856c55cdfc31097)) + +# [5.46.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.6...v5.46.0-dev.7) (2025-11-08) + + +### Bug Fixes + +* **YouTube - Settings:** Add additional languages to ReVanced language preference ([d390b54](https://github.com/ReVanced/revanced-patches/commit/d390b54dab92d75b4e0d3e38344eae489dd69d98)) + +# [5.46.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.5...v5.46.0-dev.6) (2025-11-08) + + +### Features + +* **YouTube - Debugging:** Add setting to block experimental client flags ([#6196](https://github.com/ReVanced/revanced-patches/issues/6196)) ([2e9d695](https://github.com/ReVanced/revanced-patches/commit/2e9d6959c94df7588b9e34b18770e9f437e91926)) + +# [5.46.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.4...v5.46.0-dev.5) (2025-11-07) + + +### Bug Fixes + +* **Duolingo - Disable ads:** Constrain patch to last working app target ([f238ae9](https://github.com/ReVanced/revanced-patches/commit/f238ae9895000f01d1dccb800cc8efde0d5362bd)) +* **Instagram - Hide navigation buttons:** Constrain patch to last working app target ([e030e9c](https://github.com/ReVanced/revanced-patches/commit/e030e9c07a7748e117ac44f6776a9f6317b20623)) +* **Spotify - Hide Create button:** Remove obsolete patch that is no longer needed ([#6252](https://github.com/ReVanced/revanced-patches/issues/6252)) ([59d85b2](https://github.com/ReVanced/revanced-patches/commit/59d85b28a7fcb285ff5f2bb6ae654020d76b2019)) + +# [5.46.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.3...v5.46.0-dev.4) (2025-11-07) + + +### Bug Fixes + +* **YouTube - Check watch history domain name resolution:** Fix false positive warning message if the internet connection fails halfway into the DNS check ([5726353](https://github.com/ReVanced/revanced-patches/commit/57263538c79f5a561c449229ac8e068c641285d3)) + +# [5.46.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.2...v5.46.0-dev.3) (2025-11-06) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Fix "Hide Hype points" ([#6247](https://github.com/ReVanced/revanced-patches/issues/6247)) ([5821440](https://github.com/ReVanced/revanced-patches/commit/582144026d28e57bb7adcbba39244f3c7cdbc0f3)) + +# [5.46.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.1...v5.46.0-dev.2) (2025-11-04) + + +### Bug Fixes + +* **YouTube - Settings:** Resolve settings search crash when searching for specific words ([#6231](https://github.com/ReVanced/revanced-patches/issues/6231)) ([76dcfae](https://github.com/ReVanced/revanced-patches/commit/76dcfaefd8679e45a70f265b0239436e60c055cf)) + +# [5.46.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.45.0...v5.46.0-dev.1) (2025-11-04) + + +### Features + +* **YouTube - Hide layout components:** Add "Hide Hype points" ([#6230](https://github.com/ReVanced/revanced-patches/issues/6230)) ([a52c015](https://github.com/ReVanced/revanced-patches/commit/a52c0153b12c3f6f0ad260e03d2e9850c0466392)) +* **YouTube - Hide player flyout menu items:** Add "Hide Listen with YouTube Music" ([#6232](https://github.com/ReVanced/revanced-patches/issues/6232)) ([858edbf](https://github.com/ReVanced/revanced-patches/commit/858edbf3e7f394fcc766d767c8dc54cf5ba24370)) + # [5.45.0](https://github.com/ReVanced/revanced-patches/compare/v5.44.0...v5.45.0) (2025-11-01) diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeMiniplayerColorPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeMiniplayerColorPatch.java new file mode 100644 index 000000000..ec941f7f3 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeMiniplayerColorPatch.java @@ -0,0 +1,14 @@ +package app.revanced.extension.music.patches; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class ChangeMiniplayerColorPatch { + + /** + * Injection point + */ + public static boolean changeMiniplayerColor() { + return Settings.CHANGE_MINIPLAYER_COLOR.get(); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideButtonsPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideButtonsPatch.java new file mode 100644 index 000000000..5794baa9b --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideButtonsPatch.java @@ -0,0 +1,49 @@ +package app.revanced.extension.music.patches; + +import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition; + +import android.view.View; +import android.view.ViewGroup; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class HideButtonsPatch { + + /** + * Injection point + */ + public static int hideCastButton(int original) { + return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original; + } + + /** + * Injection point + */ + public static void hideCastButton(View view) { + hideViewBy0dpUnderCondition(Settings.HIDE_CAST_BUTTON, view); + } + + /** + * Injection point + */ + public static boolean hideHistoryButton(boolean original) { + return original && !Settings.HIDE_HISTORY_BUTTON.get(); + } + + /** + * Injection point + */ + public static void hideNotificationButton(View view) { + if (view.getParent() instanceof ViewGroup viewGroup) { + hideViewBy0dpUnderCondition(Settings.HIDE_NOTIFICATION_BUTTON, viewGroup); + } + } + + /** + * Injection point + */ + public static void hideSearchButton(View view) { + hideViewBy0dpUnderCondition(Settings.HIDE_SEARCH_BUTTON, view); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideCastButtonPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideCastButtonPatch.java deleted file mode 100644 index ab03ba8e4..000000000 --- a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideCastButtonPatch.java +++ /dev/null @@ -1,24 +0,0 @@ -package app.revanced.extension.music.patches; - -import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition; - -import android.view.View; -import app.revanced.extension.music.settings.Settings; - -@SuppressWarnings("unused") -public class HideCastButtonPatch { - - /** - * Injection point - */ - public static int hideCastButton(int original) { - return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original; - } - - /** - * Injection point - */ - public static void hideCastButton(View view) { - hideViewBy0dpUnderCondition(Settings.HIDE_CAST_BUTTON, view); - } -} 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 index 79d772096..f0b090155 100644 --- 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 @@ -3,6 +3,7 @@ package app.revanced.extension.music.patches; import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition; import android.view.View; + import app.revanced.extension.music.settings.Settings; @SuppressWarnings("unused") 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 a56fb04b9..bd0c80402 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 @@ -16,8 +16,11 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true); // General - public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_music_hide_cast_button", TRUE, false); + public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_music_hide_cast_button", TRUE, true); public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_music_hide_category_bar", FALSE, true); + public static final BooleanSetting HIDE_HISTORY_BUTTON = new BooleanSetting("revanced_music_hide_history_button", FALSE, true); + public static final BooleanSetting HIDE_SEARCH_BUTTON = new BooleanSetting("revanced_music_hide_search_button", FALSE, true); + public static final BooleanSetting HIDE_NOTIFICATION_BUTTON = new BooleanSetting("revanced_music_hide_notification_button", FALSE, true); public static final BooleanSetting HIDE_NAVIGATION_BAR_HOME_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_home_button", FALSE, true); public static final BooleanSetting HIDE_NAVIGATION_BAR_SAMPLES_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_samples_button", FALSE, true); public static final BooleanSetting HIDE_NAVIGATION_BAR_EXPLORE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_explore_button", FALSE, true); @@ -27,6 +30,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_NAVIGATION_BAR_LABEL = new BooleanSetting("revanced_music_hide_navigation_bar_labels", FALSE, true); // Player + public static final BooleanSetting CHANGE_MINIPLAYER_COLOR = new BooleanSetting("revanced_music_change_miniplayer_color", FALSE, true); public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true); // Miscellaneous diff --git a/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/videoplayer/PlaybackSpeedPatch.java b/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/videoplayer/PlaybackSpeedPatch.java index ad7fd04c1..b11ec0875 100644 --- a/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/videoplayer/PlaybackSpeedPatch.java +++ b/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/videoplayer/PlaybackSpeedPatch.java @@ -16,6 +16,7 @@ import java.util.Arrays; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.ui.Dim; import com.amazon.video.sdk.player.Player; @@ -64,9 +65,8 @@ public class PlaybackSpeedPatch { SpeedIconDrawable speedIcon = new SpeedIconDrawable(); speedButton.setImageDrawable(speedIcon); - int buttonSize = Utils.dipToPixels(48); - speedButton.setMinimumWidth(buttonSize); - speedButton.setMinimumHeight(buttonSize); + speedButton.setMinimumWidth(Dim.dp48); + speedButton.setMinimumHeight(Dim.dp48); return speedButton; } @@ -197,11 +197,11 @@ class SpeedIconDrawable extends Drawable { @Override public int getIntrinsicWidth() { - return Utils.dipToPixels(32); + return Dim.dp32; } @Override public int getIntrinsicHeight() { - return Utils.dipToPixels(32); + return Dim.dp32; } -} \ No newline at end of file +} 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 a9d7b6e9d..5d6bb492d 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 @@ -23,9 +23,7 @@ import android.os.Looper; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; -import android.util.DisplayMetrics; import android.util.Pair; -import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -34,17 +32,15 @@ import android.view.Window; import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.AnimationUtils; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; import android.widget.Toast; -import android.widget.Toolbar; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.text.Bidi; +import java.text.Collator; +import java.text.Normalizer; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -61,6 +57,7 @@ import app.revanced.extension.shared.settings.AppLanguage; import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference; +import app.revanced.extension.shared.ui.Dim; @SuppressWarnings("NewApi") public class Utils { @@ -79,6 +76,15 @@ public class Utils { @Nullable private static Boolean isDarkModeEnabled; + // Cached Collator instance with its locale. + @Nullable + private static Locale cachedCollatorLocale; + @Nullable + private static Collator cachedCollator; + + private static final Pattern PUNCTUATION_PATTERN = Pattern.compile("\\p{P}+"); + private static final Pattern DIACRITICS_PATTERN = Pattern.compile("\\p{M}"); + private Utils() { } // utility class @@ -749,31 +755,25 @@ public class Utils { } /** - * Hide a view by setting its layout params to 0x0 - * @param view The view to hide. + * Hides a view by setting its layout width and height to 0dp. + * Handles null layout params safely. + * + * @param view The view to hide. If null, does nothing. */ - public static void hideViewByLayoutParams(View view) { - if (view instanceof LinearLayout) { - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0); - view.setLayoutParams(layoutParams); - } else if (view instanceof FrameLayout) { - FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(0, 0); - view.setLayoutParams(layoutParams2); - } else if (view instanceof RelativeLayout) { - RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(0, 0); - view.setLayoutParams(layoutParams3); - } else if (view instanceof Toolbar) { - Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(0, 0); - view.setLayoutParams(layoutParams4); - } else if (view instanceof ViewGroup) { - ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(0, 0); - view.setLayoutParams(layoutParams5); + public static void hideViewByLayoutParams(@Nullable View view) { + if (view == null) return; + + ViewGroup.LayoutParams params = view.getLayoutParams(); + + if (params == null) { + // Create generic 0x0 layout params accepted by all ViewGroups. + params = new ViewGroup.LayoutParams(0, 0); } else { - ViewGroup.LayoutParams params = view.getLayoutParams(); params.width = 0; params.height = 0; - view.setLayoutParams(params); } + + view.setLayoutParams(params); } /** @@ -790,13 +790,10 @@ public class Utils { public static void setDialogWindowParameters(Window window, int gravity, int yOffsetDip, int widthPercentage, boolean dimAmount) { WindowManager.LayoutParams params = window.getAttributes(); - DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics(); - int portraitWidth = Math.min(displayMetrics.widthPixels, displayMetrics.heightPixels); - - params.width = (int) (portraitWidth * (widthPercentage / 100.0f)); // Set width based on parameters. + params.width = Dim.pctPortraitWidth(widthPercentage); params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.gravity = gravity; - params.y = yOffsetDip > 0 ? dipToPixels(yOffsetDip) : 0; + params.y = yOffsetDip > 0 ? Dim.dp(yOffsetDip) : 0; if (dimAmount) { params.dimAmount = 0f; } @@ -805,18 +802,6 @@ public class Utils { window.setBackgroundDrawable(null); // Remove default dialog background } - /** - * Creates an array of corner radii for a rounded rectangle shape. - * - * @param dp Radius in density-independent pixels (dip) to apply to all corners. - * @return An array of eight float values representing the corner radii - * (top-left, top-right, bottom-right, bottom-left). - */ - public static float[] createCornerRadii(float dp) { - final float radius = dipToPixels(dp); - return new float[]{radius, radius, radius, radius, radius, radius, radius, radius}; - } - /** * Sets the theme light color used by the app. */ @@ -976,30 +961,60 @@ public class Utils { } } - private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+"); - /** - * Strips all punctuation and converts to lower case. A null parameter returns an empty string. + * Removes punctuation and converts text to lowercase. Returns an empty string if input is null. */ public static String removePunctuationToLowercase(@Nullable CharSequence original) { if (original == null) return ""; - return punctuationPattern.matcher(original).replaceAll("") + return PUNCTUATION_PATTERN.matcher(original).replaceAll("") .toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale()); } /** - * Sort a PreferenceGroup and all it's sub groups by title or key. + * Normalizes text for search: applies NFD, removes diacritics, and lowercases (locale-neutral). + * Returns an empty string if input is null. + */ + public static String normalizeTextToLowercase(@Nullable CharSequence original) { + if (original == null) return ""; + return DIACRITICS_PATTERN.matcher(Normalizer.normalize(original, Normalizer.Form.NFD)) + .replaceAll("").toLowerCase(Locale.ROOT); + } + + /** + * Returns a cached Collator for the current locale, or creates a new one if locale changed. + */ + private static Collator getCollator() { + Locale currentLocale = BaseSettings.REVANCED_LANGUAGE.get().getLocale(); + + if (cachedCollator == null || !currentLocale.equals(cachedCollatorLocale)) { + cachedCollatorLocale = currentLocale; + cachedCollator = Collator.getInstance(currentLocale); + cachedCollator.setStrength(Collator.SECONDARY); // Case-insensitive, diacritic-insensitive. + } + + return cachedCollator; + } + + /** + * Sorts a {@link PreferenceGroup} and all nested subgroups by title or key. *

- * Sort order is determined by the preferences key {@link Sort} suffix. + * The sort order is controlled by the {@link Sort} suffix present in the preference key. + * Preferences without a key or without a {@link Sort} suffix remain in their original order. *

- * If a preference has no key or no {@link Sort} suffix, - * then the preferences are left unsorted. + * Sorting is performed using {@link Collator} with the current user locale, + * ensuring correct alphabetical ordering for all supported languages + * (e.g., Ukrainian "і", German "ß", French accented characters, etc.). + * + * @param group the {@link PreferenceGroup} to sort */ @SuppressWarnings("deprecation") public static void sortPreferenceGroups(PreferenceGroup group) { Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED); List> preferences = new ArrayList<>(); + // Get cached Collator for locale-aware string comparison. + Collator collator = getCollator(); + for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { Preference preference = group.getPreference(i); @@ -1030,10 +1045,11 @@ public class Utils { preferences.add(new Pair<>(sortValue, preference)); } - //noinspection ComparatorCombinators + // Sort the list using locale-specific collation rules. Collections.sort(preferences, (pair1, pair2) - -> pair1.first.compareTo(pair2.first)); + -> collator.compare(pair1.first, pair2.first)); + // Reassign order values to reflect the new sorted sequence int index = 0; for (Pair pair : preferences) { int order = index++; @@ -1090,42 +1106,6 @@ public class Utils { return getResourceColor(colorString); } - /** - * Converts dip value to actual device pixels. - * - * @param dip The density-independent pixels value. - * @return The device pixel value. - */ - public static int dipToPixels(float dip) { - return (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - dip, - Resources.getSystem().getDisplayMetrics() - ); - } - - /** - * Converts a percentage of the screen height to actual device pixels. - * - * @param percentage The percentage of the screen height (e.g., 30 for 30%). - * @return The device pixel value corresponding to the percentage of screen height. - */ - public static int percentageHeightToPixels(int percentage) { - DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - return (int) (metrics.heightPixels * (percentage / 100.0f)); - } - - /** - * Converts a percentage of the screen width to actual device pixels. - * - * @param percentage The percentage of the screen width (e.g., 30 for 30%). - * @return The device pixel value corresponding to the percentage of screen width. - */ - public static int percentageWidthToPixels(int percentage) { - DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - return (int) (metrics.widthPixels * (percentage / 100.0f)); - } - /** * Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active. */ diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CheckWatchHistoryDomainNameResolutionPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CheckWatchHistoryDomainNameResolutionPatch.java index 23321c0e5..d12eabc0b 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CheckWatchHistoryDomainNameResolutionPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CheckWatchHistoryDomainNameResolutionPatch.java @@ -61,7 +61,11 @@ public class CheckWatchHistoryDomainNameResolutionPatch { // Prevent this false positive by verify youtube.com resolves. // If youtube.com does not resolve, then it's not a watch history domain resolving error // because the entire app will not work since no domains are resolving. - if (!domainResolvesToValidIP("youtube.com") + String domainYouTube = "youtube.com"; + if (!domainResolvesToValidIP(domainYouTube) + || domainResolvesToValidIP(HISTORY_TRACKING_ENDPOINT) + // Check multiple times, so a false positive from a flaky connection is almost impossible. + || !domainResolvesToValidIP(domainYouTube) || domainResolvesToValidIP(HISTORY_TRACKING_ENDPOINT)) { return; } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java index ca4a8ff61..9908c0be7 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java @@ -5,6 +5,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Color; +import android.view.View; import java.util.ArrayList; import java.util.List; @@ -71,6 +72,17 @@ public class CustomBrandingPatch { } } + /** + * Injection point. + */ + public static View getLottieViewOrNull(View lottieStartupView) { + if (BaseSettings.CUSTOM_BRANDING_ICON.get() == BrandingTheme.ORIGINAL) { + return lottieStartupView; + } + + return null; + } + /** * Injection point. */ diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/EnableDebuggingPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/EnableDebuggingPatch.java index 424ba7b12..e0551ed04 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/EnableDebuggingPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/EnableDebuggingPatch.java @@ -1,5 +1,9 @@ package app.revanced.extension.shared.patches; +import static java.lang.Boolean.TRUE; + +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -21,12 +25,28 @@ public final class EnableDebuggingPatch { ? new ConcurrentHashMap<>(800, 0.5f, 1) : null; + private static final Set DISABLED_FEATURE_FLAGS = parseFlags(BaseSettings.DISABLED_FEATURE_FLAGS.get()); + + // Log all disabled flags on app startup. + static { + if (LOG_FEATURE_FLAGS && !DISABLED_FEATURE_FLAGS.isEmpty()) { + StringBuilder sb = new StringBuilder("Disabled feature flags:\n"); + for (Long flag : DISABLED_FEATURE_FLAGS) { + sb.append(" ").append(flag).append('\n'); + } + Logger.printDebug(sb::toString); + } + } + /** * Injection point. */ public static boolean isBooleanFeatureFlagEnabled(boolean value, Long flag) { if (LOG_FEATURE_FLAGS && value) { - if (featureFlags.putIfAbsent(flag, true) == null) { + if (DISABLED_FEATURE_FLAGS.contains(flag)) { + return false; + } + if (featureFlags.putIfAbsent(flag, TRUE) == null) { Logger.printDebug(() -> "boolean feature is enabled: " + flag); } } @@ -70,10 +90,44 @@ public final class EnableDebuggingPatch { if (LOG_FEATURE_FLAGS && !defaultValue.equals(value)) { if (featureFlags.putIfAbsent(flag, true) == null) { Logger.printDebug(() -> " string feature is enabled: " + flag - + " value: " + value + (defaultValue.isEmpty() ? "" : " default: " + defaultValue)); + + " value: " + value + (defaultValue.isEmpty() ? "" : " default: " + defaultValue)); } } return value; } + + /** + * Get all logged feature flags. + * @return Set of all known flags + */ + public static Set getAllLoggedFlags() { + if (featureFlags != null) { + return new HashSet<>(featureFlags.keySet()); + } + + return new HashSet<>(); + } + + /** + * Public method for parsing flags. + * @param flags String containing newline-separated flag IDs + * @return Set of parsed flag IDs + */ + public static Set parseFlags(String flags) { + Set parsedFlags = new HashSet<>(); + if (!flags.isBlank()) { + for (String flag : flags.split("\n")) { + String trimmedFlag = flag.trim(); + if (trimmedFlag.isEmpty()) continue; // Skip empty lines. + try { + parsedFlags.add(Long.parseLong(trimmedFlag)); + } catch (NumberFormatException e) { + Logger.printException(() -> "Invalid flag ID: " + flag); + } + } + } + + return parsedFlags; + } } 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 index 062949db7..fb068d8ed 100644 --- 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 @@ -7,7 +7,6 @@ import android.app.Activity; import android.content.Context; 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; @@ -16,6 +15,7 @@ import android.widget.Toolbar; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment; +import app.revanced.extension.shared.ui.Dim; /** * Base class for hooking activities to inject a custom PreferenceFragment with a toolbar. @@ -109,13 +109,12 @@ public abstract class BaseActivityHook extends Activity { toolbar.setNavigationOnClickListener(getNavigationClickListener(activity)); toolbar.setTitle(STRING_REVANCED_SETTINGS_TITLE); - final int margin = Utils.dipToPixels(16); - toolbar.setTitleMarginStart(margin); - toolbar.setTitleMarginEnd(margin); + toolbar.setTitleMarginStart(Dim.dp16); + toolbar.setTitleMarginEnd(Dim.dp16); TextView toolbarTextView = Utils.getChildView(toolbar, false, view -> view instanceof TextView); if (toolbarTextView != null) { toolbarTextView.setTextColor(Utils.getAppForegroundColor()); - toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); + toolbarTextView.setTextSize(20); } setToolbarLayoutParams(toolbar); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java index c1bc849c1..e9dc280a8 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -42,4 +42,6 @@ public class BaseSettings { public static final EnumSetting CUSTOM_BRANDING_ICON = new EnumSetting<>("revanced_custom_branding_icon", BrandingTheme.ORIGINAL, true); public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", 1, true); + + public static final StringSetting DISABLED_FEATURE_FLAGS = new StringSetting("revanced_disabled_feature_flags", "", true, parent(DEBUG)); } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java index 1f24a7489..38b2cee47 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -392,10 +392,13 @@ public abstract class Setting { /** * Get the parent Settings that this setting depends on. - * @return List of parent Settings (e.g., BooleanSetting or EnumSetting), or empty list if no dependencies exist. + * @return List of parent Settings, or empty list if no dependencies exist. + * Defensive: handles null availability or missing getParentSettings() override. */ public List> getParentSettings() { - return availability == null ? Collections.emptyList() : availability.getParentSettings(); + return availability == null + ? Collections.emptyList() + : Objects.requireNonNullElse(availability.getParentSettings(), Collections.emptyList()); } /** diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java index 63cf5eb0f..e3d6abee2 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java @@ -1,7 +1,6 @@ package app.revanced.extension.shared.settings.preference; import static app.revanced.extension.shared.StringRef.str; -import static app.revanced.extension.shared.Utils.dipToPixels; import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow; import android.app.Dialog; @@ -37,6 +36,7 @@ import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.StringSetting; import app.revanced.extension.shared.ui.ColorDot; import app.revanced.extension.shared.ui.CustomDialog; +import app.revanced.extension.shared.ui.Dim; /** * A custom preference for selecting a color via a hexadecimal code or a color picker dialog. @@ -310,11 +310,8 @@ public class ColorPickerPreference extends EditTextPreference { inputLayout.setGravity(Gravity.CENTER_VERTICAL); dialogColorDot = new View(context); - LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams( - dipToPixels(20), - dipToPixels(20) - ); - previewParams.setMargins(dipToPixels(16), 0, dipToPixels(10), 0); + LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(Dim.dp20,Dim.dp20); + previewParams.setMargins(Dim.dp16, 0, Dim.dp10, 0); dialogColorDot.setLayoutParams(previewParams); inputLayout.addView(dialogColorDot); updateDialogColorDot(); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java index 810ddb3a6..b8c957711 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java @@ -1,6 +1,5 @@ package app.revanced.extension.shared.settings.preference; -import static app.revanced.extension.shared.Utils.dipToPixels; import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString; import android.annotation.SuppressLint; @@ -21,6 +20,7 @@ import androidx.annotation.ColorInt; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.ui.Dim; /** * A custom color picker view that allows the user to select a color using a hue slider, a saturation-value selector @@ -54,28 +54,28 @@ public class ColorPickerView extends View { } /** Expanded touch area for the hue and opacity bars to increase the touch-sensitive area. */ - public static final float TOUCH_EXPANSION = dipToPixels(20f); + public static final float TOUCH_EXPANSION = Dim.dp20; /** Margin between different areas of the view (saturation-value selector, hue bar, and opacity slider). */ - private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24); + private static final float MARGIN_BETWEEN_AREAS = Dim.dp24; /** Padding around the view. */ - private static final float VIEW_PADDING = dipToPixels(16); + private static final float VIEW_PADDING = Dim.dp16; /** Height of the hue bar. */ - private static final float HUE_BAR_HEIGHT = dipToPixels(12); + private static final float HUE_BAR_HEIGHT = Dim.dp12; /** Height of the opacity slider. */ - private static final float OPACITY_BAR_HEIGHT = dipToPixels(12); + private static final float OPACITY_BAR_HEIGHT = Dim.dp12; /** Corner radius for the hue bar. */ - private static final float HUE_CORNER_RADIUS = dipToPixels(6); + private static final float HUE_CORNER_RADIUS = Dim.dp6; /** Corner radius for the opacity slider. */ - private static final float OPACITY_CORNER_RADIUS = dipToPixels(6); + private static final float OPACITY_CORNER_RADIUS = Dim.dp6; /** Radius of the selector handles. */ - private static final float SELECTOR_RADIUS = dipToPixels(12); + private static final float SELECTOR_RADIUS = Dim.dp12; /** Stroke width for the selector handle outlines. */ private static final float SELECTOR_STROKE_WIDTH = 8; @@ -202,7 +202,7 @@ public class ColorPickerView extends View { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8 - final int minWidth = dipToPixels(250); + final int minWidth = Dim.dp(250); final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) + (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/FeatureFlagsManagerPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/FeatureFlagsManagerPreference.java new file mode 100644 index 000000000..1ada6584a --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/FeatureFlagsManagerPreference.java @@ -0,0 +1,623 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.preference.Preference; +import android.text.Editable; +import android.text.InputType; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.Pair; +import android.util.SparseBooleanArray; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.Space; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.patches.EnableDebuggingPatch; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.ui.CustomDialog; +import app.revanced.extension.shared.ui.Dim; + +/** + * A custom preference that opens a dialog for managing feature flags. + * Allows moving boolean flags between active and blocked states with advanced selection. + */ +@SuppressWarnings({"deprecation", "unused"}) +public class FeatureFlagsManagerPreference extends Preference { + + private static final int DRAWABLE_REVANCED_SETTINGS_SELECT_ALL = + getResourceIdentifierOrThrow("revanced_settings_select_all", "drawable"); + private static final int DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL = + getResourceIdentifierOrThrow("revanced_settings_deselect_all", "drawable"); + private static final int DRAWABLE_REVANCED_SETTINGS_COPY_ALL = + getResourceIdentifierOrThrow("revanced_settings_copy_all", "drawable"); + private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE = + getResourceIdentifierOrThrow("revanced_settings_arrow_right_one", "drawable"); + private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE = + getResourceIdentifierOrThrow("revanced_settings_arrow_right_double", "drawable"); + private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE = + getResourceIdentifierOrThrow("revanced_settings_arrow_left_one", "drawable"); + private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE = + getResourceIdentifierOrThrow("revanced_settings_arrow_left_double", "drawable"); + + /** + * Flags to hide from the UI. + */ + private static final Set FLAGS_TO_IGNORE = Set.of( + 45386834L // 'You' tab settings icon. + ); + + /** + * Tracks state for range selection in ListView. + */ + private static class ListViewSelectionState { + int lastClickedPosition = -1; // Position of the last clicked item. + boolean isRangeSelecting = false; // True while a range is being selected. + } + + /** + * Helper class to pass ListView and Adapter together. + */ + private record ColumnViews(ListView listView, FlagAdapter adapter) {} + + { + setOnPreferenceClickListener(pref -> { + showFlagsManagerDialog(); + return true; + }); + } + + public FeatureFlagsManagerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public FeatureFlagsManagerPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public FeatureFlagsManagerPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FeatureFlagsManagerPreference(Context context) { + super(context); + } + + /** + * Shows the main dialog for managing feature flags. + */ + private void showFlagsManagerDialog() { + if (!BaseSettings.DEBUG.get()) { + Utils.showToastShort(str("revanced_debug_logs_disabled")); + return; + } + + Context context = getContext(); + + // Load all known and disabled flags. + TreeSet allKnownFlags = new TreeSet<>(EnableDebuggingPatch.getAllLoggedFlags()); + allKnownFlags.removeAll(FLAGS_TO_IGNORE); + + TreeSet disabledFlags = new TreeSet<>(EnableDebuggingPatch.parseFlags( + BaseSettings.DISABLED_FEATURE_FLAGS.get())); + disabledFlags.removeAll(FLAGS_TO_IGNORE); + + if (allKnownFlags.isEmpty() && disabledFlags.isEmpty()) { + // String does not need to be localized because it's basically impossible + // to reach the settings menu without encountering at least 1 flag. + Utils.showToastShort("No feature flags logged yet"); + return; + } + + TreeSet availableFlags = new TreeSet<>(allKnownFlags); + availableFlags.removeAll(disabledFlags); + TreeSet blockedFlags = new TreeSet<>(disabledFlags); + + Pair dialogPair = CustomDialog.create( + context, + getTitle() != null ? getTitle().toString() : "", + null, + null, + str("revanced_settings_save"), + () -> saveFlags(blockedFlags), + () -> {}, + str("revanced_settings_reset"), + this::resetFlags, + true + ); + + LinearLayout mainLayout = dialogPair.second; + LinearLayout.LayoutParams contentParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, 0, 1.0f); + + // Insert content before the dialog button row. + View contentView = createContentView(context, availableFlags, blockedFlags); + mainLayout.addView(contentView, mainLayout.getChildCount() - 1, contentParams); + + Dialog dialog = dialogPair.first; + dialog.show(); + + Window window = dialog.getWindow(); + if (window != null) { + Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 100, false); + } + } + + /** + * Creates the main content view with two columns. + */ + private View createContentView(Context context, TreeSet availableFlags, TreeSet blockedFlags) { + LinearLayout contentLayout = new LinearLayout(context); + contentLayout.setOrientation(LinearLayout.VERTICAL); + + // Headers. + TextView availableHeader = createHeader(context, "revanced_debug_feature_flags_manager_active_header"); + TextView blockedHeader = createHeader(context, "revanced_debug_feature_flags_manager_blocked_header"); + + LinearLayout headersLayout = new LinearLayout(context); + headersLayout.setOrientation(LinearLayout.HORIZONTAL); + headersLayout.addView(availableHeader, new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + headersLayout.addView(blockedHeader, new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + + // Columns. + View leftColumn = createColumn(context, availableFlags, availableHeader); + View rightColumn = createColumn(context, blockedFlags, blockedHeader); + + ColumnViews leftViews = (ColumnViews) leftColumn.getTag(); + ColumnViews rightViews = (ColumnViews) rightColumn.getTag(); + + updateHeaderCount(availableHeader, leftViews.adapter); + updateHeaderCount(blockedHeader, rightViews.adapter); + + // Main columns layout. + LinearLayout columnsLayout = new LinearLayout(context); + columnsLayout.setOrientation(LinearLayout.HORIZONTAL); + columnsLayout.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)); + columnsLayout.addView(leftColumn, new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)); + + Space spaceBetweenColumns = new Space(context); + spaceBetweenColumns.setLayoutParams(new LinearLayout.LayoutParams(Dim.dp8, ViewGroup.LayoutParams.MATCH_PARENT)); + columnsLayout.addView(spaceBetweenColumns); + + columnsLayout.addView(rightColumn, new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)); + + // Move buttons below columns. + Pair moveButtons = createMoveButtons(context, + leftViews.listView, rightViews.listView, + availableFlags, blockedFlags, availableHeader, blockedHeader); + + // Layout for buttons row. + LinearLayout buttonsRow = new LinearLayout(context); + buttonsRow.setOrientation(LinearLayout.HORIZONTAL); + buttonsRow.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + buttonsRow.addView(moveButtons.first, new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + + Space spaceBetweenButtons = new Space(context); + spaceBetweenButtons.setLayoutParams(new LinearLayout.LayoutParams(Dim.dp8, ViewGroup.LayoutParams.WRAP_CONTENT)); + buttonsRow.addView(spaceBetweenButtons); + + buttonsRow.addView(moveButtons.second, new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + + contentLayout.addView(headersLayout); + contentLayout.addView(columnsLayout); + contentLayout.addView(buttonsRow); + + return contentLayout; + } + + /** + * Creates a header TextView. + */ + private TextView createHeader(Context context, String tag) { + TextView textview = new TextView(context); + textview.setTag(tag); + textview.setTextSize(16); + textview.setTextColor(Utils.getAppForegroundColor()); + textview.setGravity(Gravity.CENTER); + + return textview; + } + + /** + * Creates a single column (search + buttons + list). + */ + private View createColumn(Context context, TreeSet flags, TextView countText) { + LinearLayout wrapper = new LinearLayout(context); + wrapper.setOrientation(LinearLayout.VERTICAL); + + Pair pair = createListView(context, flags, countText); + ListView listView = pair.first; + FlagAdapter adapter = pair.second; + + EditText search = createSearchBox(context, adapter, listView, countText); + LinearLayout buttons = createActionButtons(context, listView, adapter); + + listView.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)); + ShapeDrawable background = new ShapeDrawable(new RoundRectShape( + Dim.roundedCorners(10), null, null)); + background.getPaint().setColor(Utils.getEditTextBackground()); + listView.setPadding(0, Dim.dp4, 0, Dim.dp4); + listView.setBackground(background); + listView.setOverScrollMode(View.OVER_SCROLL_NEVER); + + wrapper.addView(search); + wrapper.addView(buttons); + wrapper.addView(listView); + + // Save references for move buttons. + wrapper.setTag(new ColumnViews(listView, adapter)); + + return wrapper; + } + + /** + * Updates the header text with the current count. + */ + private void updateHeaderCount(TextView header, FlagAdapter adapter) { + header.setText(str((String) header.getTag(), adapter.getCount())); + } + + /** + * Creates a search box that filters the list. + */ + @SuppressLint("ClickableViewAccessibility") + private EditText createSearchBox(Context context, FlagAdapter adapter, ListView listView, TextView countText) { + EditText search = new EditText(context); + search.setInputType(InputType.TYPE_CLASS_NUMBER); + search.setTextSize(16); + search.setHint(str("revanced_debug_feature_flags_manager_search_hint")); + search.setHapticFeedbackEnabled(false); + search.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + + search.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + adapter.setSearchQuery(s.toString()); + listView.clearChoices(); + updateHeaderCount(countText, adapter); + Drawable clearIcon = context.getResources().getDrawable(android.R.drawable.ic_menu_close_clear_cancel); + clearIcon.setBounds(0, 0, Dim.dp20, Dim.dp20); + search.setCompoundDrawables(null, null, TextUtils.isEmpty(s) ? null : clearIcon, null); + } + @Override public void afterTextChanged(Editable s) {} + }); + + search.setOnTouchListener((v, event) -> { + if (event.getAction() == MotionEvent.ACTION_UP) { + Drawable[] compoundDrawables = search.getCompoundDrawables(); + if (compoundDrawables[2] != null && + event.getRawX() >= (search.getRight() - compoundDrawables[2].getBounds().width())) { + search.setText(""); + return true; + } + } + return false; + }); + + return search; + } + + /** + * Creates action buttons. + */ + private LinearLayout createActionButtons(Context context, ListView listView, FlagAdapter adapter) { + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER); + row.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + + ImageButton selectAll = createButton(context, DRAWABLE_REVANCED_SETTINGS_SELECT_ALL, + () -> { + for (int i = 0, count = adapter.getCount(); i < count; i++) { + listView.setItemChecked(i, true); + } + }); + + ImageButton clearAll = createButton(context, DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL, + () -> { + listView.clearChoices(); + adapter.notifyDataSetChanged(); + }); + + ImageButton copy = createButton(context, DRAWABLE_REVANCED_SETTINGS_COPY_ALL, + () -> { + List items = new ArrayList<>(); + SparseBooleanArray checked = listView.getCheckedItemPositions(); + + if (checked.size() > 0) { + for (int i = 0, count = adapter.getCount(); i < count; i++) { + if (checked.get(i)) { + items.add(adapter.getItem(i)); + } + } + } else { + for (Long flag : adapter.getFullFlags()) { + items.add(String.valueOf(flag)); + } + } + + Utils.setClipboard(TextUtils.join("\n", items)); + + Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_copied")); + }); + + row.addView(selectAll); + row.addView(clearAll); + row.addView(copy); + + return row; + } + + /** + * Creates the move buttons (left and right groups). + */ + private Pair createMoveButtons(Context context, + ListView availableListView, ListView blockedListView, + TreeSet availableFlags, TreeSet blockedFlags, + TextView availableCountText, TextView blockedCountText) { + // Left group: >> > + LinearLayout leftButtons = new LinearLayout(context); + leftButtons.setOrientation(LinearLayout.HORIZONTAL); + leftButtons.setGravity(Gravity.CENTER); + + ImageButton moveAllRight = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE, + () -> moveFlags(availableListView, blockedListView, availableFlags, blockedFlags, + availableCountText, blockedCountText, true)); + + ImageButton moveOneRight = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE, + () -> moveFlags(availableListView, blockedListView, availableFlags, blockedFlags, + availableCountText, blockedCountText, false)); + + leftButtons.addView(moveAllRight); + leftButtons.addView(moveOneRight); + + // Right group: < << + LinearLayout rightButtons = new LinearLayout(context); + rightButtons.setOrientation(LinearLayout.HORIZONTAL); + rightButtons.setGravity(Gravity.CENTER); + + ImageButton moveOneLeft = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE, + () -> moveFlags(blockedListView, availableListView, blockedFlags, availableFlags, + blockedCountText, availableCountText, false)); + + ImageButton moveAllLeft = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE, + () -> moveFlags(blockedListView, availableListView, blockedFlags, availableFlags, + blockedCountText, availableCountText, true)); + + rightButtons.addView(moveOneLeft); + rightButtons.addView(moveAllLeft); + + return new Pair<>(leftButtons, rightButtons); + } + + /** + * Creates a styled ImageButton. + */ + @SuppressLint("ResourceType") + private ImageButton createButton(Context context, int drawableResId, Runnable action) { + ImageButton button = new ImageButton(context); + + button.setImageResource(drawableResId); + button.setScaleType(ImageView.ScaleType.CENTER); + int[] attrs = {android.R.attr.selectableItemBackgroundBorderless}; + //noinspection Recycle + TypedArray ripple = context.obtainStyledAttributes(attrs); + button.setBackgroundDrawable(ripple.getDrawable(0)); + ripple.close(); + + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(Dim.dp32, Dim.dp32); + params.setMargins(Dim.dp8, Dim.dp8, Dim.dp8, Dim.dp8); + button.setLayoutParams(params); + + button.setOnClickListener(v -> action.run()); + + return button; + } + + /** + * Custom adapter with search filtering. + */ + private static class FlagAdapter extends ArrayAdapter { + private final TreeSet fullFlags; + private String searchQuery = ""; + + public FlagAdapter(Context context, TreeSet fullFlags) { + super(context, android.R.layout.simple_list_item_multiple_choice, new ArrayList<>()); + this.fullFlags = fullFlags; + updateFiltered(); + } + + public void setSearchQuery(String query) { + searchQuery = query == null ? "" : query.trim(); + updateFiltered(); + } + + private void updateFiltered() { + clear(); + for (Long flag : fullFlags) { + String flagString = String.valueOf(flag); + if (searchQuery.isEmpty() || flagString.contains(searchQuery)) { + add(flagString); + } + } + notifyDataSetChanged(); + } + + public void refresh() { + updateFiltered(); + } + + public List getFullFlags() { + return new ArrayList<>(fullFlags); + } + } + + /** + * Creates a ListView with filtering, multi-select, and range selection. + */ + @SuppressLint("ClickableViewAccessibility") + private Pair createListView(Context context, + TreeSet flags, TextView countText) { + ListView listView = new ListView(context); + listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + listView.setDividerHeight(0); + + FlagAdapter adapter = new FlagAdapter(context, flags); + listView.setAdapter(adapter); + + final ListViewSelectionState state = new ListViewSelectionState(); + + listView.setOnItemClickListener((parent, view, position, id) -> { + if (!state.isRangeSelecting) { + state.lastClickedPosition = position; + } else { + state.isRangeSelecting = false; + } + }); + + listView.setOnItemLongClickListener((parent, view, position, id) -> { + if (state.lastClickedPosition == -1) { + listView.setItemChecked(position, true); + state.lastClickedPosition = position; + } else { + int start = Math.min(state.lastClickedPosition, position); + int end = Math.max(state.lastClickedPosition, position); + for (int i = start; i <= end; i++) { + listView.setItemChecked(i, true); + } + state.isRangeSelecting = true; + } + return true; + }); + + listView.setOnTouchListener((view, event) -> { + if (event.getAction() == MotionEvent.ACTION_UP && state.isRangeSelecting) { + state.isRangeSelecting = false; + } + return false; + }); + + return new Pair<>(listView, adapter); + } + + /** + * Moves selected or all flags from one list to another. + * + * @param fromListView Source ListView. + * @param toListView Destination ListView. + * @param fromFlags Source flag set. + * @param toFlags Destination flag set. + * @param fromCountText Header showing count of source items. + * @param toCountText Header showing count of destination items. + * @param moveAll If true, move all items; if false, move only selected. + */ + private void moveFlags(ListView fromListView, ListView toListView, + TreeSet fromFlags, TreeSet toFlags, + TextView fromCountText, TextView toCountText, + boolean moveAll) { + if (fromListView == null || toListView == null) return; + + List flagsToMove = new ArrayList<>(); + FlagAdapter fromAdapter = (FlagAdapter) fromListView.getAdapter(); + + if (moveAll) { + flagsToMove.addAll(fromFlags); + } else { + SparseBooleanArray checked = fromListView.getCheckedItemPositions(); + for (int i = 0, count = fromAdapter.getCount(); i < count; i++) { + if (checked.get(i)) { + String item = fromAdapter.getItem(i); + if (item != null) { + flagsToMove.add(Long.parseLong(item)); + } + } + } + } + + if (flagsToMove.isEmpty()) return; + + for (Long flag : flagsToMove) { + fromFlags.remove(flag); + toFlags.add(flag); + } + + // Clear selections before refreshing. + fromListView.clearChoices(); + toListView.clearChoices(); + + // Refresh both adapters. + fromAdapter.refresh(); + ((FlagAdapter) toListView.getAdapter()).refresh(); + + // Update headers. + updateHeaderCount(fromCountText, fromAdapter); + updateHeaderCount(toCountText, (FlagAdapter) toListView.getAdapter()); + } + + /** + * Saves blocked flags to settings. + */ + private void saveFlags(TreeSet blockedFlags) { + StringBuilder flagsString = new StringBuilder(); + for (Long flag : blockedFlags) { + if (flagsString.length() > 0) { + flagsString.append("\n"); + } + flagsString.append(flag); + } + + BaseSettings.DISABLED_FEATURE_FLAGS.save(flagsString.toString()); + Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_saved")); + Logger.printDebug(() -> "Feature flags saved. Blocked: " + blockedFlags.size()); + + AbstractPreferenceFragment.showRestartDialog(getContext()); + } + + /** + * Resets all blocked flags. + */ + private void resetFlags() { + BaseSettings.DISABLED_FEATURE_FLAGS.save(""); + Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_reset")); + + AbstractPreferenceFragment.showRestartDialog(getContext()); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java index 1c8580241..1044ba424 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java @@ -11,7 +11,6 @@ import android.preference.Preference; import android.text.InputType; import android.util.AttributeSet; import android.util.Pair; -import android.util.TypedValue; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.LinearLayout; @@ -35,7 +34,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer editText.setAutofillHints((String) null); } editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); - editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 7); // Use a smaller font to reduce text wrap. + editText.setTextSize(14); setOnPreferenceClickListener(this); } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java index 4f4d3ef92..0d4003b91 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java @@ -1,7 +1,6 @@ package app.revanced.extension.shared.settings.preference; import static app.revanced.extension.shared.StringRef.str; -import static app.revanced.extension.shared.Utils.dipToPixels; import static app.revanced.extension.shared.requests.Route.Method.GET; import android.annotation.SuppressLint; @@ -41,6 +40,7 @@ import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.requests.Requester; import app.revanced.extension.shared.requests.Route; +import app.revanced.extension.shared.ui.Dim; /** * Opens a dialog showing official links. @@ -222,11 +222,10 @@ class WebViewDialog extends Dialog { LinearLayout mainLayout = new LinearLayout(getContext()); mainLayout.setOrientation(LinearLayout.VERTICAL); - final int padding = dipToPixels(10); - mainLayout.setPadding(padding, padding, padding, padding); + mainLayout.setPadding(Dim.dp10, Dim.dp10, Dim.dp10, Dim.dp10); // Set rounded rectangle background. ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape( - Utils.createCornerRadii(28), null, null)); + Dim.roundedCorners(28), null, null)); mainBackground.getPaint().setColor(Utils.getDialogBackgroundColor()); mainLayout.setBackground(mainBackground); 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 index e299613dc..5c595a97a 100644 --- 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 @@ -8,7 +8,6 @@ import android.os.Build; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; -import android.util.TypedValue; import android.view.ViewGroup; import android.view.Window; import android.view.WindowInsets; @@ -20,6 +19,7 @@ import androidx.annotation.Nullable; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.settings.BaseActivityHook; +import app.revanced.extension.shared.ui.Dim; @SuppressWarnings({"deprecation", "NewApi"}) public class ToolbarPreferenceFragment extends AbstractPreferenceFragment { @@ -88,14 +88,13 @@ public class ToolbarPreferenceFragment extends AbstractPreferenceFragment { toolbar.setNavigationIcon(getBackButtonDrawable()); toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss()); - final int margin = Utils.dipToPixels(16); - toolbar.setTitleMargin(margin, 0, margin, 0); + toolbar.setTitleMargin(Dim.dp16, 0, Dim.dp16, 0); TextView toolbarTextView = Utils.getChildView(toolbar, true, TextView.class::isInstance); if (toolbarTextView != null) { toolbarTextView.setTextColor(Utils.getAppForegroundColor()); - toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); + toolbarTextView.setTextSize(20); } // Allow package-specific toolbar customization. diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java index 9b5c9464c..ab1e2ee6c 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java @@ -75,7 +75,7 @@ public abstract class BaseSearchResultItem { // Shared method for highlighting text with search query. protected static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) { - if (TextUtils.isEmpty(text)) return text; + if (TextUtils.isEmpty(text) || queryPattern == null) return text; final int adjustedColor = Utils.adjustColorBrightness( Utils.getAppBackgroundColor(), 0.95f, 1.20f); @@ -84,7 +84,10 @@ public abstract class BaseSearchResultItem { Matcher matcher = queryPattern.matcher(text); while (matcher.find()) { - spannable.setSpan(highlightSpan, matcher.start(), matcher.end(), + int start = matcher.start(); + int end = matcher.end(); + if (start == end) continue; // Skip zero matches. + spannable.setSpan(highlightSpan, start, end, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); } @@ -224,10 +227,14 @@ public abstract class BaseSearchResultItem { return searchBuilder.toString(); } + /** + * Appends normalized searchable text to the builder. + * Uses full Unicode normalization for accurate search across all languages. + */ private void appendText(StringBuilder builder, CharSequence text) { if (!TextUtils.isEmpty(text)) { if (builder.length() > 0) builder.append(" "); - builder.append(Utils.removePunctuationToLowercase(text)); + builder.append(Utils.normalizeTextToLowercase(text)); } } @@ -272,7 +279,7 @@ public abstract class BaseSearchResultItem { */ @Override boolean matchesQuery(String query) { - return searchableText.contains(Utils.removePunctuationToLowercase(query)); + return searchableText.contains(Utils.normalizeTextToLowercase(query)); } /** diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java index f1893a232..cb5e792e8 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java @@ -484,7 +484,7 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter= Build.VERSION_CODES.Q) { @@ -149,7 +149,7 @@ public abstract class BaseSearchViewController { // Create cursor drawable. GradientDrawable cursorDrawable = new GradientDrawable(); cursorDrawable.setShape(GradientDrawable.RECTANGLE); - cursorDrawable.setSize(Utils.dipToPixels(2), -1); // Width: 2dp, Height: match text height. + cursorDrawable.setSize(Dim.dp2, -1); // Width: 2dp, Height: match text height. cursorDrawable.setColor(cursorColor); // Set cursor drawable. @@ -164,7 +164,7 @@ public abstract class BaseSearchViewController { overlayContainer = new FrameLayout(activity); overlayContainer.setVisibility(View.GONE); overlayContainer.setBackgroundColor(Utils.getAppBackgroundColor()); - overlayContainer.setElevation(Utils.dipToPixels(8)); + overlayContainer.setElevation(Dim.dp8); // Container for search results. FrameLayout searchResultsContainer = new FrameLayout(activity); @@ -450,7 +450,7 @@ public abstract class BaseSearchViewController { filteredSearchItems.clear(); - String queryLower = Utils.removePunctuationToLowercase(query); + String queryLower = Utils.normalizeTextToLowercase(query); Pattern queryPattern = Pattern.compile(Pattern.quote(queryLower), Pattern.CASE_INSENSITIVE); // Clear highlighting only for items that were previously visible. @@ -669,7 +669,7 @@ public abstract class BaseSearchViewController { protected static GradientDrawable createBackgroundDrawable() { GradientDrawable background = new GradientDrawable(); background.setShape(GradientDrawable.RECTANGLE); - background.setCornerRadius(Utils.dipToPixels(28)); + background.setCornerRadius(Dim.dp28); background.setColor(getSearchViewBackground()); return background; } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/ColorDot.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/ColorDot.java index f0eeef408..07e9b4113 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/ColorDot.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/ColorDot.java @@ -1,7 +1,6 @@ package app.revanced.extension.shared.ui; import static app.revanced.extension.shared.Utils.adjustColorBrightness; -import static app.revanced.extension.shared.Utils.dipToPixels; import static app.revanced.extension.shared.Utils.getAppBackgroundColor; import static app.revanced.extension.shared.Utils.isDarkModeEnabled; import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.DISABLED_ALPHA; @@ -13,7 +12,7 @@ import android.view.View; import androidx.annotation.ColorInt; public class ColorDot { - private static final int STROKE_WIDTH = dipToPixels(1.5f); // Stroke width in dp. + private static final int STROKE_WIDTH = Dim.dp(1.5f); /** * Creates a circular drawable with a main fill and a stroke. @@ -55,7 +54,7 @@ public class ColorDot { targetView.setAlpha(enabled ? 1.0f : DISABLED_ALPHA); if (!isDarkModeEnabled()) { targetView.setClipToOutline(true); - targetView.setElevation(dipToPixels(2)); + targetView.setElevation(Dim.dp2); } } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/CustomDialog.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/CustomDialog.java index 1b65bea32..15d80c916 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/CustomDialog.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/CustomDialog.java @@ -1,7 +1,5 @@ package app.revanced.extension.shared.ui; -import static app.revanced.extension.shared.Utils.dipToPixels; - import android.app.Dialog; import android.content.Context; import android.graphics.Color; @@ -37,7 +35,6 @@ public class CustomDialog { private final Context context; private final Dialog dialog; private final LinearLayout mainLayout; - private final int dip4, dip8, dip16, dip24, dip36; /** * Creates a custom dialog with a styled layout, including a title, message, buttons, and an optional EditText. @@ -93,13 +90,6 @@ public class CustomDialog { this.dialog = new Dialog(context); this.dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar. - // Preset size constants. - dip4 = dipToPixels(4); - dip8 = dipToPixels(8); - dip16 = dipToPixels(16); - dip24 = dipToPixels(24); - dip36 = dipToPixels(36); - // Create main layout. mainLayout = createMainLayout(); addTitle(title); @@ -122,11 +112,11 @@ public class CustomDialog { private LinearLayout createMainLayout() { LinearLayout layout = new LinearLayout(context); layout.setOrientation(LinearLayout.VERTICAL); - layout.setPadding(dip24, dip16, dip24, dip24); + layout.setPadding(Dim.dp24, Dim.dp16, Dim.dp24, Dim.dp24); // Set rounded rectangle background. ShapeDrawable background = new ShapeDrawable(new RoundRectShape( - Utils.createCornerRadii(28), null, null)); + Dim.roundedCorners(28), null, null)); // Dialog background. background.getPaint().setColor(Utils.getDialogBackgroundColor()); layout.setBackground(background); @@ -152,7 +142,7 @@ public class CustomDialog { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.setMargins(0, 0, 0, dip16); + params.setMargins(0, 0, 0, Dim.dp16); titleView.setLayoutParams(params); mainLayout.addView(titleView); @@ -180,9 +170,9 @@ public class CustomDialog { // EditText (if provided). if (editText != null) { ShapeDrawable background = new ShapeDrawable(new RoundRectShape( - Utils.createCornerRadii(10), null, null)); + Dim.roundedCorners(10), null, null)); background.getPaint().setColor(Utils.getEditTextBackground()); - scrollView.setPadding(dip8, dip8, dip8, dip8); + scrollView.setPadding(Dim.dp8, Dim.dp8, Dim.dp8, Dim.dp8); scrollView.setBackground(background); scrollView.setClipToOutline(true); @@ -241,7 +231,7 @@ public class CustomDialog { LinearLayout.LayoutParams buttonContainerParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - buttonContainerParams.setMargins(0, dip16, 0, 0); + buttonContainerParams.setMargins(0, Dim.dp16, 0, 0); buttonContainer.setLayoutParams(buttonContainerParams); List