availableClients = List.of(
- ANDROID_REEL,
- ANDROID_VR_1_43_32,
- VISIONOS,
- ANDROID_VR_1_61_48
- );
-
- app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse(
- availableClients, SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get());
- }
-}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/theme/ThemePatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/theme/ThemePatch.java
deleted file mode 100644
index 3f4e396699..0000000000
--- a/extensions/music/src/main/java/app/revanced/extension/music/patches/theme/ThemePatch.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package app.revanced.extension.music.patches.theme;
-
-import app.revanced.extension.shared.theme.BaseThemePatch;
-
-@SuppressWarnings("unused")
-public class ThemePatch extends BaseThemePatch {
-
- // Color constants used in relation with litho components.
- private static final int[] DARK_VALUES = {
- 0xFF212121, // Comments box background.
- 0xFF030303, // Button container background in album.
- 0xFF000000, // Button container background in playlist.
- };
-
- /**
- * Injection point.
- *
- * Change the color of Litho components.
- * If the color of the component matches one of the values, return the background color.
- *
- * @param originalValue The original color value.
- * @return The new or original color value.
- */
- public static int getValue(int originalValue) {
- return processColorValue(originalValue, DARK_VALUES, null);
- }
-}
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
deleted file mode 100644
index c3874f655c..0000000000
--- a/extensions/music/src/main/java/app/revanced/extension/music/settings/MusicActivityHook.java
+++ /dev/null
@@ -1,148 +0,0 @@
-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.VersionCheckUtils;
-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.ResourceType;
-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;
-
- /**
- * How much time has passed since the first launch of the app. Simple check to prevent
- * forcing bold icons on first launch where the settings menu is partially broken
- * due to missing icon resources the client has not yet received.
- *
- * @see app.revanced.extension.youtube.settings.YouTubeActivityHook#MINIMUM_TIME_AFTER_FIRST_LAUNCH_BEFORE_ALLOWING_BOLD_ICONS
- */
- private static final long MINIMUM_TIME_AFTER_FIRST_LAUNCH_BEFORE_ALLOWING_BOLD_ICONS = 30 * 1000; // 30 seconds.
-
- static {
- final boolean useBoldIcons = VersionCheckUtils.IS_8_40_OR_GREATER
- && !Settings.SETTINGS_DISABLE_BOLD_ICONS.get()
- && (System.currentTimeMillis() - Settings.FIRST_TIME_APP_LAUNCHED.get())
- > MINIMUM_TIME_AFTER_FIRST_LAUNCH_BEFORE_ALLOWING_BOLD_ICONS;
-
- Utils.setAppIsUsingBoldIcons(useBoldIcons);
- }
-
- /**
- * 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(
- ResourceType.STYLE, "Theme.ReVanced.YouTubeMusic.Settings"));
- }
-
- /**
- * 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);
- }
-
- /**
- * Injection point.
- *
- * Decides whether to use bold icons.
- */
- @SuppressWarnings("unused")
- public static boolean useBoldIcons(boolean original) {
- return Utils.appIsUsingBoldIcons();
- }
-}
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
deleted file mode 100644
index 7decd29b8a..0000000000
--- a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java
+++ /dev/null
@@ -1,41 +0,0 @@
-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.YouTubeAndMusicSettings;
-import app.revanced.extension.shared.settings.BooleanSetting;
-import app.revanced.extension.shared.settings.EnumSetting;
-import app.revanced.extension.shared.spoof.ClientType;
-
-public class Settings extends YouTubeAndMusicSettings {
-
- // Ads
- public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true);
- public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true);
-
- // General
- 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);
- public static final BooleanSetting HIDE_NAVIGATION_BAR_LIBRARY_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_library_button", FALSE, true);
- public static final BooleanSetting HIDE_NAVIGATION_BAR_UPGRADE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_upgrade_button", TRUE, true);
- public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_music_hide_navigation_bar", FALSE, true);
- 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
- public static final EnumSetting SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
- ClientType.ANDROID_REEL, true, parent(SPOOF_VIDEO_STREAMS));
-
- public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", TRUE, true);
-}
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
deleted file mode 100644
index 86e5173420..0000000000
--- a/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/MusicPreferenceFragment.java
+++ /dev/null
@@ -1,93 +0,0 @@
-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.GmsCoreSupport;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.settings.preference.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);
-
- // Clunky work around until preferences are custom classes that manage themselves.
- // Custom branding only works with non-root install. But the preferences must be
- // added during patched because of difficulties detecting during patching if it's
- // a root install. So instead the non-functional preferences are removed during
- // runtime if the app is mount (root) installation.
- if (GmsCoreSupport.isPackageNameOriginal()) {
- removePreferences(
- BaseSettings.CUSTOM_BRANDING_ICON.key,
- BaseSettings.CUSTOM_BRANDING_NAME.key);
- }
- } 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/search/MusicSearchResultsAdapter.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchResultsAdapter.java
deleted file mode 100644
index 65ccd4ea1a..0000000000
--- a/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchResultsAdapter.java
+++ /dev/null
@@ -1,28 +0,0 @@
-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
deleted file mode 100644
index 6681a2f027..0000000000
--- a/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchViewController.java
+++ /dev/null
@@ -1,71 +0,0 @@
-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/nothingx/build.gradle.kts b/extensions/nothingx/build.gradle.kts
deleted file mode 100644
index ed2b78c5f6..0000000000
--- a/extensions/nothingx/build.gradle.kts
+++ /dev/null
@@ -1,10 +0,0 @@
-dependencies {
- compileOnly(project(":extensions:shared:library"))
- compileOnly(project(":extensions:nothingx:stub"))
-}
-
-android {
- defaultConfig {
- minSdk = 23
- }
-}
\ No newline at end of file
diff --git a/extensions/nothingx/src/main/AndroidManifest.xml b/extensions/nothingx/src/main/AndroidManifest.xml
deleted file mode 100644
index 15e7c2ae67..0000000000
--- a/extensions/nothingx/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extensions/nothingx/src/main/java/app/revanced/extension/nothingx/patches/ShowK1TokensPatch.java b/extensions/nothingx/src/main/java/app/revanced/extension/nothingx/patches/ShowK1TokensPatch.java
deleted file mode 100644
index c301ae2fb3..0000000000
--- a/extensions/nothingx/src/main/java/app/revanced/extension/nothingx/patches/ShowK1TokensPatch.java
+++ /dev/null
@@ -1,590 +0,0 @@
-package app.revanced.extension.nothingx.patches;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Application;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Color;
-import android.graphics.Typeface;
-import android.os.Build;
-import android.os.Bundle;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.ScrollView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Patches to expose the K1 token for Nothing X app to enable pairing with GadgetBridge.
- */
-@SuppressWarnings("unused")
-public class ShowK1TokensPatch {
-
- private static final String TAG = "ReVanced";
- private static final String PACKAGE_NAME = "com.nothing.smartcenter";
- private static final String EMPTY_MD5 = "d41d8cd98f00b204e9800998ecf8427e";
- private static final String PREFS_NAME = "revanced_nothingx_prefs";
- private static final String KEY_DONT_SHOW_DIALOG = "dont_show_k1_dialog";
-
- // Colors
- private static final int COLOR_BG = 0xFF1E1E1E;
- private static final int COLOR_CARD = 0xFF2D2D2D;
- private static final int COLOR_TEXT_PRIMARY = 0xFFFFFFFF;
- private static final int COLOR_TEXT_SECONDARY = 0xFFB0B0B0;
- private static final int COLOR_ACCENT = 0xFFFF9500;
- private static final int COLOR_TOKEN_BG = 0xFF3A3A3A;
- private static final int COLOR_BUTTON_POSITIVE = 0xFFFF9500;
- private static final int COLOR_BUTTON_NEGATIVE = 0xFFFF6B6B;
-
- // Match standalone K1: k1:, K1:, k1>, etc.
- private static final Pattern K1_STANDALONE_PATTERN = Pattern.compile("(?i)(?:k1\\s*[:>]\\s*)([0-9a-f]{32})");
- // Match combined r3+k1: format (64 chars = r3(32) + k1(32))
- private static final Pattern K1_COMBINED_PATTERN = Pattern.compile("(?i)r3\\+k1\\s*:\\s*([0-9a-f]{64})");
-
- private static volatile boolean k1Logged = false;
- private static volatile boolean lifecycleCallbacksRegistered = false;
- private static Context appContext;
-
- /**
- * Get K1 tokens from database and log files.
- * Call this after the app initializes.
- *
- * @param context Application context
- */
- public static void showK1Tokens(Context context) {
- if (k1Logged) {
- return;
- }
-
- appContext = context.getApplicationContext();
-
- Set allTokens = new LinkedHashSet<>();
-
- // First try to get from database.
- String dbToken = getK1TokensFromDatabase();
- if (dbToken != null) {
- allTokens.add(dbToken);
- }
-
- // Then get from log files.
- Set logTokens = getK1TokensFromLogFiles();
- allTokens.addAll(logTokens);
-
- if (allTokens.isEmpty()) {
- return;
- }
-
- // Log all found tokens.
- int index = 1;
- for (String token : allTokens) {
- Log.i(TAG, "#" + index++ + ": " + token.toUpperCase());
- }
-
- // Register lifecycle callbacks to show dialog when an Activity is ready.
- registerLifecycleCallbacks(allTokens);
-
- k1Logged = true;
- }
-
- /**
- * Register ActivityLifecycleCallbacks to show dialog when first Activity resumes.
- *
- * @param tokens Set of K1 tokens to display
- */
- private static void registerLifecycleCallbacks(Set tokens) {
- if (lifecycleCallbacksRegistered || !(appContext instanceof Application)) {
- return;
- }
-
- Application application = (Application) appContext;
- application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
- @Override
- public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
- }
-
- @Override
- public void onActivityStarted(Activity activity) {
- }
-
- @Override
- public void onActivityResumed(Activity activity) {
- // Check if user chose not to show dialog.
- SharedPreferences prefs = activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
- if (prefs.getBoolean(KEY_DONT_SHOW_DIALOG, false)) {
- application.unregisterActivityLifecycleCallbacks(this);
- lifecycleCallbacksRegistered = false;
- return;
- }
-
- // Show dialog on first Activity resume.
- if (tokens != null && !tokens.isEmpty()) {
- activity.runOnUiThread(() -> showK1TokensDialog(activity, tokens));
- // Unregister after showing
- application.unregisterActivityLifecycleCallbacks(this);
- lifecycleCallbacksRegistered = false;
- }
- }
-
- @Override
- public void onActivityPaused(Activity activity) {
- }
-
- @Override
- public void onActivityStopped(Activity activity) {
- }
-
- @Override
- public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
- }
-
- @Override
- public void onActivityDestroyed(Activity activity) {
- }
- });
-
- lifecycleCallbacksRegistered = true;
- }
-
- /**
- * Show dialog with K1 tokens.
- *
- * @param activity Activity context
- * @param tokens Set of K1 tokens
- */
- private static void showK1TokensDialog(Activity activity, Set tokens) {
- try {
- // Create main container.
- LinearLayout mainLayout = new LinearLayout(activity);
- mainLayout.setOrientation(LinearLayout.VERTICAL);
- mainLayout.setBackgroundColor(COLOR_BG);
- mainLayout.setPadding(dpToPx(activity, 24), dpToPx(activity, 16),
- dpToPx(activity, 24), dpToPx(activity, 16));
-
- // Title.
- TextView titleView = new TextView(activity);
- titleView.setText("K1 Token(s) Found");
- titleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
- titleView.setTypeface(Typeface.DEFAULT_BOLD);
- titleView.setTextColor(COLOR_TEXT_PRIMARY);
- titleView.setGravity(Gravity.CENTER);
- mainLayout.addView(titleView, new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- ));
-
- // Subtitle.
- TextView subtitleView = new TextView(activity);
- subtitleView.setText(tokens.size() == 1 ? "1 token found • Tap to copy" : tokens.size() + " tokens found • Tap to copy");
- subtitleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
- subtitleView.setTextColor(COLOR_TEXT_SECONDARY);
- subtitleView.setGravity(Gravity.CENTER);
- LinearLayout.LayoutParams subtitleParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- );
- subtitleParams.topMargin = dpToPx(activity, 4);
- subtitleParams.bottomMargin = dpToPx(activity, 16);
- mainLayout.addView(subtitleView, subtitleParams);
-
- // Scrollable content.
- ScrollView scrollView = new ScrollView(activity);
- scrollView.setVerticalScrollBarEnabled(false);
- LinearLayout.LayoutParams scrollParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- 0,
- 1.0f
- );
- scrollParams.topMargin = dpToPx(activity, 8);
- scrollParams.bottomMargin = dpToPx(activity, 16);
- mainLayout.addView(scrollView, scrollParams);
-
- LinearLayout tokensContainer = new LinearLayout(activity);
- tokensContainer.setOrientation(LinearLayout.VERTICAL);
- scrollView.addView(tokensContainer);
-
- // Add each token as a card.
- boolean singleToken = tokens.size() == 1;
- int index = 1;
- for (String token : tokens) {
- LinearLayout tokenCard = createTokenCard(activity, token, index++, singleToken);
- LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- );
- cardParams.bottomMargin = dpToPx(activity, 12);
- tokensContainer.addView(tokenCard, cardParams);
- }
-
- // Info text.
- TextView infoView = new TextView(activity);
- infoView.setText(tokens.size() == 1 ? "Tap the token to copy it" : "Tap any token to copy it");
- infoView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
- infoView.setTextColor(COLOR_TEXT_SECONDARY);
- infoView.setGravity(Gravity.CENTER);
- infoView.setAlpha(0.7f);
- LinearLayout.LayoutParams infoParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- );
- infoParams.topMargin = dpToPx(activity, 8);
- mainLayout.addView(infoView, infoParams);
-
- // Button row.
- LinearLayout buttonRow = new LinearLayout(activity);
- buttonRow.setOrientation(LinearLayout.HORIZONTAL);
- buttonRow.setGravity(Gravity.END);
- LinearLayout.LayoutParams buttonRowParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- );
- buttonRowParams.topMargin = dpToPx(activity, 16);
- mainLayout.addView(buttonRow, buttonRowParams);
-
- // "Don't show again" button.
- Button dontShowButton = new Button(activity);
- dontShowButton.setText("Don't show again");
- dontShowButton.setTextColor(Color.WHITE);
- dontShowButton.setBackgroundColor(Color.TRANSPARENT);
- dontShowButton.setAllCaps(false);
- dontShowButton.setTypeface(Typeface.DEFAULT);
- dontShowButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
- dontShowButton.setPadding(dpToPx(activity, 16), dpToPx(activity, 8),
- dpToPx(activity, 16), dpToPx(activity, 8));
- LinearLayout.LayoutParams dontShowParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- );
- dontShowParams.rightMargin = dpToPx(activity, 8);
- buttonRow.addView(dontShowButton, dontShowParams);
-
- // "OK" button.
- Button okButton = new Button(activity);
- okButton.setText("OK");
- okButton.setTextColor(Color.BLACK);
- okButton.setBackgroundColor(COLOR_BUTTON_POSITIVE);
- okButton.setAllCaps(false);
- okButton.setTypeface(Typeface.DEFAULT_BOLD);
- okButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
- okButton.setPadding(dpToPx(activity, 24), dpToPx(activity, 12),
- dpToPx(activity, 24), dpToPx(activity, 12));
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- okButton.setElevation(dpToPx(activity, 4));
- }
- buttonRow.addView(okButton, new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- ));
-
- // Build dialog.
- AlertDialog.Builder builder = new AlertDialog.Builder(activity);
- builder.setView(mainLayout);
-
- final AlertDialog dialog = builder.create();
-
- // Style the dialog with dark background.
- if (dialog.getWindow() != null) {
- dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
- }
-
- dialog.show();
-
- // Set button click listeners after dialog is created.
- dontShowButton.setOnClickListener(v -> {
- SharedPreferences prefs = activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
- prefs.edit().putBoolean(KEY_DONT_SHOW_DIALOG, true).apply();
- Toast.makeText(activity, "Dialog disabled. Clear app data to re-enable.",
- Toast.LENGTH_SHORT).show();
- dialog.dismiss();
- });
-
- okButton.setOnClickListener(v -> {
- dialog.dismiss();
- });
-
- } catch (Exception e) {
- Log.e(TAG, "Failed to show K1 dialog", e);
- }
- }
-
- /**
- * Create a card view for a single token.
- */
- private static LinearLayout createTokenCard(Activity activity, String token, int index, boolean singleToken) {
- LinearLayout card = new LinearLayout(activity);
- card.setOrientation(LinearLayout.VERTICAL);
- card.setBackgroundColor(COLOR_TOKEN_BG);
- card.setPadding(dpToPx(activity, 16), dpToPx(activity, 12),
- dpToPx(activity, 16), dpToPx(activity, 12));
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- card.setElevation(dpToPx(activity, 2));
- }
- card.setClickable(true);
- card.setFocusable(true);
-
- // Token label (only show if multiple tokens).
- if (!singleToken) {
- TextView labelView = new TextView(activity);
- labelView.setText("Token #" + index);
- labelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
- labelView.setTextColor(COLOR_ACCENT);
- labelView.setTypeface(Typeface.DEFAULT_BOLD);
- card.addView(labelView);
- }
-
- // Token value.
- TextView tokenView = new TextView(activity);
- tokenView.setText(token.toUpperCase());
- tokenView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
- tokenView.setTextColor(COLOR_TEXT_PRIMARY);
- tokenView.setTypeface(Typeface.MONOSPACE);
- tokenView.setLetterSpacing(0.05f);
- LinearLayout.LayoutParams tokenParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- );
- if (!singleToken) {
- tokenParams.topMargin = dpToPx(activity, 8);
- }
- card.addView(tokenView, tokenParams);
-
- // Click to copy.
- card.setOnClickListener(v -> {
- ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
- if (clipboard != null) {
- clipboard.setText(token.toUpperCase());
- Toast.makeText(activity, "Token copied!", Toast.LENGTH_SHORT).show();
- }
- });
-
- return card;
- }
-
- /**
- * Convert dp to pixels.
- */
- private static int dpToPx(Context context, float dp) {
- return (int) TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP,
- dp,
- context.getResources().getDisplayMetrics()
- );
- }
-
- /**
- * Get K1 tokens from log files.
- * Prioritizes pairing K1 tokens over reconnect tokens.
- */
- private static Set getK1TokensFromLogFiles() {
- Set pairingTokens = new LinkedHashSet<>();
- Set reconnectTokens = new LinkedHashSet<>();
- try {
- File logDir = new File("/data/data/" + PACKAGE_NAME + "/files/log");
- if (!logDir.exists() || !logDir.isDirectory()) {
- return pairingTokens;
- }
-
- File[] logFiles = logDir.listFiles((dir, name) ->
- name.endsWith(".log") || name.endsWith(".log.") || name.matches(".*\\.log\\.\\d+"));
-
- if (logFiles == null || logFiles.length == 0) {
- return pairingTokens;
- }
-
- for (File logFile : logFiles) {
- try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) {
- String line;
- while ((line = reader.readLine()) != null) {
- // Determine if this is a pairing or reconnect context.
- boolean isPairingContext = line.toLowerCase().contains("watchbind");
- boolean isReconnectContext = line.toLowerCase().contains("watchreconnect");
-
- String k1Token = null;
-
- // First check for combined r3+k1 format (priority).
- Matcher combinedMatcher = K1_COMBINED_PATTERN.matcher(line);
- if (combinedMatcher.find()) {
- String combined = combinedMatcher.group(1);
- if (combined.length() == 64) {
- // Second half is the actual K1
- k1Token = combined.substring(32).toLowerCase();
- }
- }
-
- // Then check for standalone K1 format (only if not found in combined).
- if (k1Token == null) {
- Matcher standaloneMatcher = K1_STANDALONE_PATTERN.matcher(line);
- if (standaloneMatcher.find()) {
- String token = standaloneMatcher.group(1);
- if (token != null && token.length() == 32) {
- k1Token = token.toLowerCase();
- }
- }
- }
-
- // Add to appropriate set.
- if (k1Token != null) {
- if (isPairingContext && !isReconnectContext) {
- pairingTokens.add(k1Token);
- } else {
- reconnectTokens.add(k1Token);
- }
- }
- }
- } catch (Exception e) {
- // Skip unreadable files.
- }
- }
- } catch (Exception ex) {
- // Fail silently.
- }
-
- // Return pairing tokens first, add reconnect tokens if no pairing tokens found.
- if (!pairingTokens.isEmpty()) {
- Log.i(TAG, "Found " + pairingTokens.size() + " pairing K1 token(s)");
- return pairingTokens;
- }
-
- if (!reconnectTokens.isEmpty()) {
- Log.i(TAG, "Found " + reconnectTokens.size() + " reconnect K1 token(s) (may not work for initial pairing)");
- }
- return reconnectTokens;
- }
-
- /**
- * Try to get K1 tokens from the database.
- */
- private static String getK1TokensFromDatabase() {
- try {
- File dbDir = new File("/data/data/" + PACKAGE_NAME + "/databases");
- if (!dbDir.exists() || !dbDir.isDirectory()) {
- return null;
- }
-
- File[] dbFiles = dbDir.listFiles((dir, name) ->
- name.endsWith(".db") && !name.startsWith("google_app_measurement") && !name.contains("firebase"));
-
- if (dbFiles == null || dbFiles.length == 0) {
- return null;
- }
-
- for (File dbFile : dbFiles) {
- String token = getK1TokensFromDatabase(dbFile);
- if (token != null) {
- return token;
- }
- }
-
- return null;
- } catch (Exception ex) {
- return null;
- }
- }
-
- /**
- * Extract K1 tokens from a database file.
- */
- private static String getK1TokensFromDatabase(File dbFile) {
- SQLiteDatabase db = null;
- try {
- db = SQLiteDatabase.openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY);
-
- // Get all tables.
- Cursor cursor = db.rawQuery(
- "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
- null
- );
-
- List tables = new ArrayList<>();
- while (cursor.moveToNext()) {
- tables.add(cursor.getString(0));
- }
- cursor.close();
-
- // Scan all columns for 32-char hex strings.
- for (String table : tables) {
- Cursor schemaCursor = null;
- try {
- schemaCursor = db.rawQuery("PRAGMA table_info(" + table + ")", null);
- List columns = new ArrayList<>();
- while (schemaCursor.moveToNext()) {
- columns.add(schemaCursor.getString(1));
- }
- schemaCursor.close();
-
- for (String column : columns) {
- Cursor dataCursor = null;
- try {
- dataCursor = db.query(table, new String[]{column}, null, null, null, null, null);
- while (dataCursor.moveToNext()) {
- String value = dataCursor.getString(0);
- if (value != null && value.length() == 32 && value.matches("[0-9a-fA-F]{32}")) {
- // Skip obviously fake tokens (MD5 of empty string).
- if (!value.equalsIgnoreCase(EMPTY_MD5)) {
- dataCursor.close();
- db.close();
- return value.toLowerCase();
- }
- }
- }
- } catch (Exception e) {
- // Skip non-string columns.
- } finally {
- if (dataCursor != null) {
- dataCursor.close();
- }
- }
- }
- } catch (Exception e) {
- // Continue to next table.
- } finally {
- if (schemaCursor != null && !schemaCursor.isClosed()) {
- schemaCursor.close();
- }
- }
- }
-
- return null;
- } catch (Exception ex) {
- return null;
- } finally {
- if (db != null && db.isOpen()) {
- db.close();
- }
- }
- }
-
- /**
- * Reset the logged flag (useful for testing or re-pairing).
- */
- public static void resetK1Logged() {
- k1Logged = false;
- lifecycleCallbacksRegistered = false;
- }
-
- /**
- * Reset the "don't show again" preference.
- */
- public static void resetDontShowPreference() {
- if (appContext != null) {
- SharedPreferences prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
- prefs.edit().putBoolean(KEY_DONT_SHOW_DIALOG, false).apply();
- }
- }
-}
diff --git a/extensions/nothingx/stub/build.gradle.kts b/extensions/nothingx/stub/build.gradle.kts
deleted file mode 100644
index fcadc678c4..0000000000
--- a/extensions/nothingx/stub/build.gradle.kts
+++ /dev/null
@@ -1,17 +0,0 @@
-plugins {
- alias(libs.plugins.android.library)
-}
-
-android {
- namespace = "app.revanced.extension"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 26
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
-}
\ No newline at end of file
diff --git a/extensions/nothingx/stub/src/main/AndroidManifest.xml b/extensions/nothingx/stub/src/main/AndroidManifest.xml
deleted file mode 100644
index 15e7c2ae67..0000000000
--- a/extensions/nothingx/stub/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extensions/nunl/build.gradle.kts b/extensions/nunl/build.gradle.kts
deleted file mode 100644
index ab48531bba..0000000000
--- a/extensions/nunl/build.gradle.kts
+++ /dev/null
@@ -1,10 +0,0 @@
-dependencies {
- compileOnly(project(":extensions:shared:library"))
- compileOnly(project(":extensions:nunl:stub"))
-}
-
-android {
- defaultConfig {
- minSdk = 26
- }
-}
diff --git a/extensions/nunl/src/main/AndroidManifest.xml b/extensions/nunl/src/main/AndroidManifest.xml
deleted file mode 100644
index 9b65eb06cf..0000000000
--- a/extensions/nunl/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/extensions/nunl/src/main/java/app/revanced/extension/nunl/ads/HideAdsPatch.java b/extensions/nunl/src/main/java/app/revanced/extension/nunl/ads/HideAdsPatch.java
deleted file mode 100644
index 2e4ab5b069..0000000000
--- a/extensions/nunl/src/main/java/app/revanced/extension/nunl/ads/HideAdsPatch.java
+++ /dev/null
@@ -1,114 +0,0 @@
-package app.revanced.extension.nunl.ads;
-
-import nl.nu.performance.api.client.interfaces.Block;
-import nl.nu.performance.api.client.unions.SmallArticleLinkFlavor;
-import nl.nu.performance.api.client.objects.*;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import app.revanced.extension.shared.Logger;
-
-@SuppressWarnings("unused")
-public class HideAdsPatch {
- private static final String[] blockedHeaderBlocks = {
- "Aanbiedingen (Adverteerders)",
- "Aangeboden door NUshop"
- };
-
- // "Rubrieken" menu links to ads.
- private static final String[] blockedLinkBlocks = {
- "Van onze adverteerders"
- };
-
- public static void filterAds(List blocks) {
- try {
- ArrayList cleanedList = new ArrayList<>();
-
- boolean skipFullHeader = false;
- boolean skipUntilDivider = false;
-
- int index = 0;
- while (index < blocks.size()) {
- Block currentBlock = blocks.get(index);
-
- // Because of pagination, we might not see the Divider in front of it.
- // Just remove it as is and leave potential extra spacing visible on the screen.
- if (currentBlock instanceof DpgBannerBlock) {
- index++;
- continue;
- }
-
- if (index + 1 < blocks.size()) {
- // Filter Divider -> DpgMediaBanner -> Divider.
- if (currentBlock instanceof DividerBlock
- && blocks.get(index + 1) instanceof DpgBannerBlock) {
- index += 2;
- continue;
- }
-
- // Filter Divider -> LinkBlock (... -> LinkBlock -> LinkBlock-> LinkBlock -> Divider).
- if (currentBlock instanceof DividerBlock
- && blocks.get(index + 1) instanceof LinkBlock linkBlock) {
- Link link = linkBlock.getLink();
- if (link != null && link.getTitle() != null) {
- for (String blockedLinkBlock : blockedLinkBlocks) {
- if (blockedLinkBlock.equals(link.getTitle().getText())) {
- skipUntilDivider = true;
- break;
- }
- }
- if (skipUntilDivider) {
- index++;
- continue;
- }
- }
- }
- }
-
- // Skip LinkBlocks with a "flavor" claiming to be "isPartner" (sponsored inline ads).
- if (currentBlock instanceof LinkBlock linkBlock
- && linkBlock.getLink() != null
- && linkBlock.getLink().getLinkFlavor() instanceof SmallArticleLinkFlavor smallArticleLinkFlavor
- && smallArticleLinkFlavor.isPartner() != null
- && smallArticleLinkFlavor.isPartner()) {
- index++;
- continue;
- }
-
- if (currentBlock instanceof DividerBlock) {
- skipUntilDivider = false;
- }
-
- // Filter HeaderBlock with known ads until next HeaderBlock.
- if (currentBlock instanceof HeaderBlock headerBlock) {
- StyledText headerText = headerBlock.getTitle();
- if (headerText != null) {
- skipFullHeader = false;
- for (String blockedHeaderBlock : blockedHeaderBlocks) {
- if (blockedHeaderBlock.equals(headerText.getText())) {
- skipFullHeader = true;
- break;
- }
- }
- if (skipFullHeader) {
- index++;
- continue;
- }
- }
- }
-
- if (!skipFullHeader && !skipUntilDivider) {
- cleanedList.add(currentBlock);
- }
- index++;
- }
-
- // Replace list in-place to not deal with moving the result to the correct register in smali.
- blocks.clear();
- blocks.addAll(cleanedList);
- } catch (Exception ex) {
- Logger.printException(() -> "filterAds failure", ex);
- }
- }
-}
diff --git a/extensions/nunl/stub/build.gradle.kts b/extensions/nunl/stub/build.gradle.kts
deleted file mode 100644
index 7905271b26..0000000000
--- a/extensions/nunl/stub/build.gradle.kts
+++ /dev/null
@@ -1,17 +0,0 @@
-plugins {
- alias(libs.plugins.android.library)
-}
-
-android {
- namespace = "app.revanced.extension"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 26
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
-}
diff --git a/extensions/nunl/stub/src/main/AndroidManifest.xml b/extensions/nunl/stub/src/main/AndroidManifest.xml
deleted file mode 100644
index 9b65eb06cf..0000000000
--- a/extensions/nunl/stub/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/interfaces/Block.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/interfaces/Block.java
deleted file mode 100644
index 3514f360cb..0000000000
--- a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/interfaces/Block.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package nl.nu.performance.api.client.interfaces;
-
-public class Block {
-
-}
diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DividerBlock.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DividerBlock.java
deleted file mode 100644
index 0351aec049..0000000000
--- a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DividerBlock.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package nl.nu.performance.api.client.objects;
-
-import nl.nu.performance.api.client.interfaces.Block;
-
-public class DividerBlock extends Block {
-
-}
diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DpgBannerBlock.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DpgBannerBlock.java
deleted file mode 100644
index ac300b0539..0000000000
--- a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DpgBannerBlock.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package nl.nu.performance.api.client.objects;
-
-import nl.nu.performance.api.client.interfaces.Block;
-
-public class DpgBannerBlock extends Block {
-
-}
diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/HeaderBlock.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/HeaderBlock.java
deleted file mode 100644
index 7b1f7ad192..0000000000
--- a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/HeaderBlock.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package nl.nu.performance.api.client.objects;
-
-import nl.nu.performance.api.client.interfaces.Block;
-
-public class HeaderBlock extends Block {
- public final StyledText getTitle() {
- throw new UnsupportedOperationException("Stub");
- }
-}
diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/Link.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/Link.java
deleted file mode 100644
index 771d11dad1..0000000000
--- a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/Link.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package nl.nu.performance.api.client.objects;
-
-import nl.nu.performance.api.client.unions.LinkFlavor;
-
-public class Link {
- public final StyledText getTitle() {
- throw new UnsupportedOperationException("Stub");
- }
-
- public final LinkFlavor getLinkFlavor() {
- throw new UnsupportedOperationException("Stub");
- }
-}
diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/LinkBlock.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/LinkBlock.java
deleted file mode 100644
index dea1950573..0000000000
--- a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/LinkBlock.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package nl.nu.performance.api.client.objects;
-
-import android.os.Parcelable;
-import nl.nu.performance.api.client.interfaces.Block;
-
-public abstract class LinkBlock extends Block implements Parcelable {
- public final Link getLink() {
- throw new UnsupportedOperationException("Stub");
- }
-}
diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/StyledText.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/StyledText.java
deleted file mode 100644
index 719403eb4e..0000000000
--- a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/StyledText.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package nl.nu.performance.api.client.objects;
-
-public class StyledText {
- public final String getText() {
- throw new UnsupportedOperationException("Stub");
- }
-}
diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/LinkFlavor.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/LinkFlavor.java
deleted file mode 100644
index 08413d3fd9..0000000000
--- a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/LinkFlavor.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package nl.nu.performance.api.client.unions;
-
-public interface LinkFlavor {
-}
diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/SmallArticleLinkFlavor.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/SmallArticleLinkFlavor.java
deleted file mode 100644
index 4dcbf23cb9..0000000000
--- a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/SmallArticleLinkFlavor.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package nl.nu.performance.api.client.unions;
-
-public class SmallArticleLinkFlavor implements LinkFlavor {
- public final Boolean isPartner() {
- throw new UnsupportedOperationException("Stub");
- }
-}
diff --git a/extensions/primevideo/build.gradle.kts b/extensions/primevideo/build.gradle.kts
deleted file mode 100644
index 17a3c31a21..0000000000
--- a/extensions/primevideo/build.gradle.kts
+++ /dev/null
@@ -1,10 +0,0 @@
-dependencies {
- compileOnly(project(":extensions:shared:library"))
- compileOnly(project(":extensions:primevideo:stub"))
-}
-
-android {
- defaultConfig {
- minSdk = 21
- }
-}
diff --git a/extensions/primevideo/src/main/AndroidManifest.xml b/extensions/primevideo/src/main/AndroidManifest.xml
deleted file mode 100644
index 9b65eb06cf..0000000000
--- a/extensions/primevideo/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java b/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java
deleted file mode 100644
index d0a97810a2..0000000000
--- a/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package app.revanced.extension.primevideo.ads;
-
-import com.amazon.avod.fsm.SimpleTrigger;
-import com.amazon.avod.media.ads.AdBreak;
-import com.amazon.avod.media.ads.internal.state.AdBreakTrigger;
-import com.amazon.avod.media.ads.internal.state.AdEnabledPlayerTriggerType;
-import com.amazon.avod.media.playback.VideoPlayer;
-import com.amazon.avod.media.ads.internal.state.ServerInsertedAdBreakState;
-
-import app.revanced.extension.shared.Logger;
-
-@SuppressWarnings("unused")
-public final class SkipAdsPatch {
- public static void enterServerInsertedAdBreakState(ServerInsertedAdBreakState state, AdBreakTrigger trigger, VideoPlayer player) {
- try {
- AdBreak adBreak = trigger.getBreak();
-
- // There are two scenarios when entering the original method:
- // 1. Player naturally entered an ad break while watching a video.
- // 2. User is skipped/scrubbed to a position on the timeline. If seek position is past an ad break,
- // user is forced to watch an ad before continuing.
- //
- // Scenario 2 is indicated by trigger.getSeekStartPosition() != null, so skip directly to the scrubbing
- // target. Otherwise, just calculate when the ad break should end and skip to there.
- if (trigger.getSeekStartPosition() != null)
- player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
- else
- player.seekTo(player.getCurrentPosition() + adBreak.getDurationExcludingAux().getTotalMilliseconds());
-
- // Send "end of ads" trigger to state machine so everything doesn't get whacky.
- state.doTrigger(new SimpleTrigger(AdEnabledPlayerTriggerType.NO_MORE_ADS_SKIP_TRANSITION));
- } catch (Exception ex) {
- Logger.printException(() -> "Failed skipping ads", ex);
- }
- }
-}
\ No newline at end of file
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
deleted file mode 100644
index b11ec0875d..0000000000
--- a/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/videoplayer/PlaybackSpeedPatch.java
+++ /dev/null
@@ -1,207 +0,0 @@
-package app.revanced.extension.primevideo.videoplayer;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.graphics.RectF;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.ColorFilter;
-import android.graphics.PixelFormat;
-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;
-
-public class PlaybackSpeedPatch {
- private static Player player;
- private static final float[] SPEED_VALUES = {0.5f, 0.7f, 0.8f, 0.9f, 0.95f, 1.0f, 1.05f, 1.1f, 1.2f, 1.3f, 1.5f, 2.0f};
- private static final String SPEED_BUTTON_TAG = "speed_overlay";
-
- public static void setPlayer(Player playerInstance) {
- player = playerInstance;
- if (player != null) {
- // Reset playback rate when switching between episodes to ensure correct display.
- player.setPlaybackRate(1.0f);
- }
- }
-
- public static void initializeSpeedOverlay(View userControlsView) {
- try {
- LinearLayout buttonContainer = Utils.getChildViewByResourceName(userControlsView, "ButtonContainerPlayerTop");
-
- // If the speed overlay exists we should return early.
- if (Utils.getChildView(buttonContainer, false, child ->
- child instanceof ImageView && SPEED_BUTTON_TAG.equals(child.getTag())) != null) {
- return;
- }
-
- ImageView speedButton = createSpeedButton(userControlsView.getContext());
- speedButton.setOnClickListener(v -> changePlaybackSpeed(speedButton));
- buttonContainer.addView(speedButton, 0);
-
- } catch (IllegalArgumentException e) {
- Logger.printException(() -> "initializeSpeedOverlay, no button container found", e);
- } catch (Exception e) {
- Logger.printException(() -> "initializeSpeedOverlay failure", e);
- }
- }
-
- private static ImageView createSpeedButton(Context context) {
- ImageView speedButton = new ImageView(context);
- speedButton.setContentDescription("Playback Speed");
- speedButton.setTag(SPEED_BUTTON_TAG);
- speedButton.setClickable(true);
- speedButton.setFocusable(true);
- speedButton.setScaleType(ImageView.ScaleType.CENTER);
-
- SpeedIconDrawable speedIcon = new SpeedIconDrawable();
- speedButton.setImageDrawable(speedIcon);
-
- speedButton.setMinimumWidth(Dim.dp48);
- speedButton.setMinimumHeight(Dim.dp48);
-
- return speedButton;
- }
-
- private static String[] getSpeedOptions() {
- String[] options = new String[SPEED_VALUES.length];
- for (int i = 0; i < SPEED_VALUES.length; i++) {
- options[i] = SPEED_VALUES[i] + "x";
- }
- return options;
- }
-
- private static void changePlaybackSpeed(ImageView imageView) {
- if (player == null) {
- Logger.printException(() -> "Player not available");
- return;
- }
-
- try {
- player.pause();
- AlertDialog dialog = createSpeedPlaybackDialog(imageView);
- dialog.setOnDismissListener(dialogInterface -> player.play());
- dialog.show();
-
- } catch (Exception e) {
- Logger.printException(() -> "changePlaybackSpeed", e);
- }
- }
-
- private static AlertDialog createSpeedPlaybackDialog(ImageView imageView) {
- Context context = imageView.getContext();
- int currentSelection = getCurrentSpeedSelection();
-
- return new AlertDialog.Builder(context)
- .setTitle("Select Playback Speed")
- .setSingleChoiceItems(getSpeedOptions(), currentSelection,
- PlaybackSpeedPatch::handleSpeedSelection)
- .create();
- }
-
- private static int getCurrentSpeedSelection() {
- try {
- float currentRate = player.getPlaybackRate();
- int index = Arrays.binarySearch(SPEED_VALUES, currentRate);
- return Math.max(index, 0); // Use slowest speed if not found.
- } catch (Exception e) {
- Logger.printException(() -> "getCurrentSpeedSelection error getting current playback speed", e);
- return 0;
- }
- }
-
- private static void handleSpeedSelection(android.content.DialogInterface dialog, int selectedIndex) {
- try {
- float selectedSpeed = SPEED_VALUES[selectedIndex];
- player.setPlaybackRate(selectedSpeed);
- player.play();
- } catch (Exception e) {
- Logger.printException(() -> "handleSpeedSelection error setting playback speed", e);
- } finally {
- dialog.dismiss();
- }
- }
-}
-
-class SpeedIconDrawable extends Drawable {
- private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
- @Override
- public void draw(Canvas canvas) {
- int w = getBounds().width();
- int h = getBounds().height();
- float centerX = w / 2f;
- // Position gauge in lower portion.
- float centerY = h * 0.7f;
- float radius = Math.min(w, h) / 2f * 0.8f;
-
- paint.setColor(Color.WHITE);
- paint.setStyle(Paint.Style.STROKE);
- paint.setStrokeWidth(radius * 0.1f);
-
- // Draw semicircle.
- RectF oval = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
- canvas.drawArc(oval, 180, 180, false, paint);
-
- // Draw three tick marks.
- paint.setStrokeWidth(radius * 0.06f);
- for (int i = 0; i < 3; i++) {
- float angle = 180 + (i * 45); // 180°, 225°, 270°.
- float angleRad = (float) Math.toRadians(angle);
-
- float startX = centerX + (radius * 0.8f) * (float) Math.cos(angleRad);
- float startY = centerY + (radius * 0.8f) * (float) Math.sin(angleRad);
- float endX = centerX + radius * (float) Math.cos(angleRad);
- float endY = centerY + radius * (float) Math.sin(angleRad);
-
- canvas.drawLine(startX, startY, endX, endY, paint);
- }
-
- // Draw needle.
- paint.setStrokeWidth(radius * 0.08f);
- float needleAngle = 200; // Slightly right of center.
- float needleAngleRad = (float) Math.toRadians(needleAngle);
-
- float needleEndX = centerX + (radius * 0.6f) * (float) Math.cos(needleAngleRad);
- float needleEndY = centerY + (radius * 0.6f) * (float) Math.sin(needleAngleRad);
-
- canvas.drawLine(centerX, centerY, needleEndX, needleEndY, paint);
-
- // Center dot.
- paint.setStyle(Paint.Style.FILL);
- canvas.drawCircle(centerX, centerY, radius * 0.06f, paint);
- }
-
- @Override
- public void setAlpha(int alpha) {
- paint.setAlpha(alpha);
- }
-
- @Override
- public void setColorFilter(ColorFilter colorFilter) {
- paint.setColorFilter(colorFilter);
- }
-
- @Override
- public int getOpacity() {
- return PixelFormat.TRANSLUCENT;
- }
-
- @Override
- public int getIntrinsicWidth() {
- return Dim.dp32;
- }
-
- @Override
- public int getIntrinsicHeight() {
- return Dim.dp32;
- }
-}
diff --git a/extensions/primevideo/stub/build.gradle.kts b/extensions/primevideo/stub/build.gradle.kts
deleted file mode 100644
index 7744c0eaac..0000000000
--- a/extensions/primevideo/stub/build.gradle.kts
+++ /dev/null
@@ -1,17 +0,0 @@
-plugins {
- alias(libs.plugins.android.library)
-}
-
-android {
- namespace = "app.revanced.extension"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 21
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
-}
diff --git a/extensions/primevideo/stub/src/main/AndroidManifest.xml b/extensions/primevideo/stub/src/main/AndroidManifest.xml
deleted file mode 100644
index 9b65eb06cf..0000000000
--- a/extensions/primevideo/stub/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java
deleted file mode 100644
index b537fe0402..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.amazon.avod.fsm;
-
-public final class SimpleTrigger implements Trigger {
- public SimpleTrigger(T triggerType) {
- }
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java
deleted file mode 100644
index 95741308c3..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.amazon.avod.fsm;
-
-public abstract class StateBase {
- // This method orginally has protected access (modified in patch code).
- public void doTrigger(Trigger trigger) {
- }
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java
deleted file mode 100644
index 282f0f2004..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.amazon.avod.fsm;
-
-public interface Trigger {
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java
deleted file mode 100644
index cc90e43cdc..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.amazon.avod.media;
-
-public final class TimeSpan {
- public long getTotalMilliseconds() {
- throw new UnsupportedOperationException();
- }
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java
deleted file mode 100644
index 9a950434dc..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.amazon.avod.media.ads;
-
-import com.amazon.avod.media.TimeSpan;
-
-public interface AdBreak {
- TimeSpan getDurationExcludingAux();
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java
deleted file mode 100644
index f417660ed7..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.amazon.avod.media.ads.internal.state;
-
-public abstract class AdBreakState extends AdEnabledPlaybackState {
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java
deleted file mode 100644
index f8b3995650..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.amazon.avod.media.ads.internal.state;
-
-import com.amazon.avod.media.ads.AdBreak;
-import com.amazon.avod.media.TimeSpan;
-
-public class AdBreakTrigger {
- public AdBreak getBreak() {
- throw new UnsupportedOperationException();
- }
-
- public TimeSpan getSeekTarget() {
- throw new UnsupportedOperationException();
- }
-
- public TimeSpan getSeekStartPosition() {
- throw new UnsupportedOperationException();
- }
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java
deleted file mode 100644
index 445aad580a..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.amazon.avod.media.ads.internal.state;
-
-import com.amazon.avod.fsm.StateBase;
-import com.amazon.avod.media.playback.state.PlayerStateType;
-import com.amazon.avod.media.playback.state.trigger.PlayerTriggerType;
-
-public class AdEnabledPlaybackState extends StateBase {
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java
deleted file mode 100644
index e7951e9342..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.amazon.avod.media.ads.internal.state;
-
-public enum AdEnabledPlayerTriggerType {
- NO_MORE_ADS_SKIP_TRANSITION
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java
deleted file mode 100644
index 07c198013f..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.amazon.avod.media.ads.internal.state;
-
-public class ServerInsertedAdBreakState extends AdBreakState {
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java
deleted file mode 100644
index 4f82e98727..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.amazon.avod.media.playback;
-
-public interface VideoPlayer {
- long getCurrentPosition();
-
- void seekTo(long positionMs);
-
- void pause();
-
- void play();
-
- boolean isPlaying();
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java
deleted file mode 100644
index 202723285e..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.amazon.avod.media.playback.state;
-
-public interface PlayerStateType {
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java
deleted file mode 100644
index eac139f9bf..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.amazon.avod.media.playback.state.trigger;
-
-public interface PlayerTriggerType {
-}
\ No newline at end of file
diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/video/sdk/player/Player.java b/extensions/primevideo/stub/src/main/java/com/amazon/video/sdk/player/Player.java
deleted file mode 100644
index bd609e1964..0000000000
--- a/extensions/primevideo/stub/src/main/java/com/amazon/video/sdk/player/Player.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.amazon.video.sdk.player;
-
-public interface Player {
- float getPlaybackRate();
-
- void setPlaybackRate(float rate);
-
- void play();
-
- void pause();
-}
\ No newline at end of file
diff --git a/extensions/proguard-rules.pro b/extensions/proguard-rules.pro
deleted file mode 100644
index 8f804140d6..0000000000
--- a/extensions/proguard-rules.pro
+++ /dev/null
@@ -1,9 +0,0 @@
--dontobfuscate
--dontoptimize
--keepattributes *
--keep class app.revanced.** {
- *;
-}
--keep class com.google.** {
- *;
-}
diff --git a/extensions/reddit/build.gradle.kts b/extensions/reddit/build.gradle.kts
deleted file mode 100644
index 75c8d7a179..0000000000
--- a/extensions/reddit/build.gradle.kts
+++ /dev/null
@@ -1,9 +0,0 @@
-dependencies {
- compileOnly(project(":extensions:reddit:stub"))
-}
-
-android {
- defaultConfig {
- minSdk = 28
- }
-}
diff --git a/extensions/reddit/src/main/AndroidManifest.xml b/extensions/reddit/src/main/AndroidManifest.xml
deleted file mode 100644
index 9b65eb06cf..0000000000
--- a/extensions/reddit/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/extensions/reddit/src/main/java/app/revanced/extension/reddit/patches/FilterPromotedLinksPatch.java b/extensions/reddit/src/main/java/app/revanced/extension/reddit/patches/FilterPromotedLinksPatch.java
deleted file mode 100644
index 12cdc88345..0000000000
--- a/extensions/reddit/src/main/java/app/revanced/extension/reddit/patches/FilterPromotedLinksPatch.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package app.revanced.extension.reddit.patches;
-
-import com.reddit.domain.model.ILink;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@SuppressWarnings("unused")
-public final class FilterPromotedLinksPatch {
-
- /**
- * Injection point.
- *
- * Filters list from promoted links.
- **/
- public static List> filterChildren(final Iterable> links) {
- final List filteredList = new ArrayList<>();
-
- for (Object item : links) {
- if (item instanceof ILink && ((ILink) item).getPromoted()) continue;
-
- filteredList.add(item);
- }
-
- return filteredList;
- }
-}
diff --git a/extensions/reddit/stub/build.gradle.kts b/extensions/reddit/stub/build.gradle.kts
deleted file mode 100644
index b4bee8809f..0000000000
--- a/extensions/reddit/stub/build.gradle.kts
+++ /dev/null
@@ -1,17 +0,0 @@
-plugins {
- alias(libs.plugins.android.library)
-}
-
-android {
- namespace = "app.revanced.extension"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 24
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
-}
diff --git a/extensions/reddit/stub/src/main/AndroidManifest.xml b/extensions/reddit/stub/src/main/AndroidManifest.xml
deleted file mode 100644
index 15e7c2ae67..0000000000
--- a/extensions/reddit/stub/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extensions/reddit/stub/src/main/java/com/reddit/domain/model/ILink.java b/extensions/reddit/stub/src/main/java/com/reddit/domain/model/ILink.java
deleted file mode 100644
index f9cbb955cb..0000000000
--- a/extensions/reddit/stub/src/main/java/com/reddit/domain/model/ILink.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.reddit.domain.model;
-
-public class ILink {
- public boolean getPromoted() {
- throw new UnsupportedOperationException("Stub");
- }
-}
diff --git a/extensions/samsung/radio/build.gradle.kts b/extensions/samsung/radio/build.gradle.kts
deleted file mode 100644
index 15d386efb3..0000000000
--- a/extensions/samsung/radio/build.gradle.kts
+++ /dev/null
@@ -1,10 +0,0 @@
-dependencies {
- compileOnly(project(":extensions:shared:library"))
- compileOnly(project(":extensions:samsung:radio:stub"))
-}
-
-android {
- defaultConfig {
- minSdk = 26
- }
-}
diff --git a/extensions/samsung/radio/src/main/AndroidManifest.xml b/extensions/samsung/radio/src/main/AndroidManifest.xml
deleted file mode 100644
index 9b65eb06cf..0000000000
--- a/extensions/samsung/radio/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/misc/fix/crash/FixCrashPatch.java b/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/misc/fix/crash/FixCrashPatch.java
deleted file mode 100644
index 72c5addc4c..0000000000
--- a/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/misc/fix/crash/FixCrashPatch.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package app.revanced.extension.samsung.radio.misc.fix.crash;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-@SuppressWarnings("unused")
-public final class FixCrashPatch {
- /**
- * Injection point.
- *
- * Add the required permissions to the request list to avoid crashes on API 34+.
- **/
- public static final String[] fixPermissionRequestList(String[] perms) {
- List permsList = new ArrayList<>(Arrays.asList(perms));
- if (permsList.contains("android.permission.POST_NOTIFICATIONS")) {
- permsList.addAll(Arrays.asList("android.permission.RECORD_AUDIO", "android.permission.READ_PHONE_STATE", "android.permission.FOREGROUND_SERVICE_MICROPHONE"));
- }
- if (permsList.contains("android.permission.RECORD_AUDIO")) {
- permsList.add("android.permission.FOREGROUND_SERVICE_MICROPHONE");
- }
- return permsList.toArray(new String[0]);
- }
-}
diff --git a/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/restrictions/device/BypassDeviceChecksPatch.java b/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/restrictions/device/BypassDeviceChecksPatch.java
deleted file mode 100644
index 19b6c3e822..0000000000
--- a/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/restrictions/device/BypassDeviceChecksPatch.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package app.revanced.extension.samsung.radio.restrictions.device;
-
-import android.os.SemSystemProperties;
-
-import java.util.Arrays;
-
-@SuppressWarnings("unused")
-public final class BypassDeviceChecksPatch {
-
- /**
- * Injection point.
- *
- * Check if the device has the required hardware
- **/
- public static final boolean checkIfDeviceIsIncompatible(String[] deviceList) {
- String currentDevice = SemSystemProperties.getSalesCode();
- return Arrays.asList(deviceList).contains(currentDevice);
- }
-}
diff --git a/extensions/samsung/radio/stub/build.gradle.kts b/extensions/samsung/radio/stub/build.gradle.kts
deleted file mode 100644
index b4bee8809f..0000000000
--- a/extensions/samsung/radio/stub/build.gradle.kts
+++ /dev/null
@@ -1,17 +0,0 @@
-plugins {
- alias(libs.plugins.android.library)
-}
-
-android {
- namespace = "app.revanced.extension"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 24
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
-}
diff --git a/extensions/samsung/radio/stub/src/main/AndroidManifest.xml b/extensions/samsung/radio/stub/src/main/AndroidManifest.xml
deleted file mode 100644
index 15e7c2ae67..0000000000
--- a/extensions/samsung/radio/stub/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extensions/samsung/radio/stub/src/main/java/android/os/SemSystemProperties.java b/extensions/samsung/radio/stub/src/main/java/android/os/SemSystemProperties.java
deleted file mode 100644
index 33a4b4400c..0000000000
--- a/extensions/samsung/radio/stub/src/main/java/android/os/SemSystemProperties.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package android.os;
-
-public class SemSystemProperties {
- public static String getSalesCode() {
- throw new UnsupportedOperationException("Stub");
- }
-}
\ No newline at end of file
diff --git a/extensions/shared/build.gradle.kts b/extensions/shared/build.gradle.kts
deleted file mode 100644
index 3eb6ff48c7..0000000000
--- a/extensions/shared/build.gradle.kts
+++ /dev/null
@@ -1,10 +0,0 @@
-dependencies {
- implementation(project(":extensions:shared:library"))
- compileOnly(libs.okhttp)
-}
-
-android {
- defaultConfig {
- minSdk = 23
- }
-}
diff --git a/extensions/shared/library/build.gradle.kts b/extensions/shared/library/build.gradle.kts
deleted file mode 100644
index 8215e513ad..0000000000
--- a/extensions/shared/library/build.gradle.kts
+++ /dev/null
@@ -1,24 +0,0 @@
-plugins {
- alias(libs.plugins.android.library)
-}
-
-android {
- namespace = "app.revanced.extension"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 23
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
-}
-
-dependencies {
- compileOnly(libs.annotation)
- compileOnly(libs.okhttp)
- compileOnly(libs.protobuf.javalite)
- implementation(project(":extensions:shared:protobuf", configuration = "shadowRuntimeElements"))
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ByteTrieSearch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ByteTrieSearch.java
deleted file mode 100644
index c91de4a7aa..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ByteTrieSearch.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package app.revanced.extension.shared;
-
-import java.nio.charset.StandardCharsets;
-
-public final class ByteTrieSearch extends TrieSearch {
-
- private static final class ByteTrieNode extends TrieNode {
- ByteTrieNode() {
- super();
- }
- ByteTrieNode(char nodeCharacterValue) {
- super(nodeCharacterValue);
- }
- @Override
- TrieNode createNode(char nodeCharacterValue) {
- return new ByteTrieNode(nodeCharacterValue);
- }
- @Override
- char getCharValue(byte[] text, int index) {
- return (char) text[index];
- }
- @Override
- int getTextLength(byte[] text) {
- return text.length;
- }
- }
-
- /**
- * Helper method for the common usage of converting Strings to raw UTF-8 bytes.
- */
- public static byte[][] convertStringsToBytes(String... strings) {
- final int length = strings.length;
- byte[][] replacement = new byte[length][];
- for (int i = 0; i < length; i++) {
- replacement[i] = strings[i].getBytes(StandardCharsets.UTF_8);
- }
- return replacement;
- }
-
- public ByteTrieSearch(byte[]... patterns) {
- super(new ByteTrieNode(), patterns);
- }
-}
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
deleted file mode 100644
index fb7e68963a..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java
+++ /dev/null
@@ -1,412 +0,0 @@
-package app.revanced.extension.shared;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.Dialog;
-import android.app.SearchManager;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.PowerManager;
-import android.provider.Settings;
-import android.util.Pair;
-import android.widget.LinearLayout;
-
-import androidx.annotation.Nullable;
-
-import app.revanced.extension.shared.requests.Requester;
-import app.revanced.extension.shared.requests.Route;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.ui.CustomDialog;
-
-import org.json.JSONObject;
-
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.Locale;
-
-import static app.revanced.extension.shared.StringRef.str;
-import static app.revanced.extension.shared.requests.Route.Method.GET;
-
-@SuppressWarnings("unused")
-public class GmsCoreSupport {
- private static GmsCore gmsCore = GmsCore.UNKNOWN;
-
- static {
- for (GmsCore core : GmsCore.values()) {
- if (core.getGroupId().equals(getGmsCoreVendorGroupId())) {
- GmsCoreSupport.gmsCore = core;
- break;
- }
- }
- }
-
- /**
- * Injection point.
- */
- public static void checkGmsCore(Activity context) {
- gmsCore.check(context);
- }
-
- private static String getOriginalPackageName() {
- return null; // Modified during patching.
- }
-
- private static String getGmsCoreVendorGroupId() {
- return "app.revanced"; // Modified during patching.
- }
-
-
- /**
- * @return If the current package name is the same as the original unpatched app.
- * If `GmsCore support` was not included during patching, this returns true;
- */
- public static boolean isPackageNameOriginal() {
- String originalPackageName = getOriginalPackageName();
- return originalPackageName == null
- || originalPackageName.equals(Utils.getContext().getPackageName());
- }
-
- private enum GmsCore {
- REVANCED("app.revanced", "https://github.com/revanced/gmscore/releases/latest", () -> {
- try {
- HttpURLConnection connection = Requester.getConnectionFromRoute(
- "https://api.github.com",
- new Route(GET, "/repos/revanced/gmscore/releases/latest")
- );
- connection.setConnectTimeout(5000);
- connection.setReadTimeout(5000);
-
- int responseCode = connection.getResponseCode();
- if (responseCode != 200) {
- Logger.printDebug(() -> "GitHub API returned status code: " + responseCode);
- return null;
- }
-
- // Parse the response
- JSONObject releaseData = Requester.parseJSONObject(connection);
- String tagName = releaseData.optString("tag_name", "");
- connection.disconnect();
-
- if (tagName.isEmpty()) {
- Logger.printDebug(() -> "No tag_name found in GitHub release data");
- return null;
- }
-
- if (tagName.startsWith("v")) tagName = tagName.substring(1);
-
- return tagName;
- } catch (Exception ex) {
- Logger.printInfo(() -> "Failed to fetch latest GmsCore version from GitHub", ex);
- return null;
- }
- }),
- UNKNOWN(getGmsCoreVendorGroupId(), getGmsCoreVendorGroupId() + "android.gms", () -> null);
-
- private static final String DONT_KILL_MY_APP_URL
- = "https://dontkillmyapp.com/";
- private static final Route DONT_KILL_MY_APP_MANUFACTURER_API
- = new Route(GET, "/api/v2/{manufacturer}.json");
- private static final String DONT_KILL_MY_APP_NAME_PARAMETER
- = "?app=MicroG";
- private static final String BUILD_MANUFACTURER
- = Build.MANUFACTURER.toLowerCase(Locale.ROOT).replace(" ", "-");
-
- /**
- * If a manufacturer specific page exists on DontKillMyApp.
- */
- @Nullable
- private volatile Boolean dontKillMyAppManufacturerSupported;
-
- private final String groupId;
- private final String packageName;
- private final String downloadQuery;
- private final GetLatestVersion getLatestVersion;
- private final Uri gmsCoreProvider;
-
- GmsCore(String groupId, String downloadQuery, GetLatestVersion getLatestVersion) {
- this.groupId = groupId;
- this.packageName = groupId + ".android.gms";
- this.gmsCoreProvider = Uri.parse("content://" + groupId + ".android.gsf.gservices/prefix");
-
- this.downloadQuery = downloadQuery;
- this.getLatestVersion = getLatestVersion;
- }
-
- String getGroupId() {
- return groupId;
- }
-
- void check(Activity context) {
- checkInstallation(context);
- checkUpdates(context);
- }
-
- private void checkInstallation(Activity context) {
- try {
- // Verify the user has not included GmsCore for a root installation.
- // GmsCore Support changes the package name, but with a mounted installation
- // all manifest changes are ignored and the original package name is used.
- if (isPackageNameOriginal()) {
- Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
- // Cannot use localize text here, since the app will load resources
- // from the unpatched app and all patch strings are missing.
- Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
-
- // Do not exit. If the app exits before launch completes (and without
- // opening another activity), then on some devices such as Pixel phone Android 10
- // no toast will be shown and the app will continually relaunch
- // with the appearance of a hung app.
- }
-
- // Verify GmsCore is installed.
- try {
- PackageManager manager = context.getPackageManager();
- manager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES);
- } catch (PackageManager.NameNotFoundException exception) {
- Logger.printInfo(() -> "GmsCore was not found");
- // Cannot show a dialog and must show a toast,
- // because on some installations the app crashes before a dialog can be displayed.
- Utils.showToastLong(str("revanced_gms_core_toast_not_installed_message"));
-
- open(downloadQuery);
- return;
- }
-
- // Check if GmsCore is whitelisted from battery optimizations.
- if (isAndroidAutomotive(context)) {
- // Ignore Android Automotive devices (Google built-in),
- // as there is no way to disable battery optimizations.
- Logger.printDebug(() -> "Device is Android Automotive");
- } else if (batteryOptimizationsEnabled(context)) {
- Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
-
- showBatteryOptimizationDialog(context,
- "revanced_gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
- "revanced_gms_core_dialog_continue_text",
- (dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
- return;
- }
-
- // Check if GmsCore is currently running in the background.
- var client = context.getContentResolver().acquireContentProviderClient(gmsCoreProvider);
- //noinspection TryFinallyCanBeTryWithResources
- try {
- if (client == null) {
- Logger.printInfo(() -> "GmsCore is not running in the background");
- checkIfDontKillMyAppSupportsManufacturer();
-
- showBatteryOptimizationDialog(context,
- "revanced_gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
- "gmsrevanced_gms_core_log_open_website_text",
- (dialog, id) -> openDontKillMyApp());
- }
- } finally {
- if (client != null) client.close();
- }
- } catch (Exception ex) {
- Logger.printException(() -> "checkGmsCore failure", ex);
- }
- }
-
- private void checkUpdates(Activity context) {
- if (!BaseSettings.GMS_CORE_CHECK_UPDATES.get()) {
- Logger.printDebug(() -> "GmsCore update check is disabled in settings");
- return;
- }
-
- Utils.runOnBackgroundThread(() -> {
- try {
- PackageManager manager = context.getPackageManager();
- var installedVersion = manager.getPackageInfo(packageName, 0).versionName;
-
- // GmsCore adds suffixes for flavor builds. Remove the suffix for version comparison.
- int suffixIndex = installedVersion.indexOf('-');
- if (suffixIndex != -1)
- installedVersion = installedVersion.substring(0, suffixIndex);
- String finalInstalledVersion = installedVersion;
-
- Logger.printDebug(() -> "Installed GmsCore version: " + finalInstalledVersion);
-
- var latestVersion = getLatestVersion.get();
-
- if (latestVersion == null || latestVersion.isEmpty()) {
- Logger.printDebug(() -> "Could not get latest GmsCore version");
- Utils.showToastLong(str("revanced_gms_core_toast_update_check_failed_message"));
- return;
- }
-
- Logger.printDebug(() -> "Latest GmsCore version on GitHub: " + latestVersion);
-
- // Compare versions
- if (!installedVersion.equals(latestVersion)) {
- Logger.printInfo(() -> "GmsCore update available. Installed: " + finalInstalledVersion
- + ", Latest: " + latestVersion);
-
- showUpdateDialog(context, installedVersion, latestVersion);
- } else {
- Logger.printDebug(() -> "GmsCore is up to date");
- }
- } catch (Exception ex) {
- Logger.printInfo(() -> "Could not check GmsCore updates", ex);
- Utils.showToastLong(str("revanced_gms_core_toast_update_check_failed_message"));
- }
- });
- }
-
- private void open(String queryOrLink) {
- Logger.printInfo(() -> "Opening link: " + queryOrLink);
-
- Intent intent;
- try {
- // Check if queryOrLink is a valid URL.
- new URL(queryOrLink);
-
- intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink));
- } catch (MalformedURLException e) {
- intent = new Intent(Intent.ACTION_WEB_SEARCH);
- intent.putExtra(SearchManager.QUERY, queryOrLink);
- }
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- Utils.getContext().startActivity(intent);
-
- // Gracefully exit, otherwise the broken app will continue to run.
- System.exit(0);
- }
-
- private void showUpdateDialog(Activity context, String installedVersion, String latestVersion) {
- // Use a delay to allow the activity to finish initializing.
- // Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
- Utils.runOnMainThreadDelayed(() -> {
- try {
- Pair dialogPair = CustomDialog.create(
- context,
- str("revanced_gms_core_dialog_title"),
- String.format(str("revanced_gms_core_update_available_message"), latestVersion, installedVersion),
- null,
- str("revanced_gms_core_dialog_open_website_text"),
- () -> open(downloadQuery),
- () -> {
- },
- str("revanced_gms_core_dialog_cancel_text"),
- null,
- true
- );
-
- Dialog dialog = dialogPair.first;
- dialog.setCancelable(true);
- Utils.showDialog(context, dialog);
- } catch (Exception ex) {
- Logger.printException(() -> "Failed to show GmsCore update dialog", ex);
- }
- }, 100);
- }
-
- private static void showBatteryOptimizationDialog(Activity context,
- String dialogMessageRef,
- String positiveButtonTextRef,
- DialogInterface.OnClickListener onPositiveClickListener) {
- // Use a delay to allow the activity to finish initializing.
- // Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
- Utils.runOnMainThreadDelayed(() -> {
- // Create the custom dialog.
- Pair dialogPair = CustomDialog.create(
- context,
- str("revanced_gms_core_dialog_title"), // Title.
- str(dialogMessageRef), // Message.
- null, // No EditText.
- str(positiveButtonTextRef), // OK button text.
- () -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
- null, // No Cancel button action.
- null, // No Neutral button text.
- null, // No Neutral button action.
- true // Dismiss dialog when onNeutralClick.
- );
-
- Dialog dialog = dialogPair.first;
-
- // Do not set cancelable to false to allow using back button to skip the action,
- // just in case the battery change can never be satisfied.
- dialog.setCancelable(true);
-
- // Show the dialog
- Utils.showDialog(context, dialog);
- }, 100);
- }
-
- @SuppressLint("BatteryLife") // Permission is part of GmsCore
- private void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) {
- Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
- intent.setData(Uri.fromParts("package", packageName, null));
- activity.startActivityForResult(intent, 0);
- }
-
- private void checkIfDontKillMyAppSupportsManufacturer() {
- Utils.runOnBackgroundThread(() -> {
- try {
- final long start = System.currentTimeMillis();
- HttpURLConnection connection = Requester.getConnectionFromRoute(
- DONT_KILL_MY_APP_URL, DONT_KILL_MY_APP_MANUFACTURER_API, BUILD_MANUFACTURER);
- connection.setConnectTimeout(5000);
- connection.setReadTimeout(5000);
-
- final boolean supported = connection.getResponseCode() == 200;
- Logger.printInfo(() -> "Manufacturer is " + (supported ? "" : "NOT ")
- + "listed on DontKillMyApp: " + BUILD_MANUFACTURER
- + " fetch took: " + (System.currentTimeMillis() - start) + "ms");
- dontKillMyAppManufacturerSupported = supported;
- } catch (Exception ex) {
- Logger.printInfo(() -> "Could not check if manufacturer is listed on DontKillMyApp: "
- + BUILD_MANUFACTURER, ex);
- dontKillMyAppManufacturerSupported = null;
- }
- });
- }
-
- private void openDontKillMyApp() {
- final Boolean manufacturerSupported = dontKillMyAppManufacturerSupported;
-
- String manufacturerPageToOpen;
- if (manufacturerSupported == null) {
- // Fetch has not completed yet. Only happens on extremely slow internet connections
- // and the user spends less than 1 second reading what's on screen.
- // Instead of waiting for the fetch (which may timeout),
- // open the website without a vendor.
- manufacturerPageToOpen = "";
- } else if (manufacturerSupported) {
- manufacturerPageToOpen = BUILD_MANUFACTURER;
- } else {
- // No manufacturer specific page exists. Open the general page.
- manufacturerPageToOpen = "general";
- }
-
- open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER);
- }
-
- /**
- * @return If GmsCore is not whitelisted from battery optimizations.
- */
- private boolean batteryOptimizationsEnabled(Context context) {
- //noinspection ObsoleteSdkInt
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- // Android 5.0 does not have battery optimization settings.
- return false;
- }
- var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- return !powerManager.isIgnoringBatteryOptimizations(packageName);
- }
-
- private boolean isAndroidAutomotive(Context context) {
- return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
- }
- }
-
- @FunctionalInterface
- private interface GetLatestVersion {
- String get();
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java
deleted file mode 100644
index 610cd3414f..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java
+++ /dev/null
@@ -1,214 +0,0 @@
-package app.revanced.extension.shared;
-
-import static app.revanced.extension.shared.settings.BaseSettings.DEBUG;
-import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_STACKTRACE;
-import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
-
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.settings.preference.LogBufferManager;
-
-/**
- * ReVanced specific logger. Logging is done to standard device log (accessible through ADB),
- * and additionally accessible through {@link LogBufferManager}.
- *
- * All methods are thread safe, and are safe to call even
- * if {@link Utils#getContext()} is not available.
- */
-public class Logger {
-
- /**
- * Log messages using lambdas.
- */
- @FunctionalInterface
- public interface LogMessage {
- /**
- * @return Logger string message. This method is only called if logging is enabled.
- */
- @NonNull
- String buildMessageString();
- }
-
- private enum LogLevel {
- DEBUG,
- INFO,
- ERROR
- }
-
- /**
- * Log tag prefix. Only used for system logging.
- */
- private static final String REVANCED_LOG_TAG_PREFIX = "revanced: ";
-
- private static final String LOGGER_CLASS_NAME = Logger.class.getName();
-
- /**
- * @return For outer classes, this returns {@link Class#getSimpleName()}.
- * For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
- *
- * For example, each of these classes returns 'SomethingView':
- *
- * com.company.SomethingView
- * com.company.SomethingView$StaticClass
- * com.company.SomethingView$1
- *
- */
- private static String getOuterClassSimpleName(Object obj) {
- Class> logClass = obj.getClass();
- String fullClassName = logClass.getName();
- final int dollarSignIndex = fullClassName.indexOf('$');
- if (dollarSignIndex < 0) {
- return logClass.getSimpleName(); // Already an outer class.
- }
-
- // Class is inner, static, or anonymous.
- // Parse the simple name full name.
- // A class with no package returns index of -1, but incrementing gives index zero which is correct.
- final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
- return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
- }
-
- /**
- * Internal method to handle logging to Android Log and {@link LogBufferManager}.
- * Appends the log message, stack trace (if enabled), and exception (if present) to logBuffer
- * with class name but without 'revanced:' prefix.
- *
- * @param logLevel The log level.
- * @param message Log message object.
- * @param ex Optional exception.
- * @param includeStackTrace If the current stack should be included.
- * @param showToast If a toast is to be shown.
- */
- private static void logInternal(LogLevel logLevel, LogMessage message, @Nullable Throwable ex,
- boolean includeStackTrace, boolean showToast) {
- // It's very important that no Settings are used in this method,
- // as this code is used when a context is not set and thus referencing
- // a setting will crash the app.
- String messageString = message.buildMessageString();
- String className = getOuterClassSimpleName(message);
-
- String logText = messageString;
-
- // Append exception message if present.
- if (ex != null) {
- var exceptionMessage = ex.getMessage();
- if (exceptionMessage != null) {
- logText += "\nException: " + exceptionMessage;
- }
- }
-
- if (includeStackTrace) {
- var sw = new StringWriter();
- new Throwable().printStackTrace(new PrintWriter(sw));
- String stackTrace = sw.toString();
- // Remove the stacktrace elements of this class.
- final int loggerIndex = stackTrace.lastIndexOf(LOGGER_CLASS_NAME);
- final int loggerBegins = stackTrace.indexOf('\n', loggerIndex);
- logText += stackTrace.substring(loggerBegins);
- }
-
- // Do not include "revanced:" prefix in clipboard logs.
- String managerToastString = className + ": " + logText;
- LogBufferManager.appendToLogBuffer(managerToastString);
-
- String logTag = REVANCED_LOG_TAG_PREFIX + className;
- switch (logLevel) {
- case DEBUG:
- if (ex == null) Log.d(logTag, logText);
- else Log.d(logTag, logText, ex);
- break;
- case INFO:
- if (ex == null) Log.i(logTag, logText);
- else Log.i(logTag, logText, ex);
- break;
- case ERROR:
- if (ex == null) Log.e(logTag, logText);
- else Log.e(logTag, logText, ex);
- break;
- }
-
- if (showToast) {
- Utils.showToastLong(managerToastString);
- }
- }
-
- private static boolean shouldLogDebug() {
- // If the app is still starting up and the context is not yet set,
- // then allow debug logging regardless what the debug setting actually is.
- return Utils.context == null || DEBUG.get();
- }
-
- private static boolean shouldShowErrorToast() {
- return Utils.context != null && DEBUG_TOAST_ON_ERROR.get();
- }
-
- private static boolean includeStackTrace() {
- return Utils.context != null && DEBUG_STACKTRACE.get();
- }
-
- /**
- * Logs debug messages under the outer class name of the code calling this method.
- *
- * Whenever possible, the log string should be constructed entirely inside
- * {@link LogMessage#buildMessageString()} so the performance cost of
- * building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
- */
- public static void printDebug(LogMessage message) {
- printDebug(message, null);
- }
-
- /**
- * Logs debug messages under the outer class name of the code calling this method.
- *
- * Whenever possible, the log string should be constructed entirely inside
- * {@link LogMessage#buildMessageString()} so the performance cost of
- * building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
- */
- public static void printDebug(LogMessage message, @Nullable Exception ex) {
- if (shouldLogDebug()) {
- logInternal(LogLevel.DEBUG, message, ex, includeStackTrace(), false);
- }
- }
-
- /**
- * Logs information messages using the outer class name of the code calling this method.
- */
- public static void printInfo(LogMessage message) {
- printInfo(message, null);
- }
-
- /**
- * Logs information messages using the outer class name of the code calling this method.
- */
- public static void printInfo(LogMessage message, @Nullable Exception ex) {
- logInternal(LogLevel.INFO, message, ex, includeStackTrace(), false);
- }
-
- /**
- * Logs exceptions under the outer class name of the code calling this method.
- * Appends the log message, exception (if present), and toast message (if enabled) to logBuffer.
- */
- public static void printException(LogMessage message) {
- printException(message, null);
- }
-
- /**
- * Logs exceptions under the outer class name of the code calling this method.
- *
- * If the calling code is showing its own error toast,
- * instead use {@link #printInfo(LogMessage, Exception)}
- *
- * @param message log message
- * @param ex exception (optional)
- */
- public static void printException(LogMessage message, @Nullable Throwable ex) {
- logInternal(LogLevel.ERROR, message, ex, includeStackTrace(), shouldShowErrorToast());
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ResourceType.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ResourceType.java
deleted file mode 100644
index 48032017a4..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ResourceType.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package app.revanced.extension.shared;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public enum ResourceType {
- ANIM("anim"),
- ANIMATOR("animator"),
- ARRAY("array"),
- ATTR("attr"),
- BOOL("bool"),
- COLOR("color"),
- DIMEN("dimen"),
- DRAWABLE("drawable"),
- FONT("font"),
- FRACTION("fraction"),
- ID("id"),
- INTEGER("integer"),
- INTERPOLATOR("interpolator"),
- LAYOUT("layout"),
- MENU("menu"),
- MIPMAP("mipmap"),
- NAVIGATION("navigation"),
- PLURALS("plurals"),
- RAW("raw"),
- STRING("string"),
- STYLE("style"),
- STYLEABLE("styleable"),
- TRANSITION("transition"),
- VALUES("values"),
- XML("xml");
-
- private static final Map VALUE_MAP;
-
- static {
- ResourceType[] values = values();
- VALUE_MAP = new HashMap<>(2 * values.length);
-
- for (ResourceType type : values) {
- VALUE_MAP.put(type.value, type);
- }
- }
-
- public final String value;
-
- public static ResourceType fromValue(String value) {
- ResourceType type = VALUE_MAP.get(value);
- if (type == null) {
- throw new IllegalArgumentException("Unknown resource type: " + value);
- }
- return type;
- }
-
- ResourceType(String value) {
- this.value = value;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java
deleted file mode 100644
index c1c2c90d14..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package app.revanced.extension.shared;
-
-import android.content.Context;
-import android.content.res.Resources;
-
-import androidx.annotation.NonNull;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-public class StringRef {
- private static Resources resources;
- private static String packageName;
-
- // must use a thread safe map, as this class is used both on and off the main thread
- private static final Map strings = Collections.synchronizedMap(new HashMap<>());
-
- /**
- * Returns a cached instance.
- * Should be used if the same String could be loaded more than once.
- *
- * @param id string resource name/id
- * @see #sf(String)
- */
- @NonNull
- public static StringRef sfc(@NonNull String id) {
- StringRef ref = strings.get(id);
- if (ref == null) {
- ref = new StringRef(id);
- strings.put(id, ref);
- }
- return ref;
- }
-
- /**
- * Creates a new instance, but does not cache the value.
- * Should be used for Strings that are loaded exactly once.
- *
- * @param id string resource name/id
- * @see #sfc(String)
- */
- @NonNull
- public static StringRef sf(@NonNull String id) {
- return new StringRef(id);
- }
-
- /**
- * Gets string value by string id, shorthand for sfc(id).toString()
- *
- * @param id string resource name/id
- * @return String value from string.xml
- */
- @NonNull
- public static String str(@NonNull String id) {
- return sfc(id).toString();
- }
-
- /**
- * Gets string value by string id, shorthand for sfc(id).toString() and formats the string
- * with given args.
- *
- * @param id string resource name/id
- * @param args the args to format the string with
- * @return String value from string.xml formatted with given args
- */
- @NonNull
- public static String str(@NonNull String id, Object... args) {
- return String.format(str(id), args);
- }
-
- /**
- * Creates a StringRef object that'll not change its value
- *
- * @param value value which toString() method returns when invoked on returned object
- * @return Unique StringRef instance, its value will never change
- */
- @NonNull
- public static StringRef constant(@NonNull String value) {
- final StringRef ref = new StringRef(value);
- ref.resolved = true;
- return ref;
- }
-
- /**
- * Shorthand for constant("")
- * Its value always resolves to empty string
- */
- @NonNull
- public static final StringRef empty = constant("");
-
- @NonNull
- private String value;
- private boolean resolved;
-
- public StringRef(@NonNull String resName) {
- this.value = resName;
- }
-
- @Override
- @NonNull
- public String toString() {
- if (!resolved) {
- if (resources == null || packageName == null) {
- var context = Utils.getContext();
- resources = context.getResources();
- packageName = context.getPackageName();
- }
- resolved = true;
- if (resources != null) {
- final int identifier = resources.getIdentifier(value, "string", packageName);
- if (identifier == 0)
- Logger.printException(() -> "Resource not found: " + value);
- else
- value = resources.getString(identifier);
- } else {
- Logger.printException(() -> "Could not resolve resources!");
- }
- }
- return value;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringTrieSearch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringTrieSearch.java
deleted file mode 100644
index 9c7b882138..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringTrieSearch.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package app.revanced.extension.shared;
-
-/**
- * Text pattern searching using a prefix tree (trie).
- */
-public final class StringTrieSearch extends TrieSearch {
-
- private static final class StringTrieNode extends TrieNode {
- StringTrieNode() {
- super();
- }
- StringTrieNode(char nodeCharacterValue) {
- super(nodeCharacterValue);
- }
- @Override
- TrieNode createNode(char nodeValue) {
- return new StringTrieNode(nodeValue);
- }
- @Override
- char getCharValue(String text, int index) {
- return text.charAt(index);
- }
- @Override
- int getTextLength(String text) {
- return text.length();
- }
- }
-
- public StringTrieSearch(String... patterns) {
- super(new StringTrieNode(), patterns);
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java
deleted file mode 100644
index 97fa4605d8..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java
+++ /dev/null
@@ -1,425 +0,0 @@
-package app.revanced.extension.shared;
-
-import androidx.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Searches for a group of different patterns using a trie (prefix tree).
- * Can significantly speed up searching for multiple patterns.
- */
-public abstract class TrieSearch {
-
- public interface TriePatternMatchedCallback {
- /**
- * Called when a pattern is matched.
- *
- * @param textSearched Text that was searched.
- * @param matchedStartIndex Start index of the search text, where the pattern was matched.
- * @param matchedLength Length of the match.
- * @param callbackParameter Optional parameter passed into {@link TrieSearch#matches(Object, Object)}.
- * @return True, if the search should stop here.
- * If false, searching will continue to look for other matches.
- */
- boolean patternMatched(T textSearched, int matchedStartIndex, int matchedLength, Object callbackParameter);
- }
-
- /**
- * Represents a compressed tree path for a single pattern that shares no sibling nodes.
- *
- * For example, if a tree contains the patterns: "foobar", "football", "feet",
- * it would contain 3 compressed paths of: "bar", "tball", "eet".
- *
- * And the tree would contain children arrays only for the first level containing 'f',
- * the second level containing 'o',
- * and the third level containing 'o'.
- *
- * This is done to reduce memory usage, which can be substantial if many long patterns are used.
- */
- private static final class TrieCompressedPath {
- final T pattern;
- final int patternStartIndex;
- final int patternLength;
- final TriePatternMatchedCallback callback;
-
- TrieCompressedPath(T pattern, int patternStartIndex, int patternLength, TriePatternMatchedCallback callback) {
- this.pattern = pattern;
- this.patternStartIndex = patternStartIndex;
- this.patternLength = patternLength;
- this.callback = callback;
- }
- boolean matches(TrieNode enclosingNode, // Used only for the get character method.
- T searchText, int searchTextLength, int searchTextIndex, Object callbackParameter) {
- if (searchTextLength - searchTextIndex < patternLength - patternStartIndex) {
- return false; // Remaining search text is shorter than the remaining leaf pattern and they cannot match.
- }
-
- for (int i = searchTextIndex, j = patternStartIndex; j < patternLength; i++, j++) {
- if (enclosingNode.getCharValue(searchText, i) != enclosingNode.getCharValue(pattern, j)) {
- return false;
- }
- }
-
- return callback == null || callback.patternMatched(searchText,
- searchTextIndex - patternStartIndex, patternLength, callbackParameter);
- }
- }
-
- static abstract class TrieNode {
- /**
- * Dummy value used for root node. Value can be anything as it's never referenced.
- */
- private static final char ROOT_NODE_CHARACTER_VALUE = 0; // ASCII null character.
-
- /**
- * How much to expand the children array when resizing.
- */
- private static final int CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT = 2;
-
- /**
- * Character this node represents.
- * This field is ignored for the root node (which does not represent any character).
- */
- private final char nodeValue;
-
- /**
- * A compressed graph path that represents the remaining pattern characters of a single child node.
- *
- * If present then child array is always null, although callbacks for other
- * end of patterns can also exist on this same node.
- */
- @Nullable
- private TrieCompressedPath leaf;
-
- /**
- * All child nodes. Only present if no compressed leaf exist.
- *
- * Array is dynamically increased in size as needed,
- * and uses perfect hashing for the elements it contains.
- *
- * So if the array contains a given character,
- * the character will always map to the node with index: (character % arraySize).
- *
- * Elements not contained can collide with elements the array does contain,
- * so must compare the nodes character value.
- *
- /*
- * Alternatively, this could be implemented as a sorted, densely packed array
- * with lookups performed via binary search.
- * This approach would save a small amount of memory by eliminating null
- * child entries. However, it would result in a worst-case lookup time of
- * O(n log m), where:
- * - n is the number of characters in the input text, and
- * - m is the maximum size of the sorted character arrays.
- * In contrast, using a hash-based array guarantees O(n) lookup time.
- * Given that the total memory usage is already very small (all Litho filters
- * together use approximately 10KB), the hash-based implementation is preferred
- * for its superior performance.
- */
- @Nullable
- private TrieNode[] children;
-
- /**
- * Callbacks for all patterns that end at this node.
- */
- @Nullable
- private List> endOfPatternCallback;
-
- TrieNode() {
- this.nodeValue = ROOT_NODE_CHARACTER_VALUE;
- }
- TrieNode(char nodeCharacterValue) {
- this.nodeValue = nodeCharacterValue;
- }
-
- /**
- * @param pattern Pattern to add.
- * @param patternIndex Current recursive index of the pattern.
- * @param patternLength Length of the pattern.
- * @param callback Callback, where a value of NULL indicates to always accept a pattern match.
- */
- private void addPattern(T pattern, int patternIndex, int patternLength,
- @Nullable TriePatternMatchedCallback callback) {
- if (patternIndex == patternLength) { // Reached the end of the pattern.
- if (endOfPatternCallback == null) {
- endOfPatternCallback = new ArrayList<>(1);
- }
- endOfPatternCallback.add(callback);
- return;
- }
-
- if (leaf != null) {
- // Reached end of the graph and a leaf exist.
- // Recursively call back into this method and push the existing leaf down 1 level.
- if (children != null) throw new IllegalStateException();
- //noinspection unchecked
- children = new TrieNode[1];
- TrieCompressedPath temp = leaf;
- leaf = null;
- addPattern(temp.pattern, temp.patternStartIndex, temp.patternLength, temp.callback);
- // Continue onward and add the parameter pattern.
- } else if (children == null) {
- leaf = new TrieCompressedPath<>(pattern, patternIndex, patternLength, callback);
- return;
- }
-
- final char character = getCharValue(pattern, patternIndex);
- final int arrayIndex = hashIndexForTableSize(children.length, character);
- TrieNode child = children[arrayIndex];
- if (child == null) {
- child = createNode(character);
- children[arrayIndex] = child;
- } else if (child.nodeValue != character) {
- // Hash collision. Resize the table until perfect hashing is found.
- child = createNode(character);
- expandChildArray(child);
- }
- child.addPattern(pattern, patternIndex + 1, patternLength, callback);
- }
-
- /**
- * Resizes the children table until all nodes hash to exactly one array index.
- */
- private void expandChildArray(TrieNode child) {
- int replacementArraySize = Objects.requireNonNull(children).length;
- while (true) {
- replacementArraySize += CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT;
- //noinspection unchecked
- TrieNode[] replacement = new TrieNode[replacementArraySize];
- addNodeToArray(replacement, child);
-
- boolean collision = false;
- for (TrieNode existingChild : children) {
- if (existingChild != null) {
- if (!addNodeToArray(replacement, existingChild)) {
- collision = true;
- break;
- }
- }
- }
- if (collision) {
- continue;
- }
-
- children = replacement;
- return;
- }
- }
-
- private static boolean addNodeToArray(TrieNode[] array, TrieNode childToAdd) {
- final int insertIndex = hashIndexForTableSize(array.length, childToAdd.nodeValue);
- if (array[insertIndex] != null ) {
- return false; // Collision.
- }
- array[insertIndex] = childToAdd;
- return true;
- }
-
- private static int hashIndexForTableSize(int arraySize, char nodeValue) {
- return nodeValue % arraySize;
- }
-
- /**
- * This method is static and uses a loop to avoid all recursion.
- * This is done for performance since the JVM does not optimize tail recursion.
- *
- * @param startNode Node to start the search from.
- * @param searchText Text to search for patterns in.
- * @param searchTextIndex Start index, inclusive.
- * @param searchTextEndIndex End index, exclusive.
- * @return If any pattern matches, and it's associated callback halted the search.
- */
- private static boolean matches(final TrieNode startNode, final T searchText,
- int searchTextIndex, final int searchTextEndIndex,
- final Object callbackParameter) {
- TrieNode node = startNode;
- int currentMatchLength = 0;
-
- while (true) {
- TrieCompressedPath leaf = node.leaf;
- if (leaf != null && leaf.matches(startNode, searchText, searchTextEndIndex, searchTextIndex, callbackParameter)) {
- return true; // Leaf exists and it matched the search text.
- }
-
- List> endOfPatternCallback = node.endOfPatternCallback;
- if (endOfPatternCallback != null) {
- final int matchStartIndex = searchTextIndex - currentMatchLength;
- for (@Nullable TriePatternMatchedCallback callback : endOfPatternCallback) {
- if (callback == null) {
- return true; // No callback and all matches are valid.
- }
- if (callback.patternMatched(searchText, matchStartIndex, currentMatchLength, callbackParameter)) {
- return true; // Callback confirmed the match.
- }
- }
- }
-
- TrieNode[] children = node.children;
- if (children == null) {
- return false; // Reached a graph end point and there's no further patterns to search.
- }
- if (searchTextIndex == searchTextEndIndex) {
- return false; // Reached end of the search text and found no matches.
- }
-
- // Use the start node to reduce VM method lookup, since all nodes are the same class type.
- final char character = startNode.getCharValue(searchText, searchTextIndex);
- final int arrayIndex = hashIndexForTableSize(children.length, character);
- TrieNode child = children[arrayIndex];
- if (child == null || child.nodeValue != character) {
- return false;
- }
-
- node = child;
- searchTextIndex++;
- currentMatchLength++;
- }
- }
-
- /**
- * Gives an approximate memory usage.
- *
- * @return Estimated number of memory pointers used, starting from this node and including all children.
- */
- private int estimatedNumberOfPointersUsed() {
- int numberOfPointers = 4; // Number of fields in this class.
- if (leaf != null) {
- numberOfPointers += 4; // Number of fields in leaf node.
- }
-
- if (endOfPatternCallback != null) {
- numberOfPointers += endOfPatternCallback.size();
- }
-
- if (children != null) {
- numberOfPointers += children.length;
- for (TrieNode child : children) {
- if (child != null) {
- numberOfPointers += child.estimatedNumberOfPointersUsed();
- }
- }
- }
- return numberOfPointers;
- }
-
- abstract TrieNode createNode(char nodeValue);
- abstract char getCharValue(T text, int index);
- abstract int getTextLength(T text);
- }
-
- /**
- * Root node, and it's children represent the first pattern characters.
- */
- private final TrieNode root;
-
- /**
- * Patterns to match.
- */
- private final List patterns = new ArrayList<>();
-
- @SafeVarargs
- TrieSearch(TrieNode root, T... patterns) {
- this.root = Objects.requireNonNull(root);
- addPatterns(patterns);
- }
-
- @SafeVarargs
- public final void addPatterns(T... patterns) {
- for (T pattern : patterns) {
- addPattern(pattern);
- }
- }
-
- /**
- * Adds a pattern that will always return a positive match if found.
- *
- * @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
- */
- public void addPattern(T pattern) {
- addPattern(pattern, root.getTextLength(pattern), null);
- }
-
- /**
- * @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
- * @param callback Callback to determine if searching should halt when a match is found.
- */
- public void addPattern(T pattern, TriePatternMatchedCallback callback) {
- addPattern(pattern, root.getTextLength(pattern), Objects.requireNonNull(callback));
- }
-
- void addPattern(T pattern, int patternLength, @Nullable TriePatternMatchedCallback callback) {
- if (patternLength == 0) return; // Nothing to match
-
- patterns.add(pattern);
- root.addPattern(pattern, 0, patternLength, callback);
- }
-
- public final boolean matches(T textToSearch) {
- return matches(textToSearch, 0);
- }
-
- public boolean matches(T textToSearch, Object callbackParameter) {
- return matches(textToSearch, 0, root.getTextLength(textToSearch),
- Objects.requireNonNull(callbackParameter));
- }
-
- public boolean matches(T textToSearch, int startIndex) {
- return matches(textToSearch, startIndex, root.getTextLength(textToSearch));
- }
-
- public final boolean matches(T textToSearch, int startIndex, int endIndex) {
- return matches(textToSearch, startIndex, endIndex, null);
- }
-
- /**
- * Searches through text, looking for any substring that matches any pattern in this tree.
- *
- * @param textToSearch Text to search through.
- * @param startIndex Index to start searching, inclusive value.
- * @param endIndex Index to stop matching, exclusive value.
- * @param callbackParameter Optional parameter passed to the callbacks.
- * @return If any pattern matched, and it's callback halted searching.
- */
- public boolean matches(T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) {
- return matches(textToSearch, root.getTextLength(textToSearch), startIndex, endIndex, callbackParameter);
- }
-
- private boolean matches(T textToSearch, int textToSearchLength, int startIndex, int endIndex,
- @Nullable Object callbackParameter) {
- if (endIndex > textToSearchLength) {
- throw new IllegalArgumentException("endIndex: " + endIndex
- + " is greater than texToSearchLength: " + textToSearchLength);
- }
- if (patterns.isEmpty()) {
- return false; // No patterns were added.
- }
- for (int i = startIndex; i < endIndex; i++) {
- if (TrieNode.matches(root, textToSearch, i, endIndex, callbackParameter)) return true;
- }
- return false;
- }
-
- /**
- * @return Estimated memory size (in kilobytes) of this instance.
- */
- public int getEstimatedMemorySize() {
- if (patterns.isEmpty()) {
- return 0;
- }
- // Assume the device has less than 32GB of ram (and can use pointer compression),
- // or the device is 32-bit.
- final int numberOfBytesPerPointer = 4;
- return (int) Math.ceil((numberOfBytesPerPointer * root.estimatedNumberOfPointersUsed()) / 1024.0);
- }
-
- public int numberOfPatterns() {
- return patterns.size();
- }
-
- public List getPatterns() {
- return Collections.unmodifiableList(patterns);
- }
-}
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
deleted file mode 100644
index cf65db8a4c..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
+++ /dev/null
@@ -1,1222 +0,0 @@
-package app.revanced.extension.shared;
-
-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;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.net.ConnectivityManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.preference.Preference;
-import android.preference.PreferenceGroup;
-import android.preference.PreferenceScreen;
-import android.util.Pair;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.view.Window;
-import android.view.WindowManager;
-import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
-import android.widget.Toast;
-
-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.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
-import java.util.concurrent.SynchronousQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Pattern;
-
-import app.revanced.extension.shared.settings.AppLanguage;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.settings.BooleanSetting;
-import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
-import app.revanced.extension.shared.ui.Dim;
-
-public class Utils {
-
- @SuppressLint("StaticFieldLeak")
- static volatile Context context;
-
- private static String versionName;
- private static String applicationLabel;
-
- @ColorInt
- private static int darkColor = Color.BLACK;
- @ColorInt
- private static int lightColor = Color.WHITE;
-
- @Nullable
- private static Boolean isDarkModeEnabled;
-
- private static boolean appIsUsingBoldIcons;
-
- // 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
-
- /**
- * Injection point.
- *
- * @return The manifest 'Version' entry of the patches.jar used during patching.
- */
- @SuppressWarnings("SameReturnValue")
- public static String getPatchesReleaseVersion() {
- return ""; // Value is replaced during patching.
- }
-
- private static PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
- final var packageName = Objects.requireNonNull(getContext()).getPackageName();
-
- PackageManager packageManager = context.getPackageManager();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- return packageManager.getPackageInfo(
- packageName,
- PackageManager.PackageInfoFlags.of(0)
- );
- }
-
- return packageManager.getPackageInfo(
- packageName,
- 0
- );
- }
-
- /**
- * @return The version name of the app, such as 20.13.41
- */
- public static String getAppVersionName() {
- if (versionName == null) {
- try {
- versionName = getPackageInfo().versionName;
- } catch (Exception ex) {
- Logger.printException(() -> "Failed to get package info", ex);
- versionName = "Unknown";
- }
- }
-
- return versionName;
- }
-
- @SuppressWarnings("unused")
- public static String getApplicationName() {
- if (applicationLabel == null) {
- try {
- ApplicationInfo applicationInfo = getPackageInfo().applicationInfo;
- applicationLabel = (String) applicationInfo.loadLabel(context.getPackageManager());
- } catch (Exception ex) {
- Logger.printException(() -> "Failed to get application name", ex);
- applicationLabel = "Unknown";
- }
- }
-
- return applicationLabel;
- }
-
- /**
- * Hide a view by setting its layout height and width to 1dp.
- *
- * @param setting The setting to check for hiding the view.
- * @param view The view to hide.
- */
- public static void hideViewBy0dpUnderCondition(BooleanSetting setting, View view) {
- if (hideViewBy0dpUnderCondition(setting.get(), view)) {
- Logger.printDebug(() -> "View hidden by setting: " + setting);
- }
- }
-
- /**
- * Hide a view by setting its layout height and width to 0dp.
- *
- * @param condition The setting to check for hiding the view.
- * @param view The view to hide.
- */
- public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) {
- if (condition) {
- hideViewBy0dp(view);
- return true;
- }
-
- return false;
- }
-
- /**
- * Hide a view by setting its layout params to 0x0
- * @param view The view to hide.
- */
- public static void hideViewBy0dp(View view) {
- ViewGroup.LayoutParams params = view.getLayoutParams();
- if (params == null)
- params = new ViewGroup.LayoutParams(0, 0);
-
- params.width = 0;
- params.height = 0;
- view.setLayoutParams(params);
- }
-
- /**
- * Hide a view by setting its visibility as GONE.
- *
- * @param setting The setting to check for hiding the view.
- * @param view The view to hide.
- */
- public static void hideViewUnderCondition(BooleanSetting setting, View view) {
- if (hideViewUnderCondition(setting.get(), view)) {
- Logger.printDebug(() -> "View hidden by setting: " + setting);
- }
- }
-
- /**
- * Hide a view by setting its visibility as GONE.
- *
- * @param condition The setting to check for hiding the view.
- * @param view The view to hide.
- */
- public static boolean hideViewUnderCondition(boolean condition, View view) {
- if (condition) {
- view.setVisibility(View.GONE);
- return true;
- }
-
- return false;
- }
-
- public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting setting, View view) {
- if (hideViewByRemovingFromParentUnderCondition(setting.get(), view)) {
- Logger.printDebug(() -> "View hidden by setting: " + setting);
- }
- }
-
- public static boolean hideViewByRemovingFromParentUnderCondition(boolean condition, View view) {
- if (condition) {
- ViewParent parent = view.getParent();
- if (parent instanceof ViewGroup parentGroup) {
- parentGroup.removeView(view);
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * General purpose pool for network calls and other background tasks.
- * All tasks run at max thread priority.
- */
- private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor(
- 3, // 3 threads always ready to go.
- Integer.MAX_VALUE,
- 10, // For any threads over the minimum, keep them alive 10 seconds after they go idle.
- TimeUnit.SECONDS,
- new SynchronousQueue<>(),
- r -> { // ThreadFactory
- Thread t = new Thread(r);
- t.setPriority(Thread.MAX_PRIORITY); // Run at max priority.
- return t;
- });
-
- public static void runOnBackgroundThread(Runnable task) {
- backgroundThreadPool.execute(task);
- }
-
- public static Future submitOnBackgroundThread(Callable call) {
- return backgroundThreadPool.submit(call);
- }
-
- /**
- * Simulates a delay by doing meaningless calculations.
- * Used for debugging to verify UI timeout logic.
- */
- @SuppressWarnings("UnusedReturnValue")
- public static long doNothingForDuration(long amountOfTimeToWaste) {
- final long timeCalculationStarted = System.currentTimeMillis();
- Logger.printDebug(() -> "Artificially creating delay of: " + amountOfTimeToWaste + "ms");
-
- long meaninglessValue = 0;
- while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
- // Could do a thread sleep, but that will trigger an exception if the thread is interrupted.
- meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random()));
- }
- // Return the value, otherwise the compiler or VM might optimize and remove the meaningless time-wasting work,
- // leaving an empty loop that hammers on the System.currentTimeMillis native call.
- return meaninglessValue;
- }
-
- public static boolean containsAny(String value, String... targets) {
- return indexOfFirstFound(value, targets) >= 0;
- }
-
- public static int indexOfFirstFound(String value, String... targets) {
- if (isNotEmpty(value)) {
- for (String string : targets) {
- if (!string.isEmpty()) {
- final int indexOf = value.indexOf(string);
- if (indexOf >= 0) return indexOf;
- }
- }
- }
- return -1;
- }
-
- /**
- * @return zero, if the resource is not found.
- */
- @SuppressLint("DiscouragedApi")
- public static int getResourceIdentifier(Context context, @Nullable ResourceType type, String resourceIdentifierName) {
- return context.getResources().getIdentifier(resourceIdentifierName,
- type == null ? null : type.value, context.getPackageName());
- }
-
- public static int getResourceIdentifierOrThrow(Context context, @Nullable ResourceType type, String resourceIdentifierName) {
- final int resourceId = getResourceIdentifier(context, type, resourceIdentifierName);
- if (resourceId == 0) {
- throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
- + " type: " + type);
- }
- return resourceId;
- }
-
- /**
- * @return zero, if the resource is not found.
- * @see #getResourceIdentifierOrThrow(ResourceType, String)
- */
- public static int getResourceIdentifier(@Nullable ResourceType type, String resourceIdentifierName) {
- return getResourceIdentifier(getContext(), type, resourceIdentifierName);
- }
-
- /**
- * @return zero, if the resource is not found.
- * @see #getResourceIdentifier(ResourceType, String)
- */
- public static int getResourceIdentifierOrThrow(@Nullable ResourceType type, String resourceIdentifierName) {
- return getResourceIdentifierOrThrow(getContext(), type, resourceIdentifierName);
- }
-
- public static String getResourceString(int id) throws Resources.NotFoundException {
- return getContext().getResources().getString(id);
- }
-
- public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
- return getContext().getResources().getInteger(getResourceIdentifierOrThrow(ResourceType.INTEGER, resourceIdentifierName));
- }
-
- public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException {
- return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(ResourceType.ANIM, resourceIdentifierName));
- }
-
- @ColorInt
- public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException {
- //noinspection deprecation
- return getContext().getResources().getColor(getResourceIdentifierOrThrow(ResourceType.COLOR, resourceIdentifierName));
- }
-
- public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException {
- return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName));
- }
-
- public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException {
- return getContext().getResources().getDimension(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName));
- }
-
- public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
- return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(ResourceType.ARRAY, resourceIdentifierName));
- }
-
- public interface MatchFilter {
- boolean matches(T object);
- }
-
- /**
- * Includes sub children.
- */
- public static R getChildViewByResourceName(View view, String str) {
- var child = view.findViewById(Utils.getResourceIdentifierOrThrow(ResourceType.ID, str));
- //noinspection unchecked
- return (R) child;
- }
-
- /**
- * @param searchRecursively If children ViewGroups should also be
- * recursively searched using depth first search.
- * @return The first child view that matches the filter.
- */
- @Nullable
- public static T getChildView(ViewGroup viewGroup, boolean searchRecursively,
- MatchFilter filter) {
- for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) {
- View childAt = viewGroup.getChildAt(i);
-
- if (filter.matches(childAt)) {
- //noinspection unchecked
- return (T) childAt;
- }
- // Must do recursive after filter check, in case the filter is looking for a ViewGroup.
- if (searchRecursively && childAt instanceof ViewGroup) {
- T match = getChildView((ViewGroup) childAt, true, filter);
- if (match != null) return match;
- }
- }
-
- return null;
- }
-
- @Nullable
- public static ViewParent getParentView(View view, int nthParent) {
- ViewParent parent = view.getParent();
-
- int currentDepth = 0;
- while (++currentDepth < nthParent && parent != null) {
- parent = parent.getParent();
- }
-
- if (currentDepth == nthParent) {
- return parent;
- }
-
- final int currentDepthLog = currentDepth;
- Logger.printDebug(() -> "Could not find parent view of depth: " + nthParent
- + " and instead found at: " + currentDepthLog + " view: " + view);
- return null;
- }
-
- public static void restartApp(Context context) {
- String packageName = context.getPackageName();
- Intent intent = Objects.requireNonNull(context.getPackageManager().getLaunchIntentForPackage(packageName));
- Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
- // Required for API 34 and later
- // Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents
- mainIntent.setPackage(packageName);
- context.startActivity(mainIntent);
- System.exit(0);
- }
-
- public static Context getContext() {
- if (context == null) {
- Logger.printException(() -> "Context is not set by extension hook, returning null", null);
- }
- return context;
- }
-
- public static void setContext(Context appContext) {
- // Intentionally use logger before context is set,
- // to expose any bugs in the 'no context available' logger code.
- Logger.printInfo(() -> "Set context: " + appContext);
- // Must initially set context to check the app language.
- context = appContext;
-
- AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
- if (language != AppLanguage.DEFAULT) {
- // Create a new context with the desired language.
- Logger.printDebug(() -> "Using app language: " + language);
- Configuration config = new Configuration(appContext.getResources().getConfiguration());
- config.setLocale(language.getLocale());
- context = appContext.createConfigurationContext(config);
- }
-
- setThemeLightColor(getThemeColor(getThemeLightColorResourceName(), Color.WHITE));
- setThemeDarkColor(getThemeColor(getThemeDarkColorResourceName(), Color.BLACK));
- }
-
- public static void setClipboard(CharSequence text) {
- ClipboardManager clipboard = (ClipboardManager) context
- .getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clip = ClipData.newPlainText("ReVanced", text);
- clipboard.setPrimaryClip(clip);
- }
-
- public static boolean isNotEmpty(@Nullable String str) {
- return str != null && !str.isEmpty();
- }
-
- @SuppressWarnings("unused")
- public static boolean isTablet() {
- return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
- }
-
- @Nullable
- private static Boolean isRightToLeftTextLayout;
-
- /**
- * @return If the device language uses right to left text layout (Hebrew, Arabic, etc.).
- * If this should match any ReVanced language override then instead use
- * {@link #isRightToLeftLocale(Locale)} with {@link BaseSettings#REVANCED_LANGUAGE}.
- * This is the default locale of the device, which may differ if
- * {@link BaseSettings#REVANCED_LANGUAGE} is set to a different language.
- */
- public static boolean isRightToLeftLocale() {
- if (isRightToLeftTextLayout == null) {
- isRightToLeftTextLayout = isRightToLeftLocale(Locale.getDefault());
- }
- return isRightToLeftTextLayout;
- }
-
- /**
- * @return If the locale uses right to left text layout (Hebrew, Arabic, etc.).
- */
- public static boolean isRightToLeftLocale(Locale locale) {
- String displayLanguage = locale.getDisplayLanguage();
- return new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft();
- }
-
- /**
- * @return A UTF8 string containing a left-to-right or right-to-left
- * character of the device locale. If this should match any ReVanced language
- * override then instead use {@link #getTextDirectionString(Locale)} with
- * {@link BaseSettings#REVANCED_LANGUAGE}.
- */
- public static String getTextDirectionString() {
- return getTextDirectionString(isRightToLeftLocale());
- }
-
- @SuppressWarnings("unused")
- public static String getTextDirectionString(Locale locale) {
- return getTextDirectionString(isRightToLeftLocale(locale));
- }
-
- private static String getTextDirectionString(boolean isRightToLeft) {
- return isRightToLeft
- ? "\u200F" // u200F = right to left character.
- : "\u200E"; // u200E = left to right character.
- }
-
- /**
- * @return if the text contains at least 1 number character,
- * including any Unicode numbers such as Arabic.
- */
- @SuppressWarnings("BooleanMethodIsAlwaysInverted")
- public static boolean containsNumber(CharSequence text) {
- for (int index = 0, length = text.length(); index < length;) {
- final int codePoint = Character.codePointAt(text, index);
- if (Character.isDigit(codePoint)) {
- return true;
- }
- index += Character.charCount(codePoint);
- }
-
- return false;
- }
-
- /**
- * Ignore this class. It must be public to satisfy Android requirements.
- */
- @SuppressWarnings("deprecation")
- public static final class DialogFragmentWrapper extends DialogFragment {
-
- private Dialog dialog;
- @Nullable
- private DialogFragmentOnStartAction onStartAction;
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- // Do not call super method to prevent state saving.
- }
-
- @NonNull
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- return dialog;
- }
-
- @Override
- public void onStart() {
- try {
- super.onStart();
-
- if (onStartAction != null) {
- onStartAction.onStart(dialog);
- }
- } catch (Exception ex) {
- Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex);
- }
- }
- }
-
- /**
- * Interface for {@link #showDialog(Activity, Dialog, boolean, DialogFragmentOnStartAction)}.
- */
- @FunctionalInterface
- public interface DialogFragmentOnStartAction {
- void onStart(Dialog dialog);
- }
-
- public static void showDialog(Activity activity, Dialog dialog) {
- showDialog(activity, dialog, true, null);
- }
-
- /**
- * Utility method to allow showing a Dialog on top of other dialogs.
- * Calling this will always display the dialog on top of all other dialogs
- * previously called using this method.
- *
- * Be aware the on start action can be called multiple times for some situations,
- * such as the user switching apps without dismissing the dialog then switching back to this app.
- *
- * This method is only useful during app startup and multiple patches may show their own dialog,
- * and the most important dialog can be called last (using a delay) so it's always on top.
- *
- * For all other situations it's better to not use this method and
- * call {@link Dialog#show()} on the dialog.
- */
- @SuppressWarnings("deprecation")
- public static void showDialog(Activity activity,
- Dialog dialog,
- boolean isCancelable,
- @Nullable DialogFragmentOnStartAction onStartAction) {
- verifyOnMainThread();
-
- DialogFragmentWrapper fragment = new DialogFragmentWrapper();
- fragment.dialog = dialog;
- fragment.onStartAction = onStartAction;
- fragment.setCancelable(isCancelable);
-
- fragment.show(activity.getFragmentManager(), null);
- }
-
- /**
- * Safe to call from any thread.
- */
- public static void showToastShort(String messageToToast) {
- showToast(messageToToast, Toast.LENGTH_SHORT);
- }
-
- /**
- * Safe to call from any thread.
- */
- public static void showToastLong(String messageToToast) {
- showToast(messageToToast, Toast.LENGTH_LONG);
- }
-
- /**
- * Safe to call from any thread.
- *
- * @param messageToToast Message to show.
- * @param toastDuration Either {@link Toast#LENGTH_SHORT} or {@link Toast#LENGTH_LONG}.
- */
- public static void showToast(String messageToToast, int toastDuration) {
- Objects.requireNonNull(messageToToast);
- runOnMainThreadNowOrLater(() -> {
- Context currentContext = context;
-
- if (currentContext == null) {
- Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast);
- } else {
- Logger.printDebug(() -> "Showing toast: " + messageToToast);
- Toast.makeText(currentContext, messageToToast, toastDuration).show();
- }
- });
- }
-
- /**
- * @return The current dark mode as set by any patch.
- * Or if none is set, then the system dark mode status is returned.
- */
- public static boolean isDarkModeEnabled() {
- Boolean isDarkMode = isDarkModeEnabled;
- if (isDarkMode != null) {
- return isDarkMode;
- }
-
- Configuration config = Resources.getSystem().getConfiguration();
- final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
- return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
- }
-
- /**
- * Overrides dark mode status as returned by {@link #isDarkModeEnabled()}.
- */
- public static void setIsDarkModeEnabled(boolean isDarkMode) {
- isDarkModeEnabled = isDarkMode;
- Logger.printDebug(() -> "Dark mode status: " + isDarkMode);
- }
-
- public static boolean isLandscapeOrientation() {
- final int orientation = Resources.getSystem().getConfiguration().orientation;
- return orientation == Configuration.ORIENTATION_LANDSCAPE;
- }
-
- /**
- * Automatically logs any exceptions the runnable throws.
- *
- * @see #runOnMainThreadNowOrLater(Runnable)
- */
- public static void runOnMainThread(Runnable runnable) {
- runOnMainThreadDelayed(runnable, 0);
- }
-
- /**
- * Automatically logs any exceptions the runnable throws.
- */
- public static void runOnMainThreadDelayed(Runnable runnable, long delayMillis) {
- Runnable loggingRunnable = () -> {
- try {
- runnable.run();
- } catch (Exception ex) {
- Logger.printException(() -> runnable.getClass().getSimpleName() + ": " + ex.getMessage(), ex);
- }
- };
- new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis);
- }
-
- /**
- * If called from the main thread, the code is run immediately.
- * If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}.
- */
- public static void runOnMainThreadNowOrLater(Runnable runnable) {
- if (isCurrentlyOnMainThread()) {
- runnable.run();
- } else {
- runOnMainThread(runnable);
- }
- }
-
- /**
- * @return if the calling thread is on the main thread.
- */
- public static boolean isCurrentlyOnMainThread() {
- return Looper.getMainLooper().isCurrentThread();
- }
-
- /**
- * @throws IllegalStateException if the calling thread is _off_ the main thread.
- */
- public static void verifyOnMainThread() throws IllegalStateException {
- if (!isCurrentlyOnMainThread()) {
- throw new IllegalStateException("Must call _on_ the main thread");
- }
- }
-
- /**
- * @throws IllegalStateException if the calling thread is _on_ the main thread.
- */
- public static void verifyOffMainThread() throws IllegalStateException {
- if (isCurrentlyOnMainThread()) {
- throw new IllegalStateException("Must call _off_ the main thread");
- }
- }
-
- public static void openLink(String url) {
- try {
- Intent intent = new Intent("android.intent.action.VIEW", Uri.parse(url));
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
- Logger.printInfo(() -> "Opening link with external browser: " + intent);
- getContext().startActivity(intent);
- } catch (Exception ex) {
- Logger.printException(() -> "openLink failure", ex);
- }
- }
-
- public enum NetworkType {
- NONE,
- MOBILE,
- OTHER,
- }
-
- /**
- * Calling extension code must ensure the un-patched app has the permission
- * android.permission.ACCESS_NETWORK_STATE,
- * otherwise the app will crash if this method is used.
- */
- public static boolean isNetworkConnected() {
- NetworkType networkType = getNetworkType();
- return networkType == NetworkType.MOBILE
- || networkType == NetworkType.OTHER;
- }
-
- /**
- * Calling extension code must ensure the un-patched app has the permission
- * android.permission.ACCESS_NETWORK_STATE,
- * otherwise the app will crash if this method is used.
- */
- @SuppressWarnings({"MissingPermission", "deprecation"})
- public static NetworkType getNetworkType() {
- Context networkContext = getContext();
- if (networkContext == null) {
- return NetworkType.NONE;
- }
- ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- var networkInfo = cm.getActiveNetworkInfo();
-
- if (networkInfo == null || !networkInfo.isConnected()) {
- return NetworkType.NONE;
- }
- var type = networkInfo.getType();
- return (type == ConnectivityManager.TYPE_MOBILE)
- || (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER;
- }
-
- /**
- * 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(@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 {
- params.width = 0;
- params.height = 0;
- }
-
- view.setLayoutParams(params);
- }
-
- /**
- * 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 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.
- */
- public static void setDialogWindowParameters(Window window, int gravity, int yOffsetDip, int widthPercentage, boolean dimAmount) {
- WindowManager.LayoutParams params = window.getAttributes();
-
- params.width = Dim.pctPortraitWidth(widthPercentage);
- params.height = WindowManager.LayoutParams.WRAP_CONTENT;
- params.gravity = gravity;
- params.y = yOffsetDip > 0 ? Dim.dp(yOffsetDip) : 0;
- if (dimAmount) {
- params.dimAmount = 0f;
- }
-
- window.setAttributes(params); // Apply window attributes.
- window.setBackgroundDrawable(null); // Remove default dialog background
- }
-
- /**
- * @return If the unpatched app is currently using bold icons.
- */
- public static boolean appIsUsingBoldIcons() {
- return appIsUsingBoldIcons;
- }
-
- /**
- * Controls if ReVanced bold icons are shown in various places.
- * @param boldIcons If the app is currently using bold icons.
- */
- public static void setAppIsUsingBoldIcons(boolean boldIcons) {
- appIsUsingBoldIcons = boldIcons;
- }
-
- /**
- * Sets the theme light color used by the app.
- */
- public static void setThemeLightColor(@ColorInt int color) {
- Logger.printDebug(() -> "Setting theme light color: " + getColorHexString(color));
- lightColor = color;
- }
-
- /**
- * Sets the theme dark used by the app.
- */
- public static void setThemeDarkColor(@ColorInt int color) {
- Logger.printDebug(() -> "Setting theme dark color: " + getColorHexString(color));
- darkColor = color;
- }
-
- /**
- * Returns the themed light color, or {@link Color#WHITE} if no theme was set using
- * {@link #setThemeLightColor(int).
- */
- @ColorInt
- public static int getThemeLightColor() {
- return lightColor;
- }
-
- /**
- * Returns the themed dark color, or {@link Color#BLACK} if no theme was set using
- * {@link #setThemeDarkColor(int)}.
- */
- @ColorInt
- public static int getThemeDarkColor() {
- return darkColor;
- }
-
- /**
- * Injection point.
- */
- @SuppressWarnings("SameReturnValue")
- private static String getThemeLightColorResourceName() {
- // Value is changed by Settings patch.
- return "#FFFFFFFF";
- }
-
- /**
- * Injection point.
- */
- @SuppressWarnings("SameReturnValue")
- private static String getThemeDarkColorResourceName() {
- // Value is changed by Settings patch.
- return "#FF000000";
- }
-
- @ColorInt
- private static int getThemeColor(String resourceName, int defaultColor) {
- try {
- return getColorFromString(resourceName);
- } catch (Exception ex) {
- // This code can never be reached since a bad custom color will
- // fail during resource compilation. So no localized strings are needed here.
- Logger.printException(() -> "Invalid custom theme color: " + resourceName, ex);
- return defaultColor;
- }
- }
-
-
- @ColorInt
- public static int getDialogBackgroundColor() {
- if (isDarkModeEnabled()) {
- final int darkColor = getThemeDarkColor();
- return darkColor == Color.BLACK
- // Lighten the background a little if using AMOLED dark theme
- // as the dialogs are almost invisible.
- ? 0xFF080808 // 3%
- : darkColor;
- }
- return getThemeLightColor();
- }
-
- /**
- * @return The current app background color.
- */
- @ColorInt
- public static int getAppBackgroundColor() {
- return isDarkModeEnabled() ? getThemeDarkColor() : getThemeLightColor();
- }
-
- /**
- * @return The current app foreground color.
- */
- @ColorInt
- public static int getAppForegroundColor() {
- return isDarkModeEnabled()
- ? getThemeLightColor()
- : getThemeDarkColor();
- }
-
- @ColorInt
- public static int getOkButtonBackgroundColor() {
- return isDarkModeEnabled()
- // Must be inverted color.
- ? Color.WHITE
- : Color.BLACK;
- }
-
- @ColorInt
- public static int getCancelOrNeutralButtonBackgroundColor() {
- return isDarkModeEnabled()
- ? adjustColorBrightness(getDialogBackgroundColor(), 1.10f)
- : adjustColorBrightness(getThemeLightColor(), 0.95f);
- }
-
- @ColorInt
- public static int getEditTextBackground() {
- return isDarkModeEnabled()
- ? adjustColorBrightness(getDialogBackgroundColor(), 1.05f)
- : adjustColorBrightness(getThemeLightColor(), 0.97f);
- }
-
- public static String getColorHexString(@ColorInt int color) {
- return String.format("#%06X", (0x00FFFFFF & color));
- }
-
- /**
- * {@link PreferenceScreen} and {@link PreferenceGroup} sorting styles.
- */
- private enum Sort {
- /**
- * Sort by the localized preference title.
- */
- BY_TITLE("_sort_by_title"),
-
- /**
- * Sort by the preference keys.
- */
- BY_KEY("_sort_by_key"),
-
- /**
- * Unspecified sorting.
- */
- UNSORTED("_sort_by_unsorted");
-
- final String keySuffix;
-
- Sort(String keySuffix) {
- this.keySuffix = keySuffix;
- }
-
- static Sort fromKey(@Nullable String key, Sort defaultSort) {
- if (key != null) {
- for (Sort sort : values()) {
- if (key.endsWith(sort.keySuffix)) {
- return sort;
- }
- }
- }
- return defaultSort;
- }
- }
-
- /**
- * 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 PUNCTUATION_PATTERN.matcher(original).replaceAll("")
- .toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale());
- }
-
- /**
- * 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.
- *
- * 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.
- *
- * 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);
-
- final Sort preferenceSort;
- if (preference instanceof PreferenceGroup subGroup) {
- sortPreferenceGroups(subGroup);
- preferenceSort = groupSort; // Sort value for groups is for it's content, not itself.
- } else {
- // Allow individual preferences to set a key sorting.
- // Used to force a preference to the top or bottom of a group.
- preferenceSort = Sort.fromKey(preference.getKey(), groupSort);
- }
-
- final String sortValue;
- switch (preferenceSort) {
- case BY_TITLE:
- sortValue = removePunctuationToLowercase(preference.getTitle());
- break;
- case BY_KEY:
- sortValue = preference.getKey();
- break;
- case UNSORTED:
- continue; // Keep original sorting.
- default:
- throw new IllegalStateException();
- }
-
- preferences.add(new Pair<>(sortValue, preference));
- }
-
- // Sort the list using locale-specific collation rules.
- Collections.sort(preferences, (pair1, pair2)
- -> 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++;
- Preference pref = pair.second;
-
- // Move any screens, intents, and the one off About preference to the top.
- if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference
- || pref.getIntent() != null) {
- // Any arbitrary large number.
- order -= 1000;
- }
-
- pref.setOrder(order);
- }
- }
-
- /**
- * Set all preferences to multiline titles if the device is not using an English variant.
- * The English strings are heavily scrutinized and all titles fit on screen
- * except 2 or 3 preference strings and those do not affect readability.
- *
- * Allowing multiline for those 2 or 3 English preferences looks weird and out of place,
- * and visually it looks better to clip the text and keep all titles 1 line.
- */
- @SuppressWarnings("deprecation")
- public static void setPreferenceTitlesToMultiLineIfNeeded(PreferenceGroup group) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
- return;
- }
-
- String revancedLocale = Utils.getContext().getResources().getConfiguration().locale.getLanguage();
- if (revancedLocale.equals(Locale.ENGLISH.getLanguage())) {
- return;
- }
-
- for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
- Preference pref = group.getPreference(i);
- pref.setSingleLineTitle(false);
-
- if (pref instanceof PreferenceGroup subGroup) {
- setPreferenceTitlesToMultiLineIfNeeded(subGroup);
- }
- }
- }
-
- /**
- * Parse a color resource or hex code to an int representation of the color.
- */
- @ColorInt
- public static int getColorFromString(String colorString) throws IllegalArgumentException, Resources.NotFoundException {
- if (colorString.startsWith("#")) {
- return Color.parseColor(colorString);
- }
- return getResourceColor(colorString);
- }
-
- /**
- * Uses {@link #adjustColorBrightness(int, float)} depending on if light or dark mode is active.
- */
- @ColorInt
- public static int adjustColorBrightness(@ColorInt int baseColor, float lightThemeFactor, float darkThemeFactor) {
- return isDarkModeEnabled()
- ? adjustColorBrightness(baseColor, darkThemeFactor)
- : adjustColorBrightness(baseColor, lightThemeFactor);
- }
-
- /**
- * Adjusts the brightness of a color by lightening or darkening it based on the given factor.
- *
- * If the factor is greater than 1, the color is lightened by interpolating toward white (#FFFFFF).
- * If the factor is less than or equal to 1, the color is darkened by scaling its RGB components toward black (#000000).
- * The alpha channel remains unchanged.
- *
- * @param color The input color to adjust, in ARGB format.
- * @param factor The adjustment factor. Use values > 1.0f to lighten (e.g., 1.11f for slight lightening)
- * or values <= 1.0f to darken (e.g., 0.95f for slight darkening).
- * @return The adjusted color in ARGB format.
- */
- @ColorInt
- public static int adjustColorBrightness(@ColorInt int color, float factor) {
- final int alpha = Color.alpha(color);
- int red = Color.red(color);
- int green = Color.green(color);
- int blue = Color.blue(color);
-
- if (factor > 1.0f) {
- // Lighten: Interpolate toward white (255).
- final float t = 1.0f - (1.0f / factor); // Interpolation parameter.
- red = Math.round(red + (255 - red) * t);
- green = Math.round(green + (255 - green) * t);
- blue = Math.round(blue + (255 - blue) * t);
- } else {
- // Darken or no change: Scale toward black.
- red = Math.round(red * factor);
- green = Math.round(green * factor);
- blue = Math.round(blue * factor);
- }
-
- // Ensure values are within [0, 255].
- red = clamp(red, 0, 255);
- green = clamp(green, 0, 255);
- blue = clamp(blue, 0, 255);
-
- return Color.argb(alpha, red, green, blue);
- }
-
- public static int clamp(int value, int lower, int upper) {
- return Math.max(lower, Math.min(value, upper));
- }
-
- public static float clamp(float value, float lower, float upper) {
- return Math.max(lower, Math.min(value, upper));
- }
-
- /**
- * @param maxSize The maximum number of elements to keep in the map.
- * @return A {@link LinkedHashMap} that automatically evicts the oldest entry
- * when the size exceeds {@code maxSize}.
- */
- public static Map createSizeRestrictedMap(int maxSize) {
- return new LinkedHashMap<>(2 * maxSize) {
- @Override
- protected boolean removeEldestEntry(Entry eldest) {
- return size() > maxSize;
- }
- };
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java
deleted file mode 100644
index bde66a043c..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java
+++ /dev/null
@@ -1,215 +0,0 @@
-package app.revanced.extension.shared.checks;
-
-import static android.text.Html.FROM_HTML_MODE_COMPACT;
-import static app.revanced.extension.shared.StringRef.str;
-import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.Intent;
-import android.graphics.PorterDuff;
-import android.net.Uri;
-import android.os.Build;
-import android.text.Html;
-import android.util.Pair;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
-import java.util.Collection;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.ui.CustomDialog;
-
-@RequiresApi(api = Build.VERSION_CODES.N)
-abstract class Check {
- private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
-
- private static final int SECONDS_BEFORE_SHOWING_IGNORE_BUTTON = 15;
- private static final int SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON = 10;
-
- private static final Uri GOOD_SOURCE = Uri.parse("https://revanced.app");
-
- /**
- * @return If the check conclusively passed or failed. A null value indicates it neither passed nor failed.
- */
- @Nullable
- protected abstract Boolean check();
-
- protected abstract String failureReason();
-
- /**
- * Specifies a sorting order for displaying the checks that failed.
- * A lower value indicates to show first before other checks.
- */
- public abstract int uiSortingValue();
-
- /**
- * For debugging and development only.
- * Forces all checks to be performed and the check failed dialog to be shown.
- * Can be enabled by importing settings text with {@link BaseSettings#CHECK_ENVIRONMENT_WARNINGS_ISSUED}
- * set to -1.
- */
- static boolean debugAlwaysShowWarning() {
- final boolean alwaysShowWarning = BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0;
- if (alwaysShowWarning) {
- Logger.printInfo(() -> "Debug forcing environment check warning to show");
- }
-
- return alwaysShowWarning;
- }
-
- static boolean shouldRun() {
- return BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()
- < NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING;
- }
-
- static void disableForever() {
- Logger.printInfo(() -> "Environment checks disabled forever");
-
- BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
- }
-
- static void issueWarning(Activity activity, Collection failedChecks) {
- final var reasons = new StringBuilder();
-
- reasons.append("");
- for (var check : failedChecks) {
- // Add a non breaking space to fix bullet points spacing issue.
- reasons.append(" ").append(check.failureReason());
- }
- reasons.append(" ");
-
- var message = Html.fromHtml(
- str("revanced_check_environment_failed_message", reasons.toString()),
- FROM_HTML_MODE_COMPACT
- );
-
- Utils.runOnMainThreadDelayed(() -> {
- // Create the custom dialog.
- Pair dialogPair = CustomDialog.create(
- activity,
- str("revanced_check_environment_failed_title"), // Title.
- message, // Message.
- null, // No EditText.
- str("revanced_check_environment_dialog_open_official_source_button"), // OK button text.
- () -> {
- // Action for the OK (website) button.
- final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- activity.startActivity(intent);
-
- // Shutdown to prevent the user from navigating back to this app,
- // which is no longer showing a warning dialog.
- activity.finishAffinity();
- System.exit(0);
- },
- null, // No cancel button.
- str("revanced_check_environment_dialog_ignore_button"), // Neutral button text.
- () -> {
- // Neutral button action.
- // Cleanup data if the user incorrectly imported a huge negative number.
- final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
- BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
- },
- true // Dismiss dialog when onNeutralClick.
- );
-
- // Get the dialog and main layout.
- Dialog dialog = dialogPair.first;
- LinearLayout mainLayout = dialogPair.second;
-
- // Add icon to the dialog.
- ImageView iconView = new ImageView(activity);
- iconView.setImageResource(Utils.getResourceIdentifierOrThrow(
- ResourceType.DRAWABLE, "revanced_ic_dialog_alert"));
- iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
- iconView.setPadding(0, 0, 0, 0);
- LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- );
- iconParams.gravity = Gravity.CENTER;
- mainLayout.addView(iconView, 0); // Add icon at the top.
-
- dialog.setCancelable(false);
-
- // Show the dialog.
- Utils.showDialog(activity, dialog, false, new DialogFragmentOnStartAction() {
- boolean hasRun;
- @Override
- public void onStart(Dialog dialog) {
- // Only run this once, otherwise if the user changes to a different app
- // then changes back, this handler will run again and disable the buttons.
- if (hasRun) {
- return;
- }
- hasRun = true;
-
- // Get the button container to access buttons.
- LinearLayout buttonContainer = (LinearLayout) mainLayout.getChildAt(mainLayout.getChildCount() - 1);
-
- Button openWebsiteButton;
- Button ignoreButton;
-
- // Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer).
- if (buttonContainer.getChildCount() == 1
- && buttonContainer.getChildAt(0) instanceof LinearLayout rowContainer) {
- // Neutral button is the first child (index 0).
- ignoreButton = (Button) rowContainer.getChildAt(0);
- // OK button is the last child.
- openWebsiteButton = (Button) rowContainer.getChildAt(rowContainer.getChildCount() - 1);
- } else {
- // Multi-row layout: buttons are in separate containers, ordered OK, Cancel, Neutral.
- LinearLayout okContainer =
- (LinearLayout) buttonContainer.getChildAt(0); // OK is first.
- openWebsiteButton = (Button) okContainer.getChildAt(0);
- LinearLayout neutralContainer =
- (LinearLayout)buttonContainer.getChildAt(buttonContainer.getChildCount() - 1); // Neutral is last.
- ignoreButton = (Button) neutralContainer.getChildAt(0);
- }
-
- // Initially set buttons to INVISIBLE and disabled.
- openWebsiteButton.setVisibility(View.INVISIBLE);
- openWebsiteButton.setEnabled(false);
- ignoreButton.setVisibility(View.INVISIBLE);
- ignoreButton.setEnabled(false);
-
- // Start the countdown for showing and enabling buttons.
- getCountdownRunnable(ignoreButton, openWebsiteButton).run();
- }
- });
- }, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs.
- }
-
- private static Runnable getCountdownRunnable(Button ignoreButton, Button openWebsiteButton) {
- return new Runnable() {
- private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON;
-
- @Override
- public void run() {
- Utils.verifyOnMainThread();
-
- if (secondsRemaining > 0) {
- if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON <= 0) {
- openWebsiteButton.setVisibility(View.VISIBLE);
- openWebsiteButton.setEnabled(true);
- }
- secondsRemaining--;
- Utils.runOnMainThreadDelayed(this, 1000);
- } else {
- ignoreButton.setVisibility(View.VISIBLE);
- ignoreButton.setEnabled(true);
- }
- }
- };
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java
deleted file mode 100644
index e54ab27f74..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java
+++ /dev/null
@@ -1,347 +0,0 @@
-package app.revanced.extension.shared.checks;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.util.Base64;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.*;
-
-import static app.revanced.extension.shared.StringRef.str;
-import static app.revanced.extension.shared.checks.Check.debugAlwaysShowWarning;
-import static app.revanced.extension.shared.checks.PatchInfo.Build.*;
-
-/**
- * This class is used to check if the app was patched by the user
- * and not downloaded pre-patched, because pre-patched apps are difficult to trust.
- *
- * Various indicators help to detect if the app was patched by the user.
- */
-@RequiresApi(api = Build.VERSION_CODES.N)
-@SuppressWarnings("unused")
-public final class CheckEnvironmentPatch {
- private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning();
-
- private enum InstallationType {
- /**
- * CLI patching, manual installation of a previously patched using adb,
- * or root installation if stock app is first installed using adb.
- */
- ADB((String) null),
- ROOT_MOUNT_ON_APP_STORE("com.android.vending"),
- MANAGER("app.revanced.manager.flutter",
- "app.revanced.manager.flutter.debug",
- "app.revanced.manager",
- "app.revanced.manager.debug");
-
- @Nullable
- static InstallationType installTypeFromPackageName(@Nullable String packageName) {
- for (InstallationType type : values()) {
- for (String installPackageName : type.packageNames) {
- if (Objects.equals(installPackageName, packageName)) {
- return type;
- }
- }
- }
-
- return null;
- }
-
- /**
- * Array elements can be null.
- */
- final String[] packageNames;
-
- InstallationType(String... packageNames) {
- this.packageNames = packageNames;
- }
- }
-
- /**
- * Check if the app is installed by the manager, the app store, or through adb/CLI.
- *
- * Does not conclusively
- * If the app is installed by the manager or the app store, it is likely, the app was patched using the manager,
- * or installed manually via ADB (in the case of ReVanced CLI for example).
- *
- * If the app is not installed by the manager or the app store, then the app was likely downloaded pre-patched
- * and installed by the browser or another unknown app.
- */
- private static class CheckExpectedInstaller extends Check {
- @Nullable
- InstallationType installerFound;
-
- @NonNull
- @Override
- protected Boolean check() {
- final var context = Utils.getContext();
-
- final var installerPackageName =
- context.getPackageManager().getInstallerPackageName(context.getPackageName());
-
- Logger.printInfo(() -> "Installed by: " + installerPackageName);
-
- installerFound = InstallationType.installTypeFromPackageName(installerPackageName);
- final boolean passed = (installerFound != null);
-
- Logger.printInfo(() -> passed
- ? "Apk was not installed from an unknown source"
- : "Apk was installed from an unknown source");
-
- return passed;
- }
-
- @Override
- protected String failureReason() {
- return str("revanced_check_environment_manager_not_expected_installer");
- }
-
- @Override
- public int uiSortingValue() {
- return -100; // Show first.
- }
- }
-
- /**
- * Check if the build properties are the same as during the patch.
- *
- * If the build properties are the same as during the patch, it is likely, the app was patched on the same device.
- *
- * If the build properties are different, the app was likely downloaded pre-patched or patched on another device.
- */
- private static class CheckWasPatchedOnSameDevice extends Check {
- @SuppressLint("HardwareIds")
- @Override
- protected Boolean check() {
- if (PATCH_BOARD.isEmpty()) {
- // Did not patch with Manager, and cannot conclusively say where this was from.
- Logger.printInfo(() -> "APK does not contain a hardware signature and cannot compare to current device");
- return null;
- }
-
- //noinspection deprecation
- final var passed = buildFieldEqualsHash("BOARD", Build.BOARD, PATCH_BOARD) &
- buildFieldEqualsHash("BOOTLOADER", Build.BOOTLOADER, PATCH_BOOTLOADER) &
- buildFieldEqualsHash("BRAND", Build.BRAND, PATCH_BRAND) &
- buildFieldEqualsHash("CPU_ABI", Build.CPU_ABI, PATCH_CPU_ABI) &
- buildFieldEqualsHash("CPU_ABI2", Build.CPU_ABI2, PATCH_CPU_ABI2) &
- buildFieldEqualsHash("DEVICE", Build.DEVICE, PATCH_DEVICE) &
- buildFieldEqualsHash("DISPLAY", Build.DISPLAY, PATCH_DISPLAY) &
- buildFieldEqualsHash("FINGERPRINT", Build.FINGERPRINT, PATCH_FINGERPRINT) &
- buildFieldEqualsHash("HARDWARE", Build.HARDWARE, PATCH_HARDWARE) &
- buildFieldEqualsHash("HOST", Build.HOST, PATCH_HOST) &
- buildFieldEqualsHash("ID", Build.ID, PATCH_ID) &
- buildFieldEqualsHash("MANUFACTURER", Build.MANUFACTURER, PATCH_MANUFACTURER) &
- buildFieldEqualsHash("MODEL", Build.MODEL, PATCH_MODEL) &
- buildFieldEqualsHash("PRODUCT", Build.PRODUCT, PATCH_PRODUCT) &
- buildFieldEqualsHash("RADIO", Build.RADIO, PATCH_RADIO) &
- buildFieldEqualsHash("TAGS", Build.TAGS, PATCH_TAGS) &
- buildFieldEqualsHash("TYPE", Build.TYPE, PATCH_TYPE) &
- buildFieldEqualsHash("USER", Build.USER, PATCH_USER);
-
- Logger.printInfo(() -> passed
- ? "Device hardware signature matches current device"
- : "Device hardware signature does not match current device");
-
- return passed;
- }
-
- @Override
- protected String failureReason() {
- return str("revanced_check_environment_not_same_patching_device");
- }
-
- @Override
- public int uiSortingValue() {
- return 0; // Show in the middle.
- }
- }
-
- /**
- * Check if the app was installed within the last 30 minutes after being patched.
- *
- * If the app was installed within the last 30 minutes, it is likely, the app was patched by the user.
- *
- * If the app was installed much later than the patch time, it is likely the app was
- * downloaded pre-patched or the user waited too long to install the app.
- */
- private static class CheckIsNearPatchTime extends Check {
- /**
- * How soon after patching the app must be installed to pass.
- */
- static final int INSTALL_AFTER_PATCHING_DURATION_THRESHOLD = 30 * 60 * 1000; // 30 minutes.
-
- /**
- * Milliseconds between the time the app was patched, and when it was installed/updated.
- */
- long durationBetweenPatchingAndInstallation;
-
- @NonNull
- @Override
- protected Boolean check() {
- try {
- Context context = Utils.getContext();
- PackageManager packageManager = context.getPackageManager();
- PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
-
- // Duration since initial install or last update, whichever is sooner.
- durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME;
- Logger.printInfo(() -> "App was installed/updated: "
- + (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching"));
-
- if (durationBetweenPatchingAndInstallation < 0) {
- // Patch time is in the future and clearly wrong.
- return false;
- }
-
- if (durationBetweenPatchingAndInstallation < INSTALL_AFTER_PATCHING_DURATION_THRESHOLD) {
- return true;
- }
- } catch (PackageManager.NameNotFoundException ex) {
- Logger.printException(() -> "Package name not found exception", ex); // Will never happen.
- }
-
- // User installed more than 30 minutes after patching.
- return false;
- }
-
- @Override
- protected String failureReason() {
- if (durationBetweenPatchingAndInstallation < 0) {
- // Could happen if the user has their device clock incorrectly set in the past,
- // but assume that isn't the case and the apk was patched on a device with the wrong system time.
- return str("revanced_check_environment_not_near_patch_time_invalid");
- }
-
- // If patched over 1 day ago, show how old this pre-patched apk is.
- // Showing the age can help convey it's better to patch yourself and know it's the latest.
- final long oneDay = 24 * 60 * 60 * 1000;
- final long daysSincePatching = durationBetweenPatchingAndInstallation / oneDay;
- if (daysSincePatching > 1) { // Use over 1 day to avoid singular vs plural strings.
- return str("revanced_check_environment_not_near_patch_time_days", daysSincePatching);
- }
-
- return str("revanced_check_environment_not_near_patch_time");
- }
-
- @Override
- public int uiSortingValue() {
- return 100; // Show last.
- }
- }
-
- /**
- * Injection point.
- */
- public static void check(Activity context) {
- // If the warning was already issued twice, or if the check was successful in the past,
- // do not run the checks again.
- if (!Check.shouldRun() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
- Logger.printDebug(() -> "Environment checks are disabled");
- return;
- }
-
- Utils.runOnBackgroundThread(() -> {
- try {
- Logger.printInfo(() -> "Running environment checks");
- List failedChecks = new ArrayList<>();
-
- CheckWasPatchedOnSameDevice sameHardware = new CheckWasPatchedOnSameDevice();
- Boolean hardwareCheckPassed = sameHardware.check();
- if (hardwareCheckPassed != null) {
- if (hardwareCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
- // Patched on the same device using Manager,
- // and no further checks are needed.
- Check.disableForever();
- return;
- }
-
- failedChecks.add(sameHardware);
- }
-
- CheckExpectedInstaller installerCheck = new CheckExpectedInstaller();
- if (installerCheck.check() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
- // If the installer package is Manager but this code is reached,
- // that means it must not be the right Manager otherwise the hardware hash
- // signatures would be present and this check would not have run.
- if (installerCheck.installerFound == InstallationType.MANAGER) {
- failedChecks.add(installerCheck);
- // Also could not have been patched on this device.
- failedChecks.add(sameHardware);
- } else if (failedChecks.isEmpty()) {
- // ADB install of CLI build. Allow even if patched a long time ago.
- Check.disableForever();
- return;
- }
- } else {
- failedChecks.add(installerCheck);
- }
-
- CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime();
- Boolean timeCheckPassed = nearPatchTime.check();
- if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
- // Allow installing recently patched APKs,
- // even if the installation source is not Manager or ADB.
- Check.disableForever();
- return;
- } else {
- failedChecks.add(nearPatchTime);
- }
-
- if (DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
- // Show all failures for debugging layout.
- failedChecks = Arrays.asList(
- sameHardware,
- nearPatchTime,
- installerCheck
- );
- }
-
- //noinspection ComparatorCombinators
- Collections.sort(failedChecks, (o1, o2) -> o1.uiSortingValue() - o2.uiSortingValue());
-
- Check.issueWarning(
- context,
- failedChecks
- );
- } catch (Exception ex) {
- Logger.printException(() -> "check failure", ex);
- }
- });
- }
-
- private static boolean buildFieldEqualsHash(String buildFieldName, String buildFieldValue, @Nullable String hash) {
- try {
- final var sha1 = MessageDigest.getInstance("SHA-1")
- .digest(buildFieldValue.getBytes(StandardCharsets.UTF_8));
-
- // Must be careful to use same base64 encoding Kotlin uses.
- String runtimeHash = new String(Base64.encode(sha1, Base64.NO_WRAP), StandardCharsets.ISO_8859_1);
- final boolean equals = runtimeHash.equals(hash);
- if (!equals) {
- Logger.printInfo(() -> "Hashes do not match. " + buildFieldName + ": '" + buildFieldValue
- + "' runtimeHash: '" + runtimeHash + "' patchTimeHash: '" + hash + "'");
- }
-
- return equals;
- } catch (NoSuchAlgorithmException ex) {
- Logger.printException(() -> "buildFieldEqualsHash failure", ex); // Will never happen.
-
- return false;
- }
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/PatchInfo.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/PatchInfo.java
deleted file mode 100644
index cceb34f779..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/PatchInfo.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package app.revanced.extension.shared.checks;
-
-/**
- * Fields are set by the patch. Do not modify.
- * Fields are not final, because the compiler is inlining them.
- *
- * @noinspection CanBeFinal
- */
-final class PatchInfo {
- static long PATCH_TIME = 0L;
-
- final static class Build {
- static String PATCH_BOARD = "";
- static String PATCH_BOOTLOADER = "";
- static String PATCH_BRAND = "";
- static String PATCH_CPU_ABI = "";
- static String PATCH_CPU_ABI2 = "";
- static String PATCH_DEVICE = "";
- static String PATCH_DISPLAY = "";
- static String PATCH_FINGERPRINT = "";
- static String PATCH_HARDWARE = "";
- static String PATCH_HOST = "";
- static String PATCH_ID = "";
- static String PATCH_MANUFACTURER = "";
- static String PATCH_MODEL = "";
- static String PATCH_PRODUCT = "";
- static String PATCH_RADIO = "";
- static String PATCH_TAGS = "";
- static String PATCH_TYPE = "";
- static String PATCH_USER = "";
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/redgifs/BaseFixRedgifsApiPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/redgifs/BaseFixRedgifsApiPatch.java
deleted file mode 100644
index 00ee6def3b..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/redgifs/BaseFixRedgifsApiPatch.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package app.revanced.extension.shared.fixes.redgifs;
-
-import androidx.annotation.NonNull;
-
-import org.json.JSONException;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-
-import app.revanced.extension.shared.Logger;
-import okhttp3.Interceptor;
-import okhttp3.MediaType;
-import okhttp3.Protocol;
-import okhttp3.Request;
-import okhttp3.Response;
-import okhttp3.ResponseBody;
-
-public abstract class BaseFixRedgifsApiPatch implements Interceptor {
- protected static BaseFixRedgifsApiPatch INSTANCE;
- public abstract String getDefaultUserAgent();
-
- @NonNull
- @Override
- public Response intercept(@NonNull Chain chain) throws IOException {
- Request request = chain.request();
- if (!request.url().host().equals("api.redgifs.com")) {
- return chain.proceed(request);
- }
-
- String userAgent = getDefaultUserAgent();
-
- if (request.header("Authorization") != null) {
- Response response = chain.proceed(request.newBuilder().header("User-Agent", userAgent).build());
- if (response.isSuccessful()) {
- return response;
- }
- // It's possible that the user agent is being overwritten later down in the interceptor
- // chain, so make sure we grab the new user agent from the request headers.
- userAgent = response.request().header("User-Agent");
- response.close();
- }
-
- try {
- RedgifsTokenManager.RedgifsToken token = RedgifsTokenManager.refreshToken(userAgent);
-
- // Emulate response for old OAuth endpoint
- if (request.url().encodedPath().equals("/v2/oauth/client")) {
- String responseBody = RedgifsTokenManager.getEmulatedOAuthResponseBody(token);
- return new Response.Builder()
- .message("OK")
- .code(HttpURLConnection.HTTP_OK)
- .protocol(Protocol.HTTP_1_1)
- .request(request)
- .header("Content-Type", "application/json")
- .body(ResponseBody.create(
- responseBody, MediaType.get("application/json")))
- .build();
- }
-
- Request modifiedRequest = request.newBuilder()
- .header("Authorization", "Bearer " + token.getAccessToken())
- .header("User-Agent", userAgent)
- .build();
- return chain.proceed(modifiedRequest);
- } catch (JSONException ex) {
- Logger.printException(() -> "Could not parse Redgifs response", ex);
- throw new IOException(ex);
- }
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/redgifs/RedgifsTokenManager.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/redgifs/RedgifsTokenManager.java
deleted file mode 100644
index 792465a89f..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/redgifs/RedgifsTokenManager.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package app.revanced.extension.shared.fixes.redgifs;
-
-import static app.revanced.extension.shared.requests.Route.Method.GET;
-
-import androidx.annotation.GuardedBy;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-import app.revanced.extension.shared.requests.Requester;
-
-
-/**
- * Manages Redgifs token lifecycle.
- */
-public class RedgifsTokenManager {
- public static class RedgifsToken {
- // Expire after 23 hours to provide some breathing room
- private static final long EXPIRY_SECONDS = 23 * 60 * 60;
-
- private final String accessToken;
- private final long refreshTimeInSeconds;
-
- public RedgifsToken(String accessToken, long refreshTime) {
- this.accessToken = accessToken;
- this.refreshTimeInSeconds = refreshTime;
- }
-
- public String getAccessToken() {
- return accessToken;
- }
-
- public long getExpiryTimeInSeconds() {
- return refreshTimeInSeconds + EXPIRY_SECONDS;
- }
-
- public boolean isValid() {
- if (accessToken == null) return false;
- return getExpiryTimeInSeconds() >= System.currentTimeMillis() / 1000;
- }
- }
- public static final String REDGIFS_API_HOST = "https://api.redgifs.com";
- private static final String GET_TEMPORARY_TOKEN = REDGIFS_API_HOST + "/v2/auth/temporary";
- @GuardedBy("itself")
- private static final Map tokenMap = new HashMap<>();
-
- private static String getToken(String userAgent) throws IOException, JSONException {
- HttpURLConnection connection = (HttpURLConnection) new URL(GET_TEMPORARY_TOKEN).openConnection();
- connection.setFixedLengthStreamingMode(0);
- connection.setRequestMethod(GET.name());
- connection.setRequestProperty("User-Agent", userAgent);
- connection.setRequestProperty("Content-Type", "application/json");
- connection.setRequestProperty("Accept", "application/json");
- connection.setUseCaches(false);
-
- JSONObject responseObject = Requester.parseJSONObject(connection);
- return responseObject.getString("token");
- }
-
- public static RedgifsToken refreshToken(String userAgent) throws IOException, JSONException {
- synchronized(tokenMap) {
- // Reference: https://github.com/JeffreyCA/Apollo-ImprovedCustomApi/pull/67
- RedgifsToken token = tokenMap.get(userAgent);
- if (token != null && token.isValid()) {
- return token;
- }
-
- // Copy user agent from original request if present because Redgifs verifies
- // that the user agent in subsequent requests matches the one in the OAuth token.
- String accessToken = getToken(userAgent);
- long refreshTime = System.currentTimeMillis() / 1000;
- token = new RedgifsToken(accessToken, refreshTime);
- tokenMap.put(userAgent, token);
- return token;
- }
- }
-
- public static String getEmulatedOAuthResponseBody(RedgifsToken token) throws JSONException {
- // Reference: https://github.com/JeffreyCA/Apollo-ImprovedCustomApi/pull/67
- JSONObject responseObject = new JSONObject();
- responseObject.put("access_token", token.accessToken);
- responseObject.put("expiry_time", token.getExpiryTimeInSeconds() - (System.currentTimeMillis() / 1000));
- responseObject.put("scope", "read");
- responseObject.put("token_type", "Bearer");
- return responseObject.toString();
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/BaseFixSLinksPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/BaseFixSLinksPatch.java
deleted file mode 100644
index a8a6bf504e..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/BaseFixSLinksPatch.java
+++ /dev/null
@@ -1,208 +0,0 @@
-package app.revanced.extension.shared.fixes.slink;
-
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import androidx.annotation.NonNull;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.SocketTimeoutException;
-import java.net.URL;
-import java.util.Objects;
-
-import static app.revanced.extension.shared.Utils.getContext;
-
-
-/**
- * Base class to implement /s/ link resolution in 3rd party Reddit apps.
- *
- *
- * Usage:
- *
- *
- * An implementation of this class must have two static methods that are called by the app:
- *
- * public static boolean patchResolveSLink(String link)
- * public static void patchSetAccessToken(String accessToken)
- *
- * The static methods must call the instance methods of the base class.
- *
- * The singleton pattern can be used to access the instance of the class:
- *
- * {@code
- * {
- * INSTANCE = new FixSLinksPatch();
- * }
- * }
- *
- * Set the app's web view activity class as a fallback to open /s/ links if the resolution fails:
- *
- * {@code
- * private FixSLinksPatch() {
- * webViewActivityClass = WebViewActivity.class;
- * }
- * }
- *
- * Hook the app's navigation handler to call this method before doing any of its own resolution:
- *
- * {@code
- * public static boolean patchResolveSLink(Context context, String link) {
- * return INSTANCE.resolveSLink(context, link);
- * }
- * }
- *
- * If this method returns true, the app should early return and not do any of its own resolution.
- *
- *
- * Hook the app's access token so that this class can use it to resolve /s/ links:
- *
- * {@code
- * public static void patchSetAccessToken(String accessToken) {
- * INSTANCE.setAccessToken(access_token);
- * }
- * }
- *
- */
-public abstract class BaseFixSLinksPatch {
- /**
- * The class of the activity used to open links in a web view if resolving them fails.
- */
- protected Class extends Activity> webViewActivityClass;
-
- /**
- * The access token used to resolve the /s/ link.
- */
- protected String accessToken;
-
- /**
- * The URL that was trying to be resolved before the access token was set.
- * If this is not null, the URL will be resolved right after the access token is set.
- */
- protected String pendingUrl;
-
- /**
- * The singleton instance of the class.
- */
- protected static BaseFixSLinksPatch INSTANCE;
-
- public boolean resolveSLink(String link) {
- switch (resolveLink(link)) {
- case ACCESS_TOKEN_START: {
- pendingUrl = link;
- return true;
- }
- case DO_NOTHING:
- return true;
- default:
- return false;
- }
- }
-
- private ResolveResult resolveLink(String link) {
- Context context = getContext();
- if (link.matches(".*reddit\\.com/r/[^/]+/s/[^/]+")) {
- // A link ends with #bypass if it failed to resolve below.
- // resolveLink is called with the same link again but this time with #bypass
- // so that the link is opened in the app browser instead of trying to resolve it again.
- if (link.endsWith("#bypass")) {
- openInAppBrowser(context, link);
-
- return ResolveResult.DO_NOTHING;
- }
-
- Logger.printDebug(() -> "Resolving " + link);
-
- if (accessToken == null) {
- // This is not optimal.
- // However, an accessToken is necessary to make an authenticated request to Reddit.
- // in case Reddit has banned the IP - e.g. VPN.
- Intent startIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
- context.startActivity(startIntent);
-
- return ResolveResult.ACCESS_TOKEN_START;
- }
-
-
- Utils.runOnBackgroundThread(() -> {
- String bypassLink = link + "#bypass";
-
- String finalLocation = bypassLink;
- try {
- HttpURLConnection connection = getHttpURLConnection(link, accessToken);
- connection.connect();
- String location = connection.getHeaderField("location");
- connection.disconnect();
-
- Objects.requireNonNull(location, "Location is null");
-
- finalLocation = location;
- Logger.printDebug(() -> "Resolved " + link + " to " + location);
- } catch (SocketTimeoutException e) {
- Logger.printException(() -> "Timeout when trying to resolve " + link, e);
- finalLocation = bypassLink;
- } catch (Exception e) {
- Logger.printException(() -> "Failed to resolve " + link, e);
- finalLocation = bypassLink;
- } finally {
- Intent startIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(finalLocation));
- startIntent.setPackage(context.getPackageName());
- startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(startIntent);
- }
- });
-
- return ResolveResult.DO_NOTHING;
- }
-
- return ResolveResult.CONTINUE;
- }
-
- public void setAccessToken(String accessToken) {
- Logger.printDebug(() -> "Setting access token");
-
- this.accessToken = accessToken;
-
- // In case a link was trying to be resolved before access token was set.
- // The link is resolved now, after the access token is set.
- if (pendingUrl != null) {
- String link = pendingUrl;
- pendingUrl = null;
-
- Logger.printDebug(() -> "Opening pending URL");
-
- resolveLink(link);
- }
- }
-
- private void openInAppBrowser(Context context, String link) {
- Intent intent = new Intent(context, webViewActivityClass);
- intent.putExtra("url", link);
- context.startActivity(intent);
- }
-
- @NonNull
- private HttpURLConnection getHttpURLConnection(String link, String accessToken) throws IOException {
- URL url = new URL(link);
-
- HttpURLConnection connection = (HttpURLConnection) url.openConnection();
- connection.setInstanceFollowRedirects(false);
- connection.setRequestMethod("HEAD");
- connection.setConnectTimeout(2000);
- connection.setReadTimeout(2000);
-
- if (accessToken != null) {
- Logger.printDebug(() -> "Setting access token to make /s/ request");
-
- connection.setRequestProperty("Authorization", "Bearer " + accessToken);
- } else {
- Logger.printDebug(() -> "Not setting access token to make /s/ request, because it is null");
- }
-
- return connection;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/ResolveResult.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/ResolveResult.java
deleted file mode 100644
index 8026c20585..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/ResolveResult.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package app.revanced.extension.shared.fixes.slink;
-
-public enum ResolveResult {
- // Let app handle rest of stuff
- CONTINUE,
- // Start app, to make it cache its access_token
- ACCESS_TOKEN_START,
- // Don't do anything - we started resolving
- DO_NOTHING
-}
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
deleted file mode 100644
index d12eabc0bd..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CheckWatchHistoryDomainNameResolutionPatch.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package app.revanced.extension.shared.patches;
-
-import static app.revanced.extension.shared.StringRef.str;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.text.Html;
-import android.util.Pair;
-import android.widget.LinearLayout;
-
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.ui.CustomDialog;
-
-@SuppressWarnings("unused")
-public class CheckWatchHistoryDomainNameResolutionPatch {
-
- private static final String HISTORY_TRACKING_ENDPOINT = "s.youtube.com";
-
- private static final String SINKHOLE_IPV4 = "0.0.0.0";
- private static final String SINKHOLE_IPV6 = "::";
-
- private static boolean domainResolvesToValidIP(String host) {
- try {
- InetAddress address = InetAddress.getByName(host);
- String hostAddress = address.getHostAddress();
-
- if (address.isLoopbackAddress()) {
- Logger.printDebug(() -> host + " resolves to localhost");
- } else if (SINKHOLE_IPV4.equals(hostAddress) || SINKHOLE_IPV6.equals(hostAddress)) {
- Logger.printDebug(() -> host + " resolves to sinkhole ip");
- } else {
- return true; // Domain is not blocked.
- }
- } catch (UnknownHostException e) {
- Logger.printDebug(() -> host + " failed to resolve");
- }
-
- return false;
- }
-
- /**
- * Injection point.
- *
- * Checks if YouTube watch history endpoint cannot be reached.
- */
- public static void checkDnsResolver(Activity context) {
- if (!Utils.isNetworkConnected() || !BaseSettings.CHECK_WATCH_HISTORY_DOMAIN_NAME.get()) return;
-
- Utils.runOnBackgroundThread(() -> {
- try {
- // If the user has a flaky DNS server, or they just lost internet connectivity
- // and the isNetworkConnected() check has not detected it yet (it can take a few
- // seconds after losing connection), then the history tracking endpoint will
- // show a resolving error but it's actually an internet connection problem.
- //
- // 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.
- 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;
- }
-
- Utils.runOnMainThread(() -> {
- Pair dialogPair = CustomDialog.create(
- context,
- str("revanced_check_watch_history_domain_name_dialog_title"), // Title.
- Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")), // Message (HTML).
- null, // No EditText.
- null, // OK button text.
- () -> {}, // OK button action (just dismiss).
- null, // No cancel button.
- str("revanced_check_watch_history_domain_name_dialog_ignore"), // Neutral button text.
- () -> BaseSettings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false), // Neutral button action (Ignore).
- true // Dismiss dialog on Neutral button click.
- );
-
- Utils.showDialog(context, dialogPair.first, false, null);
- });
- } catch (Exception ex) {
- Logger.printException(() -> "checkDnsResolver failure", ex);
- }
- });
- }
-}
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
deleted file mode 100644
index d13513e2df..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java
+++ /dev/null
@@ -1,227 +0,0 @@
-package app.revanced.extension.shared.patches;
-
-import android.app.Notification;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.graphics.Color;
-import android.view.View;
-
-import androidx.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-
-import app.revanced.extension.shared.GmsCoreSupport;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.BaseSettings;
-
-/**
- * Patch shared by YouTube and YT Music.
- */
-@SuppressWarnings("unused")
-public class CustomBrandingPatch {
-
- // Important: In the future, additional branding themes can be added but all existing and prior
- // themes cannot be removed or renamed.
- //
- // This is because if a user has a branding theme selected, then only that launch alias is enabled.
- // If a future update removes or renames that alias, then after updating the app is effectively
- // broken and it cannot be opened and not even clearing the app data will fix it.
- // In that situation the only fix is to completely uninstall and reinstall again.
- //
- // The most that can be done is to hide a theme from the UI and keep the alias with dummy data.
- public enum BrandingTheme {
- /**
- * Original unpatched icon.
- */
- ORIGINAL,
- ROUNDED,
- MINIMAL,
- SCALED,
- /**
- * User provided custom icon.
- */
- CUSTOM;
-
- private String packageAndNameIndexToClassAlias(String packageName, int appIndex) {
- if (appIndex <= 0) {
- throw new IllegalArgumentException("App index starts at index 1");
- }
- return packageName + ".revanced_" + name().toLowerCase(Locale.US) + '_' + appIndex;
- }
- }
-
- @Nullable
- private static Integer notificationSmallIcon;
-
- private static int getNotificationSmallIcon() {
- // Cannot use static initialization block otherwise cyclic references exist
- // between Settings initialization and this class.
- if (notificationSmallIcon == null) {
- if (GmsCoreSupport.isPackageNameOriginal()) {
- Logger.printDebug(() -> "App is root mounted. Not overriding small notification icon");
- return notificationSmallIcon = 0;
- }
-
- BrandingTheme branding = BaseSettings.CUSTOM_BRANDING_ICON.get();
- if (branding == BrandingTheme.ORIGINAL) {
- notificationSmallIcon = 0;
- } else {
- // Original icon is quantum_ic_video_youtube_white_24
- String iconName = "revanced_notification_icon";
- if (branding == BrandingTheme.CUSTOM) {
- iconName += "_custom";
- }
-
- notificationSmallIcon = Utils.getResourceIdentifier(ResourceType.DRAWABLE, iconName);
- if (notificationSmallIcon == 0) {
- Logger.printException(() -> "Could not load notification small icon");
- }
- }
- }
- return notificationSmallIcon;
- }
-
- /**
- * Injection point.
- */
- public static View getLottieViewOrNull(View lottieStartupView) {
- if (BaseSettings.CUSTOM_BRANDING_ICON.get() == BrandingTheme.ORIGINAL) {
- return lottieStartupView;
- }
-
- return null;
- }
-
- /**
- * Injection point.
- */
- public static void setNotificationIcon(Notification.Builder builder) {
- try {
- final int smallIcon = getNotificationSmallIcon();
- if (smallIcon != 0) {
- builder.setSmallIcon(smallIcon)
- .setColor(Color.TRANSPARENT); // Remove YT red tint.
- }
- } catch (Exception ex) {
- Logger.printException(() -> "setNotificationIcon failure", ex);
- }
- }
-
- /**
- * Injection point.
- *
- * The total number of app name aliases, including dummy aliases.
- */
- private static int numberOfPresetAppNames() {
- // Modified during patching, but requires a default if custom branding is excluded.
- return 1;
- }
-
-
- /**
- * Injection point.
- *
- * If a custom icon was provided during patching.
- */
- private static boolean userProvidedCustomIcon() {
- // Modified during patching, but requires a default if custom branding is excluded.
- return false;
- }
-
- /**
- * Injection point.
- *
- * If a custom name was provided during patching.
- */
- private static boolean userProvidedCustomName() {
- // Modified during patching, but requires a default if custom branding is excluded..
- return false;
- }
-
- public static int getDefaultAppNameIndex() {
- return userProvidedCustomName()
- ? numberOfPresetAppNames()
- : 2;
- }
-
- public static BrandingTheme getDefaultIconStyle() {
- return userProvidedCustomIcon()
- ? BrandingTheme.CUSTOM
- : BrandingTheme.ROUNDED;
- }
-
- /**
- * Injection point.
- */
- @SuppressWarnings("ConstantConditions")
- public static void setBranding() {
- try {
- if (GmsCoreSupport.isPackageNameOriginal()) {
- Logger.printInfo(() -> "App is root mounted. Cannot dynamically change app icon");
- return;
- }
-
- Context context = Utils.getContext();
- PackageManager pm = context.getPackageManager();
- String packageName = context.getPackageName();
-
- BrandingTheme selectedBranding = BaseSettings.CUSTOM_BRANDING_ICON.get();
- final int selectedNameIndex = BaseSettings.CUSTOM_BRANDING_NAME.get();
- ComponentName componentToEnable = null;
- ComponentName defaultComponent = null;
- List componentsToDisable = new ArrayList<>();
-
- for (BrandingTheme theme : BrandingTheme.values()) {
- // Must always update all aliases including custom alias (last index).
- final int numberOfPresetAppNames = numberOfPresetAppNames();
-
- // App name indices starts at 1.
- for (int index = 1; index <= numberOfPresetAppNames; index++) {
- String aliasClass = theme.packageAndNameIndexToClassAlias(packageName, index);
- ComponentName component = new ComponentName(packageName, aliasClass);
- if (defaultComponent == null) {
- // Default is always the first alias.
- defaultComponent = component;
- }
-
- if (index == selectedNameIndex && theme == selectedBranding) {
- componentToEnable = component;
- } else {
- componentsToDisable.add(component);
- }
- }
- }
-
- if (componentToEnable == null) {
- // User imported a bad app name index value. Either the imported data
- // was corrupted, or they previously had custom name enabled and the app
- // no longer has a custom name specified.
- Utils.showToastLong("Custom branding reset");
- BaseSettings.CUSTOM_BRANDING_ICON.resetToDefault();
- BaseSettings.CUSTOM_BRANDING_NAME.resetToDefault();
-
- componentToEnable = defaultComponent;
- componentsToDisable.remove(defaultComponent);
- }
-
- for (ComponentName disable : componentsToDisable) {
- pm.setComponentEnabledSetting(disable,
- PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
- }
-
- // Use info logging because if the alias status become corrupt the app cannot launch.
- ComponentName componentToEnableFinal = componentToEnable;
- Logger.printInfo(() -> "Enabling: " + componentToEnableFinal.getClassName());
-
- pm.setComponentEnabledSetting(componentToEnable,
- PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
- } catch (Exception ex) {
- Logger.printException(() -> "setBranding failure", ex);
- }
- }
-}
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
deleted file mode 100644
index b63f2c6049..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/EnableDebuggingPatch.java
+++ /dev/null
@@ -1,138 +0,0 @@
-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;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.settings.BaseSettings;
-
-@SuppressWarnings("unused")
-public final class EnableDebuggingPatch {
-
- /**
- * Only log if debugging is enabled on startup.
- * This prevents enabling debugging
- * while the app is running then failing to restart
- * resulting in an incomplete log.
- */
- private static final boolean LOG_FEATURE_FLAGS = BaseSettings.DEBUG.get();
-
- private static final ConcurrentMap featureFlags = LOG_FEATURE_FLAGS
- ? 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) {
- Long flagObj = flag;
- if (DISABLED_FEATURE_FLAGS.contains(flagObj)) {
- return false;
- }
- if (featureFlags.putIfAbsent(flagObj, TRUE) == null) {
- Logger.printDebug(() -> "boolean feature is enabled: " + flag);
- }
- }
-
- return value;
- }
-
- /**
- * Injection point.
- */
- public static double isDoubleFeatureFlagEnabled(double value, long flag, double defaultValue) {
- if (LOG_FEATURE_FLAGS && defaultValue != value) {
- if (DISABLED_FEATURE_FLAGS.contains(flag)) return defaultValue;
-
- if (featureFlags.putIfAbsent(flag, true) == null) {
- // Align the log outputs to make post processing easier.
- Logger.printDebug(() -> " double feature is enabled: " + flag
- + " value: " + value + (defaultValue == 0 ? "" : " default: " + defaultValue));
- }
- }
-
- return value;
- }
-
- /**
- * Injection point.
- */
- public static long isLongFeatureFlagEnabled(long value, long flag, long defaultValue) {
- if (LOG_FEATURE_FLAGS && defaultValue != value) {
- if (DISABLED_FEATURE_FLAGS.contains(flag)) return defaultValue;
-
- if (featureFlags.putIfAbsent(flag, true) == null) {
- Logger.printDebug(() -> " long feature is enabled: " + flag
- + " value: " + value + (defaultValue == 0 ? "" : " default: " + defaultValue));
- }
- }
-
- return value;
- }
-
- /**
- * Injection point.
- */
- public static String isStringFeatureFlagEnabled(String value, long flag, String defaultValue) {
- 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));
- }
- }
-
- 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/patches/ForceOriginalAudioPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/ForceOriginalAudioPatch.java
deleted file mode 100644
index 8ae454e69a..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/ForceOriginalAudioPatch.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package app.revanced.extension.shared.patches;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.settings.AppLanguage;
-import app.revanced.extension.shared.spoof.ClientType;
-import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
-
-@SuppressWarnings("unused")
-public class ForceOriginalAudioPatch {
-
- private static final String DEFAULT_AUDIO_TRACKS_SUFFIX = ".4";
-
- private static volatile boolean enabled;
-
- public static void setEnabled(boolean isEnabled, ClientType client) {
- enabled = isEnabled;
-
- if (isEnabled && !client.useAuth && !client.supportsMultiAudioTracks) {
- // If client spoofing does not use authentication and lacks multi-audio streams,
- // then can use any language code for the request and if that requested language is
- // not available YT uses the original audio language. Authenticated requests ignore
- // the language code and always use the account language. Use a language that is
- // not auto-dubbed by YouTube: https://support.google.com/youtube/answer/15569972
- // but the language is also supported natively by the Meta Quest device that
- // Android VR is spoofing.
- AppLanguage override = AppLanguage.NB; // Norwegian Bokmal.
- Logger.printDebug(() -> "Setting language override: " + override);
- SpoofVideoStreamsPatch.setLanguageOverride(override);
- }
- }
-
- /**
- * Injection point.
- */
- public static boolean ignoreDefaultAudioStream(boolean original) {
- if (enabled) {
- return false;
- }
- return original;
- }
-
- /**
- * Injection point.
- */
- public static boolean isDefaultAudioStream(boolean isDefault, String audioTrackId, String audioTrackDisplayName) {
- try {
- if (!enabled) {
- return isDefault;
- }
-
- if (audioTrackId.isEmpty()) {
- // Older app targets can have empty audio tracks and these might be placeholders.
- // The real audio tracks are called after these.
- return isDefault;
- }
-
- Logger.printDebug(() -> "default: " + String.format("%-5s", isDefault) + " id: "
- + String.format("%-8s", audioTrackId) + " name:" + audioTrackDisplayName);
-
- final boolean isOriginal = audioTrackId.endsWith(DEFAULT_AUDIO_TRACKS_SUFFIX);
- if (isOriginal) {
- Logger.printDebug(() -> "Using audio: " + audioTrackId);
- }
-
- return isOriginal;
- } catch (Exception ex) {
- Logger.printException(() -> "isDefaultAudioStream failure", ex);
- return isDefault;
- }
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java
deleted file mode 100644
index b0bcbc6f04..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package app.revanced.extension.shared.patches;
-
-import app.revanced.extension.shared.privacy.LinkSanitizer;
-import app.revanced.extension.shared.settings.BaseSettings;
-
-/**
- * YouTube and YouTube Music.
- */
-@SuppressWarnings("unused")
-public final class SanitizeSharingLinksPatch {
-
- private static final LinkSanitizer sanitizer = new LinkSanitizer(
- "si",
- "feature" // Old tracking parameter name, and may be obsolete.
- );
-
- /**
- * Injection point.
- */
- public static String sanitize(String url) {
- if (BaseSettings.SANITIZE_SHARING_LINKS.get()) {
- url = sanitizer.sanitizeURLString(url);
- }
-
- if (BaseSettings.REPLACE_MUSIC_LINKS_WITH_YOUTUBE.get()) {
- url = url.replace("music.youtube.com", "youtube.com");
- }
-
- return url;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java
deleted file mode 100644
index beb623a799..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java
+++ /dev/null
@@ -1,203 +0,0 @@
-package app.revanced.extension.shared.patches.litho;
-
-import static app.revanced.extension.shared.StringRef.str;
-
-import androidx.annotation.NonNull;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.StringTrieSearch;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.ByteTrieSearch;
-import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
-import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
-
-/**
- * Allows custom filtering using a path and optionally a proto buffer string.
- */
-@SuppressWarnings("unused")
-public final class CustomFilter extends Filter {
-
- private static void showInvalidSyntaxToast(String expression) {
- Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
- }
-
- private static class CustomFilterGroup extends StringFilterGroup {
- /**
- * Optional character for the path that indicates the custom filter path must match the start.
- * Must be the first character of the expression.
- */
- public static final String SYNTAX_STARTS_WITH = "^";
-
- /**
- * Optional character that separates the path from an accessibility string pattern.
- */
- public static final String SYNTAX_ACCESSIBILITY_SYMBOL = "#";
-
- /**
- * Optional character that separates the path/accessibility from a proto buffer string pattern.
- */
- public static final String SYNTAX_BUFFER_SYMBOL = "$";
-
- /**
- * @return the parsed objects
- */
- @NonNull
- @SuppressWarnings("ConstantConditions")
- static Collection parseCustomFilterGroups() {
- String rawCustomFilterText = YouTubeAndMusicSettings.CUSTOM_FILTER_STRINGS.get();
- if (rawCustomFilterText.isBlank()) {
- return Collections.emptyList();
- }
-
- // Map key is the full path including optional special characters (^, #, $),
- // and any accessibility pattern, but does not contain any buffer patterns.
- Map result = new HashMap<>();
-
- Pattern pattern = Pattern.compile(
- "(" // Map key group.
- // Optional starts with.
- + "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)"
- // Path string.
- + "([^\\Q" + SYNTAX_ACCESSIBILITY_SYMBOL + SYNTAX_BUFFER_SYMBOL + "\\E]*)"
- // Optional accessibility string.
- + "(?:\\Q" + SYNTAX_ACCESSIBILITY_SYMBOL + "\\E([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*))?"
- // Optional buffer string.
- + "(?:\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E(.*))?"
- + ")"); // end map key group
-
- for (String expression : rawCustomFilterText.split("\n")) {
- if (expression.isBlank()) continue;
-
- Matcher matcher = pattern.matcher(expression);
- if (!matcher.find()) {
- showInvalidSyntaxToast(expression);
- continue;
- }
-
- final String mapKey = matcher.group(1);
- final boolean pathStartsWith = !matcher.group(2).isEmpty();
- final String path = matcher.group(3);
- final String accessibility = matcher.group(4); // null if not present
- final String buffer = matcher.group(5); // null if not present
-
- if (path.isBlank()
- || (accessibility != null && accessibility.isEmpty())
- || (buffer != null && buffer.isEmpty())) {
- showInvalidSyntaxToast(expression);
- continue;
- }
-
- // Use one group object for all expressions with the same path.
- // This ensures the buffer is searched exactly once
- // when multiple paths are used with different buffer strings.
- CustomFilterGroup group = result.get(mapKey);
- if (group == null) {
- group = new CustomFilterGroup(pathStartsWith, path);
- result.put(mapKey, group);
- }
-
- if (accessibility != null) {
- group.addAccessibilityString(accessibility);
- }
-
- if (buffer != null) {
- group.addBufferString(buffer);
- }
- }
-
- return result.values();
- }
-
- final boolean startsWith;
- StringTrieSearch accessibilitySearch;
- ByteTrieSearch bufferSearch;
-
- CustomFilterGroup(boolean startsWith, String path) {
- super(YouTubeAndMusicSettings.CUSTOM_FILTER, path);
- this.startsWith = startsWith;
- }
-
- void addAccessibilityString(String accessibilityString) {
- if (accessibilitySearch == null) {
- accessibilitySearch = new StringTrieSearch();
- }
- accessibilitySearch.addPattern(accessibilityString);
- }
-
- void addBufferString(String bufferString) {
- if (bufferSearch == null) {
- bufferSearch = new ByteTrieSearch();
- }
- bufferSearch.addPattern(bufferString.getBytes());
- }
-
- @NonNull
- @Override
- public String toString() {
- StringBuilder builder = new StringBuilder();
- builder.append("CustomFilterGroup{");
- if (accessibilitySearch != null) {
- builder.append(", accessibility=");
- builder.append(accessibilitySearch.getPatterns());
- }
-
- builder.append("path=");
- if (startsWith) builder.append(SYNTAX_STARTS_WITH);
- builder.append(filters[0]);
-
- if (bufferSearch != null) {
- String delimitingCharacter = "❙";
- builder.append(", bufferStrings=");
- builder.append(delimitingCharacter);
- for (byte[] bufferString : bufferSearch.getPatterns()) {
- builder.append(new String(bufferString));
- builder.append(delimitingCharacter);
- }
- }
- builder.append("}");
- return builder.toString();
- }
- }
-
- public CustomFilter() {
- Collection groups = CustomFilterGroup.parseCustomFilterGroups();
-
- if (!groups.isEmpty()) {
- CustomFilterGroup[] groupsArray = groups.toArray(new CustomFilterGroup[0]);
- Logger.printDebug(()-> "Using Custom filters: " + Arrays.toString(groupsArray));
- addPathCallbacks(groupsArray);
- }
- }
-
- @Override
- public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
- StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
- // All callbacks are custom filter groups.
- CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
-
- // Check path start requirement.
- if (custom.startsWith && contentIndex != 0) {
- return false;
- }
-
- // Check accessibility string if specified.
- if (custom.accessibilitySearch != null && !custom.accessibilitySearch.matches(accessibility)) {
- return false;
- }
-
- // Check buffer if specified.
- if (custom.bufferSearch != null && !custom.bufferSearch.matches(buffer)) {
- return false;
- }
-
- return true; // All custom filter conditions passed.
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java
deleted file mode 100644
index b34ca9bdd7..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package app.revanced.extension.shared.patches.litho;
-
-import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
-import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Filters litho based components.
- *
- * Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)}
- * and {@link #addPathCallbacks(StringFilterGroup...)}.
- *
- * To filter {@link FilterContentType#PROTOBUFFER} or {@link FilterContentType#ACCESSIBILITY}, first add a callback to
- * either an identifier or a path.
- * Then inside {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
- * search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
- * or a {@link FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern).
- *
- * All callbacks must be registered before the constructor completes.
- */
-public abstract class Filter {
-
- public enum FilterContentType {
- IDENTIFIER,
- PATH,
- ACCESSIBILITY,
- PROTOBUFFER
- }
-
- /**
- * Identifier callbacks. Do not add to this instance,
- * and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}.
- */
- public final List identifierCallbacks = new ArrayList<>();
- /**
- * Path callbacks. Do not add to this instance,
- * and instead use {@link #addPathCallbacks(StringFilterGroup...)}.
- */
- public final List pathCallbacks = new ArrayList<>();
-
- /**
- * Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
- * if any of the groups are found.
- */
- protected final void addIdentifierCallbacks(StringFilterGroup... groups) {
- identifierCallbacks.addAll(Arrays.asList(groups));
- }
-
- /**
- * Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
- * if any of the groups are found.
- */
- protected final void addPathCallbacks(StringFilterGroup... groups) {
- pathCallbacks.addAll(Arrays.asList(groups));
- }
-
- /**
- * Called after an enabled filter has been matched.
- * Default implementation is to always filter the matched component and log the action.
- * Subclasses can perform additional or different checks if needed.
- *
- * Method is called off the main thread.
- *
- * @param identifier Litho identifier.
- * @param accessibility Accessibility string, or an empty string if not present for the component.
- * @param buffer Protocol buffer.
- * @param matchedGroup The actual filter that matched.
- * @param contentType The type of content matched.
- * @param contentIndex Matched index of the identifier or path.
- * @return True if the litho component should be filtered out.
- */
- public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
- StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
- return true;
- }
-}
-
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroup.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroup.java
deleted file mode 100644
index 212787f305..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroup.java
+++ /dev/null
@@ -1,213 +0,0 @@
-package app.revanced.extension.shared.patches.litho;
-
-import androidx.annotation.NonNull;
-import app.revanced.extension.shared.ByteTrieSearch;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.settings.BooleanSetting;
-
-public abstract class FilterGroup {
- public final static class FilterGroupResult {
- private BooleanSetting setting;
- private int matchedIndex;
- private int matchedLength;
- // In the future it might be useful to include which pattern matched,
- // but for now that is not needed.
-
- FilterGroupResult() {
- this(null, -1, 0);
- }
-
- FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) {
- setValues(setting, matchedIndex, matchedLength);
- }
-
- public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) {
- this.setting = setting;
- this.matchedIndex = matchedIndex;
- this.matchedLength = matchedLength;
- }
-
- /**
- * A null value if the group has no setting,
- * or if no match is returned from {@link FilterGroupList#check(Object)}.
- */
- public BooleanSetting getSetting() {
- return setting;
- }
-
- public boolean isFiltered() {
- return matchedIndex >= 0;
- }
-
- /**
- * Matched index of first pattern that matched, or -1 if nothing matched.
- */
- public int getMatchedIndex() {
- return matchedIndex;
- }
-
- /**
- * Length of the matched filter pattern.
- */
- public int getMatchedLength() {
- return matchedLength;
- }
- }
-
- protected final BooleanSetting setting;
- public final T[] filters;
-
- /**
- * Initialize a new filter group.
- *
- * @param setting The associated setting.
- * @param filters The filters.
- */
- @SafeVarargs
- public FilterGroup(final BooleanSetting setting, final T... filters) {
- this.setting = setting;
- this.filters = filters;
- if (filters.length == 0) {
- throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
- }
- }
-
- public boolean isEnabled() {
- return setting == null || setting.get();
- }
-
- /**
- * @return If {@link FilterGroupList} should include this group when searching.
- * By default, all filters are included except non enabled settings that require reboot.
- */
- @SuppressWarnings("BooleanMethodIsAlwaysInverted")
- public boolean includeInSearch() {
- return isEnabled() || !setting.rebootApp;
- }
-
- @NonNull
- @Override
- public String toString() {
- return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
- }
-
- public abstract FilterGroupResult check(final T stack);
-
-
- public static class StringFilterGroup extends FilterGroup {
-
- public StringFilterGroup(final BooleanSetting setting, final String... filters) {
- super(setting, filters);
- }
-
- @Override
- public FilterGroupResult check(final String string) {
- int matchedIndex = -1;
- int matchedLength = 0;
- if (isEnabled()) {
- for (String pattern : filters) {
- if (!string.isEmpty()) {
- final int indexOf = string.indexOf(pattern);
- if (indexOf >= 0) {
- matchedIndex = indexOf;
- matchedLength = pattern.length();
- break;
- }
- }
- }
- }
- return new FilterGroupResult(setting, matchedIndex, matchedLength);
- }
- }
-
- /**
- * If you have more than 1 filter patterns, then all instances of
- * this class should be filtered using {@link FilterGroupList.ByteArrayFilterGroupList#check(byte[])},
- * which uses a prefix tree to give better performance.
- */
- public static class ByteArrayFilterGroup extends FilterGroup {
-
- private volatile int[][] failurePatterns;
-
- // Modified implementation from https://stackoverflow.com/a/1507813
- private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) {
- // Finds the first occurrence of the pattern in the byte array using
- // KMP matching algorithm.
- int patternLength = pattern.length;
- for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) {
- while (j > 0 && pattern[j] != data[i]) {
- j = failure[j - 1];
- }
- if (pattern[j] == data[i]) {
- j++;
- }
- if (j == patternLength) {
- return i - patternLength + 1;
- }
- }
- return -1;
- }
-
- private static int[] createFailurePattern(byte[] pattern) {
- // Computes the failure function using a bootstrapping process,
- // where the pattern is matched against itself.
- final int patternLength = pattern.length;
- final int[] failure = new int[patternLength];
-
- for (int i = 1, j = 0; i < patternLength; i++) {
- while (j > 0 && pattern[j] != pattern[i]) {
- j = failure[j - 1];
- }
- if (pattern[j] == pattern[i]) {
- j++;
- }
- failure[i] = j;
- }
- return failure;
- }
-
- public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) {
- super(setting, filters);
- }
-
- /**
- * Converts the Strings into byte arrays. Used to search for text in binary data.
- */
- public ByteArrayFilterGroup(BooleanSetting setting, String... filters) {
- super(setting, ByteTrieSearch.convertStringsToBytes(filters));
- }
-
- private synchronized void buildFailurePatterns() {
- if (failurePatterns != null) return; // Thread race and another thread already initialized the search.
- Logger.printDebug(() -> "Building failure array for: " + this);
- int[][] failurePatterns = new int[filters.length][];
- int i = 0;
- for (byte[] pattern : filters) {
- failurePatterns[i++] = createFailurePattern(pattern);
- }
- this.failurePatterns = failurePatterns; // Must set after initialization finishes.
- }
-
- @Override
- public FilterGroupResult check(final byte[] bytes) {
- int matchedLength = 0;
- int matchedIndex = -1;
- if (isEnabled()) {
- int[][] failures = failurePatterns;
- if (failures == null) {
- buildFailurePatterns(); // Lazy load.
- failures = failurePatterns;
- }
- for (int i = 0, length = filters.length; i < length; i++) {
- byte[] filter = filters[i];
- matchedIndex = indexOf(bytes, filter, failures[i]);
- if (matchedIndex >= 0) {
- matchedLength = filter.length;
- break;
- }
- }
- }
- return new FilterGroupResult(setting, matchedIndex, matchedLength);
- }
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroupList.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroupList.java
deleted file mode 100644
index da22ca9ff7..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroupList.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package app.revanced.extension.shared.patches.litho;
-
-import androidx.annotation.NonNull;
-import app.revanced.extension.shared.ByteTrieSearch;
-import app.revanced.extension.shared.StringTrieSearch;
-import app.revanced.extension.shared.TrieSearch;
-import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
-import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
-
-public abstract class FilterGroupList> implements Iterable {
-
- private final List filterGroups = new ArrayList<>();
- private final TrieSearch search = createSearchGraph();
-
- @SafeVarargs
- public final void addAll(final T... groups) {
- filterGroups.addAll(Arrays.asList(groups));
-
- for (T group : groups) {
- if (!group.includeInSearch()) {
- continue;
- }
- for (V pattern : group.filters) {
- search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
- if (group.isEnabled()) {
- FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
- result.setValues(group.setting, matchedStartIndex, matchedLength);
- return true;
- }
- return false;
- });
- }
- }
- }
-
- @NonNull
- @Override
- public Iterator iterator() {
- return filterGroups.iterator();
- }
-
- public FilterGroup.FilterGroupResult check(V stack) {
- FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
- search.matches(stack, result);
- return result;
-
- }
-
- protected abstract TrieSearch createSearchGraph();
-
- public static final class StringFilterGroupList extends FilterGroupList {
- protected StringTrieSearch createSearchGraph() {
- return new StringTrieSearch();
- }
- }
-
- /**
- * If searching for a single byte pattern, then it is slightly better to use
- * {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
- * than a prefix tree to search for only 1 pattern.
- */
- public static final class ByteArrayFilterGroupList extends FilterGroupList {
- protected ByteTrieSearch createSearchGraph() {
- return new ByteTrieSearch();
- }
- }
-}
\ No newline at end of file
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java
deleted file mode 100644
index e1b329ee54..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java
+++ /dev/null
@@ -1,439 +0,0 @@
-package app.revanced.extension.shared.patches.litho;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.StringTrieSearch;
-import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
-
-@SuppressWarnings("unused")
-public final class LithoFilterPatch {
- /**
- * Simple wrapper to pass the litho parameters through the prefix search.
- */
- private static final class LithoFilterParameters {
- final String identifier;
- final String path;
- final String accessibility;
- final byte[] buffer;
-
- LithoFilterParameters(String lithoIdentifier, String lithoPath,
- String accessibility, byte[] buffer) {
- this.identifier = lithoIdentifier;
- this.path = lithoPath;
- this.accessibility = accessibility;
- this.buffer = buffer;
- }
-
- @NonNull
- @Override
- public String toString() {
- // Estimate the percentage of the buffer that are Strings.
- StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
- builder.append("ID: ");
- builder.append(identifier);
- if (!accessibility.isEmpty()) {
- // AccessibilityId and AccessibilityText are pieces of BufferStrings.
- builder.append(" Accessibility: ");
- builder.append(accessibility);
- }
- builder.append(" Path: ");
- builder.append(path);
- if (YouTubeAndMusicSettings.DEBUG_PROTOCOLBUFFER.get()) {
- builder.append(" BufferStrings: ");
- findAsciiStrings(builder, buffer);
- }
-
- return builder.toString();
- }
-
- /**
- * Search through a byte array for all ASCII strings.
- */
- static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
- // Valid ASCII values (ignore control characters).
- final int minimumAscii = 32; // 32 = space character
- final int maximumAscii = 126; // 127 = delete character
- final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include.
- String delimitingCharacter = "❙"; // Non ascii character, to allow easier log filtering.
-
- final int length = buffer.length;
- int start = 0;
- int end = 0;
- while (end < length) {
- int value = buffer[end];
- if (value < minimumAscii || value > maximumAscii || end == length - 1) {
- if (end - start >= minimumAsciiStringLength) {
- for (int i = start; i < end; i++) {
- builder.append((char) buffer[i]);
- }
- builder.append(delimitingCharacter);
- }
- start = end + 1;
- }
- end++;
- }
- }
- }
-
- /**
- * Placeholder for actual filters.
- */
- private static final class DummyFilter extends Filter {
- }
-
- private static final Filter[] filters = new Filter[]{
- new DummyFilter() // Replaced during patching, do not touch.
- };
-
- /**
- * Litho layout fixed thread pool size override.
- *
- * Unpatched YouTube uses a layout fixed thread pool between 1 and 3 threads:
- *
- * 1 thread - > Device has less than 6 cores
- * 2 threads -> Device has over 6 cores and less than 6GB of memory
- * 3 threads -> Device has over 6 cores and more than 6GB of memory
- *
- *
- * Using more than 1 thread causes layout issues such as the You tab watch/playlist shelf
- * that is sometimes incorrectly hidden (ReVanced is not hiding it), and seems to
- * fix a race issue if using the active navigation tab status with litho filtering.
- */
- private static final int LITHO_LAYOUT_THREAD_POOL_SIZE = 1;
-
- /**
- * For YouTube 20.22+, this is set to true by a patch,
- * because it cannot use the thread buffer due to the buffer frequently not being correct,
- * especially for components that are recreated such as dragging off-screen then back on screen.
- * Instead, parse the identifier found near the start of the buffer and use that to
- * identify the correct buffer to use when filtering.
- *
- * This is set during patching, do not change manually.
- */
- private static final boolean EXTRACT_IDENTIFIER_FROM_BUFFER = false;
-
- /**
- * Turns on additional logging, used for development purposes only.
- */
- public static final boolean DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER = false;
-
- /**
- * String suffix for components.
- * Can be any of: ".eml", ".eml-fe", ".e-b", ".eml-js", "e-js-b"
- */
- private static final byte[] LITHO_COMPONENT_EXTENSION_BYTES = ".e".getBytes(StandardCharsets.US_ASCII);
-
- /**
- * Used as placeholder for litho id/path filters that do not use a buffer
- */
- private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
-
- /**
- * Because litho filtering is multithreaded and the buffer is passed in from a different injection point,
- * the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
- * Used for 20.21 and lower.
- */
- private static final ThreadLocal bufferThreadLocal = new ThreadLocal<>();
-
- /**
- * Identifier to protocol buffer mapping. Only used for 20.22+.
- * Thread local is needed because filtering is multithreaded and each thread can load
- * a different component with the same identifier.
- */
- private static final ThreadLocal> identifierToBufferThread = new ThreadLocal<>();
-
- /**
- * Global shared buffer. Used only if the buffer is not found in the ThreadLocal.
- */
- private static final Map identifierToBufferGlobal
- = Collections.synchronizedMap(createIdentifierToBufferMap());
-
- private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
- private static final StringTrieSearch identifierSearchTree = new StringTrieSearch();
-
- static {
-
- for (Filter filter : filters) {
- filterUsingCallbacks(identifierSearchTree, filter,
- filter.identifierCallbacks, Filter.FilterContentType.IDENTIFIER);
- filterUsingCallbacks(pathSearchTree, filter,
- filter.pathCallbacks, Filter.FilterContentType.PATH);
- }
-
- Logger.printDebug(() -> "Using: "
- + identifierSearchTree.numberOfPatterns() + " identifier filters"
- + " (" + identifierSearchTree.getEstimatedMemorySize() + " KB), "
- + pathSearchTree.numberOfPatterns() + " path filters"
- + " (" + pathSearchTree.getEstimatedMemorySize() + " KB)");
- }
-
- private static void filterUsingCallbacks(StringTrieSearch pathSearchTree,
- Filter filter, List groups,
- Filter.FilterContentType type) {
- String filterSimpleName = filter.getClass().getSimpleName();
-
- for (StringFilterGroup group : groups) {
- if (!group.includeInSearch()) {
- continue;
- }
-
- for (String pattern : group.filters) {
- pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex,
- matchedLength, callbackParameter) -> {
- if (!group.isEnabled()) return false;
-
- LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
- final boolean isFiltered = filter.isFiltered(parameters.identifier,
- parameters.accessibility, parameters.path, parameters.buffer,
- group, type, matchedStartIndex);
-
- if (isFiltered && BaseSettings.DEBUG.get()) {
- Logger.printDebug(() -> type == Filter.FilterContentType.IDENTIFIER
- ? filterSimpleName + " filtered identifier: " + parameters.identifier
- : filterSimpleName + " filtered path: " + parameters.path);
- }
-
- return isFiltered;
- }
- );
- }
- }
- }
-
- private static Map createIdentifierToBufferMap() {
- // It's unclear how many items should be cached. This is a guess.
- return Utils.createSizeRestrictedMap(100);
- }
-
- /**
- * Helper function that differs from {@link Character#isDigit(char)}
- * as this only matches ascii and not Unicode numbers.
- */
- private static boolean isAsciiNumber(byte character) {
- return '0' <= character && character <= '9';
- }
-
- private static boolean isAsciiLowerCaseLetter(byte character) {
- return 'a' <= character && character <= 'z';
- }
-
- /**
- * Injection point. Called off the main thread.
- * Targets 20.22+
- */
- public static void setProtoBuffer(byte[] buffer) {
- if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
- StringBuilder builder = new StringBuilder();
- LithoFilterParameters.findAsciiStrings(builder, buffer);
- Logger.printDebug(() -> "New buffer: " + builder);
- }
-
- // The identifier always seems to start very close to the buffer start.
- // Highest identifier start index ever observed is 50, with most around 30 to 40.
- // The buffer can be very large with up to 200kb has been observed,
- // so the search is restricted to only the start.
- final int maxBufferStartIndex = 500; // 10x expected upper bound.
-
- // Could use Boyer-Moore-Horspool since the string is ASCII and has a limited number of
- // unique characters, but it seems to be slower since the extra overhead of checking the
- // bad character array negates any performance gain of skipping a few extra subsearches.
- int emlIndex = -1;
- final int emlStringLength = LITHO_COMPONENT_EXTENSION_BYTES.length;
- final int lastBufferIndexToCheckFrom = Math.min(maxBufferStartIndex, buffer.length - emlStringLength);
- for (int i = 0; i < lastBufferIndexToCheckFrom; i++) {
- boolean match = true;
- for (int j = 0; j < emlStringLength; j++) {
- if (buffer[i + j] != LITHO_COMPONENT_EXTENSION_BYTES[j]) {
- match = false;
- break;
- }
- }
- if (match) {
- emlIndex = i;
- break;
- }
- }
-
- if (emlIndex < 0) {
- // Buffer is not used for creating a new litho component.
- if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
- Logger.printDebug(() -> "Could not find eml index");
- }
- return;
- }
-
- int startIndex = emlIndex - 1;
- while (startIndex > 0) {
- final byte character = buffer[startIndex];
- int startIndexFinal = startIndex;
- if (isAsciiLowerCaseLetter(character) || isAsciiNumber(character) || character == '_') {
- // Valid character for the first path element.
- startIndex--;
- } else {
- startIndex++;
- break;
- }
- }
-
- // Strip away any numbers on the start of the identifier, which can
- // be from random data in the buffer before the identifier starts.
- while (true) {
- final byte character = buffer[startIndex];
- if (isAsciiNumber(character)) {
- startIndex++;
- } else {
- break;
- }
- }
-
- // Find the pipe character after the identifier.
- int endIndex = -1;
- for (int i = emlIndex, length = buffer.length; i < length; i++) {
- if (buffer[i] == '|') {
- endIndex = i;
- break;
- }
- }
- if (endIndex < 0) {
- if (BaseSettings.DEBUG.get()) {
- Logger.printException(() -> "Debug: Could not find buffer identifier");
- }
- return;
- }
-
- String identifier = new String(buffer, startIndex, endIndex - startIndex, StandardCharsets.US_ASCII);
- if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
- Logger.printDebug(() -> "Found buffer for identifier: " + identifier);
- }
- identifierToBufferGlobal.put(identifier, buffer);
-
- Map map = identifierToBufferThread.get();
- if (map == null) {
- map = createIdentifierToBufferMap();
- identifierToBufferThread.set(map);
- }
- map.put(identifier, buffer);
- }
-
- /**
- * Injection point. Called off the main thread.
- * Targets 20.21 and lower.
- */
- public static void setProtoBuffer(@Nullable ByteBuffer buffer) {
- if (buffer == null || !buffer.hasArray()) {
- // It appears the buffer can be cleared out just before the call to #filter()
- // Ignore this null value and retain the last buffer that was set.
- Logger.printDebug(() -> "Ignoring null or empty buffer: " + buffer);
- } else {
- // Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
- // This is intentional, as it appears the buffer can be set once and then filtered multiple times.
- // The buffer will be cleared from memory after a new buffer is set by the same thread,
- // or when the calling thread eventually dies.
- bufferThreadLocal.set(buffer.array());
- }
- }
-
- /**
- * Injection point.
- */
- public static boolean isFiltered(String identifier, @Nullable String accessibilityId,
- @Nullable String accessibilityText, StringBuilder pathBuilder) {
- try {
- if (identifier.isEmpty() || pathBuilder.length() == 0) {
- return false;
- }
-
- byte[] buffer = null;
- if (EXTRACT_IDENTIFIER_FROM_BUFFER) {
- final int pipeIndex = identifier.indexOf('|');
- if (pipeIndex >= 0) {
- // If the identifier contains no pipe, then it's not an ".eml" identifier
- // and the buffer is not uniquely identified. Typically, this only happens
- // for subcomponents where buffer filtering is not used.
- String identifierKey = identifier.substring(0, pipeIndex);
-
- var map = identifierToBufferThread.get();
- if (map != null) {
- buffer = map.get(identifierKey);
- }
-
- if (buffer == null) {
- // Buffer for thread local not found. Use the last buffer found from any thread.
- buffer = identifierToBufferGlobal.get(identifierKey);
-
- if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER && buffer == null) {
- // No buffer is found for some components, such as
- // shorts_lockup_cell.eml on channel profiles.
- // For now, just ignore this and filter without a buffer.
- if (BaseSettings.DEBUG.get()) {
- Logger.printException(() -> "Debug: Could not find buffer for identifier: " + identifier);
- }
- }
- }
- }
- } else {
- buffer = bufferThreadLocal.get();
- }
-
- // Potentially the buffer may have been null or never set up until now.
- // Use an empty buffer so the litho id/path filters that do not use a buffer still work.
- if (buffer == null) {
- buffer = EMPTY_BYTE_ARRAY;
- }
-
- String path = pathBuilder.toString();
-
- String accessibility = "";
- if (accessibilityId != null && !accessibilityId.isBlank()) {
- accessibility = accessibilityId;
- }
- if (accessibilityText != null && !accessibilityText.isBlank()) {
- accessibility = accessibilityId + '|' + accessibilityText;
- }
- LithoFilterParameters parameter = new LithoFilterParameters(identifier, path, accessibility, buffer);
- Logger.printDebug(() -> "Searching " + parameter);
-
- return identifierSearchTree.matches(identifier, parameter)
- || pathSearchTree.matches(path, parameter);
- } catch (Exception ex) {
- Logger.printException(() -> "isFiltered failure", ex);
- }
-
- return false;
- }
-
- /**
- * Injection point.
- */
- public static int getExecutorCorePoolSize(int originalCorePoolSize) {
- if (originalCorePoolSize != LITHO_LAYOUT_THREAD_POOL_SIZE) {
- Logger.printDebug(() -> "Overriding core thread pool size from: " + originalCorePoolSize
- + " to: " + LITHO_LAYOUT_THREAD_POOL_SIZE);
- }
-
- return LITHO_LAYOUT_THREAD_POOL_SIZE;
- }
-
- /**
- * Injection point.
- */
- public static int getExecutorMaxThreads(int originalMaxThreads) {
- if (originalMaxThreads != LITHO_LAYOUT_THREAD_POOL_SIZE) {
- Logger.printDebug(() -> "Overriding max thread pool size from: " + originalMaxThreads
- + " to: " + LITHO_LAYOUT_THREAD_POOL_SIZE);
- }
-
- return LITHO_LAYOUT_THREAD_POOL_SIZE;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java
deleted file mode 100644
index 421761f7da..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package app.revanced.extension.shared.privacy;
-
-import android.net.Uri;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-
-import app.revanced.extension.shared.Logger;
-
-/**
- * Strips away specific parameters from URLs.
- */
-public class LinkSanitizer {
-
- private final Collection parametersToRemove;
-
- public LinkSanitizer(String ... parametersToRemove) {
- final int parameterCount = parametersToRemove.length;
-
- // List is faster if only checking a few parameters.
- this.parametersToRemove = parameterCount > 4
- ? Set.of(parametersToRemove)
- : List.of(parametersToRemove);
- }
-
- public String sanitizeURLString(String url) {
- try {
- return sanitizeURI(Uri.parse(url)).toString();
- } catch (Exception ex) {
- Logger.printException(() -> "sanitizeURLString failure: " + url, ex);
- return url;
- }
- }
-
- public Uri sanitizeURI(Uri uri) {
- try {
- String scheme = uri.getScheme();
- if (scheme == null || !(scheme.equals("http") || scheme.equals("https"))) {
- // Opening YouTube share sheet 'other' option passes the video title as a URI.
- // Checking !uri.isHierarchical() works for all cases, except if the
- // video title starts with / and then it's hierarchical but still an invalid URI.
- Logger.printDebug(() -> "Ignoring URI: " + uri);
- return uri;
- }
-
- Uri.Builder builder = uri.buildUpon().clearQuery();
-
- if (!parametersToRemove.isEmpty()) {
- for (String paramName : uri.getQueryParameterNames()) {
- if (!parametersToRemove.contains(paramName)) {
- for (String value : uri.getQueryParameters(paramName)) {
- builder.appendQueryParameter(paramName, value);
- }
- }
- }
- }
-
- Uri sanitizedURL = builder.build();
- Logger.printInfo(() -> "Sanitized URL: " + uri + " to: " + sanitizedURL);
-
- return sanitizedURL;
- } catch (Exception ex) {
- Logger.printException(() -> "sanitizeURI failure: " + uri, ex);
- return uri;
- }
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java
deleted file mode 100644
index 2e5c457f7b..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java
+++ /dev/null
@@ -1,145 +0,0 @@
-package app.revanced.extension.shared.requests;
-
-import app.revanced.extension.shared.Utils;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.net.HttpURLConnection;
-import java.net.URL;
-
-public class Requester {
- private Requester() {
- }
-
- public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException {
- return getConnectionFromCompiledRoute(apiUrl, route.compile(params));
- }
-
- public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
- String url = apiUrl + route.getCompiledRoute();
- HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
- // This request sends data via URL query parameters. No request body is included.
- // If a request body is added, the caller must set the appropriate Content-Length header.
- connection.setFixedLengthStreamingMode(0);
- connection.setRequestMethod(route.getMethod().name());
- String agentString = System.getProperty("http.agent")
- + "; ReVanced/" + Utils.getAppVersionName()
- + " (" + Utils.getPatchesReleaseVersion() + ")";
- connection.setRequestProperty("User-Agent", agentString);
-
- return connection;
- }
-
- /**
- * Parse the {@link HttpURLConnection}, and closes the underlying InputStream.
- */
- private static String parseInputStreamAndClose(InputStream inputStream) throws IOException {
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
- StringBuilder jsonBuilder = new StringBuilder();
- String line;
- while ((line = reader.readLine()) != null) {
- jsonBuilder.append(line);
- jsonBuilder.append('\n');
- }
- return jsonBuilder.toString();
- }
- }
-
- /**
- * Parse the {@link HttpURLConnection} response as a String.
- * This does not close the url connection. If further requests to this host are unlikely
- * in the near future, then instead use {@link #parseStringAndDisconnect(HttpURLConnection)}.
- */
- public static String parseString(HttpURLConnection connection) throws IOException {
- return parseInputStreamAndClose(connection.getInputStream());
- }
-
- /**
- * Parse the {@link HttpURLConnection} response as a String, and disconnect.
- *
- * Should only be used if other requests to the server in the near future are unlikely
- *
- * @see #parseString(HttpURLConnection)
- */
- public static String parseStringAndDisconnect(HttpURLConnection connection) throws IOException {
- String result = parseString(connection);
- connection.disconnect();
- return result;
- }
-
- /**
- * Parse the {@link HttpURLConnection} error stream as a String.
- * If the server sent no error response data, this returns an empty string.
- */
- public static String parseErrorString(HttpURLConnection connection) throws IOException {
- InputStream errorStream = connection.getErrorStream();
- if (errorStream == null) {
- return "";
- }
- return parseInputStreamAndClose(errorStream);
- }
-
- /**
- * Parse the {@link HttpURLConnection} error stream as a String, and disconnect.
- * If the server sent no error response data, this returns an empty string.
- *
- * Should only be used if other requests to the server are unlikely in the near future.
- *
- * @see #parseErrorString(HttpURLConnection)
- */
- public static String parseErrorStringAndDisconnect(HttpURLConnection connection) throws IOException {
- String result = parseErrorString(connection);
- connection.disconnect();
- return result;
- }
-
- /**
- * Parse the {@link HttpURLConnection} response into a JSONObject.
- * This does not close the url connection. If further requests to this host are unlikely
- * in the near future, then instead use {@link #parseJSONObjectAndDisconnect(HttpURLConnection)}.
- */
- public static JSONObject parseJSONObject(HttpURLConnection connection) throws JSONException, IOException {
- return new JSONObject(parseString(connection));
- }
-
- /**
- * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect.
- *
- * Should only be used if other requests to the server in the near future are unlikely
- *
- * @see #parseJSONObject(HttpURLConnection)
- */
- public static JSONObject parseJSONObjectAndDisconnect(HttpURLConnection connection) throws JSONException, IOException {
- JSONObject object = parseJSONObject(connection);
- connection.disconnect();
- return object;
- }
-
- /**
- * Parse the {@link HttpURLConnection}, and closes the underlying InputStream.
- * This does not close the url connection. If further requests to this host are unlikely
- * in the near future, then instead use {@link #parseJSONArrayAndDisconnect(HttpURLConnection)}.
- */
- public static JSONArray parseJSONArray(HttpURLConnection connection) throws JSONException, IOException {
- return new JSONArray(parseString(connection));
- }
-
- /**
- * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect.
- *
- * Should only be used if other requests to the server in the near future are unlikely
- *
- * @see #parseJSONArray(HttpURLConnection)
- */
- public static JSONArray parseJSONArrayAndDisconnect(HttpURLConnection connection) throws JSONException, IOException {
- JSONArray array = parseJSONArray(connection);
- connection.disconnect();
- return array;
- }
-
-}
\ No newline at end of file
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Route.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Route.java
deleted file mode 100644
index 74428224a7..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Route.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package app.revanced.extension.shared.requests;
-
-public class Route {
- private final String route;
- private final Method method;
- private final int paramCount;
-
- public Route(Method method, String route) {
- this.method = method;
- this.route = route;
- this.paramCount = countMatches(route, '{');
-
- if (paramCount != countMatches(route, '}'))
- throw new IllegalArgumentException("Not enough parameters");
- }
-
- public Method getMethod() {
- return method;
- }
-
- public CompiledRoute compile(String... params) {
- if (params.length != paramCount)
- throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " +
- "Expected: " + paramCount + ", provided: " + params.length);
-
- StringBuilder compiledRoute = new StringBuilder(route);
- for (int i = 0; i < paramCount; i++) {
- int paramStart = compiledRoute.indexOf("{");
- int paramEnd = compiledRoute.indexOf("}");
- compiledRoute.replace(paramStart, paramEnd + 1, params[i]);
- }
- return new CompiledRoute(this, compiledRoute.toString());
- }
-
- public static class CompiledRoute {
- private final Route baseRoute;
- private final String compiledRoute;
-
- private CompiledRoute(Route baseRoute, String compiledRoute) {
- this.baseRoute = baseRoute;
- this.compiledRoute = compiledRoute;
- }
-
- public String getCompiledRoute() {
- return compiledRoute;
- }
-
- public Method getMethod() {
- return baseRoute.method;
- }
- }
-
- private int countMatches(CharSequence seq, char c) {
- int count = 0;
- for (int i = 0, length = seq.length(); i < length; i++) {
- if (seq.charAt(i) == c)
- count++;
- }
- return count;
- }
-
- public enum Method {
- GET,
- POST
- }
-}
\ No newline at end of file
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java
deleted file mode 100644
index fbc734a51d..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java
+++ /dev/null
@@ -1,119 +0,0 @@
-package app.revanced.extension.shared.settings;
-
-import java.util.Locale;
-
-public enum AppLanguage {
- /**
- * The current app language.
- */
- DEFAULT,
-
- // Languages codes not included with YouTube, but are translated on Crowdin
- GA,
-
- // Language codes found in locale_config.xml
- // All region specific variants have been removed.
- AF,
- AM,
- AR,
- AS,
- AZ,
- BE,
- BG,
- BN,
- BS,
- CA,
- CS,
- DA,
- DE,
- EL,
- EN,
- ES,
- ET,
- EU,
- FA,
- FI,
- FR,
- GL,
- GU,
- HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code.
- HI,
- HR,
- HU,
- HY,
- ID,
- IS,
- IT,
- JA,
- KA,
- KK,
- KM,
- KN,
- KO,
- KY,
- LO,
- LT,
- LV,
- MK,
- ML,
- MN,
- MR,
- MS,
- MY,
- NB,
- NE,
- NL,
- OR,
- PA,
- PL,
- PT,
- RO,
- RU,
- SI,
- SK,
- SL,
- SQ,
- SR,
- SV,
- SW,
- TA,
- TE,
- TH,
- TL,
- TR,
- UK,
- UR,
- UZ,
- VI,
- ZH,
- ZU;
-
- private final String language;
- private final Locale locale;
-
- AppLanguage() {
- language = name().toLowerCase(Locale.US);
- locale = Locale.forLanguageTag(language);
- }
-
- /**
- * @return The 2 letter ISO 639_1 language code.
- */
- public String getLanguage() {
- // Changing the app language does not force the app to completely restart,
- // so the default needs to be the current language and not a static field.
- if (this == DEFAULT) {
- return Locale.getDefault().getLanguage();
- }
-
- return language;
- }
-
- public Locale getLocale() {
- if (this == DEFAULT) {
- return Locale.getDefault();
- }
-
- return locale;
- }
-}
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
deleted file mode 100644
index 1a2bfe9a2b..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java
+++ /dev/null
@@ -1,173 +0,0 @@
-package app.revanced.extension.shared.settings;
-
-import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.preference.PreferenceFragment;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-import android.widget.Toolbar;
-
-import androidx.annotation.RequiresApi;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-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.
- * Provides common logic for initializing the activity and setting up the toolbar.
- */
-@SuppressWarnings("deprecation")
-@RequiresApi(api = Build.VERSION_CODES.O)
-public abstract class BaseActivityHook extends Activity {
-
- private static final int ID_REVANCED_SETTINGS_FRAGMENTS =
- getResourceIdentifierOrThrow(ResourceType.ID, "revanced_settings_fragments");
- private static final int ID_REVANCED_TOOLBAR_PARENT =
- getResourceIdentifierOrThrow(ResourceType.ID, "revanced_toolbar_parent");
- public static final int LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR =
- getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_settings_with_toolbar");
- private static final int STRING_REVANCED_SETTINGS_TITLE =
- getResourceIdentifierOrThrow(ResourceType.STRING, "revanced_settings_title");
-
- /**
- * Layout parameters for the toolbar, extracted from the dummy toolbar.
- */
- protected static ViewGroup.LayoutParams toolbarLayoutParams;
-
- /**
- * Sets the layout parameters for the toolbar.
- */
- public static void setToolbarLayoutParams(Toolbar toolbar) {
- if (toolbarLayoutParams != null) {
- toolbar.setLayoutParams(toolbarLayoutParams);
- }
- }
-
- /**
- * Initializes the activity by setting the theme, content view and injecting a PreferenceFragment.
- */
- public static void initialize(BaseActivityHook hook, Activity activity) {
- try {
- hook.customizeActivityTheme(activity);
- activity.setContentView(hook.getContentViewResourceId());
-
- // Sanity check.
- String dataString = activity.getIntent().getDataString();
- if (!"revanced_settings_intent".equals(dataString)) {
- Logger.printException(() -> "Unknown intent: " + dataString);
- return;
- }
-
- PreferenceFragment fragment = hook.createPreferenceFragment();
- hook.createToolbar(activity, fragment);
-
- activity.getFragmentManager()
- .beginTransaction()
- .replace(ID_REVANCED_SETTINGS_FRAGMENTS, fragment)
- .commit();
- } catch (Exception ex) {
- Logger.printException(() -> "initialize failure", ex);
- }
- }
-
- /**
- * Injection point.
- * Overrides the ReVanced settings language.
- */
- @SuppressWarnings("unused")
- public static Context getAttachBaseContext(Context original) {
- AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
- if (language == AppLanguage.DEFAULT) {
- return original;
- }
-
- return Utils.getContext();
- }
-
- /**
- * Creates and configures a toolbar for the activity, replacing a dummy placeholder.
- */
- @SuppressLint("UseCompatLoadingForDrawables")
- protected void createToolbar(Activity activity, PreferenceFragment fragment) {
- // Replace dummy placeholder toolbar.
- // This is required to fix submenu title alignment issue with Android ASOP 15+
- ViewGroup toolbarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT);
- ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolbarParent, "revanced_toolbar");
- toolbarLayoutParams = dummyToolbar.getLayoutParams();
- toolbarParent.removeView(dummyToolbar);
-
- // Sets appropriate system navigation bar color for the activity.
- ToolbarPreferenceFragment.setNavigationBarColor(activity.getWindow());
-
- Toolbar toolbar = new Toolbar(toolbarParent.getContext());
- toolbar.setBackgroundColor(getToolbarBackgroundColor());
- toolbar.setNavigationIcon(getNavigationIcon());
- toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
- toolbar.setTitle(STRING_REVANCED_SETTINGS_TITLE);
-
- 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(20);
- }
- setToolbarLayoutParams(toolbar);
-
- onPostToolbarSetup(activity, toolbar, fragment);
-
- toolbarParent.addView(toolbar, 0);
- }
-
- /**
- * Returns the resource ID for the content view layout.
- */
- protected int getContentViewResourceId() {
- return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
- }
-
- /**
- * Customizes the activity's theme.
- */
- protected abstract void customizeActivityTheme(Activity activity);
-
- /**
- * Returns the background color for the toolbar.
- */
- protected abstract int getToolbarBackgroundColor();
-
- /**
- * Returns the navigation icon drawable for the toolbar.
- */
- protected abstract Drawable getNavigationIcon();
-
- /**
- * Returns the click listener for the toolbar's navigation icon.
- */
- protected abstract View.OnClickListener getNavigationClickListener(Activity activity);
-
- /**
- * Creates the PreferenceFragment to be injected into the activity.
- */
- protected PreferenceFragment createPreferenceFragment() {
- return new ToolbarPreferenceFragment();
- }
-
- /**
- * Performs additional setup after the toolbar is configured.
- *
- * @param activity The activity hosting the toolbar.
- * @param toolbar The configured toolbar.
- * @param fragment The PreferenceFragment associated with the activity.
- */
- protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {}
-}
diff --git a/extensions/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
deleted file mode 100644
index 5fc4418366..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package app.revanced.extension.shared.settings;
-
-import static java.lang.Boolean.FALSE;
-import static java.lang.Boolean.TRUE;
-import static app.revanced.extension.shared.patches.CustomBrandingPatch.BrandingTheme;
-import static app.revanced.extension.shared.settings.Setting.parent;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.patches.CustomBrandingPatch;
-
-/**
- * Settings shared across multiple apps.
- *
- * To ensure this class is loaded when the UI is created, app specific setting bundles should extend
- * or reference this class.
- */
-public class BaseSettings {
- public static final BooleanSetting DEBUG = new BooleanSetting("revanced_debug", FALSE);
- public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE, parent(DEBUG));
- public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message");
-
- public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false);
-
- public static final EnumSetting REVANCED_LANGUAGE = new EnumSetting<>("revanced_language", AppLanguage.DEFAULT, true, "revanced_language_user_dialog_message");
-
- /**
- * Use the icons declared in the preferences created during patching. If no icons or styles are declared then this setting does nothing.
- */
- public static final BooleanSetting SHOW_MENU_ICONS = new BooleanSetting("revanced_show_menu_icons", TRUE, true);
- /**
- * Do not use this setting directly. Instead use {@link app.revanced.extension.shared.Utils#appIsUsingBoldIcons()}
- */
- public static final BooleanSetting SETTINGS_DISABLE_BOLD_ICONS = new BooleanSetting("revanced_settings_disable_bold_icons", FALSE, true);
-
- public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true);
- public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "");
-
- /**
- * The first time the app was launched with no previous app data (either a clean install, or after wiping app data).
- */
- public static final LongSetting FIRST_TIME_APP_LAUNCHED = new LongSetting("revanced_last_time_app_was_launched", -1L, false, false);
-
- public static final BooleanSetting GMS_CORE_CHECK_UPDATES = new BooleanSetting("revanced_gms_core_check_updates", true, true);
-
- //
- // Settings shared by YouTube and YouTube Music.
- //
-
- public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
- public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_video_streams_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
-
- public static final BooleanSetting SANITIZE_SHARING_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
- public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);
-
- public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
-
- public static final EnumSetting CUSTOM_BRANDING_ICON = new EnumSetting<>("revanced_custom_branding_icon", CustomBrandingPatch.getDefaultIconStyle(), true);
- public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", CustomBrandingPatch.getDefaultAppNameIndex(), true);
-
- public static final StringSetting DISABLED_FEATURE_FLAGS = new StringSetting("revanced_disabled_feature_flags", "", true, parent(DEBUG));
-
- static {
- final long now = System.currentTimeMillis();
-
- if (FIRST_TIME_APP_LAUNCHED.get() < 0) {
- Logger.printInfo(() -> "First launch of installation with no prior app data");
- FIRST_TIME_APP_LAUNCHED.save(now);
- }
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java
deleted file mode 100644
index c67ebabf96..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package app.revanced.extension.shared.settings;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.Objects;
-
-@SuppressWarnings("unused")
-public class BooleanSetting extends Setting {
- public BooleanSetting(String key, Boolean defaultValue) {
- super(key, defaultValue);
- }
- public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp) {
- super(key, defaultValue, rebootApp);
- }
- public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport) {
- super(key, defaultValue, rebootApp, includeWithImportExport);
- }
- public BooleanSetting(String key, Boolean defaultValue, String userDialogMessage) {
- super(key, defaultValue, userDialogMessage);
- }
- public BooleanSetting(String key, Boolean defaultValue, Availability availability) {
- super(key, defaultValue, availability);
- }
- public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage) {
- super(key, defaultValue, rebootApp, userDialogMessage);
- }
- public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, Availability availability) {
- super(key, defaultValue, rebootApp, availability);
- }
- public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
- super(key, defaultValue, rebootApp, userDialogMessage, availability);
- }
- public BooleanSetting(@NonNull String key, @NonNull Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
- super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
- }
-
- /**
- * Sets, but does _not_ persistently save the value.
- * This method is only to be used by the Settings preference code.
- *
- * This intentionally is a static method to deter
- * accidental usage when {@link #save(Boolean)} was intended.
- */
- public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) {
- setting.value = Objects.requireNonNull(newValue);
-
- if (setting.isSetToDefault()) {
- setting.removeFromPreferences();
- }
- }
-
- @Override
- protected void load() {
- value = preferences.getBoolean(key, defaultValue);
- }
-
- @Override
- protected Boolean readFromJSON(JSONObject json, String importExportKey) throws JSONException {
- return json.getBoolean(importExportKey);
- }
-
- @Override
- protected void setValueFromString(@NonNull String newValue) {
- value = Boolean.valueOf(Objects.requireNonNull(newValue));
- }
-
- @Override
- public void saveToPreferences() {
- preferences.saveBoolean(key, value);
- }
-
- @NonNull
- @Override
- public Boolean get() {
- return value;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java
deleted file mode 100644
index 2c2cb6a3a8..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package app.revanced.extension.shared.settings;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.Locale;
-import java.util.Objects;
-
-import app.revanced.extension.shared.Logger;
-
-/**
- * If an Enum value is removed or changed, any saved or imported data using the
- * non-existent value will be reverted to the default value
- * (the event is logged, but no user error is displayed).
- *
- * All saved JSON text is converted to lowercase to keep the output less obnoxious.
- */
-@SuppressWarnings("unused")
-public class EnumSetting> extends Setting {
- public EnumSetting(String key, T defaultValue) {
- super(key, defaultValue);
- }
- public EnumSetting(String key, T defaultValue, boolean rebootApp) {
- super(key, defaultValue, rebootApp);
- }
- public EnumSetting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) {
- super(key, defaultValue, rebootApp, includeWithImportExport);
- }
- public EnumSetting(String key, T defaultValue, String userDialogMessage) {
- super(key, defaultValue, userDialogMessage);
- }
- public EnumSetting(String key, T defaultValue, Availability availability) {
- super(key, defaultValue, availability);
- }
- public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) {
- super(key, defaultValue, rebootApp, userDialogMessage);
- }
- public EnumSetting(String key, T defaultValue, boolean rebootApp, Availability availability) {
- super(key, defaultValue, rebootApp, availability);
- }
- public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
- super(key, defaultValue, rebootApp, userDialogMessage, availability);
- }
- public EnumSetting(@NonNull String key, @NonNull T defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
- super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
- }
-
- @Override
- protected void load() {
- value = preferences.getEnum(key, defaultValue);
- }
-
- @Override
- protected T readFromJSON(JSONObject json, String importExportKey) throws JSONException {
- String enumName = json.getString(importExportKey);
- try {
- return getEnumFromString(enumName);
- } catch (IllegalArgumentException ex) {
- // Info level to allow removing enum values in the future without showing any user errors.
- Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName, ex);
- return defaultValue;
- }
- }
-
- @Override
- protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException {
- // Use lowercase to keep the output less ugly.
- json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH));
- }
-
- /**
- * @param enumName Enum name. Casing does not matter.
- * @return Enum of this type with the same declared name.
- * @throws IllegalArgumentException if the name is not a valid enum of this type.
- */
- protected T getEnumFromString(String enumName) {
- //noinspection ConstantConditions
- for (Enum> value : defaultValue.getClass().getEnumConstants()) {
- if (value.name().equalsIgnoreCase(enumName)) {
- //noinspection unchecked
- return (T) value;
- }
- }
-
- throw new IllegalArgumentException("Unknown enum value: " + enumName);
- }
-
- @Override
- protected void setValueFromString(@NonNull String newValue) {
- value = getEnumFromString(Objects.requireNonNull(newValue));
- }
-
- @Override
- public void saveToPreferences() {
- preferences.saveEnumAsString(key, value);
- }
-
- @NonNull
- @Override
- public T get() {
- return value;
- }
-
- /**
- * Availability based on if this setting is currently set to any of the provided types.
- */
- @SafeVarargs
- public final Setting.Availability availability(T... types) {
- Objects.requireNonNull(types);
-
- return () -> {
- T currentEnumType = get();
- for (T enumType : types) {
- if (currentEnumType == enumType) return true;
- }
- return false;
- };
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java
deleted file mode 100644
index 59846e037f..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package app.revanced.extension.shared.settings;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.Objects;
-
-@SuppressWarnings("unused")
-public class FloatSetting extends Setting {
-
- public FloatSetting(String key, Float defaultValue) {
- super(key, defaultValue);
- }
- public FloatSetting(String key, Float defaultValue, boolean rebootApp) {
- super(key, defaultValue, rebootApp);
- }
- public FloatSetting(String key, Float defaultValue, boolean rebootApp, boolean includeWithImportExport) {
- super(key, defaultValue, rebootApp, includeWithImportExport);
- }
- public FloatSetting(String key, Float defaultValue, String userDialogMessage) {
- super(key, defaultValue, userDialogMessage);
- }
- public FloatSetting(String key, Float defaultValue, Availability availability) {
- super(key, defaultValue, availability);
- }
- public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage) {
- super(key, defaultValue, rebootApp, userDialogMessage);
- }
- public FloatSetting(String key, Float defaultValue, boolean rebootApp, Availability availability) {
- super(key, defaultValue, rebootApp, availability);
- }
- public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
- super(key, defaultValue, rebootApp, userDialogMessage, availability);
- }
- public FloatSetting(@NonNull String key, @NonNull Float defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
- super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
- }
-
- @Override
- protected void load() {
- value = preferences.getFloatString(key, defaultValue);
- }
-
- @Override
- protected Float readFromJSON(JSONObject json, String importExportKey) throws JSONException {
- return (float) json.getDouble(importExportKey);
- }
-
- @Override
- protected void setValueFromString(@NonNull String newValue) {
- value = Float.valueOf(Objects.requireNonNull(newValue));
- }
-
- @Override
- public void saveToPreferences() {
- preferences.saveFloatString(key, value);
- }
-
- @NonNull
- @Override
- public Float get() {
- return value;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java
deleted file mode 100644
index ccf128dfdd..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package app.revanced.extension.shared.settings;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.Objects;
-
-@SuppressWarnings("unused")
-public class IntegerSetting extends Setting {
-
- public IntegerSetting(String key, Integer defaultValue) {
- super(key, defaultValue);
- }
- public IntegerSetting(String key, Integer defaultValue, boolean rebootApp) {
- super(key, defaultValue, rebootApp);
- }
- public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, boolean includeWithImportExport) {
- super(key, defaultValue, rebootApp, includeWithImportExport);
- }
- public IntegerSetting(String key, Integer defaultValue, String userDialogMessage) {
- super(key, defaultValue, userDialogMessage);
- }
- public IntegerSetting(String key, Integer defaultValue, Availability availability) {
- super(key, defaultValue, availability);
- }
- public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage) {
- super(key, defaultValue, rebootApp, userDialogMessage);
- }
- public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, Availability availability) {
- super(key, defaultValue, rebootApp, availability);
- }
- public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
- super(key, defaultValue, rebootApp, userDialogMessage, availability);
- }
- public IntegerSetting(@NonNull String key, @NonNull Integer defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
- super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
- }
-
- @Override
- protected void load() {
- value = preferences.getIntegerString(key, defaultValue);
- }
-
- @Override
- protected Integer readFromJSON(JSONObject json, String importExportKey) throws JSONException {
- return json.getInt(importExportKey);
- }
-
- @Override
- protected void setValueFromString(@NonNull String newValue) {
- value = Integer.valueOf(Objects.requireNonNull(newValue));
- }
-
- @Override
- public void saveToPreferences() {
- preferences.saveIntegerString(key, value);
- }
-
- @NonNull
- @Override
- public Integer get() {
- return value;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/LongSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/LongSetting.java
deleted file mode 100644
index ea3adcebac..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/LongSetting.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package app.revanced.extension.shared.settings;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.Objects;
-
-@SuppressWarnings("unused")
-public class LongSetting extends Setting {
-
- public LongSetting(String key, Long defaultValue) {
- super(key, defaultValue);
- }
- public LongSetting(String key, Long defaultValue, boolean rebootApp) {
- super(key, defaultValue, rebootApp);
- }
- public LongSetting(String key, Long defaultValue, boolean rebootApp, boolean includeWithImportExport) {
- super(key, defaultValue, rebootApp, includeWithImportExport);
- }
- public LongSetting(String key, Long defaultValue, String userDialogMessage) {
- super(key, defaultValue, userDialogMessage);
- }
- public LongSetting(String key, Long defaultValue, Availability availability) {
- super(key, defaultValue, availability);
- }
- public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage) {
- super(key, defaultValue, rebootApp, userDialogMessage);
- }
- public LongSetting(String key, Long defaultValue, boolean rebootApp, Availability availability) {
- super(key, defaultValue, rebootApp, availability);
- }
- public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
- super(key, defaultValue, rebootApp, userDialogMessage, availability);
- }
- public LongSetting(@NonNull String key, @NonNull Long defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
- super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
- }
-
- @Override
- protected void load() {
- value = preferences.getLongString(key, defaultValue);
- }
-
- @Override
- protected Long readFromJSON(JSONObject json, String importExportKey) throws JSONException {
- return json.getLong(importExportKey);
- }
-
- @Override
- protected void setValueFromString(@NonNull String newValue) {
- value = Long.valueOf(Objects.requireNonNull(newValue));
- }
-
- @Override
- public void saveToPreferences() {
- preferences.saveLongString(key, value);
- }
-
- @NonNull
- @Override
- public Long get() {
- return value;
- }
-}
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
deleted file mode 100644
index 53a980e3c2..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java
+++ /dev/null
@@ -1,504 +0,0 @@
-package app.revanced.extension.shared.settings;
-
-import static app.revanced.extension.shared.StringRef.str;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.StringRef;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
-
-public abstract class Setting {
-
- /**
- * Indicates if a {@link Setting} is available to edit and use.
- * Typically this is dependent upon other BooleanSetting(s) set to 'true',
- * but this can be used to call into extension code and check other conditions.
- */
- public interface Availability {
- boolean isAvailable();
-
- /**
- * @return parent settings (dependencies) of this availability.
- */
- default List> getParentSettings() {
- return Collections.emptyList();
- }
- }
-
- /**
- * Availability based on a single parent setting being enabled.
- */
- public static Availability parent(BooleanSetting parent) {
- return new Availability() {
- @Override
- public boolean isAvailable() {
- return parent.get();
- }
-
- @Override
- public List> getParentSettings() {
- return Collections.singletonList(parent);
- }
- };
- }
-
- /**
- * Availability based on a single parent setting being disabled.
- */
- public static Availability parentNot(BooleanSetting parent) {
- return new Availability() {
- @Override
- public boolean isAvailable() {
- return !parent.get();
- }
-
- @Override
- public List> getParentSettings() {
- return Collections.singletonList(parent);
- }
- };
- }
-
- /**
- * Availability based on all parents being enabled.
- */
- public static Availability parentsAll(BooleanSetting... parents) {
- return new Availability() {
- @Override
- public boolean isAvailable() {
- for (BooleanSetting parent : parents) {
- if (!parent.get()) return false;
- }
- return true;
- }
-
- @Override
- public List> getParentSettings() {
- return Collections.unmodifiableList(Arrays.asList(parents));
- }
- };
- }
-
- /**
- * Availability based on any parent being enabled.
- */
- public static Availability parentsAny(BooleanSetting... parents) {
- return new Availability() {
- @Override
- public boolean isAvailable() {
- for (BooleanSetting parent : parents) {
- if (parent.get()) return true;
- }
- return false;
- }
-
- @Override
- public List> getParentSettings() {
- return Collections.unmodifiableList(Arrays.asList(parents));
- }
- };
- }
-
- /**
- * Callback for importing/exporting settings.
- */
- public interface ImportExportCallback {
- /**
- * Called after all settings have been imported.
- */
- void settingsImported(@Nullable Context context);
-
- /**
- * Called after all settings have been exported.
- */
- void settingsExported(@Nullable Context context);
- }
-
- private static final List importExportCallbacks = new ArrayList<>();
-
- /**
- * Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}.
- */
- public static void addImportExportCallback(ImportExportCallback callback) {
- importExportCallbacks.add(Objects.requireNonNull(callback));
- }
-
- /**
- * All settings that were instantiated.
- * When a new setting is created, it is automatically added to this list.
- */
- private static final List> SETTINGS = new ArrayList<>();
-
- /**
- * Map of setting path to setting object.
- */
- private static final Map> PATH_TO_SETTINGS = new HashMap<>();
-
- /**
- * Preference all instances are saved to.
- */
- public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs");
-
- @Nullable
- public static Setting> getSettingFromPath(String str) {
- return PATH_TO_SETTINGS.get(str);
- }
-
- /**
- * @return All settings that have been created.
- */
- public static List> allLoadedSettings() {
- return Collections.unmodifiableList(SETTINGS);
- }
-
- /**
- * @return All settings that have been created, sorted by keys.
- */
- private static List> allLoadedSettingsSorted() {
- //noinspection ComparatorCombinators
- Collections.sort(SETTINGS, (Setting> o1, Setting> o2) -> o1.key.compareTo(o2.key));
- return allLoadedSettings();
- }
-
- /**
- * The key used to store the value in the shared preferences.
- */
- public final String key;
-
- /**
- * The default value of the setting.
- */
- public final T defaultValue;
-
- /**
- * If the app should be rebooted, if this setting is changed
- */
- public final boolean rebootApp;
-
- /**
- * If this setting should be included when importing/exporting settings.
- */
- public final boolean includeWithImportExport;
-
- /**
- * If this setting is available to edit and use.
- * Not to be confused with it's status returned from {@link #get()}.
- */
- @Nullable
- private final Availability availability;
-
- /**
- * Confirmation message to display, if the user tries to change the setting from the default value.
- */
- @Nullable
- public final StringRef userDialogMessage;
-
- // Must be volatile, as some settings are read/write from different threads.
- // Of note, the object value is persistently stored using SharedPreferences (which is thread safe).
- /**
- * The value of the setting.
- */
- protected volatile T value;
-
- public Setting(String key, T defaultValue) {
- this(key, defaultValue, false, true, null, null);
- }
- public Setting(String key, T defaultValue, boolean rebootApp) {
- this(key, defaultValue, rebootApp, true, null, null);
- }
- public Setting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) {
- this(key, defaultValue, rebootApp, includeWithImportExport, null, null);
- }
- public Setting(String key, T defaultValue, String userDialogMessage) {
- this(key, defaultValue, false, true, userDialogMessage, null);
- }
- public Setting(String key, T defaultValue, Availability availability) {
- this(key, defaultValue, false, true, null, availability);
- }
- public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) {
- this(key, defaultValue, rebootApp, true, userDialogMessage, null);
- }
- public Setting(String key, T defaultValue, boolean rebootApp, Availability availability) {
- this(key, defaultValue, rebootApp, true, null, availability);
- }
- public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
- this(key, defaultValue, rebootApp, true, userDialogMessage, availability);
- }
-
- /**
- * A setting backed by a shared preference.
- *
- * @param key The key used to store the value in the shared preferences.
- * @param defaultValue The default value of the setting.
- * @param rebootApp If the app should be rebooted, if this setting is changed.
- * @param includeWithImportExport If this setting should be shown in the import/export dialog.
- * @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value.
- * @param availability Condition that must be true, for this setting to be available to configure.
- */
- public Setting(String key,
- T defaultValue,
- boolean rebootApp,
- boolean includeWithImportExport,
- @Nullable String userDialogMessage,
- @Nullable Availability availability
- ) {
- this.key = Objects.requireNonNull(key);
- this.value = this.defaultValue = Objects.requireNonNull(defaultValue);
- this.rebootApp = rebootApp;
- this.includeWithImportExport = includeWithImportExport;
- this.userDialogMessage = (userDialogMessage == null) ? null : new StringRef(userDialogMessage);
- this.availability = availability;
-
- SETTINGS.add(this);
- if (PATH_TO_SETTINGS.put(key, this) != null) {
- Logger.printException(() -> this.getClass().getSimpleName()
- + " error: Duplicate Setting key found: " + key);
- }
-
- load();
- }
-
- /**
- * Sets, but does _not_ persistently save the value.
- * This method is only to be used by the Settings preference code.
- *
- * This intentionally is a static method to deter
- * accidental usage when {@link #save(Object)} was intended.
- */
- public static void privateSetValueFromString(Setting> setting, String newValue) {
- setting.setValueFromString(newValue);
-
- // Clear the preference value since default is used, to allow changing
- // the changing the default for a future release. Without this after upgrading
- // the saved value will be whatever was the default when the app was first installed.
- if (setting.isSetToDefault()) {
- setting.removeFromPreferences();
- }
- }
-
- /**
- * Sets the value of {@link #value}, but do not save to {@link #preferences}.
- */
- protected abstract void setValueFromString(String newValue);
-
- /**
- * Load and set the value of {@link #value}.
- */
- protected abstract void load();
-
- /**
- * Persistently saves the value.
- */
- public final void save(T newValue) {
- if (value.equals(newValue)) {
- return;
- }
-
- // Must set before saving to preferences (otherwise importing fails to update UI correctly).
- value = Objects.requireNonNull(newValue);
-
- if (defaultValue.equals(newValue)) {
- removeFromPreferences();
- } else {
- saveToPreferences();
- }
- }
-
- /**
- * Save {@link #value} to {@link #preferences}.
- */
- protected abstract void saveToPreferences();
-
- /**
- * Remove {@link #value} from {@link #preferences}.
- */
- protected final void removeFromPreferences() {
- Logger.printDebug(() -> "Clearing stored preference value (reset to default): " + key);
- preferences.removeKey(key);
- }
-
- @NonNull
- public abstract T get();
-
- /**
- * Identical to calling {@link #save(Object)} using {@link #defaultValue}.
- *
- * @return The newly saved default value.
- */
- public T resetToDefault() {
- save(defaultValue);
- return defaultValue;
- }
-
- /**
- * @return if this setting can be configured and used.
- */
- public boolean isAvailable() {
- return availability == null || availability.isAvailable();
- }
-
- /**
- * Get the parent Settings that this setting depends on.
- * @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()
- : Objects.requireNonNullElse(availability.getParentSettings(), Collections.emptyList());
- }
-
- /**
- * @return if the currently set value is the same as {@link #defaultValue}.
- */
- public boolean isSetToDefault() {
- return value.equals(defaultValue);
- }
-
- @NonNull
- @Override
- public String toString() {
- return key + "=" + get();
- }
-
- // region Import / export
-
- /**
- * If a setting path has this prefix, then remove it before importing/exporting.
- */
- private static final String OPTIONAL_REVANCED_SETTINGS_PREFIX = "revanced_";
-
- /**
- * The path, minus any 'revanced' prefix to keep json concise.
- */
- private String getImportExportKey() {
- if (key.startsWith(OPTIONAL_REVANCED_SETTINGS_PREFIX)) {
- return key.substring(OPTIONAL_REVANCED_SETTINGS_PREFIX.length());
- }
- return key;
- }
-
- /**
- * @param importExportKey The JSON key. The JSONObject parameter will contain data for this key.
- * @return the value stored using the import/export key. Do not set any values in this method.
- */
- protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException;
-
- /**
- * Saves this instance to JSON.
- *
- * To keep the JSON simple and readable,
- * subclasses should not write out any embedded types (such as JSON Array or Dictionaries).
- *
- * If this instance is not a type supported natively by JSON (ie: it's not a String/Integer/Float/Long),
- * then subclasses can override this method and write out a String value representing the value.
- */
- protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException {
- json.put(importExportKey, value);
- }
-
- public static String exportToJson(@Nullable Context alertDialogContext) {
- try {
- JSONObject json = new JSONObject();
- for (Setting> setting : allLoadedSettingsSorted()) {
- String importExportKey = setting.getImportExportKey();
- if (json.has(importExportKey)) {
- throw new IllegalArgumentException("duplicate key found: " + importExportKey);
- }
-
- final boolean exportDefaultValues = false; // Enable to see what all settings looks like in the UI.
- //noinspection ConstantValue
- if (setting.includeWithImportExport && (!setting.isSetToDefault() || exportDefaultValues)) {
- setting.writeToJSON(json, importExportKey);
- }
- }
-
- for (ImportExportCallback callback : importExportCallbacks) {
- callback.settingsExported(alertDialogContext);
- }
-
- if (json.length() == 0) {
- return "";
- }
-
- String export = json.toString(0);
-
- // Remove the outer JSON braces to make the output more compact,
- // and leave less chance of the user forgetting to copy it
- return export.substring(2, export.length() - 2);
- } catch (JSONException e) {
- Logger.printException(() -> "Export failure", e); // should never happen
- return "";
- }
- }
-
- /**
- * @return if any settings that require a reboot were changed.
- */
- public static boolean importFromJSON(Context alertDialogContext, String settingsJsonString) {
- try {
- if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
- settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
- }
- JSONObject json = new JSONObject(settingsJsonString);
-
- boolean rebootSettingChanged = false;
- int numberOfSettingsImported = 0;
- //noinspection rawtypes
- for (Setting setting : SETTINGS) {
- String key = setting.getImportExportKey();
- if (json.has(key)) {
- Object value = setting.readFromJSON(json, key);
- if (!setting.get().equals(value)) {
- rebootSettingChanged |= setting.rebootApp;
- //noinspection unchecked
- setting.save(value);
- }
- numberOfSettingsImported++;
- } else if (setting.includeWithImportExport && !setting.isSetToDefault()) {
- Logger.printDebug(() -> "Resetting to default: " + setting);
- rebootSettingChanged |= setting.rebootApp;
- setting.resetToDefault();
- }
- }
-
- for (ImportExportCallback callback : importExportCallbacks) {
- callback.settingsImported(alertDialogContext);
- }
-
- // Use a delay, otherwise the toast can move about on screen from the dismissing dialog.
- final int numberOfSettingsImportedFinal = numberOfSettingsImported;
- Utils.runOnMainThreadDelayed(() -> Utils.showToastLong(numberOfSettingsImportedFinal == 0
- ? str("revanced_settings_import_reset")
- : str("revanced_settings_import_success", numberOfSettingsImportedFinal)),
- 150);
-
- return rebootSettingChanged;
- } catch (JSONException | IllegalArgumentException ex) {
- Utils.showToastLong(str("revanced_settings_import_failure_parse", ex.getMessage()));
- Logger.printInfo(() -> "", ex);
- } catch (Exception ex) {
- Logger.printException(() -> "Import failure: " + ex.getMessage(), ex); // should never happen
- }
- return false;
- }
-
- // End import / export
-
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/StringSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/StringSetting.java
deleted file mode 100644
index adb9beaa18..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/StringSetting.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package app.revanced.extension.shared.settings;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.Objects;
-
-@SuppressWarnings("unused")
-public class StringSetting extends Setting {
-
- public StringSetting(String key, String defaultValue) {
- super(key, defaultValue);
- }
- public StringSetting(String key, String defaultValue, boolean rebootApp) {
- super(key, defaultValue, rebootApp);
- }
- public StringSetting(String key, String defaultValue, boolean rebootApp, boolean includeWithImportExport) {
- super(key, defaultValue, rebootApp, includeWithImportExport);
- }
- public StringSetting(String key, String defaultValue, String userDialogMessage) {
- super(key, defaultValue, userDialogMessage);
- }
- public StringSetting(String key, String defaultValue, Availability availability) {
- super(key, defaultValue, availability);
- }
- public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage) {
- super(key, defaultValue, rebootApp, userDialogMessage);
- }
- public StringSetting(String key, String defaultValue, boolean rebootApp, Availability availability) {
- super(key, defaultValue, rebootApp, availability);
- }
- public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
- super(key, defaultValue, rebootApp, userDialogMessage, availability);
- }
- public StringSetting(@NonNull String key, @NonNull String defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
- super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
- }
-
- @Override
- protected void load() {
- value = preferences.getString(key, defaultValue);
- }
-
- @Override
- protected String readFromJSON(JSONObject json, String importExportKey) throws JSONException {
- return json.getString(importExportKey);
- }
-
- @Override
- protected void setValueFromString(@NonNull String newValue) {
- value = Objects.requireNonNull(newValue);
- }
-
- @Override
- public void saveToPreferences() {
- preferences.saveString(key, value);
- }
-
- @NonNull
- @Override
- public String get() {
- return value;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/YouTubeAndMusicSettings.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/YouTubeAndMusicSettings.java
deleted file mode 100644
index 221ce00456..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/YouTubeAndMusicSettings.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package app.revanced.extension.shared.settings;
-
-import static app.revanced.extension.shared.settings.Setting.parent;
-import static java.lang.Boolean.FALSE;
-
-public class YouTubeAndMusicSettings extends BaseSettings {
- // Custom filter
- public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE);
- public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER));
-
- // Miscellaneous
- public static final BooleanSetting DEBUG_PROTOCOLBUFFER = new BooleanSetting("revanced_debug_protocolbuffer", FALSE, false,
- "revanced_debug_protocolbuffer_user_dialog_message", parent(BaseSettings.DEBUG));
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java
deleted file mode 100644
index a515471a00..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java
+++ /dev/null
@@ -1,360 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import static app.revanced.extension.shared.StringRef.str;
-
-import android.annotation.SuppressLint;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.preference.Preference;
-import android.preference.PreferenceFragment;
-import android.preference.PreferenceGroup;
-import android.preference.PreferenceManager;
-import android.preference.PreferenceScreen;
-import android.preference.SwitchPreference;
-import android.preference.EditTextPreference;
-import android.preference.ListPreference;
-import android.util.Pair;
-import android.widget.LinearLayout;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Objects;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.settings.BooleanSetting;
-import app.revanced.extension.shared.settings.Setting;
-import app.revanced.extension.shared.ui.CustomDialog;
-
-@SuppressWarnings("deprecation")
-public abstract class AbstractPreferenceFragment extends PreferenceFragment {
-
- /**
- * Indicates that if a preference changes,
- * to apply the change from the Setting to the UI component.
- */
- public static boolean settingImportInProgress;
-
- /**
- * Prevents recursive calls during preference <-> UI syncing from showing extra dialogs.
- */
- private static boolean updatingPreference;
-
- /**
- * Used to prevent showing reboot dialog, if user cancels a setting user dialog.
- */
- private static boolean showingUserDialogMessage;
-
- /**
- * Confirm and restart dialog button text and title.
- * Set by subclasses if Strings cannot be added as a resource.
- */
- @Nullable
- protected static CharSequence restartDialogTitle, restartDialogMessage, restartDialogButtonText, confirmDialogTitle;
-
- private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
- try {
- if (updatingPreference) {
- Logger.printDebug(() -> "Ignoring preference change as sync is in progress");
- return;
- }
-
- Setting> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
- if (setting == null) {
- return;
- }
- Preference pref = findPreference(str);
- if (pref == null) {
- return;
- }
- Logger.printDebug(() -> "Preference changed: " + setting.key);
-
- if (!settingImportInProgress && !showingUserDialogMessage) {
- if (setting.userDialogMessage != null && !prefIsSetToDefault(pref, setting)) {
- // Do not change the setting yet, to allow preserving whatever
- // list/text value was previously set if it needs to be reverted.
- showSettingUserDialogConfirmation(pref, setting);
- return;
- } else if (setting.rebootApp) {
- showRestartDialog(getContext());
- }
- }
-
- updatingPreference = true;
- // Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
- // Updating here can cause a recursive call back into this same method.
- updatePreference(pref, setting, true, settingImportInProgress);
- // Update any other preference availability that may now be different.
- updateUIAvailability();
- updatingPreference = false;
- } catch (Exception ex) {
- Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
- }
- };
-
- /**
- * Initialize this instance, and do any custom behavior.
- *
- * To ensure all {@link Setting} instances are correctly synced to the UI,
- * it is important that subclasses make a call or otherwise reference their Settings class bundle
- * so all app specific {@link Setting} instances are loaded before this method returns.
- */
- protected void initialize() {
- String preferenceResourceName;
- if (BaseSettings.SHOW_MENU_ICONS.get()) {
- preferenceResourceName = Utils.appIsUsingBoldIcons()
- ? "revanced_prefs_icons_bold"
- : "revanced_prefs_icons";
- } else {
- preferenceResourceName = "revanced_prefs";
- }
-
- final var identifier = Utils.getResourceIdentifier(ResourceType.XML, preferenceResourceName);
- if (identifier == 0) return;
- addPreferencesFromResource(identifier);
-
- PreferenceScreen screen = getPreferenceScreen();
- Utils.sortPreferenceGroups(screen);
- Utils.setPreferenceTitlesToMultiLineIfNeeded(screen);
- }
-
- private void showSettingUserDialogConfirmation(Preference pref, Setting> setting) {
- Utils.verifyOnMainThread();
-
- final var context = getContext();
- if (confirmDialogTitle == null) {
- confirmDialogTitle = str("revanced_settings_confirm_user_dialog_title");
- }
-
- showingUserDialogMessage = true;
-
- CharSequence message = BulletPointPreference.formatIntoBulletPoints(
- Objects.requireNonNull(setting.userDialogMessage).toString());
-
- Pair dialogPair = CustomDialog.create(
- context,
- confirmDialogTitle, // Title.
- message,
- null, // No EditText.
- null, // OK button text.
- () -> {
- // OK button action. User confirmed, save to the Setting.
- updatePreference(pref, setting, true, false);
-
- // Update availability of other preferences that may be changed.
- updateUIAvailability();
-
- if (setting.rebootApp) {
- showRestartDialog(context);
- }
- },
- () -> {
- // Cancel button action. Restore whatever the setting was before the change.
- updatePreference(pref, setting, true, true);
- },
- null, // No Neutral button.
- null, // No Neutral button action.
- true // Dismiss dialog when onNeutralClick.
- );
-
- dialogPair.first.setOnDismissListener(d -> showingUserDialogMessage = false);
- dialogPair.first.setCancelable(false);
-
- // Show the dialog.
- dialogPair.first.show();
- }
-
- /**
- * Updates all Preferences values and their availability using the current values in {@link Setting}.
- */
- protected void updateUIToSettingValues() {
- updatePreferenceScreen(getPreferenceScreen(), true, true);
- }
-
- /**
- * Updates Preferences availability only using the status of {@link Setting}.
- */
- protected void updateUIAvailability() {
- updatePreferenceScreen(getPreferenceScreen(), false, false);
- }
-
- /**
- * @return If the preference is currently set to the default value of the Setting.
- */
- protected boolean prefIsSetToDefault(Preference pref, Setting> setting) {
- Object defaultValue = setting.defaultValue;
- if (pref instanceof SwitchPreference switchPref) {
- return switchPref.isChecked() == (Boolean) defaultValue;
- }
- String defaultValueString = defaultValue.toString();
- if (pref instanceof EditTextPreference editPreference) {
- return editPreference.getText().equals(defaultValueString);
- }
- if (pref instanceof ListPreference listPref) {
- return listPref.getValue().equals(defaultValueString);
- }
-
- throw new IllegalStateException("Must override method to handle "
- + "preference type: " + pref.getClass());
- }
-
- /**
- * Syncs all UI Preferences to any {@link Setting} they represent.
- */
- private void updatePreferenceScreen(@NonNull PreferenceGroup group,
- boolean syncSettingValue,
- boolean applySettingToPreference) {
- // Alternatively this could iterate through all Settings and check for any matching Preferences,
- // but there are many more Settings than UI preferences so it's more efficient to only check
- // the Preferences.
- for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
- Preference pref = group.getPreference(i);
- if (pref instanceof PreferenceGroup subGroup) {
- updatePreferenceScreen(subGroup, syncSettingValue, applySettingToPreference);
- } else if (pref.hasKey()) {
- String key = pref.getKey();
- Setting> setting = Setting.getSettingFromPath(key);
-
- if (setting != null) {
- updatePreference(pref, setting, syncSettingValue, applySettingToPreference);
- } else if (BaseSettings.DEBUG.get() && (pref instanceof SwitchPreference
- || pref instanceof EditTextPreference || pref instanceof ListPreference)) {
- // Probably a typo in the patches preference declaration.
- Logger.printException(() -> "Preference key has no setting: " + key);
- }
- }
- }
- }
-
- /**
- * Handles syncing a UI Preference with the {@link Setting} that backs it.
- * If needed, subclasses can override this to handle additional UI Preference types.
- *
- * @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
- * If false, then apply {@link Setting} <- Preference.
- */
- protected void syncSettingWithPreference(@NonNull Preference pref,
- @NonNull Setting> setting,
- boolean applySettingToPreference) {
- if (pref instanceof SwitchPreference switchPref) {
- BooleanSetting boolSetting = (BooleanSetting) setting;
- if (applySettingToPreference) {
- switchPref.setChecked(boolSetting.get());
- } else {
- BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
- }
- } else if (pref instanceof EditTextPreference editPreference) {
- if (applySettingToPreference) {
- editPreference.setText(setting.get().toString());
- } else {
- Setting.privateSetValueFromString(setting, editPreference.getText());
- }
- } else if (pref instanceof ListPreference listPref) {
- if (applySettingToPreference) {
- listPref.setValue(setting.get().toString());
- } else {
- Setting.privateSetValueFromString(setting, listPref.getValue());
- }
- updateListPreferenceSummary(listPref, setting);
- } else if (!pref.getClass().equals(Preference.class)) {
- // Ignore root preference class because there is no data to sync.
- Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
- }
- }
-
- /**
- * Updates a UI Preference with the {@link Setting} that backs it.
- *
- * @param syncSetting If the UI should be synced {@link Setting} <-> Preference
- * @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
- * If false, then apply {@link Setting} <- Preference.
- */
- private void updatePreference(@NonNull Preference pref, @NonNull Setting> setting,
- boolean syncSetting, boolean applySettingToPreference) {
- if (!syncSetting && applySettingToPreference) {
- throw new IllegalArgumentException();
- }
-
- if (syncSetting) {
- syncSettingWithPreference(pref, setting, applySettingToPreference);
- }
-
- updatePreferenceAvailability(pref, setting);
- }
-
- protected void updatePreferenceAvailability(@NonNull Preference pref, @NonNull Setting> setting) {
- pref.setEnabled(setting.isAvailable());
- }
-
- protected void updateListPreferenceSummary(ListPreference listPreference, Setting> setting) {
- String objectStringValue = setting.get().toString();
- final int entryIndex = listPreference.findIndexOfValue(objectStringValue);
- if (entryIndex >= 0) {
- listPreference.setSummary(listPreference.getEntries()[entryIndex]);
- } else {
- // Value is not an available option.
- // User manually edited import data, or options changed and current selection is no longer available.
- // Still show the value in the summary, so it's clear that something is selected.
- listPreference.setSummary(objectStringValue);
- }
- }
-
- public static void showRestartDialog(Context context) {
- Utils.verifyOnMainThread();
- if (restartDialogTitle == null) {
- restartDialogTitle = str("revanced_settings_restart_title");
- }
- if (restartDialogMessage == null) {
- restartDialogMessage = str("revanced_settings_restart_dialog_message");
- }
- if (restartDialogButtonText == null) {
- restartDialogButtonText = str("revanced_settings_restart");
- }
-
- Pair dialogPair = CustomDialog.create(
- context,
- restartDialogTitle, // Title.
- restartDialogMessage, // Message.
- null, // No EditText.
- restartDialogButtonText, // OK button text.
- () -> Utils.restartApp(context), // OK button action.
- () -> {}, // Cancel button action (dismiss only).
- null, // No Neutral button text.
- null, // No Neutral button action.
- true // Dismiss dialog when onNeutralClick.
- );
-
- // Show the dialog.
- dialogPair.first.show();
- }
-
- @SuppressLint("ResourceType")
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- try {
- PreferenceManager preferenceManager = getPreferenceManager();
- preferenceManager.setSharedPreferencesName(Setting.preferences.name);
-
- // Must initialize before adding change listener,
- // otherwise the syncing of Setting -> UI
- // causes a callback to the listener even though nothing changed.
- initialize();
- updateUIToSettingValues();
-
- preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);
- } catch (Exception ex) {
- Logger.printException(() -> "onCreate() failure", ex);
- }
- }
-
- @Override
- public void onDestroy() {
- getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener);
- super.onDestroy();
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointPreference.java
deleted file mode 100644
index ee3f02fc8c..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointPreference.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import android.content.Context;
-import android.preference.Preference;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.SpannedString;
-import android.text.TextUtils;
-import android.text.style.BulletSpan;
-import android.util.AttributeSet;
-
-/**
- * Formats the summary text bullet points into Spanned text for better presentation.
- */
-@SuppressWarnings({"unused", "deprecation"})
-public class BulletPointPreference extends Preference {
-
- /**
- * Replaces bullet points with styled spans.
- */
- public static CharSequence formatIntoBulletPoints(CharSequence source) {
- final char bulletPoint = '•';
- if (TextUtils.indexOf(source, bulletPoint) < 0) {
- return source; // Nothing to do.
- }
-
- SpannableStringBuilder builder = new SpannableStringBuilder(source);
-
- int lineStart = 0;
- int length = builder.length();
-
- while (lineStart < length) {
- int lineEnd = TextUtils.indexOf(builder, '\n', lineStart);
- if (lineEnd < 0) lineEnd = length;
-
- // Apply BulletSpan only if the line starts with the '•' character.
- if (lineEnd > lineStart && builder.charAt(lineStart) == bulletPoint) {
- int deleteEnd = lineStart + 1; // remove the bullet itself
-
- // If there's a single space right after the bullet, remove that too.
- if (deleteEnd < builder.length() && builder.charAt(deleteEnd) == ' ') {
- deleteEnd++;
- }
-
- builder.delete(lineStart, deleteEnd);
-
- // Apply the BulletSpan to the remainder of that line.
- builder.setSpan(new BulletSpan(20),
- lineStart,
- lineEnd - (deleteEnd - lineStart), // adjust for deleted chars.
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
- );
-
- // Update total length and lineEnd after deletion.
- length = builder.length();
- final int removed = deleteEnd - lineStart;
- lineEnd -= removed;
- }
-
- lineStart = lineEnd + 1;
- }
-
- return new SpannedString(builder);
- }
-
- public BulletPointPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- public BulletPointPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public BulletPointPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public BulletPointPreference(Context context) {
- super(context);
- }
-
- @Override
- public void setSummary(CharSequence summary) {
- super.setSummary(formatIntoBulletPoints(summary));
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointSwitchPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointSwitchPreference.java
deleted file mode 100644
index ccbbf1eef9..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointSwitchPreference.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import static app.revanced.extension.shared.settings.preference.BulletPointPreference.formatIntoBulletPoints;
-
-import android.content.Context;
-import android.preference.SwitchPreference;
-import android.util.AttributeSet;
-
-/**
- * Formats the summary text bullet points into Spanned text for better presentation.
- */
-@SuppressWarnings({"unused", "deprecation"})
-public class BulletPointSwitchPreference extends SwitchPreference {
-
- public BulletPointSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- public BulletPointSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public BulletPointSwitchPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public BulletPointSwitchPreference(Context context) {
- super(context);
- }
-
- @Override
- public void setSummary(CharSequence summary) {
- super.setSummary(formatIntoBulletPoints(summary));
- }
-
- @Override
- public void setSummaryOn(CharSequence summaryOn) {
- super.setSummaryOn(formatIntoBulletPoints(summaryOn));
- }
-
- @Override
- public void setSummaryOff(CharSequence summaryOff) {
- super.setSummaryOff(formatIntoBulletPoints(summaryOff));
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ClearLogBufferPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ClearLogBufferPreference.java
deleted file mode 100644
index 7dbf0dd387..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ClearLogBufferPreference.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.preference.Preference;
-
-/**
- * A custom preference that clears the ReVanced debug log buffer when clicked.
- * Invokes the {@link LogBufferManager#clearLogBuffer} method.
- */
-@SuppressWarnings("unused")
-public class ClearLogBufferPreference extends Preference {
-
- {
- setOnPreferenceClickListener(pref -> {
- LogBufferManager.clearLogBuffer();
- return true;
- });
- }
-
- public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
- public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
- public ClearLogBufferPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public ClearLogBufferPreference(Context context) {
- super(context);
- }
-}
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
deleted file mode 100644
index c9fc7b6da9..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java
+++ /dev/null
@@ -1,476 +0,0 @@
-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.app.Dialog;
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.Typeface;
-import android.os.Build;
-import android.os.Bundle;
-import android.preference.EditTextPreference;
-import android.text.Editable;
-import android.text.InputType;
-import android.text.TextWatcher;
-import android.util.AttributeSet;
-import android.util.Pair;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.ScrollView;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.Nullable;
-
-import java.util.Locale;
-import java.util.regex.Pattern;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-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.
- * Extends {@link EditTextPreference} to display a colored dot in the widget area,
- * reflecting the currently selected color. The dot is dimmed when the preference is disabled.
- */
-@SuppressWarnings({"unused", "deprecation"})
-public class ColorPickerPreference extends EditTextPreference {
- /** Length of a valid color string of format #RRGGBB (without alpha) or #AARRGGBB (with alpha). */
- public static final int COLOR_STRING_LENGTH_WITHOUT_ALPHA = 7;
- public static final int COLOR_STRING_LENGTH_WITH_ALPHA = 9;
-
- /** Matches everything that is not a hex number/letter. */
- private static final Pattern PATTERN_NOT_HEX = Pattern.compile("[^0-9A-Fa-f]");
-
- /** Alpha for dimming when the preference is disabled. */
- public static final float DISABLED_ALPHA = 0.5f; // 50%
-
- /** View displaying a colored dot in the widget area. */
- private View widgetColorDot;
-
- /** Dialog View displaying a colored dot for the selected color preview in the dialog. */
- private View dialogColorDot;
-
- /** Current color, including alpha channel if opacity slider is enabled. */
- @ColorInt
- private int currentColor;
-
- /** Associated setting for storing the color value. */
- private StringSetting colorSetting;
-
- /** Dialog TextWatcher for the EditText to monitor color input changes. */
- private TextWatcher colorTextWatcher;
-
- /** Dialog color picker view. */
- protected ColorPickerView dialogColorPickerView;
-
- /** Listener for color changes. */
- protected OnColorChangeListener colorChangeListener;
-
- /** Whether the opacity slider is enabled. */
- private boolean opacitySliderEnabled = false;
-
- public static final int ID_REVANCED_COLOR_PICKER_VIEW =
- getResourceIdentifierOrThrow(ResourceType.ID, "revanced_color_picker_view");
- public static final int ID_PREFERENCE_COLOR_DOT =
- getResourceIdentifierOrThrow(ResourceType.ID, "preference_color_dot");
- public static final int LAYOUT_REVANCED_COLOR_DOT_WIDGET =
- getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_color_dot_widget");
- public static final int LAYOUT_REVANCED_COLOR_PICKER =
- getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_color_picker");
-
- /**
- * Removes non valid hex characters, converts to all uppercase,
- * and adds # character to the start if not present.
- */
- public static String cleanupColorCodeString(String colorString, boolean includeAlpha) {
- String result = "#" + PATTERN_NOT_HEX.matcher(colorString)
- .replaceAll("").toUpperCase(Locale.ROOT);
-
- int maxLength = includeAlpha ? COLOR_STRING_LENGTH_WITH_ALPHA : COLOR_STRING_LENGTH_WITHOUT_ALPHA;
- if (result.length() < maxLength) {
- return result;
- }
-
- return result.substring(0, maxLength);
- }
-
- /**
- * @param color Color, with or without alpha channel.
- * @param includeAlpha Whether to include the alpha channel in the output string.
- * @return #RRGGBB or #AARRGGBB hex color string
- */
- public static String getColorString(@ColorInt int color, boolean includeAlpha) {
- if (includeAlpha) {
- return String.format("#%08X", color);
- }
- color = color & 0x00FFFFFF; // Mask to strip alpha.
- return String.format("#%06X", color);
- }
-
- /**
- * Interface for notifying color changes.
- */
- public interface OnColorChangeListener {
- void onColorChanged(String key, int newColor);
- }
-
- /**
- * Sets the listener for color changes.
- */
- public void setOnColorChangeListener(OnColorChangeListener listener) {
- this.colorChangeListener = listener;
- }
-
- /**
- * Enables or disables the opacity slider in the color picker dialog.
- */
- public void setOpacitySliderEnabled(boolean enabled) {
- this.opacitySliderEnabled = enabled;
- }
-
- public ColorPickerPreference(Context context) {
- super(context);
- init();
- }
-
- public ColorPickerPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
-
- public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- init();
- }
-
- /**
- * Initializes the preference by setting up the EditText, loading the color, and set the widget layout.
- */
- private void init() {
- if (getKey() != null) {
- colorSetting = (StringSetting) Setting.getSettingFromPath(getKey());
- if (colorSetting == null) {
- Logger.printException(() -> "Could not find color setting for: " + getKey());
- }
- } else {
- Logger.printDebug(() -> "initialized without key, settings will be loaded later");
- }
-
- EditText editText = getEditText();
- editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
- | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- editText.setAutofillHints((String) null);
- }
-
- // Set the widget layout to a custom layout containing the colored dot.
- setWidgetLayoutResource(LAYOUT_REVANCED_COLOR_DOT_WIDGET);
- }
-
- /**
- * Sets the selected color and updates the UI and settings.
- */
- @Override
- public void setText(String colorString) {
- try {
- Logger.printDebug(() -> "setText: " + colorString);
- super.setText(colorString);
-
- currentColor = Color.parseColor(colorString);
- if (colorSetting != null) {
- colorSetting.save(getColorString(currentColor, opacitySliderEnabled));
- }
- updateDialogColorDot();
- updateWidgetColorDot();
-
- // Notify the listener about the color change.
- if (colorChangeListener != null) {
- colorChangeListener.onColorChanged(getKey(), currentColor);
- }
- } catch (IllegalArgumentException ex) {
- // This code is reached if the user pastes settings json with an invalid color
- // since this preference is updated with the new setting text.
- Logger.printDebug(() -> "Parse color error: " + colorString, ex);
- Utils.showToastShort(str("revanced_settings_color_invalid"));
- setText(colorSetting.resetToDefault());
- } catch (Exception ex) {
- Logger.printException(() -> "setText failure: " + colorString, ex);
- }
- }
-
- /**
- * Creates a TextWatcher to monitor changes in the EditText for color input.
- */
- private TextWatcher createColorTextWatcher(ColorPickerView colorPickerView) {
- return 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) {
- }
-
- @Override
- public void afterTextChanged(Editable edit) {
- try {
- String colorString = edit.toString();
- String sanitizedColorString = cleanupColorCodeString(colorString, opacitySliderEnabled);
- if (!sanitizedColorString.equals(colorString)) {
- edit.replace(0, colorString.length(), sanitizedColorString);
- return;
- }
-
- int expectedLength = opacitySliderEnabled
- ? COLOR_STRING_LENGTH_WITH_ALPHA
- : COLOR_STRING_LENGTH_WITHOUT_ALPHA;
- if (sanitizedColorString.length() != expectedLength) {
- return;
- }
-
- final int newColor = Color.parseColor(colorString);
- if (currentColor != newColor) {
- Logger.printDebug(() -> "afterTextChanged: " + sanitizedColorString);
- currentColor = newColor;
- updateDialogColorDot();
- updateWidgetColorDot();
- colorPickerView.setColor(newColor);
- }
- } catch (Exception ex) {
- // Should never be reached since input is validated before using.
- Logger.printException(() -> "afterTextChanged failure", ex);
- }
- }
- };
- }
-
- /**
- * Hook for subclasses to add a custom view to the top of the dialog.
- */
- @Nullable
- protected View createExtraDialogContentView(Context context) {
- return null; // Default implementation returns no extra view.
- }
-
- /**
- * Hook for subclasses to handle the OK button click.
- */
- protected void onDialogOkClicked() {
- // Default implementation does nothing.
- }
-
- /**
- * Hook for subclasses to handle the Neutral button click.
- */
- protected void onDialogNeutralClicked() {
- // Default implementation.
- try {
- final int defaultColor = Color.parseColor(colorSetting.defaultValue);
- dialogColorPickerView.setColor(defaultColor);
- } catch (Exception ex) {
- Logger.printException(() -> "Reset button failure", ex);
- }
- }
-
- @Override
- protected void showDialog(Bundle state) {
- Context context = getContext();
-
- // Create content container for all dialog views.
- LinearLayout contentContainer = new LinearLayout(context);
- contentContainer.setOrientation(LinearLayout.VERTICAL);
-
- // Add extra view from subclass if it exists.
- View extraView = createExtraDialogContentView(context);
- if (extraView != null) {
- contentContainer.addView(extraView);
- }
-
- // Inflate color picker view.
- View colorPicker = LayoutInflater.from(context).inflate(LAYOUT_REVANCED_COLOR_PICKER, null);
- dialogColorPickerView = colorPicker.findViewById(ID_REVANCED_COLOR_PICKER_VIEW);
- dialogColorPickerView.setOpacitySliderEnabled(opacitySliderEnabled);
- dialogColorPickerView.setColor(currentColor);
- contentContainer.addView(colorPicker);
-
- // Horizontal layout for preview and EditText.
- LinearLayout inputLayout = new LinearLayout(context);
- inputLayout.setOrientation(LinearLayout.HORIZONTAL);
- inputLayout.setGravity(Gravity.CENTER_VERTICAL);
-
- dialogColorDot = new View(context);
- 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();
-
- EditText editText = getEditText();
- ViewParent parent = editText.getParent();
- if (parent instanceof ViewGroup parentViewGroup) {
- parentViewGroup.removeView(editText);
- }
- editText.setLayoutParams(new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- ));
- String currentColorString = getColorString(currentColor, opacitySliderEnabled);
- editText.setText(currentColorString);
- editText.setSelection(currentColorString.length());
- editText.setTypeface(Typeface.MONOSPACE);
- colorTextWatcher = createColorTextWatcher(dialogColorPickerView);
- editText.addTextChangedListener(colorTextWatcher);
- inputLayout.addView(editText);
-
- // Add a dummy view to take up remaining horizontal space,
- // otherwise it will show an oversize underlined text view.
- View paddingView = new View(context);
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
- 0,
- LinearLayout.LayoutParams.MATCH_PARENT,
- 1f
- );
- paddingView.setLayoutParams(params);
- inputLayout.addView(paddingView);
-
- contentContainer.addView(inputLayout);
-
- // Create ScrollView to wrap the content container.
- ScrollView contentScrollView = new ScrollView(context);
- contentScrollView.setVerticalScrollBarEnabled(false);
- contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
- LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- 0,
- 1.0f
- );
- contentScrollView.setLayoutParams(scrollViewParams);
- contentScrollView.addView(contentContainer);
-
- final int originalColor = currentColor;
- Pair dialogPair = CustomDialog.create(
- context,
- getTitle() != null ? getTitle().toString() : str("revanced_settings_color_picker_title"),
- null,
- null,
- null,
- () -> { // OK button action.
- try {
- String colorString = editText.getText().toString();
- int expectedLength = opacitySliderEnabled
- ? COLOR_STRING_LENGTH_WITH_ALPHA
- : COLOR_STRING_LENGTH_WITHOUT_ALPHA;
- if (colorString.length() != expectedLength) {
- Utils.showToastShort(str("revanced_settings_color_invalid"));
- setText(getColorString(originalColor, opacitySliderEnabled));
- return;
- }
- setText(colorString);
-
- onDialogOkClicked();
- } catch (Exception ex) {
- // Should never happen due to a bad color string,
- // since the text is validated and fixed while the user types.
- Logger.printException(() -> "OK button failure", ex);
- }
- },
- () -> { // Cancel button action.
- try {
- setText(getColorString(originalColor, opacitySliderEnabled));
- } catch (Exception ex) {
- Logger.printException(() -> "Cancel button failure", ex);
- }
- },
- str("revanced_settings_reset_color"), // Neutral button text.
- this::onDialogNeutralClicked, // Neutral button action.
- false // Do not dismiss dialog.
- );
-
- // Add the ScrollView to the dialog's main layout.
- LinearLayout dialogMainLayout = dialogPair.second;
- dialogMainLayout.addView(contentScrollView, dialogMainLayout.getChildCount() - 1);
-
- // Set up color picker listener with debouncing.
- // Add listener last to prevent callbacks from set calls above.
- dialogColorPickerView.setOnColorChangedListener(color -> {
- // Check if it actually changed, since this callback
- // can be caused by updates in afterTextChanged().
- if (currentColor == color) {
- return;
- }
-
- String updatedColorString = getColorString(color, opacitySliderEnabled);
- Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
- currentColor = color;
- editText.setText(updatedColorString);
- editText.setSelection(updatedColorString.length());
-
- updateDialogColorDot();
- updateWidgetColorDot();
- });
-
- // Configure and show the dialog.
- Dialog dialog = dialogPair.first;
- dialog.setCanceledOnTouchOutside(false);
- dialog.show();
- }
-
- @Override
- protected void onDialogClosed(boolean positiveResult) {
- super.onDialogClosed(positiveResult);
-
- if (colorTextWatcher != null) {
- getEditText().removeTextChangedListener(colorTextWatcher);
- colorTextWatcher = null;
- }
-
- dialogColorDot = null;
- dialogColorPickerView = null;
- }
-
- @Override
- public void setEnabled(boolean enabled) {
- super.setEnabled(enabled);
- updateWidgetColorDot();
- }
-
- @Override
- protected void onBindView(View view) {
- super.onBindView(view);
-
- widgetColorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT);
- updateWidgetColorDot();
- }
-
- private void updateWidgetColorDot() {
- if (widgetColorDot == null) return;
-
- ColorDot.applyColorDot(
- widgetColorDot,
- currentColor,
- widgetColorDot.isEnabled()
- );
- }
-
- private void updateDialogColorDot() {
- if (dialogColorDot == null) return;
-
- ColorDot.applyColorDot(
- dialogColorDot,
- currentColor,
- true
- );
- }
-}
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
deleted file mode 100644
index b8c9577112..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java
+++ /dev/null
@@ -1,639 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.ComposeShader;
-import android.graphics.LinearGradient;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.RectF;
-import android.graphics.Shader;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-
-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
- * and an optional opacity slider.
- * This implementation is density-independent and responsive across different screen sizes and DPIs.
- *
- * This view displays three main components for color selection:
- *
- * Hue Bar: A horizontal bar at the bottom that allows the user to select the hue component of the color.
- * Saturation-Value Selector: A rectangular area above the hue bar that allows the user to select the
- * saturation and value (brightness) components of the color based on the selected hue.
- * Opacity Slider: An optional horizontal bar below the hue bar that allows the user to adjust
- * the opacity (alpha channel) of the color.
- *
- *
- * The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar,
- * opacity slider, and the saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles).
- *
- * The selected color can be retrieved using {@link #getColor()} and can be set using {@link #setColor(int)}.
- * An {@link OnColorChangedListener} can be registered to receive notifications when the selected color changes.
- */
-public class ColorPickerView extends View {
- /**
- * Interface definition for a callback to be invoked when the selected color changes.
- */
- public interface OnColorChangedListener {
- /**
- * Called when the selected color has changed.
- */
- void onColorChanged(@ColorInt int color);
- }
-
- /** Expanded touch area for the hue and opacity bars to increase the touch-sensitive area. */
- 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 = Dim.dp24;
-
- /** Padding around the view. */
- private static final float VIEW_PADDING = Dim.dp16;
-
- /** Height of the hue bar. */
- private static final float HUE_BAR_HEIGHT = Dim.dp12;
-
- /** Height of the opacity slider. */
- private static final float OPACITY_BAR_HEIGHT = Dim.dp12;
-
- /** Corner radius for the hue bar. */
- private static final float HUE_CORNER_RADIUS = Dim.dp6;
-
- /** Corner radius for the opacity slider. */
- private static final float OPACITY_CORNER_RADIUS = Dim.dp6;
-
- /** Radius of the selector handles. */
- private static final float SELECTOR_RADIUS = Dim.dp12;
-
- /** Stroke width for the selector handle outlines. */
- private static final float SELECTOR_STROKE_WIDTH = 8;
-
- /**
- * Hue and opacity fill radius. Use slightly smaller radius for the selector handle fill,
- * otherwise the anti-aliasing causes the fill color to bleed past the selector outline.
- */
- private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2;
-
- /** Thin dark outline stroke width for the selector rings. */
- private static final float SELECTOR_EDGE_STROKE_WIDTH = 1;
-
- /** Radius for the outer edge of the selector rings, including stroke width. */
- public static final float SELECTOR_EDGE_RADIUS =
- SELECTOR_RADIUS + SELECTOR_STROKE_WIDTH / 2 + SELECTOR_EDGE_STROKE_WIDTH / 2;
-
- /** Selector outline inner color. */
- @ColorInt
- private static final int SELECTOR_OUTLINE_COLOR = Color.WHITE;
-
- /** Dark edge color for the selector rings. */
- @ColorInt
- private static final int SELECTOR_EDGE_COLOR = Color.parseColor("#CFCFCF");
-
- /** Precomputed array of hue colors for the hue bar (0-360 degrees). */
- private static final int[] HUE_COLORS = new int[361];
- static {
- for (int i = 0; i < 361; i++) {
- HUE_COLORS[i] = Color.HSVToColor(new float[]{i, 1, 1});
- }
- }
-
- /** Paint for the hue bar. */
- private final Paint huePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
- /** Paint for the opacity slider. */
- private final Paint opacityPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
- /** Paint for the saturation-value selector. */
- private final Paint saturationValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
- /** Paint for the draggable selector handles. */
- private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- {
- selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
- }
-
- /** Bounds of the hue bar. */
- private final RectF hueRect = new RectF();
-
- /** Bounds of the opacity slider. */
- private final RectF opacityRect = new RectF();
-
- /** Bounds of the saturation-value selector. */
- private final RectF saturationValueRect = new RectF();
-
- /** HSV color calculations to avoid allocations during drawing. */
- private final float[] hsvArray = {1, 1, 1};
-
- /** Current hue value (0-360). */
- private float hue = 0f;
-
- /** Current saturation value (0-1). */
- private float saturation = 1f;
-
- /** Current value (brightness) value (0-1). */
- private float value = 1f;
-
- /** Current opacity value (0-1). */
- private float opacity = 1f;
-
- /** The currently selected color, including alpha channel if opacity slider is enabled. */
- @ColorInt
- private int selectedColor;
-
- /** Listener for color change events. */
- private OnColorChangedListener colorChangedListener;
-
- /** Tracks if the hue selector is being dragged. */
- private boolean isDraggingHue;
-
- /** Tracks if the saturation-value selector is being dragged. */
- private boolean isDraggingSaturation;
-
- /** Tracks if the opacity selector is being dragged. */
- private boolean isDraggingOpacity;
-
- /** Flag to enable/disable the opacity slider. */
- private boolean opacitySliderEnabled = false;
-
- public ColorPickerView(Context context) {
- super(context);
- }
-
- public ColorPickerView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- /**
- * Enables or disables the opacity slider.
- */
- public void setOpacitySliderEnabled(boolean enabled) {
- if (opacitySliderEnabled != enabled) {
- opacitySliderEnabled = enabled;
- if (!enabled) {
- opacity = 1f; // Reset to fully opaque when disabled.
- updateSelectedColor();
- }
- updateOpacityShader();
- requestLayout(); // Trigger re-measure to account for opacity slider.
- invalidate();
- }
- }
-
- /**
- * Measures the view, ensuring a consistent aspect ratio and minimum dimensions.
- */
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
-
- 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);
-
- int width = resolveSize(minWidth, widthMeasureSpec);
- int height = resolveSize(minHeight, heightMeasureSpec);
-
- // Ensure minimum dimensions for usability.
- width = Math.max(width, minWidth);
- height = Math.max(height, minHeight);
-
- // Adjust height to maintain desired aspect ratio if possible.
- final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS)
- + (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0);
- if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
- height = desiredHeight;
- }
-
- setMeasuredDimension(width, height);
- }
-
- /**
- * Updates the view's layout when its size changes, recalculating bounds and shaders.
- */
- @Override
- protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
- super.onSizeChanged(width, height, oldWidth, oldHeight);
-
- // Calculate bounds with hue bar and optional opacity bar at the bottom.
- final float effectiveWidth = width - (2 * VIEW_PADDING);
- final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS
- - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0);
-
- // Adjust rectangles to account for padding and density-independent dimensions.
- saturationValueRect.set(
- VIEW_PADDING,
- VIEW_PADDING,
- VIEW_PADDING + effectiveWidth,
- VIEW_PADDING + effectiveHeight
- );
-
- hueRect.set(
- VIEW_PADDING,
- height - VIEW_PADDING - HUE_BAR_HEIGHT - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0),
- VIEW_PADDING + effectiveWidth,
- height - VIEW_PADDING - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0)
- );
-
- if (opacitySliderEnabled) {
- opacityRect.set(
- VIEW_PADDING,
- height - VIEW_PADDING - OPACITY_BAR_HEIGHT,
- VIEW_PADDING + effectiveWidth,
- height - VIEW_PADDING
- );
- }
-
- // Update the shaders.
- updateHueShader();
- updateSaturationValueShader();
- updateOpacityShader();
- }
-
- /**
- * Updates the shader for the hue bar to reflect the color gradient.
- */
- private void updateHueShader() {
- LinearGradient hueShader = new LinearGradient(
- hueRect.left, hueRect.top,
- hueRect.right, hueRect.top,
- HUE_COLORS,
- null,
- Shader.TileMode.CLAMP
- );
-
- huePaint.setShader(hueShader);
- }
-
- /**
- * Updates the shader for the opacity slider to reflect the current RGB color with varying opacity.
- */
- private void updateOpacityShader() {
- if (!opacitySliderEnabled) {
- opacityPaint.setShader(null);
- return;
- }
-
- // Create a linear gradient for opacity from transparent to opaque, using the current RGB color.
- int rgbColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
- LinearGradient opacityShader = new LinearGradient(
- opacityRect.left, opacityRect.top,
- opacityRect.right, opacityRect.top,
- rgbColor & 0x00FFFFFF, // Fully transparent
- rgbColor | 0xFF000000, // Fully opaque
- Shader.TileMode.CLAMP
- );
-
- opacityPaint.setShader(opacityShader);
- }
-
- /**
- * Updates the shader for the saturation-value selector to reflect the current hue.
- */
- private void updateSaturationValueShader() {
- // Create a saturation-value gradient based on the current hue.
- // Calculate the start color (white with the selected hue) for the saturation gradient.
- final int startColor = Color.HSVToColor(new float[]{hue, 0f, 1f});
-
- // Calculate the middle color (fully saturated color with the selected hue) for the saturation gradient.
- final int midColor = Color.HSVToColor(new float[]{hue, 1f, 1f});
-
- // Create a linear gradient for the saturation from startColor to midColor (horizontal).
- LinearGradient satShader = new LinearGradient(
- saturationValueRect.left, saturationValueRect.top,
- saturationValueRect.right, saturationValueRect.top,
- startColor,
- midColor,
- Shader.TileMode.CLAMP
- );
-
- // Create a linear gradient for the value (brightness) from white to black (vertical).
- LinearGradient valShader = new LinearGradient(
- saturationValueRect.left, saturationValueRect.top,
- saturationValueRect.left, saturationValueRect.bottom,
- Color.WHITE,
- Color.BLACK,
- Shader.TileMode.CLAMP
- );
-
- // Combine the saturation and value shaders using PorterDuff.Mode.MULTIPLY to create the final color.
- ComposeShader combinedShader = new ComposeShader(satShader, valShader, PorterDuff.Mode.MULTIPLY);
-
- // Set the combined shader for the saturation-value paint.
- saturationValuePaint.setShader(combinedShader);
- }
-
- /**
- * Draws the color picker components, including the saturation-value selector, hue bar, opacity slider, and their respective handles.
- */
- @Override
- protected void onDraw(Canvas canvas) {
- // Draw the saturation-value selector rectangle.
- canvas.drawRect(saturationValueRect, saturationValuePaint);
-
- // Draw the hue bar.
- canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
-
- // Draw the opacity bar if enabled.
- if (opacitySliderEnabled) {
- canvas.drawRoundRect(opacityRect, OPACITY_CORNER_RADIUS, OPACITY_CORNER_RADIUS, opacityPaint);
- }
-
- final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
- final float hueSelectorY = hueRect.centerY();
-
- final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
- final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
-
- // Draw the saturation and hue selector handles filled with their respective colors (fully opaque).
- hsvArray[0] = hue;
- final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray); // Force opaque for hue handle.
- final int satHandleColor = Color.HSVToColor(0xFF, new float[]{hue, saturation, value}); // Force opaque for sat-val handle.
- selectorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
-
- selectorPaint.setColor(hueHandleColor);
- canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
-
- selectorPaint.setColor(satHandleColor);
- canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
-
- if (opacitySliderEnabled) {
- final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
- final float opacitySelectorY = opacityRect.centerY();
- selectorPaint.setColor(selectedColor); // Use full ARGB color to show opacity.
- canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
- }
-
- // Draw white outlines for the handles.
- selectorPaint.setColor(SELECTOR_OUTLINE_COLOR);
- selectorPaint.setStyle(Paint.Style.STROKE);
- selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
- canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_RADIUS, selectorPaint);
- canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_RADIUS, selectorPaint);
- if (opacitySliderEnabled) {
- final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
- final float opacitySelectorY = opacityRect.centerY();
- canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_RADIUS, selectorPaint);
- }
-
- // Draw thin dark outlines for the handles at the outer edge of the white outline.
- selectorPaint.setColor(SELECTOR_EDGE_COLOR);
- selectorPaint.setStrokeWidth(SELECTOR_EDGE_STROKE_WIDTH);
- canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
- canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
- if (opacitySliderEnabled) {
- final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
- final float opacitySelectorY = opacityRect.centerY();
- canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
- }
- }
-
- /**
- * Handles touch events to allow dragging of the hue, saturation-value, and opacity selectors.
- *
- * @param event The motion event.
- * @return True if the event was handled, false otherwise.
- */
- @SuppressLint("ClickableViewAccessibility")
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- try {
- final float x = event.getX();
- final float y = event.getY();
- final int action = event.getAction();
- Logger.printDebug(() -> "onTouchEvent action: " + action + " x: " + x + " y: " + y);
-
- // Define touch expansion for the hue and opacity bars.
- RectF expandedHueRect = new RectF(
- hueRect.left,
- hueRect.top - TOUCH_EXPANSION,
- hueRect.right,
- hueRect.bottom + TOUCH_EXPANSION
- );
- RectF expandedOpacityRect = opacitySliderEnabled ? new RectF(
- opacityRect.left,
- opacityRect.top - TOUCH_EXPANSION,
- opacityRect.right,
- opacityRect.bottom + TOUCH_EXPANSION
- ) : new RectF();
-
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- // Calculate current handle positions.
- final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
- final float hueSelectorY = hueRect.centerY();
-
- final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
- final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
-
- final float opacitySelectorX = opacitySliderEnabled ? opacityRect.left + opacity * opacityRect.width() : 0;
- final float opacitySelectorY = opacitySliderEnabled ? opacityRect.centerY() : 0;
-
- // Create hit areas for all handles.
- RectF hueHitRect = new RectF(
- hueSelectorX - SELECTOR_RADIUS,
- hueSelectorY - SELECTOR_RADIUS,
- hueSelectorX + SELECTOR_RADIUS,
- hueSelectorY + SELECTOR_RADIUS
- );
- RectF satValHitRect = new RectF(
- satSelectorX - SELECTOR_RADIUS,
- valSelectorY - SELECTOR_RADIUS,
- satSelectorX + SELECTOR_RADIUS,
- valSelectorY + SELECTOR_RADIUS
- );
- RectF opacityHitRect = opacitySliderEnabled ? new RectF(
- opacitySelectorX - SELECTOR_RADIUS,
- opacitySelectorY - SELECTOR_RADIUS,
- opacitySelectorX + SELECTOR_RADIUS,
- opacitySelectorY + SELECTOR_RADIUS
- ) : new RectF();
-
- // Check if the touch started on a handle or within the expanded bar areas.
- if (hueHitRect.contains(x, y)) {
- isDraggingHue = true;
- updateHueFromTouch(x);
- } else if (satValHitRect.contains(x, y)) {
- isDraggingSaturation = true;
- updateSaturationValueFromTouch(x, y);
- } else if (opacitySliderEnabled && opacityHitRect.contains(x, y)) {
- isDraggingOpacity = true;
- updateOpacityFromTouch(x);
- } else if (expandedHueRect.contains(x, y)) {
- // Handle touch within the expanded hue bar area.
- isDraggingHue = true;
- updateHueFromTouch(x);
- } else if (saturationValueRect.contains(x, y)) {
- isDraggingSaturation = true;
- updateSaturationValueFromTouch(x, y);
- } else if (opacitySliderEnabled && expandedOpacityRect.contains(x, y)) {
- isDraggingOpacity = true;
- updateOpacityFromTouch(x);
- }
- break;
-
- case MotionEvent.ACTION_MOVE:
- // Continue updating values even if touch moves outside the view.
- if (isDraggingHue) {
- updateHueFromTouch(x);
- } else if (isDraggingSaturation) {
- updateSaturationValueFromTouch(x, y);
- } else if (isDraggingOpacity) {
- updateOpacityFromTouch(x);
- }
- break;
-
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- isDraggingHue = false;
- isDraggingSaturation = false;
- isDraggingOpacity = false;
- break;
- }
- } catch (Exception ex) {
- Logger.printException(() -> "onTouchEvent failure", ex);
- }
-
- return true;
- }
-
- /**
- * Updates the hue value based on a touch event.
- */
- private void updateHueFromTouch(float x) {
- // Clamp x to the hue rectangle bounds.
- final float clampedX = Utils.clamp(x, hueRect.left, hueRect.right);
- final float updatedHue = ((clampedX - hueRect.left) / hueRect.width()) * 360f;
- if (hue == updatedHue) {
- return;
- }
-
- hue = updatedHue;
- updateSaturationValueShader();
- updateOpacityShader();
- updateSelectedColor();
- }
-
- /**
- * Updates the saturation and value based on a touch event.
- */
- private void updateSaturationValueFromTouch(float x, float y) {
- // Clamp x and y to the saturation-value rectangle bounds.
- final float clampedX = Utils.clamp(x, saturationValueRect.left, saturationValueRect.right);
- final float clampedY = Utils.clamp(y, saturationValueRect.top, saturationValueRect.bottom);
-
- final float updatedSaturation = (clampedX - saturationValueRect.left) / saturationValueRect.width();
- final float updatedValue = 1 - ((clampedY - saturationValueRect.top) / saturationValueRect.height());
-
- if (saturation == updatedSaturation && value == updatedValue) {
- return;
- }
- saturation = updatedSaturation;
- value = updatedValue;
- updateOpacityShader();
- updateSelectedColor();
- }
-
- /**
- * Updates the opacity value based on a touch event.
- */
- private void updateOpacityFromTouch(float x) {
- if (!opacitySliderEnabled) {
- return;
- }
- final float clampedX = Utils.clamp(x, opacityRect.left, opacityRect.right);
- final float updatedOpacity = (clampedX - opacityRect.left) / opacityRect.width();
- if (opacity == updatedOpacity) {
- return;
- }
- opacity = updatedOpacity;
- updateSelectedColor();
- }
-
- /**
- * Updates the selected color based on the current hue, saturation, value, and opacity.
- */
- private void updateSelectedColor() {
- final int rgbColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
- final int updatedColor = opacitySliderEnabled
- ? (rgbColor & 0x00FFFFFF) | (((int) (opacity * 255)) << 24)
- : (rgbColor & 0x00FFFFFF) | 0xFF000000;
-
- if (selectedColor != updatedColor) {
- selectedColor = updatedColor;
-
- if (colorChangedListener != null) {
- colorChangedListener.onColorChanged(updatedColor);
- }
- }
-
- // Must always redraw, otherwise if saturation is pure grey or black
- // then the hue slider cannot be changed.
- invalidate();
- }
-
- /**
- * Sets the selected color, updating the hue, saturation, value and opacity sliders accordingly.
- */
- public void setColor(@ColorInt int color) {
- if (selectedColor == color) {
- return;
- }
-
- // Update the selected color.
- selectedColor = color;
- Logger.printDebug(() -> "setColor: " + getColorString(selectedColor, opacitySliderEnabled));
-
- // Convert the ARGB color to HSV values.
- float[] hsv = new float[3];
- Color.colorToHSV(color, hsv);
-
- // Update the hue, saturation, and value.
- hue = hsv[0];
- saturation = hsv[1];
- value = hsv[2];
- opacity = opacitySliderEnabled ? ((color >> 24) & 0xFF) / 255f : 1f;
-
- // Update the saturation-value shader based on the new hue.
- updateSaturationValueShader();
- updateOpacityShader();
-
- // Notify the listener if it's set.
- if (colorChangedListener != null) {
- colorChangedListener.onColorChanged(selectedColor);
- }
-
- // Invalidate the view to trigger a redraw.
- invalidate();
- }
-
- /**
- * Gets the currently selected color.
- */
- @ColorInt
- public int getColor() {
- return selectedColor;
- }
-
- /**
- * Sets a listener to be notified when the selected color changes.
- */
- public void setOnColorChangedListener(OnColorChangedListener listener) {
- colorChangedListener = listener;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerWithOpacitySliderPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerWithOpacitySliderPreference.java
deleted file mode 100644
index 5e24f7bf36..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerWithOpacitySliderPreference.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import android.content.Context;
-import android.util.AttributeSet;
-
-/**
- * Extended ColorPickerPreference that enables the opacity slider for color selection.
- */
-@SuppressWarnings("unused")
-public class ColorPickerWithOpacitySliderPreference extends ColorPickerPreference {
-
- public ColorPickerWithOpacitySliderPreference(Context context) {
- super(context);
- init();
- }
-
- public ColorPickerWithOpacitySliderPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
-
- public ColorPickerWithOpacitySliderPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- init();
- }
-
- /**
- * Initialize the preference with opacity slider enabled.
- */
- private void init() {
- // Enable the opacity slider for alpha channel support.
- setOpacitySliderEnabled(true);
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/CustomDialogListPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/CustomDialogListPreference.java
deleted file mode 100644
index 48c50c1f33..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/CustomDialogListPreference.java
+++ /dev/null
@@ -1,267 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.os.Bundle;
-import android.preference.ListPreference;
-import android.util.AttributeSet;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.ui.CustomDialog;
-
-/**
- * A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator,
- * supports a static summary and highlighted entries for search functionality.
- */
-@SuppressWarnings({"unused", "deprecation"})
-public class CustomDialogListPreference extends ListPreference {
-
- public static final int ID_REVANCED_CHECK_ICON = getResourceIdentifierOrThrow(
- ResourceType.ID, "revanced_check_icon");
- public static final int ID_REVANCED_CHECK_ICON_PLACEHOLDER = getResourceIdentifierOrThrow(
- ResourceType.ID, "revanced_check_icon_placeholder");
- public static final int ID_REVANCED_ITEM_TEXT = getResourceIdentifierOrThrow(
- ResourceType.ID, "revanced_item_text");
- public static final int LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED = getResourceIdentifierOrThrow(
- ResourceType.LAYOUT, "revanced_custom_list_item_checked");
- public static final int DRAWABLE_CHECKMARK = getResourceIdentifierOrThrow(
- ResourceType.DRAWABLE, "revanced_settings_custom_checkmark");
- public static final int DRAWABLE_CHECKMARK_BOLD = getResourceIdentifierOrThrow(
- ResourceType.DRAWABLE, "revanced_settings_custom_checkmark_bold");
-
- private String staticSummary = null;
- private CharSequence[] highlightedEntriesForDialog = null;
-
- /**
- * Set a static summary that will not be overwritten by value changes.
- */
- public void setStaticSummary(String summary) {
- this.staticSummary = summary;
- }
-
- /**
- * Returns the static summary if set, otherwise null.
- */
- @Nullable
- public String getStaticSummary() {
- return staticSummary;
- }
-
- /**
- * Always return static summary if set.
- */
- @Override
- public CharSequence getSummary() {
- if (staticSummary != null) {
- return staticSummary;
- }
- return super.getSummary();
- }
-
- /**
- * Sets highlighted entries for display in the dialog.
- * These entries are used only for the current dialog and are automatically cleared.
- */
- public void setHighlightedEntriesForDialog(CharSequence[] highlightedEntries) {
- this.highlightedEntriesForDialog = highlightedEntries;
- }
-
- /**
- * Clears highlighted entries after the dialog is closed.
- */
- public void clearHighlightedEntriesForDialog() {
- this.highlightedEntriesForDialog = null;
- }
-
- /**
- * Returns entries for display in the dialog.
- * If highlighted entries exist, they are used; otherwise, the original entries are returned.
- */
- private CharSequence[] getEntriesForDialog() {
- return highlightedEntriesForDialog != null ? highlightedEntriesForDialog : getEntries();
- }
-
- /**
- * Custom ArrayAdapter to handle checkmark visibility.
- */
- public static class ListPreferenceArrayAdapter extends ArrayAdapter {
- private static class SubViewDataContainer {
- ImageView checkIcon;
- View placeholder;
- TextView itemText;
- }
-
- final int layoutResourceId;
- final CharSequence[] entryValues;
- String selectedValue;
-
- public ListPreferenceArrayAdapter(Context context, int resource,
- CharSequence[] entries,
- CharSequence[] entryValues,
- String selectedValue) {
- super(context, resource, entries);
- this.layoutResourceId = resource;
- this.entryValues = entryValues;
- this.selectedValue = selectedValue;
- }
-
- @NonNull
- @Override
- public View getView(int position, View convertView, @NonNull ViewGroup parent) {
- View view = convertView;
- SubViewDataContainer holder;
-
- if (view == null) {
- LayoutInflater inflater = LayoutInflater.from(getContext());
- view = inflater.inflate(layoutResourceId, parent, false);
- holder = new SubViewDataContainer();
- holder.placeholder = view.findViewById(ID_REVANCED_CHECK_ICON_PLACEHOLDER);
- holder.itemText = view.findViewById(ID_REVANCED_ITEM_TEXT);
- holder.checkIcon = view.findViewById(ID_REVANCED_CHECK_ICON);
- holder.checkIcon.setImageResource(Utils.appIsUsingBoldIcons()
- ? DRAWABLE_CHECKMARK_BOLD
- : DRAWABLE_CHECKMARK
- );
- view.setTag(holder);
- } else {
- holder = (SubViewDataContainer) view.getTag();
- }
-
- CharSequence itemText = getItem(position);
- holder.itemText.setText(itemText);
- holder.itemText.setTextColor(Utils.getAppForegroundColor());
-
- // Show or hide checkmark and placeholder.
- String currentValue = entryValues[position].toString();
- boolean isSelected = currentValue.equals(selectedValue);
- holder.checkIcon.setVisibility(isSelected ? View.VISIBLE : View.GONE);
- holder.checkIcon.setColorFilter(Utils.getAppForegroundColor());
- holder.placeholder.setVisibility(isSelected ? View.GONE : View.VISIBLE);
-
- return view;
- }
-
- public void setSelectedValue(String value) {
- this.selectedValue = value;
- }
- }
-
- public CustomDialogListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- public CustomDialogListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public CustomDialogListPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public CustomDialogListPreference(Context context) {
- super(context);
- }
-
- @Override
- protected void showDialog(Bundle state) {
- Context context = getContext();
-
- CharSequence[] entriesToShow = getEntriesForDialog();
- CharSequence[] entryValues = getEntryValues();
-
- // Create ListView.
- ListView listView = new ListView(context);
- listView.setId(android.R.id.list);
- listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
-
- // Create custom adapter for the ListView.
- ListPreferenceArrayAdapter adapter = new ListPreferenceArrayAdapter(
- context,
- LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED,
- entriesToShow,
- entryValues,
- getValue()
- );
- listView.setAdapter(adapter);
-
- // Set checked item.
- String currentValue = getValue();
- if (currentValue != null) {
- for (int i = 0, length = entryValues.length; i < length; i++) {
- if (currentValue.equals(entryValues[i].toString())) {
- listView.setItemChecked(i, true);
- listView.setSelection(i);
- break;
- }
- }
- }
-
- // Create the custom dialog without OK button.
- Pair dialogPair = CustomDialog.create(
- context,
- getTitle() != null ? getTitle().toString() : "",
- null,
- null,
- null,
- null,
- this::clearHighlightedEntriesForDialog, // Cancel button action.
- null,
- null,
- true
- );
-
- Dialog dialog = dialogPair.first;
- // Add a listener to clear when the dialog is closed in any way.
- dialog.setOnDismissListener(dialogInterface -> clearHighlightedEntriesForDialog());
-
- // Add the ListView to the main layout.
- LinearLayout mainLayout = dialogPair.second;
- LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- 0,
- 1.0f
- );
- mainLayout.addView(listView, mainLayout.getChildCount() - 1, listViewParams);
-
- // Handle item click to select value and dismiss dialog.
- listView.setOnItemClickListener((parent, view, position, id) -> {
- String selectedValue = entryValues[position].toString();
- if (callChangeListener(selectedValue)) {
- setValue(selectedValue);
-
- // Update summaries from the original entries (without highlighting).
- if (staticSummary == null) {
- CharSequence[] originalEntries = getEntries();
- if (originalEntries != null && position < originalEntries.length) {
- setSummary(originalEntries[position]);
- }
- }
-
- adapter.setSelectedValue(selectedValue);
- adapter.notifyDataSetChanged();
- }
-
- // Clear highlighted entries before closing.
- clearHighlightedEntriesForDialog();
- dialog.dismiss();
- });
-
- // Show the dialog.
- dialog.show();
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ExportLogToClipboardPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ExportLogToClipboardPreference.java
deleted file mode 100644
index 57fb128232..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ExportLogToClipboardPreference.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.preference.Preference;
-
-/**
- * A custom preference that triggers exporting ReVanced debug logs to the clipboard when clicked.
- * Invokes the {@link LogBufferManager#exportToClipboard} method.
- */
-@SuppressWarnings({"deprecation", "unused"})
-public class ExportLogToClipboardPreference extends Preference {
-
- {
- setOnPreferenceClickListener(pref -> {
- LogBufferManager.exportToClipboard();
- return true;
- });
- }
-
- public ExportLogToClipboardPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
- public ExportLogToClipboardPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
- public ExportLogToClipboardPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public ExportLogToClipboardPreference(Context context) {
- super(context);
- }
-}
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
deleted file mode 100644
index 4e6d2e5cdd..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/FeatureFlagsManagerPreference.java
+++ /dev/null
@@ -1,626 +0,0 @@
-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.ResourceType;
-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(ResourceType.DRAWABLE, "revanced_settings_select_all");
- private static final int DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL =
- getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_deselect_all");
- private static final int DRAWABLE_REVANCED_SETTINGS_COPY_ALL =
- getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_copy_all");
- private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE =
- getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_right_one");
- private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE =
- getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_right_double");
- private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE =
- getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_left_one");
- private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE =
- getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_left_double");
-
- /**
- * Flags to hide from the UI.
- */
- private static final Set FLAGS_TO_IGNORE = Set.of(
- 45386834L, // 'You' tab settings icon.
- 45532100L // Cairo flag. Turning this off with all other flags causes the settings menu to be a mix of old/new.
- );
-
- /**
- * 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()) {
- // It's impossible to reach the settings menu without reaching at least one flag.
- // So if theres no flags, then that means the user has just enabled debugging
- // but has not restarted the app yet.
- Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_no_flags"));
- 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/ForceOriginalAudioSwitchPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ForceOriginalAudioSwitchPreference.java
deleted file mode 100644
index fdcde3668d..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ForceOriginalAudioSwitchPreference.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import static app.revanced.extension.shared.StringRef.str;
-
-import android.content.Context;
-import android.preference.SwitchPreference;
-import android.util.AttributeSet;
-
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.spoof.ClientType;
-import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
-
-@SuppressWarnings({"deprecation", "unused"})
-public class ForceOriginalAudioSwitchPreference extends SwitchPreference {
-
- // Spoof stream patch is not included, or is not currently spoofing to Android Studio.
- private static final boolean available = !SpoofVideoStreamsPatch.isPatchIncluded()
- || !(BaseSettings.SPOOF_VIDEO_STREAMS.get()
- && SpoofVideoStreamsPatch.getPreferredClient() == ClientType.ANDROID_CREATOR);
-
- {
- if (!available) {
- // Show why force audio is not available.
- String summary = str("revanced_force_original_audio_not_available");
- super.setSummary(summary);
- super.setSummaryOn(summary);
- super.setSummaryOff(summary);
- super.setEnabled(false);
- }
- }
-
- public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
- public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
- public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public ForceOriginalAudioSwitchPreference(Context context) {
- super(context);
- }
-
- @Override
- public void setEnabled(boolean enabled) {
- if (!available) {
- return;
- }
-
- super.setEnabled(enabled);
- }
-
- @Override
- public void setSummary(CharSequence summary) {
- if (!available) {
- return;
- }
-
- super.setSummary(summary);
- }
-}
-
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
deleted file mode 100644
index 1044ba424e..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java
+++ /dev/null
@@ -1,133 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import static app.revanced.extension.shared.StringRef.str;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.preference.EditTextPreference;
-import android.preference.Preference;
-import android.text.InputType;
-import android.util.AttributeSet;
-import android.util.Pair;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.Setting;
-import app.revanced.extension.shared.ui.CustomDialog;
-
-@SuppressWarnings({"unused", "deprecation"})
-public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
-
- private String existingSettings;
-
- private void init() {
- setSelectable(true);
-
- EditText editText = getEditText();
- editText.setTextIsSelectable(true);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- editText.setAutofillHints((String) null);
- }
- editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
- editText.setTextSize(14);
-
- setOnPreferenceClickListener(this);
- }
-
- public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- init();
- }
- public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- init();
- }
- public ImportExportPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
- public ImportExportPreference(Context context) {
- super(context);
- init();
- }
-
- @Override
- public boolean onPreferenceClick(Preference preference) {
- try {
- // Must set text before showing dialog,
- // otherwise text is non-selectable if this preference is later reopened.
- existingSettings = Setting.exportToJson(getContext());
- getEditText().setText(existingSettings);
- } catch (Exception ex) {
- Logger.printException(() -> "showDialog failure", ex);
- }
- return true;
- }
-
- @Override
- protected void showDialog(Bundle state) {
- try {
- Context context = getContext();
- EditText editText = getEditText();
-
- // Create a custom dialog with the EditText.
- Pair dialogPair = CustomDialog.create(
- context,
- str("revanced_pref_import_export_title"), // Title.
- null, // No message (EditText replaces it).
- editText, // Pass the EditText.
- str("revanced_settings_import"), // OK button text.
- () -> importSettings(context, editText.getText().toString()), // OK button action.
- () -> {}, // Cancel button action (dismiss only).
- str("revanced_settings_import_copy"), // Neutral button (Copy) text.
- () -> {
- // Neutral button (Copy) action. Show the user the settings in JSON format.
- Utils.setClipboard(editText.getText());
- },
- true // Dismiss dialog when onNeutralClick.
- );
-
- // If there are no settings yet, then show the on screen keyboard and bring focus to
- // the edit text. This makes it easier to paste saved settings after a reinstall.
- dialogPair.first.setOnShowListener(dialogInterface -> {
- if (existingSettings.isEmpty()) {
- editText.postDelayed(() -> {
- editText.requestFocus();
-
- InputMethodManager inputMethodManager = (InputMethodManager)
- editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
- inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
- }, 100);
- }
- });
-
- // Show the dialog.
- dialogPair.first.show();
- } catch (Exception ex) {
- Logger.printException(() -> "showDialog failure", ex);
- }
- }
-
- private void importSettings(Context context, String replacementSettings) {
- try {
- if (replacementSettings.equals(existingSettings)) {
- return;
- }
- AbstractPreferenceFragment.settingImportInProgress = true;
-
- final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings);
- if (rebootNeeded) {
- AbstractPreferenceFragment.showRestartDialog(context);
- }
- } catch (Exception ex) {
- Logger.printException(() -> "importSettings failure", ex);
- } finally {
- AbstractPreferenceFragment.settingImportInProgress = false;
- }
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/LogBufferManager.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/LogBufferManager.java
deleted file mode 100644
index 4bd54c65be..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/LogBufferManager.java
+++ /dev/null
@@ -1,113 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import static app.revanced.extension.shared.StringRef.str;
-
-import java.util.Deque;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentLinkedDeque;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.BaseSettings;
-
-/**
- * Manages a buffer for storing debug logs from {@link Logger}.
- * Stores just under 1MB of the most recent log data.
- *
- * All methods are thread-safe.
- */
-public final class LogBufferManager {
- /** Maximum byte size of all buffer entries. Must be less than Android's 1 MB Binder transaction limit. */
- private static final int BUFFER_MAX_BYTES = 900_000;
- /** Limit number of log lines. */
- private static final int BUFFER_MAX_SIZE = 10_000;
-
- private static final Deque logBuffer = new ConcurrentLinkedDeque<>();
- private static final AtomicInteger logBufferByteSize = new AtomicInteger();
-
- /**
- * Appends a log message to the internal buffer if debugging is enabled.
- * The buffer is limited to approximately {@link #BUFFER_MAX_BYTES} or {@link #BUFFER_MAX_SIZE}
- * to prevent excessive memory usage.
- *
- * @param message The log message to append.
- */
- public static void appendToLogBuffer(String message) {
- Objects.requireNonNull(message);
-
- // It's very important that no Settings are used in this method,
- // as this code is used when a context is not set and thus referencing
- // a setting will crash the app.
- logBuffer.addLast(message);
- int newSize = logBufferByteSize.addAndGet(message.length());
-
- // Remove oldest entries if over the log size limits.
- while (newSize > BUFFER_MAX_BYTES || logBuffer.size() > BUFFER_MAX_SIZE) {
- String removed = logBuffer.pollFirst();
- if (removed == null) {
- // Thread race of two different calls to this method, and the other thread won.
- return;
- }
-
- newSize = logBufferByteSize.addAndGet(-removed.length());
- }
- }
-
- /**
- * Exports all logs from the internal buffer to the clipboard.
- * Displays a toast with the result.
- */
- public static void exportToClipboard() {
- try {
- if (!BaseSettings.DEBUG.get()) {
- Utils.showToastShort(str("revanced_debug_logs_disabled"));
- return;
- }
-
- if (logBuffer.isEmpty()) {
- Utils.showToastShort(str("revanced_debug_logs_none_found"));
- clearLogBufferData(); // Clear toast log entry that was just created.
- return;
- }
-
- // Most (but not all) Android 13+ devices always show a "copied to clipboard" toast
- // and there is no way to programmatically detect if a toast will show or not.
- // Show a toast even if using Android 13+, but show ReVanced toast first (before copying to clipboard).
- Utils.showToastShort(str("revanced_debug_logs_copied_to_clipboard"));
-
- Utils.setClipboard(String.join("\n", logBuffer));
- } catch (Exception ex) {
- // Handle security exception if clipboard access is denied.
- String errorMessage = String.format(str("revanced_debug_logs_failed_to_export"), ex.getMessage());
- Utils.showToastLong(errorMessage);
- Logger.printDebug(() -> errorMessage, ex);
- }
- }
-
- private static void clearLogBufferData() {
- // Cannot simply clear the log buffer because there is no
- // write lock for both the deque and the atomic int.
- // Instead pop off log entries and decrement the size one by one.
- while (!logBuffer.isEmpty()) {
- String removed = logBuffer.pollFirst();
- if (removed != null) {
- logBufferByteSize.addAndGet(-removed.length());
- }
- }
- }
-
- /**
- * Clears the internal log buffer and displays a toast with the result.
- */
- public static void clearLogBuffer() {
- if (!BaseSettings.DEBUG.get()) {
- Utils.showToastShort(str("revanced_debug_logs_disabled"));
- return;
- }
-
- // Show toast before clearing, otherwise toast log will still remain.
- Utils.showToastShort(str("revanced_debug_logs_clear_toast"));
- clearLogBufferData();
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/NoTitlePreferenceCategory.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/NoTitlePreferenceCategory.java
deleted file mode 100644
index d6b895f22a..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/NoTitlePreferenceCategory.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.preference.PreferenceCategory;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * Empty preference category with no title, used to organize and group related preferences together.
- */
-@SuppressWarnings({"unused", "deprecation"})
-public class NoTitlePreferenceCategory extends PreferenceCategory {
-
- public NoTitlePreferenceCategory(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- public NoTitlePreferenceCategory(Context context) {
- super(context);
- }
-
- @Override
- @SuppressLint("MissingSuperCall")
- protected View onCreateView(ViewGroup parent) {
- // Return an zero-height view to eliminate empty title space.
- return new View(getContext());
- }
-
- @Override
- public CharSequence getTitle() {
- // Title can be used for sorting. Return the first sub preference title.
- if (getPreferenceCount() > 0) {
- return getPreference(0).getTitle();
- }
-
- return super.getTitle();
- }
-
- @Override
- public int getTitleRes() {
- if (getPreferenceCount() > 0) {
- return getPreference(0).getTitleRes();
- }
-
- return super.getTitleRes();
- }
-}
-
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
deleted file mode 100644
index 0d4003b913..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java
+++ /dev/null
@@ -1,377 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import static app.revanced.extension.shared.StringRef.str;
-import static app.revanced.extension.shared.requests.Route.Method.GET;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.Dialog;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.drawable.ShapeDrawable;
-import android.graphics.drawable.shapes.RoundRectShape;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.preference.Preference;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.view.Window;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-import android.widget.LinearLayout;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.net.HttpURLConnection;
-import java.net.SocketTimeoutException;
-import java.util.ArrayList;
-import java.util.List;
-
-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.
- */
-@SuppressWarnings({"unused", "deprecation"})
-public class ReVancedAboutPreference extends Preference {
-
- private static String useNonBreakingHyphens(String text) {
- // Replace any dashes with non breaking dashes, so the English text 'pre-release'
- // and the dev release number does not break and cover two lines.
- return text.replace("-", "‑"); // #8209 = non breaking hyphen.
- }
-
- /**
- * Apps that do not support bundling resources must override this.
- *
- * @return A localized string to display for the key.
- */
- protected String getString(String key, Object ... args) {
- return str(key, args);
- }
-
- private String createDialogHtml(WebLink[] aboutLinks) {
- final boolean isNetworkConnected = Utils.isNetworkConnected();
-
- StringBuilder builder = new StringBuilder();
- builder.append("");
- builder.append("");
-
- String foregroundColorHex = Utils.getColorHexString(Utils.getAppForegroundColor());
- String backgroundColorHex = Utils.getColorHexString(Utils.getDialogBackgroundColor());
- // Apply light/dark mode colors.
- builder.append(String.format(
- "",
- backgroundColorHex, foregroundColorHex, foregroundColorHex));
-
- if (isNetworkConnected) {
- builder.append(" ");
- }
-
- String patchesVersion = Utils.getPatchesReleaseVersion();
-
- // Add the title.
- builder.append("")
- .append("ReVanced")
- .append(" ");
-
- builder.append("")
- // Replace hyphens with non breaking dashes so the version number does not break lines.
- .append(useNonBreakingHyphens(getString("revanced_settings_about_links_body", patchesVersion)))
- .append("
");
-
- // Add a disclaimer if using a dev release.
- if (patchesVersion.contains("dev")) {
- builder.append("")
- // English text 'Pre-release' can break lines.
- .append(useNonBreakingHyphens(getString("revanced_settings_about_links_dev_header")))
- .append(" ");
-
- builder.append("")
- .append(getString("revanced_settings_about_links_dev_body"))
- .append("
");
- }
-
- builder.append("")
- .append(getString("revanced_settings_about_links_header"))
- .append(" ");
-
- builder.append("");
- for (WebLink link : aboutLinks) {
- builder.append("
");
- builder.append(String.format("
%s ", link.url, link.name));
- builder.append("
");
- }
- builder.append("
");
-
- builder.append("");
- return builder.toString();
- }
-
- {
- setOnPreferenceClickListener(pref -> {
- Context context = pref.getContext();
-
- // Show a progress spinner if the social links are not fetched yet.
- if (!AboutLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) {
- // Show a progress spinner, but only if the api fetch takes more than a half a second.
- final long delayToShowProgressSpinner = 500;
- ProgressDialog progress = new ProgressDialog(getContext());
- progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
-
- Handler handler = new Handler(Looper.getMainLooper());
- Runnable showDialogRunnable = progress::show;
- handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner);
-
- Utils.runOnBackgroundThread(() ->
- fetchLinksAndShowDialog(context, handler, showDialogRunnable, progress));
- } else {
- // No network call required and can run now.
- fetchLinksAndShowDialog(context, null, null, null);
- }
-
- return false;
- });
- }
-
- private void fetchLinksAndShowDialog(Context context,
- @Nullable Handler handler,
- Runnable showDialogRunnable,
- @Nullable ProgressDialog progress) {
- WebLink[] links = AboutLinksRoutes.fetchAboutLinks();
- String htmlDialog = createDialogHtml(links);
-
- // Enable to randomly force a delay to debug the spinner logic.
- final boolean debugSpinnerDelayLogic = false;
- //noinspection ConstantConditions
- if (debugSpinnerDelayLogic && handler != null && Math.random() < 0.5f) {
- Utils.doNothingForDuration((long) (Math.random() * 4000));
- }
-
- Utils.runOnMainThreadNowOrLater(() -> {
- if (handler != null) {
- handler.removeCallbacks(showDialogRunnable);
- }
-
- // Don't continue if the activity is done. To test this tap the
- // about dialog and immediately press back before the dialog can show.
- if (context instanceof Activity activity) {
- if (activity.isFinishing() || activity.isDestroyed()) {
- Logger.printDebug(() -> "Not showing about dialog, activity is closed");
- return;
- }
- }
-
- if (progress != null && progress.isShowing()) {
- progress.dismiss();
- }
- new WebViewDialog(getContext(), htmlDialog).show();
- });
- }
-
- public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
- public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
- public ReVancedAboutPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public ReVancedAboutPreference(Context context) {
- super(context);
- }
-}
-
-/**
- * Displays html content as a dialog. Any links a user taps on are opened in an external browser.
- */
-class WebViewDialog extends Dialog {
-
- private final String htmlContent;
-
- public WebViewDialog(@NonNull Context context, @NonNull String htmlContent) {
- super(context);
- this.htmlContent = htmlContent;
- }
-
- // JS required to hide any broken images. No remote javascript is ever loaded.
- @SuppressLint("SetJavaScriptEnabled")
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
-
- // Create main layout.
- LinearLayout mainLayout = new LinearLayout(getContext());
- mainLayout.setOrientation(LinearLayout.VERTICAL);
-
- mainLayout.setPadding(Dim.dp10, Dim.dp10, Dim.dp10, Dim.dp10);
- // Set rounded rectangle background.
- ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape(
- Dim.roundedCorners(28), null, null));
- mainBackground.getPaint().setColor(Utils.getDialogBackgroundColor());
- mainLayout.setBackground(mainBackground);
-
- // Create WebView.
- WebView webView = new WebView(getContext());
- webView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar.
- webView.setOverScrollMode(View.OVER_SCROLL_NEVER);
- webView.getSettings().setJavaScriptEnabled(true);
- webView.setWebViewClient(new OpenLinksExternallyWebClient());
- webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null);
-
- // Add WebView to layout.
- mainLayout.addView(webView);
-
- setContentView(mainLayout);
-
- // Set dialog window attributes.
- Window window = getWindow();
- if (window != null) {
- Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 90, false);
- }
- }
-
- private class OpenLinksExternallyWebClient extends WebViewClient {
- @Override
- public boolean shouldOverrideUrlLoading(WebView view, String url) {
- try {
- Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
- getContext().startActivity(intent);
- } catch (Exception ex) {
- Logger.printException(() -> "Open link failure", ex);
- }
- // Dismiss the about dialog using a delay,
- // otherwise without a delay the UI looks hectic with the dialog dismissing
- // to show the settings while simultaneously a web browser is opening.
- Utils.runOnMainThreadDelayed(WebViewDialog.this::dismiss, 500);
- return true;
- }
- }
-}
-
-class WebLink {
- final boolean preferred;
- String name;
- final String url;
-
- WebLink(JSONObject json) throws JSONException {
- this(json.getBoolean("preferred"),
- json.getString("name"),
- json.getString("url")
- );
- }
-
- WebLink(boolean preferred, String name, String url) {
- this.preferred = preferred;
- this.name = name;
- this.url = url;
- }
-
- @NonNull
- @Override
- public String toString() {
- return "WebLink{" +
- "preferred=" + preferred +
- ", name='" + name + '\'' +
- ", url='" + url + '\'' +
- '}';
- }
-}
-
-class AboutLinksRoutes {
- /**
- * Backup icon url if the API call fails.
- */
- public static volatile String aboutLogoUrl = "https://revanced.app/favicon.ico";
-
- /**
- * Links to use if fetch links api call fails.
- */
- private static final WebLink[] NO_CONNECTION_STATIC_LINKS = {
- new WebLink(true, "ReVanced.app", "https://revanced.app")
- };
-
- private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v4";
- private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/about").compile();
-
- @Nullable
- private static volatile WebLink[] fetchedLinks;
-
- static boolean hasFetchedLinks() {
- return fetchedLinks != null;
- }
-
- static WebLink[] fetchAboutLinks() {
- try {
- if (hasFetchedLinks()) return fetchedLinks;
-
- // Check if there is no internet connection.
- if (!Utils.isNetworkConnected()) return NO_CONNECTION_STATIC_LINKS;
-
- HttpURLConnection connection = Requester.getConnectionFromCompiledRoute(SOCIAL_LINKS_PROVIDER, GET_SOCIAL);
- connection.setConnectTimeout(5000);
- connection.setReadTimeout(5000);
- Logger.printDebug(() -> "Fetching social links from: " + connection.getURL());
-
- // Do not show an exception toast if the server is down
- final int responseCode = connection.getResponseCode();
- if (responseCode != 200) {
- Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode);
- return NO_CONNECTION_STATIC_LINKS;
- }
-
- JSONObject json = Requester.parseJSONObjectAndDisconnect(connection);
- aboutLogoUrl = json.getJSONObject("branding").getString("logo");
-
- List links = new ArrayList<>();
-
- JSONArray donations = json.getJSONObject("donations").getJSONArray("links");
- for (int i = 0, length = donations.length(); i < length; i++) {
- WebLink link = new WebLink(donations.getJSONObject(i));
- if (link.preferred) {
- // This could be localized, but TikTok does not support localized resources.
- // All link names returned by the api are also non localized.
- link.name = "Donate";
- links.add(link);
- }
- }
-
- JSONArray socials = json.getJSONArray("socials");
- for (int i = 0, length = socials.length(); i < length; i++) {
- WebLink link = new WebLink(socials.getJSONObject(i));
- links.add(link);
- }
-
- Logger.printDebug(() -> "links: " + links);
-
- return fetchedLinks = links.toArray(new WebLink[0]);
-
- } catch (SocketTimeoutException ex) {
- Logger.printInfo(() -> "Could not fetch social links", ex); // No toast.
- } catch (JSONException ex) {
- Logger.printException(() -> "Could not parse about information", ex);
- } catch (Exception ex) {
- Logger.printException(() -> "Failed to get about information", ex);
- }
-
- return NO_CONNECTION_STATIC_LINKS;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java
deleted file mode 100644
index c6f323ceb4..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import static app.revanced.extension.shared.StringRef.str;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.os.Bundle;
-import android.preference.EditTextPreference;
-import android.util.AttributeSet;
-import android.util.Pair;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-
-import androidx.annotation.Nullable;
-
-import java.util.Objects;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.settings.Setting;
-import app.revanced.extension.shared.ui.CustomDialog;
-
-@SuppressWarnings({"unused", "deprecation"})
-public class ResettableEditTextPreference extends EditTextPreference {
-
- /**
- * Setting to reset.
- */
- @Nullable
- private Setting> setting;
-
- public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
- public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
- public ResettableEditTextPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public ResettableEditTextPreference(Context context) {
- super(context);
- }
-
- public void setSetting(@Nullable Setting> setting) {
- this.setting = setting;
- }
-
- @Override
- protected void showDialog(Bundle state) {
- try {
- Context context = getContext();
- EditText editText = getEditText();
-
- // Resolve setting if not already set.
- if (setting == null) {
- String key = getKey();
- if (key != null) {
- setting = Setting.getSettingFromPath(key);
- }
- }
-
- // Set initial EditText value to the current persisted value or empty string.
- String initialValue = getText() != null ? getText() : "";
- editText.setText(initialValue);
- editText.setSelection(initialValue.length()); // Move cursor to end.
-
- // Create custom dialog.
- String neutralButtonText = (setting != null) ? str("revanced_settings_reset") : null;
- Pair dialogPair = CustomDialog.create(
- context,
- getTitle() != null ? getTitle().toString() : "", // Title.
- null, // Message is replaced by EditText.
- editText, // Pass the EditText.
- null, // OK button text.
- () -> {
- // OK button action. Persist the EditText value when OK is clicked.
- String newValue = editText.getText().toString();
- if (callChangeListener(newValue)) {
- setText(newValue);
- }
- },
- () -> {}, // Cancel button action (dismiss only).
- neutralButtonText, // Neutral button text (Reset).
- () -> {
- // Neutral button action.
- if (setting != null) {
- try {
- String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
- editText.setText(defaultStringValue);
- editText.setSelection(defaultStringValue.length()); // Move cursor to end of text.
- } catch (Exception ex) {
- Logger.printException(() -> "reset failure", ex);
- }
- }
- },
- false // Do not dismiss dialog when onNeutralClick.
- );
-
- // Show the dialog.
- dialogPair.first.show();
- } catch (Exception ex) {
- Logger.printException(() -> "showDialog failure", ex);
- }
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java
deleted file mode 100644
index ed5db6b235..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java
+++ /dev/null
@@ -1,190 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.PreferenceFragment;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-
-import java.util.Objects;
-
-/**
- * Shared categories, and helper methods.
- *
- * The various save methods store numbers as Strings,
- * which is required if using {@link PreferenceFragment}.
- *
- * If saved numbers will not be used with a preference fragment,
- * then store the primitive numbers using the {@link #preferences} itself.
- */
-public class SharedPrefCategory {
- @NonNull
- public final String name;
- @NonNull
- public final SharedPreferences preferences;
-
- public SharedPrefCategory(@NonNull String name) {
- this.name = Objects.requireNonNull(name);
- preferences = Objects.requireNonNull(Utils.getContext()).getSharedPreferences(name, Context.MODE_PRIVATE);
- }
-
- private void removeConflictingPreferenceKeyValue(@NonNull String key) {
- Logger.printException(() -> "Found conflicting preference: " + key);
- removeKey(key);
- }
-
- private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
- preferences.edit().putString(key, (value == null ? null : value.toString())).commit();
- }
-
- /**
- * Removes any preference data type that has the specified key.
- */
- public void removeKey(@NonNull String key) {
- preferences.edit().remove(Objects.requireNonNull(key)).commit();
- }
-
- public void saveBoolean(@NonNull String key, boolean value) {
- preferences.edit().putBoolean(key, value).commit();
- }
-
- /**
- * @param value a NULL parameter removes the value from the preferences
- */
- public void saveEnumAsString(@NonNull String key, @Nullable Enum> value) {
- saveObjectAsString(key, value);
- }
-
- /**
- * @param value a NULL parameter removes the value from the preferences
- */
- public void saveIntegerString(@NonNull String key, @Nullable Integer value) {
- saveObjectAsString(key, value);
- }
-
- /**
- * @param value a NULL parameter removes the value from the preferences
- */
- public void saveLongString(@NonNull String key, @Nullable Long value) {
- saveObjectAsString(key, value);
- }
-
- /**
- * @param value a NULL parameter removes the value from the preferences
- */
- public void saveFloatString(@NonNull String key, @Nullable Float value) {
- saveObjectAsString(key, value);
- }
-
- /**
- * @param value a NULL parameter removes the value from the preferences
- */
- public void saveString(@NonNull String key, @Nullable String value) {
- saveObjectAsString(key, value);
- }
-
- @NonNull
- public String getString(@NonNull String key, @NonNull String _default) {
- Objects.requireNonNull(_default);
- try {
- return preferences.getString(key, _default);
- } catch (ClassCastException ex) {
- // Value stored is a completely different type (should never happen).
- removeConflictingPreferenceKeyValue(key);
- return _default;
- }
- }
-
- @NonNull
- public > T getEnum(@NonNull String key, @NonNull T _default) {
- Objects.requireNonNull(_default);
- try {
- String enumName = preferences.getString(key, null);
- if (enumName != null) {
- try {
- // noinspection unchecked
- return (T) Enum.valueOf(_default.getClass(), enumName);
- } catch (IllegalArgumentException ex) {
- // Info level to allow removing enum values in the future without showing any user errors.
- Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName);
- removeKey(key);
- }
- }
- } catch (ClassCastException ex) {
- // Value stored is a completely different type (should never happen).
- removeConflictingPreferenceKeyValue(key);
- }
- return _default;
- }
-
- public boolean getBoolean(@NonNull String key, boolean _default) {
- try {
- return preferences.getBoolean(key, _default);
- } catch (ClassCastException ex) {
- // Value stored is a completely different type (should never happen).
- removeConflictingPreferenceKeyValue(key);
- return _default;
- }
- }
-
- @NonNull
- public Integer getIntegerString(@NonNull String key, @NonNull Integer _default) {
- try {
- String value = preferences.getString(key, null);
- if (value != null) {
- return Integer.valueOf(value);
- }
- } catch (ClassCastException | NumberFormatException ex) {
- try {
- // Old data previously stored as primitive.
- return preferences.getInt(key, _default);
- } catch (ClassCastException ex2) {
- // Value stored is a completely different type (should never happen).
- removeConflictingPreferenceKeyValue(key);
- }
- }
- return _default;
- }
-
- @NonNull
- public Long getLongString(@NonNull String key, @NonNull Long _default) {
- try {
- String value = preferences.getString(key, null);
- if (value != null) {
- return Long.valueOf(value);
- }
- } catch (ClassCastException | NumberFormatException ex) {
- try {
- return preferences.getLong(key, _default);
- } catch (ClassCastException ex2) {
- removeConflictingPreferenceKeyValue(key);
- }
- }
- return _default;
- }
-
- @NonNull
- public Float getFloatString(@NonNull String key, @NonNull Float _default) {
- try {
- String value = preferences.getString(key, null);
- if (value != null) {
- return Float.valueOf(value);
- }
- } catch (ClassCastException | NumberFormatException ex) {
- try {
- return preferences.getFloat(key, _default);
- } catch (ClassCastException ex2) {
- removeConflictingPreferenceKeyValue(key);
- }
- }
- return _default;
- }
-
- @NonNull
- @Override
- public String toString() {
- return name;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java
deleted file mode 100644
index fb32e7bc07..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java
+++ /dev/null
@@ -1,124 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.util.Pair;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import app.revanced.extension.shared.Utils;
-
-/**
- * PreferenceList that sorts itself.
- * By default the first entry is preserved in its original position,
- * and all other entries are sorted alphabetically.
- *
- * Ideally the 'keep first entries to preserve' is an xml parameter,
- * but currently that's not so simple since Extensions code cannot use
- * generated code from the Patches repo (which is required for custom xml parameters).
- *
- * If any class wants to use a different getFirstEntriesToPreserve value,
- * it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}.
- */
-@SuppressWarnings({"unused", "deprecation"})
-public class SortedListPreference extends CustomDialogListPreference {
-
- /**
- * Sorts the current list entries.
- *
- * @param firstEntriesToPreserve The number of entries to preserve in their original position,
- * or a negative value to not sort and leave entries
- * as they current are.
- */
- public void sortEntryAndValues(int firstEntriesToPreserve) {
- CharSequence[] entries = getEntries();
- CharSequence[] entryValues = getEntryValues();
- if (entries == null || entryValues == null) {
- return;
- }
-
- final int entrySize = entries.length;
- if (entrySize != entryValues.length) {
- // Xml array declaration has a missing/extra entry.
- throw new IllegalStateException();
- }
-
- if (firstEntriesToPreserve < 0) {
- return; // Nothing to do.
- }
-
- List> firstEntries = new ArrayList<>(firstEntriesToPreserve);
-
- // Android does not have a triple class like Kotlin, So instead use a nested pair.
- // Cannot easily use a SortedMap, because if two entries incorrectly have
- // identical names then the duplicates entries are not preserved.
- List>> lastEntries = new ArrayList<>();
-
- for (int i = 0; i < entrySize; i++) {
- Pair pair = new Pair<>(entries[i], entryValues[i]);
- if (i < firstEntriesToPreserve) {
- firstEntries.add(pair);
- } else {
- lastEntries.add(new Pair<>(Utils.removePunctuationToLowercase(pair.first), pair));
- }
- }
-
- //noinspection ComparatorCombinators
- Collections.sort(lastEntries, (pair1, pair2)
- -> pair1.first.compareTo(pair2.first));
-
- CharSequence[] sortedEntries = new CharSequence[entrySize];
- CharSequence[] sortedEntryValues = new CharSequence[entrySize];
-
- int i = 0;
- for (Pair pair : firstEntries) {
- sortedEntries[i] = pair.first;
- sortedEntryValues[i] = pair.second;
- i++;
- }
-
- for (Pair> outer : lastEntries) {
- Pair inner = outer.second;
- sortedEntries[i] = inner.first;
- sortedEntryValues[i] = inner.second;
- i++;
- }
-
- super.setEntries(sortedEntries);
- super.setEntryValues(sortedEntryValues);
- }
-
- public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
-
- sortEntryAndValues(getFirstEntriesToPreserve());
- }
-
- public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- sortEntryAndValues(getFirstEntriesToPreserve());
- }
-
- public SortedListPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- sortEntryAndValues(getFirstEntriesToPreserve());
- }
-
- public SortedListPreference(Context context) {
- super(context);
-
- sortEntryAndValues(getFirstEntriesToPreserve());
- }
-
- /**
- * @return The number of first entries to leave exactly where they are, and do not sort them.
- * A negative value indicates do not sort any entries.
- */
- protected int getFirstEntriesToPreserve() {
- return 1;
- }
-}
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
deleted file mode 100644
index 8b1d8b882d..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ToolbarPreferenceFragment.java
+++ /dev/null
@@ -1,173 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import android.annotation.SuppressLint;
-import android.app.Dialog;
-import android.graphics.Insets;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.preference.Preference;
-import android.preference.PreferenceGroup;
-import android.preference.PreferenceScreen;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowInsets;
-import android.widget.TextView;
-import android.widget.Toolbar;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.BaseActivityHook;
-import app.revanced.extension.shared.ui.Dim;
-
-@SuppressWarnings("deprecation")
-@RequiresApi(api = Build.VERSION_CODES.O)
-public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
-
- /**
- * Removes the list of preferences from this fragment, if they exist.
- * @param keys Preference keys.
- */
- protected void removePreferences(String ... keys) {
- for (String key : keys) {
- Preference pref = findPreference(key);
- if (pref != null) {
- PreferenceGroup parent = pref.getParent();
- if (parent != null) {
- Logger.printDebug(() -> "Removing preference: " + key);
- parent.removePreference(pref);
- }
- }
- }
- }
-
- /**
- * Sets toolbar for all nested preference screens.
- */
- protected void setPreferenceScreenToolbar(PreferenceScreen parentScreen) {
- for (int i = 0, count = parentScreen.getPreferenceCount(); i < count; i++) {
- Preference childPreference = parentScreen.getPreference(i);
- if (childPreference instanceof PreferenceScreen) {
- // Recursively set sub preferences.
- setPreferenceScreenToolbar((PreferenceScreen) childPreference);
-
- childPreference.setOnPreferenceClickListener(
- childScreen -> {
- Dialog preferenceScreenDialog = ((PreferenceScreen) childScreen).getDialog();
- ViewGroup rootView = (ViewGroup) preferenceScreenDialog
- .findViewById(android.R.id.content)
- .getParent();
-
- // Allow package-specific background customization.
- customizeDialogBackground(rootView);
-
- // Fix the system navigation bar color for submenus.
- setNavigationBarColor(preferenceScreenDialog.getWindow());
-
- // Fix edge-to-edge screen with Android 15 and YT 19.45+
- // https://developer.android.com/develop/ui/views/layout/edge-to-edge#system-bars-insets
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- rootView.setOnApplyWindowInsetsListener((v, insets) -> {
- Insets statusInsets = insets.getInsets(WindowInsets.Type.statusBars());
- Insets navInsets = insets.getInsets(WindowInsets.Type.navigationBars());
- Insets cutoutInsets = insets.getInsets(WindowInsets.Type.displayCutout());
-
- // Apply padding for display cutout in landscape.
- int leftPadding = cutoutInsets.left;
- int rightPadding = cutoutInsets.right;
- int topPadding = statusInsets.top;
- int bottomPadding = navInsets.bottom;
-
- v.setPadding(leftPadding, topPadding, rightPadding, bottomPadding);
- return insets;
- });
- }
-
- Toolbar toolbar = new Toolbar(childScreen.getContext());
- toolbar.setTitle(childScreen.getTitle());
- toolbar.setNavigationIcon(getBackButtonDrawable());
- toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
-
- 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(20);
- }
-
- // Allow package-specific toolbar customization.
- customizeToolbar(toolbar);
-
- // Allow package-specific post-toolbar setup.
- onPostToolbarSetup(toolbar, preferenceScreenDialog);
-
- rootView.addView(toolbar, 0);
- return false;
- }
- );
- }
- }
- }
-
- /**
- * Sets the system navigation bar color for the activity.
- * Applies the background color obtained from {@link Utils#getAppBackgroundColor()} to the navigation bar.
- * For Android 10 (API 29) and above, enforces navigation bar contrast to ensure visibility.
- */
- public static void setNavigationBarColor(@Nullable Window window) {
- if (window == null) {
- Logger.printDebug(() -> "Cannot set navigation bar color, window is null");
- return;
- }
-
- window.setNavigationBarColor(Utils.getAppBackgroundColor());
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- window.setNavigationBarContrastEnforced(true);
- }
- }
-
- /**
- * Returns the drawable for the back button.
- */
- @SuppressLint("UseCompatLoadingForDrawables")
- public static Drawable getBackButtonDrawable() {
- final int backButtonResource = Utils.getResourceIdentifierOrThrow(ResourceType.DRAWABLE,
- Utils.appIsUsingBoldIcons()
- ? "revanced_settings_toolbar_arrow_left_bold"
- : "revanced_settings_toolbar_arrow_left");
- Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
- customizeBackButtonDrawable(drawable);
- return drawable;
- }
-
- /**
- * Customizes the back button drawable.
- */
- protected static void customizeBackButtonDrawable(Drawable drawable) {
- drawable.setTint(Utils.getAppForegroundColor());
- }
-
- /**
- * Allows subclasses to customize the dialog's root view background.
- */
- protected void customizeDialogBackground(ViewGroup rootView) {
- rootView.setBackgroundColor(Utils.getAppBackgroundColor());
- }
-
- /**
- * Allows subclasses to customize the toolbar.
- */
- protected void customizeToolbar(Toolbar toolbar) {
- BaseActivityHook.setToolbarLayoutParams(toolbar);
- }
-
- /**
- * Allows subclasses to perform actions after toolbar setup.
- */
- protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {}
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/URLLinkPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/URLLinkPreference.java
deleted file mode 100644
index 59f3077ceb..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/URLLinkPreference.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package app.revanced.extension.shared.settings.preference;
-
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.preference.Preference;
-import android.util.AttributeSet;
-
-import app.revanced.extension.shared.Logger;
-
-/**
- * Simple preference that opens a URL when clicked.
- */
-@SuppressWarnings("deprecation")
-public class URLLinkPreference extends Preference {
-
- protected String externalURL;
-
- {
- setOnPreferenceClickListener(pref -> {
- if (externalURL == null) {
- Logger.printException(() -> "URL not set " + getClass().getSimpleName());
- return false;
- }
- Intent i = new Intent(Intent.ACTION_VIEW);
- i.setData(Uri.parse(externalURL));
- pref.getContext().startActivity(i);
- return true;
- });
- }
-
- public URLLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
- public URLLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
- public URLLinkPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public URLLinkPreference(Context context) {
- super(context);
- }
-}
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
deleted file mode 100644
index 95731418d2..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java
+++ /dev/null
@@ -1,373 +0,0 @@
-package app.revanced.extension.shared.settings.search;
-
-import android.graphics.Color;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.SwitchPreference;
-import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
-import android.text.style.BackgroundColorSpan;
-
-import androidx.annotation.ColorInt;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
-import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
-import app.revanced.extension.shared.settings.preference.URLLinkPreference;
-
-/**
- * Abstract base class for search result items, defining common fields and behavior.
- */
-public abstract class BaseSearchResultItem {
- // Enum to represent view types.
- public enum ViewType {
- REGULAR,
- SWITCH,
- LIST,
- COLOR_PICKER,
- GROUP_HEADER,
- NO_RESULTS,
- URL_LINK;
-
- // Get the corresponding layout resource ID.
- public int getLayoutResourceId() {
- return switch (this) {
- case REGULAR, URL_LINK -> getResourceIdentifier("revanced_preference_search_result_regular");
- case SWITCH -> getResourceIdentifier("revanced_preference_search_result_switch");
- case LIST -> getResourceIdentifier("revanced_preference_search_result_list");
- case COLOR_PICKER -> getResourceIdentifier("revanced_preference_search_result_color");
- case GROUP_HEADER -> getResourceIdentifier("revanced_preference_search_result_group_header");
- case NO_RESULTS -> getResourceIdentifier("revanced_preference_search_no_result");
- };
- }
-
- private static int getResourceIdentifier(String name) {
- // Placeholder for actual resource identifier retrieval.
- return Utils.getResourceIdentifierOrThrow(ResourceType.LAYOUT, name);
- }
- }
-
- final String navigationPath;
- final List navigationKeys;
- final ViewType preferenceType;
- CharSequence highlightedTitle;
- CharSequence highlightedSummary;
- boolean highlightingApplied;
-
- BaseSearchResultItem(String navPath, List navKeys, ViewType type) {
- this.navigationPath = navPath;
- this.navigationKeys = new ArrayList<>(navKeys != null ? navKeys : Collections.emptyList());
- this.preferenceType = type;
- this.highlightedTitle = "";
- this.highlightedSummary = "";
- this.highlightingApplied = false;
- }
-
- abstract boolean matchesQuery(String query);
- abstract void applyHighlighting(Pattern queryPattern);
- abstract void clearHighlighting();
-
- // Shared method for highlighting text with search query.
- protected static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
- if (TextUtils.isEmpty(text) || queryPattern == null) return text;
-
- final int adjustedColor = Utils.adjustColorBrightness(
- Utils.getAppBackgroundColor(), 0.95f, 1.20f);
- BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
- SpannableStringBuilder spannable = new SpannableStringBuilder(text);
-
- Matcher matcher = queryPattern.matcher(text);
- while (matcher.find()) {
- int start = matcher.start();
- int end = matcher.end();
- if (start == end) continue; // Skip zero matches.
- spannable.setSpan(highlightSpan, start, end,
- SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
-
- return spannable;
- }
-
- /**
- * Search result item for group headers (navigation path only).
- */
- public static class GroupHeaderItem extends BaseSearchResultItem {
- GroupHeaderItem(String navPath, List navKeys) {
- super(navPath, navKeys, ViewType.GROUP_HEADER);
- this.highlightedTitle = navPath;
- }
-
- @Override
- boolean matchesQuery(String query) {
- return false; // Headers are not directly searchable.
- }
-
- @Override
- void applyHighlighting(Pattern queryPattern) {}
-
- @Override
- void clearHighlighting() {}
- }
-
- /**
- * Search result item for preferences, handling type-specific data and search text.
- */
- @SuppressWarnings("deprecation")
- public static class PreferenceSearchItem extends BaseSearchResultItem {
- public final Preference preference;
- final String searchableText;
- final CharSequence originalTitle;
- final CharSequence originalSummary;
- final CharSequence originalSummaryOn;
- final CharSequence originalSummaryOff;
- final CharSequence[] originalEntries;
- private CharSequence[] highlightedEntries;
- private boolean entriesHighlightingApplied;
-
- @ColorInt
- private int color;
-
- // Store last applied highlighting pattern to reapply when needed.
- Pattern lastQueryPattern;
-
- PreferenceSearchItem(Preference pref, String navPath, List navKeys) {
- super(navPath, navKeys, determineType(pref));
- this.preference = pref;
- this.originalTitle = pref.getTitle() != null ? pref.getTitle() : "";
- this.originalSummary = pref.getSummary();
- this.highlightedTitle = this.originalTitle;
- this.highlightedSummary = this.originalSummary != null ? this.originalSummary : "";
- this.color = 0;
- this.lastQueryPattern = null;
-
- // Initialize type-specific fields.
- FieldInitializationResult result = initTypeSpecificFields(pref);
- this.originalSummaryOn = result.summaryOn;
- this.originalSummaryOff = result.summaryOff;
- this.originalEntries = result.entries;
-
- // Build searchable text.
- this.searchableText = buildSearchableText(pref);
- }
-
- private static class FieldInitializationResult {
- CharSequence summaryOn = null;
- CharSequence summaryOff = null;
- CharSequence[] entries = null;
- }
-
- private static ViewType determineType(Preference pref) {
- if (pref instanceof SwitchPreference) return ViewType.SWITCH;
- if (pref instanceof ListPreference) return ViewType.LIST;
- if (pref instanceof ColorPickerPreference) return ViewType.COLOR_PICKER;
- if (pref instanceof URLLinkPreference) return ViewType.URL_LINK;
- if ("no_results_placeholder".equals(pref.getKey())) return ViewType.NO_RESULTS;
- return ViewType.REGULAR;
- }
-
- private FieldInitializationResult initTypeSpecificFields(Preference pref) {
- FieldInitializationResult result = new FieldInitializationResult();
-
- if (pref instanceof SwitchPreference switchPref) {
- result.summaryOn = switchPref.getSummaryOn();
- result.summaryOff = switchPref.getSummaryOff();
- } else if (pref instanceof ColorPickerPreference colorPref) {
- String colorString = colorPref.getText();
- this.color = TextUtils.isEmpty(colorString) ? 0 : Color.parseColor(colorString);
- } else if (pref instanceof ListPreference listPref) {
- result.entries = listPref.getEntries();
- if (result.entries != null) {
- this.highlightedEntries = new CharSequence[result.entries.length];
- System.arraycopy(result.entries, 0, this.highlightedEntries, 0, result.entries.length);
- }
- }
-
- this.entriesHighlightingApplied = false;
- return result;
- }
-
- private String buildSearchableText(Preference pref) {
- StringBuilder searchBuilder = new StringBuilder();
- String key = pref.getKey();
- String normalizedKey = "";
- if (key != null) {
- // Normalize preference key by removing the common "revanced_" prefix
- // so that users can search by the meaningful part only.
- normalizedKey = key.startsWith("revanced_")
- ? key.substring("revanced_".length())
- : key;
- }
- appendText(searchBuilder, normalizedKey);
- appendText(searchBuilder, originalTitle);
- appendText(searchBuilder, originalSummary);
-
- // Add type-specific searchable content.
- if (pref instanceof ListPreference) {
- if (originalEntries != null) {
- for (CharSequence entry : originalEntries) {
- appendText(searchBuilder, entry);
- }
- }
- } else if (pref instanceof SwitchPreference) {
- appendText(searchBuilder, originalSummaryOn);
- appendText(searchBuilder, originalSummaryOff);
- } else if (pref instanceof ColorPickerPreference) {
- appendText(searchBuilder, ColorPickerPreference.getColorString(color, false));
- }
-
- // Include navigation path in searchable text.
- appendText(searchBuilder, navigationPath);
-
- 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.normalizeTextToLowercase(text));
- }
- }
-
- /**
- * Gets the current effective summary for this preference, considering state-dependent summaries.
- */
- public CharSequence getCurrentEffectiveSummary() {
- if (preference instanceof CustomDialogListPreference customPref) {
- String staticSum = customPref.getStaticSummary();
- if (staticSum != null) {
- return staticSum;
- }
- }
- if (preference instanceof SwitchPreference switchPref) {
- boolean currentState = switchPref.isChecked();
- return currentState
- ? (originalSummaryOn != null ? originalSummaryOn :
- originalSummary != null ? originalSummary : "")
- : (originalSummaryOff != null ? originalSummaryOff :
- originalSummary != null ? originalSummary : "");
- } else if (preference instanceof ListPreference listPref) {
- String value = listPref.getValue();
- CharSequence[] entries = listPref.getEntries();
- CharSequence[] entryValues = listPref.getEntryValues();
- if (value != null && entries != null && entryValues != null) {
- for (int i = 0, length = entries.length; i < length; i++) {
- if (value.equals(entryValues[i].toString())) {
- return originalEntries != null && i < originalEntries.length && originalEntries[i] != null
- ? originalEntries[i]
- : originalSummary != null ? originalSummary : "";
- }
- }
- }
- return originalSummary != null ? originalSummary : "";
- }
- return originalSummary != null ? originalSummary : "";
- }
-
- /**
- * Checks if this search result item matches the provided query.
- * Uses case-insensitive matching against the searchable text.
- */
- @Override
- boolean matchesQuery(String query) {
- return searchableText.contains(Utils.normalizeTextToLowercase(query));
- }
-
- /**
- * Get highlighted entries to show in dialog.
- */
- public CharSequence[] getHighlightedEntries() {
- return highlightedEntries;
- }
-
- /**
- * Whether highlighting is applied to entries.
- */
- public boolean isEntriesHighlightingApplied() {
- return entriesHighlightingApplied;
- }
-
- /**
- * Highlights the search query in the title and summary.
- */
- @Override
- void applyHighlighting(Pattern queryPattern) {
- this.lastQueryPattern = queryPattern;
- // Highlight the title.
- highlightedTitle = highlightSearchQuery(originalTitle, queryPattern);
-
- // Get the current effective summary and highlight it.
- CharSequence currentSummary = getCurrentEffectiveSummary();
- highlightedSummary = highlightSearchQuery(currentSummary, queryPattern);
-
- // Highlight the entries.
- if (preference instanceof ListPreference && originalEntries != null) {
- highlightedEntries = new CharSequence[originalEntries.length];
- for (int i = 0, length = originalEntries.length; i < length; i++) {
- if (originalEntries[i] != null) {
- highlightedEntries[i] = highlightSearchQuery(originalEntries[i], queryPattern);
- } else {
- highlightedEntries[i] = null;
- }
- }
- entriesHighlightingApplied = true;
- }
-
- highlightingApplied = true;
- }
-
- /**
- * Clears all search query highlighting and restores original state completely.
- */
- @Override
- void clearHighlighting() {
- if (!highlightingApplied) return;
-
- // Restore original title.
- highlightedTitle = originalTitle;
-
- // Restore current effective summary without highlighting.
- highlightedSummary = getCurrentEffectiveSummary();
-
- // Restore original entries.
- if (originalEntries != null && highlightedEntries != null) {
- System.arraycopy(originalEntries, 0, highlightedEntries, 0,
- Math.min(originalEntries.length, highlightedEntries.length));
- }
-
- entriesHighlightingApplied = false;
- highlightingApplied = false;
- lastQueryPattern = null;
- }
-
- /**
- * Refreshes highlighting for dynamic summaries (like switch preferences).
- * Should be called when the preference state changes.
- */
- public void refreshHighlighting() {
- if (highlightingApplied && lastQueryPattern != null) {
- CharSequence currentSummary = getCurrentEffectiveSummary();
- highlightedSummary = highlightSearchQuery(currentSummary, lastQueryPattern);
- }
- }
-
- public void setColor(int newColor) {
- this.color = newColor;
- }
-
- @ColorInt
- public int getColor() {
- return color;
- }
- }
-}
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
deleted file mode 100644
index 04d69c6b6b..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java
+++ /dev/null
@@ -1,621 +0,0 @@
-package app.revanced.extension.shared.settings.search;
-
-import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
-
-import android.animation.AnimatorSet;
-import android.animation.ArgbEvaluator;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.os.Handler;
-import android.os.Looper;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.PreferenceGroup;
-import android.preference.PreferenceScreen;
-import android.preference.SwitchPreference;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AbsListView;
-import android.widget.ArrayAdapter;
-import android.widget.ImageView;
-import android.widget.ListAdapter;
-import android.widget.ListView;
-import android.widget.Switch;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.lang.reflect.Method;
-import java.util.List;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
-import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
-import app.revanced.extension.shared.settings.preference.URLLinkPreference;
-import app.revanced.extension.shared.ui.ColorDot;
-
-/**
- * Abstract adapter for displaying search results in overlay ListView with ViewHolder pattern.
- */
-@SuppressWarnings("deprecation")
-public abstract class BaseSearchResultsAdapter extends ArrayAdapter {
- protected final LayoutInflater inflater;
- protected final BaseSearchViewController.BasePreferenceFragment fragment;
- protected final BaseSearchViewController searchViewController;
- protected AnimatorSet currentAnimator;
- protected abstract PreferenceScreen getMainPreferenceScreen();
-
- protected static final int BLINK_DURATION = 400;
- protected static final int PAUSE_BETWEEN_BLINKS = 100;
-
- protected static final int ID_PREFERENCE_TITLE = getResourceIdentifierOrThrow(
- ResourceType.ID, "preference_title");
- protected static final int ID_PREFERENCE_SUMMARY = getResourceIdentifierOrThrow(
- ResourceType.ID, "preference_summary");
- protected static final int ID_PREFERENCE_PATH = getResourceIdentifierOrThrow(
- ResourceType.ID, "preference_path");
- protected static final int ID_PREFERENCE_SWITCH = getResourceIdentifierOrThrow(
- ResourceType.ID, "preference_switch");
- protected static final int ID_PREFERENCE_COLOR_DOT = getResourceIdentifierOrThrow(
- ResourceType.ID, "preference_color_dot");
-
- protected static class RegularViewHolder {
- TextView titleView;
- TextView summaryView;
- }
-
- protected static class SwitchViewHolder {
- TextView titleView;
- TextView summaryView;
- Switch switchWidget;
- }
-
- protected static class ColorViewHolder {
- TextView titleView;
- TextView summaryView;
- View colorDot;
- }
-
- protected static class GroupHeaderViewHolder {
- TextView pathView;
- }
-
- protected static class NoResultsViewHolder {
- TextView titleView;
- TextView summaryView;
- ImageView iconView;
- }
-
- public BaseSearchResultsAdapter(Context context, List items,
- BaseSearchViewController.BasePreferenceFragment fragment,
- BaseSearchViewController searchViewController) {
- super(context, 0, items);
- this.inflater = LayoutInflater.from(context);
- this.fragment = fragment;
- this.searchViewController = searchViewController;
- }
-
- @Override
- public int getItemViewType(int position) {
- BaseSearchResultItem item = getItem(position);
- return item == null ? 0 : item.preferenceType.ordinal();
- }
-
- @Override
- public int getViewTypeCount() {
- return BaseSearchResultItem.ViewType.values().length;
- }
-
- @NonNull
- @Override
- public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
- BaseSearchResultItem item = getItem(position);
- if (item == null) return new View(getContext());
- // Use the ViewType enum.
- BaseSearchResultItem.ViewType viewType = item.preferenceType;
- // Create or reuse preference view based on type.
- return createPreferenceView(item, convertView, viewType, parent);
- }
-
- @Override
- public boolean isEnabled(int position) {
- BaseSearchResultItem item = getItem(position);
- // Disable for NO_RESULTS items to prevent ripple/selection.
- return item != null && item.preferenceType != BaseSearchResultItem.ViewType.NO_RESULTS;
- }
-
- /**
- * Creates or reuses a view for the given SearchResultItem.
- *
- * Thanks to {@link #getItemViewType(int)} and {@link #getViewTypeCount()}, ListView knows
- * how many different row types exist and keeps a separate "recycling pool" for each.
- * That means convertView passed here is ALWAYS of the correct type for this position.
- * So only need to check if (view == null), and if so – inflate a new layout and create the proper ViewHolder.
- */
- protected View createPreferenceView(BaseSearchResultItem item, View convertView,
- BaseSearchResultItem.ViewType viewType, ViewGroup parent) {
- View view = convertView;
- if (view == null) {
- view = inflateViewForType(viewType, parent);
- createViewHolderForType(view, viewType);
- }
-
- // Retrieve the cached ViewHolder.
- Object holder = view.getTag();
- bindDataToViewHolder(item, holder, viewType, view);
- return view;
- }
-
- protected View inflateViewForType(BaseSearchResultItem.ViewType viewType, ViewGroup parent) {
- return inflater.inflate(viewType.getLayoutResourceId(), parent, false);
- }
-
- protected void createViewHolderForType(View view, BaseSearchResultItem.ViewType viewType) {
- switch (viewType) {
- case REGULAR, LIST, URL_LINK -> {
- RegularViewHolder regularHolder = new RegularViewHolder();
- regularHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
- regularHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
- view.setTag(regularHolder);
- }
- case SWITCH -> {
- SwitchViewHolder switchHolder = new SwitchViewHolder();
- switchHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
- switchHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
- switchHolder.switchWidget = view.findViewById(ID_PREFERENCE_SWITCH);
- view.setTag(switchHolder);
- }
- case COLOR_PICKER -> {
- ColorViewHolder colorHolder = new ColorViewHolder();
- colorHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
- colorHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
- colorHolder.colorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT);
- view.setTag(colorHolder);
- }
- case GROUP_HEADER -> {
- GroupHeaderViewHolder groupHolder = new GroupHeaderViewHolder();
- groupHolder.pathView = view.findViewById(ID_PREFERENCE_PATH);
- view.setTag(groupHolder);
- }
- case NO_RESULTS -> {
- NoResultsViewHolder noResultsHolder = new NoResultsViewHolder();
- noResultsHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
- noResultsHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
- noResultsHolder.iconView = view.findViewById(android.R.id.icon);
- view.setTag(noResultsHolder);
- }
- default -> throw new IllegalStateException("Unknown viewType: " + viewType);
- }
- }
-
- protected void bindDataToViewHolder(BaseSearchResultItem item, Object holder,
- BaseSearchResultItem.ViewType viewType, View view) {
- switch (viewType) {
- case REGULAR, URL_LINK, LIST -> bindRegularViewHolder(item, (RegularViewHolder) holder, view);
- case SWITCH -> bindSwitchViewHolder(item, (SwitchViewHolder) holder, view);
- case COLOR_PICKER -> bindColorViewHolder(item, (ColorViewHolder) holder, view);
- case GROUP_HEADER -> bindGroupHeaderViewHolder(item, (GroupHeaderViewHolder) holder, view);
- case NO_RESULTS -> bindNoResultsViewHolder(item, (NoResultsViewHolder) holder);
- default -> throw new IllegalStateException("Unknown viewType: " + viewType);
- }
- }
-
- protected void bindRegularViewHolder(BaseSearchResultItem item, RegularViewHolder holder, View view) {
- BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item;
- prefItem.refreshHighlighting();
- holder.titleView.setText(item.highlightedTitle);
- holder.summaryView.setText(item.highlightedSummary);
- holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
- setupPreferenceView(view, holder.titleView, holder.summaryView, prefItem.preference,
- () -> {
- handlePreferenceClick(prefItem.preference);
- if (prefItem.preference instanceof ListPreference) {
- prefItem.refreshHighlighting();
- holder.summaryView.setText(prefItem.getCurrentEffectiveSummary());
- holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE);
- notifyDataSetChanged();
- }
- },
- () -> navigateAndScrollToPreference(item));
- }
-
- protected void bindSwitchViewHolder(BaseSearchResultItem item, SwitchViewHolder holder, View view) {
- BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item;
- SwitchPreference switchPref = (SwitchPreference) prefItem.preference;
- holder.titleView.setText(item.highlightedTitle);
- holder.switchWidget.setBackground(null); // Remove ripple/highlight.
- // Sync switch state with preference without animation.
- boolean currentState = switchPref.isChecked();
- if (holder.switchWidget.isChecked() != currentState) {
- holder.switchWidget.setChecked(currentState);
- holder.switchWidget.jumpDrawablesToCurrentState();
- }
- prefItem.refreshHighlighting();
- holder.summaryView.setText(prefItem.highlightedSummary);
- holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE);
- setupPreferenceView(view, holder.titleView, holder.summaryView, switchPref,
- () -> {
- boolean newState = !switchPref.isChecked();
- switchPref.setChecked(newState);
- holder.switchWidget.setChecked(newState);
- prefItem.refreshHighlighting();
- holder.summaryView.setText(prefItem.getCurrentEffectiveSummary());
- holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE);
- if (switchPref.getOnPreferenceChangeListener() != null) {
- switchPref.getOnPreferenceChangeListener().onPreferenceChange(switchPref, newState);
- }
- notifyDataSetChanged();
- },
- () -> navigateAndScrollToPreference(item));
- holder.switchWidget.setEnabled(switchPref.isEnabled());
- }
-
- protected void bindColorViewHolder(BaseSearchResultItem item, ColorViewHolder holder, View view) {
- BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item;
- holder.titleView.setText(item.highlightedTitle);
- holder.summaryView.setText(item.highlightedSummary);
- holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
- ColorDot.applyColorDot(holder.colorDot, prefItem.getColor(), prefItem.preference.isEnabled());
- setupPreferenceView(view, holder.titleView, holder.summaryView, prefItem.preference,
- () -> handlePreferenceClick(prefItem.preference),
- () -> navigateAndScrollToPreference(item));
- }
-
- protected void bindGroupHeaderViewHolder(BaseSearchResultItem item, GroupHeaderViewHolder holder, View view) {
- holder.pathView.setText(item.highlightedTitle);
- view.setOnClickListener(v -> navigateToTargetScreen(item));
- }
-
- protected void bindNoResultsViewHolder(BaseSearchResultItem item, NoResultsViewHolder holder) {
- holder.titleView.setText(item.highlightedTitle);
- holder.summaryView.setText(item.highlightedSummary);
- holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
- holder.iconView.setImageResource(BaseSearchViewController.getSearchIcon());
- }
-
- /**
- * Sets up a preference view with click listeners and proper enabled state handling.
- */
- protected void setupPreferenceView(View view, TextView titleView, TextView summaryView, Preference preference,
- Runnable onClickAction, Runnable onLongClickAction) {
- boolean enabled = preference.isEnabled();
-
- // To enable long-click navigation for disabled settings, manually control the enabled state of the title
- // and summary and disable the ripple effect instead of using 'view.setEnabled(enabled)'.
-
- titleView.setEnabled(enabled);
- summaryView.setEnabled(enabled);
-
- if (!enabled) view.setBackground(null); // Disable ripple effect.
-
- // In light mode, alpha 0.5 is applied to a disabled title automatically,
- // but in dark mode it needs to be applied manually.
- if (Utils.isDarkModeEnabled()) {
- titleView.setAlpha(enabled ? 1.0f : ColorPickerPreference.DISABLED_ALPHA);
- }
- // Set up click and long-click listeners.
- view.setOnClickListener(enabled ? v -> onClickAction.run() : null);
- view.setOnLongClickListener(v -> {
- onLongClickAction.run();
- return true;
- });
- }
-
- /**
- * Navigates to the settings screen containing the given search result item and triggers scrolling.
- */
- protected void navigateAndScrollToPreference(BaseSearchResultItem item) {
- // No navigation for URL_LINK items.
- if (item.preferenceType == BaseSearchResultItem.ViewType.URL_LINK) return;
-
- PreferenceScreen targetScreen = navigateToTargetScreen(item);
- if (targetScreen == null) return;
- if (!(item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem)) return;
-
- Preference targetPreference = prefItem.preference;
-
- fragment.getView().post(() -> {
- ListView listView = targetScreen == getMainPreferenceScreen()
- ? getPreferenceListView()
- : targetScreen.getDialog().findViewById(android.R.id.list);
-
- if (listView == null) return;
-
- int targetPosition = findPreferencePosition(targetPreference, listView);
- if (targetPosition == -1) return;
-
- int firstVisible = listView.getFirstVisiblePosition();
- int lastVisible = listView.getLastVisiblePosition();
-
- if (targetPosition >= firstVisible && targetPosition <= lastVisible) {
- // The preference is already visible, but still scroll it to the bottom of the list for consistency.
- View child = listView.getChildAt(targetPosition - firstVisible);
- if (child != null) {
- // Calculate how much to scroll so the item is aligned at the bottom.
- int scrollAmount = child.getBottom() - listView.getHeight();
- if (scrollAmount > 0) {
- // Perform smooth scroll animation for better user experience.
- listView.smoothScrollBy(scrollAmount, 300);
- }
- }
- // Highlight the preference once it is positioned.
- highlightPreferenceAtPosition(listView, targetPosition);
- } else {
- // The preference is outside of the current visible range, scroll to it from the top.
- listView.smoothScrollToPositionFromTop(targetPosition, 0);
-
- Handler handler = new Handler(Looper.getMainLooper());
- // Fallback runnable in case the OnScrollListener does not trigger.
- Runnable fallback = () -> {
- listView.setOnScrollListener(null);
- highlightPreferenceAtPosition(listView, targetPosition);
- };
- // Post fallback with a small delay.
- handler.postDelayed(fallback, 350);
-
- listView.setOnScrollListener(new AbsListView.OnScrollListener() {
- private boolean isScrolling = false;
-
- @Override
- public void onScrollStateChanged(AbsListView view, int scrollState) {
- if (scrollState == SCROLL_STATE_TOUCH_SCROLL || scrollState == SCROLL_STATE_FLING) {
- // Mark that scrolling has started.
- isScrolling = true;
- }
- if (scrollState == SCROLL_STATE_IDLE && isScrolling) {
- // Scrolling is finished, cleanup listener and cancel fallback.
- isScrolling = false;
- listView.setOnScrollListener(null);
- handler.removeCallbacks(fallback);
- // Highlight the target preference when scrolling is done.
- highlightPreferenceAtPosition(listView, targetPosition);
- }
- }
-
- @Override
- public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
- });
- }
- });
- }
-
- /**
- * Navigates to the final PreferenceScreen using preference keys or titles as fallback.
- */
- protected PreferenceScreen navigateToTargetScreen(BaseSearchResultItem item) {
- PreferenceScreen currentScreen = getMainPreferenceScreen();
- Preference targetPref = null;
-
- // Try key-based navigation first.
- if (item.navigationKeys != null && !item.navigationKeys.isEmpty()) {
- String finalKey = item.navigationKeys.get(item.navigationKeys.size() - 1);
- targetPref = findPreferenceByKey(currentScreen, finalKey);
- }
-
- // Fallback to title-based navigation.
- if (targetPref == null && !TextUtils.isEmpty(item.navigationPath)) {
- String[] pathSegments = item.navigationPath.split(" > ");
- String finalSegment = pathSegments[pathSegments.length - 1].trim();
- if (!TextUtils.isEmpty(finalSegment)) {
- targetPref = findPreferenceByTitle(currentScreen, finalSegment);
- }
- }
-
- if (targetPref instanceof PreferenceScreen targetScreen) {
- handlePreferenceClick(targetScreen);
- return targetScreen;
- }
-
- return currentScreen;
- }
-
- /**
- * Recursively searches for a preference by title in a preference group.
- */
- protected Preference findPreferenceByTitle(PreferenceGroup group, String title) {
- for (int i = 0; i < group.getPreferenceCount(); i++) {
- Preference pref = group.getPreference(i);
- CharSequence prefTitle = pref.getTitle();
- if (prefTitle != null && (prefTitle.toString().trim().equalsIgnoreCase(title)
- || normalizeString(prefTitle.toString()).equals(normalizeString(title)))) {
- return pref;
- }
- if (pref instanceof PreferenceGroup) {
- Preference found = findPreferenceByTitle((PreferenceGroup) pref, title);
- if (found != null) {
- return found;
- }
- }
- }
- return null;
- }
-
- /**
- * Normalizes string for comparison (removes extra characters, spaces etc.).
- */
- protected String normalizeString(String input) {
- if (TextUtils.isEmpty(input)) return "";
- return input.trim().toLowerCase().replaceAll("\\s+", " ").replaceAll("[^\\w\\s]", "");
- }
-
- /**
- * Gets the ListView from the PreferenceFragment.
- */
- protected ListView getPreferenceListView() {
- View fragmentView = fragment.getView();
- if (fragmentView != null) {
- ListView listView = findListViewInViewGroup(fragmentView);
- if (listView != null) {
- return listView;
- }
- }
- return fragment.getActivity().findViewById(android.R.id.list);
- }
-
- /**
- * Recursively searches for a ListView in a ViewGroup.
- */
- protected ListView findListViewInViewGroup(View view) {
- if (view instanceof ListView) {
- return (ListView) view;
- }
- if (view instanceof ViewGroup group) {
- for (int i = 0; i < group.getChildCount(); i++) {
- ListView result = findListViewInViewGroup(group.getChildAt(i));
- if (result != null) {
- return result;
- }
- }
- }
- return null;
- }
-
- /**
- * Finds the position of a preference in the ListView adapter.
- */
- protected int findPreferencePosition(Preference targetPreference, ListView listView) {
- ListAdapter adapter = listView.getAdapter();
- if (adapter == null) {
- return -1;
- }
-
- for (int i = 0, count = adapter.getCount(); i < count; i++) {
- Object item = adapter.getItem(i);
- if (item == targetPreference) {
- return i;
- }
- if (item instanceof Preference pref && targetPreference.getKey() != null) {
- if (targetPreference.getKey().equals(pref.getKey())) {
- return i;
- }
- }
- }
- return -1;
- }
-
- /**
- * Highlights a preference at the specified position with a blink effect.
- */
- protected void highlightPreferenceAtPosition(ListView listView, int position) {
- int firstVisible = listView.getFirstVisiblePosition();
- if (position < firstVisible || position > listView.getLastVisiblePosition()) {
- return;
- }
-
- View itemView = listView.getChildAt(position - firstVisible);
- if (itemView != null) {
- blinkView(itemView);
- }
- }
-
- /**
- * Creates a smooth double-blink effect on a view's background without affecting the text.
- * @param view The View to apply the animation to.
- */
- protected void blinkView(View view) {
- // If a previous animation is still running, cancel it to prevent conflicts.
- if (currentAnimator != null && currentAnimator.isRunning()) {
- currentAnimator.cancel();
- }
- final int startColor = Utils.getAppBackgroundColor();
- final int highlightColor = Utils.adjustColorBrightness(
- startColor,
- Utils.isDarkModeEnabled() ? 1.25f : 0.8f
- );
- // Animator for transitioning from the start color to the highlight color.
- ObjectAnimator fadeIn = ObjectAnimator.ofObject(
- view,
- "backgroundColor",
- new ArgbEvaluator(),
- startColor,
- highlightColor
- );
- fadeIn.setDuration(BLINK_DURATION);
- // Animator to return to the start color.
- ObjectAnimator fadeOut = ObjectAnimator.ofObject(
- view,
- "backgroundColor",
- new ArgbEvaluator(),
- highlightColor,
- startColor
- );
- fadeOut.setDuration(BLINK_DURATION);
-
- currentAnimator = new AnimatorSet();
- // Create the sequence: fadeIn -> fadeOut -> (pause) -> fadeIn -> fadeOut.
- AnimatorSet firstBlink = new AnimatorSet();
- firstBlink.playSequentially(fadeIn, fadeOut);
- AnimatorSet secondBlink = new AnimatorSet();
- secondBlink.playSequentially(fadeIn.clone(), fadeOut.clone()); // Use clones for the second blink.
-
- currentAnimator.play(secondBlink).after(firstBlink).after(PAUSE_BETWEEN_BLINKS);
- currentAnimator.start();
- }
-
- /**
- * Recursively finds a preference by key in a preference group.
- */
- protected Preference findPreferenceByKey(PreferenceGroup group, String key) {
- if (group == null || TextUtils.isEmpty(key)) {
- return null;
- }
-
- // First search on current level.
- for (int i = 0, count = group.getPreferenceCount(); i < count; i++) {
- Preference pref = group.getPreference(i);
- if (key.equals(pref.getKey())) {
- return pref;
- }
- if (pref instanceof PreferenceGroup) {
- Preference found = findPreferenceByKey((PreferenceGroup) pref, key);
- if (found != null) {
- return found;
- }
- }
- }
- return null;
- }
-
- /**
- * Handles preference click actions by invoking the preference's performClick method via reflection.
- */
- @SuppressWarnings("all")
- private void handlePreferenceClick(Preference preference) {
- try {
- if (preference instanceof CustomDialogListPreference listPref) {
- BaseSearchResultItem.PreferenceSearchItem searchItem =
- searchViewController.findSearchItemByPreference(preference);
- if (searchItem != null && searchItem.isEntriesHighlightingApplied()) {
- listPref.setHighlightedEntriesForDialog(searchItem.getHighlightedEntries());
- }
- }
-
- Method m = Preference.class.getDeclaredMethod("performClick", PreferenceScreen.class);
- m.setAccessible(true);
- m.invoke(preference, fragment.getPreferenceScreenForSearch());
- } catch (Exception e) {
- Logger.printException(() -> "Failed to invoke performClick()", e);
- }
- }
-
- /**
- * Checks if a preference has navigation capability (can open a new screen).
- */
- boolean hasNavigationCapability(Preference preference) {
- // PreferenceScreen always allows navigation.
- if (preference instanceof PreferenceScreen) return true;
- // URLLinkPreference does not navigate to a new screen, it opens an external URL.
- if (preference instanceof URLLinkPreference) return false;
- // Other group types that might have their own screens.
- if (preference instanceof PreferenceGroup) {
- // Check if it has its own fragment or intent.
- return preference.getIntent() != null || preference.getFragment() != null;
- }
- return false;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchViewController.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchViewController.java
deleted file mode 100644
index 3be942f6f6..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchViewController.java
+++ /dev/null
@@ -1,704 +0,0 @@
-package app.revanced.extension.shared.settings.search;
-
-import static app.revanced.extension.shared.StringRef.str;
-import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
-
-import android.app.Activity;
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.drawable.GradientDrawable;
-import android.os.Build;
-import android.preference.Preference;
-import android.preference.PreferenceCategory;
-import android.preference.PreferenceGroup;
-import android.preference.PreferenceScreen;
-import android.text.TextUtils;
-import android.view.Gravity;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.WindowManager;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.EditText;
-import android.widget.FrameLayout;
-import android.widget.ListView;
-import android.widget.SearchView;
-import android.widget.Toolbar;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.RequiresApi;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.regex.Pattern;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.AppLanguage;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.settings.Setting;
-import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
-import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
-import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
-import app.revanced.extension.shared.ui.Dim;
-
-/**
- * Abstract controller for managing the overlay search view in ReVanced settings.
- * Subclasses must implement app-specific preference handling.
- */
-@SuppressWarnings("deprecation")
-public abstract class BaseSearchViewController {
- protected SearchView searchView;
- protected FrameLayout searchContainer;
- protected FrameLayout overlayContainer;
- protected final Toolbar toolbar;
- protected final Activity activity;
- protected final BasePreferenceFragment fragment;
- protected final CharSequence originalTitle;
- protected BaseSearchResultsAdapter searchResultsAdapter;
- protected final List allSearchItems;
- protected final List filteredSearchItems;
- protected final Map keyToSearchItem;
- protected final InputMethodManager inputMethodManager;
- protected SearchHistoryManager searchHistoryManager;
- protected boolean isSearchActive;
- protected boolean isShowingSearchHistory;
-
- protected static final int MAX_SEARCH_RESULTS = 50; // Maximum number of search results displayed.
-
- protected static final int ID_REVANCED_SEARCH_VIEW = getResourceIdentifierOrThrow(
- ResourceType.ID, "revanced_search_view");
- protected static final int ID_REVANCED_SEARCH_VIEW_CONTAINER = getResourceIdentifierOrThrow(
- ResourceType.ID, "revanced_search_view_container");
- protected static final int ID_ACTION_SEARCH = getResourceIdentifierOrThrow(
- ResourceType.ID, "action_search");
- protected static final int ID_REVANCED_SETTINGS_FRAGMENTS = getResourceIdentifierOrThrow(
- ResourceType.ID, "revanced_settings_fragments");
- private static final int DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON = getResourceIdentifierOrThrow(
- ResourceType.DRAWABLE, "revanced_settings_search_icon");
- private static final int DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON_BOLD = getResourceIdentifierOrThrow(
- ResourceType.DRAWABLE, "revanced_settings_search_icon_bold");
- protected static final int MENU_REVANCED_SEARCH_MENU = getResourceIdentifierOrThrow(
- ResourceType.MENU, "revanced_search_menu");
-
- /**
- * @return The search icon, either bold or not bold, depending on the ReVanced UI setting.
- */
- public static int getSearchIcon() {
- return Utils.appIsUsingBoldIcons()
- ? DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON_BOLD
- : DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON;
- }
-
- /**
- * Constructs a new BaseSearchViewController instance.
- *
- * @param activity The activity hosting the search view.
- * @param toolbar The toolbar containing the search action.
- * @param fragment The preference fragment to manage search preferences.
- */
- protected BaseSearchViewController(Activity activity, Toolbar toolbar, BasePreferenceFragment fragment) {
- this.activity = activity;
- this.toolbar = toolbar;
- this.fragment = fragment;
- this.originalTitle = toolbar.getTitle();
- this.allSearchItems = new ArrayList<>();
- this.filteredSearchItems = new ArrayList<>();
- this.keyToSearchItem = new HashMap<>();
- this.inputMethodManager = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
- this.isShowingSearchHistory = false;
-
- // Initialize components
- initializeSearchView();
- initializeOverlayContainer();
- initializeSearchHistoryManager();
- setupToolbarMenu();
- setupListeners();
- }
-
- /**
- * Initializes the search view with proper configurations, such as background, query hint, and RTL support.
- */
- private void initializeSearchView() {
- // Retrieve SearchView and container from XML.
- searchView = activity.findViewById(ID_REVANCED_SEARCH_VIEW);
- EditText searchEditText = searchView.findViewById(Utils.getResourceIdentifierOrThrow(
- null, "android:id/search_src_text"));
- // Disable fullscreen keyboard mode.
- searchEditText.setImeOptions(searchEditText.getImeOptions() | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
-
- searchContainer = activity.findViewById(ID_REVANCED_SEARCH_VIEW_CONTAINER);
-
- // Set background and query hint.
- searchView.setBackground(createBackgroundDrawable());
- searchView.setQueryHint(str("revanced_settings_search_hint"));
-
- // Set text size.
- searchEditText.setTextSize(16);
-
- // Set cursor color.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- setCursorColor(searchEditText);
- }
-
- // Configure RTL support based on app language.
- AppLanguage appLanguage = BaseSettings.REVANCED_LANGUAGE.get();
- if (Utils.isRightToLeftLocale(appLanguage.getLocale())) {
- searchView.setTextDirection(View.TEXT_DIRECTION_RTL);
- searchView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
- }
- }
-
- /**
- * Sets the cursor color (for Android 10+ devices).
- */
- @RequiresApi(api = Build.VERSION_CODES.Q)
- private void setCursorColor(EditText editText) {
- // Get the cursor color based on the current theme.
- final int cursorColor = Utils.isDarkModeEnabled() ? Color.WHITE : Color.BLACK;
-
- // Create cursor drawable.
- GradientDrawable cursorDrawable = new GradientDrawable();
- cursorDrawable.setShape(GradientDrawable.RECTANGLE);
- cursorDrawable.setSize(Dim.dp2, -1); // Width: 2dp, Height: match text height.
- cursorDrawable.setColor(cursorColor);
-
- // Set cursor drawable.
- editText.setTextCursorDrawable(cursorDrawable);
- }
-
- /**
- * Initializes the overlay container for displaying search results and history.
- */
- private void initializeOverlayContainer() {
- // Create overlay container for search results and history.
- overlayContainer = new FrameLayout(activity);
- overlayContainer.setVisibility(View.GONE);
- overlayContainer.setBackgroundColor(Utils.getAppBackgroundColor());
- overlayContainer.setElevation(Dim.dp8);
-
- // Container for search results.
- FrameLayout searchResultsContainer = new FrameLayout(activity);
- searchResultsContainer.setVisibility(View.VISIBLE);
-
- // Create a ListView for the results.
- ListView searchResultsListView = new ListView(activity);
- searchResultsListView.setDivider(null);
- searchResultsListView.setDividerHeight(0);
- searchResultsAdapter = createSearchResultsAdapter();
- searchResultsListView.setAdapter(searchResultsAdapter);
-
- // Add results list into container.
- searchResultsContainer.addView(searchResultsListView, new FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT));
-
- // Add results container into overlay.
- overlayContainer.addView(searchResultsContainer, new FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT));
-
- // Add overlay to the main content container.
- FrameLayout mainContainer = activity.findViewById(ID_REVANCED_SETTINGS_FRAGMENTS);
- if (mainContainer != null) {
- FrameLayout.LayoutParams overlayParams = new FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT);
- overlayParams.gravity = Gravity.TOP;
- mainContainer.addView(overlayContainer, overlayParams);
- }
- }
-
- /**
- * Initializes the search history manager with the specified overlay container and listener.
- */
- private void initializeSearchHistoryManager() {
- searchHistoryManager = new SearchHistoryManager(activity, overlayContainer, query -> {
- searchView.setQuery(query, true);
- hideSearchHistory();
- });
- }
-
- // Abstract methods that subclasses must implement.
- protected abstract BaseSearchResultsAdapter createSearchResultsAdapter();
- @SuppressWarnings("BooleanMethodIsAlwaysInverted")
- protected abstract boolean isSpecialPreferenceGroup(Preference preference);
- protected abstract void setupSpecialPreferenceListeners(BaseSearchResultItem item);
-
- // Abstract interface for preference fragments.
- public interface BasePreferenceFragment {
- PreferenceScreen getPreferenceScreenForSearch();
- android.view.View getView();
- Activity getActivity();
- }
-
- /**
- * Determines whether a preference should be included in the search index.
- *
- * @param preference The preference to evaluate.
- * @param currentDepth The current depth in the preference hierarchy.
- * @param includeDepth The maximum depth to include in the search index.
- * @return True if the preference should be included, false otherwise.
- */
- protected boolean shouldIncludePreference(Preference preference, int currentDepth, int includeDepth) {
- return includeDepth <= currentDepth
- && !(preference instanceof PreferenceCategory)
- && !isSpecialPreferenceGroup(preference)
- && !(preference instanceof PreferenceScreen);
- }
-
- /**
- * Sets up the toolbar menu for the search action.
- */
- protected void setupToolbarMenu() {
- toolbar.inflateMenu(MENU_REVANCED_SEARCH_MENU);
- toolbar.setOnMenuItemClickListener(item -> {
- if (item.getItemId() == ID_ACTION_SEARCH && !isSearchActive) {
- openSearch();
- return true;
- }
- return false;
- });
-
- // Set bold icon if needed.
- MenuItem search = toolbar.getMenu().findItem(ID_ACTION_SEARCH);
- search.setIcon(getSearchIcon());
- }
-
- /**
- * Configures listeners for the search view and toolbar navigation.
- */
- protected void setupListeners() {
- searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
- @Override
- public boolean onQueryTextSubmit(String query) {
- try {
- String queryTrimmed = query.trim();
- if (!queryTrimmed.isEmpty()) {
- searchHistoryManager.saveSearchQuery(queryTrimmed);
- }
- } catch (Exception ex) {
- Logger.printException(() -> "onQueryTextSubmit failure", ex);
- }
- return false;
- }
-
- @Override
- public boolean onQueryTextChange(String newText) {
- try {
- Logger.printDebug(() -> "Search query: " + newText);
-
- String trimmedText = newText.trim();
- if (!isSearchActive) {
- Logger.printDebug(() -> "Search is not active, skipping query processing");
- return true;
- }
-
- if (trimmedText.isEmpty()) {
- // If empty query: show history.
- hideSearchResults();
- showSearchHistory();
- } else {
- // If has search text: hide history and show search results.
- hideSearchHistory();
- filterAndShowResults(newText);
- }
- } catch (Exception ex) {
- Logger.printException(() -> "onQueryTextChange failure", ex);
- }
- return true;
- }
- });
- // Set navigation click listener.
- toolbar.setNavigationOnClickListener(view -> {
- if (isSearchActive) {
- closeSearch();
- } else {
- activity.finish();
- }
- });
- }
-
- /**
- * Initializes search data by collecting all searchable preferences from the fragment.
- * This method should be called after the preference fragment is fully loaded.
- * Runs on the UI thread to ensure proper access to preference components.
- */
- public void initializeSearchData() {
- allSearchItems.clear();
- keyToSearchItem.clear();
- // Wait until fragment is properly initialized.
- activity.runOnUiThread(() -> {
- try {
- PreferenceScreen screen = fragment.getPreferenceScreenForSearch();
- if (screen != null) {
- collectSearchablePreferences(screen);
- for (BaseSearchResultItem item : allSearchItems) {
- if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
- String key = prefItem.preference.getKey();
- if (key != null) {
- keyToSearchItem.put(key, item);
- }
- }
- }
- setupPreferenceListeners();
- Logger.printDebug(() -> "Collected " + allSearchItems.size() + " searchable preferences");
- }
- } catch (Exception ex) {
- Logger.printException(() -> "Failed to initialize search data", ex);
- }
- });
- }
-
- /**
- * Sets up listeners for preferences to keep search results in sync when preference values change.
- */
- protected void setupPreferenceListeners() {
- for (BaseSearchResultItem item : allSearchItems) {
- // Skip non-preference items.
- if (!(item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem)) continue;
- Preference pref = prefItem.preference;
-
- if (pref instanceof ColorPickerPreference colorPref) {
- colorPref.setOnColorChangeListener((prefKey, newColor) -> {
- BaseSearchResultItem.PreferenceSearchItem searchItem =
- (BaseSearchResultItem.PreferenceSearchItem) keyToSearchItem.get(prefKey);
- if (searchItem != null) {
- searchItem.setColor(newColor);
- refreshSearchResults();
- }
- });
- } else if (pref instanceof CustomDialogListPreference listPref) {
- listPref.setOnPreferenceChangeListener((preference, newValue) -> {
- BaseSearchResultItem.PreferenceSearchItem searchItem =
- (BaseSearchResultItem.PreferenceSearchItem) keyToSearchItem.get(preference.getKey());
- if (searchItem == null) return true;
-
- int index = listPref.findIndexOfValue(newValue.toString());
- if (index >= 0) {
- // Check if a static summary is set.
- boolean isStaticSummary = listPref.getStaticSummary() != null;
- if (!isStaticSummary) {
- // Only update summary if it is not static.
- CharSequence newSummary = listPref.getEntries()[index];
- listPref.setSummary(newSummary);
- }
- }
-
- listPref.clearHighlightedEntriesForDialog();
- searchItem.refreshHighlighting();
- refreshSearchResults();
- return true;
- });
- }
-
- // Let subclasses handle special preferences.
- setupSpecialPreferenceListeners(item);
- }
- }
-
- /**
- * Collects searchable preferences from a preference group.
- */
- protected void collectSearchablePreferences(PreferenceGroup group) {
- collectSearchablePreferencesWithKeys(group, "", new ArrayList<>(), 1, 0);
- }
-
- /**
- * Collects searchable preferences with their navigation paths and keys.
- *
- * @param group The preference group to collect from.
- * @param parentPath The navigation path of the parent group.
- * @param parentKeys The keys of parent preferences.
- * @param includeDepth The maximum depth to include in the search index.
- * @param currentDepth The current depth in the preference hierarchy.
- */
- protected void collectSearchablePreferencesWithKeys(PreferenceGroup group, String parentPath,
- List parentKeys, int includeDepth, int currentDepth) {
- if (group == null) return;
-
- for (int i = 0, count = group.getPreferenceCount(); i < count; i++) {
- Preference preference = group.getPreference(i);
-
- // Add to search results only if it is not a category, special group, or PreferenceScreen.
- if (shouldIncludePreference(preference, currentDepth, includeDepth)) {
- allSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem(
- preference, parentPath, parentKeys));
- }
-
- // If the preference is a group, recurse into it.
- if (preference instanceof PreferenceGroup subGroup) {
- String newPath = parentPath;
- List newKeys = new ArrayList<>(parentKeys);
-
- // Append the group title to the path and save key for navigation.
- if (!isSpecialPreferenceGroup(preference)
- && !(preference instanceof NoTitlePreferenceCategory)) {
- CharSequence title = preference.getTitle();
- if (!TextUtils.isEmpty(title)) {
- newPath = TextUtils.isEmpty(parentPath)
- ? title.toString()
- : parentPath + " > " + title;
- }
-
- // Add key for navigation if this is a PreferenceScreen or group with navigation capability.
- String key = preference.getKey();
- if (!TextUtils.isEmpty(key) && (preference instanceof PreferenceScreen
- || searchResultsAdapter.hasNavigationCapability(preference))) {
- newKeys.add(key);
- }
- }
-
- collectSearchablePreferencesWithKeys(subGroup, newPath, newKeys, includeDepth, currentDepth + 1);
- }
- }
- }
-
- /**
- * Filters all search items based on the provided query and displays results in the overlay.
- * Applies highlighting to matching text and shows a "no results" message if nothing matches.
- */
- protected void filterAndShowResults(String query) {
- hideSearchHistory();
- // Keep track of the previously displayed items to clear their highlights.
- List previouslyDisplayedItems = new ArrayList<>(filteredSearchItems);
-
- filteredSearchItems.clear();
-
- String queryLower = Utils.normalizeTextToLowercase(query);
- Pattern queryPattern = Pattern.compile(Pattern.quote(queryLower), Pattern.CASE_INSENSITIVE);
-
- // Clear highlighting only for items that were previously visible.
- // This avoids iterating through all items on every keystroke during filtering.
- for (BaseSearchResultItem item : previouslyDisplayedItems) {
- item.clearHighlighting();
- }
-
- // Collect matched items first.
- List matched = new ArrayList<>();
- int matchCount = 0;
- for (BaseSearchResultItem item : allSearchItems) {
- if (matchCount >= MAX_SEARCH_RESULTS) break; // Stop after collecting max results.
- if (item.matchesQuery(queryLower)) {
- item.applyHighlighting(queryPattern);
- matched.add(item);
- matchCount++;
- }
- }
-
- // Build filteredSearchItems, inserting parent enablers for disabled dependents.
- Set addedParentKeys = new HashSet<>(2 * matched.size());
- for (BaseSearchResultItem item : matched) {
- if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
- String key = prefItem.preference.getKey();
- Setting> setting = (key != null) ? Setting.getSettingFromPath(key) : null;
- if (setting != null && !setting.isAvailable()) {
- List> parentSettings = setting.getParentSettings();
- for (Setting> parentSetting : parentSettings) {
- BaseSearchResultItem parentItem = keyToSearchItem.get(parentSetting.key);
- if (parentItem != null && !addedParentKeys.contains(parentSetting.key)) {
- if (!parentItem.matchesQuery(queryLower)) {
- // Apply highlighting to parent items even if they don't match the query.
- // This ensures they get their current effective summary calculated.
- parentItem.applyHighlighting(queryPattern);
- filteredSearchItems.add(parentItem);
- }
- addedParentKeys.add(parentSetting.key);
- }
- }
- }
- filteredSearchItems.add(item);
- if (key != null) {
- addedParentKeys.add(key);
- }
- }
- }
-
- if (!filteredSearchItems.isEmpty()) {
- //noinspection ComparatorCombinators
- Collections.sort(filteredSearchItems, (o1, o2) ->
- o1.navigationPath.compareTo(o2.navigationPath)
- );
- List displayItems = new ArrayList<>();
- String currentPath = null;
- for (BaseSearchResultItem item : filteredSearchItems) {
- if (!item.navigationPath.equals(currentPath)) {
- BaseSearchResultItem header = new BaseSearchResultItem.GroupHeaderItem(item.navigationPath, item.navigationKeys);
- displayItems.add(header);
- currentPath = item.navigationPath;
- }
- displayItems.add(item);
- }
- filteredSearchItems.clear();
- filteredSearchItems.addAll(displayItems);
- }
- // Show "No results found" if search results are empty.
- if (filteredSearchItems.isEmpty()) {
- Preference noResultsPreference = new Preference(activity);
- noResultsPreference.setKey("no_results_placeholder");
- noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
- noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
- noResultsPreference.setSelectable(false);
- noResultsPreference.setIcon(getSearchIcon());
- filteredSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem(noResultsPreference, "", Collections.emptyList()));
- }
-
- searchResultsAdapter.notifyDataSetChanged();
- overlayContainer.setVisibility(View.VISIBLE);
- }
-
- /**
- * Opens the search interface by showing the search view and hiding the menu item.
- * Configures the UI for search mode, shows the keyboard, and displays search suggestions.
- */
- protected void openSearch() {
- isSearchActive = true;
- toolbar.getMenu().findItem(ID_ACTION_SEARCH).setVisible(false);
- toolbar.setTitle("");
- searchContainer.setVisibility(View.VISIBLE);
- searchView.requestFocus();
- // Configure soft input mode to adjust layout and show keyboard.
- activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
- | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
- inputMethodManager.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT);
- // Always show search history when opening search.
- showSearchHistory();
- }
-
- /**
- * Closes the search interface and restores the normal UI state.
- * Hides the overlay, clears search results, dismisses the keyboard, and removes highlighting.
- */
- public void closeSearch() {
- isSearchActive = false;
- isShowingSearchHistory = false;
-
- searchHistoryManager.hideSearchHistoryContainer();
- overlayContainer.setVisibility(View.GONE);
-
- filteredSearchItems.clear();
-
- searchContainer.setVisibility(View.GONE);
- toolbar.getMenu().findItem(ID_ACTION_SEARCH).setVisible(true);
- toolbar.setTitle(originalTitle);
- searchView.setQuery("", false);
- // Hide keyboard and reset soft input mode.
- inputMethodManager.hideSoftInputFromWindow(searchView.getWindowToken(), 0);
- activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
- // Clear highlighting for all search items.
- for (BaseSearchResultItem item : allSearchItems) {
- item.clearHighlighting();
- }
-
- searchResultsAdapter.notifyDataSetChanged();
- }
-
- /**
- * Shows the search history if enabled.
- */
- protected void showSearchHistory() {
- if (searchHistoryManager.isSearchHistoryEnabled()) {
- overlayContainer.setVisibility(View.VISIBLE);
- searchHistoryManager.showSearchHistory();
- isShowingSearchHistory = true;
- } else {
- hideAllOverlays();
- }
- }
-
- /**
- * Hides the search history container.
- */
- protected void hideSearchHistory() {
- searchHistoryManager.hideSearchHistoryContainer();
- isShowingSearchHistory = false;
- }
-
- /**
- * Hides all overlay containers, including search results and history.
- */
- protected void hideAllOverlays() {
- hideSearchHistory();
- hideSearchResults();
- }
-
- /**
- * Hides the search results overlay and clears the filtered results.
- */
- protected void hideSearchResults() {
- overlayContainer.setVisibility(View.GONE);
- filteredSearchItems.clear();
- searchResultsAdapter.notifyDataSetChanged();
- for (BaseSearchResultItem item : allSearchItems) {
- item.clearHighlighting();
- }
- }
-
- /**
- * Refreshes the search results display if the search is active and history is not shown.
- */
- protected void refreshSearchResults() {
- if (isSearchActive && !isShowingSearchHistory) {
- searchResultsAdapter.notifyDataSetChanged();
- }
- }
-
- /**
- * Finds a search item corresponding to the given preference.
- *
- * @param preference The preference to find a search item for.
- * @return The corresponding PreferenceSearchItem, or null if not found.
- */
- public BaseSearchResultItem.PreferenceSearchItem findSearchItemByPreference(Preference preference) {
- // First, search in filtered results.
- for (BaseSearchResultItem item : filteredSearchItems) {
- if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
- if (prefItem.preference == preference) {
- return prefItem;
- }
- }
- }
- // If not found, search in all items.
- for (BaseSearchResultItem item : allSearchItems) {
- if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
- if (prefItem.preference == preference) {
- return prefItem;
- }
- }
- }
-
- return null;
- }
-
- /**
- * Gets the background color for search view components based on current theme.
- */
- @ColorInt
- public static int getSearchViewBackground() {
- return Utils.adjustColorBrightness(Utils.getDialogBackgroundColor(), Utils.isDarkModeEnabled() ? 1.11f : 0.95f);
- }
-
- /**
- * Creates a rounded background drawable for the main search view.
- */
- protected static GradientDrawable createBackgroundDrawable() {
- GradientDrawable background = new GradientDrawable();
- background.setShape(GradientDrawable.RECTANGLE);
- background.setCornerRadius(Dim.dp28);
- background.setColor(getSearchViewBackground());
- return background;
- }
-
- /**
- * Return if a search is currently active.
- */
- public boolean isSearchActive() {
- return isSearchActive;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/SearchHistoryManager.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/SearchHistoryManager.java
deleted file mode 100644
index 773c8a9241..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/SearchHistoryManager.java
+++ /dev/null
@@ -1,402 +0,0 @@
-package app.revanced.extension.shared.settings.search;
-
-import static app.revanced.extension.shared.StringRef.str;
-import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
-import static app.revanced.extension.shared.settings.BaseSettings.SETTINGS_SEARCH_ENTRIES;
-import static app.revanced.extension.shared.settings.BaseSettings.SETTINGS_SEARCH_HISTORY;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.Context;
-import android.util.Pair;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Deque;
-import java.util.LinkedList;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.preference.BulletPointPreference;
-import app.revanced.extension.shared.ui.CustomDialog;
-
-/**
- * Manager for search history functionality.
- */
-public class SearchHistoryManager {
- /**
- * Interface for handling history item selection.
- */
- private static final int MAX_HISTORY_SIZE = 5; // Maximum history items stored.
-
- private static final int ID_CLEAR_HISTORY_BUTTON = getResourceIdentifierOrThrow(
- ResourceType.ID, "clear_history_button");
- private static final int ID_HISTORY_TEXT = getResourceIdentifierOrThrow(
- ResourceType.ID, "history_text");
- private static final int ID_HISTORY_ICON = getResourceIdentifierOrThrow(
- ResourceType.ID, "history_icon");
- private static final int ID_DELETE_ICON = getResourceIdentifierOrThrow(
- ResourceType.ID, "delete_icon");
- private static final int ID_EMPTY_HISTORY_TITLE = getResourceIdentifierOrThrow(
- ResourceType.ID, "empty_history_title");
- private static final int ID_EMPTY_HISTORY_SUMMARY = getResourceIdentifierOrThrow(
- ResourceType.ID, "empty_history_summary");
- private static final int ID_SEARCH_HISTORY_HEADER = getResourceIdentifierOrThrow(
- ResourceType.ID, "search_history_header");
- private static final int ID_SEARCH_TIPS_SUMMARY = getResourceIdentifierOrThrow(
- ResourceType.ID, "revanced_settings_search_tips_summary");
- private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN = getResourceIdentifierOrThrow(
- ResourceType.LAYOUT, "revanced_preference_search_history_screen");
- private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM = getResourceIdentifierOrThrow(
- ResourceType.LAYOUT, "revanced_preference_search_history_item");
- private static final int ID_SEARCH_HISTORY_LIST = getResourceIdentifierOrThrow(
- ResourceType.ID, "search_history_list");
- private static final int ID_SEARCH_REMOVE_ICON = getResourceIdentifierOrThrow(
- ResourceType.DRAWABLE, "revanced_settings_search_remove");
- private static final int ID_SEARCH_REMOVE_ICON_BOLD = getResourceIdentifierOrThrow(
- ResourceType.DRAWABLE, "revanced_settings_search_remove_bold");
- private static final int ID_SEARCH_ARROW_TIME_ICON = getResourceIdentifierOrThrow(
- ResourceType.DRAWABLE, "revanced_settings_arrow_time");
- private static final int ID_SEARCH_ARROW_TIME_ICON_BOLD = getResourceIdentifierOrThrow(
- ResourceType.DRAWABLE, "revanced_settings_arrow_time_bold");
-
- private final Deque searchHistory;
- private final Activity activity;
- private final SearchHistoryAdapter searchHistoryAdapter;
- private final boolean showSettingsSearchHistory;
- private final FrameLayout searchHistoryContainer;
-
- public interface OnSelectHistoryItemListener {
- void onSelectHistoryItem(String query);
- }
-
- /**
- * Constructor for SearchHistoryManager.
- *
- * @param activity The parent activity.
- * @param overlayContainer The overlay container to hold the search history container.
- * @param onSelectHistoryItemAction Callback for when a history item is selected.
- */
- SearchHistoryManager(Activity activity, FrameLayout overlayContainer,
- OnSelectHistoryItemListener onSelectHistoryItemAction) {
- this.activity = activity;
- this.showSettingsSearchHistory = SETTINGS_SEARCH_HISTORY.get();
- this.searchHistory = new LinkedList<>();
-
- // Initialize search history from settings.
- if (showSettingsSearchHistory) {
- String entries = SETTINGS_SEARCH_ENTRIES.get();
- if (!entries.isBlank()) {
- searchHistory.addAll(Arrays.asList(entries.split("\n")));
- }
- } else {
- // Clear old saved history if the feature is disabled.
- SETTINGS_SEARCH_ENTRIES.resetToDefault();
- }
-
- // Create search history container.
- this.searchHistoryContainer = new FrameLayout(activity);
- searchHistoryContainer.setVisibility(View.GONE);
-
- // Inflate search history layout.
- LayoutInflater inflater = LayoutInflater.from(activity);
- View historyView = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN,
- searchHistoryContainer, false);
- searchHistoryContainer.addView(historyView, new FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT));
-
- // Add history container to overlay.
- FrameLayout.LayoutParams overlayParams = new FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT);
- overlayParams.gravity = Gravity.TOP;
- overlayContainer.addView(searchHistoryContainer, overlayParams);
-
- // Find the LinearLayout for the history list within the container.
- LinearLayout searchHistoryListView = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_LIST);
- if (searchHistoryListView == null) {
- throw new IllegalStateException("Search history list view not found in container");
- }
-
- // Set up history adapter. Use a copy of the search history.
- this.searchHistoryAdapter = new SearchHistoryAdapter(activity, searchHistoryListView,
- new ArrayList<>(searchHistory), onSelectHistoryItemAction);
-
- // Set up clear history button.
- TextView clearHistoryButton = searchHistoryContainer.findViewById(ID_CLEAR_HISTORY_BUTTON);
- clearHistoryButton.setOnClickListener(v -> createAndShowDialog(
- str("revanced_settings_search_clear_history"),
- str("revanced_settings_search_clear_history_message"),
- this::clearAllSearchHistory
- ));
-
- // Set up search tips summary.
- CharSequence text = BulletPointPreference.formatIntoBulletPoints(
- str("revanced_settings_search_tips_summary"));
- TextView tipsSummary = historyView.findViewById(ID_SEARCH_TIPS_SUMMARY);
- tipsSummary.setText(text);
- }
-
- /**
- * Shows search history screen - either with history items or empty history message.
- */
- public void showSearchHistory() {
- if (!showSettingsSearchHistory) {
- return;
- }
-
- // Find all view elements.
- TextView emptyHistoryTitle = searchHistoryContainer.findViewById(ID_EMPTY_HISTORY_TITLE);
- TextView emptyHistorySummary = searchHistoryContainer.findViewById(ID_EMPTY_HISTORY_SUMMARY);
- TextView historyHeader = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_HEADER);
- LinearLayout historyList = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_LIST);
- TextView clearHistoryButton = searchHistoryContainer.findViewById(ID_CLEAR_HISTORY_BUTTON);
-
- if (searchHistory.isEmpty()) {
- // Show empty history state.
- showEmptyHistoryViews(emptyHistoryTitle, emptyHistorySummary);
- hideHistoryViews(historyHeader, historyList, clearHistoryButton);
- } else {
- // Show history list state.
- hideEmptyHistoryViews(emptyHistoryTitle, emptyHistorySummary);
- showHistoryViews(historyHeader, historyList, clearHistoryButton);
-
- // Update adapter with current history.
- searchHistoryAdapter.clear();
- searchHistoryAdapter.addAll(searchHistory);
- searchHistoryAdapter.notifyDataSetChanged();
- }
-
- // Show the search history container.
- showSearchHistoryContainer();
- }
-
- /**
- * Saves a search query to the history, maintaining the size limit.
- */
- public void saveSearchQuery(String query) {
- if (!showSettingsSearchHistory) return;
-
- searchHistory.remove(query); // Remove if already exists to update position.
- searchHistory.addFirst(query); // Add to the most recent.
-
- // Remove extra old entries.
- while (searchHistory.size() > MAX_HISTORY_SIZE) {
- String last = searchHistory.removeLast();
- Logger.printDebug(() -> "Removing search history query: " + last);
- }
-
- saveSearchHistory();
- }
-
- /**
- * Saves the search history to shared preferences.
- */
- protected void saveSearchHistory() {
- Logger.printDebug(() -> "Saving search history: " + searchHistory);
- SETTINGS_SEARCH_ENTRIES.save(String.join("\n", searchHistory));
- }
-
- /**
- * Removes a search query from the history.
- */
- public void removeSearchQuery(String query) {
- searchHistory.remove(query);
- saveSearchHistory();
- }
-
- /**
- * Clears all search history.
- */
- public void clearAllSearchHistory() {
- searchHistory.clear();
- saveSearchHistory();
- searchHistoryAdapter.clear();
- searchHistoryAdapter.notifyDataSetChanged();
- showSearchHistory();
- }
-
- /**
- * Checks if search history feature is enabled.
- */
- public boolean isSearchHistoryEnabled() {
- return showSettingsSearchHistory;
- }
-
- /**
- * Shows the search history container and overlay.
- */
- public void showSearchHistoryContainer() {
- searchHistoryContainer.setVisibility(View.VISIBLE);
- }
-
- /**
- * Hides the search history container.
- */
- public void hideSearchHistoryContainer() {
- searchHistoryContainer.setVisibility(View.GONE);
- }
-
- /**
- * Helper method to show empty history views.
- */
- protected void showEmptyHistoryViews(TextView emptyTitle, TextView emptySummary) {
- emptyTitle.setVisibility(View.VISIBLE);
- emptyTitle.setText(str("revanced_settings_search_empty_history_title"));
- emptySummary.setVisibility(View.VISIBLE);
- emptySummary.setText(str("revanced_settings_search_empty_history_summary"));
- }
-
- /**
- * Helper method to hide empty history views.
- */
- protected void hideEmptyHistoryViews(TextView emptyTitle, TextView emptySummary) {
- emptyTitle.setVisibility(View.GONE);
- emptySummary.setVisibility(View.GONE);
- }
-
- /**
- * Helper method to show history list views.
- */
- protected void showHistoryViews(TextView header, LinearLayout list, TextView clearButton) {
- header.setVisibility(View.VISIBLE);
- list.setVisibility(View.VISIBLE);
- clearButton.setVisibility(View.VISIBLE);
- }
-
- /**
- * Helper method to hide history list views.
- */
- protected void hideHistoryViews(TextView header, LinearLayout list, TextView clearButton) {
- header.setVisibility(View.GONE);
- list.setVisibility(View.GONE);
- clearButton.setVisibility(View.GONE);
- }
-
- /**
- * Creates and shows a dialog with the specified title, message, and confirmation action.
- *
- * @param title The title of the dialog.
- * @param message The message to display in the dialog.
- * @param confirmAction The action to perform when the dialog is confirmed.
- */
- protected void createAndShowDialog(String title, String message, Runnable confirmAction) {
- Pair dialogPair = CustomDialog.create(
- activity,
- title,
- message,
- null,
- null,
- confirmAction,
- () -> {},
- null,
- null,
- false
- );
-
- Dialog dialog = dialogPair.first;
- dialog.setCancelable(true);
- dialog.show();
- }
-
-
- /**
- * Custom adapter for search history items.
- */
- protected class SearchHistoryAdapter {
- protected final Collection history;
- protected final LayoutInflater inflater;
- protected final LinearLayout container;
- protected final OnSelectHistoryItemListener onSelectHistoryItemListener;
-
- public SearchHistoryAdapter(Context context, LinearLayout container, Collection history,
- OnSelectHistoryItemListener listener) {
- this.history = history;
- this.inflater = LayoutInflater.from(context);
- this.container = container;
- this.onSelectHistoryItemListener = listener;
- }
-
- /**
- * Updates the container with current history items.
- */
- public void notifyDataSetChanged() {
- container.removeAllViews();
- for (String query : history) {
- View view = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM,
- container, false);
- // Set click listener for main item (select query).
- view.setOnClickListener(v -> onSelectHistoryItemListener.onSelectHistoryItem(query));
-
- // Set history icon.
- ImageView historyIcon = view.findViewById(ID_HISTORY_ICON);
- historyIcon.setImageResource(Utils.appIsUsingBoldIcons()
- ? ID_SEARCH_ARROW_TIME_ICON_BOLD
- : ID_SEARCH_ARROW_TIME_ICON
- );
-
- TextView historyText = view.findViewById(ID_HISTORY_TEXT);
- historyText.setText(query);
-
- // Set click listener for delete icon.
- ImageView deleteIcon = view.findViewById(ID_DELETE_ICON);
-
- deleteIcon.setImageResource(Utils.appIsUsingBoldIcons()
- ? ID_SEARCH_REMOVE_ICON_BOLD
- : ID_SEARCH_REMOVE_ICON
- );
-
- deleteIcon.setOnClickListener(v -> createAndShowDialog(
- query,
- str("revanced_settings_search_remove_message"),
- () -> {
- removeSearchQuery(query);
- remove(query);
- notifyDataSetChanged();
- }
- ));
-
- container.addView(view);
- }
- }
-
- /**
- * Clears all views from the container and history list.
- */
- public void clear() {
- history.clear();
- container.removeAllViews();
- }
-
- /**
- * Adds all provided history items to the container.
- */
- public void addAll(Collection items) {
- history.addAll(items);
- notifyDataSetChanged();
- }
-
- /**
- * Removes a query from the history and updates the container.
- */
- public void remove(String query) {
- history.remove(query);
- if (history.isEmpty()) {
- // If history is now empty, show the empty history state.
- showSearchHistory();
- } else {
- notifyDataSetChanged();
- }
- }
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java
deleted file mode 100644
index 9abd430719..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java
+++ /dev/null
@@ -1,270 +0,0 @@
-package app.revanced.extension.shared.spoof;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Locale;
-import java.util.Objects;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-
-@SuppressWarnings("ConstantLocale")
-public enum ClientType {
- /**
- * Video not playable: Paid, Movie, Private, Age-restricted.
- * Uses non-adaptive bitrate.
- * AV1 codec available.
- */
- ANDROID_REEL(
- 3,
- "ANDROID",
- "com.google.android.youtube",
- Build.MANUFACTURER,
- Build.MODEL,
- "Android",
- Build.VERSION.RELEASE,
- String.valueOf(Build.VERSION.SDK_INT),
- Build.ID,
- "20.44.38",
- // This client has been used by most open-source YouTube stream extraction tools since 2024, including NewPipe Extractor, SmartTube, and Grayjay.
- // This client can log in, but if an access token is used in the request, GVS can more easily identify the request as coming from ReVanced.
- // This means that the GVS server can strengthen its validation of the ANDROID_REEL client.
- true,
- true,
- false,
- "Android Reel"
- ),
- /**
- * Video not playable: Kids / Paid / Movie / Private / Age-restricted.
- * This client can only be used when logged out.
- */
- // https://dumps.tadiphone.dev/dumps/oculus/eureka
- ANDROID_VR_1_61_48(
- 28,
- "ANDROID_VR",
- "com.google.android.apps.youtube.vr.oculus",
- "Oculus",
- "Quest 3",
- "Android",
- "12",
- // Android 12.1
- "32",
- "SQ3A.220605.009.A1",
- "1.61.48",
- false,
- false,
- true,
- "Android VR 1.61"
- ),
- /**
- * Uses non adaptive bitrate, which fixes audio stuttering with YT Music.
- * Does not use AV1.
- */
- ANDROID_VR_1_43_32(
- ANDROID_VR_1_61_48.id,
- ANDROID_VR_1_61_48.clientName,
- Objects.requireNonNull(ANDROID_VR_1_61_48.packageName),
- ANDROID_VR_1_61_48.deviceMake,
- ANDROID_VR_1_61_48.deviceModel,
- ANDROID_VR_1_61_48.osName,
- ANDROID_VR_1_61_48.osVersion,
- Objects.requireNonNull(ANDROID_VR_1_61_48.androidSdkVersion),
- Objects.requireNonNull(ANDROID_VR_1_61_48.buildId),
- "1.43.32",
- ANDROID_VR_1_61_48.useAuth,
- ANDROID_VR_1_61_48.supportsMultiAudioTracks,
- ANDROID_VR_1_61_48.usePlayerEndpoint,
- "Android VR 1.43"
- ),
- /**
- * Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
- * Google Pixel 9 Pro Fold
- */
- ANDROID_CREATOR(
- 14,
- "ANDROID_CREATOR",
- "com.google.android.apps.youtube.creator",
- "Google",
- "Pixel 9 Pro Fold",
- "Android",
- "15",
- "35",
- "AP3A.241005.015.A2",
- "23.47.101",
- true,
- false,
- true,
- "Android Studio"
- ),
- /**
- * Internal YT client for an unreleased YT client. May stop working at any time.
- */
- VISIONOS(101,
- "VISIONOS",
- "Apple",
- "RealityDevice14,1",
- "visionOS",
- "1.3.21O771",
- "0.1",
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
- false,
- false,
- true,
- "visionOS"
- );
-
- /**
- * YouTube
- * client type
- */
- public final int id;
-
- public final String clientName;
-
- /**
- * App package name.
- */
- @Nullable
- private final String packageName;
-
- /**
- * Player user-agent.
- */
- public final String userAgent;
-
- /**
- * Device model, equivalent to {@link Build#MANUFACTURER} (System property: ro.product.vendor.manufacturer)
- */
- public final String deviceMake;
-
- /**
- * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.vendor.model)
- */
- public final String deviceModel;
-
- /**
- * Device OS name.
- */
- public final String osName;
-
- /**
- * Device OS version.
- */
- public final String osVersion;
-
- /**
- * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
- * Field is null if not applicable.
- */
- @Nullable
- public final String androidSdkVersion;
-
- /**
- * Android build id, equivalent to {@link Build#ID}.
- * Field is null if not applicable.
- */
- @Nullable
- private final String buildId;
-
- /**
- * App version.
- */
- public final String clientVersion;
-
- /**
- * If the client should use authentication if available.
- */
- public final boolean useAuth;
-
- /**
- * If the client supports multiple audio tracks.
- */
- public final boolean supportsMultiAudioTracks;
-
- /**
- * If the client should use the player endpoint for stream extraction.
- */
- public final boolean usePlayerEndpoint;
-
- /**
- * Friendly name displayed in stats for nerds.
- */
- public final String friendlyName;
-
- /**
- * Android constructor.
- */
- ClientType(int id,
- String clientName,
- @NonNull String packageName,
- String deviceMake,
- String deviceModel,
- String osName,
- String osVersion,
- @NonNull String androidSdkVersion,
- @NonNull String buildId,
- String clientVersion,
- boolean useAuth,
- boolean supportsMultiAudioTracks,
- boolean usePlayerEndpoint,
- String friendlyName) {
- this.id = id;
- this.clientName = clientName;
- this.packageName = packageName;
- this.deviceMake = deviceMake;
- this.deviceModel = deviceModel;
- this.osName = osName;
- this.osVersion = osVersion;
- this.androidSdkVersion = androidSdkVersion;
- this.buildId = buildId;
- this.clientVersion = clientVersion;
- this.useAuth = useAuth;
- this.supportsMultiAudioTracks = supportsMultiAudioTracks;
- this.usePlayerEndpoint = usePlayerEndpoint;
- this.friendlyName = friendlyName;
-
- Locale defaultLocale = Locale.getDefault();
- this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s)",
- packageName,
- clientVersion,
- osVersion,
- defaultLocale,
- deviceModel,
- buildId
- );
- Logger.printDebug(() -> "userAgent: " + this.userAgent);
- }
-
- @SuppressWarnings("ConstantLocale")
- ClientType(int id,
- String clientName,
- String deviceMake,
- String deviceModel,
- String osName,
- String osVersion,
- String clientVersion,
- String userAgent,
- boolean useAuth,
- boolean supportsMultiAudioTracks,
- boolean usePlayerEndpoint,
- String friendlyName) {
- this.id = id;
- this.clientName = clientName;
- this.deviceMake = deviceMake;
- this.deviceModel = deviceModel;
- this.osName = osName;
- this.osVersion = osVersion;
- this.clientVersion = clientVersion;
- this.userAgent = userAgent;
- this.useAuth = useAuth;
- this.supportsMultiAudioTracks = supportsMultiAudioTracks;
- this.usePlayerEndpoint = usePlayerEndpoint;
- this.friendlyName = friendlyName;
- this.packageName = null;
- this.androidSdkVersion = null;
- this.buildId = null;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java
deleted file mode 100644
index 0c861510fe..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java
+++ /dev/null
@@ -1,322 +0,0 @@
-package app.revanced.extension.shared.spoof;
-
-import android.net.Uri;
-import android.text.TextUtils;
-
-import androidx.annotation.Nullable;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.AppLanguage;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
-
-@SuppressWarnings("unused")
-public class SpoofVideoStreamsPatch {
-
- /**
- * Domain used for internet connectivity verification.
- * It has an empty response body and is only used to check for a 204 response code.
- *
- * If an unreachable IP address (127.0.0.1) is used, no response code is provided.
- *
- * YouTube handles unreachable IP addresses without issue.
- * YouTube Music has an issue with waiting for the Cronet connect timeout of 30s on mobile networks.
- *
- * Using a VPN or DNS can temporarily resolve this issue,
- * But the ideal workaround is to avoid using an unreachable IP address.
- */
- private static final String INTERNET_CONNECTION_CHECK_URI_STRING = "https://www.google.com/gen_204";
- private static final Uri INTERNET_CONNECTION_CHECK_URI = Uri.parse(INTERNET_CONNECTION_CHECK_URI_STRING);
-
- private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_VIDEO_STREAMS.get();
-
- @Nullable
- private static volatile AppLanguage languageOverride;
-
- private static volatile ClientType preferredClient = ClientType.ANDROID_REEL;
-
- /**
- * @return If this patch was included during patching.
- */
- public static boolean isPatchIncluded() {
- return false; // Modified during patching.
- }
-
- @Nullable
- public static AppLanguage getLanguageOverride() {
- return languageOverride;
- }
-
- /**
- * @param language Language override for non-authenticated requests.
- */
- public static void setLanguageOverride(@Nullable AppLanguage language) {
- languageOverride = language;
- }
-
- public static void setClientsToUse(List availableClients, ClientType client) {
- preferredClient = Objects.requireNonNull(client);
- StreamingDataRequest.setClientOrderToUse(availableClients, client);
- }
-
- public static ClientType getPreferredClient() {
- return preferredClient;
- }
-
- public static boolean spoofingToClientWithNoMultiAudioStreams() {
- return isPatchIncluded()
- && SPOOF_STREAMING_DATA
- && !preferredClient.supportsMultiAudioTracks;
- }
-
- /**
- * Injection point.
- * Blocks /get_watch requests by returning an unreachable URI.
- *
- * @param playerRequestUri The URI of the player request.
- * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
- */
- public static Uri blockGetWatchRequest(Uri playerRequestUri) {
- if (SPOOF_STREAMING_DATA) {
- try {
- String path = playerRequestUri.getPath();
-
- if (path != null && path.contains("get_watch")) {
- Logger.printDebug(() -> "Blocking 'get_watch' by returning internet connection check uri");
-
- return INTERNET_CONNECTION_CHECK_URI;
- }
- } catch (Exception ex) {
- Logger.printException(() -> "blockGetWatchRequest failure", ex);
- }
- }
-
- return playerRequestUri;
- }
-
- /**
- * Injection point.
- *
- * Blocks /get_watch requests by returning an unreachable URI.
- * /att/get requests are used to obtain a PoToken challenge.
- * See: botGuardScript.js#L15
- *
- * Since the Spoof streaming data patch was implemented because a valid PoToken cannot be obtained,
- * Blocking /att/get requests are not a problem.
- */
- public static String blockGetAttRequest(String originalUrlString) {
- if (SPOOF_STREAMING_DATA) {
- try {
- var originalUri = Uri.parse(originalUrlString);
- String path = originalUri.getPath();
-
- if (path != null && path.contains("att/get")) {
- Logger.printDebug(() -> "Blocking 'att/get' by returning internet connection check uri");
-
- return INTERNET_CONNECTION_CHECK_URI_STRING;
- }
- } catch (Exception ex) {
- Logger.printException(() -> "blockGetAttRequest failure", ex);
- }
- }
-
- return originalUrlString;
- }
-
- /**
- * Injection point.
- *
- * Blocks /initplayback requests.
- */
- public static String blockInitPlaybackRequest(String originalUrlString) {
- if (SPOOF_STREAMING_DATA) {
- try {
- var originalUri = Uri.parse(originalUrlString);
- String path = originalUri.getPath();
-
- if (path != null && path.contains("initplayback")) {
- Logger.printDebug(() -> "Blocking 'initplayback' by returning internet connection check uri");
-
- return INTERNET_CONNECTION_CHECK_URI_STRING;
- }
- } catch (Exception ex) {
- Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
- }
- }
-
- return originalUrlString;
- }
-
- /**
- * Injection point.
- */
- public static boolean isSpoofingEnabled() {
- return SPOOF_STREAMING_DATA;
- }
-
- /**
- * Injection point.
- * Only invoked when playing a livestream on an Apple client.
- */
- public static boolean fixHLSCurrentTime(boolean original) {
- if (!SPOOF_STREAMING_DATA) {
- return original;
- }
- return false;
- }
-
- /*
- * Injection point.
- * Fix audio stuttering in YouTube Music.
- */
- public static boolean disableSABR() {
- return SPOOF_STREAMING_DATA;
- }
-
- /**
- * Injection point.
- * Turns off a feature flag that interferes with spoofing.
- */
- public static boolean useMediaFetchHotConfigReplacement(boolean original) {
- if (original) {
- Logger.printDebug(() -> "useMediaFetchHotConfigReplacement is set on");
- }
-
- if (!SPOOF_STREAMING_DATA) {
- return original;
- }
- return false;
- }
-
- /**
- * Injection point.
- * Turns off a feature flag that interferes with video playback.
- */
- public static boolean usePlaybackStartFeatureFlag(boolean original) {
- if (original) {
- Logger.printDebug(() -> "usePlaybackStartFeatureFlag is set on");
- }
-
- if (!SPOOF_STREAMING_DATA) {
- return original;
- }
- return false;
- }
-
- /**
- * Injection point.
- */
- public static void fetchStreams(String url, Map requestHeaders) {
- if (SPOOF_STREAMING_DATA) {
- try {
- Uri uri = Uri.parse(url);
- String path = uri.getPath();
- if (path == null || !path.contains("player")) {
- return;
- }
-
- // 'get_drm_license' has no video id and appears to happen when waiting for a paid video to start.
- // 'heartbeat' has no video id and appears to be only after playback has started.
- // 'refresh' has no video id and appears to happen when waiting for a livestream to start.
- // 'ad_break' has no video id.
- if (path.contains("get_drm_license") || path.contains("heartbeat")
- || path.contains("refresh") || path.contains("ad_break")) {
- Logger.printDebug(() -> "Ignoring path: " + path);
- return;
- }
-
- String id = uri.getQueryParameter("id");
- if (id == null) {
- Logger.printException(() -> "Ignoring request with no id: " + url);
- return;
- }
-
- StreamingDataRequest.fetchRequest(id, requestHeaders);
- } catch (Exception ex) {
- Logger.printException(() -> "buildRequest failure", ex);
- }
- }
- }
-
- /**
- * Injection point.
- * Fix playback by replace the streaming data.
- * Called after {@link #fetchStreams(String, Map)}.
- */
- @Nullable
- public static byte[] getStreamingData(String videoId) {
- if (SPOOF_STREAMING_DATA) {
- try {
- StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);
- if (request != null) {
- // This hook is always called off the main thread,
- // but this can later be called for the same video id from the main thread.
- // This is not a concern, since the fetch will always be finished
- // and never block the main thread.
- // But if debugging, then still verify this is the situation.
- if (BaseSettings.DEBUG.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
- Logger.printException(() -> "Error: Blocking main thread");
- }
-
- var stream = request.getStream();
- if (stream != null) {
- Logger.printDebug(() -> "Overriding video stream: " + videoId);
- return stream;
- }
- }
-
- Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId);
- } catch (Exception ex) {
- Logger.printException(() -> "getStreamingData failure", ex);
- }
- }
-
- return null;
- }
-
- /**
- * Injection point.
- * Called after {@link #getStreamingData(String)}.
- */
- @Nullable
- public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) {
- if (SPOOF_STREAMING_DATA) {
- try {
- final int methodPost = 2;
- if (method == methodPost) {
- String path = uri.getPath();
- if (path != null && path.contains("videoplayback")) {
- return null;
- }
- }
- } catch (Exception ex) {
- Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex);
- }
- }
-
- return postData;
- }
-
- /**
- * Injection point.
- */
- public static String appendSpoofedClient(String videoFormat) {
- try {
- if (SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get()
- && !TextUtils.isEmpty(videoFormat)) {
- // Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages.
- return "\u202D" + videoFormat + "\u2009(" // u202D = left to right override
- + StreamingDataRequest.getLastSpoofedClientName() + ")";
- }
- } catch (Exception ex) {
- Logger.printException(() -> "appendSpoofedClient failure", ex);
- }
-
- return videoFormat;
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java
deleted file mode 100644
index 959048d1e2..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package app.revanced.extension.shared.spoof.requests;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.util.Locale;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.requests.Requester;
-import app.revanced.extension.shared.requests.Route;
-import app.revanced.extension.shared.settings.AppLanguage;
-import app.revanced.extension.shared.spoof.ClientType;
-import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
-
-final class PlayerRoutes {
- static final Route.CompiledRoute GET_PLAYER_STREAMING_DATA = new Route(
- Route.Method.POST,
- "player" +
- "?fields=streamingData" +
- "&alt=proto"
- ).compile();
-
- static final Route.CompiledRoute GET_REEL_STREAMING_DATA = new Route(
- Route.Method.POST,
- "reel/reel_item_watch" +
- "?fields=playerResponse.playabilityStatus,playerResponse.streamingData" +
- "&alt=proto"
- ).compile();
-
- private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
-
- /**
- * TCP connection and HTTP read timeout
- */
- private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds.
-
- private PlayerRoutes() {
- }
-
- static String createInnertubeBody(ClientType clientType, String videoId) {
- JSONObject innerTubeBody = new JSONObject();
-
- try {
- JSONObject context = new JSONObject();
-
- AppLanguage language = SpoofVideoStreamsPatch.getLanguageOverride();
- if (language == null) {
- // Force original audio has not overrode the language.
- language = AppLanguage.DEFAULT;
- }
- //noinspection ExtractMethodRecommender
- Locale streamLocale = language.getLocale();
-
- JSONObject client = new JSONObject();
-
- client.put("deviceMake", clientType.deviceMake);
- client.put("deviceModel", clientType.deviceModel);
- client.put("clientName", clientType.clientName);
- client.put("clientVersion", clientType.clientVersion);
- client.put("osName", clientType.osName);
- client.put("osVersion", clientType.osVersion);
- if (clientType.androidSdkVersion != null) {
- client.put("androidSdkVersion", clientType.androidSdkVersion);
- }
- client.put("hl", streamLocale.getLanguage());
- client.put("gl", streamLocale.getCountry());
- context.put("client", client);
-
- innerTubeBody.put("context", context);
-
- if (clientType.usePlayerEndpoint) {
- innerTubeBody.put("contentCheckOk", true);
- innerTubeBody.put("racyCheckOk", true);
- innerTubeBody.put("videoId", videoId);
- } else {
- JSONObject playerRequest = new JSONObject();
- playerRequest.put("contentCheckOk", true);
- playerRequest.put("racyCheckOk", true);
- playerRequest.put("videoId", videoId);
- innerTubeBody.put("playerRequest", playerRequest);
- innerTubeBody.put("disablePlayerResponse", false);
- }
- } catch (JSONException e) {
- Logger.printException(() -> "Failed to create innerTubeBody", e);
- }
-
- return innerTubeBody.toString();
- }
-
- @SuppressWarnings("SameParameterValue")
- static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
- var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
-
- connection.setRequestProperty("Content-Type", "application/json");
- connection.setRequestProperty("User-Agent", clientType.userAgent);
- // Not a typo. "Client-Name" uses the client type id.
- connection.setRequestProperty("X-YouTube-Client-Name", String.valueOf(clientType.id));
- connection.setRequestProperty("X-YouTube-Client-Version", clientType.clientVersion);
-
- connection.setUseCaches(false);
- connection.setDoOutput(true);
-
- connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
- connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
- return connection;
- }
-}
\ No newline at end of file
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java
deleted file mode 100644
index fb8a8e79e8..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java
+++ /dev/null
@@ -1,318 +0,0 @@
-package app.revanced.extension.shared.spoof.requests;
-
-import static app.revanced.extension.shared.ByteTrieSearch.convertStringsToBytes;
-import static app.revanced.extension.shared.Utils.isNotEmpty;
-import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_PLAYER_STREAMING_DATA;
-import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_REEL_STREAMING_DATA;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.SocketTimeoutException;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import app.revanced.extension.shared.ByteTrieSearch;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.innertube.PlayerResponseOuterClass;
-import app.revanced.extension.shared.innertube.PlayerResponseOuterClass.PlayerResponse;
-import app.revanced.extension.shared.innertube.PlayerResponseOuterClass.StreamingData;
-import app.revanced.extension.shared.innertube.ReelItemWatchResponseOuterClass.ReelItemWatchResponse;
-import app.revanced.extension.shared.requests.Route;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.spoof.ClientType;
-
-/**
- * Video streaming data. Fetching is tied to the behavior YT uses,
- * where this class fetches the streams only when YT fetches.
- *
- * Effectively the cache expiration of these fetches is the same as the stock app,
- * since the stock app would not use expired streams and therefor
- * the extension replace stream hook is called only if YT
- * did use its own client streams.
- */
-public class StreamingDataRequest {
-
- private static volatile ClientType[] clientOrderToUse = ClientType.values();
-
- public static void setClientOrderToUse(List availableClients, ClientType preferredClient) {
- Objects.requireNonNull(preferredClient);
-
- int availableClientSize = availableClients.size();
- if (!availableClients.contains(preferredClient)) {
- availableClientSize++;
- }
-
- clientOrderToUse = new ClientType[availableClientSize];
- clientOrderToUse[0] = preferredClient;
-
- int i = 1;
- for (ClientType c : availableClients) {
- if (c != preferredClient) {
- clientOrderToUse[i++] = c;
- }
- }
-
- Logger.printDebug(() -> "Available spoof clients: " + Arrays.toString(clientOrderToUse));
- }
-
- private static final String AUTHORIZATION_HEADER = "Authorization";
-
- private static final String[] REQUEST_HEADER_KEYS = {
- AUTHORIZATION_HEADER, // Available only to logged-in users.
- "X-GOOG-API-FORMAT-VERSION",
- "X-Goog-Visitor-Id"
- };
-
- /**
- * TCP connection and HTTP read timeout.
- */
- private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000;
-
- /**
- * Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS}
- */
- private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000;
-
- /**
- * Cache limit must be greater than the maximum number of videos open at once,
- * which theoretically is more than 4 (3 Shorts + one regular minimized video).
- * But instead use a much larger value, to handle if a video viewed a while ago
- * is somehow still referenced. Each stream is a small array of Strings
- * so memory usage is not a concern.
- */
- private static final Map cache = Collections.synchronizedMap(
- Utils.createSizeRestrictedMap(50));
-
- /**
- * Strings found in the response if the video is a livestream.
- */
- private static final ByteTrieSearch liveStreamBufferSearch = new ByteTrieSearch(
- convertStringsToBytes(
- "yt_live_broadcast",
- "yt_premiere_broadcast"
- )
- );
-
- private static volatile ClientType lastSpoofedClientType;
-
- public static String getLastSpoofedClientName() {
- ClientType client = lastSpoofedClientType;
- return client == null ? "Unknown" : client.friendlyName;
- }
-
- private final String videoId;
-
- private final Future future;
-
- private StreamingDataRequest(String videoId, Map playerHeaders) {
- Objects.requireNonNull(playerHeaders);
- this.videoId = videoId;
- this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
- }
-
- public static void fetchRequest(String videoId, Map fetchHeaders) {
- // Always fetch, even if there is an existing request for the same video.
- cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
- }
-
- @Nullable
- public static StreamingDataRequest getRequestForVideoId(String videoId) {
- return cache.get(videoId);
- }
-
- private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) {
- if (showToast) Utils.showToastShort(toastMessage);
- Logger.printInfo(() -> toastMessage, ex);
- }
-
- private static void handleDebugToast(String toastMessage, ClientType clientType) {
- if (BaseSettings.DEBUG.get() && BaseSettings.DEBUG_TOAST_ON_ERROR.get()) {
- Utils.showToastShort(String.format(toastMessage, clientType));
- }
- }
-
- @Nullable
- private static HttpURLConnection send(ClientType clientType,
- String videoId,
- Map playerHeaders,
- boolean showErrorToasts) {
- Objects.requireNonNull(clientType);
- Objects.requireNonNull(videoId);
- Objects.requireNonNull(playerHeaders);
-
- final long startTime = System.currentTimeMillis();
-
- try {
- Route.CompiledRoute route = clientType.usePlayerEndpoint ?
- GET_PLAYER_STREAMING_DATA : GET_REEL_STREAMING_DATA;
-
- HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(route, clientType);
- connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
- connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
-
- boolean authHeadersIncludes = false;
-
- for (String key : REQUEST_HEADER_KEYS) {
- String value = playerHeaders.get(key);
-
- if (value != null) {
- if (key.equals(AUTHORIZATION_HEADER)) {
- if (!clientType.useAuth) {
- Logger.printDebug(() -> "Not including request header: " + key);
- continue;
- }
- authHeadersIncludes = true;
- }
-
- Logger.printDebug(() -> "Including request header: " + key);
- connection.setRequestProperty(key, value);
- }
- }
-
- if (!authHeadersIncludes && clientType.useAuth) {
- Logger.printDebug(() -> "Skipping client since user is not logged in: " + clientType
- + " videoId: " + videoId);
- return null;
- }
-
- Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType);
-
- String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId);
- byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
- connection.setFixedLengthStreamingMode(requestBody.length);
- connection.getOutputStream().write(requestBody);
-
- final int responseCode = connection.getResponseCode();
- if (responseCode == 200) return connection;
-
- // This situation likely means the patches are outdated.
- // Use a toast message that suggests updating.
- handleConnectionError("Playback error (App is outdated?) " + clientType + ": "
- + responseCode + " response: " + connection.getResponseMessage(),
- null, showErrorToasts);
- } catch (SocketTimeoutException ex) {
- handleConnectionError("Connection timeout", ex, showErrorToasts);
- } catch (IOException ex) {
- handleConnectionError("Network error", ex, showErrorToasts);
- } catch (Exception ex) {
- Logger.printException(() -> "send failed", ex);
- } finally {
- Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
- }
-
- return null;
- }
-
- private static byte[] fetch(String videoId, Map playerHeaders) {
- final boolean debugEnabled = BaseSettings.DEBUG.get();
-
- // Retry with different client if empty response body is received.
- int i = 0;
- for (ClientType clientType : clientOrderToUse) {
- // Show an error if the last client type fails, or if debug is enabled then show for all attempts.
- final boolean showErrorToast = (++i == clientOrderToUse.length) || debugEnabled;
-
- HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
- if (connection != null) {
- byte[] playerResponseBuffer = buildPlayerResponseBuffer(clientType, connection);
- if (playerResponseBuffer != null) {
- lastSpoofedClientType = clientType;
-
- return playerResponseBuffer;
- }
- }
- }
-
- lastSpoofedClientType = null;
- handleConnectionError("Could not fetch any client streams", null, true);
- return null;
- }
-
- @Nullable
- private static byte[] buildPlayerResponseBuffer(ClientType clientType,
- HttpURLConnection connection) {
- // gzip encoding doesn't response with content length (-1),
- // but empty response body does.
- if (connection.getContentLength() == 0) {
- handleDebugToast("Debug: Ignoring empty spoof stream client (%s)", clientType);
- return null;
- }
-
- try (InputStream inputStream = connection.getInputStream()) {
- PlayerResponse playerResponse = clientType.usePlayerEndpoint
- ? PlayerResponse.parseFrom(inputStream)
- : ReelItemWatchResponse.parseFrom(inputStream).getPlayerResponse();
-
- var playabilityStatus = playerResponse.getPlayabilityStatus();
- if (playabilityStatus.getStatus() != PlayerResponseOuterClass.Status.OK) {
- handleDebugToast("Debug: Ignoring unplayable video (%s)", clientType);
- String reason = playabilityStatus.getReason();
- if (isNotEmpty(reason)) {
- Logger.printDebug(() -> String.format("Debug: Ignoring unplayable video (%s), reason: %s", clientType, reason));
- }
-
- return null;
- }
-
- PlayerResponse.Builder responseBuilder = playerResponse.toBuilder();
- if (!playerResponse.hasStreamingData()) {
- handleDebugToast("Debug: Ignoring empty streaming data (%s)", clientType);
- return null;
- }
-
- // Android Studio only supports the HLS protocol for live streams.
- // HLS protocol can theoretically be played with ExoPlayer,
- // but the related code has not yet been implemented.
- // If DASH protocol is not available, the client will be skipped.
- StreamingData streamingData = playerResponse.getStreamingData();
- if (streamingData.getAdaptiveFormatsCount() == 0) {
- handleDebugToast("Debug: Ignoring empty adaptiveFormat (%s)", clientType);
- return null;
- }
-
- return responseBuilder.build().toByteArray();
- } catch (IOException ex) {
- Logger.printException(() -> "Failed to write player response to buffer array", ex);
- return null;
- }
- }
-
- public boolean fetchCompleted() {
- return future.isDone();
- }
-
- @Nullable
- public byte[] getStream() {
- try {
- return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
- } catch (TimeoutException ex) {
- Logger.printInfo(() -> "getStream timed out", ex);
- } catch (InterruptedException ex) {
- Logger.printException(() -> "getStream interrupted", ex);
- Thread.currentThread().interrupt(); // Restore interrupt status flag.
- } catch (ExecutionException ex) {
- Logger.printException(() -> "getStream failure", ex);
- }
-
- return null;
- }
-
- @NonNull
- @Override
- public String toString() {
- return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}';
- }
-}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/theme/BaseThemePatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/theme/BaseThemePatch.java
deleted file mode 100644
index 2d12b0c1f3..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/theme/BaseThemePatch.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package app.revanced.extension.shared.theme;
-
-import androidx.annotation.Nullable;
-
-import app.revanced.extension.shared.Utils;
-
-@SuppressWarnings("unused")
-public abstract class BaseThemePatch {
- // Background colors.
- protected static final int BLACK_COLOR = Utils.getResourceColor("yt_black1");
- protected static final int WHITE_COLOR = Utils.getResourceColor("yt_white1");
-
- /**
- * Check if a value matches any of the provided values.
- *
- * @param value The value to check.
- * @param of The array of values to compare against.
- * @return True if the value matches any of the provided values.
- */
- protected static boolean anyEquals(int value, int... of) {
- for (int v : of) {
- if (value == v) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Helper method to process color values for Litho components.
- *
- * @param originalValue The original color value.
- * @param darkValues Array of dark mode color values to match.
- * @param lightValues Array of light mode color values to match.
- * @return The new or original color value.
- */
- protected static int processColorValue(int originalValue, int[] darkValues, @Nullable int[] lightValues) {
- if (Utils.isDarkModeEnabled()) {
- if (anyEquals(originalValue, darkValues)) {
- return BLACK_COLOR;
- }
- } else if (lightValues != null && anyEquals(originalValue, lightValues)) {
- return WHITE_COLOR;
- }
-
- return originalValue;
- }
-}
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
deleted file mode 100644
index 07e9b41135..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/ColorDot.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package app.revanced.extension.shared.ui;
-
-import static app.revanced.extension.shared.Utils.adjustColorBrightness;
-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;
-
-import android.graphics.Color;
-import android.graphics.drawable.GradientDrawable;
-import android.view.View;
-
-import androidx.annotation.ColorInt;
-
-public class ColorDot {
- private static final int STROKE_WIDTH = Dim.dp(1.5f);
-
- /**
- * Creates a circular drawable with a main fill and a stroke.
- * Stroke adapts to dark/light theme and transparency, applied only when color is transparent or matches app background.
- */
- public static GradientDrawable createColorDotDrawable(@ColorInt int color) {
- final boolean isDarkTheme = isDarkModeEnabled();
- final boolean isTransparent = Color.alpha(color) == 0;
- final int opaqueColor = color | 0xFF000000;
- final int appBackground = getAppBackgroundColor();
- final int strokeColor;
- final int strokeWidth;
-
- // Determine stroke color.
- if (isTransparent || (opaqueColor == appBackground)) {
- final int baseColor = isTransparent ? appBackground : opaqueColor;
- strokeColor = adjustColorBrightness(baseColor, isDarkTheme ? 1.2f : 0.8f);
- strokeWidth = STROKE_WIDTH;
- } else {
- strokeColor = 0;
- strokeWidth = 0;
- }
-
- // Create circular drawable with conditional stroke.
- GradientDrawable circle = new GradientDrawable();
- circle.setShape(GradientDrawable.OVAL);
- circle.setColor(color);
- circle.setStroke(strokeWidth, strokeColor);
-
- return circle;
- }
-
- /**
- * Applies the color dot drawable to the target view.
- */
- public static void applyColorDot(View targetView, @ColorInt int color, boolean enabled) {
- if (targetView == null) return;
- targetView.setBackground(createColorDotDrawable(color));
- targetView.setAlpha(enabled ? 1.0f : DISABLED_ALPHA);
- if (!isDarkModeEnabled()) {
- targetView.setClipToOutline(true);
- 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
deleted file mode 100644
index 15d80c916e..0000000000
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/CustomDialog.java
+++ /dev/null
@@ -1,461 +0,0 @@
-package app.revanced.extension.shared.ui;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.Typeface;
-import android.graphics.drawable.ShapeDrawable;
-import android.graphics.drawable.shapes.RoundRectShape;
-import android.text.Spanned;
-import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
-import android.util.Pair;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.ScrollView;
-import android.widget.TextView;
-import androidx.annotation.Nullable;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A utility class for creating a customizable dialog with a title, message or EditText, and up to three buttons (OK, Cancel, Neutral).
- * The dialog supports themed colors, rounded corners, and dynamic button layout based on screen width. It is dismissible by default.
- */
-public class CustomDialog {
- private final Context context;
- private final Dialog dialog;
- private final LinearLayout mainLayout;
-
- /**
- * Creates a custom dialog with a styled layout, including a title, message, buttons, and an optional EditText.
- * The dialog's appearance adapts to the app's dark mode setting, with rounded corners and customizable button actions.
- * Buttons adjust dynamically to their text content and are arranged in a single row if they fit within 80% of the
- * screen width, with the Neutral button aligned to the left and OK/Cancel buttons centered on the right.
- * If buttons do not fit, each is placed on a separate row, all aligned to the right.
- *
- * @param context Context used to create the dialog.
- * @param title Title text of the dialog.
- * @param message Message text of the dialog (supports Spanned for HTML), or null if replaced by EditText.
- * @param editText EditText to include in the dialog, or null if no EditText is needed.
- * @param okButtonText OK button text, or null to use the default "OK" string.
- * @param onOkClick Action to perform when the OK button is clicked.
- * @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed.
- * @param neutralButtonText Neutral button text, or null if no Neutral button is needed.
- * @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed.
- * @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked.
- * @return The Dialog and its main LinearLayout container.
- */
- public static Pair create(Context context, CharSequence title, CharSequence message,
- @Nullable EditText editText, CharSequence okButtonText,
- Runnable onOkClick, Runnable onCancelClick,
- @Nullable CharSequence neutralButtonText,
- @Nullable Runnable onNeutralClick,
- boolean dismissDialogOnNeutralClick) {
- Logger.printDebug(() -> "Creating custom dialog with title: " + title);
- CustomDialog customDialog = new CustomDialog(context, title, message, editText,
- okButtonText, onOkClick, onCancelClick,
- neutralButtonText, onNeutralClick, dismissDialogOnNeutralClick);
- return new Pair<>(customDialog.dialog, customDialog.mainLayout);
- }
-
- /**
- * Initializes a custom dialog with the specified parameters.
- *
- * @param context Context used to create the dialog.
- * @param title Title text of the dialog.
- * @param message Message text of the dialog, 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.
- */
- private CustomDialog(Context context, CharSequence title, CharSequence message, @Nullable EditText editText,
- CharSequence okButtonText, Runnable onOkClick, Runnable onCancelClick,
- @Nullable CharSequence neutralButtonText, @Nullable Runnable onNeutralClick,
- boolean dismissDialogOnNeutralClick) {
- this.context = context;
- this.dialog = new Dialog(context);
- this.dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
-
- // Create main layout.
- mainLayout = createMainLayout();
- addTitle(title);
- addContent(message, editText);
- addButtons(okButtonText, onOkClick, onCancelClick, neutralButtonText, onNeutralClick, dismissDialogOnNeutralClick);
-
- // Set dialog content and window attributes.
- dialog.setContentView(mainLayout);
- Window window = dialog.getWindow();
- if (window != null) {
- Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 90, false);
- }
- }
-
- /**
- * Creates the main layout for the dialog with vertical orientation and rounded corners.
- *
- * @return The configured LinearLayout for the dialog.
- */
- private LinearLayout createMainLayout() {
- LinearLayout layout = new LinearLayout(context);
- layout.setOrientation(LinearLayout.VERTICAL);
- layout.setPadding(Dim.dp24, Dim.dp16, Dim.dp24, Dim.dp24);
-
- // Set rounded rectangle background.
- ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
- Dim.roundedCorners(28), null, null));
- // Dialog background.
- background.getPaint().setColor(Utils.getDialogBackgroundColor());
- layout.setBackground(background);
-
- return layout;
- }
-
- /**
- * Adds a title to the dialog if provided.
- *
- * @param title The title text to display.
- */
- private void addTitle(CharSequence title) {
- if (TextUtils.isEmpty(title)) return;
-
- TextView titleView = new TextView(context);
- titleView.setText(title);
- titleView.setTypeface(Typeface.DEFAULT_BOLD);
- titleView.setTextSize(18);
- titleView.setTextColor(Utils.getAppForegroundColor());
- titleView.setGravity(Gravity.CENTER);
-
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- params.setMargins(0, 0, 0, Dim.dp16);
- titleView.setLayoutParams(params);
-
- mainLayout.addView(titleView);
- }
-
- /**
- * Adds a message or EditText to the dialog within a ScrollView.
- *
- * @param message The message text to display (supports Spanned for HTML), or null if replaced by EditText.
- * @param editText The EditText to include, or null if no EditText is needed.
- */
- private void addContent(CharSequence message, @Nullable EditText editText) {
- // Create content container (message/EditText) inside a ScrollView only if message or editText is provided.
- if (message == null && editText == null) return;
-
- ScrollView scrollView = new ScrollView(context);
- // Disable the vertical scrollbar.
- scrollView.setVerticalScrollBarEnabled(false);
- scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
-
- LinearLayout contentContainer = new LinearLayout(context);
- contentContainer.setOrientation(LinearLayout.VERTICAL);
- scrollView.addView(contentContainer);
-
- // EditText (if provided).
- if (editText != null) {
- ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
- Dim.roundedCorners(10), null, null));
- background.getPaint().setColor(Utils.getEditTextBackground());
- scrollView.setPadding(Dim.dp8, Dim.dp8, Dim.dp8, Dim.dp8);
- scrollView.setBackground(background);
- scrollView.setClipToOutline(true);
-
- // 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(Utils.getAppForegroundColor());
- editText.setBackgroundColor(Color.TRANSPARENT);
- editText.setPadding(0, 0, 0, 0);
- contentContainer.addView(editText, new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT));
- // Message (if not replaced by EditText).
- } else {
- TextView messageView = new TextView(context);
- // Supports Spanned (HTML).
- messageView.setText(message);
- messageView.setTextSize(16);
- messageView.setTextColor(Utils.getAppForegroundColor());
- // Enable HTML link clicking if the message contains links.
- if (message instanceof Spanned) {
- messageView.setMovementMethod(LinkMovementMethod.getInstance());
- }
- contentContainer.addView(messageView, new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT));
- }
-
- // Weight to take available space.
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- 0,
- 1.0f);
- scrollView.setLayoutParams(params);
- // Add ScrollView to main layout only if content exist.
- mainLayout.addView(scrollView);
- }
-
- /**
- * Adds buttons to the dialog, arranging them dynamically based on their widths.
- *
- * @param okButtonText OK button text, or null to use the default "OK" string.
- * @param onOkClick Action for the OK button click.
- * @param onCancelClick Action for the Cancel button click, or null if no Cancel button.
- * @param neutralButtonText Neutral button text, or null if no Neutral button.
- * @param onNeutralClick Action for the Neutral button click, or null if no Neutral button.
- * @param dismissDialogOnNeutralClick If the dialog should dismiss on Neutral button click.
- */
- private void addButtons(CharSequence okButtonText, Runnable onOkClick, Runnable onCancelClick,
- @Nullable CharSequence neutralButtonText, @Nullable Runnable onNeutralClick,
- boolean dismissDialogOnNeutralClick) {
- // Button container.
- LinearLayout buttonContainer = new LinearLayout(context);
- buttonContainer.setOrientation(LinearLayout.VERTICAL);
- LinearLayout.LayoutParams buttonContainerParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT);
- buttonContainerParams.setMargins(0, Dim.dp16, 0, 0);
- buttonContainer.setLayoutParams(buttonContainerParams);
-
- List buttons = new ArrayList<>();
- List buttonWidths = new ArrayList<>();
-
- // Create buttons in order: Neutral, Cancel, OK.
- if (neutralButtonText != null && onNeutralClick != null) {
- Button neutralButton = createButton(neutralButtonText, onNeutralClick, false, dismissDialogOnNeutralClick);
- buttons.add(neutralButton);
- buttonWidths.add(measureButtonWidth(neutralButton));
- }
- if (onCancelClick != null) {
- Button cancelButton = createButton(context.getString(android.R.string.cancel), onCancelClick, false, true);
- buttons.add(cancelButton);
- buttonWidths.add(measureButtonWidth(cancelButton));
- }
- if (onOkClick != null) {
- Button okButton = createButton(
- okButtonText != null ? okButtonText : context.getString(android.R.string.ok),
- onOkClick, true, true);
- buttons.add(okButton);
- buttonWidths.add(measureButtonWidth(okButton));
- }
-
- // Handle button layout.
- layoutButtons(buttonContainer, buttons, buttonWidths);
- mainLayout.addView(buttonContainer);
- }
-
- /**
- * Creates a styled button with customizable text, click behavior, and appearance.
- *
- * @param text The button text to display.
- * @param onClick The action to perform on button click.
- * @param isOkButton If this is the OK button, which uses distinct styling.
- * @param dismissDialog If the dialog should dismiss when the button is clicked.
- * @return The created Button.
- */
- private Button createButton(CharSequence text, Runnable onClick, boolean isOkButton, boolean dismissDialog) {
- Button button = new Button(context, null, 0);
- button.setText(text);
- button.setTextSize(14);
- button.setAllCaps(false);
- button.setSingleLine(true);
- button.setEllipsize(TextUtils.TruncateAt.END);
- button.setGravity(Gravity.CENTER);
- // Set internal padding.
- button.setPadding(Dim.dp16, 0, Dim.dp16, 0);
-
- // Background color for OK button (inversion).
- // Background color for Cancel or Neutral buttons.
- ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
- Dim.roundedCorners(20), null, null));
- background.getPaint().setColor(isOkButton
- ? Utils.getOkButtonBackgroundColor()
- : Utils.getCancelOrNeutralButtonBackgroundColor());
- button.setBackground(background);
-
- button.setTextColor(Utils.isDarkModeEnabled()
- ? (isOkButton ? Color.BLACK : Color.WHITE)
- : (isOkButton ? Color.WHITE : Color.BLACK));
-
- button.setOnClickListener(v -> {
- if (onClick != null) onClick.run();
- if (dismissDialog) dialog.dismiss();
- });
-
- return button;
- }
-
- /**
- * Measures the width of a button.
- */
- private int measureButtonWidth(Button button) {
- button.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
- return button.getMeasuredWidth();
- }
-
- /**
- * Arranges buttons in the dialog, either in a single row or multiple rows based on their total width.
- *
- * @param buttonContainer The container for the buttons.
- * @param buttons The list of buttons to arrange.
- * @param buttonWidths The measured widths of the buttons.
- */
- private void layoutButtons(LinearLayout buttonContainer, List buttons, List buttonWidths) {
- if (buttons.isEmpty()) return;
-
- // Check if buttons fit in one row.
- int totalWidth = 0;
- for (Integer width : buttonWidths) {
- totalWidth += width;
- }
- if (buttonWidths.size() > 1) {
- // Add margins for gaps.
- totalWidth += (buttonWidths.size() - 1) * Dim.dp8;
- }
-
- // Single button: stretch to full width.
- if (buttons.size() == 1) {
- layoutSingleButton(buttonContainer, buttons.get(0));
- } else if (totalWidth <= Dim.pctWidth(80)) {
- // Single row: Neutral, Cancel, OK.
- layoutButtonsInRow(buttonContainer, buttons, buttonWidths);
- } else {
- // Multiple rows: OK, Cancel, Neutral.
- layoutButtonsInColumns(buttonContainer, buttons);
- }
- }
-
- /**
- * Arranges a single button, stretching it to full width.
- *
- * @param buttonContainer The container for the button.
- * @param button The button to arrange.
- */
- private void layoutSingleButton(LinearLayout buttonContainer, Button button) {
- LinearLayout singleContainer = new LinearLayout(context);
- singleContainer.setOrientation(LinearLayout.HORIZONTAL);
- singleContainer.setGravity(Gravity.CENTER);
-
- ViewGroup parent = (ViewGroup) button.getParent();
- if (parent != null) parent.removeView(button);
-
- button.setLayoutParams(new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- Dim.dp36));
- singleContainer.addView(button);
- buttonContainer.addView(singleContainer);
- }
-
- /**
- * Arranges buttons in a single horizontal row with proportional widths.
- *
- * @param buttonContainer The container for the buttons.
- * @param buttons The list of buttons to arrange.
- * @param buttonWidths The measured widths of the buttons.
- */
- private void layoutButtonsInRow(LinearLayout buttonContainer, List buttons, List buttonWidths) {
- LinearLayout rowContainer = new LinearLayout(context);
- rowContainer.setOrientation(LinearLayout.HORIZONTAL);
- rowContainer.setGravity(Gravity.CENTER);
- rowContainer.setLayoutParams(new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT));
-
- // Add all buttons with proportional weights and specific margins.
- for (int i = 0; i < buttons.size(); i++) {
- Button button = getButton(buttons, buttonWidths, i);
- rowContainer.addView(button);
- }
-
- buttonContainer.addView(rowContainer);
- }
-
- @NotNull
- private Button getButton(List