diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d9759449b..f0e4bcfb27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,176 @@ +# [1.8.0-dev.20](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.19...v1.8.0-dev.20) (2024-04-21) + + +### Features + +* **YouTube - Swipe controls:** Save and restore brightness and add auto-brightness toggle ([#610](https://github.com/ReVanced/revanced-integrations/issues/610)) ([1c8e2b2](https://github.com/ReVanced/revanced-integrations/commit/1c8e2b29410048a352cb6aad3dd02773459f91a0)) + +# [1.8.0-dev.19](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.18...v1.8.0-dev.19) (2024-04-18) + + +### Bug Fixes + +* **YouTube - Hide keyword content:** Correctly hide content in the subscription tab ([c3bfa77](https://github.com/ReVanced/revanced-integrations/commit/c3bfa77d62b15dedfed8f697583f2f0805f0c2c1)) + +# [1.8.0-dev.18](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.17...v1.8.0-dev.18) (2024-04-18) + + +### Bug Fixes + +* **YouTube - Spoof device dimensions:** Warn about potential performance issues ([#617](https://github.com/ReVanced/revanced-integrations/issues/617)) ([786ac9d](https://github.com/ReVanced/revanced-integrations/commit/786ac9d2b71886964454fcb748e656d1beed1964)) + +# [1.8.0-dev.17](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.16...v1.8.0-dev.17) (2024-04-18) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Hide paid promotion label ([3ce100c](https://github.com/ReVanced/revanced-integrations/commit/3ce100ced57d7099c2209d9a955484f1e7d418e0)) + +# [1.8.0-dev.16](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.15...v1.8.0-dev.16) (2024-04-18) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Hide horizontal tile shelves ([ba30869](https://github.com/ReVanced/revanced-integrations/commit/ba308690cf83067d3ddd54622eebcbd14bc15ac8)) + +# [1.8.0-dev.15](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.14...v1.8.0-dev.15) (2024-04-17) + + +### Features + +* **YouTube:** Add 'About' preference to settings menu ([#608](https://github.com/ReVanced/revanced-integrations/issues/608)) ([b8f260e](https://github.com/ReVanced/revanced-integrations/commit/b8f260ebd3e7c2dc50a57cd060b76f2e0fc4a89c)) + +# [1.8.0-dev.14](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.13...v1.8.0-dev.14) (2024-04-17) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Hide subscribe button in channel bar ([9938bbf](https://github.com/ReVanced/revanced-integrations/commit/9938bbf0de9592db015ae0cfea83e855e12f0c7e)) + +# [1.8.0-dev.13](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.12...v1.8.0-dev.13) (2024-04-16) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Hide suggested actions in incognito mode ([bba421d](https://github.com/ReVanced/revanced-integrations/commit/bba421ddb63597bf918ecccacfd4a33493016b9f)) + +# [1.8.0-dev.12](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.11...v1.8.0-dev.12) (2024-04-15) + + +### Bug Fixes + +* **YouTube - Hide ads:** rename `Hide paid content` to `Hide paid promotion label` ([#616](https://github.com/ReVanced/revanced-integrations/issues/616)) ([13dc172](https://github.com/ReVanced/revanced-integrations/commit/13dc17288d13d024a3fbe318ee0fb23a0d46af85)) + +# [1.8.0-dev.11](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.10...v1.8.0-dev.11) (2024-04-14) + + +### Bug Fixes + +* **YouTube - Return YouTube Dislike:** Do not show error toast if API success response contains new lines ([#612](https://github.com/ReVanced/revanced-integrations/issues/612)) ([9108205](https://github.com/ReVanced/revanced-integrations/commit/9108205445c533550db454731d4f9460a3241a03)) + +# [1.8.0-dev.10](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.9...v1.8.0-dev.10) (2024-04-14) + + +### Features + +* **YouTube - Hide Shorts components:** Hide tagged products, hide search suggestions ([#615](https://github.com/ReVanced/revanced-integrations/issues/615)) ([0586fb7](https://github.com/ReVanced/revanced-integrations/commit/0586fb70e347c25742e03102441cfb37315b5937)) + +# [1.8.0-dev.9](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.8...v1.8.0-dev.9) (2024-04-14) + + +### Bug Fixes + +* Use correct hide playables setting key ([a2b1543](https://github.com/ReVanced/revanced-integrations/commit/a2b15433cffec082394a50d14f7eef625a6351c1)) + +# [1.8.0-dev.8](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.7...v1.8.0-dev.8) (2024-04-14) + + +### Features + +* **YouTube - Hide layout components:** Hide playables ([d6cd550](https://github.com/ReVanced/revanced-integrations/commit/d6cd550880596de5cd2eb4a0d1325a73326d4af9)) + +# [1.8.0-dev.7](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.6...v1.8.0-dev.7) (2024-04-12) + + +### Features + +* **YouTube - Hide Shorts components:** Hide `Shop`, `Location` and `Save sound to playlist` buttons ([#614](https://github.com/ReVanced/revanced-integrations/issues/614)) ([acfa3c9](https://github.com/ReVanced/revanced-integrations/commit/acfa3c98868b6d84572ee682ad806a0282ac6dad)) + +# [1.8.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.5...v1.8.0-dev.6) (2024-04-12) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Do not show Shorts suggestions in video player, if all hide Shorts options are enabled ([#613](https://github.com/ReVanced/revanced-integrations/issues/613)) ([c132670](https://github.com/ReVanced/revanced-integrations/commit/c132670400e6bdf17c46b8d04d579fb49c3d2749)) + +# [1.8.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.4...v1.8.0-dev.5) (2024-04-10) + + +### Bug Fixes + +* **YouTube - Return YouTube Dislike:** Do not clip compact text when not using English ([eeaeb49](https://github.com/ReVanced/revanced-integrations/commit/eeaeb49f2a562d2690dae184153c303a5b1c4368)) + +# [1.8.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.3...v1.8.0-dev.4) (2024-04-10) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Correctly hide Shorts if navigation tab is changed using device back button ([#611](https://github.com/ReVanced/revanced-integrations/issues/611)) ([ffc3437](https://github.com/ReVanced/revanced-integrations/commit/ffc3437843c24af255d2a0dda9930d2843cac4b6)) + +# [1.8.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.2...v1.8.0-dev.3) (2024-04-09) + + +### Bug Fixes + +* **YouTube - Settings:** Do not show a toast if migrating old unknown settings ([f2e15a2](https://github.com/ReVanced/revanced-integrations/commit/f2e15a2e1ff59ae7780cfbd366e5165f4e2b191d)) + +# [1.8.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.1...v1.8.0-dev.2) (2024-04-07) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Do not hide playlist shelf in library ([c5d38a7](https://github.com/ReVanced/revanced-integrations/commit/c5d38a7e0791ebb8fe59397fff959cc94e0a7aed)) + +# [1.8.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.7.1-dev.4...v1.8.0-dev.1) (2024-04-06) + + +### Bug Fixes + +* **YouTube - GmsCore support:** Prompt to disable battery optimizations, if not done already ([#601](https://github.com/ReVanced/revanced-integrations/issues/601)) ([c5c9de5](https://github.com/ReVanced/revanced-integrations/commit/c5c9de500d8f1268799e55c31c446bfe8336f79a)) + + +### Features + +* **YouTube - Hide layout components:** Add option to hide horizontal shelves ([#598](https://github.com/ReVanced/revanced-integrations/issues/598)) ([fedace0](https://github.com/ReVanced/revanced-integrations/commit/fedace02fd5c443ef37dcf77253438b041f4c3f9)) + +## [1.7.1-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.7.1-dev.3...v1.7.1-dev.4) (2024-04-04) + + +### Bug Fixes + +* **YouTube - Hide load more button:** Include patch with `Hide layout components`, and hide button only in search feed ([#600](https://github.com/ReVanced/revanced-integrations/issues/600)) ([c420891](https://github.com/ReVanced/revanced-integrations/commit/c420891e3ef134f30af79cf2f30da3fa2fe5a455)) + +## [1.7.1-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.7.1-dev.2...v1.7.1-dev.3) (2024-04-04) + + +### Bug Fixes + +* **YouTube - Player flyout menu:** Add hide Lock screen menu ([#609](https://github.com/ReVanced/revanced-integrations/issues/609)) ([b2fe105](https://github.com/ReVanced/revanced-integrations/commit/b2fe105199d4a5958676cbc8f9c701541e8ff24a)) + +## [1.7.1-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.7.1-dev.1...v1.7.1-dev.2) (2024-04-03) + + +### Bug Fixes + +* **YouTube - Navigation bar hook:** Handle if search is active but hidden behind a maximized player ([cbccb46](https://github.com/ReVanced/revanced-integrations/commit/cbccb46e639003adbed941f9b88c41b4c9998729)) + +## [1.7.1-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.7.0...v1.7.1-dev.1) (2024-04-01) + + +### Bug Fixes + +* **YouTube - Disable suggested video end screen:** Require app restart ([38ae5aa](https://github.com/ReVanced/revanced-integrations/commit/38ae5aac845745824218a08053db519a3325d7a9)) + # [1.7.0](https://github.com/ReVanced/revanced-integrations/compare/v1.6.0...v1.7.0) (2024-03-30) diff --git a/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java b/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java index bab1a99402..16f0ed0c2a 100644 --- a/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java +++ b/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java @@ -1,21 +1,25 @@ package app.revanced.integrations.shared; +import static app.revanced.integrations.shared.StringRef.str; + +import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; 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 androidx.annotation.RequiresApi; import java.net.MalformedURLException; import java.net.URL; -import static app.revanced.integrations.shared.StringRef.str; - /** * @noinspection unused */ @@ -45,26 +49,19 @@ private static void open(String queryOrLink) { System.exit(0); } - private static void showToastOrDialog(Context context, String toastMessageKey, String dialogMessageKey, String link) { - if (!(context instanceof Activity)) { - // Context is for the application and cannot show a dialog using it. - Utils.showToastLong(str(toastMessageKey)); - open(link); - return; - } - + private static void showBatteryOptimizationDialog(Activity context, + String dialogMessageRef, + String positiveButtonStringRef, + 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(() -> { new AlertDialog.Builder(context) .setIconAttribute(android.R.attr.alertDialogIcon) .setTitle(str("gms_core_dialog_title")) - .setMessage(str(dialogMessageKey)) - .setPositiveButton(str("gms_core_dialog_ok_button_text"), (dialog, id) -> { - open(link); - }) - // Manually allow using the back button to dismiss the dialog with the back button, - // if troubleshooting and somehow the GmsCore verification checks always fail. + .setMessage(str(dialogMessageRef)) + .setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener) + // Allow using back button to skip the action, just in case the check can never be satisfied. .setCancelable(true) .show(); }, 100); @@ -74,47 +71,62 @@ private static void showToastOrDialog(Context context, String toastMessageKey, S * Injection point. */ @RequiresApi(api = Build.VERSION_CODES.N) - public static void checkGmsCore(Context context) { + public static void checkGmsCore(Activity context) { try { // Verify GmsCore is installed. try { PackageManager manager = context.getPackageManager(); manager.getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES); } catch (PackageManager.NameNotFoundException exception) { - Logger.printDebug(() -> "GmsCore was not found"); + Logger.printInfo(() -> "GmsCore was not found"); // Cannot show a dialog and must show a toast, - // because on some installations the app crashes before the dialog can display. + // because on some installations the app crashes before a dialog can be displayed. Utils.showToastLong(str("gms_core_toast_not_installed_message")); open(getGmsCoreDownload()); return; } - // Check if GmsCore is whitelisted from battery optimizations. - var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - if (!powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME)) { - Logger.printDebug(() -> "GmsCore is not whitelisted from battery optimizations"); - showToastOrDialog(context, - "gms_core_toast_not_whitelisted_message", - "gms_core_dialog_not_whitelisted_using_battery_optimizations_message", - DONT_KILL_MY_APP_LINK); - return; - } - // Check if GmsCore is running in the background. try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) { if (client == null) { - Logger.printDebug(() -> "GmsCore is not running in the background"); - showToastOrDialog(context, - "gms_core_toast_not_whitelisted_message", + Logger.printInfo(() -> "GmsCore is not running in the background"); + + showBatteryOptimizationDialog(context, "gms_core_dialog_not_whitelisted_not_allowed_in_background_message", - DONT_KILL_MY_APP_LINK); + "gms_core_dialog_open_website_text", + (dialog, id) -> open(DONT_KILL_MY_APP_LINK)); + return; } } + + // Check if GmsCore is whitelisted from battery optimizations. + if (batteryOptimizationsEnabled(context)) { + Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations"); + showBatteryOptimizationDialog(context, + "gms_core_dialog_not_whitelisted_using_battery_optimizations_message", + "gms_core_dialog_continue_text", + (dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context)); + } } catch (Exception ex) { Logger.printException(() -> "checkGmsCore failure", ex); } } + @SuppressLint("BatteryLife") // Permission is part of GmsCore + private static void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) { + Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.fromParts("package", GMS_CORE_PACKAGE_NAME, null)); + activity.startActivityForResult(intent, 0); + } + + /** + * @return If GmsCore is not whitelisted from battery optimizations. + */ + private static boolean batteryOptimizationsEnabled(Context context) { + var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME); + } + private static String getGmsCoreDownload() { final var vendorGroupId = getGmsCoreVendorGroupId(); //noinspection SwitchStatementWithTooFewBranches diff --git a/app/src/main/java/app/revanced/integrations/shared/Utils.java b/app/src/main/java/app/revanced/integrations/shared/Utils.java index 371100d1b7..bdad852fe7 100644 --- a/app/src/main/java/app/revanced/integrations/shared/Utils.java +++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java @@ -35,6 +35,7 @@ import java.util.concurrent.TimeUnit; import app.revanced.integrations.shared.settings.BooleanSetting; +import app.revanced.integrations.shared.settings.preference.ReVancedAboutPreference; import kotlin.text.Regex; public class Utils { @@ -47,30 +48,44 @@ public class Utils { private Utils() { } // utility class - public static String getVersionName() { - if (versionName != null) return versionName; - - PackageInfo packageInfo; - try { - final var packageName = Objects.requireNonNull(getContext()).getPackageName(); - - PackageManager packageManager = context.getPackageManager(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - packageInfo = packageManager.getPackageInfo( - packageName, - PackageManager.PackageInfoFlags.of(0) - ); - else - packageInfo = packageManager.getPackageInfo( - packageName, - 0 - ); - } catch (PackageManager.NameNotFoundException e) { - Logger.printException(() -> "Failed to get package info", e); - return null; + /** + * Injection point. + * + * @return The manifest 'Version' entry of the patches.jar used during patching. + */ + public static String getPatchesReleaseVersion() { + return ""; // Value is replaced during patching. + } + + /** + * @return The version name of the app, such as "YouTube". + */ + public static String getAppVersionName() { + if (versionName == null) { + try { + final var packageName = Objects.requireNonNull(getContext()).getPackageName(); + + PackageManager packageManager = context.getPackageManager(); + PackageInfo packageInfo; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageInfo = packageManager.getPackageInfo( + packageName, + PackageManager.PackageInfoFlags.of(0) + ); + } else { + packageInfo = packageManager.getPackageInfo( + packageName, + 0 + ); + } + versionName = packageInfo.versionName; + } catch (Exception ex) { + Logger.printException(() -> "Failed to get package info", ex); + versionName = "Unknown"; + } } - return versionName = packageInfo.versionName; + return versionName; } /** @@ -185,6 +200,7 @@ public static Animation getResourceAnimation(@NonNull String resourceIdentifierN } public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + //noinspection deprecation return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color")); } @@ -323,7 +339,7 @@ public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delay try { runnable.run(); } catch (Exception ex) { - Logger.printException(() -> runnable.getClass() + ": " + ex.getMessage(), ex); + Logger.printException(() -> runnable.getClass().getSimpleName() + ": " + ex.getMessage(), ex); } }; new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis); @@ -445,11 +461,8 @@ private enum Sort { this.keySuffix = keySuffix; } - /** - * Defaults to {@link #UNSORTED} if key is null or has no sort suffix. - */ @NonNull - static Sort fromKey(@Nullable String key) { + static Sort fromKey(@Nullable String key, @NonNull Sort defaultSort) { if (key != null) { for (Sort sort : values()) { if (key.endsWith(sort.keySuffix)) { @@ -457,7 +470,7 @@ static Sort fromKey(@Nullable String key) { } } } - return UNSORTED; + return defaultSort; } } @@ -479,19 +492,26 @@ public static String removePunctuationConvertToLowercase(@Nullable CharSequence * If a preference has no key or no {@link Sort} suffix, * then the preferences are left unsorted. */ + @SuppressWarnings("deprecation") public static void sortPreferenceGroups(@NonNull PreferenceGroup group) { - Sort sort = Sort.fromKey(group.getKey()); + Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED); SortedMap preferences = new TreeMap<>(); for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { Preference preference = group.getPreference(i); + final Sort preferenceSort; if (preference instanceof PreferenceGroup) { sortPreferenceGroups((PreferenceGroup) preference); + 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 (sort) { + switch (preferenceSort) { case BY_TITLE: sortValue = removePunctuationConvertToLowercase(preference.getTitle()); break; @@ -511,8 +531,9 @@ public static void sortPreferenceGroups(@NonNull PreferenceGroup group) { for (Preference pref : preferences.values()) { int order = index++; - // If the preference is a PreferenceScreen or is an intent preference, move to the top. - if (pref instanceof PreferenceScreen || pref.getIntent() != null) { + // Move any screens, intents, and the one off About preference to the top. + if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference + || pref.getIntent() != null) { // Arbitrary high number. order -= 1000; } diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/EnumSetting.java b/app/src/main/java/app/revanced/integrations/shared/settings/EnumSetting.java index 20ef4821e0..a6301def24 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/EnumSetting.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/EnumSetting.java @@ -17,7 +17,7 @@ * All saved JSON text is converted to lowercase to keep the output less obnoxious. */ @SuppressWarnings("unused") -public class EnumSetting extends Setting { +public class EnumSetting> extends Setting { public EnumSetting(String key, T defaultValue) { super(key, defaultValue); } @@ -72,7 +72,7 @@ protected void writeToJSON(JSONObject json, String importExportKey) throws JSONE @NonNull private T getEnumFromString(String enumName) { //noinspection ConstantConditions - for (Enum value : defaultValue.getClass().getEnumConstants()) { + for (Enum value : defaultValue.getClass().getEnumConstants()) { if (value.name().equalsIgnoreCase(enumName)) { // noinspection unchecked return (T) value; diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/Setting.java b/app/src/main/java/app/revanced/integrations/shared/settings/Setting.java index f5f7b890e9..499e4d0bf2 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/Setting.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/Setting.java @@ -224,6 +224,7 @@ public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPref if (!oldPrefs.preferences.contains(settingKey)) { return; // Nothing to do. } + Object newValue = setting.get(); final Object migratedValue; if (setting instanceof BooleanSetting) { @@ -238,13 +239,17 @@ public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPref migratedValue = oldPrefs.getString(settingKey, (String) newValue); } else { Logger.printException(() -> "Unknown setting: " + setting); + // Remove otherwise it'll show a toast on every launch + oldPrefs.preferences.edit().remove(settingKey).apply(); return; } + oldPrefs.preferences.edit().remove(settingKey).apply(); // Remove the old setting. if (migratedValue.equals(newValue)) { Logger.printDebug(() -> "Value does not need migrating: " + settingKey); return; // Old value is already equal to the new setting value. } + Logger.printDebug(() -> "Migrating old preference value into current preference: " + settingKey); //noinspection unchecked setting.save(migratedValue); diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java index 34a348a371..b7fa68ac69 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java @@ -16,10 +16,7 @@ import static app.revanced.integrations.shared.StringRef.str; -/** - * - * - * @noinspection deprecation, DataFlowIssue , unused */ +@SuppressWarnings({"unused", "deprecation"}) public abstract class AbstractPreferenceFragment extends PreferenceFragment { /** * Indicates that if a preference changes, diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java index 0a9b54c1dd..5c8e7c9ba9 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java @@ -15,7 +15,7 @@ import static app.revanced.integrations.shared.StringRef.str; -/** @noinspection deprecation, unused */ +@SuppressWarnings({"unused", "deprecation"}) public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener { private String existingSettings; diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ReVancedAboutPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ReVancedAboutPreference.java new file mode 100644 index 0000000000..f5911a0179 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ReVancedAboutPreference.java @@ -0,0 +1,312 @@ +package app.revanced.integrations.shared.settings.preference; + +import static app.revanced.integrations.shared.StringRef.str; +import static app.revanced.integrations.youtube.requests.Route.Method.GET; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.Window; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +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.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.youtube.requests.Requester; +import app.revanced.integrations.youtube.requests.Route; + +/** + * Opens a dialog showing the links from {@link SocialLinksRoutes}. + */ +@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. + } + + private static String getColorHexString(int color) { + return String.format("#%06X", (0x00FFFFFF & color)); + } + + protected boolean isDarkModeEnabled() { + Configuration config = getContext().getResources().getConfiguration(); + final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; + return currentNightMode == Configuration.UI_MODE_NIGHT_YES; + } + + /** + * Subclasses can override this and provide a themed color. + */ + protected int getLightColor() { + return Color.WHITE; + } + + /** + * Subclasses can override this and provide a themed color. + */ + protected int getDarkColor() { + return Color.BLACK; + } + + private String createDialogHtml(ReVancedSocialLink[] socialLinks) { + final boolean isNetworkConnected = Utils.isNetworkConnected(); + + StringBuilder builder = new StringBuilder(); + builder.append(""); + builder.append(""); + + final boolean isDarkMode = isDarkModeEnabled(); + String backgroundColorHex = getColorHexString(isDarkMode ? getDarkColor() : getLightColor()); + String foregroundColorHex = getColorHexString(isDarkMode ? getLightColor() : getDarkColor()); + // 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(str("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(str("revanced_settings_about_links_dev_header"))) + .append("

