diff --git a/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java index 95c68b41..8e10eea9 100644 --- a/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java @@ -1,11 +1,9 @@ package app.revanced.integrations.patches.components; import android.os.Build; + import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -14,6 +12,10 @@ import java.util.Iterator; import java.util.Spliterator; import java.util.function.Consumer; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + abstract class FilterGroup { final static class FilterGroupResult { private final boolean filtered; @@ -49,7 +51,7 @@ abstract class FilterGroup { } public boolean isEnabled() { - return setting.getBoolean(); + return setting == null || setting.getBoolean(); } public abstract FilterGroupResult check(final T stack); @@ -208,7 +210,7 @@ abstract class Filter { /** * Check if the given path, identifier or protobuf buffer is filtered by any - * {@link FilterGroup}. + * {@link FilterGroup}. Method is called off the main thread. * * @return True if filtered, false otherwise. */ @@ -239,9 +241,12 @@ public final class LithoFilterPatch { new DummyFilter() // Replaced by patch. }; + /** + * Injection point. Called off the main thread. + */ @SuppressWarnings("unused") public static boolean filter(final StringBuilder pathBuilder, final String identifier, - final ByteBuffer protobufBuffer) { + final ByteBuffer protobufBuffer) { // TODO: Maybe this can be moved to the Filter class, to prevent unnecessary // string creation // because some filters might not need the path. diff --git a/app/src/main/java/app/revanced/integrations/patches/components/VideoQualityMenuFilterPatch.java b/app/src/main/java/app/revanced/integrations/patches/components/VideoQualityMenuFilterPatch.java new file mode 100644 index 00000000..a500c7ba --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/components/VideoQualityMenuFilterPatch.java @@ -0,0 +1,23 @@ +package app.revanced.integrations.patches.components; + +import app.revanced.integrations.settings.SettingsEnum; + +// Abuse LithoFilter for OldVideoQualityMenuPatch. +public final class VideoQualityMenuFilterPatch extends Filter { + // Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread. + public static volatile boolean isVideoQualityMenuVisible; + + public VideoQualityMenuFilterPatch() { + pathFilterGroups.addAll(new StringFilterGroup( + SettingsEnum.SHOW_OLD_VIDEO_QUALITY_MENU, + "quick_quality_sheet_content.eml-js" + )); + } + + @Override + boolean isFiltered(final String path, final String identifier, final byte[] protobufBufferArray) { + isVideoQualityMenuVisible = super.isFiltered(path, identifier, protobufBufferArray); + + return false; + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/components/VideoSpeedMenuFilterPatch.java b/app/src/main/java/app/revanced/integrations/patches/components/VideoSpeedMenuFilterPatch.java new file mode 100644 index 00000000..f01d30d1 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/components/VideoSpeedMenuFilterPatch.java @@ -0,0 +1,21 @@ +package app.revanced.integrations.patches.components; + +// Abuse LithoFilter for CustomVideoSpeedPatch. +public final class VideoSpeedMenuFilterPatch extends Filter { + // Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread. + public static volatile boolean isVideoSpeedMenuVisible; + + public VideoSpeedMenuFilterPatch() { + pathFilterGroups.addAll(new StringFilterGroup( + null, + "playback_speed_sheet_content.eml-js" + )); + } + + @Override + boolean isFiltered(final String path, final String identifier, final byte[] protobufBufferArray) { + isVideoSpeedMenuVisible = super.isFiltered(path, identifier, protobufBufferArray); + + return false; + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldQualityLayoutPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldQualityLayoutPatch.java deleted file mode 100644 index 71899a7a..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldQualityLayoutPatch.java +++ /dev/null @@ -1,35 +0,0 @@ -package app.revanced.integrations.patches.playback.quality; - -import android.view.View; -import android.view.ViewGroup; -import android.widget.ListView; - -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; - -public class OldQualityLayoutPatch { - public static void showOldQualityMenu(ListView listView) - { - if (!SettingsEnum.SHOW_OLD_VIDEO_MENU.getBoolean()) return; - - listView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { - @Override - public void onChildViewAdded(View parent, View child) { - LogHelper.printDebug(() -> "Added: " + child); - - parent.setVisibility(View.GONE); - - final var indexOfAdvancedQualityMenuItem = 4; - if (listView.indexOfChild(child) != indexOfAdvancedQualityMenuItem) return; - - LogHelper.printDebug(() -> "Found advanced menu: " + child); - - final var qualityItemMenuPosition = 4; - listView.performItemClick(null, qualityItemMenuPosition, 0); - } - - @Override - public void onChildViewRemoved(View parent, View child) {} - }); - } -} diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldVideoQualityMenuPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldVideoQualityMenuPatch.java new file mode 100644 index 00000000..97582040 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldVideoQualityMenuPatch.java @@ -0,0 +1,94 @@ +package app.revanced.integrations.patches.playback.quality; + +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; +import android.widget.ListView; + +import androidx.annotation.NonNull; + +import app.revanced.integrations.patches.components.VideoQualityMenuFilterPatch; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import com.facebook.litho.ComponentHost; +import kotlin.Deprecated; + +// This patch contains the logic to show the old video quality menu. +// Two methods are required, because the quality menu is a RecyclerView in the new YouTube version +// and a ListView in the old one. +public final class OldVideoQualityMenuPatch { + + public static void onFlyoutMenuCreate(final LinearLayout linearLayout) { + if (!SettingsEnum.SHOW_OLD_VIDEO_QUALITY_MENU.getBoolean()) return; + + // The quality menu is a RecyclerView with 3 children. The third child is the "Advanced" quality menu. + addRecyclerListener(linearLayout, 3, 2, recyclerView -> { + // Check if the current view is the quality menu. + if (VideoQualityMenuFilterPatch.isVideoQualityMenuVisible) {// Hide the video quality menu. + linearLayout.setVisibility(View.GONE); + + // Click the "Advanced" quality menu to show the "old" quality menu. + ((ComponentHost) recyclerView.getChildAt(0)).getChildAt(3).performClick(); + LogHelper.printDebug(() -> "Advanced quality menu in new type of quality menu clicked"); + } + }); + } + + public static void addRecyclerListener(@NonNull LinearLayout linearLayout, + int expectedLayoutChildCount, int recyclerViewIndex, + @NonNull RecyclerViewGlobalLayoutListener listener) { + if (linearLayout.getChildCount() != expectedLayoutChildCount) return; + + var layoutChild = linearLayout.getChildAt(recyclerViewIndex); + if (!(layoutChild instanceof RecyclerView)) return; + final var recyclerView = (RecyclerView) layoutChild; + + recyclerView.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + try { + listener.recyclerOnGlobalLayout(recyclerView); + } catch (Exception ex) { + LogHelper.printException(() -> "addRecyclerListener failure", ex); + } finally { + // Remove the listener because it will be added again. + recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + } + } + ); + } + + public interface RecyclerViewGlobalLayoutListener { + void recyclerOnGlobalLayout(@NonNull RecyclerView recyclerView); + } + + @Deprecated(message = "This patch is deprecated because the quality menu is not a ListView anymore") + public static void showOldVideoQualityMenu(final ListView listView) { + if (!SettingsEnum.SHOW_OLD_VIDEO_QUALITY_MENU.getBoolean()) return; + + listView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { + @Override + public void onChildViewAdded(View parent, View child) { + LogHelper.printDebug(() -> "Added listener to old type of quality menu"); + + parent.setVisibility(View.GONE); + + final var indexOfAdvancedQualityMenuItem = 4; + if (listView.indexOfChild(child) != indexOfAdvancedQualityMenuItem) return; + + LogHelper.printDebug(() -> "Found advanced menu item in old type of quality menu"); + + final var qualityItemMenuPosition = 4; + listView.performItemClick(null, qualityItemMenuPosition, 0); + } + + @Override + public void onChildViewRemoved(View parent, View child) { + } + }); + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java index d1ef08eb..04c19f77 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java @@ -38,13 +38,8 @@ public class RememberVideoQualityPatch { private static List videoQualities; private static void changeDefaultQuality(int defaultQuality) { - NetworkType networkType = ReVancedUtils.getNetworkType(); - if (networkType == NetworkType.NONE) { - ReVancedUtils.showToastShort("No internet connection"); - return; - } String networkTypeMessage; - if (networkType == NetworkType.MOBILE) { + if (ReVancedUtils.getNetworkType() == NetworkType.MOBILE) { mobileQualitySetting.saveValue(defaultQuality); networkTypeMessage = "mobile"; } else { @@ -139,15 +134,24 @@ public class RememberVideoQualityPatch { } /** - * Injection point. + * Injection point. Old quality menu. */ - public static void userChangedQuality(int selectedQuality) { + public static void userChangedQuality(int selectedQualityIndex) { if (!SettingsEnum.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.getBoolean()) return; - userSelectedQualityIndex = selectedQuality; + userSelectedQualityIndex = selectedQualityIndex; userChangedDefaultQuality = true; } + /** + * Injection point. New quality menu. + */ + public static void userChangedQualityInNewFlyout(int selectedQuality) { + if (!SettingsEnum.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.getBoolean()) return; + + changeDefaultQuality(selectedQuality); // Quality is human readable resolution (ie: 1080). + } + /** * Injection point. */ diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java index df80c1f5..a8e2e603 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java @@ -1,11 +1,19 @@ package app.revanced.integrations.patches.playback.speed; +import static app.revanced.integrations.patches.playback.quality.OldVideoQualityMenuPatch.addRecyclerListener; + import android.preference.ListPreference; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; import androidx.annotation.NonNull; +import com.facebook.litho.ComponentHost; + import java.util.Arrays; +import app.revanced.integrations.patches.components.VideoSpeedMenuFilterPatch; import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; @@ -37,7 +45,7 @@ public class CustomVideoSpeedPatch { private static String[] preferenceListEntries, preferenceListEntryValues; static { - loadSpeeds(); + loadCustomSpeeds(); } private static void resetCustomSpeeds(@NonNull String toastMessage) { @@ -45,7 +53,7 @@ public class CustomVideoSpeedPatch { SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.saveValue(SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.defaultValue); } - private static void loadSpeeds() { + private static void loadCustomSpeeds() { try { String[] speedStrings = SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.getString().split("\\s+"); Arrays.sort(speedStrings); @@ -61,7 +69,7 @@ public class CustomVideoSpeedPatch { if (speed >= MAXIMUM_PLAYBACK_SPEED) { resetCustomSpeeds("Custom speeds must be less than " + MAXIMUM_PLAYBACK_SPEED + ". Using default values."); - loadSpeeds(); + loadCustomSpeeds(); return; } minVideoSpeed = Math.min(minVideoSpeed, speed); @@ -71,7 +79,7 @@ public class CustomVideoSpeedPatch { } catch (Exception ex) { LogHelper.printInfo(() -> "parse error", ex); resetCustomSpeeds("Invalid custom video speeds. Using default values."); - loadSpeeds(); + loadCustomSpeeds(); } } @@ -100,4 +108,32 @@ public class CustomVideoSpeedPatch { preference.setEntries(preferenceListEntries); preference.setEntryValues(preferenceListEntryValues); } + + /* + * To reduce copy paste between two similar code paths. + */ + public static void onFlyoutMenuCreate(final LinearLayout linearLayout) { + // The playback rate menu is a RecyclerView with 2 children. The third child is the "Advanced" quality menu. + addRecyclerListener(linearLayout, 2, 1, recyclerView -> { + if (VideoSpeedMenuFilterPatch.isVideoSpeedMenuVisible && + recyclerView.getChildCount() == 1 && + recyclerView.getChildAt(0) instanceof ComponentHost + ) { + linearLayout.setVisibility(View.GONE); + + // Close the new video speed menu and instead show the old one. + showOldVideoSpeedMenu(); + + // DismissView [R.id.touch_outside] is the 1st ChildView of the 3rd ParentView. + ((ViewGroup) linearLayout.getParent().getParent().getParent()) + .getChildAt(0).performClick(); + } + }); + } + + public static void showOldVideoSpeedMenu() { + LogHelper.printDebug(() -> "Old video quality menu shown"); + + // Rest of the implementation added by patch. + } } diff --git a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java index 596c0ff2..10fb1ea5 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -42,7 +42,9 @@ public enum SettingsEnum { // Video HDR_AUTO_BRIGHTNESS("revanced_hdr_auto_brightness", BOOLEAN, TRUE), - SHOW_OLD_VIDEO_MENU("revanced_show_old_video_menu", BOOLEAN, TRUE), + SHOW_OLD_VIDEO_QUALITY_MENU("revanced_show_old_video_quality_menu", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_SHOW_OLD_VIDEO_QUALITY_MENU("revanced_show_old_video_menu", BOOLEAN, TRUE), REMEMBER_VIDEO_QUALITY_LAST_SELECTED("revanced_remember_video_quality_last_selected", BOOLEAN, TRUE), VIDEO_QUALITY_DEFAULT_WIFI("revanced_video_quality_default_wifi", INTEGER, -2), VIDEO_QUALITY_DEFAULT_MOBILE("revanced_video_quality_default_mobile", INTEGER, -2), @@ -51,9 +53,6 @@ public enum SettingsEnum { CUSTOM_PLAYBACK_SPEEDS("revanced_custom_playback_speeds", STRING, "0.25\n0.5\n0.75\n0.9\n0.95\n1.0\n1.05\n1.1\n1.25\n1.5\n1.75\n2.0\n3.0\n4.0\n5.0", true), - // Whitelist - //WHITELIST("revanced_whitelist_ads", BOOLEAN, FALSE), // TODO: Unused currently - // Ads HIDE_BUTTONED_ADS("revanced_hide_buttoned_ads", BOOLEAN, TRUE), HIDE_GENERAL_ADS("revanced_hide_general_ads", BOOLEAN, TRUE), @@ -349,31 +348,31 @@ public enum SettingsEnum { setting.load(); } - // TODO: eventually delete this. // region Migration - SettingsEnum[][] renamedSettings = { - // TODO: do _not_ delete this SB private user id migration property until sometime in 2024. - // This is the only setting that cannot be reconfigured if lost, - // and more time should be given for users who rarely upgrade. - {DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING, SB_PRIVATE_USER_ID}, - }; + // TODO: do _not_ delete this SB private user id migration property until sometime in 2024. + // This is the only setting that cannot be reconfigured if lost, + // and more time should be given for users who rarely upgrade. + migrateOldSettingToNew(DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING, SB_PRIVATE_USER_ID); - for (SettingsEnum[] oldNewSetting : renamedSettings) { - SettingsEnum oldSetting = oldNewSetting[0]; - SettingsEnum newSetting = oldNewSetting[1]; - - if (!oldSetting.isSetToDefault()) { - LogHelper.printInfo(() -> "Migrating old setting of '" + oldSetting.value - + "' from: " + oldSetting + " into replacement setting: " + newSetting); - newSetting.saveValue(oldSetting.value); - oldSetting.saveValue(oldSetting.defaultValue); // reset old value - } - } + // TODO: delete DEPRECATED_SHOW_OLD_VIDEO_QUALITY_MENU (When? anytime). + migrateOldSettingToNew(DEPRECATED_SHOW_OLD_VIDEO_QUALITY_MENU, SHOW_OLD_VIDEO_QUALITY_MENU); // endregion } + /** + * Migrate a setting value if the path is renamed but otherwise the old and new settings are identical. + */ + private static void migrateOldSettingToNew(SettingsEnum oldSetting, SettingsEnum newSetting) { + if (!oldSetting.isSetToDefault()) { + LogHelper.printInfo(() -> "Migrating old setting of '" + oldSetting.value + + "' from: " + oldSetting + " into replacement setting: " + newSetting); + newSetting.saveValue(oldSetting.value); + oldSetting.saveValue(oldSetting.defaultValue); // reset old value + } + } + private void load() { switch (returnType) { case BOOLEAN: diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java index 9bbc8387..50d4a725 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java @@ -180,6 +180,11 @@ public class ReVancedSettingsFragment extends PreferenceFragment { if (entryIndex >= 0) { listPreference.setSummary(listPreference.getEntries()[entryIndex]); listPreference.setValue(objectStringValue); + } else { + // Value is not an available option. + // User manually edited import data, or options changed and current selection is no longer available. + // Still show the value in the summary so it's clear that something is selected. + listPreference.setSummary(objectStringValue); } } diff --git a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt index 4992170f..0fd89bfc 100644 --- a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt +++ b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt @@ -8,15 +8,16 @@ import app.revanced.integrations.utils.LogHelper */ enum class PlayerType { /** - * Includes Shorts and Stories playback. + * Either no video, or a Short is playing. */ NONE, /** - * A Shorts or Stories, if a regular video is minimized and a Short/Story is then opened. + * A Short is playing. Occurs if a regular video is first opened + * and then a Short is opened (without first closing the regular video). */ HIDDEN, /** - * When spoofing to an old version of YouTube, and watching a short with a regular video in the background, + * When spoofing to 16.x YouTube and watching a short with a regular video in the background, * the type will be this (and not [HIDDEN]). */ WATCH_WHILE_MINIMIZED, @@ -76,7 +77,7 @@ enum class PlayerType { * Useful to check if a short is currently playing. * * Does not include the first moment after a short is opened when a regular video is minimized on screen, - * or while watching a short with a regular video present on a spoofed old version of YouTube. + * or while watching a short with a regular video present on a spoofed 16.x version of YouTube. * To include those situations instead use [isNoneHiddenOrMinimized]. */ fun isNoneOrHidden(): Boolean { @@ -84,12 +85,13 @@ enum class PlayerType { } /** - * Check if the current player type is [NONE], [HIDDEN], [WATCH_WHILE_MINIMIZED], [WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED]. + * Check if the current player type is + * [NONE], [HIDDEN], [WATCH_WHILE_MINIMIZED], [WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED]. * * Useful to check if a Short is being played, - * although can return false positive if the player is minimized. + * although will return false positive if a regular video is opened and minimized (and no short is playing). * - * @return If nothing, a Short, a Story, + * @return If nothing, a Short, * or a regular video is minimized video or sliding off screen to a dismissed or hidden state. */ fun isNoneHiddenOrMinimized(): Boolean { diff --git a/dummy/src/main/java/android/support/v7/widget/RecyclerView.java b/dummy/src/main/java/android/support/v7/widget/RecyclerView.java new file mode 100644 index 00000000..d902dbfc --- /dev/null +++ b/dummy/src/main/java/android/support/v7/widget/RecyclerView.java @@ -0,0 +1,19 @@ +package android.support.v7.widget; + +import android.content.Context; +import android.view.View; + +public class RecyclerView extends View { + + public RecyclerView(Context context) { + super(context); + } + + public View getChildAt(@SuppressWarnings("unused") final int index) { + return null; + } + + public int getChildCount() { + return 0; + } +} diff --git a/dummy/src/main/java/com/facebook/litho/ComponentHost.java b/dummy/src/main/java/com/facebook/litho/ComponentHost.java new file mode 100644 index 00000000..ea2cde3f --- /dev/null +++ b/dummy/src/main/java/com/facebook/litho/ComponentHost.java @@ -0,0 +1,10 @@ +package com.facebook.litho; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; + +public final class ComponentHost extends RecyclerView { + public ComponentHost(Context context) { + super(context); + } +}