"); + + builder.append("

") + .append(str("revanced_settings_about_links_dev_body")) + .append("

"); + } + + builder.append("

") + .append(str("revanced_settings_about_links_header")) + .append("

"); + + builder.append("
"); + for (ReVancedSocialLink social : socialLinks) { + builder.append("
"); + builder.append(String.format("%s", social.url, social.name)); + builder.append("
"); + } + builder.append("
"); + + builder.append(""); + return builder.toString(); + } + + { + setOnPreferenceClickListener(pref -> { + // Show a progress spinner if the social links are not fetched yet. + if (!SocialLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) { + ProgressDialog progress = new ProgressDialog(getContext()); + progress.setProgressStyle(ProgressDialog.STYLE_SPINNER); + progress.show(); + Utils.runOnBackgroundThread(() -> fetchLinksAndShowDialog(progress)); + } else { + // No network call required and can run now. + fetchLinksAndShowDialog(null); + } + + return false; + }); + } + + private void fetchLinksAndShowDialog(@Nullable ProgressDialog progress) { + ReVancedSocialLink[] socialLinks = SocialLinksRoutes.fetchSocialLinks(); + String htmlDialog = createDialogHtml(socialLinks); + + Utils.runOnMainThreadNowOrLater(() -> { + if (progress != null) { + 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); + + WebView webView = new WebView(getContext()); + webView.getSettings().setJavaScriptEnabled(true); + webView.setWebViewClient(new OpenLinksExternallyWebClient()); + webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null); + + setContentView(webView); + } + + 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 ReVancedSocialLink { + final boolean preferred; + final String name; + final String url; + + ReVancedSocialLink(JSONObject json) throws JSONException { + this(json.getBoolean("preferred"), + json.getString("name"), + json.getString("url") + ); + } + + ReVancedSocialLink(boolean preferred, String name, String url) { + this.preferred = preferred; + this.name = name; + this.url = url; + } + + @NonNull + @Override + public String toString() { + return "ReVancedSocialLink{" + + "preferred=" + preferred + + ", name='" + name + '\'' + + ", url='" + url + '\'' + + '}'; + } +} + +class SocialLinksRoutes { + /** + * Links to use if fetch links api call fails. + */ + private static final ReVancedSocialLink[] NO_CONNECTION_STATIC_LINKS = { + new ReVancedSocialLink(true, "ReVanced.app", "https://revanced.app") + }; + + private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v2"; + private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/socials").compile(); + + @Nullable + private static volatile ReVancedSocialLink[] fetchedLinks; + + static boolean hasFetchedLinks() { + return fetchedLinks != null; + } + + static ReVancedSocialLink[] fetchSocialLinks() { + 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); + JSONArray socials = json.getJSONArray("socials"); + + List links = new ArrayList<>(); + for (int i = 0, length = socials.length(); i < length; i++) { + ReVancedSocialLink link = new ReVancedSocialLink(socials.getJSONObject(i)); + links.add(link); + } + Logger.printDebug(() -> "links: " + links); + + return fetchedLinks = links.toArray(new ReVancedSocialLink[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/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java index 804b51c400..4cf1f27795 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java @@ -14,7 +14,7 @@ import static app.revanced.integrations.shared.StringRef.str; -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "deprecation"}) public class ResettableEditTextPreference extends EditTextPreference { public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { @@ -33,7 +33,7 @@ public ResettableEditTextPreference(Context context) { @Override protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { super.onPrepareDialogBuilder(builder); - Setting setting = Setting.getSettingFromPath(getKey()); + Setting setting = Setting.getSettingFromPath(getKey()); if (setting != null) { builder.setNeutralButton(str("revanced_settings_reset"), null); } @@ -50,7 +50,7 @@ protected void showDialog(Bundle state) { } button.setOnClickListener(v -> { try { - Setting setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey())); + Setting setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey())); String defaultStringValue = setting.defaultValue.toString(); EditText editText = getEditText(); editText.setText(defaultStringValue); diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/SharedPrefCategory.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/SharedPrefCategory.java index 9c7fa45ca8..ddbc31cb04 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/SharedPrefCategory.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/SharedPrefCategory.java @@ -53,7 +53,7 @@ public void saveBoolean(@NonNull String key, boolean value) { /** * @param value a NULL parameter removes the value from the preferences */ - public void saveEnumAsString(@NonNull String key, @Nullable Enum value) { + public void saveEnumAsString(@NonNull String key, @Nullable Enum value) { saveObjectAsString(key, value); } @@ -98,7 +98,7 @@ public String getString(@NonNull String key, @NonNull String _default) { } @NonNull - public T getEnum(@NonNull String key, @NonNull T _default) { + public > T getEnum(@NonNull String key, @NonNull T _default) { Objects.requireNonNull(_default); try { String enumName = preferences.getString(key, null); diff --git a/app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java b/app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java index 0faa46b14c..0cc8061c70 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java +++ b/app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java @@ -1,14 +1,23 @@ package app.revanced.integrations.youtube; import android.app.Activity; +import android.graphics.Color; + +import androidx.annotation.Nullable; + import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; public class ThemeHelper { + @Nullable + private static Integer darkThemeColor, lightThemeColor; private static int themeValue; - public static void setTheme(Object value) { - final int newOrdinalValue = ((Enum) value).ordinal(); + /** + * Injection point. + */ + public static void setTheme(Enum value) { + final int newOrdinalValue = value.ordinal(); if (themeValue != newOrdinalValue) { themeValue = newOrdinalValue; Logger.printDebug(() -> "Theme value: " + newOrdinalValue); @@ -26,4 +35,48 @@ public static void setActivityTheme(Activity activity) { activity.setTheme(Utils.getResourceIdentifier(theme, "style")); } + /** + * Injection point. + */ + private static String darkThemeResourceName() { + // Value is changed by Theme patch, if included. + return "@android:color/black"; + } + + /** + * @return The dark theme color as specified by the Theme patch (if included), + * or the Android color of black. + */ + public static int getDarkThemeColor() { + if (darkThemeColor == null) { + darkThemeColor = getColorInt(darkThemeResourceName()); + } + return darkThemeColor; + } + + /** + * Injection point. + */ + private static String lightThemeResourceName() { + // Value is changed by Theme patch, if included. + return "@android:color/white"; + } + + /** + * @return The light theme color as specified by the Theme patch (if included), + * or the Android color of white. + */ + public static int getLightThemeColor() { + if (lightThemeColor == null) { + lightThemeColor = getColorInt(lightThemeResourceName()); + } + return lightThemeColor; + } + + private static int getColorInt(String colorString) { + if (colorString.startsWith("#")) { + return Color.parseColor(colorString); + } + return Utils.getResourceColor(colorString); + } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/AlternativeThumbnailsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/AlternativeThumbnailsPatch.java index 1e0d2e1eeb..df7aab9fcd 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/AlternativeThumbnailsPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/AlternativeThumbnailsPatch.java @@ -1,19 +1,15 @@ package app.revanced.integrations.youtube.patches; +import static app.revanced.integrations.shared.StringRef.str; +import static app.revanced.integrations.youtube.settings.Settings.*; +import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; + import android.net.Uri; + import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.shared.settings.EnumSetting; -import app.revanced.integrations.shared.settings.Setting; -import app.revanced.integrations.youtube.settings.Settings; -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.youtube.shared.NavigationBar; -import app.revanced.integrations.youtube.shared.PlayerType; - import org.chromium.net.UrlRequest; import org.chromium.net.UrlResponseInfo; import org.chromium.net.impl.CronetUrlRequest; @@ -26,13 +22,12 @@ import java.util.Map; import java.util.concurrent.ExecutionException; -import static app.revanced.integrations.shared.StringRef.str; -import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_HOME; -import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_LIBRARY; -import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_PLAYER; -import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_SEARCH; -import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_SUBSCRIPTIONS; -import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.Setting; +import app.revanced.integrations.youtube.settings.Settings; +import app.revanced.integrations.youtube.shared.NavigationBar; +import app.revanced.integrations.youtube.shared.PlayerType; /** * Alternative YouTube thumbnails. @@ -134,11 +129,6 @@ public enum ThumbnailStillTime { */ private static volatile long timeToResumeDeArrowAPICalls; - /** - * Used only for debug logging. - */ - private static volatile EnumSetting currentOptionSetting; - static { dearrowApiUri = validateSettings(); final int port = dearrowApiUri.getPort(); @@ -162,21 +152,38 @@ private static Uri validateSettings() { return apiUri; } - private static EnumSetting optionSettingForCurrentNavigation() { - if (NavigationBar.isSearchBarActive()) { // Must check search first. - return ALT_THUMBNAIL_SEARCH; - } + private static ThumbnailOption optionSettingForCurrentNavigation() { + // Must check player type first, as search bar can be active behind the player. if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { - return ALT_THUMBNAIL_PLAYER; + return ALT_THUMBNAIL_PLAYER.get(); } - if (NavigationButton.HOME.isSelected()) { - return ALT_THUMBNAIL_HOME; + + // Must check second, as search can be from any tab. + if (NavigationBar.isSearchBarActive()) { + return ALT_THUMBNAIL_SEARCH.get(); } - if (NavigationButton.SUBSCRIPTIONS.isSelected() || NavigationButton.NOTIFICATIONS.isSelected()) { - return ALT_THUMBNAIL_SUBSCRIPTIONS; + + // Avoid checking which navigation button is selected, if all other settings are the same. + ThumbnailOption homeOption = ALT_THUMBNAIL_HOME.get(); + ThumbnailOption subscriptionsOption = ALT_THUMBNAIL_SUBSCRIPTIONS.get(); + ThumbnailOption libraryOption = ALT_THUMBNAIL_LIBRARY.get(); + if ((homeOption == subscriptionsOption) && (homeOption == libraryOption)) { + return homeOption; // All are the same option. + } + + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + // Unknown tab, treat as the home tab; + return homeOption; + } + if (selectedNavButton == NavigationButton.HOME) { + return homeOption; + } + if (selectedNavButton == NavigationButton.SUBSCRIPTIONS || selectedNavButton == NavigationButton.NOTIFICATIONS) { + return subscriptionsOption; } // A library tab variant is active. - return ALT_THUMBNAIL_LIBRARY; + return libraryOption; } /** @@ -254,14 +261,7 @@ private static void handleDeArrowError(@NonNull String url, int statusCode) { */ public static String overrideImageURL(String originalUrl) { try { - EnumSetting optionSetting = optionSettingForCurrentNavigation(); - ThumbnailOption option = optionSetting.get(); - if (BaseSettings.DEBUG.get()) { - if (currentOptionSetting != optionSetting) { - currentOptionSetting = optionSetting; - Logger.printDebug(() -> "Changed to setting: " + optionSetting.key); - } - } + ThumbnailOption option = optionSettingForCurrentNavigation(); if (option == ThumbnailOption.ORIGINAL) { return originalUrl; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/HideBreakingNewsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/HideBreakingNewsPatch.java deleted file mode 100644 index bd6d89fa57..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/HideBreakingNewsPatch.java +++ /dev/null @@ -1,29 +0,0 @@ -package app.revanced.integrations.youtube.patches; - -import android.view.View; - -import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch; -import app.revanced.integrations.youtube.settings.Settings; -import app.revanced.integrations.shared.Utils; - -@SuppressWarnings("unused") -public class HideBreakingNewsPatch { - - /** - * When spoofing to app versions 17.31.00 and older, the watch history preview bar uses - * the same layout components as the breaking news shelf. - * - * Breaking news does not appear to be present in these older versions anyways. - */ - private static final boolean isSpoofingOldVersionWithHorizontalCardListWatchHistory = - SpoofAppVersionPatch.isSpoofingToLessThan("18.01.00"); - - /** - * Injection point. - */ - public static void hideBreakingNews(View view) { - if (!Settings.HIDE_BREAKING_NEWS.get() - || isSpoofingOldVersionWithHorizontalCardListWatchHistory) return; - Utils.hideViewByLayoutParams(view); - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/HideLoadMoreButtonPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/HideLoadMoreButtonPatch.java deleted file mode 100644 index fa3a0e70ef..0000000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/HideLoadMoreButtonPatch.java +++ /dev/null @@ -1,14 +0,0 @@ -package app.revanced.integrations.youtube.patches; - -import android.view.View; - -import app.revanced.integrations.youtube.settings.Settings; -import app.revanced.integrations.shared.Utils; - -@SuppressWarnings("unused") -public class HideLoadMoreButtonPatch { - public static void hideLoadMoreButton(View view){ - if(!Settings.HIDE_LOAD_MORE_BUTTON.get()) return; - Utils.hideViewByLayoutParams(view); - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java index 4938fb3765..1c66cb930d 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java @@ -176,12 +176,6 @@ public static void setOldUILayoutDislikes(int buttonViewResourceId, @Nullable Te textView.removeTextChangedListener(oldUiTextWatcher); textView.addTextChangedListener(oldUiTextWatcher); - /** - * If the patch is changed to include the dislikes button as a parameter to this method, - * then if the button is already selected the dislikes could be adjusted using - * {@link ReturnYouTubeDislike#setUserVote(Vote)} - */ - updateOldUIDislikesTextView(); } catch (Exception ex) { @@ -314,19 +308,25 @@ public static String onRollingNumberLoaded(@NonNull Object conversionContext, */ public static float onRollingNumberMeasured(String text, float measuredTextWidth) { try { - if (Settings.RYD_ENABLED.get() && !Settings.RYD_COMPACT_LAYOUT.get()) { + if (Settings.RYD_ENABLED.get()) { if (ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(text)) { // +1 pixel is needed for some foreign languages that measure // the text different from what is used for layout (Greek in particular). // Probably a bug in Android, but who knows. // Single line mode is also used as an additional fix for this issue. - return measuredTextWidth + ReturnYouTubeDislike.leftSeparatorBounds.right - + ReturnYouTubeDislike.leftSeparatorShapePaddingPixels + 1; + if (Settings.RYD_COMPACT_LAYOUT.get()) { + return measuredTextWidth + 1; + } + + return measuredTextWidth + 1 + + ReturnYouTubeDislike.leftSeparatorBounds.right + + ReturnYouTubeDislike.leftSeparatorShapePaddingPixels; } } } catch (Exception ex) { Logger.printException(() -> "onRollingNumberMeasured failure", ex); } + return measuredTextWidth; } @@ -344,10 +344,12 @@ private static void addRollingNumberPatchChanges(TextView view) { } else { view.setCompoundDrawables(separator, null, null, null); } + // Disliking can cause the span to grow in size, which is ok and is laid out correctly, // but if the user then removes their dislike the layout will not adjust to the new shorter width. // Use a center alignment to take up any extra space. view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); + // Single line mode does not clip words if the span is larger than the view bounds. // The styled span applied to the view should always have the same bounds, // but use this feature just in case the measurements are somehow off by a few pixels. diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch.java index db1e37554c..eec599ecdf 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch.java @@ -61,7 +61,7 @@ public static void showAnnouncement(final Activity context) { return; } - var jsonString = Requester.parseInputStreamAndClose(connection.getInputStream(), false); + var jsonString = Requester.parseStringAndDisconnect(connection); // Parse the announcement. Fall-back to raw string if it fails. diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java index 7858bb7477..acb9c9b26c 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java @@ -112,34 +112,36 @@ final class KeywordContentFilter extends Filter { private volatile ByteTrieSearch bufferSearch; - private static void logNavigationState(String state) { - // Enable locally to debug filtering. Default off to reduce log spam. - final boolean LOG_NAVIGATION_STATE = false; - // noinspection ConstantValue - if (LOG_NAVIGATION_STATE) { - Logger.printDebug(() -> "Navigation state: " + state); + private static boolean hideKeywordSettingIsActive() { + // Must check player type first, as search bar can be active behind the player. + if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { + // For now, consider the under video results the same as the home feed. + return Settings.HIDE_KEYWORD_CONTENT_HOME.get(); } - } - private static boolean hideKeywordSettingIsActive() { + // Must check second, as search can be from any tab. if (NavigationBar.isSearchBarActive()) { - // Must check first. Search bar can be active with almost any tab. - logNavigationState("Search"); return Settings.HIDE_KEYWORD_CONTENT_SEARCH.get(); - } else if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { - // For now, consider the under video results the same as the home feed. - logNavigationState("Player active"); - return Settings.HIDE_KEYWORD_CONTENT_HOME.get(); - } else if (NavigationButton.HOME.isSelected()) { - logNavigationState("Home tab"); - return Settings.HIDE_KEYWORD_CONTENT_HOME.get(); - } else if (NavigationButton.SUBSCRIPTIONS.isSelected()) { - logNavigationState("Subscription tab"); - return Settings.HIDE_SUBSCRIPTIONS_BUTTON.get(); - } else { - // User is in the Library or Notifications tab. - logNavigationState("Ignored tab"); } + + // Avoid checking navigation button status if all other settings are off. + final boolean hideHome = Settings.HIDE_KEYWORD_CONTENT_HOME.get(); + final boolean hideSubscriptions = Settings.HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS.get(); + if (!hideHome && !hideSubscriptions) { + return false; + } + + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + return hideHome; // Unknown tab, treat the same as home. + } + if (selectedNavButton == NavigationButton.HOME) { + return hideHome; + } + if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) { + return hideSubscriptions; + } + // User is in the Library or Notifications tab. return false; } @@ -195,6 +197,7 @@ private static boolean phrasesWillHideAllVideos(@NonNull String[] phrases) { private synchronized void parseKeywords() { // Must be synchronized since Litho is multi-threaded. String rawKeywords = Settings.HIDE_KEYWORD_CONTENT_PHRASES.get(); + //noinspection StringEquality if (rawKeywords == lastKeywordPhrasesParsed) { Logger.printDebug(() -> "Using previously initialized search"); return; // Another thread won the race, and search is already initialized. @@ -265,6 +268,7 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff if (!hideKeywordSettingIsActive()) return false; // Field is intentionally compared using reference equality. + //noinspection StringEquality if (Settings.HIDE_KEYWORD_CONTENT_PHRASES.get() != lastKeywordPhrasesParsed) { // User changed the keywords. parseKeywords(); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java index f32ad545bc..90ed38412d 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java @@ -1,13 +1,19 @@ package app.revanced.integrations.youtube.patches.components; +import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; + import android.os.Build; +import android.view.View; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; -import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.StringTrieSearch; +import app.revanced.integrations.youtube.settings.Settings; +import app.revanced.integrations.youtube.shared.NavigationBar; +import app.revanced.integrations.youtube.shared.PlayerType; @SuppressWarnings("unused") public final class LayoutComponentsFilter extends Filter { @@ -31,6 +37,7 @@ public final class LayoutComponentsFilter extends Filter { private final StringFilterGroup compactChannelBarInner; private final StringFilterGroup compactChannelBarInnerButton; private final ByteArrayFilterGroup joinMembershipButton; + private final StringFilterGroup horizontalShelves; static { mixPlaylistsExceptions.addPatterns( @@ -39,7 +46,6 @@ public final class LayoutComponentsFilter extends Filter { ); } - @RequiresApi(api = Build.VERSION_CODES.N) public LayoutComponentsFilter() { exceptions.addPatterns( @@ -107,8 +113,8 @@ public LayoutComponentsFilter() { "medical_panel" ); - final var paidContent = new StringFilterGroup( - Settings.HIDE_PAID_CONTENT, + final var paidPromotion = new StringFilterGroup( + Settings.HIDE_PAID_PROMOTION_LABEL, "paid_content_overlay" ); @@ -171,6 +177,11 @@ public LayoutComponentsFilter() { "fullscreen_related_videos" ); + final var playables = new StringFilterGroup( + Settings.HIDE_PLAYABLES, + "horizontal_gaming_shelf.eml" + ); + final var quickActions = new StringFilterGroup( Settings.HIDE_QUICK_ACTIONS, "quick_actions" @@ -233,17 +244,25 @@ public LayoutComponentsFilter() { "endorsement_header_footer" ); + horizontalShelves = new StringFilterGroup( + Settings.HIDE_HORIZONTAL_SHELVES, + "horizontal_video_shelf.eml", + "horizontal_shelf.eml", + "horizontal_tile_shelf.eml" + ); + addPathCallbacks( expandableMetadata, inFeedSurvey, notifyMe, channelBar, communityPosts, - paidContent, + paidPromotion, searchResultVideo, latestPosts, channelWatermark, communityGuidelines, + playables, quickActions, relatedVideos, compactBanner, @@ -259,7 +278,8 @@ public LayoutComponentsFilter() { timedReactions, imageShelf, channelMemberShelf, - forYouShelf + forYouShelf, + horizontalShelves ); } @@ -270,12 +290,15 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff if (searchResultRecommendations.check(protobufBufferArray).isFiltered()) { return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } + return false; } // The groups are excluded from the filter due to the exceptions list below. // Filter them separately here. if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata) + { return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); + } if (exceptions.matches(path)) return false; // Exceptions are not filtered. @@ -294,6 +317,14 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff // TODO: This also hides the feed Shorts shelf header if (matchedGroup == searchResultShelfHeader && contentIndex != 0) return false; + if (matchedGroup == horizontalShelves) { + if (contentIndex == 0 && hideShelves()) { + return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + + return false; + } + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } @@ -322,7 +353,40 @@ public static boolean filterMixPlaylists(final Object conversionContext, @Nullab return true; } + /** + * Injection point. + */ public static boolean showWatermark() { return !Settings.HIDE_VIDEO_CHANNEL_WATERMARK.get(); } + + private static final boolean HIDE_SHOW_MORE_BUTTON_ENABLED = Settings.HIDE_SHOW_MORE_BUTTON.get(); + + /** + * Injection point. + */ + public static void hideShowMoreButton(View view) { + if (HIDE_SHOW_MORE_BUTTON_ENABLED + && NavigationBar.isSearchBarActive() + // Search bar can be active but behind the player. + && !PlayerType.getCurrent().isMaximizedOrFullscreen()) { + Utils.hideViewByLayoutParams(view); + } + } + + private static boolean hideShelves() { + // If the player is opened while library is selected, + // then filter any recommendations below the player. + if (PlayerType.getCurrent().isMaximizedOrFullscreen() + // Or if the search is active while library is selected, then also filter. + || NavigationBar.isSearchBarActive()) { + return true; + } + + // Check navigation button last. + // Only filter if the library tab is not selected. + // This check is important as the shelf layout is used for the library tab playlists. + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + return selectedNavButton != null && !selectedNavButton.isLibraryOrYouTab(); + } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java index 19ccfac5ce..7099bf2f5e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java @@ -57,6 +57,10 @@ public PlayerFlyoutMenuItemsFilter() { Settings.HIDE_MORE_INFO_MENU, "yt_outline_info_circle" ), + new ByteArrayFilterGroup( + Settings.HIDE_LOCK_SCREEN_MENU, + "yt_outline_lock" + ), new ByteArrayFilterGroup( Settings.HIDE_SPEED_MENU, "yt_outline_play_arrow_half_circle" @@ -75,15 +79,21 @@ public PlayerFlyoutMenuItemsFilter() { @Override boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + // Only 1 path callback was added, so the matched group must be the overflow menu. + if (contentIndex != 0) { + return false; // Overflow menu is always the start of the path. + } + // Shorts also use this player flyout panel - if (PlayerType.getCurrent().isNoneOrHidden() || exception.check(protobufBufferArray).isFiltered()) + if (PlayerType.getCurrent().isNoneOrHidden() || exception.check(protobufBufferArray).isFiltered()) { return false; + } - // Only 1 path callback was added, so the matched group must be the overflow menu. - if (contentIndex == 0 && flyoutFilterGroupList.check(protobufBufferArray).isFiltered()) { + if (flyoutFilterGroupList.check(protobufBufferArray).isFiltered()) { // Super class handles logging. return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } + return false; } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java index 8c3dbf26be..371fde562a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java @@ -1,6 +1,7 @@ package app.revanced.integrations.youtube.patches.components; import static app.revanced.integrations.shared.Utils.hideViewUnderCondition; +import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; import android.view.View; @@ -16,29 +17,33 @@ @SuppressWarnings("unused") public final class ShortsFilter extends Filter { public static PivotBar pivotBar; // Set by patch. - private final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml"; + + private final static String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml"; + /** + * For paid promotion label and subscribe button that appears in the channel bar. + */ + private final static String REEL_METAPANEL_PATH = "reel_metapanel.eml"; private final StringFilterGroup shortsCompactFeedVideoPath; private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer; - private final StringFilterGroup channelBar; - private final StringFilterGroup fullVideoLinkLabel; - private final StringFilterGroup videoTitle; - private final StringFilterGroup reelSoundMetadata; private final StringFilterGroup subscribeButton; - private final StringFilterGroup subscribeButtonPaused; - private final StringFilterGroup soundButton; - private final StringFilterGroup infoPanel; private final StringFilterGroup joinButton; + private final StringFilterGroup paidPromotionButton; private final StringFilterGroup shelfHeader; + private final StringFilterGroup suggestedAction; + private final ByteArrayFilterGroupList suggestedActionsGroupList = new ByteArrayFilterGroupList(); + private final StringFilterGroup actionBar; private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList(); public ShortsFilter() { + // // Identifier components. + // - var shorts = new StringFilterGroup( + var shortsIdentifiers = new StringFilterGroup( null, // Setting is based on navigation state. "shorts_shelf", "inline_shorts", @@ -46,6 +51,7 @@ public ShortsFilter() { "shorts_video_cell", "shorts_pivot_item" ); + // Feed Shorts shelf header. // Use a different filter group for this pattern, as it requires an additional check after matching. shelfHeader = new StringFilterGroup( @@ -53,15 +59,11 @@ public ShortsFilter() { "shelf_header.eml" ); - // Home / subscription feed components. - var thanksButton = new StringFilterGroup( - Settings.HIDE_SHORTS_THANKS_BUTTON, - "suggested_action" - ); - - addIdentifierCallbacks(shorts, shelfHeader, thanksButton); + addIdentifierCallbacks(shortsIdentifiers, shelfHeader); + // // Path components. + // // Shorts that appear in the feed/search when the device is using tablet layout. shortsCompactFeedVideoPath = new StringFilterGroup(null, "compact_video.eml"); @@ -71,94 +73,128 @@ public ShortsFilter() { shortsCompactFeedVideoBuffer = new ByteArrayFilterGroup(null, "/frame0.jpg"); // Shorts player components. - joinButton = new StringFilterGroup( - Settings.HIDE_SHORTS_JOIN_BUTTON, - "sponsor_button" - ); - - subscribeButton = new StringFilterGroup( - Settings.HIDE_SHORTS_SUBSCRIBE_BUTTON, - "subscribe_button" - ); - - subscribeButtonPaused = new StringFilterGroup( - Settings.HIDE_SHORTS_SUBSCRIBE_BUTTON_PAUSED, + StringFilterGroup pausedOverlayButtons = new StringFilterGroup( + Settings.HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS, "shorts_paused_state" ); - channelBar = new StringFilterGroup( + StringFilterGroup channelBar = new StringFilterGroup( Settings.HIDE_SHORTS_CHANNEL_BAR, REEL_CHANNEL_BAR_PATH ); - fullVideoLinkLabel = new StringFilterGroup( + StringFilterGroup fullVideoLinkLabel = new StringFilterGroup( Settings.HIDE_SHORTS_FULL_VIDEO_LINK_LABEL, "reel_multi_format_link" ); - videoTitle = new StringFilterGroup( + StringFilterGroup videoTitle = new StringFilterGroup( Settings.HIDE_SHORTS_VIDEO_TITLE, "shorts_video_title_item" ); - reelSoundMetadata = new StringFilterGroup( + StringFilterGroup reelSoundMetadata = new StringFilterGroup( Settings.HIDE_SHORTS_SOUND_METADATA_LABEL, "reel_sound_metadata" ); - soundButton = new StringFilterGroup( + StringFilterGroup soundButton = new StringFilterGroup( Settings.HIDE_SHORTS_SOUND_BUTTON, "reel_pivot_button" ); - infoPanel = new StringFilterGroup( + StringFilterGroup infoPanel = new StringFilterGroup( Settings.HIDE_SHORTS_INFO_PANEL, "shorts_info_panel_overview" ); - actionBar = new StringFilterGroup( - null, - "shorts_action_bar" + joinButton = new StringFilterGroup( + Settings.HIDE_SHORTS_JOIN_BUTTON, + "sponsor_button" ); - addPathCallbacks( - shortsCompactFeedVideoPath, - joinButton, subscribeButton, subscribeButtonPaused, - channelBar, fullVideoLinkLabel, videoTitle, reelSoundMetadata, - soundButton, infoPanel, actionBar + subscribeButton = new StringFilterGroup( + Settings.HIDE_SHORTS_SUBSCRIBE_BUTTON, + "subscribe_button" ); - var shortsLikeButton = new ByteArrayFilterGroup( - Settings.HIDE_SHORTS_LIKE_BUTTON, - "shorts_like_button" + paidPromotionButton = new StringFilterGroup( + Settings.HIDE_PAID_PROMOTION_LABEL, + "reel_player_disclosure.eml" ); - var shortsDislikeButton = new ByteArrayFilterGroup( - Settings.HIDE_SHORTS_DISLIKE_BUTTON, - "shorts_dislike_button" + actionBar = new StringFilterGroup( + null, + "shorts_action_bar" ); - var shortsCommentButton = new ByteArrayFilterGroup( - Settings.HIDE_SHORTS_COMMENTS_BUTTON, - "reel_comment_button" + suggestedAction = new StringFilterGroup( + null, + "suggested_action.eml" ); - var shortsShareButton = new ByteArrayFilterGroup( - Settings.HIDE_SHORTS_SHARE_BUTTON, - "reel_share_button" + addPathCallbacks( + shortsCompactFeedVideoPath, suggestedAction, actionBar, joinButton, subscribeButton, + paidPromotionButton, pausedOverlayButtons, channelBar, fullVideoLinkLabel, videoTitle, + reelSoundMetadata, soundButton, infoPanel ); - var shortsRemixButton = new ByteArrayFilterGroup( - Settings.HIDE_SHORTS_REMIX_BUTTON, - "reel_remix_button" + // + // Action buttons + // + videoActionButtonGroupList.addAll( + // This also appears as the path item 'shorts_like_button.eml' + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_LIKE_BUTTON, + "reel_like_button", + "reel_like_toggled_button" + ), + // This also appears as the path item 'shorts_dislike_button.eml' + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_DISLIKE_BUTTON, + "reel_dislike_button", + "reel_dislike_toggled_button" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_COMMENTS_BUTTON, + "reel_comment_button" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_SHARE_BUTTON, + "reel_share_button" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_REMIX_BUTTON, + "reel_remix_button" + ) ); - videoActionButtonGroupList.addAll( - shortsLikeButton, - shortsDislikeButton, - shortsCommentButton, - shortsShareButton, - shortsRemixButton + // + // Suggested actions. + // + suggestedActionsGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_SHOP_BUTTON, + "yt_outline_bag_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_TAGGED_PRODUCTS, + // Product buttons show pictures of the products, and does not have any unique icons to identify. + // Instead use a unique identifier found in the buffer. + "PAproduct_listZ" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_LOCATION_LABEL, + "yt_outline_location_point_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_SAVE_SOUND_BUTTON, + "yt_outline_list_add_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_SEARCH_SUGGESTIONS, + "yt_outline_search_" + ) ); } @@ -166,15 +202,15 @@ public ShortsFilter() { boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { if (contentType == FilterContentType.PATH) { - // Always filter if matched. - if (matchedGroup == soundButton || - matchedGroup == infoPanel || - matchedGroup == channelBar || - matchedGroup == fullVideoLinkLabel || - matchedGroup == videoTitle || - matchedGroup == reelSoundMetadata || - matchedGroup == subscribeButtonPaused - ) return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); + if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) { + // Selectively filter to avoid false positive filtering of other subscribe/join buttons. + if (path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH)) { + return super.isFiltered( + identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex + ); + } + return false; + } if (matchedGroup == shortsCompactFeedVideoPath) { if (shouldHideShortsFeedItems() && contentIndex == 0 @@ -186,25 +222,26 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff // Video action buttons (like, dislike, comment, share, remix) have the same path. if (matchedGroup == actionBar) { - if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) return super.isFiltered( - identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex - ); + if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) { + return super.isFiltered( + identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex + ); + } return false; } - // Filter other path groups from pathFilterGroupList, only when reelChannelBar is visible - // to avoid false positives. - if (matchedGroup == subscribeButton || - matchedGroup == joinButton - ) { - if (path.startsWith(REEL_CHANNEL_BAR_PATH)) return super.isFiltered( - identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex - ); // else, return false. + if (matchedGroup == suggestedAction) { + // Suggested actions can be at the start or in the middle of a path. + if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) { + return super.isFiltered( + identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex + ); + } + return false; } - return false; } else { - // Feed/search path components. + // Feed/search identifier components. if (matchedGroup == shelfHeader) { // Because the header is used in watch history and possibly other places, check for the index, // which is 0 when the shelf header is used for Shorts. @@ -219,14 +256,49 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff } private static boolean shouldHideShortsFeedItems() { - if (NavigationBar.isSearchBarActive()) { // Must check search first. - return Settings.HIDE_SHORTS_SEARCH.get(); - } else if (PlayerType.getCurrent().isMaximizedOrFullscreen() - || NavigationBar.NavigationButton.HOME.isSelected()) { - return Settings.HIDE_SHORTS_HOME.get(); - } else if (NavigationBar.NavigationButton.SUBSCRIPTIONS.isSelected()) { - return Settings.HIDE_SHORTS_SUBSCRIPTIONS.get(); + final boolean hideHome = Settings.HIDE_SHORTS_HOME.get(); + final boolean hideSubscriptions = Settings.HIDE_SHORTS_SUBSCRIPTIONS.get(); + final boolean hideSearch = Settings.HIDE_SHORTS_SEARCH.get(); + + if (hideHome && hideSubscriptions && hideSearch) { + // Shorts suggestions can load in the background if a video is opened and + // then immediately minimized before any suggestions are loaded. + // In this state the player type will show minimized, which makes it not possible to + // distinguish between Shorts suggestions loading in the player and between + // scrolling thru search/home/subscription tabs while a player is minimized. + // + // To avoid this situation for users that never want to show Shorts (all hide Shorts options are enabled) + // then hide all Shorts everywhere including the Library history and Library playlists. + return true; + } + + // Must check player type first, as search bar can be active behind the player. + if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { + // For now, consider the under video results the same as the home feed. + return hideHome; + } + + // Must check second, as search can be from any tab. + if (NavigationBar.isSearchBarActive()) { + return hideSearch; + } + + // Avoid checking navigation button status if all other Shorts should show. + if (!hideHome && !hideSubscriptions) { + return false; + } + + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + return hideHome; // Unknown tab, treat the same as home. + } + if (selectedNavButton == NavigationButton.HOME) { + return hideHome; + } + if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) { + return hideSubscriptions; } + // User must be in the library tab. Don't hide the history or any playlists here. return false; } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java index 67e6950229..7909387f22 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java @@ -37,7 +37,7 @@ final class PlayerRoutes { JSONObject client = new JSONObject(); client.put("clientName", "ANDROID"); - client.put("clientVersion", Utils.getVersionName()); + client.put("clientVersion", Utils.getAppVersionName()); client.put("androidSdkVersion", 34); context.put("client", client); @@ -85,7 +85,7 @@ static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRout connection.setRequestProperty( "User-Agent", "com.google.android.youtube/" + - Utils.getVersionName() + + Utils.getAppVersionName() + " (Linux; U; Android 12; GB) gzip" ); connection.setRequestProperty("X-Goog-Api-Format-Version", "2"); diff --git a/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java b/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java index 5c109f9880..ef409b52d3 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java @@ -24,7 +24,10 @@ public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Ro String url = apiUrl + route.getCompiledRoute(); HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestMethod(route.getMethod().name()); - connection.setRequestProperty("User-Agent", System.getProperty("http.agent") + "; ReVanced/" + Utils.getVersionName()); + String agentString = System.getProperty("http.agent") + + "; ReVanced/" + Utils.getAppVersionName() + + " (" + Utils.getPatchesReleaseVersion() + ")"; + connection.setRequestProperty("User-Agent", agentString); return connection; } @@ -32,72 +35,79 @@ public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Ro /** * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. */ - public static String parseJson(HttpURLConnection connection) throws IOException { - return parseInputStreamAndClose(connection.getInputStream(), true); + 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}, close the underlying InputStream, and disconnect. - * - * Should only be used if other requests to the server are unlikely in the near future - * - * @see #parseJson(HttpURLConnection) + * 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 parseJsonAndDisconnect(HttpURLConnection connection) throws IOException { - String result = parseJson(connection); - connection.disconnect(); - return result; + public static String parseString(HttpURLConnection connection) throws IOException { + return parseInputStreamAndClose(connection.getInputStream()); } /** - * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + * 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 * - * @param stripNewLineCharacters if newline (\n) characters should be stripped from the InputStream + * @see #parseString(HttpURLConnection) */ - public static String parseInputStreamAndClose(InputStream inputStream, boolean stripNewLineCharacters) throws IOException { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { - StringBuilder jsonBuilder = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - jsonBuilder.append(line); - if (!stripNewLineCharacters) - jsonBuilder.append("\n"); - } - return jsonBuilder.toString(); - } + public static String parseStringAndDisconnect(HttpURLConnection connection) throws IOException { + String result = parseString(connection); + connection.disconnect(); + return result; } /** - * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + * 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 parseErrorJson(HttpURLConnection connection) throws IOException { - return parseInputStreamAndClose(connection.getErrorStream(), false); + public static String parseErrorString(HttpURLConnection connection) throws IOException { + InputStream errorStream = connection.getErrorStream(); + if (errorStream == null) { + return ""; + } + return parseInputStreamAndClose(errorStream); } /** - * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. + * 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 + * Should only be used if other requests to the server are unlikely in the near future. * - * @see #parseErrorJson(HttpURLConnection) + * @see #parseErrorString(HttpURLConnection) */ - public static String parseErrorJsonAndDisconnect(HttpURLConnection connection) throws IOException { - String result = parseErrorJson(connection); + public static String parseErrorStringAndDisconnect(HttpURLConnection connection) throws IOException { + String result = parseErrorString(connection); connection.disconnect(); return result; } /** - * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + * 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(parseJson(connection)); + 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 are unlikely in the near future + * Should only be used if other requests to the server in the near future are unlikely * * @see #parseJSONObject(HttpURLConnection) */ @@ -109,15 +119,17 @@ public static JSONObject parseJSONObjectAndDisconnect(HttpURLConnection connecti /** * 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(parseJson(connection)); + 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 are unlikely in the near future + * Should only be used if other requests to the server in the near future are unlikely * * @see #parseJSONArray(HttpURLConnection) */ diff --git a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java index 73db5f003f..bc729e4794 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java +++ b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -1,7 +1,7 @@ package app.revanced.integrations.youtube.returnyoutubedislike.requests; -import static app.revanced.integrations.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute; import static app.revanced.integrations.shared.StringRef.str; +import static app.revanced.integrations.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute; import android.util.Base64; @@ -22,11 +22,11 @@ import java.security.SecureRandom; import java.util.Objects; +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.requests.Requester; import app.revanced.integrations.youtube.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.integrations.youtube.settings.Settings; -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; public class ReturnYouTubeDislikeApi { /** @@ -279,7 +279,7 @@ public static RYDVoteData fetchVotes(String videoId) { } if (responseCode == HTTP_STATUS_CODE_SUCCESS) { - // do not disconnect, the same server connection will likely be used again soon + // Do not disconnect, the same server connection will likely be used again soon. JSONObject json = Requester.parseJSONObject(connection); try { RYDVoteData votingData = new RYDVoteData(json); @@ -377,20 +377,17 @@ private static String confirmRegistration(String userId, String solution) { connection.disconnect(); // disconnect, as no more connections will be made for a little while return null; } - String result = null; if (responseCode == HTTP_STATUS_CODE_SUCCESS) { - result = Requester.parseJson(connection); - if (result.equalsIgnoreCase("true")) { - Logger.printDebug(() -> "Registration confirmation successful"); - return userId; - } + Logger.printDebug(() -> "Registration confirmation successful"); + return userId; } - final String resultLog = result == null ? "(no response)" : result; + + // Something went wrong, might as well disconnect. + String response = Requester.parseStringAndDisconnect(connection); Logger.printInfo(() -> "Failed to confirm registration for user: " + userId - + " solution: " + solution + " responseCode: " + responseCode + " responseString: " + resultLog); + + " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "''"); handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null, true); - connection.disconnect(); // something went wrong, might as well disconnect } catch (SocketTimeoutException ex) { handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); } catch (IOException ex) { @@ -461,6 +458,7 @@ public static boolean sendVote(String videoId, ReturnYouTubeDislike.Vote vote) { String solution = solvePuzzle(challenge, difficulty); return confirmVote(videoId, userId, solution); } + Logger.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote + " response code was: " + responseCode); handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), @@ -501,20 +499,17 @@ private static boolean confirmVote(String videoId, String userId, String solutio connection.disconnect(); // disconnect, as no more connections will be made for a little while return false; } - String result = null; if (responseCode == HTTP_STATUS_CODE_SUCCESS) { - result = Requester.parseJson(connection); - if (result.equalsIgnoreCase("true")) { - Logger.printDebug(() -> "Vote confirm successful for video: " + videoId); - return true; - } + Logger.printDebug(() -> "Vote confirm successful for video: " + videoId); + return true; } - final String resultLog = result == null ? "(no response)" : result; + + // Something went wrong, might as well disconnect. + String response = Requester.parseStringAndDisconnect(connection); Logger.printInfo(() -> "Failed to confirm vote for video: " + videoId - + " solution: " + solution + " responseCode: " + responseCode + " responseString: " + resultLog); + + " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "'"); handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null, true); - connection.disconnect(); // something went wrong, might as well disconnect } catch (SocketTimeoutException ex) { handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); } catch (IOException ex) { diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 6d2b722455..b74098c72e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -49,12 +49,13 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_GET_PREMIUM = new BooleanSetting("revanced_hide_get_premium", TRUE); public static final BooleanSetting HIDE_HIDE_LATEST_POSTS = new BooleanSetting("revanced_hide_latest_posts_ads", TRUE); public static final BooleanSetting HIDE_MERCHANDISE_BANNERS = new BooleanSetting("revanced_hide_merchandise_banners", TRUE); - public static final BooleanSetting HIDE_PAID_CONTENT = new BooleanSetting("revanced_hide_paid_content_ads", TRUE); + public static final BooleanSetting HIDE_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_paid_promotion_label", TRUE); public static final BooleanSetting HIDE_PRODUCTS_BANNER = new BooleanSetting("revanced_hide_products_banner", TRUE); public static final BooleanSetting HIDE_SHOPPING_LINKS = new BooleanSetting("revanced_hide_shopping_links", TRUE); public static final BooleanSetting HIDE_SELF_SPONSOR = new BooleanSetting("revanced_hide_self_sponsor_ads", TRUE); public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_hide_video_ads", TRUE, true); public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE); + // Layout public static final EnumSetting ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL); public static final EnumSetting ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscription", ThumbnailOption.ORIGINAL); @@ -71,12 +72,12 @@ public class Settings extends BaseSettings { public static final BooleanSetting DISABLE_FULLSCREEN_AMBIENT_MODE = new BooleanSetting("revanced_disable_fullscreen_ambient_mode", TRUE, true); public static final BooleanSetting DISABLE_RESUMING_SHORTS_PLAYER = new BooleanSetting("revanced_disable_resuming_shorts_player", FALSE); public static final BooleanSetting DISABLE_ROLLING_NUMBER_ANIMATIONS = new BooleanSetting("revanced_disable_rolling_number_animations", FALSE); - public static final BooleanSetting DISABLE_SUGGESTED_VIDEO_END_SCREEN = new BooleanSetting("revanced_disable_suggested_video_end_screen", FALSE); + public static final BooleanSetting DISABLE_SUGGESTED_VIDEO_END_SCREEN = new BooleanSetting("revanced_disable_suggested_video_end_screen", FALSE, true); public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE); public static final BooleanSetting HIDE_ALBUM_CARDS = new BooleanSetting("revanced_hide_album_cards", FALSE, true); public static final BooleanSetting HIDE_ARTIST_CARDS = new BooleanSetting("revanced_hide_artist_cards", FALSE); public static final BooleanSetting HIDE_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_autoplay_button", TRUE, true); - public static final BooleanSetting HIDE_BREAKING_NEWS = new BooleanSetting("revanced_hide_breaking_news", TRUE, true); + public static final BooleanSetting HIDE_HORIZONTAL_SHELVES = new BooleanSetting("revanced_hide_horizontal_shelves", TRUE); public static final BooleanSetting HIDE_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_captions_button", FALSE); public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE, true); public static final BooleanSetting HIDE_CHANNEL_BAR = new BooleanSetting("revanced_hide_channel_bar", FALSE); @@ -108,13 +109,15 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_KEYWORD_CONTENT_SEARCH = new BooleanSetting("revanced_hide_keyword_content_search", FALSE); public static final StringSetting HIDE_KEYWORD_CONTENT_PHRASES = new StringSetting("revanced_hide_keyword_content_phrases", "", parentsAny(HIDE_KEYWORD_CONTENT_HOME, HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS, HIDE_KEYWORD_CONTENT_SEARCH)); - public static final BooleanSetting HIDE_LOAD_MORE_BUTTON = new BooleanSetting("revanced_hide_load_more_button", TRUE, true); + @Deprecated public static final BooleanSetting HIDE_LOAD_MORE_BUTTON = new BooleanSetting("revanced_hide_load_more_button", TRUE); + public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true); public static final BooleanSetting HIDE_MEDICAL_PANELS = new BooleanSetting("revanced_hide_medical_panels", TRUE); public static final BooleanSetting HIDE_MIX_PLAYLISTS = new BooleanSetting("revanced_hide_mix_playlists", TRUE); public static final BooleanSetting HIDE_MOVIES_SECTION = new BooleanSetting("revanced_hide_movies_section", TRUE); public static final BooleanSetting HIDE_NOTIFY_ME_BUTTON = new BooleanSetting("revanced_hide_notify_me_button", TRUE); public static final BooleanSetting HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE); public static final BooleanSetting HIDE_PREVIEW_COMMENT = new BooleanSetting("revanced_hide_preview_comment", FALSE, true); + public static final BooleanSetting HIDE_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE); public static final BooleanSetting HIDE_QUICK_ACTIONS = new BooleanSetting("revanced_hide_quick_actions", FALSE); public static final BooleanSetting HIDE_RELATED_VIDEOS = new BooleanSetting("revanced_hide_related_videos", FALSE); public static final BooleanSetting HIDE_SEARCH_RESULT_SHELF_HEADER = new BooleanSetting("revanced_hide_search_result_shelf_header", FALSE); @@ -156,8 +159,12 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_SHORTS_SEARCH = new BooleanSetting("revanced_hide_shorts_search", FALSE); public static final BooleanSetting HIDE_SHORTS_JOIN_BUTTON = new BooleanSetting("revanced_hide_shorts_join_button", TRUE); public static final BooleanSetting HIDE_SHORTS_SUBSCRIBE_BUTTON = new BooleanSetting("revanced_hide_shorts_subscribe_button", TRUE); - public static final BooleanSetting HIDE_SHORTS_SUBSCRIBE_BUTTON_PAUSED = new BooleanSetting("revanced_hide_shorts_subscribe_button_paused", FALSE); - public static final BooleanSetting HIDE_SHORTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_thanks_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS = new BooleanSetting("revanced_hide_shorts_paused_overlay_buttons", FALSE); + public static final BooleanSetting HIDE_SHORTS_SHOP_BUTTON = new BooleanSetting("revanced_hide_shorts_shop_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_TAGGED_PRODUCTS = new BooleanSetting("revanced_hide_shorts_tagged_products", TRUE); + public static final BooleanSetting HIDE_SHORTS_LOCATION_LABEL = new BooleanSetting("revanced_hide_shorts_location_label", FALSE); + public static final BooleanSetting HIDE_SHORTS_SAVE_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_save_sound_button", FALSE); + public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS = new BooleanSetting("revanced_hide_shorts_search_suggestions", FALSE); public static final BooleanSetting HIDE_SHORTS_LIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_like_button", FALSE); public static final BooleanSetting HIDE_SHORTS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_dislike_button", FALSE); public static final BooleanSetting HIDE_SHORTS_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_shorts_comments_button", FALSE); @@ -198,6 +205,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_HELP_MENU = new BooleanSetting("revanced_hide_player_flyout_help", TRUE); public static final BooleanSetting HIDE_SPEED_MENU = new BooleanSetting("revanced_hide_player_flyout_speed", FALSE); public static final BooleanSetting HIDE_MORE_INFO_MENU = new BooleanSetting("revanced_hide_player_flyout_more_info", TRUE); + public static final BooleanSetting HIDE_LOCK_SCREEN_MENU = new BooleanSetting("revanced_hide_player_flyout_lock_screen", FALSE); public static final BooleanSetting HIDE_AUDIO_TRACK_MENU = new BooleanSetting("revanced_hide_player_flyout_audio_track", FALSE); public static final BooleanSetting HIDE_WATCH_IN_VR_MENU = new BooleanSetting("revanced_hide_player_flyout_watch_in_vr", TRUE); @@ -215,10 +223,11 @@ public class Settings extends BaseSettings { parent(SPOOF_SIGNATURE)); public static final BooleanSetting SPOOF_STORYBOARD_RENDERER = new BooleanSetting("revanced_spoof_storyboard", TRUE, true, parent(SPOOF_SIGNATURE)); - public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true); + public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true, + "revanced_spoof_device_dimensions_user_dialog_message"); public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE); public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE); - @Deprecated + @Deprecated public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", ""); public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1); public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE); @@ -240,9 +249,9 @@ public class Settings extends BaseSettings { parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true, parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); - public static final BooleanSetting SWIPE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_swipe_save_and_restore_brightness", TRUE, true, - parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); - + public static final BooleanSetting SWIPE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_swipe_save_and_restore_brightness", TRUE, true, parent(SWIPE_BRIGHTNESS)); + public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1f); + public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_swipe_lowest_value_enable_auto_brightness", FALSE, true, parent(SWIPE_BRIGHTNESS)); // Debugging /** * When enabled, share the debug logs with care. @@ -383,6 +392,8 @@ public class Settings extends BaseSettings { HIDE_SHORTS_SEARCH.save(true); } + migrateOldSettingToNew(HIDE_LOAD_MORE_BUTTON, HIDE_SHOW_MORE_BUTTON); + // endregion } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java index b631e2e106..f0153e7e20 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java @@ -9,7 +9,7 @@ /** * Allows tapping the DeArrow about preference to open the DeArrow website. */ -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "deprecation"}) public class AlternativeThumbnailsAboutDeArrowPreference extends Preference { { setOnPreferenceClickListener(pref -> { diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedYouTubeAboutPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedYouTubeAboutPreference.java new file mode 100644 index 0000000000..336016514c --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedYouTubeAboutPreference.java @@ -0,0 +1,32 @@ +package app.revanced.integrations.youtube.settings.preference; + +import android.content.Context; +import android.util.AttributeSet; + +import app.revanced.integrations.shared.settings.preference.ReVancedAboutPreference; +import app.revanced.integrations.youtube.ThemeHelper; + +@SuppressWarnings("unused") +public class ReVancedYouTubeAboutPreference extends ReVancedAboutPreference { + + public int getLightColor() { + return ThemeHelper.getLightThemeColor(); + } + + public int getDarkColor() { + return ThemeHelper.getDarkThemeColor(); + } + + public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ReVancedYouTubeAboutPreference(Context context) { + super(context); + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java index 117c2f9945..c4a6c9be97 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java @@ -79,10 +79,9 @@ public void onCreate(Bundle savedInstanceState) { shortsPreference = new SwitchPreference(context); shortsPreference.setChecked(Settings.RYD_SHORTS.get()); shortsPreference.setTitle(str("revanced_ryd_shorts_title")); - String shortsSummary = str("revanced_ryd_shorts_summary_on", - ReturnYouTubeDislikePatch.IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER - ? "" - : "\n\n" + str("revanced_ryd_shorts_summary_disclaimer")); + String shortsSummary = ReturnYouTubeDislikePatch.IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER + ? str("revanced_ryd_shorts_summary_on") + : str("revanced_ryd_shorts_summary_on_disclaimer"); shortsPreference.setSummaryOn(shortsSummary); shortsPreference.setSummaryOff(str("revanced_ryd_shorts_summary_off")); shortsPreference.setOnPreferenceChangeListener((pref, newValue) -> { diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java index 137509d862..371ef38934 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java @@ -100,10 +100,10 @@ private void updateUI() { privateUserId.setEnabled(enabled); // If the user has a private user id, then include a subtext that mentions not to share it. - String exportSummarySubText = SponsorBlockSettings.userHasSBPrivateId() + String importExportSummary = SponsorBlockSettings.userHasSBPrivateId() ? str("revanced_sb_settings_ie_sum_warning") - : ""; - importExport.setSummary(str("revanced_sb_settings_ie_sum", exportSummarySubText)); + : str("revanced_sb_settings_ie_sum"); + importExport.setSummary(importExportSummary); apiUrl.setEnabled(enabled); importExport.setEnabled(enabled); diff --git a/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java b/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java index 4cec2680af..5123e78d6a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java +++ b/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java @@ -2,21 +2,29 @@ import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton.CREATE; +import android.app.Activity; import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; import androidx.annotation.Nullable; import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.BaseSettings; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public final class NavigationBar { + // + // Search bar + // + private static volatile WeakReference searchBarResultsRef = new WeakReference<>(null); /** @@ -27,24 +35,115 @@ public static void searchBarResultsViewLoaded(View searchbarResults) { } /** - * @return If the search bar is on screen. + * @return If the search bar is on screen. This includes if the player + * is on screen and the search results are behind the player (and not visible). + * Detecting the search is covered by the player can be done by checking {@link PlayerType#isMaximizedOrFullscreen()}. */ public static boolean isSearchBarActive() { View searchbarResults = searchBarResultsRef.get(); return searchbarResults != null && searchbarResults.getParent() != null; } + // + // Navigation bar buttons + // + + /** + * How long to wait for the set nav button latch to be released. Maximum wait time must + * be as small as possible while still allowing enough time for the nav bar to update. + * + * YT calls it's back button handlers out of order, + * and litho starts filtering before the navigation bar is updated. + * + * Fixing this situation and not needlessly waiting requires somehow + * detecting if a back button key-press will cause a tab change. + * + * Typically after pressing the back button, the time between the first litho event and + * when the nav button is updated is about 10-20ms. Using 50-100ms here should be enough time + * and not noticeable, since YT typically takes 100-200ms (or more) to update the view anyways. + * + * This issue can also be avoided on a patch by patch basis, by avoiding calls to + * {@link NavigationButton#getSelectedNavigationButton()} unless absolutely necessary. + */ + private static final long LATCH_AWAIT_TIMEOUT_MILLISECONDS = 75; + + /** + * Used as a workaround to fix the issue of YT calling back button handlers out of order. + * Used to hold calls to {@link NavigationButton#getSelectedNavigationButton()} + * until the current navigation button can be determined. + * + * Only used when the hardware back button is pressed. + */ + @Nullable + private static volatile CountDownLatch navButtonLatch; + + /** + * Map of nav button layout views to Enum type. + * No synchronization is needed, and this is always accessed from the main thread. + */ + private static final Map viewToButtonMap = new WeakHashMap<>(); + + static { + // On app startup litho can start before the navigation bar is initialized. + // Force it to wait until the nav bar is updated. + createNavButtonLatch(); + } + + private static void createNavButtonLatch() { + navButtonLatch = new CountDownLatch(1); + } + + private static void releaseNavButtonLatch() { + CountDownLatch latch = navButtonLatch; + if (latch != null) { + navButtonLatch = null; + latch.countDown(); + } + } + + private static void waitForNavButtonLatchIfNeeded() { + CountDownLatch latch = navButtonLatch; + if (latch == null) { + return; + } + + if (Utils.isCurrentlyOnMainThread()) { + // The latch is released from the main thread, and waiting from the main thread will always timeout. + // This situation has only been observed when navigating out of a submenu and not changing tabs. + // and for that use case the nav bar does not change so it's safe to return here. + Logger.printDebug(() -> "Cannot block main thread waiting for nav button. Using last known navbar button status."); + return; + } + + try { + Logger.printDebug(() -> "Latch wait started"); + if (latch.await(LATCH_AWAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)) { + // Back button changed the navigation tab. + Logger.printDebug(() -> "Latch wait complete"); + return; + } + + // Timeout occurred, and a normal event when pressing the physical back button + // does not change navigation tabs. + releaseNavButtonLatch(); // Prevent other threads from waiting for no reason. + Logger.printDebug(() -> "Latch wait timed out"); + + } catch (InterruptedException ex) { + Logger.printException(() -> "Latch wait interrupted failure", ex); // Will never happen. + } + } /** * Last YT navigation enum loaded. Not necessarily the active navigation tab. + * Always accessed from the main thread. */ @Nullable - private static volatile String lastYTNavigationEnumName; + private static String lastYTNavigationEnumName; /** * Injection point. */ - public static void setLastAppNavigationEnum(@Nullable Enum ytNavigationEnumName) { + public static void setLastAppNavigationEnum(@Nullable Enum ytNavigationEnumName) { if (ytNavigationEnumName != null) { lastYTNavigationEnumName = ytNavigationEnumName.name(); } @@ -56,21 +155,16 @@ public static void setLastAppNavigationEnum(@Nullable Enum ytNavigationEnumName) public static void navigationTabLoaded(final View navigationButtonGroup) { try { String lastEnumName = lastYTNavigationEnumName; + for (NavigationButton button : NavigationButton.values()) { if (button.ytEnumName.equals(lastEnumName)) { - ImageView imageView = Utils.getChildView((ViewGroup) navigationButtonGroup, - true, view -> view instanceof ImageView); - - if (imageView != null) { - Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName); - - button.imageViewRef = new WeakReference<>(imageView); - navigationTabCreatedCallback(button, navigationButtonGroup); - - return; - } + Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName); + viewToButtonMap.put(navigationButtonGroup, button); + navigationTabCreatedCallback(button, navigationButtonGroup); + return; } } + // Log the unknown tab as exception level, only if debug is enabled. // This is because unknown tabs do no harm, and it's only relevant to developers. if (Settings.DEBUG.get()) { @@ -98,6 +192,45 @@ public static void navigationImageResourceTabLoaded(View view) { } } + /** + * Injection point. + */ + public static void navigationTabSelected(View navButtonImageView, boolean isSelected) { + try { + if (!isSelected) { + return; + } + + NavigationButton button = viewToButtonMap.get(navButtonImageView); + + if (button == null) { // An unknown tab was selected. + // Show a toast only if debug mode is enabled. + if (BaseSettings.DEBUG.get()) { + Logger.printException(() -> "Unknown navigation view selected: " + navButtonImageView); + } + + NavigationButton.selectedNavigationButton = null; + return; + } + + NavigationButton.selectedNavigationButton = button; + Logger.printDebug(() -> "Changed to navigation button: " + button); + + // Release any threads waiting for the selected nav button. + releaseNavButtonLatch(); + } catch (Exception ex) { + Logger.printException(() -> "navigationTabSelected failure", ex); + } + } + + /** + * Injection point. + */ + public static void onBackPressed(Activity activity) { + Logger.printDebug(() -> "Back button pressed"); + createNavButtonLatch(); + } + /** @noinspection EmptyMethod*/ private static void navigationTabCreatedCallback(NavigationButton button, View tabView) { // Code is added during patching. @@ -108,8 +241,7 @@ public enum NavigationButton { SHORTS("TAB_SHORTS"), /** * Create new video tab. - * - * {@link #isSelected()} always returns false, even if the create video UI is on screen. + * This tab will never be in a selected state, even if the create video UI is on screen. */ CREATE("CREATION_TAB_LARGE"), SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS"), @@ -144,41 +276,43 @@ public enum NavigationButton { // The hooked YT code does not use an enum, and a dummy name is used here. LIBRARY_YOU("YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME"); + @Nullable + private static volatile NavigationButton selectedNavigationButton; + /** + * This will return null only if the currently selected tab is unknown. + * This scenario will only happen if the UI has different tabs due to an A/B user test + * or YT abruptly changes the navigation layout for some other reason. + * + * All code calling this method should handle a null return value. + * + * Due to issues with how YT processes physical back button events, + * this patch uses workarounds that can cause this method to take up to 75ms + * if the device back button was recently pressed. + * * @return The active navigation tab. - * If the user is in the create new video UI, this returns NULL. + * If the user is in the upload video UI, this returns tab that is still visually + * selected on screen (whatever tab the user was on before tapping the upload button). */ @Nullable public static NavigationButton getSelectedNavigationButton() { - for (NavigationButton button : values()) { - if (button.isSelected()) return button; - } - return null; - } - - /** - * @return If the currently selected tab is a 'You' or library type. - * Covers all known app states including incognito mode and version spoofing. - */ - public static boolean libraryOrYouTabIsSelected() { - return LIBRARY_YOU.isSelected() || LIBRARY_PIVOT_UNKNOWN.isSelected() - || LIBRARY_OLD_UI.isSelected() || LIBRARY_INCOGNITO.isSelected() - || LIBRARY_LOGGED_OUT.isSelected(); + waitForNavButtonLatchIfNeeded(); + return selectedNavigationButton; } /** * YouTube enum name for this tab. */ private final String ytEnumName; - private volatile WeakReference imageViewRef = new WeakReference<>(null); NavigationButton(String ytEnumName) { this.ytEnumName = ytEnumName; } - public boolean isSelected() { - ImageView view = imageViewRef.get(); - return view != null && view.isSelected(); + public boolean isLibraryOrYouTab() { + return this == LIBRARY_YOU || this == LIBRARY_PIVOT_UNKNOWN + || this == LIBRARY_OLD_UI || this == LIBRARY_INCOGNITO + || this == LIBRARY_LOGGED_OUT; } } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/requests/SBRequester.java b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/requests/SBRequester.java index 020fd03849..9e25e9f614 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/requests/SBRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/requests/SBRequester.java @@ -159,13 +159,13 @@ public static void submitSegments(@NonNull String videoId, @NonNull String categ messageToToast = str("revanced_sb_submit_failed_duplicate"); break; case 403: - messageToToast = str("revanced_sb_submit_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection)); + messageToToast = str("revanced_sb_submit_failed_forbidden", Requester.parseErrorStringAndDisconnect(connection)); break; case 429: messageToToast = str("revanced_sb_submit_failed_rate_limit"); break; case 400: - messageToToast = str("revanced_sb_submit_failed_invalid", Requester.parseErrorJsonAndDisconnect(connection)); + messageToToast = str("revanced_sb_submit_failed_invalid", Requester.parseErrorStringAndDisconnect(connection)); break; default: messageToToast = str("revanced_sb_submit_failed_unknown_error", responseCode, connection.getResponseMessage()); @@ -223,7 +223,7 @@ private static void voteOrRequestCategoryChange(@NonNull SponsorSegment segment, break; case 403: Utils.showToastLong( - str("revanced_sb_vote_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection))); + str("revanced_sb_vote_failed_forbidden", Requester.parseErrorStringAndDisconnect(connection))); break; default: Utils.showToastLong( diff --git a/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt b/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt index 0e2d799797..893ccd5795 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt +++ b/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt @@ -104,5 +104,17 @@ class SwipeControlsConfigurationProvider( val shouldSaveAndRestoreBrightness: Boolean get() = Settings.SWIPE_SAVE_AND_RESTORE_BRIGHTNESS.get() + /** + * should auto-brightness be enabled at the lowest value of the brightness gesture + */ + val shouldLowestValueEnableAutoBrightness: Boolean + get() = Settings.SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS.get() + + /** + * variable that stores the brightness gesture value in the settings + */ + var savedScreenBrightnessValue: Float + get() = Settings.SWIPE_BRIGHTNESS_VALUE.get() + set(value) = Settings.SWIPE_BRIGHTNESS_VALUE.save(value) //endregion } diff --git a/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/SwipeControlsHostActivity.kt b/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/SwipeControlsHostActivity.kt index e6f233e830..552363144d 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/SwipeControlsHostActivity.kt +++ b/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/SwipeControlsHostActivity.kt @@ -166,20 +166,31 @@ class SwipeControlsHostActivity : Activity() { contentRoot.addView(overlay) } + // Flag that indicates whether the brightness has been saved and restored default brightness + private var isBrightnessSaved = false + /** * called when the player type changes * * @param type the new player type */ private fun onPlayerTypeChanged(type: PlayerType) { - if (config.shouldSaveAndRestoreBrightness) { - when (type) { - PlayerType.WATCH_WHILE_FULLSCREEN -> screen?.restore() - else -> { - screen?.save() - screen?.restoreDefaultBrightness() - } + when { + // If saving and restoring brightness is enabled, and the player type is WATCH_WHILE_FULLSCREEN, + // and brightness has already been saved, then restore the screen brightness + config.shouldSaveAndRestoreBrightness && type == PlayerType.WATCH_WHILE_FULLSCREEN && isBrightnessSaved -> { + screen?.restore() + isBrightnessSaved = false + } + // If saving and restoring brightness is enabled, and brightness has not been saved, + // then save the current screen state, restore default brightness, and mark brightness as saved + config.shouldSaveAndRestoreBrightness && !isBrightnessSaved -> { + screen?.save() + screen?.restoreDefaultBrightness() + isBrightnessSaved = true } + // If saving and restoring brightness is disabled, simply keep the default brightness + else -> screen?.restoreDefaultBrightness() } } @@ -222,4 +233,4 @@ class SwipeControlsHostActivity : Activity() { var currentHost: WeakReference = WeakReference(null) private set } -} +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/controller/ScreenBrightnessController.kt b/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/controller/ScreenBrightnessController.kt index 1cf282e892..08e2700485 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/controller/ScreenBrightnessController.kt +++ b/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/controller/ScreenBrightnessController.kt @@ -1,21 +1,17 @@ package app.revanced.integrations.youtube.swipecontrols.controller -import android.app.Activity import android.view.WindowManager +import app.revanced.integrations.youtube.swipecontrols.SwipeControlsHostActivity import app.revanced.integrations.youtube.swipecontrols.misc.clamp /** * controller to adjust the screen brightness level * - * @param host the host activity of which the brightness is adjusted + * @param host the host activity of which the brightness is adjusted, the main controller instance */ class ScreenBrightnessController( - private val host: Activity, + val host: SwipeControlsHostActivity, ) { - /** - * screen brightness saved by [save] - */ - private var savedScreenBrightness: Float? = null /** * the current screen brightness in percent, ranging from 0.0 to 100.0 @@ -26,6 +22,12 @@ class ScreenBrightnessController( rawScreenBrightness = (value.toFloat() / 100f).clamp(0f, 1f) } + /** + * is the screen brightness set to device- default? + */ + val isDefaultBrightness + get() = (rawScreenBrightness == WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) + /** * restore the screen brightness to the default device brightness */ @@ -33,29 +35,29 @@ class ScreenBrightnessController( rawScreenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE } - /** - * is the screen brightness set to device- default? - */ - val isDefaultBrightness - get() = (rawScreenBrightness == WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) + // Flag that indicates whether the brightness has been restored + private var isBrightnessRestored = false /** - * save the current screen brightness, to be brought back using [restore] + * save the current screen brightness into settings, to be brought back using [restore] */ fun save() { - if (savedScreenBrightness == null) { - savedScreenBrightness = rawScreenBrightness + if (isBrightnessRestored) { + // Saves the current screen brightness value into settings + host.config.savedScreenBrightnessValue = rawScreenBrightness + // Reset the flag + isBrightnessRestored = false } } /** - * restore the screen brightness saved using [save] + * restore the screen brightness from settings saved using [save] */ fun restore() { - savedScreenBrightness?.let { - rawScreenBrightness = it - } - savedScreenBrightness = null + // Restores the screen brightness value from the saved settings + rawScreenBrightness = host.config.savedScreenBrightnessValue + // Mark that brightness has been restored + isBrightnessRestored = true } /** diff --git a/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt b/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt index dec14b81a1..f15478ec38 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt +++ b/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt @@ -77,12 +77,17 @@ class VolumeAndBrightnessScrollerImpl( ), ) { _, _, direction -> screenController?.run { - if (screenBrightness > 0 || direction > 0) { + val shouldAdjustBrightness = if (host.config.shouldLowestValueEnableAutoBrightness) { + screenBrightness > 0 || direction > 0 + } else { + screenBrightness >= 0 || direction >= 0 + } + + if (shouldAdjustBrightness) { screenBrightness += direction } else { restoreDefaultBrightness() } - overlayController.onBrightnessChanged(screenBrightness) } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt b/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt index dbe2434f3c..f68fcda1e2 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt +++ b/app/src/main/java/app/revanced/integrations/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt @@ -11,6 +11,7 @@ import android.view.View import android.view.ViewGroup import android.widget.RelativeLayout import android.widget.TextView +import app.revanced.integrations.shared.StringRef.str import app.revanced.integrations.shared.Utils import app.revanced.integrations.youtube.swipecontrols.SwipeControlsConfigurationProvider import app.revanced.integrations.youtube.swipecontrols.misc.SwipeControlsOverlay @@ -122,10 +123,13 @@ class SwipeControlsOverlayLayout( } override fun onBrightnessChanged(brightness: Double) { - if (brightness > 0) { + if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) { + showFeedbackView( + str("revanced_swipe_lowest_value_enable_auto_brightness_overlay_text"), + autoBrightnessIcon, + ) + } else if (brightness >= 0) { showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon) - } else { - showFeedbackView("AUTO", autoBrightnessIcon) } } diff --git a/gradle.properties b/gradle.properties index ba78657a15..d049f49574 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.7.0 +version = 1.8.0-dev.20