From 88d33b847de4d2ad834a4940ee257e06e3c3ad31 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 7 Feb 2026 23:45:08 +0100 Subject: [PATCH] feat: Update YouTube & YouTube Music patches (#6571) This commit is a squash of multiple commits, authored by the individuals referenced below. To see the exact commits by each author, see the unsquashed tree at https://github.com/ReVanced/revanced-patches/pull/6571 or with commit 03940665d27a42ed08992757dfe4534bd8243356. Co-authored-by: oSumAtrIX Co-authored-by: hoodles <207470673+hoo-dles@users.noreply.github.com> Co-authored-by: ILoveOpenSourceApplications <117499019+iloveopensourceapplications@users.noreply.github.com> Co-authored-by: LisoUseInAIKyrios <118716522+lisouseinaikyrios@users.noreply.github.com> Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com> Co-authored-by: OxrxL <108184954+oxrxl@users.noreply.github.com> --- .../privacy/SanitizeSharingLinksPatch.java | 2 +- .../privacy/SanitizeSharingLinksPatch.java | 2 +- .../music/patches/NavigationBarPatch.java | 34 +- .../app/revanced/extension/shared/Logger.java | 6 +- .../revanced/extension/shared/StringRef.java | 4 +- .../revanced/extension/shared/TrieSearch.java | 20 +- .../app/revanced/extension/shared/Utils.java | 28 +- .../shared/checks/CheckEnvironmentPatch.java | 4 +- .../shared/patches/CustomBrandingPatch.java | 81 ++- .../shared/patches/EnableDebuggingPatch.java | 11 +- .../patches/SanitizeSharingLinksPatch.java | 4 +- .../shared/patches/litho/CustomFilter.java | 80 ++- .../shared/patches/litho/Filter.java | 14 +- .../shared/patches/litho/FilterGroup.java | 4 +- .../patches/litho/LithoFilterPatch.java | 67 +- .../shared/privacy/LinkSanitizer.java | 18 +- .../extension/shared/requests/Requester.java | 4 +- .../shared/settings/BaseActivityHook.java | 10 +- .../shared/settings/BaseSettings.java | 7 +- .../shared/settings/BooleanSetting.java | 2 +- .../shared/settings/EnumSetting.java | 2 +- .../extension/shared/settings/Setting.java | 58 +- .../AbstractPreferenceFragment.java | 2 +- .../FeatureFlagsManagerPreference.java | 9 +- .../preference/SharedPrefCategory.java | 6 +- ...Preference.java => URLLinkPreference.java} | 18 +- .../settings/search/BaseSearchResultItem.java | 4 +- .../search/BaseSearchResultsAdapter.java | 8 +- .../spoof/requests/StreamingDataRequest.java | 4 +- .../privacy/SanitizeSharingLinksPatch.java | 2 +- .../ExtensionPreferenceCategory.java | 2 +- .../tiktok/share/ShareUrlSanitizer.java | 5 +- extensions/youtube/build.gradle.kts | 2 +- .../youtube/src/main/AndroidManifest.xml | 5 +- .../patches/AlternativeThumbnailsPatch.java | 176 +++--- .../patches/BackgroundPlaybackPatch.java | 4 +- .../BypassImageRegionRestrictionsPatch.java | 16 +- .../patches/ChangeFormFactorPatch.java | 2 +- .../youtube/patches/ChangeStartPagePatch.java | 6 +- ...eoUrlPatch.java => CopyVideoURLPatch.java} | 8 +- .../patches/DisableHapticFeedbackPatch.java | 41 +- .../DisablePlayerPopupPanelsPatch.java | 6 +- ....java => DisableSignInToTVPopupPatch.java} | 4 +- .../youtube/patches/DownloadsPatch.java | 4 +- .../youtube/patches/ExitFullscreenPatch.java | 8 +- ...tch.java => HideAutoplayPreviewPatch.java} | 6 +- .../patches/HideEndScreenCardsPatch.java | 4 +- .../HidePlayerOverlayButtonsPatch.java | 54 +- .../youtube/patches/LoopVideoPatch.java | 9 +- .../youtube/patches/MiniplayerPatch.java | 18 +- .../youtube/patches/NavigationBarPatch.java | 205 +++++++ .../patches/NavigationButtonsPatch.java | 108 ---- .../OpenShortsInRegularPlayerPatch.java | 15 +- .../RemoveViewerDiscretionDialogPatch.java | 93 ++- .../patches/ReturnYouTubeDislikePatch.java | 47 +- .../patches/SeekbarThumbnailsPatch.java | 39 -- .../youtube/patches/ShortsAutoplayPatch.java | 2 +- ...rTappingPatch.java => TapToSeekPatch.java} | 6 +- .../youtube/patches/ToolbarPatch.java | 33 + .../youtube/patches/VideoAdsPatch.java | 9 + .../youtube/patches/VideoInformation.java | 74 ++- .../youtube/patches/WideSearchbarPatch.java | 46 -- .../announcements/AnnouncementsPatch.java | 10 +- .../youtube/patches/litho/AdsFilter.java | 212 ++++--- .../litho/AdvancedVideoQualityMenuFilter.java | 2 +- .../youtube/patches/litho/ButtonsFilter.java | 154 ----- .../youtube/patches/litho/CommentsFilter.java | 13 +- .../litho/DescriptionComponentsFilter.java | 117 ++-- .../patches/litho/KeywordContentFilter.java | 16 +- .../patches/litho/LayoutComponentsFilter.java | 410 +++++++++++-- .../litho/PlaybackSpeedMenuFilter.java | 2 +- .../litho/PlayerFlyoutMenuItemsFilter.java | 66 +- .../litho/ReturnYouTubeDislikeFilter.java | 38 +- .../youtube/patches/litho/ShortsFilter.java | 173 +++++- .../litho/VideoActionButtonsFilter.java | 180 ++++++ .../quality/HidePremiumVideoQualityPatch.java | 31 + .../quality/RememberVideoQualityPatch.java | 7 +- .../youtube/patches/theme/ThemePatch.java | 25 +- .../ReturnYouTubeDislike.java | 18 +- .../requests/RYDVoteData.java | 4 +- ...eApi.java => ReturnYouTubeDislikeAPI.java} | 64 +- .../ReturnYouTubeDislikeAboutPreference.java | 6 +- ...beDislikeDebugStatsPreferenceCategory.java | 18 +- .../extension/youtube/settings/Settings.java | 78 ++- .../youtube/settings/YouTubeActivityHook.java | 1 + ...ativeThumbnailsAboutDeArrowPreference.java | 6 +- .../ExternalDownloaderPreference.java | 2 +- ...tmlPreference.java => HTMLPreference.java} | 12 +- .../youtube/shared/EngagementPanel.java | 42 ++ .../extension/youtube/{ => shared}/Event.kt | 4 +- .../youtube/shared/NavigationBar.java | 4 +- .../shared/PlayerControlsVisibility.kt | 2 +- .../PlayerControlsVisibilityObserver.kt | 2 +- .../youtube/shared/PlayerOverlays.kt | 2 +- .../extension/youtube/shared/PlayerType.kt | 8 +- .../youtube/shared/ShortsPlayerState.kt | 2 +- .../SegmentPlaybackController.java | 87 ++- .../sponsorblock/SponsorBlockSettings.java | 24 +- .../sponsorblock/SponsorBlockUtils.java | 6 +- .../objects/CategoryBehaviour.java | 2 +- .../sponsorblock/objects/UserStats.java | 20 +- .../sponsorblock/requests/SBRequester.java | 10 +- .../sponsorblock/ui/SkipSponsorButton.java | 14 +- .../ui/SponsorBlockAboutPreference.java | 6 +- .../ui/SponsorBlockPreferenceGroup.java | 14 +- .../SponsorBlockStatsPreferenceCategory.java | 6 +- .../ui/SponsorBlockViewController.java | 130 +++- .../controller/ScreenBrightnessController.kt | 2 +- .../controller/VolumeKeysController.kt | 2 +- .../misc/SwipeControlsOverlay.kt | 2 +- ...UrlButton.java => CopyVideoURLButton.java} | 8 +- ....java => CopyVideoURLTimestampButton.java} | 8 +- .../youtube/videoplayer/LoopVideoButton.java | 76 ++- .../videoplayer/PlayerControlButton.java | 36 +- .../videoplayer/VideoQualityDialogButton.java | 25 +- .../innertube/model/media/VideoQuality.java | 8 - patches/api/patches.api | 146 +++-- .../all/misc/transformation/MethodCall.kt | 19 +- .../patches/music/ad/video/HideVideoAds.kt | 5 +- .../EnableExclusiveAudioPlayback.kt | 2 + .../permanentrepeat/PermanentRepeatPatch.kt | 5 +- .../layout/branding/CustomBrandingPatch.kt | 8 +- .../music/layout/branding/Fingerprints.kt | 2 +- .../music/layout/buttons/Fingerprints.kt | 4 +- .../music/layout/buttons/HideButtons.kt | 13 +- .../layout/compactheader/HideCategoryBar.kt | 8 +- .../hide/general/HideLayoutComponentsPatch.kt | 9 +- .../miniplayercolor/ChangeMiniplayerColor.kt | 14 +- .../layout/miniplayercolor/Fingerprints.kt | 11 +- .../layout/navigationbar/Fingerprints.kt | 57 +- .../navigationbar/NavigationBarPatch.kt | 76 +-- .../layout/premium/HideGetPremiumPatch.kt | 7 +- .../patches/music/layout/theme/ThemePatch.kt | 18 +- .../UnlockAndroidAutoMediaBrowserPatch.kt | 20 +- .../ForceOriginalAudioPatch.kt | 6 +- .../BackgroundPlaybackPatch.kt | 2 + .../misc/debugging/EnableDebuggingPatch.kt | 28 +- ...ckWatchHistoryDomainNameResolutionPatch.kt | 4 +- .../music/misc/gms/GmsCoreSupportPatch.kt | 2 + .../misc/privacy/SanitizeSharingLinksPatch.kt | 4 +- .../music/misc/settings/SettingsPatch.kt | 24 +- .../misc/spoof/SpoofVideoStreamsPatch.kt | 4 +- .../music/playservice/VersionCheckPatch.kt | 7 +- .../revanced/patches/shared/Fingerprints.kt | 18 + .../layout/branding/AddBrandLicensePatch.kt | 6 +- .../branding/BaseCustomBrandingPatch.kt | 42 +- .../shared/layout/branding/Fingerprints.kt | 19 +- .../shared/layout/theme/BaseThemePatch.kt | 27 +- .../shared/layout/theme/Fingerprints.kt | 8 +- .../misc/audio/ForceOriginalAudioPatch.kt | 91 ++- .../misc/debugging/EnableDebuggingPatch.kt | 197 ++++-- .../shared/misc/debugging/Fingerprints.kt | 33 +- .../patches/shared/misc/gms/Fingerprints.kt | 1 + .../shared/misc/gms/GmsCoreSupportPatch.kt | 3 + .../shared/misc/hex/HexPatchBuilder.kt | 9 +- .../shared/misc/litho/filter/Fingerprints.kt | 29 +- .../misc/litho/filter/LithoFilterPatch.kt | 102 +++- .../shared/misc/settings/Fingerprints.kt | 14 +- .../shared/misc/settings/SettingsPatch.kt | 11 +- .../misc/spoof/SpoofVideoStreamsPatch.kt | 16 +- .../youtube/ad/general/Fingerprints.kt | 73 ++- .../youtube/ad/general/HideAdsPatch.kt | 148 ++++- .../youtube/ad/getpremium/Fingerprints.kt | 20 - .../ad/getpremium/HideGetPremiumPatch.kt | 66 -- .../patches/youtube/ad/video/VideoAdsPatch.kt | 6 +- ...yVideoUrlPatch.kt => CopyVideoURLPatch.kt} | 16 +- .../interaction/dialog/Fingerprints.kt | 27 + .../RemoveViewerDiscretionDialogPatch.kt | 68 ++- ...ddMoreDoubleTapToSeekLengthOptionsPatch.kt | 66 ++ .../DisableChapterSkipDoubleTapPatch.kt | 14 +- .../interaction/downloads/DownloadsPatch.kt | 14 +- .../interaction/downloads/Fingerprints.kt | 2 +- .../DisableHapticFeedbackPatch.kt | 139 +++++ .../hapticfeedback/Fingerprints.kt | 56 ++ .../seekbar/EnableSlideToSeekPatch.kt | 2 +- ...appingPatch.kt => EnableTapToSeekPatch.kt} | 18 +- .../interaction/seekbar/Fingerprints.kt | 33 +- .../interaction/seekbar/SeekbarPatch.kt | 14 +- .../seekbar/SeekbarThumbnailsPatch.kt | 71 --- .../swipecontrols/SwipeControlsPatch.kt | 11 +- .../layout/autocaptions/AutoCaptionsPatch.kt | 9 +- .../layout/autocaptions/Fingerprints.kt | 15 - .../layout/branding/CustomBrandingPatch.kt | 6 +- .../branding/header/ChangeHeaderPatch.kt | 9 +- ...tch.kt => HideVideoActionsButtonsPatch.kt} | 28 +- .../layout/buttons/navigation/Fingerprints.kt | 48 +- .../buttons/navigation/NavigationBarPatch.kt | 253 ++++++++ .../navigation/NavigationButtonsPatch.kt | 150 ----- .../layout/buttons/overlay/Fingerprints.kt | 21 + .../overlay/HidePlayerOverlayButtonsPatch.kt | 77 ++- .../formfactor/ChangeFormFactorPatch.kt | 13 +- .../HideAutoplayPreviewPatch.kt | 74 +++ .../hide/endscreencards/Fingerprints.kt | 26 +- .../endscreencards/HideEndScreenCardsPatch.kt | 11 +- .../Fingerprints.kt | 2 +- .../HideEndScreenSuggestedVideoPatch.kt | 23 +- .../DisableFullscreenAmbientModePatch.kt | 11 +- .../layout/hide/general/Fingerprints.kt | 211 ++++++- .../hide/general/HideLayoutComponentsPatch.kt | 403 ++++++++++-- .../hide/infocards/HideInfoCardsPatch.kt | 17 +- .../HidePlayerFlyoutMenuPatch.kt | 27 +- .../player}/popup/PlayerPopupPanelsPatch.kt | 14 +- .../HideRelatedVideoOverlayPatch.kt | 6 +- .../DisableRollingNumberAnimationPatch.kt | 6 +- .../layout/hide/shorts/Fingerprints.kt | 32 +- .../hide/shorts/HideShortsComponentsPatch.kt | 202 +++--- ...opup.kt => DisableSignInToTVPopupPatch.kt} | 16 +- .../hide/signintotvpopup/Fingerprints.kt | 2 +- .../layout/hide/time/HideTimestampPatch.kt | 9 +- .../youtube/layout/miniplayer/Fingerprints.kt | 20 +- .../layout/miniplayer/MiniplayerPatch.kt | 72 ++- .../layout/panels/popup/Fingerprints.kt | 12 - .../player/fullscreen/ExitFullscreenPatch.kt | 24 +- .../OpenVideosFullscreenHookPatch.kt | 2 +- .../fullscreen/OpenVideosFullscreenPatch.kt | 7 +- .../CustomPlayerOverlayOpacityPatch.kt | 11 +- .../returnyoutubedislike/Fingerprints.kt | 44 +- .../ReturnYouTubeDislikePatch.kt | 194 +++--- .../youtube/layout/searchbar/Fingerprints.kt | 34 -- .../layout/searchbar/WideSearchbarPatch.kt | 105 ---- .../youtube/layout/seekbar/Fingerprints.kt | 2 +- .../layout/seekbar/SeekbarColorPatch.kt | 19 +- .../layout/shortsautoplay/Fingerprints.kt | 1 - .../shortsautoplay/ShortsAutoplayPatch.kt | 25 +- .../layout/shortsplayer/Fingerprints.kt | 71 +-- .../OpenShortsInRegularPlayerPatch.kt | 169 +++-- .../layout/sponsorblock/SponsorBlockPatch.kt | 26 +- .../layout/spoofappversion/Fingerprints.kt | 15 - .../spoofappversion/SpoofAppVersionPatch.kt | 16 +- .../layout/startpage/ChangeStartPagePatch.kt | 12 +- .../youtube/layout/startpage/Fingerprints.kt | 2 +- .../DisableResumingShortsOnStartupPatch.kt | 19 +- .../youtube/layout/theme/Fingerprints.kt | 54 ++ .../youtube/layout/theme/ThemePatch.kt | 77 ++- .../thumbnails/AlternativeThumbnailsPatch.kt | 22 +- .../BypassImageRegionRestrictionsPatch.kt | 18 +- .../youtube/layout/toolbar/Fingerprints.kt | 14 + .../layout/toolbar/ToolbarHookPatch.kt | 74 +++ .../misc/announcements/AnnouncementsPatch.kt | 6 +- .../audiofocus/PauseOnAudioInterruptPatch.kt | 5 + .../BackgroundPlaybackPatch.kt | 19 +- .../misc/backgroundplayback/Fingerprints.kt | 11 +- .../contexthook/ClientContextHookPatch.kt | 177 ++++++ .../youtube/misc/contexthook/Fingerprints.kt | 153 +++++ .../misc/debugging/EnableDebuggingPatch.kt | 34 +- .../spoof/SpoofDeviceDimensionsPatch.kt | 29 +- ...ckWatchHistoryDomainNameResolutionPatch.kt | 6 +- .../engagement/EngagementPanelHookPatch.kt | 80 +++ .../youtube/misc/engagement/Fingerprints.kt | 22 + .../gms/AccountCredentialsInvalidTextPatch.kt | 4 +- .../patches/youtube/misc/gms/Fingerprints.kt | 4 +- .../youtube/misc/gms/GmsCoreSupportPatch.kt | 6 +- .../DisableHapticFeedbackPatch.kt | 69 --- .../misc/hapticfeedback/Fingerprints.kt | 28 - .../misc/imageurlhook/CronetImageUrlHook.kt | 22 +- .../youtube/misc/imageurlhook/Fingerprints.kt | 4 +- .../misc/links/BypassURLRedirectsPatch.kt | 31 +- .../youtube/misc/links/Fingerprints.kt | 12 - .../misc/links/OpenLinksExternallyPatch.kt | 6 +- .../youtube/misc/litho/filter/Fingerprints.kt | 1 - .../misc/litho/filter/LithoFilterPatch.kt | 2 +- .../youtube/misc/loopvideo/Fingerprints.kt | 18 - .../youtube/misc/loopvideo/LoopVideoPatch.kt | 44 +- .../youtube/misc/navigation/Fingerprints.kt | 12 +- .../misc/navigation/NavigationBarHookPatch.kt | 10 +- .../playercontrols/PlayerControlsPatch.kt | 4 +- .../misc/playservice/VersionCheckPatch.kt | 141 +++-- .../misc/privacy/SanitizeSharingLinksPatch.kt | 6 +- .../youtube/misc/settings/Fingerprints.kt | 11 - .../youtube/misc/settings/SettingsPatch.kt | 3 +- .../misc/spoof/SpoofVideoStreamsPatch.kt | 6 +- .../patches/youtube/shared/Fingerprints.kt | 70 ++- .../video/audio/ForceOriginalAudioPatch.kt | 6 +- .../video/codecs/DisableVideoCodecsPatch.kt | 9 +- .../youtube/video/information/Fingerprints.kt | 105 +++- .../information/VideoInformationPatch.kt | 105 +++- .../video/playerresponse/Fingerprints.kt | 14 +- .../youtube/video/quality/Fingerprints.kt | 46 ++ .../quality/HidePremiumVideoQualityPatch.kt | 97 +++ .../quality/RememberVideoQualityPatch.kt | 2 +- .../video/quality/VideoQualityPatch.kt | 9 +- .../youtube/video/speed/PlaybackSpeedPatch.kt | 10 +- .../speed/custom/CustomPlaybackSpeedPatch.kt | 26 +- .../video/speed/custom/Fingerprints.kt | 33 +- .../remember/RememberPlaybackSpeedPatch.kt | 5 +- .../youtube/video/videoid/Fingerprints.kt | 16 +- .../youtube/video/videoid/VideoIdPatch.kt | 22 +- .../kotlin/app/revanced/util/BytecodeUtils.kt | 426 ++++++------- .../app/revanced/util/FreeRegisterProvider.kt | 575 ++++++++++++++++++ .../kotlin/app/revanced/util/ResourceUtils.kt | 16 +- .../revanced/util/resource/StringResource.kt | 1 - .../addresources/values-bs-rBA/strings.xml | 2 +- .../resources/addresources/values/arrays.xml | 6 +- .../resources/addresources/values/strings.xml | 241 +++++--- .../drawable/revanced_yt_copy.xml | 85 ++- .../drawable/revanced_yt_copy_timestamp.xml | 91 ++- .../drawable/revanced_yt_download_button.xml | 85 ++- ..._video_quality_dialog_button_rectangle.xml | 54 +- ...playback_speed_dialog_button_rectangle.xml | 54 +- settings.gradle.kts | 6 + 300 files changed, 8226 insertions(+), 3643 deletions(-) rename extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/{UrlLinkPreference.java => URLLinkPreference.java} (65%) rename extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/{CopyVideoUrlPatch.java => CopyVideoURLPatch.java} (93%) rename extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/{DisableSignInToTvPopupPatch.java => DisableSignInToTVPopupPatch.java} (69%) rename extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/{HideGetPremiumPatch.java => HideAutoplayPreviewPatch.java} (55%) create mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/NavigationBarPatch.java delete mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/NavigationButtonsPatch.java delete mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/SeekbarThumbnailsPatch.java rename extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/{SeekbarTappingPatch.java => TapToSeekPatch.java} (50%) create mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ToolbarPatch.java delete mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java delete mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ButtonsFilter.java create mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/VideoActionButtonsFilter.java create mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/HidePremiumVideoQualityPatch.java rename extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/{ReturnYouTubeDislikeApi.java => ReturnYouTubeDislikeAPI.java} (95%) rename extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/{HtmlPreference.java => HTMLPreference.java} (65%) create mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/EngagementPanel.java rename extensions/youtube/src/main/java/app/revanced/extension/youtube/{ => shared}/Event.kt (94%) rename extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/{CopyVideoUrlButton.java => CopyVideoURLButton.java} (86%) rename extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/{CopyVideoUrlTimestampButton.java => CopyVideoURLTimestampButton.java} (86%) delete mode 100644 extensions/youtube/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/VideoQuality.java rename patches/src/main/kotlin/app/revanced/patches/music/misc/{tracks => audio}/ForceOriginalAudioPatch.kt (89%) delete mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/Fingerprints.kt delete mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatch.kt rename patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/{CopyVideoUrlPatch.kt => CopyVideoURLPatch.kt} (86%) create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/AddMoreDoubleTapToSeekLengthOptionsPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/interaction/hapticfeedback/Fingerprints.kt rename patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/{EnableSeekbarTappingPatch.kt => EnableTapToSeekPatch.kt} (85%) delete mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarThumbnailsPatch.kt rename patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/{HideButtonsPatch.kt => HideVideoActionsButtonsPatch.kt} (76%) create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt delete mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatch.kt rename patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/{endscreensuggestion => endscreensuggestedvideo}/Fingerprints.kt (99%) rename patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/{endscreensuggestion => endscreensuggestedvideo}/HideEndScreenSuggestedVideoPatch.kt (81%) rename patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/{flyoutmenupanel => flyoutmenu}/HidePlayerFlyoutMenuPatch.kt (90%) rename patches/src/main/kotlin/app/revanced/patches/youtube/layout/{panels => hide/player}/popup/PlayerPopupPanelsPatch.kt (78%) rename patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/{DisableSignInToTvPatchPopup.kt => DisableSignInToTVPopupPatch.kt} (83%) delete mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/Fingerprints.kt delete mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/Fingerprints.kt delete mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/WideSearchbarPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/layout/toolbar/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/layout/toolbar/ToolbarHookPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/misc/engagement/EngagementPanelHookPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/misc/engagement/Fingerprints.kt delete mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/misc/hapticfeedback/DisableHapticFeedbackPatch.kt delete mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/misc/hapticfeedback/Fingerprints.kt delete mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/misc/loopvideo/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/HidePremiumVideoQualityPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/util/FreeRegisterProvider.kt diff --git a/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/privacy/SanitizeSharingLinksPatch.java b/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/privacy/SanitizeSharingLinksPatch.java index b7a017e77f..058ee19f90 100644 --- a/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/privacy/SanitizeSharingLinksPatch.java +++ b/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/privacy/SanitizeSharingLinksPatch.java @@ -10,6 +10,6 @@ public final class SanitizeSharingLinksPatch { * Injection point. */ public static String sanitizeSharingLink(String url) { - return sanitizer.sanitizeUrlString(url); + return sanitizer.sanitizeURLString(url); } } diff --git a/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/share/privacy/SanitizeSharingLinksPatch.java b/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/share/privacy/SanitizeSharingLinksPatch.java index 9b3aa6347a..0566f68acb 100644 --- a/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/share/privacy/SanitizeSharingLinksPatch.java +++ b/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/share/privacy/SanitizeSharingLinksPatch.java @@ -10,6 +10,6 @@ public final class SanitizeSharingLinksPatch { * Injection point. */ public static String sanitizeSharingLink(String url) { - return sanitizer.sanitizeUrlString(url); + return sanitizer.sanitizeURLString(url); } } diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/NavigationBarPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/NavigationBarPatch.java index 511e503044..6131401ce2 100644 --- a/extensions/music/src/main/java/app/revanced/extension/music/patches/NavigationBarPatch.java +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/NavigationBarPatch.java @@ -5,14 +5,15 @@ import static app.revanced.extension.shared.Utils.hideViewUnderCondition; import android.view.View; import android.widget.TextView; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Arrays; +import java.util.List; + import app.revanced.extension.music.settings.Settings; @SuppressWarnings("unused") public class NavigationBarPatch { - @NonNull private static String lastYTNavigationEnumName = ""; public static void setLastAppNavigationEnum(@Nullable Enum ytNavigationEnumName) { @@ -25,7 +26,7 @@ public class NavigationBarPatch { hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR_LABEL.get(), textview); } - public static void hideNavigationButton(@NonNull View view) { + public static void hideNavigationButton(View view) { // Hide entire navigation bar. if (Settings.HIDE_NAVIGATION_BAR.get() && view.getParent() != null) { hideViewUnderCondition(true, (View) view.getParent()); @@ -34,7 +35,7 @@ public class NavigationBarPatch { // Hide navigation buttons based on their type. for (NavigationButton button : NavigationButton.values()) { - if (button.ytEnumNames.equals(lastYTNavigationEnumName)) { + if (button.ytEnumNames.contains(lastYTNavigationEnumName)) { hideViewUnderCondition(button.hidden, view); break; } @@ -43,30 +44,41 @@ public class NavigationBarPatch { private enum NavigationButton { HOME( - "TAB_HOME", + Arrays.asList( + "TAB_HOME" + ), Settings.HIDE_NAVIGATION_BAR_HOME_BUTTON.get() ), SAMPLES( - "TAB_SAMPLES", + Arrays.asList( + "TAB_SAMPLES" + ), Settings.HIDE_NAVIGATION_BAR_SAMPLES_BUTTON.get() ), EXPLORE( - "TAB_EXPLORE", + Arrays.asList( + "TAB_EXPLORE" + ), Settings.HIDE_NAVIGATION_BAR_EXPLORE_BUTTON.get() ), LIBRARY( - "LIBRARY_MUSIC", + Arrays.asList( + "LIBRARY_MUSIC", + "TAB_BOOKMARK" // YouTube Music 8.24+ + ), Settings.HIDE_NAVIGATION_BAR_LIBRARY_BUTTON.get() ), UPGRADE( - "TAB_MUSIC_PREMIUM", + Arrays.asList( + "TAB_MUSIC_PREMIUM" + ), Settings.HIDE_NAVIGATION_BAR_UPGRADE_BUTTON.get() ); - private final String ytEnumNames; + private final List ytEnumNames; private final boolean hidden; - NavigationButton(@NonNull String ytEnumNames, boolean hidden) { + NavigationButton(List ytEnumNames, boolean hidden) { this.ytEnumNames = ytEnumNames; this.hidden = hidden; } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java index 47f6da3e3f..610cd3414f 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java @@ -16,8 +16,8 @@ import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.preference.LogBufferManager; /** - * ReVanced specific logger. Logging is done to standard device log (accessible thru ADB), - * and additionally accessible thru {@link LogBufferManager}. + * ReVanced specific logger. Logging is done to standard device log (accessible through ADB), + * and additionally accessible through {@link LogBufferManager}. * * All methods are thread safe, and are safe to call even * if {@link Utils#getContext()} is not available. @@ -202,7 +202,7 @@ public class Logger { /** * Logs exceptions under the outer class name of the code calling this method. *

- * If the calling code is showing it's own error toast, + * If the calling code is showing its own error toast, * instead use {@link #printInfo(LogMessage, Exception)} * * @param message log message diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java index 4390137de7..c1c2c90d14 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java @@ -70,7 +70,7 @@ public class StringRef { } /** - * Creates a StringRef object that'll not change it's value + * Creates a StringRef object that'll not change its value * * @param value value which toString() method returns when invoked on returned object * @return Unique StringRef instance, its value will never change @@ -102,7 +102,7 @@ public class StringRef { public String toString() { if (!resolved) { if (resources == null || packageName == null) { - Context context = Utils.getContext(); + var context = Utils.getContext(); resources = context.getResources(); packageName = context.getPackageName(); } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java index 7628a51d9b..97fa4605d8 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java @@ -106,14 +106,18 @@ public abstract class TrieSearch { * Elements not contained can collide with elements the array does contain, * so must compare the nodes character value. * - * Alternatively this array could be a sorted and densely packed array, - * and lookup is done using binary search. - * That would save a small amount of memory because there's no null children entries, - * but would give a worst case search of O(nlog(m)) where n is the number of - * characters in the searched text and m is the maximum size of the sorted character arrays. - * Using a hash table array always gives O(n) search time. - * The memory usage here is very small (all Litho filters use ~10KB of memory), - * so the more performant hash implementation is chosen. + /* + * Alternatively, this could be implemented as a sorted, densely packed array + * with lookups performed via binary search. + * This approach would save a small amount of memory by eliminating null + * child entries. However, it would result in a worst-case lookup time of + * O(n log m), where: + * - n is the number of characters in the input text, and + * - m is the maximum size of the sorted character arrays. + * In contrast, using a hash-based array guarantees O(n) lookup time. + * Given that the total memory usage is already very small (all Litho filters + * together use approximately 10KB), the hash-based implementation is preferred + * for its superior performance. */ @Nullable private TrieNode[] children; diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java index 0093317082..e978301a65 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java @@ -206,7 +206,7 @@ public class Utils { } /** - * Hide a view by setting its visibility to GONE. + * Hide a view by setting its visibility as GONE. * * @param setting The setting to check for hiding the view. * @param view The view to hide. @@ -218,7 +218,7 @@ public class Utils { } /** - * Hide a view by setting its visibility to GONE. + * Hide a view by setting its visibility as GONE. * * @param condition The setting to check for hiding the view. * @param view The view to hide. @@ -288,7 +288,7 @@ public class Utils { // Could do a thread sleep, but that will trigger an exception if the thread is interrupted. meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random())); } - // Return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work, + // Return the value, otherwise the compiler or VM might optimize and remove the meaningless time-wasting work, // leaving an empty loop that hammers on the System.currentTimeMillis native call. return meaninglessValue; } @@ -298,10 +298,12 @@ public class Utils { } public static int indexOfFirstFound(String value, String... targets) { - for (String string : targets) { - if (!string.isEmpty()) { - final int indexOf = value.indexOf(string); - if (indexOf >= 0) return indexOf; + if (isNotEmpty(value)) { + for (String string : targets) { + if (!string.isEmpty()) { + final int indexOf = value.indexOf(string); + if (indexOf >= 0) return indexOf; + } } } return -1; @@ -473,6 +475,10 @@ public class Utils { clipboard.setPrimaryClip(clip); } + public static boolean isNotEmpty(@Nullable String str) { + return str != null && !str.isEmpty(); + } + public static boolean isTablet() { return context.getResources().getConfiguration().smallestScreenWidthDp >= 600; } @@ -481,7 +487,7 @@ public class Utils { private static Boolean isRightToLeftTextLayout; /** - * @return If the device language uses right to left text layout (Hebrew, Arabic, etc). + * @return If the device language uses right to left text layout (Hebrew, Arabic, etc.). * If this should match any ReVanced language override then instead use * {@link #isRightToLeftLocale(Locale)} with {@link BaseSettings#REVANCED_LANGUAGE}. * This is the default locale of the device, which may differ if @@ -495,7 +501,7 @@ public class Utils { } /** - * @return If the locale uses right to left text layout (Hebrew, Arabic, etc). + * @return If the locale uses right to left text layout (Hebrew, Arabic, etc.). */ public static boolean isRightToLeftLocale(Locale locale) { String displayLanguage = locale.getDisplayLanguage(); @@ -524,7 +530,7 @@ public class Utils { /** * @return if the text contains at least 1 number character, - * including any unicode numbers such as Arabic. + * including any Unicode numbers such as Arabic. */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") public static boolean containsNumber(CharSequence text) { @@ -1156,7 +1162,7 @@ public class Utils { } /** - * Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active. + * Uses {@link #adjustColorBrightness(int, float)} depending on if light or dark mode is active. */ @ColorInt public static int adjustColorBrightness(@ColorInt int baseColor, float lightThemeFactor, float darkThemeFactor) { diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java index d63f8b7e3f..4eb4c23c48 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java @@ -288,8 +288,8 @@ public final class CheckEnvironmentPatch { CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime(); Boolean timeCheckPassed = nearPatchTime.check(); if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { - // Allow installing recently patched apks, - // even if the install source is not Manager or ADB. + // Allow installing recently patched APKs, + // even if the installation source is not Manager or ADB. Check.disableForever(); return; } else { diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java index 2b81178eac..0dc411f8b0 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java @@ -7,6 +7,8 @@ import android.content.pm.PackageManager; import android.graphics.Color; import android.view.View; +import androidx.annotation.Nullable; + import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -53,24 +55,35 @@ public class CustomBrandingPatch { } } - private static final int notificationSmallIcon; + @Nullable + private static Integer notificationSmallIcon; - static { - BrandingTheme branding = BaseSettings.CUSTOM_BRANDING_ICON.get(); - if (branding == BrandingTheme.ORIGINAL) { - notificationSmallIcon = 0; - } else { - // Original icon is quantum_ic_video_youtube_white_24 - String iconName = "revanced_notification_icon"; - if (branding == BrandingTheme.CUSTOM) { - iconName += "_custom"; + private static int getNotificationSmallIcon() { + // Cannot use static initialization block otherwise cyclic references exist + // between Settings initialization and this class. + if (notificationSmallIcon == null) { + if (GmsCoreSupport.isPackageNameOriginal()) { + Logger.printDebug(() -> "App is root mounted. Not overriding small notification icon"); + return notificationSmallIcon = 0; } - notificationSmallIcon = Utils.getResourceIdentifier(ResourceType.DRAWABLE, iconName); - if (notificationSmallIcon == 0) { - Logger.printException(() -> "Could not load notification small icon"); + BrandingTheme branding = BaseSettings.CUSTOM_BRANDING_ICON.get(); + if (branding == BrandingTheme.ORIGINAL) { + notificationSmallIcon = 0; + } else { + // Original icon is quantum_ic_video_youtube_white_24 + String iconName = "revanced_notification_icon"; + if (branding == BrandingTheme.CUSTOM) { + iconName += "_custom"; + } + + notificationSmallIcon = Utils.getResourceIdentifier(ResourceType.DRAWABLE, iconName); + if (notificationSmallIcon == 0) { + Logger.printException(() -> "Could not load notification small icon"); + } } } + return notificationSmallIcon; } /** @@ -89,8 +102,9 @@ public class CustomBrandingPatch { */ public static void setNotificationIcon(Notification.Builder builder) { try { - if (notificationSmallIcon != 0) { - builder.setSmallIcon(notificationSmallIcon) + final int smallIcon = getNotificationSmallIcon(); + if (smallIcon != 0) { + builder.setSmallIcon(smallIcon) .setColor(Color.TRANSPARENT); // Remove YT red tint. } } catch (Exception ex) { @@ -104,8 +118,41 @@ public class CustomBrandingPatch { * The total number of app name aliases, including dummy aliases. */ private static int numberOfPresetAppNames() { - // Modified during patching. - throw new IllegalStateException(); + // Modified during patching, but requires a default if custom branding is excluded. + return 1; + } + + + /** + * Injection point. + *

+ * If a custom icon was provided during patching. + */ + private static boolean userProvidedCustomIcon() { + // Modified during patching, but requires a default if custom branding is excluded. + return false; + } + + /** + * Injection point. + *

+ * If a custom name was provided during patching. + */ + private static boolean userProvidedCustomName() { + // Modified during patching, but requires a default if custom branding is excluded.. + return false; + } + + public static int getDefaultAppNameIndex() { + return userProvidedCustomName() + ? numberOfPresetAppNames() + : 1; + } + + public static BrandingTheme getDefaultIconStyle() { + return userProvidedCustomIcon() + ? BrandingTheme.CUSTOM + : BrandingTheme.ORIGINAL; } /** diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/EnableDebuggingPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/EnableDebuggingPatch.java index e0551ed045..b63f2c6049 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/EnableDebuggingPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/EnableDebuggingPatch.java @@ -41,12 +41,13 @@ public final class EnableDebuggingPatch { /** * Injection point. */ - public static boolean isBooleanFeatureFlagEnabled(boolean value, Long flag) { + public static boolean isBooleanFeatureFlagEnabled(boolean value, long flag) { if (LOG_FEATURE_FLAGS && value) { - if (DISABLED_FEATURE_FLAGS.contains(flag)) { + Long flagObj = flag; + if (DISABLED_FEATURE_FLAGS.contains(flagObj)) { return false; } - if (featureFlags.putIfAbsent(flag, TRUE) == null) { + if (featureFlags.putIfAbsent(flagObj, TRUE) == null) { Logger.printDebug(() -> "boolean feature is enabled: " + flag); } } @@ -59,6 +60,8 @@ public final class EnableDebuggingPatch { */ public static double isDoubleFeatureFlagEnabled(double value, long flag, double defaultValue) { if (LOG_FEATURE_FLAGS && defaultValue != value) { + if (DISABLED_FEATURE_FLAGS.contains(flag)) return defaultValue; + if (featureFlags.putIfAbsent(flag, true) == null) { // Align the log outputs to make post processing easier. Logger.printDebug(() -> " double feature is enabled: " + flag @@ -74,6 +77,8 @@ public final class EnableDebuggingPatch { */ public static long isLongFeatureFlagEnabled(long value, long flag, long defaultValue) { if (LOG_FEATURE_FLAGS && defaultValue != value) { + if (DISABLED_FEATURE_FLAGS.contains(flag)) return defaultValue; + if (featureFlags.putIfAbsent(flag, true) == null) { Logger.printDebug(() -> " long feature is enabled: " + flag + " value: " + value + (defaultValue == 0 ? "" : " default: " + defaultValue)); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java index d028997276..b0bcbc6f04 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java @@ -18,8 +18,8 @@ public final class SanitizeSharingLinksPatch { * Injection point. */ public static String sanitize(String url) { - if (BaseSettings.SANITIZE_SHARED_LINKS.get()) { - url = sanitizer.sanitizeUrlString(url); + if (BaseSettings.SANITIZE_SHARING_LINKS.get()) { + url = sanitizer.sanitizeURLString(url); } if (BaseSettings.REPLACE_MUSIC_LINKS_WITH_YOUTUBE.get()) { diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java index c82c28353a..beb623a799 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java @@ -13,6 +13,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.StringTrieSearch; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.ByteTrieSearch; import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; @@ -24,7 +25,7 @@ import app.revanced.extension.shared.settings.YouTubeAndMusicSettings; @SuppressWarnings("unused") public final class CustomFilter extends Filter { - private static void showInvalidSyntaxToast(@NonNull String expression) { + private static void showInvalidSyntaxToast(String expression) { Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression)); } @@ -36,7 +37,12 @@ public final class CustomFilter extends Filter { public static final String SYNTAX_STARTS_WITH = "^"; /** - * Optional character that separates the path from a proto buffer string pattern. + * Optional character that separates the path from an accessibility string pattern. + */ + public static final String SYNTAX_ACCESSIBILITY_SYMBOL = "#"; + + /** + * Optional character that separates the path/accessibility from a proto buffer string pattern. */ public static final String SYNTAX_BUFFER_SYMBOL = "$"; @@ -51,15 +57,21 @@ public final class CustomFilter extends Filter { return Collections.emptyList(); } - // Map key is the path including optional special characters (^ and/or $) + // Map key is the full path including optional special characters (^, #, $), + // and any accessibility pattern, but does not contain any buffer patterns. Map result = new HashMap<>(); + Pattern pattern = Pattern.compile( - "(" // map key group - + "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" // optional starts with - + "([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*)" // path - + "(\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E?)" // optional buffer symbol - + ")" // end map key group - + "(.*)"); // optional buffer string + "(" // Map key group. + // Optional starts with. + + "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" + // Path string. + + "([^\\Q" + SYNTAX_ACCESSIBILITY_SYMBOL + SYNTAX_BUFFER_SYMBOL + "\\E]*)" + // Optional accessibility string. + + "(?:\\Q" + SYNTAX_ACCESSIBILITY_SYMBOL + "\\E([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*))?" + // Optional buffer string. + + "(?:\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E(.*))?" + + ")"); // end map key group for (String expression : rawCustomFilterText.split("\n")) { if (expression.isBlank()) continue; @@ -73,10 +85,12 @@ public final class CustomFilter extends Filter { final String mapKey = matcher.group(1); final boolean pathStartsWith = !matcher.group(2).isEmpty(); final String path = matcher.group(3); - final boolean hasBufferSymbol = !matcher.group(4).isEmpty(); - final String bufferString = matcher.group(5); + final String accessibility = matcher.group(4); // null if not present + final String buffer = matcher.group(5); // null if not present - if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) { + if (path.isBlank() + || (accessibility != null && accessibility.isEmpty()) + || (buffer != null && buffer.isEmpty())) { showInvalidSyntaxToast(expression); continue; } @@ -89,8 +103,13 @@ public final class CustomFilter extends Filter { group = new CustomFilterGroup(pathStartsWith, path); result.put(mapKey, group); } - if (hasBufferSymbol) { - group.addBufferString(bufferString); + + if (accessibility != null) { + group.addAccessibilityString(accessibility); + } + + if (buffer != null) { + group.addBufferString(buffer); } } @@ -98,14 +117,22 @@ public final class CustomFilter extends Filter { } final boolean startsWith; + StringTrieSearch accessibilitySearch; ByteTrieSearch bufferSearch; - CustomFilterGroup(boolean startsWith, @NonNull String path) { + CustomFilterGroup(boolean startsWith, String path) { super(YouTubeAndMusicSettings.CUSTOM_FILTER, path); this.startsWith = startsWith; } - void addBufferString(@NonNull String bufferString) { + void addAccessibilityString(String accessibilityString) { + if (accessibilitySearch == null) { + accessibilitySearch = new StringTrieSearch(); + } + accessibilitySearch.addPattern(accessibilityString); + } + + void addBufferString(String bufferString) { if (bufferSearch == null) { bufferSearch = new ByteTrieSearch(); } @@ -117,6 +144,11 @@ public final class CustomFilter extends Filter { public String toString() { StringBuilder builder = new StringBuilder(); builder.append("CustomFilterGroup{"); + if (accessibilitySearch != null) { + builder.append(", accessibility="); + builder.append(accessibilitySearch.getPatterns()); + } + builder.append("path="); if (startsWith) builder.append(SYNTAX_STARTS_WITH); builder.append(filters[0]); @@ -146,18 +178,26 @@ public final class CustomFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String path, byte[] buffer, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { // All callbacks are custom filter groups. CustomFilterGroup custom = (CustomFilterGroup) matchedGroup; + + // Check path start requirement. if (custom.startsWith && contentIndex != 0) { return false; } - if (custom.bufferSearch == null) { - return true; // No buffer filter, only path filtering. + // Check accessibility string if specified. + if (custom.accessibilitySearch != null && !custom.accessibilitySearch.matches(accessibility)) { + return false; } - return custom.bufferSearch.matches(buffer); + // Check buffer if specified. + if (custom.bufferSearch != null && !custom.bufferSearch.matches(buffer)) { + return false; + } + + return true; // All custom filter conditions passed. } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java index ccd152262b..b34ca9bdd7 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java @@ -13,9 +13,9 @@ import java.util.List; * Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)} * and {@link #addPathCallbacks(StringFilterGroup...)}. * - * To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to + * To filter {@link FilterContentType#PROTOBUFFER} or {@link FilterContentType#ACCESSIBILITY}, first add a callback to * either an identifier or a path. - * Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} + * Then inside {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} * search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern) * or a {@link FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern). * @@ -26,6 +26,7 @@ public abstract class Filter { public enum FilterContentType { IDENTIFIER, PATH, + ACCESSIBILITY, PROTOBUFFER } @@ -41,7 +42,7 @@ public abstract class Filter { public final List pathCallbacks = new ArrayList<>(); /** - * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} + * Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} * if any of the groups are found. */ protected final void addIdentifierCallbacks(StringFilterGroup... groups) { @@ -49,7 +50,7 @@ public abstract class Filter { } /** - * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} + * Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} * if any of the groups are found. */ protected final void addPathCallbacks(StringFilterGroup... groups) { @@ -63,12 +64,15 @@ public abstract class Filter { *

* Method is called off the main thread. * + * @param identifier Litho identifier. + * @param accessibility Accessibility string, or an empty string if not present for the component. + * @param buffer Protocol buffer. * @param matchedGroup The actual filter that matched. * @param contentType The type of content matched. * @param contentIndex Matched index of the identifier or path. * @return True if the litho component should be filtered out. */ - public boolean isFiltered(String identifier, String path, byte[] buffer, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { return true; } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroup.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroup.java index 5b75858f3d..212787f305 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroup.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroup.java @@ -122,7 +122,7 @@ public abstract class FilterGroup { /** * If you have more than 1 filter patterns, then all instances of - * this class should filtered using {@link FilterGroupList.ByteArrayFilterGroupList#check(byte[])}, + * this class should be filtered using {@link FilterGroupList.ByteArrayFilterGroupList#check(byte[])}, * which uses a prefix tree to give better performance. */ public static class ByteArrayFilterGroup extends FilterGroup { @@ -149,7 +149,7 @@ public abstract class FilterGroup { } private static int[] createFailurePattern(byte[] pattern) { - // Computes the failure function using a boot-strapping process, + // Computes the failure function using a bootstrapping process, // where the pattern is matched against itself. final int patternLength = pattern.length; final int[] failure = new int[patternLength]; diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java index ddcfe41cc0..dc60f5d7da 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java @@ -24,11 +24,14 @@ public final class LithoFilterPatch { private static final class LithoFilterParameters { final String identifier; final String path; + final String accessibility; final byte[] buffer; - LithoFilterParameters(String lithoIdentifier, String lithoPath, byte[] buffer) { + LithoFilterParameters(String lithoIdentifier, String lithoPath, + String accessibility, byte[] buffer) { this.identifier = lithoIdentifier; this.path = lithoPath; + this.accessibility = accessibility; this.buffer = buffer; } @@ -39,6 +42,11 @@ public final class LithoFilterPatch { StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2)); builder.append("ID: "); builder.append(identifier); + if (!accessibility.isEmpty()) { + // AccessibilityId and AccessibilityText are pieces of BufferStrings. + builder.append(" Accessibility: "); + builder.append(accessibility); + } builder.append(" Path: "); builder.append(path); if (YouTubeAndMusicSettings.DEBUG_PROTOBUFFER.get()) { @@ -122,7 +130,7 @@ public final class LithoFilterPatch { /** * String suffix for components. - * Can be any of: ".eml", ".e-b", ".eml-js", "e-js-b" + * Can be any of: ".eml", ".eml-fe", ".e-b", ".eml-js", "e-js-b" */ private static final byte[] LITHO_COMPONENT_EXTENSION_BYTES = ".e".getBytes(StandardCharsets.US_ASCII); @@ -132,7 +140,7 @@ public final class LithoFilterPatch { private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; /** - * Because litho filtering is multi-threaded and the buffer is passed in from a different injection point, + * Because litho filtering is multithreaded and the buffer is passed in from a different injection point, * the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads. * Used for 20.21 and lower. */ @@ -140,7 +148,7 @@ public final class LithoFilterPatch { /** * Identifier to protocol buffer mapping. Only used for 20.22+. - * Thread local is needed because filtering is multi-threaded and each thread can load + * Thread local is needed because filtering is multithreaded and each thread can load * a different component with the same identifier. */ private static final ThreadLocal> identifierToBufferThread = new ThreadLocal<>(); @@ -155,6 +163,7 @@ public final class LithoFilterPatch { private static final StringTrieSearch identifierSearchTree = new StringTrieSearch(); static { + for (Filter filter : filters) { filterUsingCallbacks(identifierSearchTree, filter, filter.identifierCallbacks, Filter.FilterContentType.IDENTIFIER); @@ -186,16 +195,13 @@ public final class LithoFilterPatch { LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter; final boolean isFiltered = filter.isFiltered(parameters.identifier, - parameters.path, parameters.buffer, group, type, matchedStartIndex); + parameters.accessibility, parameters.path, parameters.buffer, + group, type, matchedStartIndex); if (isFiltered && BaseSettings.DEBUG.get()) { - if (type == Filter.FilterContentType.IDENTIFIER) { - Logger.printDebug(() -> "Filtered " + filterSimpleName - + " identifier: " + parameters.identifier); - } else { - Logger.printDebug(() -> "Filtered " + filterSimpleName - + " path: " + parameters.path); - } + Logger.printDebug(() -> type == Filter.FilterContentType.IDENTIFIER + ? filterSimpleName + " filtered identifier: " + parameters.identifier + : filterSimpleName + " filtered path: " + parameters.path); } return isFiltered; @@ -212,7 +218,7 @@ public final class LithoFilterPatch { /** * Helper function that differs from {@link Character#isDigit(char)} - * as this only matches ascii and not unicode numbers. + * as this only matches ascii and not Unicode numbers. */ private static boolean isAsciiNumber(byte character) { return '0' <= character && character <= '9'; @@ -233,12 +239,19 @@ public final class LithoFilterPatch { Logger.printDebug(() -> "New buffer: " + builder); } + // The identifier always seems to start very close to the buffer start. + // Highest identifier start index ever observed is 50, with most around 30 to 40. + // The buffer can be very large with up to 200kb has been observed, + // so the search is restricted to only the start. + final int maxBufferStartIndex = 500; // 10x expected upper bound. + // Could use Boyer-Moore-Horspool since the string is ASCII and has a limited number of // unique characters, but it seems to be slower since the extra overhead of checking the // bad character array negates any performance gain of skipping a few extra subsearches. int emlIndex = -1; final int emlStringLength = LITHO_COMPONENT_EXTENSION_BYTES.length; - for (int i = 0, lastStartIndex = buffer.length - emlStringLength; i <= lastStartIndex; i++) { + final int lastBufferIndexToCheckFrom = Math.min(maxBufferStartIndex, buffer.length - emlStringLength); + for (int i = 0; i < lastBufferIndexToCheckFrom; i++) { boolean match = true; for (int j = 0; j < emlStringLength; j++) { if (buffer[i + j] != LITHO_COMPONENT_EXTENSION_BYTES[j]) { @@ -254,6 +267,9 @@ public final class LithoFilterPatch { if (emlIndex < 0) { // Buffer is not used for creating a new litho component. + if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) { + Logger.printDebug(() -> "Could not find eml index"); + } return; } @@ -290,7 +306,9 @@ public final class LithoFilterPatch { } } if (endIndex < 0) { - Logger.printException(() -> "Could not find buffer identifier"); + if (BaseSettings.DEBUG.get()) { + Logger.printException(() -> "Debug: Could not find buffer identifier"); + } return; } @@ -329,7 +347,8 @@ public final class LithoFilterPatch { /** * Injection point. */ - public static boolean isFiltered(String identifier, StringBuilder pathBuilder) { + public static boolean isFiltered(String identifier, @Nullable String accessibilityId, + @Nullable String accessibilityText, StringBuilder pathBuilder) { try { if (identifier.isEmpty() || pathBuilder.length() == 0) { return false; @@ -340,7 +359,7 @@ public final class LithoFilterPatch { final int pipeIndex = identifier.indexOf('|'); if (pipeIndex >= 0) { // If the identifier contains no pipe, then it's not an ".eml" identifier - // and the buffer is not uniquely identified. Typically this only happens + // and the buffer is not uniquely identified. Typically, this only happens // for subcomponents where buffer filtering is not used. String identifierKey = identifier.substring(0, pipeIndex); @@ -357,7 +376,9 @@ public final class LithoFilterPatch { // No buffer is found for some components, such as // shorts_lockup_cell.eml on channel profiles. // For now, just ignore this and filter without a buffer. - Logger.printException(() -> "Could not find global buffer for identifier: " + identifier); + if (BaseSettings.DEBUG.get()) { + Logger.printException(() -> "Debug: Could not find buffer for identifier: " + identifier); + } } } } @@ -372,7 +393,15 @@ public final class LithoFilterPatch { } String path = pathBuilder.toString(); - LithoFilterParameters parameter = new LithoFilterParameters(identifier, path, buffer); + + String accessibility = ""; + if (accessibilityId != null && !accessibilityId.isBlank()) { + accessibility = accessibilityId; + } + if (accessibilityText != null && !accessibilityText.isBlank()) { + accessibility = accessibilityId + '|' + accessibilityText; + } + LithoFilterParameters parameter = new LithoFilterParameters(identifier, path, accessibility, buffer); Logger.printDebug(() -> "Searching " + parameter); return identifierSearchTree.matches(identifier, parameter) diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java index 941279f29b..421761f7da 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java @@ -24,23 +24,23 @@ public class LinkSanitizer { : List.of(parametersToRemove); } - public String sanitizeUrlString(String url) { + public String sanitizeURLString(String url) { try { - return sanitizeUri(Uri.parse(url)).toString(); + return sanitizeURI(Uri.parse(url)).toString(); } catch (Exception ex) { - Logger.printException(() -> "sanitizeUrlString failure: " + url, ex); + Logger.printException(() -> "sanitizeURLString failure: " + url, ex); return url; } } - public Uri sanitizeUri(Uri uri) { + public Uri sanitizeURI(Uri uri) { try { String scheme = uri.getScheme(); if (scheme == null || !(scheme.equals("http") || scheme.equals("https"))) { // Opening YouTube share sheet 'other' option passes the video title as a URI. // Checking !uri.isHierarchical() works for all cases, except if the // video title starts with / and then it's hierarchical but still an invalid URI. - Logger.printDebug(() -> "Ignoring uri: " + uri); + Logger.printDebug(() -> "Ignoring URI: " + uri); return uri; } @@ -56,12 +56,12 @@ public class LinkSanitizer { } } - Uri sanitizedUrl = builder.build(); - Logger.printInfo(() -> "Sanitized url: " + uri + " to: " + sanitizedUrl); + Uri sanitizedURL = builder.build(); + Logger.printInfo(() -> "Sanitized URL: " + uri + " to: " + sanitizedURL); - return sanitizedUrl; + return sanitizedURL; } catch (Exception ex) { - Logger.printException(() -> "sanitizeUri failure: " + uri, ex); + Logger.printException(() -> "sanitizeURI failure: " + uri, ex); return uri; } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java index c25e71d78c..2e5c457f7b 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java @@ -23,8 +23,8 @@ public class Requester { public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException { String url = apiUrl + route.getCompiledRoute(); HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); - // Request data is in the URL parameters and no body is sent. - // The calling code must set a length if using a request body. + // This request sends data via URL query parameters. No request body is included. + // If a request body is added, the caller must set the appropriate Content-Length header. connection.setFixedLengthStreamingMode(0); connection.setRequestMethod(route.getMethod().name()); String agentString = System.getProperty("http.agent") diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java index 0076e6d762..7a7b4d3337 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java @@ -96,15 +96,15 @@ public abstract class BaseActivityHook extends Activity { protected void createToolbar(Activity activity, PreferenceFragment fragment) { // Replace dummy placeholder toolbar. // This is required to fix submenu title alignment issue with Android ASOP 15+ - ViewGroup toolBarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT); - ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolBarParent, "revanced_toolbar"); + ViewGroup toolbarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT); + ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolbarParent, "revanced_toolbar"); toolbarLayoutParams = dummyToolbar.getLayoutParams(); - toolBarParent.removeView(dummyToolbar); + toolbarParent.removeView(dummyToolbar); // Sets appropriate system navigation bar color for the activity. ToolbarPreferenceFragment.setNavigationBarColor(activity.getWindow()); - Toolbar toolbar = new Toolbar(toolBarParent.getContext()); + Toolbar toolbar = new Toolbar(toolbarParent.getContext()); toolbar.setBackgroundColor(getToolbarBackgroundColor()); toolbar.setNavigationIcon(getNavigationIcon()); toolbar.setNavigationOnClickListener(getNavigationClickListener(activity)); @@ -121,7 +121,7 @@ public abstract class BaseActivityHook extends Activity { onPostToolbarSetup(activity, toolbar, fragment); - toolBarParent.addView(toolbar, 0); + toolbarParent.addView(toolbar, 0); } /** diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java index c8570d2513..2a3aef407f 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -6,6 +6,7 @@ import static app.revanced.extension.shared.patches.CustomBrandingPatch.Branding import static app.revanced.extension.shared.settings.Setting.parent; import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.patches.CustomBrandingPatch; /** * Settings shared across multiple apps. @@ -48,13 +49,13 @@ public class BaseSettings { public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message"); public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS)); - public static final BooleanSetting SANITIZE_SHARED_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE); + public static final BooleanSetting SANITIZE_SHARING_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE); public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE); public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false); - public static final EnumSetting CUSTOM_BRANDING_ICON = new EnumSetting<>("revanced_custom_branding_icon", BrandingTheme.ORIGINAL, true); - public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", 1, true); + public static final EnumSetting CUSTOM_BRANDING_ICON = new EnumSetting<>("revanced_custom_branding_icon", CustomBrandingPatch.getDefaultIconStyle(), true); + public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", CustomBrandingPatch.getDefaultAppNameIndex(), true); public static final StringSetting DISABLED_FEATURE_FLAGS = new StringSetting("revanced_disabled_feature_flags", "", true, parent(DEBUG)); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java index 58745a16cb..c67ebabf96 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java @@ -43,7 +43,7 @@ public class BooleanSetting extends Setting { * This method is only to be used by the Settings preference code. * * This intentionally is a static method to deter - * accidental usage when {@link #save(Boolean)} was intnded. + * accidental usage when {@link #save(Boolean)} was intended. */ public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) { setting.value = Objects.requireNonNull(newValue); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java index 88de4029ef..2c2cb6a3a8 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java @@ -72,7 +72,7 @@ public class EnumSetting> extends Setting { } /** - * @param enumName Enum name. Casing does not matter. + * @param enumName Enum name. Casing does not matter. * @return Enum of this type with the same declared name. * @throws IllegalArgumentException if the name is not a valid enum of this type. */ diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java index 32d24e3e1f..53a980e3c2 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -274,60 +274,6 @@ public abstract class Setting { load(); } - /** - * Migrate a setting value if the path is renamed but otherwise the old and new settings are identical. - */ - public static void migrateOldSettingToNew(Setting oldSetting, Setting newSetting) { - if (oldSetting == newSetting) throw new IllegalArgumentException(); - - if (!oldSetting.isSetToDefault()) { - Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting); - newSetting.save(oldSetting.value); - oldSetting.resetToDefault(); - } - } - - /** - * Migrate an old Setting value previously stored in a different SharedPreference. - *

- * This method will be deleted in the future. - */ - @SuppressWarnings({"rawtypes", "NewApi"}) - public static void migrateFromOldPreferences(SharedPrefCategory oldPrefs, Setting setting, String settingKey) { - if (!oldPrefs.preferences.contains(settingKey)) { - return; // Nothing to do. - } - - Object newValue = setting.get(); - final Object migratedValue; - if (setting instanceof BooleanSetting) { - migratedValue = oldPrefs.getBoolean(settingKey, (Boolean) newValue); - } else if (setting instanceof IntegerSetting) { - migratedValue = oldPrefs.getIntegerString(settingKey, (Integer) newValue); - } else if (setting instanceof LongSetting) { - migratedValue = oldPrefs.getLongString(settingKey, (Long) newValue); - } else if (setting instanceof FloatSetting) { - migratedValue = oldPrefs.getFloatString(settingKey, (Float) newValue); - } else if (setting instanceof StringSetting) { - 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); - } - /** * Sets, but does _not_ persistently save the value. * This method is only to be used by the Settings preference code. @@ -419,7 +365,7 @@ public abstract class Setting { } /** - * @return if the currently set value is the same as {@link #defaultValue} + * @return if the currently set value is the same as {@link #defaultValue}. */ public boolean isSetToDefault() { return value.equals(defaultValue); @@ -450,7 +396,7 @@ public abstract class Setting { /** * @param importExportKey The JSON key. The JSONObject parameter will contain data for this key. - * @return the value stored using the import/export key. Do not set any values in this method. + * @return the value stored using the import/export key. Do not set any values in this method. */ protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException; diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java index 70ae3a5ab2..a515471a00 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java @@ -208,7 +208,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { private void updatePreferenceScreen(@NonNull PreferenceGroup group, boolean syncSettingValue, boolean applySettingToPreference) { - // Alternatively this could iterate thru all Settings and check for any matching Preferences, + // Alternatively this could iterate through all Settings and check for any matching Preferences, // but there are many more Settings than UI preferences so it's more efficient to only check // the Preferences. for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/FeatureFlagsManagerPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/FeatureFlagsManagerPreference.java index 061f230e4f..4e6d2e5cdd 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/FeatureFlagsManagerPreference.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/FeatureFlagsManagerPreference.java @@ -72,7 +72,7 @@ public class FeatureFlagsManagerPreference extends Preference { */ private static final Set FLAGS_TO_IGNORE = Set.of( 45386834L, // 'You' tab settings icon. - 45685201L // Bold icons. Forcing off interferes with patch changes and YT icons are broken. + 45532100L // Cairo flag. Turning this off with all other flags causes the settings menu to be a mix of old/new. ); /** @@ -131,9 +131,10 @@ public class FeatureFlagsManagerPreference extends Preference { disabledFlags.removeAll(FLAGS_TO_IGNORE); if (allKnownFlags.isEmpty() && disabledFlags.isEmpty()) { - // String does not need to be localized because it's basically impossible - // to reach the settings menu without encountering at least 1 flag. - Utils.showToastShort("No feature flags logged yet"); + // It's impossible to reach the settings menu without reaching at least one flag. + // So if theres no flags, then that means the user has just enabled debugging + // but has not restarted the app yet. + Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_no_flags")); return; } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java index 4e9c1f2e0b..ed5db6b235 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java @@ -36,18 +36,18 @@ public class SharedPrefCategory { } private void saveObjectAsString(@NonNull String key, @Nullable Object value) { - preferences.edit().putString(key, (value == null ? null : value.toString())).apply(); + preferences.edit().putString(key, (value == null ? null : value.toString())).commit(); } /** * Removes any preference data type that has the specified key. */ public void removeKey(@NonNull String key) { - preferences.edit().remove(Objects.requireNonNull(key)).apply(); + preferences.edit().remove(Objects.requireNonNull(key)).commit(); } public void saveBoolean(@NonNull String key, boolean value) { - preferences.edit().putBoolean(key, value).apply(); + preferences.edit().putBoolean(key, value).commit(); } /** diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/UrlLinkPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/URLLinkPreference.java similarity index 65% rename from extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/UrlLinkPreference.java rename to extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/URLLinkPreference.java index 8b9a76f039..59f3077ceb 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/UrlLinkPreference.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/URLLinkPreference.java @@ -9,36 +9,36 @@ import android.util.AttributeSet; import app.revanced.extension.shared.Logger; /** - * Simple preference that opens a url when clicked. + * Simple preference that opens a URL when clicked. */ @SuppressWarnings("deprecation") -public class UrlLinkPreference extends Preference { +public class URLLinkPreference extends Preference { - protected String externalUrl; + protected String externalURL; { setOnPreferenceClickListener(pref -> { - if (externalUrl == null) { + if (externalURL == null) { Logger.printException(() -> "URL not set " + getClass().getSimpleName()); return false; } Intent i = new Intent(Intent.ACTION_VIEW); - i.setData(Uri.parse(externalUrl)); + i.setData(Uri.parse(externalURL)); pref.getContext().startActivity(i); return true; }); } - public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public URLLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } - public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) { + public URLLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } - public UrlLinkPreference(Context context, AttributeSet attrs) { + public URLLinkPreference(Context context, AttributeSet attrs) { super(context, attrs); } - public UrlLinkPreference(Context context) { + public URLLinkPreference(Context context) { super(context); } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java index 7b5830a463..95731418d2 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java @@ -20,7 +20,7 @@ import app.revanced.extension.shared.ResourceType; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.settings.preference.ColorPickerPreference; import app.revanced.extension.shared.settings.preference.CustomDialogListPreference; -import app.revanced.extension.shared.settings.preference.UrlLinkPreference; +import app.revanced.extension.shared.settings.preference.URLLinkPreference; /** * Abstract base class for search result items, defining common fields and behavior. @@ -167,7 +167,7 @@ public abstract class BaseSearchResultItem { if (pref instanceof SwitchPreference) return ViewType.SWITCH; if (pref instanceof ListPreference) return ViewType.LIST; if (pref instanceof ColorPickerPreference) return ViewType.COLOR_PICKER; - if (pref instanceof UrlLinkPreference) return ViewType.URL_LINK; + if (pref instanceof URLLinkPreference) return ViewType.URL_LINK; if ("no_results_placeholder".equals(pref.getKey())) return ViewType.NO_RESULTS; return ViewType.REGULAR; } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java index d6a8167f4e..04d69c6b6b 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java @@ -36,7 +36,7 @@ import app.revanced.extension.shared.ResourceType; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.settings.preference.ColorPickerPreference; import app.revanced.extension.shared.settings.preference.CustomDialogListPreference; -import app.revanced.extension.shared.settings.preference.UrlLinkPreference; +import app.revanced.extension.shared.settings.preference.URLLinkPreference; import app.revanced.extension.shared.ui.ColorDot; /** @@ -436,7 +436,7 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter * Effectively the cache expiration of these fetches is the same as the stock app, @@ -86,7 +86,7 @@ public class StreamingDataRequest { * Cache limit must be greater than the maximum number of videos open at once, * which theoretically is more than 4 (3 Shorts + one regular minimized video). * But instead use a much larger value, to handle if a video viewed a while ago - * is somehow still referenced. Each stream is a small array of Strings + * is somehow still referenced. Each stream is a small array of Strings * so memory usage is not a concern. */ private static final Map cache = Collections.synchronizedMap( diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch.java index 61cb977a2f..9124b2f36f 100644 --- a/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch.java +++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch.java @@ -14,6 +14,6 @@ public final class SanitizeSharingLinksPatch { * Injection point. */ public static String sanitizeSharingLink(String url) { - return sanitizer.sanitizeUrlString(url); + return sanitizer.sanitizeURLString(url); } } diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ExtensionPreferenceCategory.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ExtensionPreferenceCategory.java index 7383a5582c..efa11b210b 100644 --- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ExtensionPreferenceCategory.java +++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ExtensionPreferenceCategory.java @@ -26,7 +26,7 @@ public class ExtensionPreferenceCategory extends ConditionalPreferenceCategory { addPreference(new TogglePreference(context, "Sanitize sharing links", "Remove tracking parameters from shared links.", - BaseSettings.SANITIZE_SHARED_LINKS + BaseSettings.SANITIZE_SHARING_LINKS )); addPreference(new TogglePreference(context, diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/share/ShareUrlSanitizer.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/share/ShareUrlSanitizer.java index 5d09c10c0b..4b9b31dcd8 100644 --- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/share/ShareUrlSanitizer.java +++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/share/ShareUrlSanitizer.java @@ -1,6 +1,5 @@ package app.revanced.extension.tiktok.share; -import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.privacy.LinkSanitizer; import app.revanced.extension.shared.settings.BaseSettings; @@ -13,7 +12,7 @@ public final class ShareUrlSanitizer { * Injection point for setting check. */ public static boolean shouldSanitize() { - return BaseSettings.SANITIZE_SHARED_LINKS.get(); + return BaseSettings.SANITIZE_SHARING_LINKS.get(); } /** @@ -24,6 +23,6 @@ public final class ShareUrlSanitizer { return url; } - return sanitizer.sanitizeUrlString(url); + return sanitizer.sanitizeURLString(url); } } diff --git a/extensions/youtube/build.gradle.kts b/extensions/youtube/build.gradle.kts index c7116666bd..f84a54a0d3 100644 --- a/extensions/youtube/build.gradle.kts +++ b/extensions/youtube/build.gradle.kts @@ -6,6 +6,6 @@ dependencies { android { defaultConfig { - minSdk = 23 + minSdk = 26 } } diff --git a/extensions/youtube/src/main/AndroidManifest.xml b/extensions/youtube/src/main/AndroidManifest.xml index 9b65eb06cf..042e61a707 100644 --- a/extensions/youtube/src/main/AndroidManifest.xml +++ b/extensions/youtube/src/main/AndroidManifest.xml @@ -1 +1,4 @@ - + + + + diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java index 57531bf462..53c1e9c0e8 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java @@ -36,7 +36,7 @@ import app.revanced.extension.youtube.shared.PlayerType; * Can show YouTube provided screen captures of beginning/middle/end of the video. * (ie: sd1.jpg, sd2.jpg, sd3.jpg). *

- * Or can show crowd-sourced thumbnails provided by DeArrow (...). + * Or can show crowdsourced thumbnails provided by DeArrow (...). *

* Or can use DeArrow and fall back to screen captures if DeArrow is not available. *

@@ -135,12 +135,12 @@ public final class AlternativeThumbnailsPatch { } } - private static final Uri dearrowApiUri; + private static final Uri dearrowAPIURI; /** - * The scheme and host of {@link #dearrowApiUri}. + * The scheme and host of {@link #dearrowAPIURI}. */ - private static final String deArrowApiUrlPrefix; + private static final String deArrowAPIURLPrefix; /** * How long to temporarily turn off DeArrow if it fails for any reason. @@ -148,31 +148,31 @@ public final class AlternativeThumbnailsPatch { private static final long DEARROW_FAILURE_API_BACKOFF_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes. /** - * If non zero, then the system time of when DeArrow API calls can resume. + * If non-zero, then the system time of when DeArrow API calls can resume. */ private static volatile long timeToResumeDeArrowAPICalls; static { - dearrowApiUri = validateSettings(); - final int port = dearrowApiUri.getPort(); + dearrowAPIURI = validateSettings(); + final int port = dearrowAPIURI.getPort(); String portString = port == -1 ? "" : (":" + port); - deArrowApiUrlPrefix = dearrowApiUri.getScheme() + "://" + dearrowApiUri.getHost() + portString + "/"; - Logger.printDebug(() -> "Using DeArrow API address: " + deArrowApiUrlPrefix); + deArrowAPIURLPrefix = dearrowAPIURI.getScheme() + "://" + dearrowAPIURI.getHost() + portString + "/"; + Logger.printDebug(() -> "Using DeArrow API address: " + deArrowAPIURLPrefix); } /** * Fix any bad imported data. */ private static Uri validateSettings() { - Uri apiUri = Uri.parse(Settings.ALT_THUMBNAIL_DEARROW_API_URL.get()); + Uri apiURI = Uri.parse(Settings.ALT_THUMBNAIL_DEARROW_API_URL.get()); // Cannot use unsecured 'http', otherwise the connections fail to start and no callbacks hooks are made. - String scheme = apiUri.getScheme(); - if (scheme == null || scheme.equals("http") || apiUri.getHost() == null) { + String scheme = apiURI.getScheme(); + if (scheme == null || scheme.equals("http") || apiURI.getHost() == null) { Utils.showToastLong("Invalid DeArrow API URL. Using default"); Settings.ALT_THUMBNAIL_DEARROW_API_URL.resetToDefault(); return validateSettings(); } - return apiUri; + return apiURI; } private static ThumbnailOption optionSettingForCurrentNavigation() { @@ -209,16 +209,16 @@ public final class AlternativeThumbnailsPatch { } /** - * Build the alternative thumbnail url using YouTube provided still video captures. + * Build the alternative thumbnail URL using YouTube provided still video captures. * - * @param decodedUrl Decoded original thumbnail request url. - * @return The alternative thumbnail url, or if not available NULL. + * @param decodedURL Decoded original thumbnail request url. + * @return The alternative thumbnail URL, or if not available NULL. */ @Nullable - private static String buildYouTubeVideoStillURL(@NonNull DecodedThumbnailUrl decodedUrl, + private static String buildYouTubeVideoStillURL(@NonNull DecodedThumbnailURL decodedURL, @NonNull ThumbnailQuality qualityToUse) { - String sanitizedReplacement = decodedUrl.createStillsUrl(qualityToUse, false); - if (VerifiedQualities.verifyAltThumbnailExist(decodedUrl.videoId, qualityToUse, sanitizedReplacement)) { + String sanitizedReplacement = decodedURL.createStillsURL(qualityToUse, false); + if (VerifiedQualities.verifyAltThumbnailExist(decodedURL.videoId, qualityToUse, sanitizedReplacement)) { return sanitizedReplacement; } @@ -226,26 +226,26 @@ public final class AlternativeThumbnailsPatch { } /** - * Build the alternative thumbnail url using DeArrow thumbnail cache. + * Build the alternative thumbnail URL using DeArrow thumbnail cache. * * @param videoId ID of the video to get a thumbnail of. Can be any video (regular or Short). - * @param fallbackUrl URL to fall back to in case. - * @return The alternative thumbnail url, without tracking parameters. + * @param fallbackURL URL to fall back to in case. + * @return The alternative thumbnail URL, without tracking parameters. */ @NonNull - private static String buildDeArrowThumbnailURL(String videoId, String fallbackUrl) { - // Build thumbnail request url. + private static String buildDeArrowThumbnailURL(String videoId, String fallbackURL) { + // Build thumbnail request URL. // See https://github.com/ajayyy/DeArrowThumbnailCache/blob/29eb4359ebdf823626c79d944a901492d760bbbc/app.py#L29. - return dearrowApiUri + return dearrowAPIURI .buildUpon() - .appendQueryParameter("videoID", videoId) - .appendQueryParameter("redirectUrl", fallbackUrl) + .appendQueryParameter("videoId", videoId) + .appendQueryParameter("redirectURL", fallbackURL) .build() .toString(); } - private static boolean urlIsDeArrow(@NonNull String imageUrl) { - return imageUrl.startsWith(deArrowApiUrlPrefix); + private static boolean urlIsDeArrow(@NonNull String imageURL) { + return imageURL.startsWith(deArrowAPIURLPrefix); } /** @@ -264,7 +264,7 @@ public final class AlternativeThumbnailsPatch { } private static void handleDeArrowError(@NonNull String url, int statusCode) { - Logger.printDebug(() -> "Encountered DeArrow error. Url: " + url); + Logger.printDebug(() -> "Encountered DeArrow error. URL: " + url); final long now = System.currentTimeMillis(); if (timeToResumeDeArrowAPICalls < now) { timeToResumeDeArrowAPICalls = now + DEARROW_FAILURE_API_BACKOFF_MILLISECONDS; @@ -278,63 +278,63 @@ public final class AlternativeThumbnailsPatch { } /** - * Injection point. Called off the main thread and by multiple threads at the same time. + * Injection point. Called off the main thread and by multiple threads at the same time. * - * @param originalUrl Image url for all url images loaded, including video thumbnails. + * @param originalURL Image URL for all URL images loaded, including video thumbnails. */ - public static String overrideImageURL(String originalUrl) { + public static String overrideImageURL(String originalURL) { try { ThumbnailOption option = optionSettingForCurrentNavigation(); if (option == ThumbnailOption.ORIGINAL) { - return originalUrl; + return originalURL; } - final var decodedUrl = DecodedThumbnailUrl.decodeImageUrl(originalUrl); - if (decodedUrl == null) { - return originalUrl; // Not a thumbnail. + final var decodedURL = DecodedThumbnailURL.decodeImageURL(originalURL); + if (decodedURL == null) { + return originalURL; // Not a thumbnail. } - Logger.printDebug(() -> "Original url: " + decodedUrl.sanitizedUrl); + Logger.printDebug(() -> "Original URL: " + decodedURL.sanitizedURL); - ThumbnailQuality qualityToUse = ThumbnailQuality.getQualityToUse(decodedUrl.imageQuality); + ThumbnailQuality qualityToUse = ThumbnailQuality.getQualityToUse(decodedURL.imageQuality); if (qualityToUse == null) { // Thumbnail is a Short or a Storyboard image used for seekbar thumbnails (must not replace these). - return originalUrl; + return originalURL; } - String sanitizedReplacementUrl; + String sanitizedReplacementURL; final boolean includeTracking; if (option.useDeArrow && canUseDeArrowAPI()) { includeTracking = false; // Do not include view tracking parameters with API call. - String fallbackUrl = null; + String fallbackURL = null; if (option.useStillImages) { - fallbackUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse); + fallbackURL = buildYouTubeVideoStillURL(decodedURL, qualityToUse); } - if (fallbackUrl == null) { - fallbackUrl = decodedUrl.sanitizedUrl; + if (fallbackURL == null) { + fallbackURL = decodedURL.sanitizedURL; } - sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl); + sanitizedReplacementURL = buildDeArrowThumbnailURL(decodedURL.videoId, fallbackURL); } else if (option.useStillImages) { includeTracking = true; // Include view tracking parameters if present. - sanitizedReplacementUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse); - if (sanitizedReplacementUrl == null) { - return originalUrl; // Still capture is not available. Return the untouched original url. + sanitizedReplacementURL = buildYouTubeVideoStillURL(decodedURL, qualityToUse); + if (sanitizedReplacementURL == null) { + return originalURL; // Still capture is not available. Return the untouched original url. } } else { - return originalUrl; // Recently experienced DeArrow failure and video stills are not enabled. + return originalURL; // Recently experienced DeArrow failure and video stills are not enabled. } // Do not log any tracking parameters. - Logger.printDebug(() -> "Replacement url: " + sanitizedReplacementUrl); + Logger.printDebug(() -> "Replacement URL: " + sanitizedReplacementURL); return includeTracking - ? sanitizedReplacementUrl + decodedUrl.viewTrackingParameters - : sanitizedReplacementUrl; + ? sanitizedReplacementURL + decodedURL.viewTrackingParameters + : sanitizedReplacementURL; } catch (Exception ex) { Logger.printException(() -> "overrideImageURL failure", ex); - return originalUrl; + return originalURL; } } @@ -370,21 +370,21 @@ public final class AlternativeThumbnailsPatch { // - very old // - very low view count // Take note of this, so if the image reloads the original thumbnail will be used. - DecodedThumbnailUrl decodedUrl = DecodedThumbnailUrl.decodeImageUrl(url); - if (decodedUrl == null) { + DecodedThumbnailURL decodedURL = DecodedThumbnailURL.decodeImageURL(url); + if (decodedURL == null) { return; // Not a thumbnail. } - Logger.printDebug(() -> "handleCronetSuccess, image not available: " + decodedUrl.sanitizedUrl); + Logger.printDebug(() -> "handleCronetSuccess, image not available: " + decodedURL.sanitizedURL); - ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedUrl.imageQuality); + ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedURL.imageQuality); if (quality == null) { // Video is a short or a seekbar thumbnail, but somehow did not load. Should not happen. - Logger.printDebug(() -> "Failed to recognize image quality of url: " + decodedUrl.sanitizedUrl); + Logger.printDebug(() -> "Failed to recognize image quality of URL: " + decodedURL.sanitizedURL); return; } - VerifiedQualities.setAltThumbnailDoesNotExist(decodedUrl.videoId, quality); + VerifiedQualities.setAltThumbnailDoesNotExist(decodedURL.videoId, quality); } } catch (Exception ex) { Logger.printException(() -> "Callback success error", ex); @@ -482,7 +482,7 @@ public final class AlternativeThumbnailsPatch { // (even though search and subscriptions use the exact same layout as the home feed). // Of note, this image quality issue only appears with the alt thumbnail images, // and the regular thumbnails have identical color/contrast quality for all sizes. - // Fix this by falling thru and upgrading SD to 720. + // Fix this by falling through and upgrading SD to 720. case SDDEFAULT, HQ720 -> { // SD is max resolution for fast alt images. if (useFastQuality) { yield SDDEFAULT; @@ -525,7 +525,7 @@ public final class AlternativeThumbnailsPatch { private static final long NOT_AVAILABLE_TIMEOUT_MILLISECONDS = 10 * 60 * 1000; // 10 minutes. /** - * Cache used to verify if an alternative thumbnails exists for a given video id. + * Cache used to verify if an alternative thumbnails exists for a given video ID. */ @GuardedBy("itself") private static final Map altVideoIdLookup = @@ -546,10 +546,10 @@ public final class AlternativeThumbnailsPatch { } static boolean verifyAltThumbnailExist(@NonNull String videoId, @NonNull ThumbnailQuality quality, - @NonNull String imageUrl) { + @NonNull String imageURL) { VerifiedQualities verified = getVerifiedQualities(videoId, Settings.ALT_THUMBNAIL_STILLS_FAST.get()); if (verified == null) return true; // Fast alt thumbnails is enabled. - return verified.verifyYouTubeThumbnailExists(videoId, quality, imageUrl); + return verified.verifyYouTubeThumbnailExists(videoId, quality, imageURL); } static void setAltThumbnailDoesNotExist(@NonNull String videoId, @NonNull ThumbnailQuality quality) { @@ -590,10 +590,10 @@ public final class AlternativeThumbnailsPatch { } /** - * Verify if a video alt thumbnail exists. Does so by making a minimal HEAD http request. + * Verify if a video alt thumbnail exists. Does so by making a minimal HEAD HTTP request. */ synchronized boolean verifyYouTubeThumbnailExists(@NonNull String videoId, @NonNull ThumbnailQuality quality, - @NonNull String imageUrl) { + @NonNull String imageURL) { if (highestQualityVerified != null && highestQualityVerified.ordinal() >= quality.ordinal()) { return true; // Previously verified as existing. } @@ -609,7 +609,7 @@ public final class AlternativeThumbnailsPatch { } if (fastQuality) { - return true; // Unknown if it exists or not. Use the URL anyways and update afterwards if loading fails. + return true; // Unknown if it exists or not. Use the URL anyway and update afterward if loading fails. } boolean imageFileFound; @@ -619,7 +619,7 @@ public final class AlternativeThumbnailsPatch { final long start = System.currentTimeMillis(); imageFileFound = Utils.submitOnBackgroundThread(() -> { final int connectionTimeoutMillis = 10000; // 10 seconds. - HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection(); + HttpURLConnection connection = (HttpURLConnection) new URL(imageURL).openConnection(); connection.setConnectTimeout(connectionTimeoutMillis); connection.setReadTimeout(connectionTimeoutMillis); connection.setRequestMethod("HEAD"); @@ -632,13 +632,13 @@ public final class AlternativeThumbnailsPatch { return (contentType != null && contentType.startsWith("image")); } if (responseCode != HttpURLConnection.HTTP_NOT_FOUND) { - Logger.printDebug(() -> "Unexpected response code: " + responseCode + " for url: " + imageUrl); + Logger.printDebug(() -> "Unexpected response code: " + responseCode + " for URL: " + imageURL); } return false; }).get(); - Logger.printDebug(() -> "Verification took: " + (System.currentTimeMillis() - start) + "ms for image: " + imageUrl); + Logger.printDebug(() -> "Verification took: " + (System.currentTimeMillis() - start) + "ms for image: " + imageURL); } catch (ExecutionException | InterruptedException ex) { - Logger.printInfo(() -> "Could not verify alt url: " + imageUrl, ex); + Logger.printInfo(() -> "Could not verify alt URL: " + imageURL, ex); imageFileFound = false; } @@ -650,11 +650,11 @@ public final class AlternativeThumbnailsPatch { /** * YouTube video thumbnail url, decoded into it's relevant parts. */ - private static class DecodedThumbnailUrl { + private static class DecodedThumbnailURL { private static final String YOUTUBE_THUMBNAIL_DOMAIN = "https://i.ytimg.com/"; @Nullable - static DecodedThumbnailUrl decodeImageUrl(String url) { + static DecodedThumbnailURL decodeImageURL(String url) { final int urlPathStartIndex = url.indexOf('/', "https://".length()) + 1; if (urlPathStartIndex <= 0) return null; @@ -674,14 +674,14 @@ public final class AlternativeThumbnailsPatch { int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex); if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length(); - return new DecodedThumbnailUrl(url, urlPathStartIndex, urlPathEndIndex, videoIdStartIndex, videoIdEndIndex, + return new DecodedThumbnailURL(url, urlPathStartIndex, urlPathEndIndex, videoIdStartIndex, videoIdEndIndex, imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex); } - final String originalFullUrl; + final String originalFullURL; /** Full usable url, but stripped of any tracking information. */ - final String sanitizedUrl; - /** Url path, such as 'vi' or 'vi_webp' */ + final String sanitizedURL; + /** URL path, such as 'vi' or 'vi_webp' */ final String urlPath; final String videoId; /** Quality, such as hq720 or sddefault. */ @@ -691,25 +691,25 @@ public final class AlternativeThumbnailsPatch { /** User view tracking parameters, only present on some images. */ final String viewTrackingParameters; - DecodedThumbnailUrl(String fullUrl, int urlPathStartIndex, int urlPathEndIndex, int videoIdStartIndex, int videoIdEndIndex, + DecodedThumbnailURL(String fullURL, int urlPathStartIndex, int urlPathEndIndex, int videoIdStartIndex, int videoIdEndIndex, int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) { - originalFullUrl = fullUrl; - sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex); - urlPath = fullUrl.substring(urlPathStartIndex, urlPathEndIndex); - videoId = fullUrl.substring(videoIdStartIndex, videoIdEndIndex); - imageQuality = fullUrl.substring(imageSizeStartIndex, imageSizeEndIndex); - imageExtension = fullUrl.substring(imageSizeEndIndex + 1, imageExtensionEndIndex); - viewTrackingParameters = (imageExtensionEndIndex == fullUrl.length()) - ? "" : fullUrl.substring(imageExtensionEndIndex); + originalFullURL = fullURL; + sanitizedURL = fullURL.substring(0, imageExtensionEndIndex); + urlPath = fullURL.substring(urlPathStartIndex, urlPathEndIndex); + videoId = fullURL.substring(videoIdStartIndex, videoIdEndIndex); + imageQuality = fullURL.substring(imageSizeStartIndex, imageSizeEndIndex); + imageExtension = fullURL.substring(imageSizeEndIndex + 1, imageExtensionEndIndex); + viewTrackingParameters = (imageExtensionEndIndex == fullURL.length()) + ? "" : fullURL.substring(imageExtensionEndIndex); } @SuppressWarnings("SameParameterValue") - String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) { + String createStillsURL(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) { // Images could be upgraded to webp if they are not already, but this fails quite often, // especially for new videos uploaded in the last hour. // And even if alt webp images do exist, sometimes they can load much slower than the original jpg alt images. // (as much as 4x slower network response has been observed, despite the alt webp image being a smaller file). - StringBuilder builder = new StringBuilder(originalFullUrl.length() + 2); + StringBuilder builder = new StringBuilder(originalFullURL.length() + 2); // Many different "i.ytimage.com" domains exist such as "i9.ytimg.com", // but still captures are frequently not available on the other domains (especially newly uploaded videos). // So always use the primary domain for a higher success rate. diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/BackgroundPlaybackPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/BackgroundPlaybackPatch.java index 7575414ba1..87af8b0ccf 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/BackgroundPlaybackPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/BackgroundPlaybackPatch.java @@ -15,7 +15,7 @@ public class BackgroundPlaybackPatch { // Steps to verify most edge cases (with Shorts background playback set to off): // 1. Open a regular video - // 2. Minimize app (PIP should appear) + // 2. Minimize app (PiP should appear) // 3. Reopen app // 4. Open a Short (without closing the regular video) // (try opening both Shorts in the video player suggestions AND Shorts from the home feed) @@ -23,7 +23,7 @@ public class BackgroundPlaybackPatch { // 6. Reopen app // 7. Close the Short // 8. Resume playing the regular video - // 9. Minimize the app (PIP should appear) + // 9. Minimize the app (PiP should appear) if (ShortsPlayerState.isOpen()) { return false; } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/BypassImageRegionRestrictionsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/BypassImageRegionRestrictionsPatch.java index ccc853d4c3..1272992993 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/BypassImageRegionRestrictionsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/BypassImageRegionRestrictionsPatch.java @@ -15,24 +15,24 @@ public final class BypassImageRegionRestrictionsPatch { private static final String REPLACEMENT_IMAGE_DOMAIN = "https://yt4.ggpht.com"; /** - * YouTube static images domain. Includes user and channel avatar images and community post images. + * YouTube static images' domain. Includes user and channel avatar images and community post images. */ private static final Pattern YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN = Pattern.compile("^https://(yt3|lh[3-6]|play-lh)\\.(ggpht|googleusercontent)\\.com"); /** - * Injection point. Called off the main thread and by multiple threads at the same time. + * Injection point. Called off the main thread and by multiple threads at the same time. * - * @param originalUrl Image url for all image urls loaded. + * @param originalURL Image URL for all image URLs loaded. */ - public static String overrideImageURL(String originalUrl) { + public static String overrideImageURL(String originalURL) { try { if (BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED) { String replacement = YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN - .matcher(originalUrl).replaceFirst(REPLACEMENT_IMAGE_DOMAIN); + .matcher(originalURL).replaceFirst(REPLACEMENT_IMAGE_DOMAIN); - if (Settings.DEBUG.get() && !replacement.equals(originalUrl)) { - Logger.printDebug(() -> "Replaced: '" + originalUrl + "' with: '" + replacement + "'"); + if (Settings.DEBUG.get() && !replacement.equals(originalURL)) { + Logger.printDebug(() -> "Replaced: '" + originalURL + "' with: '" + replacement + "'"); } return replacement; @@ -41,6 +41,6 @@ public final class BypassImageRegionRestrictionsPatch { Logger.printException(() -> "overrideImageURL failure", ex); } - return originalUrl; + return originalURL; } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java index 706b135aff..d7cf0faba1 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java @@ -94,7 +94,7 @@ public class ChangeFormFactorPatch { public static void navigationTabCreated(NavigationButton button, View tabView) { // On first startup of the app the navigation buttons are fetched and updated. // If the user immediately opens the 'You' or opens a video, then the call to - // update the navigtation buttons will use the non automotive form factor + // update the navigation buttons will use the non-automotive form factor // and the explore tab is missing. // Fixing this is not so simple because of the concurrent calls for the player and You tab. // For now, always hide the explore tab. diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeStartPagePatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeStartPagePatch.java index 9748a3d68f..b21ad89923 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeStartPagePatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeStartPagePatch.java @@ -24,7 +24,7 @@ public final class ChangeStartPagePatch { DEFAULT("", null), /** - * Browse id. + * BrowseId. */ ALL_SUBSCRIPTIONS("FEchannels", TRUE), BROWSE("FEguide_builder", TRUE), @@ -39,7 +39,7 @@ public final class ChangeStartPagePatch { YOUR_CLIPS("FEclips", TRUE), /** - * Channel id, this can be used as a browseId. + * Channel ID, this can be used as a browseId. */ COURSES("UCtFRv9O2AHqOZjjynzrv-xg", TRUE), FASHION("UCrpQ4p1Ql_hG8rKXIKM1MOQ", TRUE), @@ -52,7 +52,7 @@ public final class ChangeStartPagePatch { VIRTUAL_REALITY("UCzuqhhs6NWbgTzMuM09WKDQ", TRUE), /** - * Playlist id, this can be used as a browseId. + * Playlist ID, this can be used as a browseId. */ LIKED_VIDEO("VLLL", TRUE), WATCH_LATER("VLWL", TRUE), diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CopyVideoUrlPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CopyVideoURLPatch.java similarity index 93% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CopyVideoUrlPatch.java rename to extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CopyVideoURLPatch.java index db3338f527..cae9bb3df1 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CopyVideoUrlPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CopyVideoURLPatch.java @@ -7,9 +7,9 @@ import android.os.Build; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; -public class CopyVideoUrlPatch { +public class CopyVideoURLPatch { - public static void copyUrl(boolean withTimestamp) { + public static void copyURL(boolean withTimestamp) { try { StringBuilder builder = new StringBuilder("https://youtu.be/"); builder.append(VideoInformation.getVideoId()); @@ -31,7 +31,7 @@ public class CopyVideoUrlPatch { } Utils.setClipboard(builder.toString()); - // Do not show a toast if using Android 13+ as it shows it's own toast. + // Do not show a toast if using Android 13+ as it shows its own toast. // But if the user copied with a timestamp then show a toast. // Unfortunately this will show 2 toasts on Android 13+, but no way around this. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 || (withTimestamp && currentVideoTimeInSeconds > 0)) { @@ -40,7 +40,7 @@ public class CopyVideoUrlPatch { : str("revanced_share_copy_url_success")); } } catch (Exception e) { - Logger.printException(() -> "Failed to generate video url", e); + Logger.printException(() -> "Failed to generate video URL", e); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableHapticFeedbackPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableHapticFeedbackPatch.java index 6ae49427af..f466bf9a62 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableHapticFeedbackPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableHapticFeedbackPatch.java @@ -1,5 +1,8 @@ package app.revanced.extension.youtube.patches; +import android.os.VibrationEffect; +import android.os.Vibrator; + import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") @@ -12,6 +15,13 @@ public class DisableHapticFeedbackPatch { return Settings.DISABLE_HAPTIC_FEEDBACK_CHAPTERS.get(); } + /** + * Injection point. + */ + public static boolean disablePreciseSeekingVibrate() { + return Settings.DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING.get(); + } + /** * Injection point. */ @@ -22,8 +32,10 @@ public class DisableHapticFeedbackPatch { /** * Injection point. */ - public static boolean disablePreciseSeekingVibrate() { - return Settings.DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING.get(); + public static Object disableTapAndHoldVibrate(Object vibrator) { + return Settings.DISABLE_HAPTIC_FEEDBACK_TAP_AND_HOLD.get() + ? null + : vibrator; } /** @@ -32,4 +44,29 @@ public class DisableHapticFeedbackPatch { public static boolean disableZoomVibrate() { return Settings.DISABLE_HAPTIC_FEEDBACK_ZOOM.get(); } + + /** + * Injection point. + */ + public static void vibrate(Vibrator vibrator, VibrationEffect vibrationEffect) { + if (disableVibrate()) return; + vibrator.vibrate(vibrationEffect); + } + + /** + * Injection point. + */ + @SuppressWarnings("deprecation") + public static void vibrate(Vibrator vibrator, long milliseconds) { + if (disableVibrate()) return; + vibrator.vibrate(milliseconds); + } + + private static boolean disableVibrate() { + return Settings.DISABLE_HAPTIC_FEEDBACK_CHAPTERS.get() + && Settings.DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING.get() + && Settings.DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO.get() + && Settings.DISABLE_HAPTIC_FEEDBACK_TAP_AND_HOLD.get() + && Settings.DISABLE_HAPTIC_FEEDBACK_ZOOM.get(); + } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisablePlayerPopupPanelsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisablePlayerPopupPanelsPatch.java index dd2e0ca8f9..b33b2c25cc 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisablePlayerPopupPanelsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisablePlayerPopupPanelsPatch.java @@ -4,8 +4,10 @@ import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") public class DisablePlayerPopupPanelsPatch { - //Used by app.revanced.patches.youtube.layout.playerpopuppanels.patch.PlayerPopupPanelsPatch + /** + * Injection point. + */ public static boolean disablePlayerPopupPanels() { - return Settings.PLAYER_POPUP_PANELS.get(); + return Settings.DISABLE_PLAYER_POPUP_PANELS.get(); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableSignInToTvPopupPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableSignInToTVPopupPatch.java similarity index 69% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableSignInToTvPopupPatch.java rename to extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableSignInToTVPopupPatch.java index f6298ce8b6..baceefa1ab 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableSignInToTvPopupPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableSignInToTVPopupPatch.java @@ -3,12 +3,12 @@ package app.revanced.extension.youtube.patches; import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") -public class DisableSignInToTvPopupPatch { +public class DisableSignInToTVPopupPatch { /** * Injection point. */ public static boolean disableSignInToTvPopup() { - return Settings.DISABLE_SIGNIN_TO_TV_POPUP.get(); + return Settings.DISABLE_SIGN_IN_TO_TV_POPUP.get(); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DownloadsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DownloadsPatch.java index 4114d525c9..715d349e1d 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DownloadsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DownloadsPatch.java @@ -28,7 +28,7 @@ public final class DownloadsPatch { /** * Injection point. *

- * Called from the in app download hook, + * Called from the in-app download hook, * for both the player action button (below the video) * and the 'Download video' flyout option for feed videos. *

@@ -41,7 +41,7 @@ public final class DownloadsPatch { } // If possible, use the main activity as the context. - // Otherwise fall back on using the application context. + // Otherwise, fall back on using the application context. Context context = activityRef.get(); boolean isActivityContext = true; if (context == null) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ExitFullscreenPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ExitFullscreenPatch.java index 972c2cd924..abb2d6ba9e 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ExitFullscreenPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ExitFullscreenPatch.java @@ -20,8 +20,10 @@ public class ExitFullscreenPatch { /** * Injection point. */ - public static void endOfVideoReached() { + public static void endOfVideoReached(Enum status) { try { + if (status == null || !"ENDED".equals(status.name())) return; + FullscreenMode mode = Settings.EXIT_FULLSCREEN.get(); if (mode == FullscreenMode.DISABLED) { return; @@ -43,7 +45,7 @@ public class ExitFullscreenPatch { // set because the overlay controls are not attached. // To fix this, push the perform click to the back fo the main thread, // and by then the overlay controls will be visible since the video is now finished. - Utils.runOnMainThread(() -> { + Utils.runOnMainThreadDelayed(() -> { ImageView button = PlayerControlsPatch.fullscreenButtonRef.get(); if (button == null) { Logger.printDebug(() -> "Fullscreen button is null, cannot click"); @@ -54,7 +56,7 @@ public class ExitFullscreenPatch { button.performClick(); button.setSoundEffectsEnabled(soundEffectsEnabled); } - }); + }, 10); } } catch (Exception ex) { Logger.printException(() -> "endOfVideoReached failure", ex); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideGetPremiumPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideAutoplayPreviewPatch.java similarity index 55% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideGetPremiumPatch.java rename to extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideAutoplayPreviewPatch.java index 35592c0ff8..293fe9b151 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideGetPremiumPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideAutoplayPreviewPatch.java @@ -3,11 +3,11 @@ package app.revanced.extension.youtube.patches; import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") -public class HideGetPremiumPatch { +public final class HideAutoplayPreviewPatch { /** * Injection point. */ - public static boolean hideGetPremiumView() { - return Settings.HIDE_GET_PREMIUM.get(); + public static boolean hideAutoplayPreview() { + return Settings.HIDE_AUTOPLAY_PREVIEW.get(); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideEndScreenCardsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideEndScreenCardsPatch.java index 03668aa735..55dab84a55 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideEndScreenCardsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideEndScreenCardsPatch.java @@ -12,13 +12,13 @@ public class HideEndScreenCardsPatch { * Injection point. */ public static void hideEndScreenCardView(View view) { - Utils.hideViewUnderCondition(Settings.HIDE_ENDSCREEN_CARDS, view); + Utils.hideViewUnderCondition(Settings.HIDE_END_SCREEN_CARDS, view); } /** * Injection point. */ public static boolean hideEndScreenCards() { - return Settings.HIDE_ENDSCREEN_CARDS.get(); + return Settings.HIDE_END_SCREEN_CARDS.get(); } } \ No newline at end of file diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HidePlayerOverlayButtonsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HidePlayerOverlayButtonsPatch.java index a4c23aa11e..d7a413b790 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HidePlayerOverlayButtonsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HidePlayerOverlayButtonsPatch.java @@ -19,7 +19,7 @@ public final class HidePlayerOverlayButtonsPatch { /** * Injection point. */ - public static boolean hideAutoPlayButton() { + public static boolean hideAutoplayButton() { return HIDE_AUTOPLAY_BUTTON_ENABLED; } @@ -46,6 +46,41 @@ public final class HidePlayerOverlayButtonsPatch { imageView.setVisibility(Settings.HIDE_CAPTIONS_BUTTON.get() ? ImageView.GONE : ImageView.VISIBLE); } + /** + * Injection point. + */ + public static void hideCollapseButton(ImageView imageView) { + if (!Settings.HIDE_COLLAPSE_BUTTON.get()) return; + + // Make the collapse button invisible + imageView.setImageResource(android.R.color.transparent); + imageView.setImageAlpha(0); + imageView.setEnabled(false); + + // Adjust layout params if RelativeLayout + var layoutParams = imageView.getLayoutParams(); + if (layoutParams instanceof android.widget.RelativeLayout.LayoutParams) { + android.widget.RelativeLayout.LayoutParams lp = new android.widget.RelativeLayout.LayoutParams(0, 0); + imageView.setLayoutParams(lp); + } else { + Logger.printDebug(() -> "Unknown collapse button layout params: " + layoutParams); + } + } + + /** + * Injection point. + */ + public static void setTitleAnchorStartMargin(View titleAnchorView) { + if (!Settings.HIDE_COLLAPSE_BUTTON.get()) return; + + var layoutParams = titleAnchorView.getLayoutParams(); + if (layoutParams instanceof android.widget.RelativeLayout.LayoutParams relativeParams) { + relativeParams.setMarginStart(0); + } else { + Logger.printDebug(() -> "Unknown title anchor layout params: " + layoutParams); + } + } + private static final boolean HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS_ENABLED = Settings.HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS.get(); @@ -64,13 +99,28 @@ public final class HidePlayerOverlayButtonsPatch { } // Must use a deferred call to main thread to hide the button. - // Otherwise the layout crashes if set to hidden now. + // Otherwise, the layout crashes if set to hidden now. Utils.runOnMainThread(() -> { hideView(parentView, PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID); hideView(parentView, PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID); }); } + /** + * Injection point. + */ + public static ImageView hideFullscreenButton(ImageView imageView) { + if (!Settings.HIDE_FULLSCREEN_BUTTON.get()) { + return imageView; + } + + if (imageView != null) { + imageView.setVisibility(View.GONE); + } + + return null; + } + /** * Injection point. */ diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/LoopVideoPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/LoopVideoPatch.java index 8504aad931..425d719f33 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/LoopVideoPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/LoopVideoPatch.java @@ -7,7 +7,12 @@ public class LoopVideoPatch { /** * Injection point */ - public static boolean shouldLoopVideo() { - return Settings.LOOP_VIDEO.get(); + public static boolean shouldLoopVideo(Enum status) { + boolean shouldLoop = status != null && "ENDED".equals(status.name()) + && Settings.LOOP_VIDEO.get(); + + // Instead of calling a method to loop the video, just seek to 00:00. + if (shouldLoop) VideoInformation.seekTo(0); + return shouldLoop; } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java index f24108b97f..84fb868ac9 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java @@ -13,6 +13,7 @@ import android.widget.TextView; import androidx.annotation.Nullable; import java.util.List; +import java.util.Objects; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.ResourceType; @@ -29,7 +30,6 @@ public final class MiniplayerPatch { public enum MiniplayerType { /** * Disabled. When swiped down the miniplayer is immediately closed. - * Only available with 19.43+ */ DISABLED(false, null), /** Unmodified type, and same as un-patched. */ @@ -89,9 +89,9 @@ public final class MiniplayerPatch { final int HORIZONTAL_PADDING_DIP = 15; // Estimated padding. // Round down to the nearest 5 pixels, to keep any error toasts easier to read. final int estimatedWidthDipMax = 5 * ((deviceDipWidth - HORIZONTAL_PADDING_DIP) / 5); - // On some ultra low end devices the pixel width and density are the same number, + // On some ultra-low-end devices the pixel width and density are the same number, // which causes the estimate to always give a value of 1. - // Fix this by using a fixed size of double the min width. + // Fix this by using a fixed size twice the minimum width. final int WIDTH_DIP_MAX = estimatedWidthDipMax <= WIDTH_DIP_MIN ? 2 * WIDTH_DIP_MIN : estimatedWidthDipMax; @@ -140,8 +140,8 @@ public final class MiniplayerPatch { (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3 || CURRENT_TYPE == MODERN_4) && Settings.MINIPLAYER_HIDE_SUBTEXT.get(); - // 19.25 is last version that has forward/back buttons for phones, - // but buttons still show for tablets/foldable devices and they don't work well so always hide. + // 19.25 is last version that uses forward/back buttons for phones, + // but buttons still show for tablets/foldable devices, and they don't work well so always hide. private static final boolean HIDE_REWIND_FORWARD_ENABLED = CURRENT_TYPE == MODERN_1 && (VersionCheckPatch.IS_19_34_OR_GREATER || Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get()); @@ -281,6 +281,12 @@ public final class MiniplayerPatch { * Injection point. */ public static int getModernMiniplayerOverrideType(int original) { + if (CURRENT_TYPE == MINIMAL) { + // In newer app targets the minimal player can show the wrong icon if modern 4 is allowed. + // Forcing to modern 1 seems to work. + return Objects.requireNonNull(MODERN_1.modernPlayerType); + } + Integer modernValue = CURRENT_TYPE.modernPlayerType; return modernValue == null ? original @@ -385,7 +391,7 @@ public final class MiniplayerPatch { public static boolean allowBoldIcons(boolean original) { if (CURRENT_TYPE == MINIMAL) { // Minimal player does not have the correct pause/play icon (it's too large). - // Use the non bold icons instead. + // Use the non-bold icons instead. return false; } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/NavigationBarPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/NavigationBarPatch.java new file mode 100644 index 0000000000..7de8bce90c --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/NavigationBarPatch.java @@ -0,0 +1,205 @@ +package app.revanced.extension.youtube.patches; + +import static app.revanced.extension.shared.Utils.hideViewUnderCondition; +import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; + +import android.os.Build; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; + +import java.util.EnumMap; +import java.util.Map; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.ui.Dim; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class NavigationBarPatch { + + private static final Map shouldHideMap = new EnumMap<>(NavigationButton.class) { + { + put(NavigationButton.HOME, Settings.HIDE_HOME_BUTTON.get()); + put(NavigationButton.CREATE, Settings.HIDE_CREATE_BUTTON.get()); + put(NavigationButton.NOTIFICATIONS, Settings.HIDE_NOTIFICATIONS_BUTTON.get()); + put(NavigationButton.SHORTS, Settings.HIDE_SHORTS_BUTTON.get()); + put(NavigationButton.SUBSCRIPTIONS, Settings.HIDE_SUBSCRIPTIONS_BUTTON.get()); + } + }; + + private static final boolean SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON + = Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get(); + + private static final boolean DISABLE_TRANSLUCENT_STATUS_BAR + = Settings.DISABLE_TRANSLUCENT_STATUS_BAR.get(); + + private static final boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT + = Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT.get(); + + private static final boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK + = Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK.get(); + + private static final boolean NARROW_NAVIGATION_BUTTONS + = Settings.NARROW_NAVIGATION_BUTTONS.get(); + + /** + * Injection point. + */ + public static String switchCreateWithNotificationButton(String osName) { + return SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON + ? "Android Automotive" + : osName; + } + + /** + * Injection point. + */ + public static void navigationTabCreated(NavigationButton button, View tabView) { + if (Boolean.TRUE.equals(shouldHideMap.get(button))) { + tabView.setVisibility(View.GONE); + } + } + + /** + * Injection point. + */ + public static void hideNavigationButtonLabels(TextView navigationLabelsView) { + hideViewUnderCondition(Settings.HIDE_NAVIGATION_BUTTON_LABELS, navigationLabelsView); + } + + /** + * Injection point. + */ + public static boolean useAnimatedNavigationButtons(boolean original) { + return Settings.NAVIGATION_BAR_ANIMATIONS.get(); + } + + /** + * Injection point. + */ + public static boolean enableNarrowNavigationButton(boolean original) { + return NARROW_NAVIGATION_BUTTONS || original; + } + + /** + * Injection point. + */ + public static boolean useTranslucentNavigationStatusBar(boolean original) { + // Must check Android version, as forcing this on Android 11 or lower causes app hang and crash. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return original; + } + + if (DISABLE_TRANSLUCENT_STATUS_BAR) { + return false; + } + + return original; + } + + /** + * Injection point. + */ + public static boolean useTranslucentNavigationButtons(boolean original) { + // Feature requires Android 13+ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return original; + } + + if (!DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK && !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT) { + return original; + } + + if (DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK && DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT) { + return false; + } + + return Utils.isDarkModeEnabled() + ? !DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK + : !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT; + } + + // Toolbar + + public static void hideCreateButton(String enumString, View view) { + if (!Settings.HIDE_TOOLBAR_CREATE_BUTTON.get()) + return; + + hideViewUnderCondition(isCreateButton(enumString), view); + } + + public static void hideNotificationButton(String enumString, View view) { + if (!Settings.HIDE_TOOLBAR_NOTIFICATION_BUTTON.get()) + return; + + hideViewUnderCondition(isNotificationButton(enumString), view); + } + + public static void hideSearchButton(String enumString, View view) { + if (!Settings.HIDE_TOOLBAR_SEARCH_BUTTON.get()) + return; + + hideViewUnderCondition(isSearchButton(enumString), view); + } + + public static void hideSearchButton(MenuItem menuItem, int original) { + menuItem.setShowAsAction( + Settings.HIDE_TOOLBAR_SEARCH_BUTTON.get() + ? MenuItem.SHOW_AS_ACTION_NEVER + : original + ); + } + + private static boolean isCreateButton(String enumString) { + return "CREATION_ENTRY".equals(enumString) // Create button for Phone layout. + || "FAB_CAMERA".equals(enumString); // Create button for Tablet layout. + } + + private static boolean isNotificationButton(String enumString) { + return "TAB_ACTIVITY".equals(enumString) // Notification button. + || "TAB_ACTIVITY_CAIRO".equals(enumString); // Notification button (New layout). + } + + private static boolean isSearchButton(String enumString) { + return "SEARCH".equals(enumString) // Search button. + || "SEARCH_CAIRO".equals(enumString) // Search button (New layout). + || "SEARCH_BOLD".equals(enumString); // Search button (Shorts). + } + + // Wide searchbar + private static final Boolean WIDE_SEARCHBAR_ENABLED = Settings.WIDE_SEARCHBAR.get(); + + /** + * Injection point. + */ + public static boolean enableWideSearchbar(boolean original) { + return WIDE_SEARCHBAR_ENABLED || original; + } + + /** + * Injection point. + */ + public static void setActionBar(View view) { + try { + if (!WIDE_SEARCHBAR_ENABLED) return; + + View searchBarView = Utils.getChildViewByResourceName(view, "search_bar"); + + final int paddingLeft = searchBarView.getPaddingLeft(); + final int paddingRight = searchBarView.getPaddingRight(); + final int paddingTop = searchBarView.getPaddingTop(); + final int paddingBottom = searchBarView.getPaddingBottom(); + final int paddingStart = Dim.dp8; + + if (Utils.isRightToLeftLocale()) { + searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom); + } else { + searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom); + } + } catch (Exception ex) { + Logger.printException(() -> "setActionBar failure", ex); + } + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/NavigationButtonsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/NavigationButtonsPatch.java deleted file mode 100644 index 76a3156596..0000000000 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/NavigationButtonsPatch.java +++ /dev/null @@ -1,108 +0,0 @@ -package app.revanced.extension.youtube.patches; - -import static app.revanced.extension.shared.Utils.hideViewUnderCondition; -import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; - -import android.os.Build; -import android.view.View; -import android.widget.TextView; - -import java.util.EnumMap; -import java.util.Map; - -import app.revanced.extension.shared.Utils; -import app.revanced.extension.youtube.settings.Settings; - -@SuppressWarnings("unused") -public final class NavigationButtonsPatch { - - private static final Map shouldHideMap = new EnumMap<>(NavigationButton.class) { - { - put(NavigationButton.HOME, Settings.HIDE_HOME_BUTTON.get()); - put(NavigationButton.CREATE, Settings.HIDE_CREATE_BUTTON.get()); - put(NavigationButton.NOTIFICATIONS, Settings.HIDE_NOTIFICATIONS_BUTTON.get()); - put(NavigationButton.SHORTS, Settings.HIDE_SHORTS_BUTTON.get()); - put(NavigationButton.SUBSCRIPTIONS, Settings.HIDE_SUBSCRIPTIONS_BUTTON.get()); - } - }; - - private static final boolean SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON - = Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get(); - - private static final boolean DISABLE_TRANSLUCENT_STATUS_BAR - = Settings.DISABLE_TRANSLUCENT_STATUS_BAR.get(); - - private static final boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT - = Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT.get(); - - private static final boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK - = Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK.get(); - - /** - * Injection point. - */ - public static boolean switchCreateWithNotificationButton() { - return SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON; - } - - /** - * Injection point. - */ - public static void navigationTabCreated(NavigationButton button, View tabView) { - if (Boolean.TRUE.equals(shouldHideMap.get(button))) { - tabView.setVisibility(View.GONE); - } - } - - /** - * Injection point. - */ - public static void hideNavigationButtonLabels(TextView navigationLabelsView) { - hideViewUnderCondition(Settings.HIDE_NAVIGATION_BUTTON_LABELS, navigationLabelsView); - } - - /** - * Injection point. - */ - public static boolean useAnimatedNavigationButtons(boolean original) { - return Settings.NAVIGATION_BAR_ANIMATIONS.get(); - } - - /** - * Injection point. - */ - public static boolean useTranslucentNavigationStatusBar(boolean original) { - // Must check Android version, as forcing this on Android 11 or lower causes app hang and crash. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - return original; - } - - if (DISABLE_TRANSLUCENT_STATUS_BAR) { - return false; - } - - return original; - } - - /** - * Injection point. - */ - public static boolean useTranslucentNavigationButtons(boolean original) { - // Feature requires Android 13+ - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - return original; - } - - if (!DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK && !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT) { - return original; - } - - if (DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK && DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT) { - return false; - } - - return Utils.isDarkModeEnabled() - ? !DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK - : !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT; - } -} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/OpenShortsInRegularPlayerPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/OpenShortsInRegularPlayerPatch.java index 02fc01c9fa..937d35260b 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/OpenShortsInRegularPlayerPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/OpenShortsInRegularPlayerPatch.java @@ -31,6 +31,13 @@ public class OpenShortsInRegularPlayerPatch { mainActivityRef = new WeakReference<>(activity); } + /** + * Injection point. + */ + public static boolean overrideBackPressToExit() { + return overrideBackPressToExit(true); + } + /** * Injection point. */ @@ -46,7 +53,7 @@ public class OpenShortsInRegularPlayerPatch { /** * Injection point. */ - public static boolean openShort(String videoID) { + public static boolean openShort(String videoId) { try { ShortsPlayerType type = Settings.SHORTS_PLAYER_TYPE.get(); if (type == ShortsPlayerType.SHORTS_PLAYER) { @@ -54,7 +61,7 @@ public class OpenShortsInRegularPlayerPatch { return false; // Default unpatched behavior. } - if (videoID.isEmpty()) { + if (videoId.isEmpty()) { // Shorts was opened using launcher app shortcut. // // This check will not detect if the Shorts app shortcut is used @@ -84,12 +91,12 @@ public class OpenShortsInRegularPlayerPatch { // Can use the application context and add intent flags of // FLAG_ACTIVITY_NEW_TASK and FLAG_ACTIVITY_CLEAR_TOP // But the activity context seems to fix random app crashes - // if Shorts urls are opened outside the app. + // if Shorts URLs are opened outside the app. var context = mainActivityRef.get(); Intent videoPlayerIntent = new Intent( Intent.ACTION_VIEW, - Uri.parse("https://youtube.com/watch?v=" + videoID) + Uri.parse("https://youtube.com/watch?v=" + videoId) ); videoPlayerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); videoPlayerIntent.setPackage(context.getPackageName()); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/RemoveViewerDiscretionDialogPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/RemoveViewerDiscretionDialogPatch.java index 6f2ea968d2..8b3437316f 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/RemoveViewerDiscretionDialogPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/RemoveViewerDiscretionDialogPatch.java @@ -1,6 +1,12 @@ package app.revanced.extension.youtube.patches; import android.app.AlertDialog; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; @@ -8,22 +14,89 @@ import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") public class RemoveViewerDiscretionDialogPatch { + private static final String[] VIEWER_DISCRETION_DIALOG_PLAYABILITY_STATUS = { + "AGE_CHECK_REQUIRED", + "AGE_VERIFICATION_REQUIRED", + "CONTENT_CHECK_REQUIRED", + "LOGIN_REQUIRED" + }; + @NonNull + private static volatile String playabilityStatus = ""; /** * Injection point. */ public static void confirmDialog(AlertDialog dialog) { - if (Settings.REMOVE_VIEWER_DISCRETION_DIALOG.get()) { - Logger.printDebug(() -> "Clicking alert dialog dismiss button"); - - final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); - button.setSoundEffectsEnabled(false); - button.performClick(); - return; + // The dialog may already be shown due to the AlertDialog#create() method. + // Call the AlertDialog#show() method only when the dialog is not shown. + if (!dialog.isShowing()) { + // Since the patch replaces the AlertDialog#show() method, we need to call the original method here. + dialog.show(); } - // Since the patch replaces the AlertDialog#show() method, we need to call the original method here. - Logger.printDebug(() -> "Showing alert dialog"); - dialog.show(); + if (shouldConfirmDialog()) { + Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + if (button != null) { + Window window = dialog.getWindow(); + if (window != null) { + // Resize the dialog to 0 before clicking the button. + // If the dialog is not resized to 0, it will remain visible for about a second before closing. + WindowManager.LayoutParams params = window.getAttributes(); + params.height = 0; + params.width = 0; + + // Change the size of AlertDialog to 0. + window.setAttributes(params); + + // Disable AlertDialog's background dim. + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + } + Logger.printDebug(() -> "Clicking alert dialog dismiss button"); + button.callOnClick(); + } + } + } + + /** + * Injection point. + */ + public static AlertDialog confirmDialog(AlertDialog.Builder builder) { + AlertDialog dialog = builder.create(); + confirmDialog(dialog); + return dialog; + } + + /** + * Injection point. + * Modern-style dialog is controlled by an obfuscated class and require additional hooking to get the buttons. + * Disabling the modern-style dialog is the simplest workaround. + * Since the purpose of the patch is to close the dialog immediately, this isn't a problem. + * + * @return Whether to use modern-style dialog. + * If false, AlertDialog is used. + */ + public static boolean disableModernDialog(boolean original) { + return !shouldConfirmDialog() && original; + } + + /** + * Injection point. + * + * @param status Enum value of 'playabilityStatus.status' in '/player' endpoint responses. + */ + public static void setPlayabilityStatus(@Nullable Enum status) { + playabilityStatus = status == null ? "" : status.name(); + } + + /** + * The viewer discretion dialog shows when the playability status is + * [AGE_CHECK_REQUIRED], [AGE_VERIFICATION_REQUIRED], [CONTENT_CHECK_REQUIRED], or [LOGIN_REQUIRED]. + * Verify the playability status to prevent unintended dialog closures. + * + * @return Whether to close the dialog. + */ + private static boolean shouldConfirmDialog() { + return Settings.REMOVE_VIEWER_DISCRETION_DIALOG.get() + && Utils.containsAny(playabilityStatus, VIEWER_DISCRETION_DIALOG_PLAYABILITY_STATUS); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java index bb0c7dec9b..beaa3eac82 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java @@ -49,21 +49,21 @@ public class ReturnYouTubeDislikePatch { /** * The last litho based Shorts loaded. - * May be the same value as {@link #currentVideoData}, but usually is the next short to swipe to. + * Maybe the same value as {@link #currentVideoData}, but usually is the next short to swipe to. */ @Nullable private static volatile ReturnYouTubeDislike lastLithoShortsVideoData; /** * Because litho Shorts spans are created offscreen after {@link ReturnYouTubeDislikeFilter} - * detects the video ids, but the current Short can arbitrarily reload the same span, + * detects the video IDs, but the current Short can arbitrarily reload the same span, * then use the {@link #lastLithoShortsVideoData} if this value is greater than zero. */ @GuardedBy("ReturnYouTubeDislikePatch.class") private static int useLithoShortsVideoDataCount; /** - * Last video id prefetched. Field is to prevent prefetching the same video id multiple times in a row. + * Last video ID prefetched. Field is to prevent prefetching the same video ID multiple times in a row. */ @Nullable private static volatile String lastPrefetchedVideoId; @@ -98,6 +98,19 @@ public class ReturnYouTubeDislikePatch { // Litho player for both regular videos and Shorts. // + /** + * Injection point. + * + * Logs if new litho text layout is used. + */ + public static boolean useNewLithoTextCreation(boolean useNewLithoTextCreation) { + // Don't force flag on/off unless debugging patch hooks, + // because forcing off with newer YT targets causes Shorts player to show no buttons, + // presumably because the old litho data isn't in the layout data. + Logger.printDebug(() -> "useNewLithoTextCreation: " + useNewLithoTextCreation); + return useNewLithoTextCreation; + } + /** * Injection point. * @@ -113,7 +126,7 @@ public class ReturnYouTubeDislikePatch { * Called when a litho text component is initially created, * and also when a Span is later reused again (such as scrolling off/on screen). * - * This method is sometimes called on the main thread, but it usually is called _off_ the main thread. + * This method is sometimes called on the main thread, but it is usually called _off_ the main thread. * This method can be called multiple times for the same UI element (including after dislikes was added). * * @param original Original char sequence was created or reused by Litho. @@ -185,14 +198,14 @@ public class ReturnYouTubeDislikePatch { final ReturnYouTubeDislike videoData; if (decrementUseLithoDataIfNeeded()) { - // New Short is loading off screen. + // New Short is loading off-screen. videoData = lastLithoShortsVideoData; } else { videoData = currentVideoData; } if (videoData == null) { - // The Shorts litho video id filter did not detect the video id. + // The Shorts litho video ID filter did not detect the video ID. // This is normal in incognito mode, but otherwise is abnormal. Logger.printDebug(() -> "Cannot modify Shorts litho span, data is null"); return original; @@ -292,7 +305,7 @@ public class ReturnYouTubeDislikePatch { /** * Remove Rolling Number text view modifications made by this patch. - * Required as it appears text views can be reused for other rolling numbers (view count, upload time, etc). + * Required as it appears text views can be reused for other rolling numbers (view count, upload time, etc.). */ private static void removeRollingNumberPatchChanges(TextView view) { if (view.getCompoundDrawablePadding() != 0) { @@ -314,7 +327,7 @@ public class ReturnYouTubeDislikePatch { return original; } // Called for all instances of RollingNumber, so must check if text is for a dislikes. - // Text will already have the correct content but it's missing the drawable separators. + // Text will already have the correct content, but it's missing the drawable separators. if (!ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(original.toString())) { // The text is the video view count, upload time, or some other text. removeRollingNumberPatchChanges(view); @@ -351,13 +364,13 @@ public class ReturnYouTubeDislikePatch { } // - // Video Id and voting hooks (all players). + // Video ID and voting hooks (all players). // private static volatile boolean lastPlayerResponseWasShort; /** - * Injection point. Uses 'playback response' video id hook to preload RYD. + * Injection point. Uses 'playback response' video ID hook to preload RYD. */ public static void preloadVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) { try { @@ -387,7 +400,7 @@ public class ReturnYouTubeDislikePatch { if (waitForFetchToComplete && !fetch.fetchCompleted()) { // This call is off the main thread, so wait until the RYD fetch completely finishes, // otherwise if this returns before the fetch completes then the UI can - // become frozen when the main thread tries to modify the litho Shorts dislikes and + // become frozen when the main thread tries to modify the litho Shorts dislikes, and // it must wait for the fetch. // Only need to do this for the first Short opened, as the next Short to swipe to // are preloaded in the background. @@ -406,7 +419,7 @@ public class ReturnYouTubeDislikePatch { } /** - * Injection point. Uses 'current playing' video id hook. Always called on main thread. + * Injection point. Uses 'current playing' video ID hook. Always called on main thread. */ public static void newVideoLoaded(@NonNull String videoId) { try { @@ -424,7 +437,7 @@ public class ReturnYouTubeDislikePatch { if (videoIdIsSame(currentVideoData, videoId)) { return; } - Logger.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType); + Logger.printDebug(() -> "New video ID: " + videoId + " playerType: " + currentPlayerType); if (!Utils.isNetworkConnected()) { Logger.printDebug(() -> "Cannot fetch RYD, network is not connected"); @@ -450,16 +463,16 @@ public class ReturnYouTubeDislikePatch { } if (videoId == null) { - // Litho filter did not detect the video id. App is in incognito mode, - // or the proto buffer structure was changed and the video id is no longer present. + // Litho filter did not detect the video ID. App is in incognito mode, + // or the proto buffer structure was changed and the video ID is no longer present. // Must clear both currently playing and last litho data otherwise the // next regular video may use the wrong data. - Logger.printDebug(() -> "Litho filter did not find any video ids"); + Logger.printDebug(() -> "Litho filter did not find any video IDs"); clearData(); return; } - Logger.printDebug(() -> "New litho Shorts video id: " + videoId); + Logger.printDebug(() -> "New litho Shorts video ID: " + videoId); ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(videoId); videoData.setVideoIdIsShort(true); lastLithoShortsVideoData = videoData; diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/SeekbarThumbnailsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/SeekbarThumbnailsPatch.java deleted file mode 100644 index 30722c0878..0000000000 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/SeekbarThumbnailsPatch.java +++ /dev/null @@ -1,39 +0,0 @@ -package app.revanced.extension.youtube.patches; - -import app.revanced.extension.shared.settings.Setting; -import app.revanced.extension.youtube.settings.Settings; - -import java.util.List; - -@SuppressWarnings("unused") -public class SeekbarThumbnailsPatch { - - public static final class SeekbarThumbnailsHighQualityAvailability implements Setting.Availability { - @Override - public boolean isAvailable() { - return VersionCheckPatch.IS_19_17_OR_GREATER || !Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get(); - } - - @Override - public List> getParentSettings() { - return List.of(Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS); - } - } - - private static final boolean SEEKBAR_THUMBNAILS_HIGH_QUALITY_ENABLED - = Settings.SEEKBAR_THUMBNAILS_HIGH_QUALITY.get(); - - /** - * Injection point. - */ - public static boolean useHighQualityFullscreenThumbnails() { - return SEEKBAR_THUMBNAILS_HIGH_QUALITY_ENABLED; - } - - /** - * Injection point. - */ - public static boolean useFullscreenSeekbarThumbnails() { - return !Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get(); - } -} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java index e7de421368..905ca6dfa0 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java @@ -109,7 +109,7 @@ public class ShortsAutoplayPatch { } if (original == null) { - // Cannot return null, as null is used to indicate Short was auto played. + // Cannot return null, as null is used to indicate the Short was autoplayed. // Unpatched app replaces null with unknown enum type (appears to fix for bad api data). Enum unknown = ShortsLoopBehavior.UNKNOWN.ytEnumValue; Logger.printDebug(() -> "Original is null, returning: " + unknown.name()); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/SeekbarTappingPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/TapToSeekPatch.java similarity index 50% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/SeekbarTappingPatch.java rename to extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/TapToSeekPatch.java index dbbb363e1f..4fbc8fdf2c 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/SeekbarTappingPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/TapToSeekPatch.java @@ -3,8 +3,8 @@ package app.revanced.extension.youtube.patches; import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") -public final class SeekbarTappingPatch { - public static boolean seekbarTappingEnabled() { - return Settings.SEEKBAR_TAPPING.get(); +public final class TapToSeekPatch { + public static boolean tapToSeekEnabled() { + return Settings.TAP_TO_SEEK.get(); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ToolbarPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ToolbarPatch.java new file mode 100644 index 0000000000..c33e54e5b2 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ToolbarPatch.java @@ -0,0 +1,33 @@ +package app.revanced.extension.youtube.patches; + +import android.view.View; +import android.widget.ImageView; + +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public class ToolbarPatch { + + /** + * Injection point. + */ + public static void hookToolbar(Enum buttonEnum, ImageView imageView) { + final String enumString = buttonEnum.name(); + if (enumString.isEmpty() || + imageView == null || + !(imageView.getParent() instanceof View view)) { + return; + } + + Logger.printDebug(() -> "enumString: " + enumString); + + hookToolbar(enumString, view); + } + + /** + * Injection point. + */ + private static void hookToolbar(String enumString, View parentView) { + // Code added by patch. + } +} \ No newline at end of file diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoAdsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoAdsPatch.java index ce07d8d16d..22447193c9 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoAdsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoAdsPatch.java @@ -14,4 +14,13 @@ public class VideoAdsPatch { return SHOW_VIDEO_ADS; } + /** + * Injection point. + */ + public static String hideShortsAds(String osName) { + return SHOW_VIDEO_ADS + ? osName + : "Android Automotive"; + } + } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoInformation.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoInformation.java index 9c9b060a7b..934876578b 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoInformation.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoInformation.java @@ -3,15 +3,13 @@ package app.revanced.extension.youtube.patches; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.android.libraries.youtube.innertube.model.media.VideoQuality; - import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.Objects; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; -import app.revanced.extension.youtube.Event; +import app.revanced.extension.youtube.shared.Event; import app.revanced.extension.youtube.shared.ShortsPlayerState; import app.revanced.extension.youtube.shared.VideoState; @@ -32,11 +30,20 @@ public final class VideoInformation { */ public interface VideoQualityMenuInterface { // Method is added during patching. - void patch_setQuality(VideoQuality quality); + void patch_setQuality(VideoQualityInterface quality); } /** - * Video resolution of the automatic quality option.. + * Interface to use obfuscated methods. + */ + public interface VideoQualityInterface { + // Methods are added during patching. + String patch_getQualityName(); + int patch_getResolution(); + } + + /** + * Video resolution of the automatic quality option. */ public static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2; @@ -44,7 +51,7 @@ public final class VideoInformation { * Video quality names are the same text for all languages. * Premium can be "1080p Premium" or "1080p60 Premium" */ - public static final String VIDEO_QUALITY_PREMIUM_NAME = "Premium"; + private static final String VIDEO_QUALITY_PREMIUM_NAME = "Premium"; private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; /** @@ -76,14 +83,14 @@ public final class VideoInformation { * The available qualities of the current video. */ @Nullable - private static VideoQuality[] currentQualities; + private static VideoQualityInterface[] currentQualities; /** * The current quality of the video playing. * This is always the actual quality even if Automatic quality is active. */ @Nullable - private static VideoQuality currentQuality; + private static VideoQualityInterface currentQuality; /** * The current VideoQualityMenuInterface, set during setVideoQuality. @@ -94,15 +101,15 @@ public final class VideoInformation { /** * Callback for when the current quality changes. */ - public static final Event onQualityChange = new Event<>(); + public static final Event onQualityChange = new Event<>(); @Nullable - public static VideoQuality[] getCurrentQualities() { + public static VideoQualityInterface[] getCurrentQualities() { return currentQualities; } @Nullable - public static VideoQuality getCurrentQuality() { + public static VideoQualityInterface getCurrentQuality() { return currentQuality; } @@ -133,7 +140,7 @@ public final class VideoInformation { * * @param mdxPlayerDirector MDX player director object (casting mode). */ - public static void initializeMdx(@NonNull PlaybackController mdxPlayerDirector) { + public static void initializeMDX(@NonNull PlaybackController mdxPlayerDirector) { try { mdxPlayerDirectorRef = new WeakReference<>(Objects.requireNonNull(mdxPlayerDirector)); } catch (Exception ex) { @@ -144,11 +151,11 @@ public final class VideoInformation { /** * Injection point. * - * @param newlyLoadedVideoId id of the current video + * @param newlyLoadedVideoId ID of the current video */ public static void setVideoId(@NonNull String newlyLoadedVideoId) { if (!videoId.equals(newlyLoadedVideoId)) { - Logger.printDebug(() -> "New video id: " + newlyLoadedVideoId); + Logger.printDebug(() -> "New video ID: " + newlyLoadedVideoId); videoId = newlyLoadedVideoId; } } @@ -178,11 +185,11 @@ public final class VideoInformation { /** * Injection point. Called off the main thread. * - * @param videoId The id of the last video loaded. + * @param videoId The ID of the last video loaded. */ public static void setPlayerResponseVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) { if (!playerResponseVideoId.equals(videoId)) { - Logger.printDebug(() -> "New player response video id: " + videoId); + Logger.printDebug(() -> "New player response video ID: " + videoId); playerResponseVideoId = videoId; } } @@ -273,7 +280,7 @@ public final class VideoInformation { // The difference has to be a different second mark in order to avoid infinite skip loops // as the Lounge API only supports whole seconds. if (adjustedSeekTime / 1000 == videoTime / 1000) { - Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small " + Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small" + "(" + (adjustedSeekTime - videoTime) + "ms)"); return false; } @@ -301,7 +308,7 @@ public final class VideoInformation { Logger.printDebug(() -> "Seeking relative to: " + seekTime); // 19.39+ does not have a boolean return type for relative seek. - // But can call both methods and it works correctly for both situations. + // But can call both methods, and it works correctly for both situations. PlaybackController controller = playerControllerRef.get(); if (controller == null) { Logger.printDebug(() -> "Cannot seek relative as player controller is null"); @@ -310,7 +317,7 @@ public final class VideoInformation { } // Adjust the fine adjustment function so it's at least 1 second before/after. - // Otherwise the fine adjustment will do nothing when casting. + // Otherwise, the fine adjustment will do nothing when casting. final long adjustedSeekTime; if (seekTime < 0) { adjustedSeekTime = Math.min(seekTime, -1000); @@ -330,9 +337,9 @@ public final class VideoInformation { } /** - * Id of the last video opened. Includes Shorts. + * ID of the last video opened. Includes Shorts. * - * @return The id of the video, or an empty string if no videos have been opened yet. + * @return The ID of the video, or an empty string if no videos have been opened yet. */ @NonNull public static String getVideoId() { @@ -340,7 +347,7 @@ public final class VideoInformation { } /** - * Differs from {@link #videoId} as this is the video id for the + * Differs from {@link #videoId} as this is the video ID for the * last player response received, which may not be the last video opened. *

* If Shorts are loading the background, this commonly will be @@ -348,7 +355,7 @@ public final class VideoInformation { *

* For most use cases, you should instead use {@link #getVideoId()}. * - * @return The id of the last video loaded, or an empty string if no videos have been loaded yet. + * @return The ID of the last video loaded, or an empty string if no videos have been loaded yet. */ @NonNull public static String getPlayerResponseVideoId() { @@ -356,8 +363,8 @@ public final class VideoInformation { } /** - * @return If the last player response video id was a Short. - * Includes Shorts shelf items appearing in the feed that are not opened. + * @return If the last player response video ID was a Short. + * Include Shorts shelf items appearing in the feed that are not opened. * @see #lastVideoIdIsShort() */ public static boolean lastPlayerResponseIsShort() { @@ -365,7 +372,7 @@ public final class VideoInformation { } /** - * @return If the last player response video id _that was opened_ was a Short. + * @return If the last player response video ID _that was opened_ was a Short. */ public static boolean lastVideoIdIsShort() { return videoIdIsShort; @@ -450,7 +457,7 @@ public final class VideoInformation { qualityNeedsUpdating = true; } - private static void setCurrentQuality(@Nullable VideoQuality quality) { + private static void setCurrentQuality(@Nullable VideoQualityInterface quality) { Utils.verifyOnMainThread(); if (currentQuality != quality) { Logger.printDebug(() -> "Current quality changed to: " + quality); @@ -462,7 +469,7 @@ public final class VideoInformation { /** * Forcefully changes the video quality of the currently playing video. */ - public static void changeQuality(VideoQuality quality) { + public static void changeQuality(VideoQualityInterface quality) { Utils.verifyOnMainThread(); if (currentMenuInterface == null) { @@ -501,7 +508,7 @@ public final class VideoInformation { * @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2 * @param originalQualityIndex quality index to use, as chosen by YouTube */ - public static int setVideoQuality(VideoQuality[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) { + public static int setVideoQuality(VideoQualityInterface[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) { try { Utils.verifyOnMainThread(); currentMenuInterface = menu; @@ -516,7 +523,7 @@ public final class VideoInformation { // On extremely slow internet connections the index can initially be -1 originalQualityIndex = Math.max(0, originalQualityIndex); - VideoQuality updatedCurrentQuality = qualities[originalQualityIndex]; + VideoQualityInterface updatedCurrentQuality = qualities[originalQualityIndex]; if (updatedCurrentQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE && (currentQuality == null || currentQuality != updatedCurrentQuality)) { setCurrentQuality(updatedCurrentQuality); @@ -538,7 +545,7 @@ public final class VideoInformation { // Find the highest quality that is equal to or less than the preferred. int i = 0; final int lastQualityIndex = qualities.length - 1; - for (VideoQuality quality : qualities) { + for (VideoQualityInterface quality : qualities) { final int qualityResolution = quality.patch_getResolution(); if ((qualityResolution != AUTOMATIC_VIDEO_QUALITY_VALUE && qualityResolution <= preferredQuality) // Use the lowest video quality if the default is lower than all available. @@ -572,4 +579,9 @@ public final class VideoInformation { } return originalQualityIndex; } + + public static boolean isPremiumVideoQuality(@NonNull VideoQualityInterface quality) { + String qualityName = quality.patch_getQualityName(); + return qualityName != null && qualityName.contains(VIDEO_QUALITY_PREMIUM_NAME); + } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java deleted file mode 100644 index 094ce457e7..0000000000 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java +++ /dev/null @@ -1,46 +0,0 @@ -package app.revanced.extension.youtube.patches; - -import android.view.View; - -import app.revanced.extension.shared.Logger; -import app.revanced.extension.shared.Utils; -import app.revanced.extension.shared.ui.Dim; -import app.revanced.extension.youtube.settings.Settings; - -@SuppressWarnings("unused") -public final class WideSearchbarPatch { - - private static final Boolean WIDE_SEARCHBAR_ENABLED = Settings.WIDE_SEARCHBAR.get(); - - /** - * Injection point. - */ - public static boolean enableWideSearchbar(boolean original) { - return WIDE_SEARCHBAR_ENABLED || original; - } - - /** - * Injection point. - */ - public static void setActionBar(View view) { - try { - if (!WIDE_SEARCHBAR_ENABLED) return; - - View searchBarView = Utils.getChildViewByResourceName(view, "search_bar"); - - final int paddingLeft = searchBarView.getPaddingLeft(); - final int paddingRight = searchBarView.getPaddingRight(); - final int paddingTop = searchBarView.getPaddingTop(); - final int paddingBottom = searchBarView.getPaddingBottom(); - final int paddingStart = Dim.dp8; - - if (Utils.isRightToLeftLocale()) { - searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom); - } else { - searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom); - } - } catch (Exception ex) { - Logger.printException(() -> "setActionBar failure", ex); - } - } -} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java index b19aa7a901..1212416069 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java @@ -36,7 +36,7 @@ public final class AnnouncementsPatch { HttpURLConnection connection = AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT_IDS); - Logger.printDebug(() -> "Get latest announcement IDs route connection url: " + connection.getURL()); + Logger.printDebug(() -> "Get latest announcement IDs route connection URL: " + connection.getURL()); try { // Do not show the announcement if the request failed. @@ -59,10 +59,10 @@ public final class AnnouncementsPatch { // Parse the ID. Fall-back to raw string if it fails. int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue; try { - final var announcementIds = new JSONArray(jsonString); - if (announcementIds.length() == 0) return true; + final var announcementIDs = new JSONArray(jsonString); + if (announcementIDs.length() == 0) return true; - id = announcementIds.getJSONObject(0).getInt("id"); + id = announcementIDs.getJSONObject(0).getInt("id"); } catch (Throwable ex) { Logger.printException(() -> "Failed to parse announcement ID", ex); } @@ -84,7 +84,7 @@ public final class AnnouncementsPatch { HttpURLConnection connection = AnnouncementsRoutes .getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENTS); - Logger.printDebug(() -> "Get latest announcements route connection url: " + connection.getURL()); + Logger.printDebug(() -> "Get latest announcements route connection URL: " + connection.getURL()); var jsonString = Requester.parseStringAndDisconnect(connection); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdsFilter.java index 064a7f7e9b..cb262e0165 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdsFilter.java @@ -2,9 +2,13 @@ package app.revanced.extension.youtube.patches.litho; import static app.revanced.extension.shared.StringRef.str; -import android.app.Instrumentation; +import android.app.Dialog; import android.view.KeyEvent; import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.Nullable; import java.util.List; @@ -19,12 +23,18 @@ import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") public final class AdsFilter extends Filter { // region Fullscreen ad - private static volatile long lastTimeClosedFullscreenAd; - private static final Instrumentation instrumentation = new Instrumentation(); - private final StringFilterGroup fullscreenAd; + private static final ByteArrayFilterGroup fullscreenAd = new ByteArrayFilterGroup( + null, + "_interstitial" + ); // endregion + private static final String[] PLAYER_POPUP_AD_PANEL_IDS = { + "PAproduct", // Shopping. + "jumpahead" // Premium promotion. + }; + // https://encrypted-tbn0.gstatic.com/shopping?q=abc private static final String STORE_BANNER_DOMAIN = "gstatic.com/shopping"; private static final boolean HIDE_END_SCREEN_STORE_BANNER = @@ -32,9 +42,10 @@ public final class AdsFilter extends Filter { private final StringTrieSearch exceptions = new StringTrieSearch(); - private final StringFilterGroup playerShoppingShelf; - private final ByteArrayFilterGroup playerShoppingShelfBuffer; - + private final StringFilterGroup promotionBanner; + private final ByteArrayFilterGroup promotionBannerBuffer; + private final StringFilterGroup buyMovieAd; + private final ByteArrayFilterGroup buyMovieAdBuffer; public AdsFilter() { exceptions.addPatterns( @@ -47,7 +58,6 @@ public final class AdsFilter extends Filter { // Identifiers. - final var carouselAd = new StringFilterGroup( Settings.HIDE_GENERAL_ADS, "carousel_ad" @@ -56,11 +66,6 @@ public final class AdsFilter extends Filter { // Paths. - fullscreenAd = new StringFilterGroup( - Settings.HIDE_FULLSCREEN_ADS, - "_interstitial" - ); - final var generalAds = new StringFilterGroup( Settings.HIDE_GENERAL_ADS, "_ad_with", @@ -79,11 +84,11 @@ public final class AdsFilter extends Filter { "hero_promo_image", // text_image_button_group_layout, landscape_image_button_group_layout, full_width_square_image_button_group_layout "image_button_group_layout", + "landscape_image_carousel_layout", "landscape_image_wide_button_layout", "primetime_promo", "product_details", "square_image_layout", - "statement_banner", "text_image_button_layout", "text_image_no_button_layout", // Tablet layout search results. "video_display_button_group_layout", @@ -91,7 +96,8 @@ public final class AdsFilter extends Filter { "video_display_carousel_buttoned_short_dr_layout", "video_display_full_buttoned_short_dr_layout", "video_display_full_layout", - "watch_metadata_app_promo" + "watch_metadata_app_promo", + "shopping_timely_shelf." // Injection point below hides the empty space. ); final var movieAds = new StringFilterGroup( @@ -104,6 +110,16 @@ public final class AdsFilter extends Filter { "offer_module_root" ); + buyMovieAd = new StringFilterGroup( + Settings.HIDE_MOVIES_SECTION, + "video_lockup_with_attachment.e" + ); + + buyMovieAdBuffer = new ByteArrayFilterGroup( + null, + "FEstorefront" + ); + final var viewProducts = new StringFilterGroup( Settings.HIDE_VIEW_PRODUCTS_BANNER, "product_item", @@ -116,64 +132,126 @@ public final class AdsFilter extends Filter { "shopping_description_shelf.e" ); - playerShoppingShelf = new StringFilterGroup( - Settings.HIDE_CREATOR_STORE_SHELF, - "horizontal_shelf.e" - ); - - playerShoppingShelfBuffer = new ByteArrayFilterGroup( - null, - "shopping_item_card_list" - ); - - final var webLinkPanel = new StringFilterGroup( - Settings.HIDE_WEB_SEARCH_RESULTS, - "web_link_panel" - ); - final var merchandise = new StringFilterGroup( Settings.HIDE_MERCHANDISE_BANNERS, "product_carousel", "shopping_carousel.e" // Channel profile shopping shelf. ); + promotionBanner = new StringFilterGroup( + Settings.HIDE_YOUTUBE_PREMIUM_PROMOTIONS, + "statement_banner" + ); + + promotionBannerBuffer = new ByteArrayFilterGroup( + null, + "img/promos/growth/", // Link, https://www.gstatic.com/youtube/img/promos/growth/ is only used for ads. + "SPunlimited" // Word associated with Premium, should be unique to differentiate Doodle from ad banner. + ); + final var selfSponsor = new StringFilterGroup( Settings.HIDE_SELF_SPONSOR, "cta_shelf_card" ); addPathCallbacks( - fullscreenAd, + buyMovieAd, generalAds, merchandise, movieAds, - playerShoppingShelf, + promotionBanner, selfSponsor, shoppingLinks, - viewProducts, - webLinkPanel + viewProducts ); } @Override - public boolean isFiltered(String identifier, String path, byte[] buffer, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { - if (matchedGroup == playerShoppingShelf) { - return contentIndex == 0 && playerShoppingShelfBuffer.check(buffer).isFiltered(); + if (matchedGroup == buyMovieAd) { + return contentIndex == 0 && buyMovieAdBuffer.check(buffer).isFiltered(); } - if (exceptions.matches(path)) { - return false; + if (matchedGroup == promotionBanner) { + return contentIndex == 0 && promotionBannerBuffer.check(buffer).isFiltered(); } - if (matchedGroup == fullscreenAd) { - if (path.contains("|ImageType|")) closeFullscreenAd(); + return !exceptions.matches(path); + } - // Do not actually filter the fullscreen ad otherwise it will leave a dimmed screen. - return false; + /** + * Injection point. + * Called from a different place then the other filters. + */ + public static void closeFullscreenAd(Object customDialog, @Nullable byte[] buffer) { + try { + if (!Settings.HIDE_FULLSCREEN_ADS.get()) { + return; + } + + if (buffer == null) { + Logger.printDebug(() -> "buffer is null"); + return; + } + + if (fullscreenAd.check(buffer).isFiltered() && + customDialog instanceof Dialog dialog) { + Logger.printDebug(() -> "Closing fullscreen ad"); + + Window window = dialog.getWindow(); + + if (window != null) { + // Set the dialog size to 0 before closing + // If the dialog is not resized to 0, it will remain visible for about a second before closing + WindowManager.LayoutParams params = window.getAttributes(); + params.height = 0; + params.width = 0; + + // Change the size of dialog to 0 + window.setAttributes(params); + + // Disable dialog's background dim + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + + // Restore window flags + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED); + + // Restore decorView visibility + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + } + + // Dismiss dialog + dialog.dismiss(); + } + } catch (Exception ex) { + Logger.printException(() -> "closeFullscreenAd failure", ex); } + } - return true; + /** + * Injection point. + */ + public static boolean hideAds() { + return Settings.HIDE_GENERAL_ADS.get(); + } + + /** + * Injection point. + */ + public static String hideAds(String osName) { + return Settings.HIDE_GENERAL_ADS.get() + ? "Android Automotive" + : osName; + } + + /** + * Hide the view, which shows ads in the homepage. + * + * @param view The view, which shows ads. + */ + public static void hideAdAttributionView(View view) { + Utils.hideViewBy0dpUnderCondition(Settings.HIDE_GENERAL_ADS, view); } /** @@ -191,50 +269,18 @@ public final class AdsFilter extends Filter { elementsList.add(protobufList); } - /** - * Hide the view, which shows ads in the homepage. - * - * @param view The view, which shows ads. + * Injection point. */ - public static void hideAdAttributionView(View view) { - Utils.hideViewBy0dpUnderCondition(Settings.HIDE_GENERAL_ADS, view); + public static boolean hideGetPremiumView() { + return Settings.HIDE_YOUTUBE_PREMIUM_PROMOTIONS.get(); } /** - * Close the fullscreen ad. - *

- * The strategy is to send a back button event to the app to close the fullscreen ad using the back button event. + * Injection point. */ - private static void closeFullscreenAd() { - final var currentTime = System.currentTimeMillis(); - - // Prevent spamming the back button. - if (currentTime - lastTimeClosedFullscreenAd < 10000) return; - lastTimeClosedFullscreenAd = currentTime; - - Logger.printDebug(() -> "Closing fullscreen ad"); - - Utils.runOnMainThreadDelayed(() -> { - // Must run off main thread (Odd, but whatever). - Utils.runOnBackgroundThread(() -> { - try { - instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); - } catch (Exception ex) { - // Injecting user events on Android 10+ requires the manifest to include - // INJECT_EVENTS, and it's usage is heavily restricted - // and requires the user to manually approve the permission in the device settings. - // - // And no matter what, permissions cannot be added for root installations - // as manifest changes are ignored for mount installations. - // - // Instead, catch the SecurityException and turn off hide full screen ads - // since this functionality does not work for these devices. - Logger.printInfo(() -> "Could not inject back button event", ex); - Settings.HIDE_FULLSCREEN_ADS.save(false); - Utils.showToastLong(str("revanced_hide_fullscreen_ads_feature_not_available_toast")); - } - }); - }, 1000); + public static boolean hidePlayerPopupAds(String panelId) { + return Settings.HIDE_PLAYER_POPUP_ADS.get() + && Utils.containsAny(panelId, PLAYER_POPUP_AD_PANEL_IDS); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdvancedVideoQualityMenuFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdvancedVideoQualityMenuFilter.java index b47f0939d1..a969c9b0e0 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdvancedVideoQualityMenuFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdvancedVideoQualityMenuFilter.java @@ -21,7 +21,7 @@ public final class AdvancedVideoQualityMenuFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String path, byte[] buffer, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { isVideoQualityMenuVisible = true; diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ButtonsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ButtonsFilter.java deleted file mode 100644 index 557847a00d..0000000000 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ButtonsFilter.java +++ /dev/null @@ -1,154 +0,0 @@ -package app.revanced.extension.youtube.patches.litho; - -import app.revanced.extension.shared.patches.litho.Filter; -import app.revanced.extension.youtube.patches.VersionCheckPatch; -import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList; -import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup; -import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; -import app.revanced.extension.youtube.settings.Settings; - -@SuppressWarnings("unused") -public final class ButtonsFilter extends Filter { - private static final String COMPACT_CHANNEL_BAR_PATH_PREFIX = "compact_channel_bar.e"; - private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.e"; - private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.e"; - /** - * Video bar path when the video information is collapsed. Seems to shown only with 20.14+ - */ - private static final String COMPACTIFY_VIDEO_ACTION_BAR_PATH = "compactify_video_action_bar.e"; - private static final String ANIMATED_VECTOR_TYPE_PATH = "AnimatedVectorType"; - - private final StringFilterGroup likeSubscribeGlow; - private final StringFilterGroup actionBarGroup; - private final StringFilterGroup bufferFilterPathGroup; - private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList(); - - public ButtonsFilter() { - actionBarGroup = new StringFilterGroup( - null, - VIDEO_ACTION_BAR_PATH - ); - addIdentifierCallbacks(actionBarGroup); - - - likeSubscribeGlow = new StringFilterGroup( - Settings.DISABLE_LIKE_SUBSCRIBE_GLOW, - "animated_button_border.e" - ); - - bufferFilterPathGroup = new StringFilterGroup( - null, - "|ContainerType|button.e" - ); - - addPathCallbacks( - likeSubscribeGlow, - new StringFilterGroup( - Settings.HIDE_LIKE_DISLIKE_BUTTON, - "|segmented_like_dislike_button" - ), - new StringFilterGroup( - Settings.HIDE_DOWNLOAD_BUTTON, - "|download_button.e" - ), - new StringFilterGroup( - Settings.HIDE_SAVE_BUTTON, - "|save_to_playlist_button" - ), - new StringFilterGroup( - Settings.HIDE_CLIP_BUTTON, - "|clip_button.e" - ) - ); - - // FIXME: 20.22+ filtering of the action buttons doesn't work because - // the buffer is the same for all buttons. - if (!VersionCheckPatch.IS_20_22_OR_GREATER) { - addPathCallbacks(bufferFilterPathGroup); - } - - bufferButtonsGroupList.addAll( - new ByteArrayFilterGroup( - Settings.HIDE_REPORT_BUTTON, - "yt_outline_flag" - ), - new ByteArrayFilterGroup( - Settings.HIDE_SHARE_BUTTON, - "yt_outline_share" - ), - new ByteArrayFilterGroup( - Settings.HIDE_REMIX_BUTTON, - "yt_outline_youtube_shorts_plus" - ), - new ByteArrayFilterGroup( - Settings.HIDE_THANKS_BUTTON, - "yt_outline_dollar_sign_heart" - ), - new ByteArrayFilterGroup( - Settings.HIDE_ASK_BUTTON, - "yt_fill_spark" - ), - new ByteArrayFilterGroup( - Settings.HIDE_SHOP_BUTTON, - "yt_outline_bag" - ), - new ByteArrayFilterGroup( - Settings.HIDE_STOP_ADS_BUTTON, - "yt_outline_slash_circle_left" - ), - new ByteArrayFilterGroup( - Settings.HIDE_COMMENTS_BUTTON, - "yt_outline_message_bubble_right" - ), - // Check for clip button both here and using a path filter, - // as there's a chance the path is a generic action button and won't contain 'clip_button' - new ByteArrayFilterGroup( - Settings.HIDE_CLIP_BUTTON, - "yt_outline_scissors" - ), - new ByteArrayFilterGroup( - Settings.HIDE_HYPE_BUTTON, - "yt_outline_star_shooting" - ), - new ByteArrayFilterGroup( - Settings.HIDE_PROMOTE_BUTTON, - "yt_outline_megaphone" - ) - ); - } - - private boolean isEveryFilterGroupEnabled() { - for (var group : pathCallbacks) { - if (!group.isEnabled()) return false; - } - - for (var group : bufferButtonsGroupList) { - if (!group.isEnabled()) return false; - } - - return true; - } - - @Override - public boolean isFiltered(String identifier, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { - if (matchedGroup == likeSubscribeGlow) { - return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX)) - && path.contains(ANIMATED_VECTOR_TYPE_PATH); - } - - // If the current matched group is the action bar group, - // in case every filter group is enabled, hide the action bar. - if (matchedGroup == actionBarGroup) { - return isEveryFilterGroupEnabled(); - } - - if (matchedGroup == bufferFilterPathGroup) { - // Make sure the current path is the right one to avoid false positives. - return (path.startsWith(VIDEO_ACTION_BAR_PATH) || path.startsWith(COMPACTIFY_VIDEO_ACTION_BAR_PATH)) - && bufferButtonsGroupList.check(buffer).isFiltered(); - } - - return true; - } -} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/CommentsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/CommentsFilter.java index 793eb5f9b0..70330c843f 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/CommentsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/CommentsFilter.java @@ -9,9 +9,11 @@ import app.revanced.extension.youtube.shared.PlayerType; public final class CommentsFilter extends Filter { private static final String COMMENT_COMPOSER_PATH = "comment_composer.e"; + private static final String VIDEO_LOCKUP_WITH_ATTACHMENT_PATH = "video_lockup_with_attachment.e"; private final StringFilterGroup chipBar; private final ByteArrayFilterGroup aiCommentsSummary; + private final StringFilterGroup comments; private final StringFilterGroup emojiAndTimestampButtons; public CommentsFilter() { @@ -41,7 +43,7 @@ public final class CommentsFilter extends Filter { "sponsorships_comments_footer.e" ); - var comments = new StringFilterGroup( + comments = new StringFilterGroup( Settings.HIDE_COMMENTS_SECTION, "video_metadata_carousel", "_comments" @@ -90,7 +92,7 @@ public final class CommentsFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String path, byte[] buffer, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { if (matchedGroup == chipBar) { // Playlist sort button uses same components and must only filter if the player is opened. @@ -98,6 +100,13 @@ public final class CommentsFilter extends Filter { && aiCommentsSummary.check(buffer).isFiltered(); } + if (matchedGroup == comments) { + if (path.startsWith(VIDEO_LOCKUP_WITH_ATTACHMENT_PATH)) { + return Settings.HIDE_COMMENTS_SECTION_IN_HOME_FEED.get(); + } + return Settings.HIDE_COMMENTS_SECTION.get(); + } + if (matchedGroup == emojiAndTimestampButtons) { return path.startsWith(COMMENT_COMPOSER_PATH); } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/DescriptionComponentsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/DescriptionComponentsFilter.java index 771902ccef..9990aa46a4 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/DescriptionComponentsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/DescriptionComponentsFilter.java @@ -1,10 +1,10 @@ package app.revanced.extension.youtube.patches.litho; import app.revanced.extension.shared.patches.litho.Filter; -import app.revanced.extension.shared.StringTrieSearch; +import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.shared.patches.litho.FilterGroup.*; import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList; -import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.EngagementPanel; import app.revanced.extension.youtube.shared.PlayerType; @SuppressWarnings("unused") @@ -12,26 +12,16 @@ public final class DescriptionComponentsFilter extends Filter { private static final String INFOCARDS_SECTION_PATH = "infocards_section.e"; - private final StringTrieSearch exceptions = new StringTrieSearch(); private final StringFilterGroup macroMarkersCarousel; private final ByteArrayFilterGroupList macroMarkersCarouselGroupList = new ByteArrayFilterGroupList(); - private final StringFilterGroup horizontalShelf; - private final ByteArrayFilterGroup cellVideoAttribute; - private final StringFilterGroup infoCardsSection; + private final StringFilterGroup playlistSection; + private final ByteArrayFilterGroupList playlistSectionGroupList = new ByteArrayFilterGroupList(); + private final StringFilterGroup featuredLinksSection; + private final StringFilterGroup featuredVideosSection; private final StringFilterGroup subscribeButton; - private final StringFilterGroup aiGeneratedVideoSummarySection; - private final StringFilterGroup hypePoints; public DescriptionComponentsFilter() { - exceptions.addPatterns( - "compact_channel", - "description", - "grid_video", - "inline_expander", - "metadata" - ); - - aiGeneratedVideoSummarySection = new StringFilterGroup( + final StringFilterGroup aiGeneratedVideoSummarySection = new StringFilterGroup( Settings.HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION, "cell_expandable_metadata.e" ); @@ -47,19 +37,33 @@ public final class DescriptionComponentsFilter extends Filter { "video_attributes_section" ); - final StringFilterGroup featuredLinksSection = new StringFilterGroup( + featuredLinksSection = new StringFilterGroup( Settings.HIDE_FEATURED_LINKS_SECTION, "media_lockup" ); - final StringFilterGroup featuredVideosSection = new StringFilterGroup( + featuredVideosSection = new StringFilterGroup( Settings.HIDE_FEATURED_VIDEOS_SECTION, "structured_description_video_lockup" ); - final StringFilterGroup podcastSection = new StringFilterGroup( - Settings.HIDE_PODCAST_SECTION, - "playlist_section" + playlistSection = new StringFilterGroup( + // YT v20.14.43 doesn't use any buffer for Courses and Podcasts. + // So this component is also needed. + null, + "playlist_section.e" + ); + + playlistSectionGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_EXPLORE_COURSE_SECTION, + "yt_outline_creator_academy", // For Disable bold icons. + "yt_outline_experimental_graduation_cap" + ), + new ByteArrayFilterGroup( + Settings.HIDE_EXPLORE_PODCAST_SECTION, + "FEpodcasts_destination" + ) ); final StringFilterGroup transcriptSection = new StringFilterGroup( @@ -72,12 +76,17 @@ public final class DescriptionComponentsFilter extends Filter { "how_this_was_made_section" ); - hypePoints = new StringFilterGroup( + final StringFilterGroup courseProgressSection = new StringFilterGroup( + Settings.HIDE_COURSE_PROGRESS_SECTION, + "course_progress" + ); + + final StringFilterGroup hypePoints = new StringFilterGroup( Settings.HIDE_HYPE_POINTS, "hype_points_factoid" ); - infoCardsSection = new StringFilterGroup( + final StringFilterGroup infoCardsSection = new StringFilterGroup( Settings.HIDE_INFO_CARDS_SECTION, INFOCARDS_SECTION_PATH ); @@ -95,64 +104,72 @@ public final class DescriptionComponentsFilter extends Filter { macroMarkersCarouselGroupList.addAll( new ByteArrayFilterGroup( Settings.HIDE_CHAPTERS_SECTION, - "chapters_horizontal_shelf" + "chapters_horizontal_shelf", + "auto-chapters", + "description-chapters" ), new ByteArrayFilterGroup( Settings.HIDE_KEY_CONCEPTS_SECTION, - "learning_concept_macro_markers_carousel_shelf" + "learning_concept_macro_markers_carousel_shelf", + "learning-concept" ) ); - horizontalShelf = new StringFilterGroup( - Settings.HIDE_ATTRIBUTES_SECTION, - "horizontal_shelf.e" - ); - - cellVideoAttribute = new ByteArrayFilterGroup( - null, - "cell_video_attribute" - ); - addPathCallbacks( aiGeneratedVideoSummarySection, askSection, - attributesSection, + courseProgressSection, featuredLinksSection, featuredVideosSection, - horizontalShelf, howThisWasMadeSection, hypePoints, infoCardsSection, macroMarkersCarousel, - podcastSection, + playlistSection, subscribeButton, transcriptSection ); } @Override - public boolean isFiltered(String identifier, String path, byte[] buffer, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { - - if (matchedGroup == aiGeneratedVideoSummarySection || matchedGroup == hypePoints) { - // Only hide if player is open, in case this component is used somewhere else. - return PlayerType.getCurrent().isMaximizedOrFullscreen(); + // The description panel can be opened in both the regular player and Shorts. + // If the description panel is opened in a Shorts, PlayerType is 'HIDDEN', + // so 'PlayerType.getCurrent().isMaximizedOrFullscreen()' does not guarantee that the description panel is open. + // Instead, use the engagement id to check if the description panel is opened. + if (!EngagementPanel.isDescription() + // The user can minimize the player while the engagement panel is open. + // + // In this case, the engagement panel is treated as open. + // (If the player is dismissed, the engagement panel is considered closed) + // + // Therefore, the following exceptions can occur: + // 1. The user opened a regular video and opened the description panel. + // 2. The 'horizontalShelf' elements were hidden. + // 3. The user minimized the player. + // 4. The user manually refreshed the library tab without dismissing the player. + // 5. Since the engagement panel is treated as open, the history shelf is filtered. + // + // To handle these exceptions, filtering is not performed even when the player is minimized. + || PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED + ) { + return false; } - if (matchedGroup == subscribeButton) { + if (matchedGroup == featuredLinksSection || matchedGroup == featuredVideosSection || matchedGroup == subscribeButton) { return path.startsWith(INFOCARDS_SECTION_PATH); } - if (exceptions.matches(path)) return false; + if (matchedGroup == playlistSection) { + if (contentIndex != 0) return false; + return Settings.HIDE_EXPLORE_SECTION.get() || playlistSectionGroupList.check(buffer).isFiltered(); + } if (matchedGroup == macroMarkersCarousel) { return contentIndex == 0 && macroMarkersCarouselGroupList.check(buffer).isFiltered(); } - if (matchedGroup == horizontalShelf) { - return cellVideoAttribute.check(buffer).isFiltered(); - } - return true; } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/KeywordContentFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/KeywordContentFilter.java index 93fd1734d4..4f18eb412d 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/KeywordContentFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/KeywordContentFilter.java @@ -36,7 +36,7 @@ import app.revanced.extension.youtube.shared.PlayerType; * - Some layout component residue will remain, such as the video chapter previews for some search results. * These components do not include the video title or channel name, and they * appear outside the filtered components so they are not caught. - * - Keywords are case sensitive, but some casing variation is manually added. + * - Keywords are case-sensitive, but some casing variation is manually added. * (ie: "mr beast" automatically filters "Mr Beast" and "MR BEAST"). * - Keywords present in the layout or video data cannot be used as filters, otherwise all videos * will always be hidden. This patch checks for some words of these words. @@ -46,7 +46,7 @@ import app.revanced.extension.youtube.shared.PlayerType; public final class KeywordContentFilter extends Filter { /** - * Strings found in the buffer for every videos. Full strings should be specified. + * Strings found in the buffer for every video. Full strings should be specified. * * This list does not include every common buffer string, and this can be added/changed as needed. * Words must be entered with the exact casing as found in the buffer. @@ -190,7 +190,7 @@ public final class KeywordContentFilter extends Filter { return sentence; } final int firstCodePoint = sentence.codePointAt(0); - // In some non English languages title case is different than uppercase. + // In some non-English languages title case is different from uppercase. return new StringBuilder() .appendCodePoint(Character.toTitleCase(firstCodePoint)) .append(sentence, Character.charCount(firstCodePoint), sentence.length()) @@ -206,7 +206,7 @@ public final class KeywordContentFilter extends Filter { } final int delimiter = ' '; - // Use code points and not characters to handle unicode surrogates. + // Use code points and not characters to handle Unicode surrogates. int[] codePoints = sentence.codePoints().toArray(); boolean capitalizeNext = true; for (int i = 0, length = codePoints.length; i < length; i++) { @@ -376,7 +376,7 @@ public final class KeywordContentFilter extends Filter { return phrase.substring(1, phrase.length() - 1); } - private synchronized void parseKeywords() { // Must be synchronized since Litho is multi-threaded. + private synchronized void parseKeywords() { // Must be synchronized since Litho is multithreaded. String rawKeywords = Settings.HIDE_KEYWORD_CONTENT_PHRASES.get(); //noinspection StringEquality @@ -417,14 +417,14 @@ public final class KeywordContentFilter extends Filter { // Common casing that might appear. // - // This could be simplified by adding case insensitive search to the prefix search, + // This could be simplified by adding case-insensitive search to the prefix search, // which is very simple to add to StringTreSearch for Unicode and ByteTrieSearch for ASCII. // // But to support Unicode with ByteTrieSearch would require major changes because // UTF-8 characters can be different byte lengths, which does // not allow comparing two different byte arrays using simple plain array indexes. // - // Instead use all common case variations of the words. + // Instead, use all common case variations of the words. String[] phraseVariations = { phrase, phrase.toLowerCase(), @@ -556,7 +556,7 @@ public final class KeywordContentFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String path, byte[] buffer, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { if (contentIndex != 0 && matchedGroup == startsWithFilter) { return false; diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/LayoutComponentsFilter.java index 16d4c166b3..dda1f46e95 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/LayoutComponentsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/LayoutComponentsFilter.java @@ -7,18 +7,30 @@ import android.graphics.drawable.Drawable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextUtils; +import android.util.Pair; import android.view.View; - +import android.view.ViewGroup; +import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.TextView; + import androidx.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import app.revanced.extension.shared.ByteTrieSearch; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.StringTrieSearch; import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.patches.litho.Filter; import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup; import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; -import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.litho.FilterGroupList.StringFilterGroupList; +import app.revanced.extension.shared.settings.StringSetting; import app.revanced.extension.youtube.patches.ChangeHeaderPatch; import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.shared.NavigationBar; @@ -27,7 +39,7 @@ import app.revanced.extension.youtube.shared.PlayerType; @SuppressWarnings("unused") public final class LayoutComponentsFilter extends Filter { private static final StringTrieSearch mixPlaylistsContextExceptions = new StringTrieSearch( - "V.ED", // Playlist browse id. + "V.ED", // Playlist browseId. "java.lang.ref.WeakReference" ); private static final ByteArrayFilterGroup mixPlaylistsBufferExceptions = new ByteArrayFilterGroup( @@ -40,24 +52,43 @@ public final class LayoutComponentsFilter extends Filter { "&list=" ); - private static final String PAGE_HEADER_PATH = "page_header.e"; + private static final List channelTabFilterStrings; + private static final List flyoutMenuFilterStrings; + + static { + channelTabFilterStrings = getFilterStrings(Settings.HIDE_CHANNEL_TAB_FILTER_STRINGS); + flyoutMenuFilterStrings = getFilterStrings(Settings.HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS); + } + + private static List getFilterStrings(StringSetting setting) { + String[] filterArray = setting.get().split("\\n"); + List filters = new ArrayList<>(filterArray.length); + + for (String line : filterArray) { + String trimmed = line.trim(); + if (!trimmed.isEmpty()) filters.add(trimmed); + } + + return filters; + } private final StringTrieSearch exceptions = new StringTrieSearch(); private final StringFilterGroup communityPosts; private final StringFilterGroup surveys; - private final StringFilterGroup subscribeButton; private final StringFilterGroup notifyMe; private final StringFilterGroup singleItemInformationPanel; private final StringFilterGroup expandableMetadata; private final StringFilterGroup compactChannelBarInner; private final StringFilterGroup compactChannelBarInnerButton; private final ByteArrayFilterGroup joinMembershipButton; - private final StringFilterGroup horizontalShelves; - private final ByteArrayFilterGroup ticketShelfBuffer; private final StringFilterGroup chipBar; private final StringFilterGroup channelProfile; - private final ByteArrayFilterGroupList channelProfileBuffer; + private final StringFilterGroupList channelProfileGroupList; + private final StringFilterGroup horizontalShelves; private final ByteArrayFilterGroup playablesBuffer; + private final ByteArrayFilterGroup ticketShelfBuffer; + private final ByteArrayFilterGroup playerShoppingShelfBuffer; + private final ByteTrieSearch descriptionSearch; public LayoutComponentsFilter() { exceptions.addPatterns( @@ -71,11 +102,28 @@ public final class LayoutComponentsFilter extends Filter { // Identifiers. + final var cellDivider = new StringFilterGroup( + Settings.HIDE_COMPACT_BANNER, + // Empty padding and a relic from very old YT versions. Not related to compact banner but included here to avoid adding another setting. + "cell_divider" + ); + final var chipsShelf = new StringFilterGroup( Settings.HIDE_CHIPS_SHELF, "chips_shelf" ); + final var liveChatReplay = new StringFilterGroup( + Settings.HIDE_LIVE_CHAT_REPLAY_BUTTON, + "live_chat_ep_entrypoint.e" + ); + + addIdentifierCallbacks( + cellDivider, + chipsShelf, + liveChatReplay + ); + final var visualSpacer = new StringFilterGroup( Settings.HIDE_VISUAL_SPACER, "cell_divider" @@ -83,6 +131,7 @@ public final class LayoutComponentsFilter extends Filter { addIdentifierCallbacks( chipsShelf, + liveChatReplay, visualSpacer ); @@ -126,6 +175,11 @@ public final class LayoutComponentsFilter extends Filter { "subscriptions_chip_bar" ); + final var subscribedChannelsBar = new StringFilterGroup( + Settings.HIDE_SUBSCRIBED_CHANNELS_BAR, + "subscriptions_channel_bar" + ); + chipBar = new StringFilterGroup( Settings.HIDE_FILTER_BAR_FEED_IN_HISTORY, "chip_bar" @@ -272,31 +326,41 @@ public final class LayoutComponentsFilter extends Filter { "endorsement_header_footer.e" ); + final var videoTitle = new StringFilterGroup( + Settings.HIDE_VIDEO_TITLE, + "player_overlay_video_heading.e" + ); + + final var webLinkPanel = new StringFilterGroup( + Settings.HIDE_WEB_SEARCH_RESULTS, + "web_link_panel", + "web_result_panel" + ); + channelProfile = new StringFilterGroup( null, "channel_profile.e", - PAGE_HEADER_PATH + "page_header.e" ); - channelProfileBuffer = new ByteArrayFilterGroupList(); - channelProfileBuffer.addAll(new ByteArrayFilterGroup( - Settings.HIDE_STORE_BUTTON, - "store_button" - ), - new ByteArrayFilterGroup( + channelProfileGroupList = new StringFilterGroupList(); + channelProfileGroupList.addAll(new StringFilterGroup( Settings.HIDE_COMMUNITY_BUTTON, "community_button" ), - new ByteArrayFilterGroup( + new StringFilterGroup( Settings.HIDE_JOIN_BUTTON, "sponsor_button" + ), + new StringFilterGroup( + Settings.HIDE_STORE_BUTTON, + "header_store_button" + ), + new StringFilterGroup( + Settings.HIDE_SUBSCRIBE_BUTTON_IN_CHANNEL_PAGE, + "subscribe_button" ) ); - subscribeButton = new StringFilterGroup( - Settings.HIDE_SUBSCRIBE_BUTTON_IN_CHANNEL_PAGE, - "subscribe_button" - ); - horizontalShelves = new StringFilterGroup( null, // Setting is checked in isFiltered() "horizontal_video_shelf.e", @@ -310,6 +374,38 @@ public final class LayoutComponentsFilter extends Filter { "ticket_item.e" ); + playerShoppingShelfBuffer = new ByteArrayFilterGroup( + null, + "shopping_item_card_list" + ); + + // Work around for unique situation where filtering is based on the setting, + // but it must not fall over to other filters if the setting is _not_ enabled. + // This is only needed for the horizontal shelf that is used so extensively everywhere. + descriptionSearch = new ByteTrieSearch(); + List.of( + new Pair<>(Settings.HIDE_FEATURED_PLACES_SECTION, "yt_fill_star"), + new Pair<>(Settings.HIDE_FEATURED_PLACES_SECTION, "yt_fill_experimental_star"), + new Pair<>(Settings.HIDE_GAMING_SECTION, "yt_outline_gaming"), + new Pair<>(Settings.HIDE_GAMING_SECTION, "yt_outline_experimental_gaming"), + new Pair<>(Settings.HIDE_MUSIC_SECTION, "yt_outline_audio"), + new Pair<>(Settings.HIDE_MUSIC_SECTION, "yt_outline_experimental_audio"), + new Pair<>(Settings.HIDE_QUIZZES_SECTION, "post_base_wrapper_slim"), + // May no longer work on v20.31+, even though the component is still there. + new Pair<>(Settings.HIDE_ATTRIBUTES_SECTION, "cell_video_attribute") + ).forEach(pair -> { + BooleanSetting setting = pair.first; + descriptionSearch.addPattern(pair.second.getBytes(StandardCharsets.UTF_8), + (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { + //noinspection unchecked + AtomicReference hide = (AtomicReference) callbackParameter; + hide.set(setting.get()); + return true; + } + ); + } + ); + addPathCallbacks( artistCard, audioTrackButton, @@ -337,21 +433,23 @@ public final class LayoutComponentsFilter extends Filter { quickActions, relatedVideos, singleItemInformationPanel, - subscribeButton, + subscribedChannelsBar, subscribersCommunityGuidelines, subscriptionsChipBar, surveys, timedReactions, - videoRecommendationLabels + videoTitle, + videoRecommendationLabels, + webLinkPanel ); } @Override - public boolean isFiltered(String identifier, String path, byte[] buffer, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { // This identifier is used not only in players but also in search results: // https://github.com/ReVanced/revanced-patches/issues/3245 - // Until 2024, medical information panels such as Covid 19 also used this identifier and were shown in the search results. + // Until 2024, medical information panels such as Covid-19 also used this identifier and were shown in the search results. // From 2025, the medical information panel is no longer shown in the search results. // Therefore, this identifier does not filter when the search bar is activated. if (matchedGroup == singleItemInformationPanel) { @@ -365,14 +463,13 @@ public final class LayoutComponentsFilter extends Filter { } if (matchedGroup == channelProfile) { - return channelProfileBuffer.check(buffer).isFiltered(); + return channelProfileGroupList.check(accessibility).isFiltered(); } - if (matchedGroup == subscribeButton) { - return path.startsWith(PAGE_HEADER_PATH); - } - - if (matchedGroup == communityPosts && NavigationBar.isBackButtonVisible()) { + if (matchedGroup == communityPosts + && NavigationBar.isBackButtonVisible() + && !NavigationBar.isSearchBarActive() + && PlayerType.getCurrent() != PlayerType.WATCH_WHILE_MAXIMIZED) { // Allow community posts on channel profile page, // or if viewing an individual channel in the feed. return false; @@ -387,18 +484,43 @@ public final class LayoutComponentsFilter extends Filter { && joinMembershipButton.check(buffer).isFiltered(); } + // Horizontal shelves are used everywhere in the app. And to prevent the generic "hide shelves" + // from incorrectly hiding other stuff that has its own hide filters, + // the more specific shelf filters must check first _and_ they must halt falling over + // to other filters if the buffer matches but the setting is off. if (matchedGroup == horizontalShelves) { if (contentIndex != 0) return false; + + AtomicReference descriptionFilterResult = new AtomicReference<>(null); + if (descriptionSearch.matches(buffer, descriptionFilterResult)) { + return descriptionFilterResult.get(); + } + + // Check if others are off before searching. final boolean hideShelves = Settings.HIDE_HORIZONTAL_SHELVES.get(); final boolean hideTickets = Settings.HIDE_TICKET_SHELF.get(); final boolean hidePlayables = Settings.HIDE_PLAYABLES.get(); + final boolean hidePlayerShoppingShelf = Settings.HIDE_CREATOR_STORE_SHELF.get(); + if (!hideShelves && !hideTickets && !hidePlayables && !hidePlayerShoppingShelf) + return false; - if (!hideShelves && !hideTickets && !hidePlayables) return false; - - // Must always check other buffers first, to prevent incorrectly hiding them - // if they are set to show but hide horizontal shelves is set to hidden. if (ticketShelfBuffer.check(buffer).isFiltered()) return hideTickets; if (playablesBuffer.check(buffer).isFiltered()) return hidePlayables; + if (playerShoppingShelfBuffer.check(buffer).isFiltered()) + return hidePlayerShoppingShelf; + + // 20.31+ when exiting fullscreen after watching for a while or when resuming the app, + // then sometimes the buffer isn't correct and the player shopping shelf is shown. + // If filtering reaches this point then there are no more shelves that could be in the player. + // If shopping shelves are set to hidden and the player is active, then assume + // it's the shopping shelf. + if (hidePlayerShoppingShelf) { + PlayerType type = PlayerType.getCurrent(); + if (type == PlayerType.WATCH_WHILE_MAXIMIZED || type == PlayerType.WATCH_WHILE_FULLSCREEN + || type == PlayerType.WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN) { + return true; + } + } return hideShelves && hideShelves(); } @@ -474,6 +596,13 @@ public final class LayoutComponentsFilter extends Filter { return original || Settings.HIDE_FLOATING_MICROPHONE_BUTTON.get(); } + /** + * Injection point. + */ + public static void hideLatestVideosButton(View view) { + Utils.hideViewUnderCondition(Settings.HIDE_LATEST_VIDEOS_BUTTON.get(), view); + } + /** * Injection point. */ @@ -523,22 +652,111 @@ public final class LayoutComponentsFilter extends Filter { imageView.setImageDrawable(replacement); } + private static final FrameLayout.LayoutParams EMPTY_LAYOUT_PARAMS = new FrameLayout.LayoutParams(0, 0); private static final boolean HIDE_SHOW_MORE_BUTTON_ENABLED = Settings.HIDE_SHOW_MORE_BUTTON.get(); + /** + * The ShowMoreButton should not always be hidden. + * According to the preference summary, only the ShowMoreButton in search results is hidden. + * Since the ShowMoreButton should be visible on other pages, such as channels, + * the original values of the Views are saved in fields. + */ + private static FrameLayout.LayoutParams cachedLayoutParams; + private static int cachedButtonContainerMinimumHeight = -1; + private static int cachedPlaceHolderMinimumHeight = -1; + private static int cachedRootViewMinimumHeight = -1; + /** * Injection point. */ - public static void hideShowMoreButton(View view) { + public static void hideShowMoreButton(View view, View buttonContainer, TextView textView) { if (HIDE_SHOW_MORE_BUTTON_ENABLED - && NavigationBar.isSearchBarActive() - // Search bar can be active but behind the player. - && !PlayerType.getCurrent().isMaximizedOrFullscreen()) { - // FIXME: "Show more" button is visible hidden, - // but an empty space remains that can be clicked. - Utils.hideViewByLayoutParams(view); + && view instanceof ViewGroup rootView + && buttonContainer != null + && textView != null + && buttonContainer.getLayoutParams() instanceof FrameLayout.LayoutParams lp + ) { + View placeHolder = rootView.getChildAt(0); + + // For some users, ShowMoreButton has a PlaceHolder ViewGroup (A/B tests). + // When a PlaceHolder is present, a different method is used to hide or show the ViewGroup. + boolean hasPlaceHolder = placeHolder instanceof FrameLayout; + + // Only in search results, the content description of RootView and the text of TextView match. + // Hide ShowMoreButton in search results, but show ShowMoreButton in other pages (e.g. channels). + boolean isSearchResults = TextUtils.equals(rootView.getContentDescription(), textView.getText()); + + if (hasPlaceHolder) { + hideShowMoreButtonWithPlaceHolder(placeHolder, isSearchResults); + } else { + hideShowMoreButtonWithOutPlaceHolder(buttonContainer, lp, isSearchResults); + } + + if (cachedRootViewMinimumHeight == -1) { + cachedRootViewMinimumHeight = rootView.getMinimumHeight(); + } + + if (isSearchResults) { + rootView.setMinimumHeight(0); + rootView.setVisibility(View.GONE); + } else { + rootView.setMinimumHeight(cachedRootViewMinimumHeight); + rootView.setVisibility(View.VISIBLE); + } } } + private static void hideShowMoreButtonWithPlaceHolder(View placeHolder, boolean isSearchResults) { + if (cachedPlaceHolderMinimumHeight == -1) { + cachedPlaceHolderMinimumHeight = placeHolder.getMinimumHeight(); + } + + if (isSearchResults) { + placeHolder.setMinimumHeight(0); + placeHolder.setVisibility(View.GONE); + } else { + placeHolder.setMinimumHeight(cachedPlaceHolderMinimumHeight); + placeHolder.setVisibility(View.VISIBLE); + } + } + + private static void hideShowMoreButtonWithOutPlaceHolder(View buttonContainer, FrameLayout.LayoutParams lp, + boolean isSearchResults) { + if (cachedButtonContainerMinimumHeight == -1) { + cachedButtonContainerMinimumHeight = buttonContainer.getMinimumHeight(); + } + + if (cachedLayoutParams == null) { + cachedLayoutParams = lp; + } + + if (isSearchResults) { + buttonContainer.setMinimumHeight(0); + buttonContainer.setLayoutParams(EMPTY_LAYOUT_PARAMS); + buttonContainer.setVisibility(View.GONE); + } else { + buttonContainer.setMinimumHeight(cachedButtonContainerMinimumHeight); + buttonContainer.setLayoutParams(cachedLayoutParams); + buttonContainer.setVisibility(View.VISIBLE); + } + } + + /** + * Injection point. + */ + public static void hideSubscribedChannelsBar(View view) { + Utils.hideViewByRemovingFromParentUnderCondition(Settings.HIDE_SUBSCRIBED_CHANNELS_BAR, view); + } + + /** + * Injection point. + */ + public static int hideSubscribedChannelsBar(int original) { + return Settings.HIDE_SUBSCRIBED_CHANNELS_BAR.get() + ? 0 + : original; + } + private static boolean hideShelves() { // Horizontal shelves are used for music/game links in video descriptions, // such as https://youtube.com/watch?v=W8kI1na3S2M @@ -621,4 +839,112 @@ public final class LayoutComponentsFilter extends Filter { return original; } + + /** + * + * Injection point. + *

+ * Hide feed flyout menu for phone + * + * @param menuTitleCharSequence menu title + */ + @Nullable + public static CharSequence hideFlyoutMenu(@Nullable CharSequence menuTitleCharSequence) { + if (menuTitleCharSequence == null || !Settings.HIDE_FEED_FLYOUT_MENU.get() + || flyoutMenuFilterStrings.isEmpty()) { + return menuTitleCharSequence; + } + + String menuTitleString = menuTitleCharSequence.toString(); + + for (String filter : flyoutMenuFilterStrings) { + if (menuTitleString.equalsIgnoreCase(filter)) { + return null; + } + } + + return menuTitleCharSequence; + } + + /** + * Injection point. + *

+ * hide feed flyout panel for tablet + * + * @param menuTextView flyout text view + * @param menuTitleCharSequence raw text + */ + public static void hideFlyoutMenu(TextView menuTextView, CharSequence menuTitleCharSequence) { + if (menuTitleCharSequence == null || !Settings.HIDE_FEED_FLYOUT_MENU.get() + || flyoutMenuFilterStrings.isEmpty() + || !(menuTextView.getParent() instanceof View parentView)) { + return; + } + + String menuTitleString = menuTitleCharSequence.toString(); + + for (String filter : flyoutMenuFilterStrings) { + if (menuTitleString.equalsIgnoreCase(filter)) { + Utils.hideViewByLayoutParams(parentView); + } + } + } + + /** + * + * Injection point. + *

+ * Rather than simply hiding the channel tab view, completely remove the channel tab from the list. + * If a channel tab is removed from the list, users will not be able to open it by swiping. + * + * @param channelTabText Text assigned to the channel tab, such as "Shorts", "Playlists", + * "Community", "Store". This text follows the user's language. + * @return Whether to remove the channel tab from the list. + */ + public static boolean hideChannelTab(@Nullable String channelTabText) { + if (channelTabText == null || !Settings.HIDE_CHANNEL_TAB.get() + || channelTabFilterStrings.isEmpty()) { + return false; + } + + for (String filter : channelTabFilterStrings) { + if (channelTabText.equalsIgnoreCase(filter)) { + return true; + } + } + + return false; + } + + /** + * Injection point. + * + * @param typedString Keywords typed in the search bar. + * @return Whether the setting is enabled and the typed string is empty. + */ + public static boolean hideYouMayLikeSection(String typedString) { + return Settings.HIDE_YOU_MAY_LIKE_SECTION.get() + // The 'You may like' section is only visible when no search terms are entered. + // To avoid unnecessary collection traversals, filtering is performed only when the typedString is empty. + && TextUtils.isEmpty(typedString); + } + + /** + * Injection point. + * + * @param searchTerm This class contains information related to search terms. + * The {@code toString()} method of this class overrides the search term. + * @param endpoint Endpoint related with the search term. + * For search history, this value is: + * '/complete/deleteitems?client=youtube-android-pb&delq=${searchTerm}&deltok=${token}'. + * For search suggestions, this value is null or empty. + * @return Whether search term is a search history or not. + */ + public static boolean isSearchHistory(Object searchTerm, String endpoint) { + boolean isSearchHistory = endpoint != null && endpoint.contains("/delete"); + if (!isSearchHistory) { + Logger.printDebug(() -> "Remove search suggestion: " + searchTerm); + } + return isSearchHistory; + } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlaybackSpeedMenuFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlaybackSpeedMenuFilter.java index 05b2dda470..e1752f184b 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlaybackSpeedMenuFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlaybackSpeedMenuFilter.java @@ -38,7 +38,7 @@ public final class PlaybackSpeedMenuFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String path, byte[] buffer, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { if (matchedGroup == oldPlaybackMenuGroup) { isOldPlaybackSpeedMenuVisible = true; diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlayerFlyoutMenuItemsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlayerFlyoutMenuItemsFilter.java index a8b88fa8f6..c1edd9a0dc 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlayerFlyoutMenuItemsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlayerFlyoutMenuItemsFilter.java @@ -7,6 +7,7 @@ import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilt import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch; +import app.revanced.extension.youtube.patches.VersionCheckPatch; import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.shared.ShortsPlayerState; @@ -44,61 +45,74 @@ public final class PlayerFlyoutMenuItemsFilter extends Filter { flyoutFilterGroupList.addAll( new ByteArrayFilterGroup( Settings.HIDE_PLAYER_FLYOUT_CAPTIONS, - "closed_caption_" - ), - new ByteArrayFilterGroup( - Settings.HIDE_PLAYER_FLYOUT_ADDITIONAL_SETTINGS, - "yt_outline_gear_" - ), - new ByteArrayFilterGroup( - Settings.HIDE_PLAYER_FLYOUT_LOOP_VIDEO, - "yt_outline_arrow_repeat_1_" - ), - new ByteArrayFilterGroup( - Settings.HIDE_PLAYER_FLYOUT_AMBIENT_MODE, - "yt_outline_screen_light_" - ), - new ByteArrayFilterGroup( - Settings.HIDE_PLAYER_FLYOUT_STABLE_VOLUME, - "volume_stable_" + "closed_caption_", + "yt_outline_experimental_closed_captions_" ), new ByteArrayFilterGroup( Settings.HIDE_PLAYER_FLYOUT_LISTEN_WITH_YOUTUBE_MUSIC, - "yt_outline_youtube_music_" + "yt_outline_youtube_music_", + "yt_outline_experimental_youtube_music_" ), new ByteArrayFilterGroup( Settings.HIDE_PLAYER_FLYOUT_HELP, - "yt_outline_question_circle_" + "yt_outline_question_circle_", + "yt_outline_experimental_help_circle_" ), new ByteArrayFilterGroup( Settings.HIDE_PLAYER_FLYOUT_LOCK_SCREEN, - "yt_outline_lock_" + "yt_outline_lock_", + "yt_outline_experimental_lock_" ), new ByteArrayFilterGroup( Settings.HIDE_PLAYER_FLYOUT_SPEED, - "yt_outline_play_arrow_half_circle_" + "yt_outline_play_arrow_half_circle_", + "yt_outline_experimental_play_circle_half_dashed_" ), new ByteArrayFilterGroup( Settings.HIDE_PLAYER_FLYOUT_AUDIO_TRACK, - "yt_outline_person_radar_" + "yt_outline_person_radar_", + "yt_outline_experimental_person_radar_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_ADDITIONAL_SETTINGS, + "yt_outline_gear_", + "yt_outline_experimental_gear_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_AMBIENT_MODE, + "yt_outline_screen_light_", + "yt_outline_experimental_ambient_mode_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_LOOP_VIDEO, + "yt_outline_arrow_repeat_1_", + "yt_outline_experimental_repeat1_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_STABLE_VOLUME, + "volume_stable_", + "yt_outline_experimental_stable_volume_" ), new ByteArrayFilterGroup( Settings.HIDE_PLAYER_FLYOUT_SLEEP_TIMER, - "yt_outline_moon_z_" + "yt_outline_moon_z_", + "yt_outline_experimental_sleep_timer_" ), new ByteArrayFilterGroup( Settings.HIDE_PLAYER_FLYOUT_WATCH_IN_VR, - "yt_outline_vr_" + "yt_outline_vr_", + "yt_outline_experimental_vr_" ), new ByteArrayFilterGroup( Settings.HIDE_PLAYER_FLYOUT_VIDEO_QUALITY, - "yt_outline_adjust_" + "yt_outline_adjust_", + "yt_outline_experimental_adjust_" ) ); } @Override - public boolean isFiltered(String identifier, String path, byte[] buffer, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { if (matchedGroup == videoQualityMenuFooter) { return true; diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ReturnYouTubeDislikeFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ReturnYouTubeDislikeFilter.java index ba1d44d017..a9513f28e0 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ReturnYouTubeDislikeFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ReturnYouTubeDislikeFilter.java @@ -18,21 +18,21 @@ import app.revanced.extension.youtube.patches.VideoInformation; import app.revanced.extension.youtube.settings.Settings; /** - * Searches for video id's in the proto buffer of Shorts dislike. + * Searches for video IDs in the proto buffer of Shorts dislike. * * Because multiple litho dislike spans are created in the background * (and also anytime litho refreshes the components, which is somewhat arbitrary), * that makes the value of {@link VideoInformation#getVideoId()} and {@link VideoInformation#getPlayerResponseVideoId()} - * unreliable to determine which video id a Shorts litho span belongs to. + * unreliable to determine which video ID a Shorts litho span belongs to. * - * But the correct video id does appear in the protobuffer just before a Shorts litho span is created. + * But the correct video ID does appear in the protobuffer just before a Shorts litho span is created. * * Once a way to asynchronously update litho text is found, this strategy will no longer be needed. */ public final class ReturnYouTubeDislikeFilter extends Filter { /** - * Last unique video id's loaded. Value is ignored and Map is treated as a Set. + * Last unique video IDs loaded. Value is ignored and Map is treated as a Set. * Cannot use {@link LinkedHashSet} because it's missing #removeEldestEntry(). */ @GuardedBy("itself") @@ -49,7 +49,7 @@ public final class ReturnYouTubeDislikeFilter extends Filter { } synchronized (lastVideoIds) { if (lastVideoIds.put(videoId, Boolean.TRUE) == null) { - Logger.printDebug(() -> "New Short video id: " + videoId); + Logger.printDebug(() -> "New Short video ID: " + videoId); } } } catch (Exception ex) { @@ -64,19 +64,31 @@ public final class ReturnYouTubeDislikeFilter extends Filter { // But if swiping back to a previous video and liking/disliking, then only that single button reloads. // So must check for both buttons. addPathCallbacks( - new StringFilterGroup(null, "|shorts_like_button.e"), - new StringFilterGroup(null, "|shorts_dislike_button.e") + new StringFilterGroup( + null, + "shorts_like_button.e", + "reel_like_button.e", + "reel_like_toggled_button.e", + "shorts_dislike_button.e", + "reel_dislike_button.e", + "reel_dislike_toggled_button.e" + ) ); - // After the button identifiers is binary data and then the video id for that specific short. + // After the button identifiers is binary data and then the video ID for that specific short. videoIdFilterGroup.addAll( - new ByteArrayFilterGroup(null, "id.reel_like_button"), - new ByteArrayFilterGroup(null, "id.reel_dislike_button") + new ByteArrayFilterGroup( + null, + "id.reel_like_button", + "id.reel_dislike_button", + "ic_right_like", + "ic_right_dislike" + ) ); } @Override - public boolean isFiltered(String identifier, String path, byte[] buffer, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) { return false; @@ -86,8 +98,8 @@ public final class ReturnYouTubeDislikeFilter extends Filter { if (result.isFiltered()) { String matchedVideoId = findVideoId(buffer); // Matched video will be null if in incognito mode. - // Must pass a null id to correctly clear out the current video data. - // Otherwise if a Short is opened in non-incognito, then incognito is enabled and another Short is opened, + // Must pass a null ID to correctly clear out the current video data. + // Otherwise, if a Short is opened in non-incognito, then incognito is enabled and another Short is opened, // the new incognito Short will show the old prior data. ReturnYouTubeDislikePatch.setLastLithoShortsVideoId(matchedVideoId); } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ShortsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ShortsFilter.java index bd07da6a76..462cd8c2e1 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ShortsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ShortsFilter.java @@ -4,10 +4,12 @@ import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButt import android.view.View; +import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.patches.litho.Filter; import app.revanced.extension.shared.patches.litho.FilterGroup.*; import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup; import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.litho.LithoFilterPatch; import app.revanced.extension.shared.settings.BooleanSetting; import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar; @@ -20,10 +22,11 @@ import java.util.Map; import app.revanced.extension.shared.Logger; import app.revanced.extension.youtube.patches.VersionCheckPatch; import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.EngagementPanel; import app.revanced.extension.youtube.shared.NavigationBar; import app.revanced.extension.youtube.shared.PlayerType; -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "FieldCanBeLocal"}) public final class ShortsFilter extends Filter { private static final boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get(); private static final String COMPONENT_TYPE = "ComponentType"; @@ -67,18 +70,22 @@ public final class ShortsFilter extends Filter { private final StringFilterGroup shortsCompactFeedVideo; private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer; + private final StringFilterGroup channelProfile; + private final ByteArrayFilterGroup channelProfileShelfHeaderBuffer; private final StringFilterGroup useSoundButton; private final ByteArrayFilterGroup useSoundButtonBuffer; private final StringFilterGroup useTemplateButton; private final ByteArrayFilterGroup useTemplateButtonBuffer; - private final StringFilterGroup reelCarousel; - private final ByteArrayFilterGroup reelCarouselBuffer; private final StringFilterGroup autoDubbedLabel; private final StringFilterGroup subscribeButton; private final StringFilterGroup joinButton; private final StringFilterGroup paidPromotionLabel; - private final StringFilterGroup shelfHeader; + private final StringFilterGroup shelfHeaderIdentifier; + private final StringFilterGroup shelfHeaderPath; + + private final StringFilterGroup reelCarousel; + private final ByteArrayFilterGroupList reelCarouselBuffer = new ByteArrayFilterGroupList(); private final StringFilterGroup suggestedAction; private final ByteArrayFilterGroupList suggestedActionsBuffer = new ByteArrayFilterGroupList(); @@ -97,24 +104,34 @@ public final class ShortsFilter extends Filter { "shorts_shelf", "inline_shorts", "shorts_grid", - "shorts_video_cell", + "shorts_video_cell" + ); + + channelProfile = new StringFilterGroup( + Settings.HIDE_SHORTS_CHANNEL, "shorts_pivot_item" ); + channelProfileShelfHeaderBuffer = new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_CHANNEL, + "Shorts" + ); + // Feed Shorts shelf header. // Use a different filter group for this pattern, as it requires an additional check after matching. - shelfHeader = new StringFilterGroup( + shelfHeaderIdentifier = new StringFilterGroup( null, "shelf_header.e" ); - addIdentifierCallbacks(shortsIdentifiers, shelfHeader); + addIdentifierCallbacks(shortsIdentifiers, channelProfile, shelfHeaderIdentifier); // // Path components. // - shortsCompactFeedVideo = new StringFilterGroup(null, + shortsCompactFeedVideo = new StringFilterGroup( + null, // Shorts that appear in the feed/search when the device is using tablet layout. "compact_video.e", // 'video_lockup_with_attachment.e' is shown instead of 'compact_video.e' for some users @@ -125,7 +142,14 @@ public final class ShortsFilter extends Filter { // Filter out items that use the 'frame0' thumbnail. // This is a valid thumbnail for both regular videos and Shorts, // but it appears these thumbnails are used only for Shorts. - shortsCompactFeedVideoBuffer = new ByteArrayFilterGroup(null, "/frame0.jpg"); + shortsCompactFeedVideoBuffer = new ByteArrayFilterGroup( + null, + "/frame0.jpg"); + + shelfHeaderPath = new StringFilterGroup( + null, + "shelf_header.e" + ); // Shorts player components. StringFilterGroup pausedOverlayButtons = new StringFilterGroup( @@ -229,13 +253,21 @@ public final class ShortsFilter extends Filter { ); reelCarousel = new StringFilterGroup( - Settings.HIDE_SHORTS_SOUND_METADATA_LABEL, + null, "reel_carousel.e" ); - reelCarouselBuffer = new ByteArrayFilterGroup( - null, - "FEsfv_audio_pivot" + reelCarouselBuffer.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_AI_BUTTON, + "yt_outline_info_circle", + "yt_outline_experimental_info_circle" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_SOUND_METADATA_LABEL, + "yt_outline_audio", // Doesn't seem to be needed as v20.14.43 uses 'yt_outline_experimental_audio' as well. But still just in case. + "yt_outline_experimental_audio" + ) ); useSoundButton = new StringFilterGroup( @@ -279,10 +311,10 @@ public final class ShortsFilter extends Filter { ); addPathCallbacks( - shortsCompactFeedVideo, joinButton, subscribeButton, paidPromotionLabel, livePreview, - suggestedAction, pausedOverlayButtons, channelBar, previewComment, autoDubbedLabel, - fullVideoLinkLabel, videoTitle, useSoundButton, reelSoundMetadata, soundButton, reelCarousel, - infoPanel, stickers, likeFountain, likeButton, dislikeButton + shortsCompactFeedVideo, shelfHeaderPath, joinButton, subscribeButton, paidPromotionLabel, + livePreview, suggestedAction, pausedOverlayButtons, channelBar, infoPanel, previewComment, + autoDubbedLabel, fullVideoLinkLabel, videoTitle, useSoundButton, soundButton, stickers, + reelCarousel, reelSoundMetadata, likeFountain, likeButton, dislikeButton ); // Legacy hiding of Shorts action buttons. Because of 20.31+ buffer changes @@ -331,7 +363,7 @@ public final class ShortsFilter extends Filter { 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. + // Instead, use a unique identifier found in the buffer. "PAproduct_listZ" ), new ByteArrayFilterGroup( @@ -402,8 +434,23 @@ public final class ShortsFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String path, byte[] buffer, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (contentType == FilterContentType.IDENTIFIER) { + if (matchedGroup == shelfHeaderIdentifier) { + // Shelf header reused in history/channel/etc. + // Shorts header is always index 0 + if (contentIndex != 0) { + return false; + } + } + if (matchedGroup == channelProfile) { + return true; + } + + return shouldHideShortsFeedItems(); + } + if (contentType == FilterContentType.PATH) { if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionLabel || matchedGroup == autoDubbedLabel) { @@ -428,6 +475,19 @@ public final class ShortsFilter extends Filter { return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(buffer).isFiltered(); } + if (matchedGroup == shelfHeaderPath) { + // Shelf header reused in history/channel/etc. + // Shorts header is always index 0 + if (contentIndex != 0) { + return false; + } + if (!channelProfileShelfHeaderBuffer.check(buffer).isFiltered()) { + return false; + } + + return shouldHideShortsFeedItems(); + } + // Video action buttons (comment, share, remix) have the same path. // Like and dislike are separate path filters and don't require buffer searching. if (matchedGroup == shortsActionBar) { @@ -449,40 +509,35 @@ public final class ShortsFilter extends Filter { return true; } - // 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. - if (contentIndex != 0) return false; - } - - return shouldHideShortsFeedItems(); + return false; } - private static boolean shouldHideShortsFeedItems() { + private boolean shouldHideShortsFeedItems() { // Known issue if hide home is on but at least one other hide is off: // // Shorts suggestions will load in the background if a video is opened and // immediately minimized before any suggestions are loaded. // In this state the player type will show minimized, which cannot // distinguish between Shorts suggestions loading in the player and between - // scrolling thru search/home/subscription tabs while a player is minimized. + // scrolling through search/home/subscription tabs while a player is minimized. final boolean hideHome = Settings.HIDE_SHORTS_HOME.get(); final boolean hideSubscriptions = Settings.HIDE_SHORTS_SUBSCRIPTIONS.get(); final boolean hideSearch = Settings.HIDE_SHORTS_SEARCH.get(); + final boolean hideVideoDescription = Settings.HIDE_SHORTS_VIDEO_DESCRIPTION.get(); final boolean hideHistory = Settings.HIDE_SHORTS_HISTORY.get(); - if (!hideHome && !hideSubscriptions && !hideSearch && !hideHistory) { + if (!hideHome && !hideSubscriptions && !hideSearch && !hideVideoDescription && !hideHistory) { return false; } - if (hideHome && hideSubscriptions && hideSearch && hideHistory) { + if (hideHome && hideSubscriptions && hideSearch && hideVideoDescription && hideHistory) { 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; + return EngagementPanel.isDescription() + ? hideVideoDescription // Player video description panel opened. + : hideHome; // For now, consider Shorts under video player the same as the home feed. } // Must check second, as search can be from any tab. @@ -509,6 +564,58 @@ public final class ShortsFilter extends Filter { }; } + /** + * Injection point. + *

+ * Hide action buttons by index. + *

+ * Regular video action buttons vary in order by video, country, and account. + * Therefore, hiding buttons by index may hide unintended buttons. + *

+ * Shorts action buttons are almost always in the same order. + * (From top to bottom: Like, Dislike, Comment, Share, Remix). + * Therefore, we can hide Shorts action buttons by index. + * + * @param pathBuilder Same as pathBuilder used in {@link LithoFilterPatch}. + * @param treeNodeResultList List containing Litho components. + */ + public static void hideActionButtons(StringBuilder pathBuilder, List treeNodeResultList) { + try { + if (pathBuilder == null || pathBuilder.length() == 0 || treeNodeResultList == null) { + return; + } + int size = treeNodeResultList.size(); + + // The minimum size of the target List is 4. + if (size < 4) { + return; + } + String path = pathBuilder.toString(); + + if (!Utils.containsAny(path, REEL_ACTION_BAR_PATHS) + // Regular Shorts: [ComponentType, ComponentType, ComponentType, ComponentType, ComponentType] + // Shorts ads: [ComponentType, ComponentType, ComponentType, ComponentType] (No Remix button) + || !COMPONENT_TYPE.equals(treeNodeResultList.get(0).toString())) { + return; + } + // Removing elements without iterating through the list in reverse order will throw an exception. + for (int i = size - 1; i > -1; i--) { + // treeNodeResult is each button. + Object treeNodeResult = treeNodeResultList.get(i); + if (treeNodeResult != null) { + BooleanSetting setting = REEL_ACTION_BUTTONS_MAP.get(i); + if (setting != null && setting.get()) { + int finalI = i; + Logger.printDebug(() -> "Hiding action button by index: " + finalI + ", key: " + setting.key); + treeNodeResultList.remove(i); + } + } + } + } catch (Exception ex) { + Logger.printException(() -> "hideActionButtons failed", ex); + } + } + /** * Injection point. */ diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/VideoActionButtonsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/VideoActionButtonsFilter.java new file mode 100644 index 0000000000..7d1e1cca71 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/VideoActionButtonsFilter.java @@ -0,0 +1,180 @@ +package app.revanced.extension.youtube.patches.litho; + +import app.revanced.extension.shared.patches.litho.Filter; +import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; +import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.litho.FilterGroupList.StringFilterGroupList; +import app.revanced.extension.youtube.patches.VersionCheckPatch; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class VideoActionButtonsFilter extends Filter { + private static final String COMPACT_CHANNEL_BAR_PATH_PREFIX = "compact_channel_bar.e"; + private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.e"; + private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.e"; + /** + * Video bar path when the video information is collapsed. Seems to shown only with 20.14+ + */ + private static final String COMPACTIFY_VIDEO_ACTION_BAR_PATH = "compactify_video_action_bar.e"; + private static final String ANIMATED_VECTOR_TYPE_PATH = "AnimatedVectorType"; + + private final StringFilterGroup likeSubscribeGlow; + private final StringFilterGroup actionBarGroup; + private final StringFilterGroup buttonFilterPathGroup; + private final StringFilterGroupList accessibilityButtonsGroupList = new StringFilterGroupList(); + private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList(); + + public VideoActionButtonsFilter() { + actionBarGroup = new StringFilterGroup( + null, + VIDEO_ACTION_BAR_PATH + ); + addIdentifierCallbacks(actionBarGroup); + + + likeSubscribeGlow = new StringFilterGroup( + Settings.DISABLE_LIKE_SUBSCRIBE_GLOW, + "animated_button_border.e" + ); + + buttonFilterPathGroup = new StringFilterGroup( + null, + "|ContainerType|button.e" + ); + + addPathCallbacks( + likeSubscribeGlow, + new StringFilterGroup( + Settings.HIDE_LIKE_DISLIKE_BUTTON, + "|segmented_like_dislike_button" + ), + new StringFilterGroup( + Settings.HIDE_DOWNLOAD_BUTTON, + "|download_button.e" + ), + new StringFilterGroup( + Settings.HIDE_SAVE_BUTTON, + "|save_to_playlist_button" + ), + new StringFilterGroup( + Settings.HIDE_CLIP_BUTTON, + "|clip_button.e" + ) + ); + + addPathCallbacks(buttonFilterPathGroup); + + if (VersionCheckPatch.IS_20_22_OR_GREATER) { + // FIXME: Most buttons do not have an accessibilityId. + // Instead, they have an accessibilityText, so hiding functionality must be implemented using this + // (e.g. custom filter - 'video_action_bar#Hype') + accessibilityButtonsGroupList.addAll( + new StringFilterGroup( + Settings.HIDE_SHARE_BUTTON, + "id.video.share.button" + ), + new StringFilterGroup( + Settings.HIDE_REMIX_BUTTON, + "id.video.remix.button" + ) + ); + } else { + bufferButtonsGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_REPORT_BUTTON, + "yt_outline_flag" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHARE_BUTTON, + "yt_outline_share" + ), + new ByteArrayFilterGroup( + Settings.HIDE_REMIX_BUTTON, + "yt_outline_youtube_shorts_plus" + ), + new ByteArrayFilterGroup( + Settings.HIDE_THANKS_BUTTON, + "yt_outline_dollar_sign_heart" + ), + new ByteArrayFilterGroup( + Settings.HIDE_ASK_BUTTON, + "yt_fill_spark" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHOP_BUTTON, + "yt_outline_bag" + ), + new ByteArrayFilterGroup( + Settings.HIDE_STOP_ADS_BUTTON, + "yt_outline_slash_circle_left" + ), + new ByteArrayFilterGroup( + Settings.HIDE_COMMENTS_BUTTON, + "yt_outline_message_bubble_right" + ), + // Check for clip button both here and using a path filter, + // as there's a chance the path is a generic action button and won't contain 'clip_button' + new ByteArrayFilterGroup( + Settings.HIDE_CLIP_BUTTON, + "yt_outline_scissors" + ), + new ByteArrayFilterGroup( + Settings.HIDE_HYPE_BUTTON, + "yt_outline_star_shooting" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PROMOTE_BUTTON, + "yt_outline_megaphone" + ) + ); + } + } + + private boolean isEveryFilterGroupEnabled() { + for (var group : pathCallbacks) { + if (!group.isEnabled()) return false; + } + + var buttonList = VersionCheckPatch.IS_20_22_OR_GREATER + ? accessibilityButtonsGroupList + : bufferButtonsGroupList; + for (var group : buttonList) { + if (!group.isEnabled()) return false; + } + + return true; + } + + private boolean hideButtons(String path, String accessibility, byte[] buffer) { + // Make sure the current path is the right one to avoid false positives. + if (!path.startsWith(VIDEO_ACTION_BAR_PATH) && !path.startsWith(COMPACTIFY_VIDEO_ACTION_BAR_PATH)) { + return false; + } + + return VersionCheckPatch.IS_20_22_OR_GREATER + ? accessibilityButtonsGroupList.check(accessibility).isFiltered() + : bufferButtonsGroupList.check(buffer).isFiltered(); + } + + @Override + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (matchedGroup == likeSubscribeGlow) { + return path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX) + || path.startsWith(COMPACTIFY_VIDEO_ACTION_BAR_PATH); + } + + // If the current matched group is the action bar group, + // in case every filter group is enabled, hide the action bar. + if (matchedGroup == actionBarGroup) { + return isEveryFilterGroupEnabled(); + } + + if (matchedGroup == buttonFilterPathGroup) { + return hideButtons(path, accessibility, buffer); + } + + return true; + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/HidePremiumVideoQualityPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/HidePremiumVideoQualityPatch.java new file mode 100644 index 0000000000..907d1f4762 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/HidePremiumVideoQualityPatch.java @@ -0,0 +1,31 @@ +package app.revanced.extension.youtube.patches.playback.quality; + +import static app.revanced.extension.youtube.patches.VideoInformation.isPremiumVideoQuality; + +import java.util.Arrays; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.youtube.patches.VideoInformation.VideoQualityInterface; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class HidePremiumVideoQualityPatch { + private static final boolean HIDE_PREMIUM_VIDEO_QUALITY = Settings.HIDE_PREMIUM_VIDEO_QUALITY.get(); + + /** + * Injection point. + */ + public static Object[] hidePremiumVideoQuality(VideoQualityInterface[] qualities) { + if (HIDE_PREMIUM_VIDEO_QUALITY && qualities != null && qualities.length > 0) { + try { + return Arrays.stream(qualities) + .filter(quality -> quality != null && !isPremiumVideoQuality(quality)) + .toArray(VideoQualityInterface[]::new); + } catch (Exception ex) { + Logger.printException(() -> "Failed to hide Premium video quality", ex); + } + } + + return qualities; + } +} \ No newline at end of file diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index 1f726c2a5b..1cd584d617 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -3,13 +3,12 @@ package app.revanced.extension.youtube.patches.playback.quality; import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.Utils.NetworkType; -import com.google.android.libraries.youtube.innertube.model.media.VideoQuality; - import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.IntegerSetting; import app.revanced.extension.youtube.patches.VideoInformation; +import app.revanced.extension.youtube.patches.VideoInformation.*; import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.shared.ShortsPlayerState; @@ -74,12 +73,12 @@ public class RememberVideoQualityPatch { public static void userChangedShortsQuality(int userSelectedQualityIndex) { try { if (shouldRememberVideoQuality()) { - VideoQuality[] currentQualities = VideoInformation.getCurrentQualities(); + VideoQualityInterface[] currentQualities = VideoInformation.getCurrentQualities(); if (currentQualities == null) { Logger.printDebug(() -> "Cannot save default quality, qualities is null"); return; } - VideoQuality quality = currentQualities[userSelectedQualityIndex]; + VideoQualityInterface quality = currentQualities[userSelectedQualityIndex]; saveDefaultQuality(quality.patch_getResolution()); } } catch (Exception ex) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/ThemePatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/ThemePatch.java index 16e0b0459a..e99b86c402 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/ThemePatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/ThemePatch.java @@ -9,7 +9,8 @@ import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") public class ThemePatch extends BaseThemePatch { public enum SplashScreenAnimationStyle { - DEFAULT(0), + // 0 int style exists in target app as a fall through default, but its value is repurposed to be disabled. + DISABLED(0), FPS_60_ONE_SECOND(1), FPS_60_TWO_SECOND(2), FPS_60_FIVE_SECOND(3), @@ -18,7 +19,7 @@ public class ThemePatch extends BaseThemePatch { FPS_30_TWO_SECOND(6), FPS_30_FIVE_SECOND(7), FPS_30_BLACK_AND_WHITE(8); - // There exists a 10th json style used as the switch statement default, + // There exists a 10th JSON style used as the switch statement default, // but visually it is identical to 60fps one second. @Nullable @@ -75,12 +76,30 @@ public class ThemePatch extends BaseThemePatch { return Settings.GRADIENT_LOADING_SCREEN.get(); } + /** + * Injection point. + */ + public static boolean showSplashScreen(boolean original) { + return Settings.SPLASH_SCREEN_ANIMATION_STYLE.get() != SplashScreenAnimationStyle.DISABLED && original; + } + + /** + * Injection point. + */ + public static int showSplashScreen(int i, int i2) { + if (Settings.SPLASH_SCREEN_ANIMATION_STYLE.get() != SplashScreenAnimationStyle.DISABLED || i != i2) { + return i; + } + return i - 1; + } + /** * Injection point. */ public static int getLoadingScreenType(int original) { SplashScreenAnimationStyle style = Settings.SPLASH_SCREEN_ANIMATION_STYLE.get(); - if (style == SplashScreenAnimationStyle.DEFAULT) { + + if (style == SplashScreenAnimationStyle.DISABLED) { return original; } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java index 5328fb1071..f304f3673e 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java @@ -42,7 +42,7 @@ import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.ui.Dim; import app.revanced.extension.youtube.returnyoutubedislike.requests.RYDVoteData; -import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; +import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeAPI; import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.shared.PlayerType; @@ -91,7 +91,7 @@ public class ReturnYouTubeDislike { private static final char MIDDLE_SEPARATOR_CHARACTER = '◎'; // 'bullseye' /** - * Cached lookup of all video ids. + * Cached lookup of all video IDs. */ @GuardedBy("itself") private static final Map fetchCache = new HashMap<>(); @@ -206,8 +206,8 @@ public class ReturnYouTubeDislike { return newSpannableWithDislikes(oldSpannable, voteData); } - // Note: Some locales use right to left layout (Arabic, Hebrew, etc). - // If making changes to this code, change device settings to a RTL language and verify layout is correct. + // Note: Some locales use right to left layout (Arabic, Hebrew, etc.). + // If making changes to this code, change device settings to an RTL language and verify layout is correct. CharSequence oldLikes = oldSpannable; // YouTube creators can hide the like count on a video, @@ -419,7 +419,7 @@ public class ReturnYouTubeDislike { private ReturnYouTubeDislike(@NonNull String videoId) { this.videoId = Objects.requireNonNull(videoId); this.timeFetched = System.currentTimeMillis(); - this.future = Utils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId)); + this.future = Utils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeAPI.fetchVotes(videoId)); } private boolean isExpired(long now) { @@ -512,7 +512,7 @@ public class ReturnYouTubeDislike { if (votingData == null) { // Method automatically prevents showing multiple toasts if the connection failed. // This call is needed here in case the api call did succeed but took too long. - ReturnYouTubeDislikeApi.handleConnectionError( + ReturnYouTubeDislikeAPI.handleConnectionError( str("revanced_ryd_failure_connection_timeout"), null, null, Toast.LENGTH_SHORT); Logger.printDebug(() -> "Cannot add dislike to UI (RYD data not available)"); @@ -549,7 +549,7 @@ public class ReturnYouTubeDislike { } // Scrolling Shorts does not cause the Spans to be reloaded, - // so there is no need to cache the likes for this situations. + // so there is no need to cache the likes for these situations. Logger.printDebug(() -> "Creating likes span for: " + votingData.videoId); return newSpannableWithLikes(original, votingData); } @@ -601,7 +601,7 @@ public class ReturnYouTubeDislike { voteSerialExecutor.execute(() -> { try { // Must wrap in try/catch to properly log exceptions. - ReturnYouTubeDislikeApi.sendVote(videoId, vote); + ReturnYouTubeDislikeAPI.sendVote(videoId, vote); } catch (Exception ex) { Logger.printException(() -> "Failed to send vote", ex); } @@ -675,7 +675,7 @@ class VerticallyCenteredImageSpan extends ImageSpan { /** * @param useOriginalWidth Use the original layout width of the text this span is applied to, - * and not the bounds of the Drawable. Drawable is always displayed using it's own bounds, + * and not the bounds of the Drawable. Drawable is always displayed using its own bounds, * and this setting only affects the layout width of the entire span. */ public VerticallyCenteredImageSpan(Drawable drawable, boolean useOriginalWidth) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/RYDVoteData.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/RYDVoteData.java index b57eadcfd0..78841e286d 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/RYDVoteData.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/RYDVoteData.java @@ -29,7 +29,7 @@ public final class RYDVoteData { private volatile long likeCount; // Read/write from different threads. /** * Like count can be hidden by video creator, but RYD still tracks the number - * of like/dislikes it received thru it's browser extension and and API. + * of like/dislikes it received through its browser extension and API. * The raw like/dislikes can be used to calculate a percentage. * * Raw values can be null, especially for older videos with little to no views. @@ -74,7 +74,7 @@ public final class RYDVoteData { } /** - * Public like count of the video, as reported by YT when RYD last updated it's data. + * Public like count of the video, as reported by YT when RYD last updated its data. * * If the likes were hidden by the video creator, then this returns an * estimated likes using the same extrapolation as the dislikes. diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeAPI.java similarity index 95% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java rename to extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeAPI.java index 18d46282c9..7c35682209 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeAPI.java @@ -28,7 +28,7 @@ import app.revanced.extension.shared.requests.Requester; import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.extension.youtube.settings.Settings; -public class ReturnYouTubeDislikeApi { +public class ReturnYouTubeDislikeAPI { /** * {@link #fetchVotes(String)} TCP connection timeout. */ @@ -43,7 +43,7 @@ public class ReturnYouTubeDislikeApi { /** * Default connection and response timeout for voting and registration. * - * Voting and user registration runs in the background and has has no urgency + * Voting and user registration runs in the background and has no urgency * so this can be a larger value. */ private static final int API_REGISTER_VOTE_TIMEOUT_MILLISECONDS = 60 * 1000; // 60 Seconds. @@ -109,7 +109,7 @@ public class ReturnYouTubeDislikeApi { /** * Total time spent waiting for {@link #fetchVotes(String)} network call to complete. - * Value does does not persist on app shut down. + * Value does not persist on app shut down. */ private static volatile long fetchCallResponseTimeTotal; @@ -147,7 +147,7 @@ public class ReturnYouTubeDislikeApi { return numberOfRateLimitRequestsEncountered; } - private ReturnYouTubeDislikeApi() { + private ReturnYouTubeDislikeAPI() { } // utility class /** @@ -251,7 +251,7 @@ public class ReturnYouTubeDislikeApi { if (!lastApiCallFailed && Settings.RYD_TOAST_ON_CONNECTION_ERROR.get()) { if (responseCode != null && responseCode == HTTP_STATUS_CODE_UNAUTHORIZED) { Logger.printInfo(() -> "Ignoring status code " + HTTP_STATUS_CODE_UNAUTHORIZED - + " (API authorization erorr)"); + + " (API authorization error)"); return; // Do not set api failure field. } else if (toastDuration != null) { Utils.showToast(toastMessage, toastDuration); @@ -281,7 +281,7 @@ public class ReturnYouTubeDislikeApi { // request headers, as per https://returnyoutubedislike.com/docs/fetching // the documentation says to use 'Accept:text/html', but the RYD browser plugin uses 'Accept:application/json' connection.setRequestProperty("Accept", "application/json"); - connection.setRequestProperty("Connection", "keep-alive"); // keep-alive is on by default with http 1.1, but specify anyways + connection.setRequestProperty("Connection", "keep-alive"); // keep-alive is on by default with http 1.1, but specify anyway connection.setRequestProperty("Pragma", "no-cache"); connection.setRequestProperty("Cache-Control", "no-cache"); connection.setUseCaches(false); @@ -306,7 +306,7 @@ public class ReturnYouTubeDislikeApi { Logger.printDebug(() -> "Voting data fetched: " + votingData); return votingData; } catch (JSONException ex) { - Logger.printException(() -> "Failed to parse video: " + videoId + " json: " + json, ex); + Logger.printException(() -> "Failed to parse video: " + videoId + " JSON: " + json, ex); // fall thru to update statistics } } else { @@ -329,7 +329,7 @@ public class ReturnYouTubeDislikeApi { } /** - * @return The newly created and registered user id. Returns NULL if registration failed. + * @return The newly created and registered user ID. Returns NULL if registration failed. */ @Nullable public static String registerAsNewUser() { @@ -338,10 +338,10 @@ public class ReturnYouTubeDislikeApi { if (checkIfRateLimitInEffect("registerAsNewUser")) { return null; } - String userId = randomString(36); + String userID = randomString(36); Logger.printDebug(() -> "Trying to register new user"); - HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId); + HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userID); connection.setRequestProperty("Accept", "application/json"); connection.setConnectTimeout(API_REGISTER_VOTE_TIMEOUT_MILLISECONDS); connection.setReadTimeout(API_REGISTER_VOTE_TIMEOUT_MILLISECONDS); @@ -357,7 +357,7 @@ public class ReturnYouTubeDislikeApi { int difficulty = json.getInt("difficulty"); String solution = solvePuzzle(challenge, difficulty); - return confirmRegistration(userId, solution); + return confirmRegistration(userID, solution); } handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), @@ -374,9 +374,9 @@ public class ReturnYouTubeDislikeApi { } @Nullable - private static String confirmRegistration(String userId, String solution) { + private static String confirmRegistration(String userID, String solution) { Utils.verifyOffMainThread(); - Objects.requireNonNull(userId); + Objects.requireNonNull(userID); Objects.requireNonNull(solution); try { if (checkIfRateLimitInEffect("confirmRegistration")) { @@ -384,7 +384,7 @@ public class ReturnYouTubeDislikeApi { } Logger.printDebug(() -> "Trying to confirm registration with solution: " + solution); - HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userId); + HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userID); applyCommonPostRequestSettings(connection); String jsonInputString = "{\"solution\": \"" + solution + "\"}"; @@ -401,12 +401,12 @@ public class ReturnYouTubeDislikeApi { } if (responseCode == HTTP_STATUS_CODE_SUCCESS) { Logger.printDebug(() -> "Registration confirmation successful"); - return userId; + return userID; } // Something went wrong, might as well disconnect. String response = Requester.parseStringAndDisconnect(connection); - Logger.printInfo(() -> "Failed to confirm registration for user: " + userId + Logger.printInfo(() -> "Failed to confirm registration for user: " + userID + " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "''"); handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), responseCode, null, Toast.LENGTH_LONG); @@ -416,7 +416,7 @@ public class ReturnYouTubeDislikeApi { handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"), null, ex, Toast.LENGTH_LONG); } catch (Exception ex) { - Logger.printException(() -> "Failed to confirm registration for user: " + userId + Logger.printException(() -> "Failed to confirm registration for user: " + userID + "solution: " + solution, ex); } return null; @@ -429,19 +429,19 @@ public class ReturnYouTubeDislikeApi { * and the network call fails, this returns NULL. */ @Nullable - private static String getUserId() { + private static String getUserID() { Utils.verifyOffMainThread(); - String userId = Settings.RYD_USER_ID.get(); - if (!userId.isEmpty()) { - return userId; + String userID = Settings.RYD_USER_ID.get(); + if (!userID.isEmpty()) { + return userID; } - userId = registerAsNewUser(); - if (userId != null) { - Settings.RYD_USER_ID.save(userId); + userID = registerAsNewUser(); + if (userID != null) { + Settings.RYD_USER_ID.save(userID); } - return userId; + return userID; } public static boolean sendVote(String videoId, ReturnYouTubeDislike.Vote vote) { @@ -450,8 +450,8 @@ public class ReturnYouTubeDislikeApi { Objects.requireNonNull(vote); try { - String userId = getUserId(); - if (userId == null) return false; + String userID = getUserID(); + if (userID == null) return false; if (checkIfRateLimitInEffect("sendVote")) { return false; @@ -461,7 +461,7 @@ public class ReturnYouTubeDislikeApi { HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE); applyCommonPostRequestSettings(connection); - String voteJsonString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote.value + "\"}"; + String voteJsonString = "{\"userId\": \"" + userID + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote.value + "\"}"; byte[] body = voteJsonString.getBytes(StandardCharsets.UTF_8); connection.setFixedLengthStreamingMode(body.length); try (OutputStream os = connection.getOutputStream()) { @@ -479,7 +479,7 @@ public class ReturnYouTubeDislikeApi { int difficulty = json.getInt("difficulty"); String solution = solvePuzzle(challenge, difficulty); - return confirmVote(videoId, userId, solution); + return confirmVote(videoId, userID, solution); } Logger.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote @@ -498,10 +498,10 @@ public class ReturnYouTubeDislikeApi { return false; } - private static boolean confirmVote(String videoId, String userId, String solution) { + private static boolean confirmVote(String videoId, String userID, String solution) { Utils.verifyOffMainThread(); Objects.requireNonNull(videoId); - Objects.requireNonNull(userId); + Objects.requireNonNull(userID); Objects.requireNonNull(solution); try { @@ -512,7 +512,7 @@ public class ReturnYouTubeDislikeApi { HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE); applyCommonPostRequestSettings(connection); - String jsonInputString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}"; + String jsonInputString = "{\"userId\": \"" + userID + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}"; byte[] body = jsonInputString.getBytes(StandardCharsets.UTF_8); connection.setFixedLengthStreamingMode(body.length); try (OutputStream os = connection.getOutputStream()) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java index 8aa739d206..996158160c 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java @@ -3,15 +3,15 @@ package app.revanced.extension.youtube.returnyoutubedislike.ui; import android.content.Context; import android.util.AttributeSet; -import app.revanced.extension.shared.settings.preference.UrlLinkPreference; +import app.revanced.extension.shared.settings.preference.URLLinkPreference; /** * Allows tapping the RYD about preference to open the website. */ @SuppressWarnings("unused") -public class ReturnYouTubeDislikeAboutPreference extends UrlLinkPreference { +public class ReturnYouTubeDislikeAboutPreference extends URLLinkPreference { { - externalUrl = "https://returnyoutubedislike.com"; + externalURL = "https://returnyoutubedislike.com"; } public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeDebugStatsPreferenceCategory.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeDebugStatsPreferenceCategory.java index dac275e4fc..67764d02be 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeDebugStatsPreferenceCategory.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeDebugStatsPreferenceCategory.java @@ -11,7 +11,7 @@ import android.view.ViewGroup; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.settings.BaseSettings; -import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; +import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeAPI; @SuppressWarnings({"unused", "deprecation"}) public class ReturnYouTubeDislikeDebugStatsPreferenceCategory extends PreferenceCategory { @@ -63,22 +63,22 @@ public class ReturnYouTubeDislikeDebugStatsPreferenceCategory extends Preference addStatisticPreference( "revanced_ryd_statistics_getFetchCallResponseTimeAverage_title", - createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage()) + createMillisecondStringFromNumber(ReturnYouTubeDislikeAPI.getFetchCallResponseTimeAverage()) ); addStatisticPreference( "revanced_ryd_statistics_getFetchCallResponseTimeMin_title", - createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin()) + createMillisecondStringFromNumber(ReturnYouTubeDislikeAPI.getFetchCallResponseTimeMin()) ); addStatisticPreference( "revanced_ryd_statistics_getFetchCallResponseTimeMax_title", - createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax()) + createMillisecondStringFromNumber(ReturnYouTubeDislikeAPI.getFetchCallResponseTimeMax()) ); String fetchCallTimeWaitingLastSummary; - final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast(); - if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) { + final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeAPI.getFetchCallResponseTimeLast(); + if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeAPI.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) { fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary"); } else { fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast); @@ -90,7 +90,7 @@ public class ReturnYouTubeDislikeDebugStatsPreferenceCategory extends Preference addStatisticPreference( "revanced_ryd_statistics_getFetchCallCount_title", - createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(), + createSummaryText(ReturnYouTubeDislikeAPI.getFetchCallCount(), "revanced_ryd_statistics_getFetchCallCount_zero_summary", "revanced_ryd_statistics_getFetchCallCount_non_zero_summary" ) @@ -98,7 +98,7 @@ public class ReturnYouTubeDislikeDebugStatsPreferenceCategory extends Preference addStatisticPreference( "revanced_ryd_statistics_getFetchCallNumberOfFailures_title", - createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(), + createSummaryText(ReturnYouTubeDislikeAPI.getFetchCallNumberOfFailures(), "revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary", "revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary" ) @@ -106,7 +106,7 @@ public class ReturnYouTubeDislikeDebugStatsPreferenceCategory extends Preference addStatisticPreference( "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title", - createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(), + createSummaryText(ReturnYouTubeDislikeAPI.getNumberOfRateLimitRequestsEncountered(), "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary", "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary" ) diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java index d2b41b229b..da8d9d4e95 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -18,7 +18,6 @@ import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerH import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerHorizontalDragAvailability; import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType; import static app.revanced.extension.youtube.patches.OpenShortsInRegularPlayerPatch.ShortsPlayerType; -import static app.revanced.extension.youtube.patches.SeekbarThumbnailsPatch.SeekbarThumbnailsHighQualityAvailability; import static app.revanced.extension.youtube.patches.litho.PlayerFlyoutMenuItemsFilter.HideAudioFlyoutMenuAvailability; import static app.revanced.extension.youtube.patches.spoof.SpoofVideoStreamsPatch.SpoofClientAv1Availability; import static app.revanced.extension.youtube.patches.theme.ThemePatch.SplashScreenAnimationStyle; @@ -56,6 +55,7 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE); public static final BooleanSetting FORCE_AVC_CODEC = new BooleanSetting("revanced_force_avc_codec", FALSE, true, "revanced_force_avc_codec_user_dialog_message"); public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", TRUE, true); + public static final BooleanSetting HIDE_PREMIUM_VIDEO_QUALITY = new BooleanSetting("revanced_hide_premium_video_quality", TRUE, true); public static final IntegerSetting VIDEO_QUALITY_DEFAULT_WIFI = new IntegerSetting("revanced_video_quality_default_wifi", -2); public static final IntegerSetting VIDEO_QUALITY_DEFAULT_MOBILE = new IntegerSetting("revanced_video_quality_default_mobile", -2); public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", FALSE); @@ -80,16 +80,15 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_CREATOR_STORE_SHELF = new BooleanSetting("revanced_hide_creator_store_shelf", TRUE); public static final BooleanSetting HIDE_END_SCREEN_STORE_BANNER = new BooleanSetting("revanced_hide_end_screen_store_banner", TRUE, true); public static final BooleanSetting HIDE_FULLSCREEN_ADS = new BooleanSetting("revanced_hide_fullscreen_ads", TRUE); + public static final BooleanSetting HIDE_PLAYER_POPUP_ADS = new BooleanSetting("revanced_hide_player_popup_ads", TRUE); public static final BooleanSetting HIDE_GENERAL_ADS = new BooleanSetting("revanced_hide_general_ads", TRUE); - public static final BooleanSetting HIDE_GET_PREMIUM = new BooleanSetting("revanced_hide_get_premium", TRUE); - public static final BooleanSetting HIDE_LATEST_POSTS = new BooleanSetting("revanced_hide_latest_posts", TRUE); public static final BooleanSetting HIDE_MERCHANDISE_BANNERS = new BooleanSetting("revanced_hide_merchandise_banners", TRUE); public static final BooleanSetting HIDE_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_paid_promotion_label", TRUE); public static final BooleanSetting HIDE_SELF_SPONSOR = new BooleanSetting("revanced_hide_self_sponsor_ads", TRUE); public static final BooleanSetting HIDE_SHOPPING_LINKS = new BooleanSetting("revanced_hide_shopping_links", TRUE); public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_hide_video_ads", TRUE, true); public static final BooleanSetting HIDE_VIEW_PRODUCTS_BANNER = new BooleanSetting("revanced_hide_view_products_banner", TRUE); - public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE); + public static final BooleanSetting HIDE_YOUTUBE_PREMIUM_PROMOTIONS = new BooleanSetting("revanced_hide_youtube_premium_promotions", TRUE); // Feed public static final BooleanSetting HIDE_ALBUM_CARDS = new BooleanSetting("revanced_hide_album_cards", FALSE, true); @@ -97,8 +96,10 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_CHIPS_SHELF = new BooleanSetting("revanced_hide_chips_shelf", TRUE); public static final BooleanSetting HIDE_COMMUNITY_POSTS = new BooleanSetting("revanced_hide_community_posts", FALSE); public static final BooleanSetting HIDE_COMPACT_BANNER = new BooleanSetting("revanced_hide_compact_banner", TRUE); - public static final BooleanSetting HIDE_DOODLES = new BooleanSetting("revanced_hide_doodles", FALSE, true, "revanced_hide_doodles_user_dialog_message"); + public static final BooleanSetting HIDE_DOODLES = new BooleanSetting("revanced_hide_doodles", FALSE, true); public static final BooleanSetting HIDE_EXPANDABLE_CARD = new BooleanSetting("revanced_hide_expandable_card", TRUE); + public static final BooleanSetting HIDE_FEED_FLYOUT_MENU = new BooleanSetting("revanced_hide_feed_flyout_menu", FALSE); + public static final StringSetting HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS = new StringSetting("revanced_hide_feed_flyout_menu_filter_strings", "", true, parent(HIDE_FEED_FLYOUT_MENU)); public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_FEED = new BooleanSetting("revanced_hide_filter_bar_feed_in_feed", FALSE, true); public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_HISTORY = new BooleanSetting("revanced_hide_filter_bar_feed_in_history", FALSE); public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS = new BooleanSetting("revanced_hide_filter_bar_feed_in_related_videos", FALSE, true); @@ -106,16 +107,21 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_FLOATING_MICROPHONE_BUTTON = new BooleanSetting("revanced_hide_floating_microphone_button", TRUE, true); public static final BooleanSetting HIDE_HORIZONTAL_SHELVES = new BooleanSetting("revanced_hide_horizontal_shelves", TRUE); public static final BooleanSetting HIDE_IMAGE_SHELF = new BooleanSetting("revanced_hide_image_shelf", TRUE); + public static final BooleanSetting HIDE_LATEST_POSTS = new BooleanSetting("revanced_hide_latest_posts", TRUE); + public static final BooleanSetting HIDE_LATEST_VIDEOS_BUTTON = new BooleanSetting("revanced_hide_latest_videos_button", FALSE); 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_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE); public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true); + public static final BooleanSetting HIDE_SUBSCRIBED_CHANNELS_BAR = new BooleanSetting("revanced_hide_subscribed_channels_bar", FALSE, true); public static final BooleanSetting HIDE_SURVEYS = new BooleanSetting("revanced_hide_surveys", TRUE); public static final BooleanSetting HIDE_TICKET_SHELF = new BooleanSetting("revanced_hide_ticket_shelf", FALSE); public static final BooleanSetting HIDE_UPLOAD_TIME = new BooleanSetting("revanced_hide_upload_time", FALSE, "revanced_hide_upload_time_user_dialog_message"); public static final BooleanSetting HIDE_VIDEO_RECOMMENDATION_LABELS = new BooleanSetting("revanced_hide_video_recommendation_labels", TRUE); public static final BooleanSetting HIDE_VIEW_COUNT = new BooleanSetting("revanced_hide_view_count", FALSE, "revanced_hide_view_count_user_dialog_message"); + public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE); + public static final BooleanSetting HIDE_YOU_MAY_LIKE_SECTION = new BooleanSetting("revanced_hide_you_may_like_section", TRUE, true); public static final BooleanSetting HIDE_VISUAL_SPACER = new BooleanSetting("revanced_hide_visual_spacer", TRUE); // Alternative thumbnails @@ -138,6 +144,8 @@ public class Settings extends YouTubeAndMusicSettings { parentsAny(HIDE_KEYWORD_CONTENT_HOME, HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS, HIDE_KEYWORD_CONTENT_SEARCH)); // Channel page + public static final BooleanSetting HIDE_CHANNEL_TAB = new BooleanSetting("revanced_hide_channel_tab", FALSE); + public static final StringSetting HIDE_CHANNEL_TAB_FILTER_STRINGS = new StringSetting("revanced_hide_channel_tab_filter_strings", "", true, parent(HIDE_CHANNEL_TAB)); public static final BooleanSetting HIDE_COMMUNITY_BUTTON = new BooleanSetting("revanced_hide_community_button", TRUE); public static final BooleanSetting HIDE_FOR_YOU_SHELF = new BooleanSetting("revanced_hide_for_you_shelf", FALSE); public static final BooleanSetting HIDE_JOIN_BUTTON = new BooleanSetting("revanced_hide_join_button", FALSE); @@ -151,21 +159,31 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_copy_video_url_timestamp", TRUE); public static final BooleanSetting DISABLE_AUTO_CAPTIONS = new BooleanSetting("revanced_disable_auto_captions", FALSE, true); public static final BooleanSetting DISABLE_CHAPTER_SKIP_DOUBLE_TAP = new BooleanSetting("revanced_disable_chapter_skip_double_tap", FALSE); + public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE); + public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING = new BooleanSetting("revanced_disable_haptic_feedback_precise_seeking", FALSE); + public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO = new BooleanSetting("revanced_disable_haptic_feedback_seek_undo", FALSE); + public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_TAP_AND_HOLD = new BooleanSetting("revanced_disable_haptic_feedback_tap_and_hold", FALSE); + public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_ZOOM = new BooleanSetting("revanced_disable_haptic_feedback_zoom", FALSE); + public static final BooleanSetting DISABLE_PLAYER_POPUP_PANELS = new BooleanSetting("revanced_disable_player_popup_panels", FALSE); public static final BooleanSetting DISABLE_FULLSCREEN_AMBIENT_MODE = new BooleanSetting("revanced_disable_fullscreen_ambient_mode", TRUE, true); public static final BooleanSetting DISABLE_ROLLING_NUMBER_ANIMATIONS = new BooleanSetting("revanced_disable_rolling_number_animations", FALSE); public static final EnumSetting EXIT_FULLSCREEN = new EnumSetting<>("revanced_exit_fullscreen", FullscreenMode.DISABLED); public static final BooleanSetting HIDE_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_autoplay_button", TRUE, true); + public static final BooleanSetting HIDE_AUTOPLAY_PREVIEW = new BooleanSetting("revanced_hide_autoplay_preview", FALSE, 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_COLLAPSE_BUTTON = new BooleanSetting("revanced_hide_collapse_button", FALSE, true); public static final BooleanSetting HIDE_CHANNEL_BAR = new BooleanSetting("revanced_hide_channel_bar", FALSE); public static final BooleanSetting HIDE_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE); public static final BooleanSetting HIDE_CROWDFUNDING_BOX = new BooleanSetting("revanced_hide_crowdfunding_box", FALSE, true); public static final BooleanSetting HIDE_EMERGENCY_BOX = new BooleanSetting("revanced_hide_emergency_box", TRUE); - public static final BooleanSetting HIDE_ENDSCREEN_CARDS = new BooleanSetting("revanced_hide_endscreen_cards", FALSE); + public static final BooleanSetting HIDE_END_SCREEN_CARDS = new BooleanSetting("revanced_hide_end_screen_cards", FALSE); public static final BooleanSetting HIDE_END_SCREEN_SUGGESTED_VIDEO = new BooleanSetting("revanced_end_screen_suggested_video", FALSE, true); + public static final BooleanSetting HIDE_FULLSCREEN_BUTTON = new BooleanSetting("revanced_hide_fullscreen_button", FALSE, true); public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", FALSE); public static final BooleanSetting HIDE_INFO_PANELS = new BooleanSetting("revanced_hide_info_panels", TRUE); - public static final BooleanSetting HIDE_JOIN_MEMBERSHIP_BUTTON = new BooleanSetting("revanced_hide_join_membership_button", TRUE); + public static final BooleanSetting HIDE_JOIN_MEMBERSHIP_BUTTON = new BooleanSetting("revanced_hide_join_membership_button", TRUE, parentNot(HIDE_CHANNEL_BAR)); + public static final BooleanSetting HIDE_LIVE_CHAT_REPLAY_BUTTON = new BooleanSetting("revanced_hide_live_chat_replay_button", FALSE); public static final BooleanSetting HIDE_MEDICAL_PANELS = new BooleanSetting("revanced_hide_medical_panels", TRUE); public static final BooleanSetting HIDE_PLAYER_CONTROL_BUTTONS_BACKGROUND = new BooleanSetting("revanced_hide_player_control_buttons_background", FALSE, true); public static final BooleanSetting HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS = new BooleanSetting("revanced_hide_player_previous_next_buttons", FALSE, true); @@ -174,11 +192,11 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_RELATED_VIDEOS = new BooleanSetting("revanced_hide_related_videos", FALSE); public static final BooleanSetting HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES = new BooleanSetting("revanced_hide_subscribers_community_guidelines", TRUE); public static final BooleanSetting HIDE_TIMED_REACTIONS = new BooleanSetting("revanced_hide_timed_reactions", TRUE); + public static final BooleanSetting HIDE_VIDEO_TITLE = new BooleanSetting("revanced_hide_video_title", FALSE); public static final BooleanSetting OPEN_VIDEOS_FULLSCREEN_PORTRAIT = new BooleanSetting("revanced_open_videos_fullscreen_portrait", FALSE); public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE); public static final BooleanSetting VIDEO_QUALITY_DIALOG_BUTTON = new BooleanSetting("revanced_video_quality_dialog_button", FALSE); public static final IntegerSetting PLAYER_OVERLAY_OPACITY = new IntegerSetting("revanced_player_overlay_opacity", 100, true); - public static final BooleanSetting PLAYER_POPUP_PANELS = new BooleanSetting("revanced_hide_player_popup_panels", FALSE); // Miniplayer public static final EnumSetting MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.DEFAULT, true); @@ -208,6 +226,7 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_COMMENTS_PREVIEW_COMMENT = new BooleanSetting("revanced_hide_comments_preview_comment", FALSE); public static final BooleanSetting HIDE_COMMENTS_EMOJI_AND_TIMESTAMP_BUTTONS = new BooleanSetting("revanced_hide_comments_emoji_and_timestamp_buttons", FALSE); public static final BooleanSetting HIDE_COMMENTS_SECTION = new BooleanSetting("revanced_hide_comments_section", FALSE); + public static final BooleanSetting HIDE_COMMENTS_SECTION_IN_HOME_FEED = new BooleanSetting("revanced_hide_comments_section_in_home_feed", FALSE, parentNot(HIDE_COMMENTS_SECTION)); public static final BooleanSetting HIDE_COMMENTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_comments_thanks_button", TRUE); // Description @@ -215,6 +234,12 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_ASK_SECTION = new BooleanSetting("revanced_hide_ask_section", FALSE); public static final BooleanSetting HIDE_ATTRIBUTES_SECTION = new BooleanSetting("revanced_hide_attributes_section", FALSE); public static final BooleanSetting HIDE_CHAPTERS_SECTION = new BooleanSetting("revanced_hide_chapters_section", TRUE); + public static final BooleanSetting HIDE_COURSE_PROGRESS_SECTION = new BooleanSetting("revanced_hide_course_progress_section", FALSE); + public static final BooleanSetting HIDE_EXPLORE_SECTION = new BooleanSetting("revanced_hide_explore_section", TRUE); + public static final BooleanSetting HIDE_EXPLORE_COURSE_SECTION = new BooleanSetting("revanced_hide_explore_course_section", FALSE, parentNot(HIDE_EXPLORE_SECTION)); + public static final BooleanSetting HIDE_EXPLORE_PODCAST_SECTION = new BooleanSetting("revanced_hide_explore_podcast_section", FALSE, parentNot(HIDE_EXPLORE_SECTION)); + public static final BooleanSetting HIDE_FEATURED_PLACES_SECTION = new BooleanSetting("revanced_hide_featured_places_section", FALSE); + public static final BooleanSetting HIDE_GAMING_SECTION = new BooleanSetting("revanced_hide_gaming_section", FALSE); public static final BooleanSetting HIDE_HOW_THIS_WAS_MADE_SECTION = new BooleanSetting("revanced_hide_how_this_was_made_section", FALSE); public static final BooleanSetting HIDE_HYPE_POINTS = new BooleanSetting("revanced_hide_hype_points", FALSE); public static final BooleanSetting HIDE_INFO_CARDS_SECTION = new BooleanSetting("revanced_hide_info_cards_section", TRUE); @@ -222,14 +247,15 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_FEATURED_VIDEOS_SECTION = new BooleanSetting("revanced_hide_featured_videos_section", FALSE, parentNot(HIDE_INFO_CARDS_SECTION)); public static final BooleanSetting HIDE_SUBSCRIBE_BUTTON = new BooleanSetting("revanced_hide_subscribe_button", FALSE, parentNot(HIDE_INFO_CARDS_SECTION)); public static final BooleanSetting HIDE_KEY_CONCEPTS_SECTION = new BooleanSetting("revanced_hide_key_concepts_section", FALSE); - public static final BooleanSetting HIDE_PODCAST_SECTION = new BooleanSetting("revanced_hide_podcast_section", TRUE); + public static final BooleanSetting HIDE_MUSIC_SECTION = new BooleanSetting("revanced_hide_music_section", FALSE); public static final BooleanSetting HIDE_TRANSCRIPT_SECTION = new BooleanSetting("revanced_hide_transcript_section", TRUE); + public static final BooleanSetting HIDE_QUIZZES_SECTION = new BooleanSetting("revanced_hide_quizzes_section", FALSE); // Action buttons public static final BooleanSetting DISABLE_LIKE_SUBSCRIBE_GLOW = new BooleanSetting("revanced_disable_like_subscribe_glow", FALSE); public static final BooleanSetting HIDE_ASK_BUTTON = new BooleanSetting("revanced_hide_ask_button", FALSE); - public static final BooleanSetting HIDE_CLIP_BUTTON = new BooleanSetting("revanced_hide_clip_button", TRUE); - public static final BooleanSetting HIDE_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_comments_button", TRUE); + public static final BooleanSetting HIDE_CLIP_BUTTON = new BooleanSetting("revanced_hide_clip_button", FALSE, "revanced_hide_clip_button_user_dialog_message"); + public static final BooleanSetting HIDE_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_comments_button", FALSE); public static final BooleanSetting HIDE_DOWNLOAD_BUTTON = new BooleanSetting("revanced_hide_download_button", FALSE); public static final BooleanSetting HIDE_HYPE_BUTTON = new BooleanSetting("revanced_hide_hype_button", FALSE); public static final BooleanSetting HIDE_LIKE_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_like_dislike_button", FALSE); @@ -256,7 +282,7 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_PLAYER_FLYOUT_STABLE_VOLUME = new BooleanSetting("revanced_hide_player_flyout_stable_volume", FALSE); public static final BooleanSetting HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER = new BooleanSetting("revanced_hide_player_flyout_video_quality_footer", FALSE); public static final BooleanSetting HIDE_PLAYER_FLYOUT_VIDEO_QUALITY = new BooleanSetting("revanced_hide_player_flyout_video_quality", FALSE); - public static final BooleanSetting HIDE_PLAYER_FLYOUT_WATCH_IN_VR = new BooleanSetting("revanced_hide_player_flyout_watch_in_vr", TRUE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_WATCH_IN_VR = new BooleanSetting("revanced_hide_player_flyout_watch_in_vr", FALSE); // General layout public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("revanced_restore_old_settings_menus", FALSE, true); @@ -265,22 +291,22 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true); public static final EnumSetting SPLASH_SCREEN_ANIMATION_STYLE = new EnumSetting<>("revanced_splash_screen_animation_style", SplashScreenAnimationStyle.FPS_60_ONE_SECOND, true); public static final EnumSetting HEADER_LOGO = new EnumSetting<>("revanced_header_logo", HeaderLogo.DEFAULT, true); - public static final BooleanSetting DISABLE_SIGNIN_TO_TV_POPUP = new BooleanSetting("revanced_disable_signin_to_tv_popup", FALSE); + public static final BooleanSetting DISABLE_SIGN_IN_TO_TV_POPUP = new BooleanSetting("revanced_disable_sign_in_to_tv_popup", FALSE); public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE, "revanced_remove_viewer_discretion_dialog_user_dialog_message"); public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", FALSE, true, "revanced_spoof_app_version_user_dialog_message"); - public static final BooleanSetting WIDE_SEARCHBAR = new BooleanSetting("revanced_wide_searchbar", FALSE, true); public static final EnumSetting CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.DEFAULT, true); public static final BooleanSetting CHANGE_START_PAGE_ALWAYS = new BooleanSetting("revanced_change_start_page_always", FALSE, true, new ChangeStartPageTypeAvailability()); - public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "19.01.34", true, parent(SPOOF_APP_VERSION)); + public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "19.35.36", true, parent(SPOOF_APP_VERSION)); // Navigation buttons public static final BooleanSetting HIDE_HOME_BUTTON = new BooleanSetting("revanced_hide_home_button", FALSE, true); public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", TRUE, true); public static final BooleanSetting HIDE_SHORTS_BUTTON = new BooleanSetting("revanced_hide_shorts_button", TRUE, true); public static final BooleanSetting HIDE_SUBSCRIPTIONS_BUTTON = new BooleanSetting("revanced_hide_subscriptions_button", FALSE, true); public static final BooleanSetting HIDE_NAVIGATION_BUTTON_LABELS = new BooleanSetting("revanced_hide_navigation_button_labels", FALSE, true); + public static final BooleanSetting NARROW_NAVIGATION_BUTTONS = new BooleanSetting("revanced_narrow_navigation_buttons", FALSE, true); public static final BooleanSetting HIDE_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_hide_notifications_button", FALSE, true); public static final BooleanSetting SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_switch_create_with_notifications_button", TRUE, true, "revanced_switch_create_with_notifications_button_user_dialog_message"); @@ -290,11 +316,19 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT = new BooleanSetting("revanced_disable_translucent_navigation_bar_light", FALSE, true); public static final BooleanSetting DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK = new BooleanSetting("revanced_disable_translucent_navigation_bar_dark", FALSE, true); + // Toolbar + public static final BooleanSetting HIDE_TOOLBAR_CREATE_BUTTON = new BooleanSetting("revanced_hide_toolbar_create_button", TRUE, true); + public static final BooleanSetting HIDE_TOOLBAR_NOTIFICATION_BUTTON = new BooleanSetting("revanced_hide_toolbar_notification_button", FALSE, true); + public static final BooleanSetting HIDE_TOOLBAR_SEARCH_BUTTON = new BooleanSetting("revanced_hide_toolbar_search_button", FALSE, true); + public static final BooleanSetting WIDE_SEARCHBAR = new BooleanSetting("revanced_wide_searchbar", FALSE, true); + // Shorts public static final BooleanSetting DISABLE_RESUMING_SHORTS_PLAYER = new BooleanSetting("revanced_disable_resuming_shorts_player", FALSE); public static final BooleanSetting DISABLE_SHORTS_BACKGROUND_PLAYBACK = new BooleanSetting("revanced_shorts_disable_background_playback", FALSE); public static final EnumSetting SHORTS_PLAYER_TYPE = new EnumSetting<>("revanced_shorts_player_type", ShortsPlayerType.SHORTS_PLAYER); + public static final BooleanSetting HIDE_SHORTS_AI_BUTTON = new BooleanSetting("revanced_hide_shorts_ai_button", FALSE); public static final BooleanSetting HIDE_SHORTS_AUTO_DUBBED_LABEL = new BooleanSetting("revanced_hide_shorts_auto_dubbed_label", FALSE); + public static final BooleanSetting HIDE_SHORTS_CHANNEL = new BooleanSetting("revanced_hide_shorts_channel", FALSE); public static final BooleanSetting HIDE_SHORTS_CHANNEL_BAR = new BooleanSetting("revanced_hide_shorts_channel_bar", FALSE); public static final BooleanSetting HIDE_SHORTS_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_shorts_comments_button", FALSE); public static final BooleanSetting HIDE_SHORTS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_dislike_button", FALSE); @@ -330,6 +364,7 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_SHORTS_UPCOMING_BUTTON = new BooleanSetting("revanced_hide_shorts_upcoming_button", TRUE); public static final BooleanSetting HIDE_SHORTS_USE_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_use_sound_button", TRUE); public static final BooleanSetting HIDE_SHORTS_USE_TEMPLATE_BUTTON = new BooleanSetting("revanced_hide_shorts_use_template_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_VIDEO_DESCRIPTION = new BooleanSetting("revanced_hide_shorts_video_description", FALSE); public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE); public static final BooleanSetting SHORTS_AUTOPLAY = new BooleanSetting("revanced_shorts_autoplay", FALSE); public static final BooleanSetting SHORTS_AUTOPLAY_BACKGROUND = new BooleanSetting("revanced_shorts_autoplay_background", TRUE); @@ -340,11 +375,8 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE, true); public static final BooleanSetting FULLSCREEN_LARGE_SEEKBAR = new BooleanSetting("revanced_fullscreen_large_seekbar", FALSE); public static final BooleanSetting HIDE_TIMESTAMP = new BooleanSetting("revanced_hide_timestamp", FALSE); - public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails", TRUE); - public static final BooleanSetting SEEKBAR_TAPPING = new BooleanSetting("revanced_seekbar_tapping", FALSE); - public static final BooleanSetting SEEKBAR_THUMBNAILS_HIGH_QUALITY = new BooleanSetting("revanced_seekbar_thumbnails_high_quality", FALSE, true, - "revanced_seekbar_thumbnails_high_quality_dialog_message", new SeekbarThumbnailsHighQualityAvailability()); public static final BooleanSetting SLIDE_TO_SEEK = new BooleanSetting("revanced_slide_to_seek", FALSE, true); + public static final BooleanSetting TAP_TO_SEEK = new BooleanSetting("revanced_tap_to_seek", FALSE); public static final BooleanSetting SEEKBAR_CUSTOM_COLOR = new BooleanSetting("revanced_seekbar_custom_color", FALSE, true); public static final StringSetting SEEKBAR_CUSTOM_COLOR_PRIMARY = new StringSetting("revanced_seekbar_custom_color_primary", "#FF0033", true, parent(SEEKBAR_CUSTOM_COLOR)); public static final StringSetting SEEKBAR_CUSTOM_COLOR_ACCENT = new StringSetting("revanced_seekbar_custom_color_accent", "#FF2791", true, parent(SEEKBAR_CUSTOM_COLOR)); @@ -356,10 +388,6 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting LOOP_VIDEO_BUTTON = new BooleanSetting("revanced_loop_video_button", FALSE); public static final BooleanSetting PAUSE_ON_AUDIO_INTERRUPT = new BooleanSetting("revanced_pause_on_audio_interrupt", FALSE, true); public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE); - public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE); - public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING = new BooleanSetting("revanced_disable_haptic_feedback_precise_seeking", FALSE); - public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO = new BooleanSetting("revanced_disable_haptic_feedback_seek_undo", FALSE); - public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_ZOOM = new BooleanSetting("revanced_disable_haptic_feedback_zoom", FALSE); public static final BooleanSetting EXTERNAL_BROWSER = new BooleanSetting("revanced_external_browser", TRUE, true); public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true, "revanced_spoof_device_dimensions_user_dialog_message"); @@ -536,10 +564,6 @@ public class Settings extends YouTubeAndMusicSettings { SPOOF_VIDEO_STREAMS_CLIENT_TYPE.resetToDefault(); } - // RYD requires manually migrating old settings since the lack of - // a "revanced_" on the old setting causes duplicate key exceptions during export. - Setting.migrateFromOldPreferences(Setting.preferences, RYD_USER_ID, "ryd_user_id"); - // Migrate old saved data. Must be done here before the settings can be used by any other code. applyOldSbOpacityToColor(SB_CATEGORY_SPONSOR_COLOR, DEPRECATED_SB_CATEGORY_SPONSOR_OPACITY); applyOldSbOpacityToColor(SB_CATEGORY_SELF_PROMO_COLOR, DEPRECATED_SB_CATEGORY_SELF_PROMO_OPACITY); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/YouTubeActivityHook.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/YouTubeActivityHook.java index 0d7e1c70de..d95069898d 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/YouTubeActivityHook.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/YouTubeActivityHook.java @@ -31,6 +31,7 @@ public class YouTubeActivityHook extends BaseActivityHook { private static final boolean USE_BOLD_ICONS = VersionCheckPatch.IS_20_31_OR_GREATER && !Settings.SETTINGS_DISABLE_BOLD_ICONS.get() + && !Settings.RESTORE_OLD_SETTINGS_MENUS.get() && (System.currentTimeMillis() - Settings.FIRST_TIME_APP_LAUNCHED.get()) > MINIMUM_TIME_AFTER_FIRST_LAUNCH_BEFORE_ALLOWING_BOLD_ICONS; diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java index fd213ca0e4..218d31eeaa 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java @@ -2,15 +2,15 @@ package app.revanced.extension.youtube.settings.preference; import android.content.Context; import android.util.AttributeSet; -import app.revanced.extension.shared.settings.preference.UrlLinkPreference; +import app.revanced.extension.shared.settings.preference.URLLinkPreference; /** * Allows tapping the DeArrow about preference to open the DeArrow website. */ @SuppressWarnings("unused") -public class AlternativeThumbnailsAboutDeArrowPreference extends UrlLinkPreference { +public class AlternativeThumbnailsAboutDeArrowPreference extends URLLinkPreference { { - externalUrl = "https://dearrow.ajay.app"; + externalURL = "https://dearrow.ajay.app"; } public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPreference.java index cf55dcc5dd..88c5c9bffe 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPreference.java @@ -247,7 +247,7 @@ public class ExternalDownloaderPreference extends CustomDialogListPreference { } else { String savedPackageName = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get(); editText.setText(Downloader.findByPackageName(savedPackageName) == null - ? savedPackageName // If the user is clicking thru options then retain existing other app. + ? savedPackageName // If the user is clicking through options then retain existing other app. : "" ); editText.setEnabled(true); // Enable editing for Custom. diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HTMLPreference.java similarity index 65% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java rename to extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HTMLPreference.java index 1994bf3681..38852c5b76 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HTMLPreference.java @@ -8,27 +8,27 @@ import android.text.Html; import android.util.AttributeSet; /** - * Allows using basic html for the summary text. + * Allows using basic HTML for the summary text. */ @SuppressWarnings({"unused", "deprecation"}) -public class HtmlPreference extends Preference { +public class HTMLPreference extends Preference { { setSummary(Html.fromHtml(getSummary().toString(), FROM_HTML_MODE_COMPACT)); } - public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public HTMLPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } - public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) { + public HTMLPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } - public HtmlPreference(Context context, AttributeSet attrs) { + public HTMLPreference(Context context, AttributeSet attrs) { super(context, attrs); } - public HtmlPreference(Context context) { + public HTMLPreference(Context context) { super(context); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/EngagementPanel.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/EngagementPanel.java new file mode 100644 index 0000000000..773e9c583e --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/EngagementPanel.java @@ -0,0 +1,42 @@ +package app.revanced.extension.youtube.shared; + +import androidx.annotation.Nullable; + +import java.util.concurrent.atomic.AtomicReference; + +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public final class EngagementPanel { + private static final AtomicReference lastEngagementPanelId = new AtomicReference<>(""); + + /** + * Injection point. + */ + public static void close() { + String panelId = getId(); + if (!panelId.isEmpty()) { + lastEngagementPanelId.set(""); + Logger.printDebug(() -> "EngagementPanel closed, Last panel id: " + panelId); + } + } + + /** + * Injection point. + */ + public static void open(@Nullable String panelId) { + if (panelId != null && !panelId.isEmpty()) { + lastEngagementPanelId.set(panelId); + Logger.printDebug(() -> "EngagementPanel open, New panel id: " + panelId); + } + } + + public static boolean isDescription() { + return getId().equals("video-description-ep-identifier"); + } + + private static String getId() { + return lastEngagementPanelId.get(); + } + +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/Event.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/Event.kt similarity index 94% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/Event.kt rename to extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/Event.kt index 802c995b37..d8f7258c05 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/Event.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/Event.kt @@ -1,4 +1,4 @@ -package app.revanced.extension.youtube +package app.revanced.extension.youtube.shared import app.revanced.extension.shared.Logger import java.util.Collections @@ -31,4 +31,4 @@ class Event { observer.invoke(value) } } -} +} \ No newline at end of file diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java index 52d2f1dcf4..8f15b79c51 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java @@ -147,7 +147,7 @@ public final class NavigationBar { } if (Utils.isCurrentlyOnMainThread()) { - // The latch is released from the main thread, and waiting from the main thread will always timeout. + // The latch is released from the main thread, and waiting from the main thread will always time out. // 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. " + @@ -307,7 +307,7 @@ public final class NavigationBar { SHORTS("TAB_SHORTS", "TAB_SHORTS_CAIRO"), /** * Create new video tab. - * This tab will never be in a selected state, 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", "CREATION_TAB_LARGE_CAIRO"), /** diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibility.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibility.kt index 5585bc157a..161dc6b73e 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibility.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibility.kt @@ -1,7 +1,7 @@ package app.revanced.extension.youtube.shared import app.revanced.extension.shared.Logger -import app.revanced.extension.youtube.Event +import app.revanced.extension.youtube.shared.Event /** * PlayerControls visibility state. diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibilityObserver.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibilityObserver.kt index f5c51c5f39..ecf71cb05c 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibilityObserver.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibilityObserver.kt @@ -17,7 +17,7 @@ class PlayerControlsVisibilityObserverImpl( ) : PlayerControlsVisibilityObserver { /** - * id of the direct parent of controls_layout, R.id.youtube_controls_overlay + * ID of the direct parent of controls_layout, R.id.youtube_controls_overlay */ private val controlsLayoutParentId = Utils.getResourceIdentifier(activity, ResourceType.ID, "youtube_controls_overlay") diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerOverlays.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerOverlays.kt index ec82053fa1..6706c5c7be 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerOverlays.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerOverlays.kt @@ -2,7 +2,7 @@ package app.revanced.extension.youtube.shared import android.view.View import android.view.ViewGroup -import app.revanced.extension.youtube.Event +import app.revanced.extension.youtube.shared.Event import app.revanced.extension.youtube.swipecontrols.misc.Rectangle /** diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt index 0b97b05c6e..555dbfaeaa 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt @@ -1,7 +1,7 @@ package app.revanced.extension.youtube.shared import app.revanced.extension.shared.Logger -import app.revanced.extension.youtube.Event +import app.revanced.extension.youtube.shared.Event /** * Regular player type. @@ -107,7 +107,7 @@ enum class PlayerType { * Instead of this method, consider using {@link ShortsPlayerState} * which may work better for some situations. * - * @return If nothing, a Short, or a regular video is sliding off screen to a dismissed or hidden state. + * @return If nothing, a Short, or a regular video is sliding off-screen to a dismissed or hidden state. * @see ShortsPlayerState */ fun isNoneHiddenOrSlidingMinimized(): Boolean { @@ -119,7 +119,7 @@ enum class PlayerType { * [NONE], [HIDDEN], [WATCH_WHILE_MINIMIZED], [WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED]. * * Useful to check if a Short is being played, - * although will return false positive if a regular video is + * although it will return false positive if a regular video is * opened and minimized (and a Short is not playing or being opened). * * Typically used to detect if a Short is playing when the player cannot be in a minimized state, @@ -128,7 +128,7 @@ enum class PlayerType { * Instead of this method, consider using {@link ShortsPlayerState} * which may work better for some situations. * - * @return If nothing, a Short, a regular video is sliding off screen to a dismissed or hidden state, + * @return If nothing, a Short, a regular video is sliding off-screen to a dismissed or hidden state, * a regular video is minimized (and a new video is not being opened). * @see ShortsPlayerState */ diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt index 48927d88aa..d960225c4a 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt @@ -1,7 +1,7 @@ package app.revanced.extension.youtube.shared import app.revanced.extension.shared.Logger -import app.revanced.extension.youtube.Event +import app.revanced.extension.youtube.shared.Event /** * Shorts player state. diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java index f4d5d5f62a..a296f9e000 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java @@ -35,7 +35,9 @@ import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.ui.Dim; import app.revanced.extension.youtube.patches.VideoInformation; import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerControlsVisibility; import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.shared.ShortsPlayerState; import app.revanced.extension.youtube.shared.VideoState; import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour; import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory; @@ -128,7 +130,7 @@ public class SegmentPlaybackController { /** * Current segments that have been auto skipped. - * If field is non null then the range will always contain the current video time. + * If field is non-null then the range will always contain the current video time. * Range is used to prevent auto-skipping after undo. * Android Range object has inclusive end time, unlike {@link SponsorSegment}. */ @@ -136,7 +138,7 @@ public class SegmentPlaybackController { private static Range undoAutoSkipRange; /** * Range to undo if the toast is tapped. - * Is always null or identical to the last non null value of {@link #undoAutoSkipRange}. + * Is always null or identical to the last non-null value of {@link #undoAutoSkipRange}. */ @Nullable private static Range undoAutoSkipRangeToast; @@ -311,7 +313,10 @@ public class SegmentPlaybackController { if (videoId == null || !Settings.SB_ENABLED.get()) { return; } - if (PlayerType.getCurrent().isNoneOrHidden()) { + // Cannot use PlayerType to check because on some newer targets + // the player type can be updated out of order and incorrectly + // is "none" when the regular player is open + if (ShortsPlayerState.isOpen()) { Logger.printDebug(() -> "Ignoring Short"); return; } @@ -382,8 +387,8 @@ public class SegmentPlaybackController { Logger.printDebug(() -> { String visibilityMessage = switch (visibility) { - case View.VISIBLE -> "VISIBLE"; - case View.GONE -> "GONE"; + case View.VISIBLE -> "VISIBLE"; + case View.GONE -> "GONE"; case View.INVISIBLE -> "INVISIBLE"; default -> "UNKNOWN"; }; @@ -394,12 +399,18 @@ public class SegmentPlaybackController { /** * When a video ad is playing in a regular video player, segments or the Skip button should be hidden. + * * @return Whether the Ad Progress TextView is visible in the regular video player. */ public static boolean isAdProgressTextVisible() { return adProgressTextVisibility == View.VISIBLE; } + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private static boolean autoSkipIsEnabledAndPlayerOverlayIsActive() { + return Settings.SB_AUTO_HIDE_SKIP_BUTTON.get() && + PlayerControlsVisibility.getCurrent() != PlayerControlsVisibility.PLAYER_CONTROLS_VISIBILITY_HIDDEN; + } /** * Injection point. @@ -422,7 +433,7 @@ public class SegmentPlaybackController { // Amount of time to look ahead for the next segment, // and the threshold to determine if a scheduled show/hide is at the correct video time when it's run. // - // This value must be greater than largest time between calls to this method (1000ms), + // This value must be greater than the largest time between calls to this method (1000ms), // and must be adjusted for the video speed. // // To debug the stale skip logic, set this to a very large value (5000 or more) @@ -490,7 +501,7 @@ public class SegmentPlaybackController { // Only schedule, if the segment start time is not near the end time of the current segment. // This check is needed to prevent scheduled hide and show from clashing with each other. - // Instead the upcoming segment will be handled when the current segment scheduled hide calls back into this method. + // Instead, the upcoming segment will be handled when the current segment scheduled hide calls back into this method. final long minTimeBetweenStartEndOfSegments = 1000; if (foundSegmentCurrentlyPlaying == null || !foundSegmentCurrentlyPlaying.endIsNear(segment.start, minTimeBetweenStartEndOfSegments)) { @@ -519,7 +530,11 @@ public class SegmentPlaybackController { Logger.printDebug(() -> "Auto hiding skip button for segment: " + segmentCurrentlyPlaying); skipSegmentButtonEndTime = 0; hiddenSkipSegmentsForCurrentVideoTime.add(foundSegmentCurrentlyPlaying); - SponsorBlockViewController.hideSkipSegmentButton(); + // Do not hide if auto-hide is enabled and player controls are visible. + // Skip button will hide when the overlay controls are dismissed. + if (!autoSkipIsEnabledAndPlayerOverlayIsActive()) { + SponsorBlockViewController.hideSkipSegmentButton(); + } } // Schedule a hide, but only if the segment end is near. @@ -602,12 +617,12 @@ public class SegmentPlaybackController { } }, delayUntilSkip); } - } - // Clear undo range if video time is outside the segment. Must check last. - if (undoAutoSkipRange != null && !undoAutoSkipRange.contains(millis)) { - Logger.printDebug(() -> "Clearing undo range as current time is now outside range: " + undoAutoSkipRange); - undoAutoSkipRange = null; + // Clear undo range if video time is outside the segment. Must check last. + if (undoAutoSkipRange != null && !undoAutoSkipRange.contains(millis)) { + Logger.printDebug(() -> "Clearing undo range as current time is now outside range: " + undoAutoSkipRange); + undoAutoSkipRange = null; + } } } catch (Exception e) { Logger.printException(() -> "setVideoTime failure", e); @@ -615,7 +630,7 @@ public class SegmentPlaybackController { } /** - * Removes all previously hidden segments that are not longer contained in the given video time. + * Removes all previously hidden segments that are no longer contained in the given video time. */ private static void updateHiddenSegments(long currentVideoTime) { hiddenSkipSegmentsForCurrentVideoTime.removeIf((hiddenSegment) -> { @@ -629,7 +644,9 @@ public class SegmentPlaybackController { private static void setSegmentCurrentlyPlaying(@Nullable SponsorSegment segment) { if (segment == null) { - if (segmentCurrentlyPlaying != null) Logger.printDebug(() -> "Hiding segment: " + segmentCurrentlyPlaying); + if (segmentCurrentlyPlaying != null) { + Logger.printDebug(() -> "Hiding segment: " + segmentCurrentlyPlaying); + } segmentCurrentlyPlaying = null; skipSegmentButtonEndTime = 0; SponsorBlockViewController.hideSkipSegmentButton(); @@ -643,7 +660,12 @@ public class SegmentPlaybackController { if (hiddenSkipSegmentsForCurrentVideoTime.contains(segment)) { // Playback exited a nested segment and the outer segment skip button was previously hidden. Logger.printDebug(() -> "Ignoring previously auto-hidden segment: " + segment); - SponsorBlockViewController.hideSkipSegmentButton(); + // Must set view segment so overlay controls shows the correct skip button. + SponsorBlockViewController.setSkipSegment(segment); + // Do not hide skip button if + if (!autoSkipIsEnabledAndPlayerOverlayIsActive()) { + SponsorBlockViewController.hideSkipSegmentButton(); + } return; } skipSegmentButtonEndTime = System.currentTimeMillis() + getSkipButtonDuration(); @@ -746,6 +768,15 @@ public class SegmentPlaybackController { || !undoAutoSkipRange.contains(currentVideoTime)); } + public static boolean currentlyInsideSkippableSegment() { + return segmentCurrentlyPlaying != null || !hiddenSkipSegmentsForCurrentVideoTime.isEmpty(); + } + + public static boolean shouldNotFadeOutPlayerOverlaySkipButton() { + // Only fade out overlay if auto hide is enabled and a scheduled button auto hide is not scheduled. + return skipSegmentButtonEndTime != 0 || !Settings.SB_AUTO_HIDE_SKIP_BUTTON.get(); + } + private static void showSkippedSegmentToast(SponsorSegment segment) { Utils.verifyOnMainThread(); toastSegmentSkipped = segment; @@ -757,8 +788,8 @@ public class SegmentPlaybackController { final long delayToToastMilliseconds = 250; Utils.runOnMainThreadDelayed(() -> { try { - // Do not show a toast if the user is scrubbing thru a paused video. - // Cannot do this video state check in setTime or before calling this this method, + // Do not show a toast if the user is scrubbing through a paused video. + // Cannot do this video state check in setTime or before calling this method, // as the video state may not be up to date. So instead, only ignore the toast // just before it's about to show since the video state is up to date. if (VideoState.getCurrent() == VideoState.PAUSED) { @@ -792,6 +823,13 @@ public class SegmentPlaybackController { Objects.requireNonNull(messageToToast); Utils.verifyOnMainThread(); + if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL) { + // Cannot easily show a toast since there is no layout view context. + // Probably better to not show a toast here anyway. + Logger.printDebug(() -> "Not showing undo toast for feed playback"); + return; + } + Context currentContext = SponsorBlockViewController.getOverLaysViewGroupContext(); if (currentContext == null) { Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast); @@ -836,13 +874,17 @@ public class SegmentPlaybackController { fadeIn.setDuration(fadeDurationFast); fadeOut.setDuration(fadeDurationFast); fadeOut.setAnimationListener(new Animation.AnimationListener() { - public void onAnimationStart(Animation animation) { } + public void onAnimationStart(Animation animation) { + } + public void onAnimationEnd(Animation animation) { if (dialog.isShowing()) { dialog.dismiss(); } } - public void onAnimationRepeat(Animation animation) { } + + public void onAnimationRepeat(Animation animation) { + } }); mainLayout.setOnClickListener(v -> { @@ -891,7 +933,8 @@ public class SegmentPlaybackController { */ public static void onSkipSegmentClicked(SponsorSegment segment) { try { - if (segment != highlightSegment && segment != segmentCurrentlyPlaying) { + if (segment != highlightSegment && segment != segmentCurrentlyPlaying + && !hiddenSkipSegmentsForCurrentVideoTime.contains(segment)) { Logger.printException(() -> "error: segment not available to skip"); // Should never happen. SponsorBlockViewController.hideSkipSegmentButton(); SponsorBlockViewController.hideSkipHighlightButton(); @@ -994,7 +1037,7 @@ public class SegmentPlaybackController { @SuppressWarnings("unused") public static void drawSegmentTimeBars(final Canvas canvas, final float posY) { try { - if (segments == null) return; + if (segments == null || isAdProgressTextVisible()) return; final long videoLength = VideoInformation.getVideoLength(); if (videoLength <= 0) return; diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java index a8c76628c5..a86a17cc8a 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java @@ -29,7 +29,7 @@ import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGrou @SuppressWarnings("NewApi") public class SponsorBlockSettings { /** - * Minimum length a SB user id must be, as set by SB API. + * Minimum length an SB user ID must be, as set by SB API. */ private static final int SB_PRIVATE_USER_ID_MINIMUM_LENGTH = 30; @@ -80,7 +80,7 @@ public class SponsorBlockSettings { Utils.showToastLong(categoryKey + " unknown behavior key: " + categoryKey); } else if (category == SegmentCategory.HIGHLIGHT && behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE) { Utils.showToastLong("Skip-once behavior not allowed for " + category.keyValue); - category.setBehaviour(CategoryBehaviour.SKIP_AUTOMATICALLY); // Use closest match. + category.setBehaviour(CategoryBehaviour.SKIP_AUTOMATICALLY); // Use the closest match. } else { category.setBehaviour(behaviour); } @@ -90,7 +90,7 @@ public class SponsorBlockSettings { if (settingsJson.has("userID")) { // User id does not exist if user never voted or created any segments. String userID = settingsJson.getString("userID"); - if (isValidSBUserId(userID)) { + if (isValidSBUserID(userID)) { Settings.SB_PRIVATE_USER_ID.save(userID); } } @@ -159,7 +159,7 @@ public class SponsorBlockSettings { categorySelectionsArray.put(behaviorObject); } } - if (SponsorBlockSettings.userHasSBPrivateId()) { + if (SponsorBlockSettings.userHasSBPrivateID()) { json.put("userID", Settings.SB_PRIVATE_USER_ID.get()); } json.put("isVip", Settings.SB_USER_IS_VIP.get()); @@ -183,14 +183,14 @@ public class SponsorBlockSettings { } /** - * Export the categories using flatten json (no embedded dictionaries or arrays). + * Export the categories using flatten JSON (no embedded dictionaries or arrays). */ private static void showExportWarningIfNeeded(@Nullable Context dialogContext) { Utils.verifyOnMainThread(); initialize(); - // If user has a SponsorBlock user id then show a warning. - if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateId() + // If user has a SponsorBlock user ID then show a warning. + if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateID() && !Settings.SB_HIDE_EXPORT_WARNING.get()) { // Create the custom dialog. Pair dialogPair = CustomDialog.create( @@ -214,12 +214,12 @@ public class SponsorBlockSettings { } } - public static boolean isValidSBUserId(@NonNull String userId) { - return !userId.isEmpty() && userId.length() >= SB_PRIVATE_USER_ID_MINIMUM_LENGTH; + public static boolean isValidSBUserID(@NonNull String userID) { + return !userID.isEmpty() && userID.length() >= SB_PRIVATE_USER_ID_MINIMUM_LENGTH; } /** - * A non comprehensive check if a SB api server address is valid. + * A non-comprehensive check if an SB API server address is valid. */ public static boolean isValidSBServerAddress(@NonNull String serverAddress) { if (!Patterns.WEB_URL.matcher(serverAddress).matches()) { @@ -237,12 +237,12 @@ public class SponsorBlockSettings { /** * @return if the user has ever voted, created a segment, or imported existing SB settings. */ - public static boolean userHasSBPrivateId() { + public static boolean userHasSBPrivateID() { return !Settings.SB_PRIVATE_USER_ID.get().isEmpty(); } /** - * Use this only if a user id is required (creating segments, voting). + * Use this only if a user ID is required (creating segments, voting). */ @NonNull public static String getSBPrivateUserID() { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java index 76c0403d5b..943cfc0373 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java @@ -416,10 +416,10 @@ public class SponsorBlockUtils { if (!matcher.matches()) { return -1; } - String hoursStr = matcher.group(2); // Hours is optional. + String hoursStr = matcher.group(2); // Hours are optional. String minutesStr = matcher.group(3); String secondsStr = matcher.group(4); - String millisecondsStr = matcher.group(6); // Milliseconds is optional. + String millisecondsStr = matcher.group(6); // Milliseconds are optional. try { final int hours = (hoursStr != null) ? Integer.parseInt(hoursStr) : 0; @@ -447,7 +447,7 @@ public class SponsorBlockUtils { // Use same time formatting as shown in the video player. final long videoLength = VideoInformation.getVideoLength(); - // Cannot use DateFormatter, as videos over 24 hours will rollover and not display correctly. + // Cannot use DateFormatter, as videos over 24 hours will roll over and not display correctly. final long hours = TimeUnit.MILLISECONDS.toHours(segmentTime); final long minutes = TimeUnit.MILLISECONDS.toMinutes(segmentTime) % 60; final long seconds = TimeUnit.MILLISECONDS.toSeconds(segmentTime) % 60; diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/CategoryBehaviour.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/CategoryBehaviour.java index 7cd4a44c55..cc123d1e7d 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/CategoryBehaviour.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/CategoryBehaviour.java @@ -16,7 +16,7 @@ public enum CategoryBehaviour { SKIP_AUTOMATICALLY_ONCE("skip-once", 3, true, sf("revanced_sb_skip_automatically_once")), MANUAL_SKIP("manual-skip", 1, false, sf("revanced_sb_skip_showbutton")), SHOW_IN_SEEKBAR("seekbar-only", 0, false, sf("revanced_sb_skip_seekbaronly")), - // ignored categories are not exported to json, and ignore is the default behavior when importing + // ignored categories are not exported to JSON, and ignore is the default behavior when importing IGNORE("ignore", -1, false, sf("revanced_sb_skip_ignore")); /** diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java index ff0eaffcf1..d26f48bfe9 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java @@ -16,8 +16,8 @@ public class UserStats { */ private static final long STATS_EXPIRATION_MILLISECONDS = 60 * 60 * 1000; // 60 minutes. - private final String privateUserId; - public final String publicUserId; + private final String privateUserID; + public final String publicUserID; public final String userName; /** * "User reputation". Unclear how SB determines this value. @@ -37,9 +37,9 @@ public class UserStats { */ public final long fetchTime; - public UserStats(String privateSbId, @NonNull JSONObject json) throws JSONException { - privateUserId = privateSbId; - publicUserId = json.getString("userID"); + public UserStats(String privateSBID, @NonNull JSONObject json) throws JSONException { + privateUserID = privateSBID; + publicUserID = json.getString("userID"); userName = json.getString("userName"); reputation = (float)json.getDouble("reputation"); segmentCount = json.getInt("segmentCount"); @@ -55,17 +55,17 @@ public class UserStats { return true; } - // User changed their SB private user id. - return !SponsorBlockSettings.userHasSBPrivateId() - || !SponsorBlockSettings.getSBPrivateUserID().equals(privateUserId); + // User changed their SB private user ID. + return !SponsorBlockSettings.userHasSBPrivateID() + || !SponsorBlockSettings.getSBPrivateUserID().equals(privateUserID); } @NonNull @Override public String toString() { - // Do not include private user id in toString(). + // Do not include private user ID in toString(). return "UserStats{" - + "publicUserId='" + publicUserId + '\'' + + "publicUserID='" + publicUserID + '\'' + ", userName='" + userName + '\'' + ", reputation=" + reputation + ", segmentCount=" + segmentCount diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java index dc86e1d0d3..bdfb3ae933 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java @@ -153,7 +153,7 @@ public class SBRequester { segments.add(new SponsorSegment(SegmentCategory.SELF_PROMO, "debug", 8000, 15000, false)); // Test multiple autoskip dialogs rapidly showing. - // Only one toast should be shown at anytime. + // Only one toast should be shown at any time. segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 16000, 17000, false)); segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 18000, 19000, false)); segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 20000, 21000, false)); @@ -168,13 +168,13 @@ public class SBRequester { Utils.verifyOffMainThread(); try { - String privateUserId = SponsorBlockSettings.getSBPrivateUserID(); + String privateUserID = SponsorBlockSettings.getSBPrivateUserID(); String start = String.format(Locale.US, TIME_TEMPLATE, startTime / 1000f); String end = String.format(Locale.US, TIME_TEMPLATE, endTime / 1000f); String duration = String.format(Locale.US, TIME_TEMPLATE, videoLength / 1000f); HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, - privateUserId, videoId, category, start, end, duration); + privateUserID, videoId, category, start, end, duration); final int responseCode = connection.getResponseCode(); if (responseCode == HTTP_STATUS_CODE_SUCCESS) { @@ -317,8 +317,8 @@ public class SBRequester { } public static void runVipCheckInBackgroundIfNeeded() { - if (!SponsorBlockSettings.userHasSBPrivateId()) { - return; // User cannot be a VIP. User has never voted, created any segments, or has imported a SB user id. + if (!SponsorBlockSettings.userHasSBPrivateID()) { + return; // User cannot be a VIP. User has never voted, created any segments, or has imported an SB user ID. } long now = System.currentTimeMillis(); if (now < (Settings.SB_LAST_VIP_CHECK.get() + TimeUnit.DAYS.toMillis(3))) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SkipSponsorButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SkipSponsorButton.java index 9573102f32..22d5b0db55 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SkipSponsorButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SkipSponsorButton.java @@ -78,15 +78,17 @@ public class SkipSponsorButton extends FrameLayout { updateLayout(); - skipSponsorBtnContainer.setOnClickListener(v -> { - // The view controller handles hiding this button, but hide it here as well just in case something goofs. - setVisibility(View.GONE); - SegmentPlaybackController.onSkipSegmentClicked(segment); - }); + skipSponsorBtnContainer.setOnClickListener(v -> skipButtonClicked()); + } + + public void skipButtonClicked() { + // The view controller handles hiding this button, but hide it here as well just in case something goofs. + setVisibility(View.GONE); + SegmentPlaybackController.onSkipSegmentClicked(segment); } @Override // android.view.ViewGroup - protected final void dispatchDraw(Canvas canvas) { + protected final void dispatchDraw(@NonNull Canvas canvas) { final int left = skipSponsorBtnContainer.getLeft(); final int top = skipSponsorBtnContainer.getTop(); final int right = left + skipSponsorBtnContainer.getWidth(); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java index d8c8a13fef..5857a1c1ed 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java @@ -3,12 +3,12 @@ package app.revanced.extension.youtube.sponsorblock.ui; import android.content.Context; import android.util.AttributeSet; -import app.revanced.extension.shared.settings.preference.UrlLinkPreference; +import app.revanced.extension.shared.settings.preference.URLLinkPreference; @SuppressWarnings("unused") -public class SponsorBlockAboutPreference extends UrlLinkPreference { +public class SponsorBlockAboutPreference extends URLLinkPreference { { - externalUrl = "https://sponsor.ajay.app"; + externalURL = "https://sponsor.ajay.app"; } public SponsorBlockAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java index cb05661e7f..e81e2e08aa 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java @@ -96,8 +96,8 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { // Preferences are synced by AbstractPreferenceFragment since keys are set // and a Setting exist with the same key. - // If the user has a private user id, then include a subtext that mentions not to share it. - String importExportSummary = SponsorBlockSettings.userHasSBPrivateId() + // If the user has a private user ID, then include a subtext that mentions not to share it. + String importExportSummary = SponsorBlockSettings.userHasSBPrivateID() ? str("revanced_sb_settings_ie_sum_warning") : str("revanced_sb_settings_ie_sum"); importExport.setSummary(importExportSummary); @@ -393,7 +393,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { }); generalCategory.addPreference(minSegmentDuration); - EditTextPreference privateUserId = new EditTextPreference(context) { + EditTextPreference privateUserID = new EditTextPreference(context) { @Override protected void showDialog(Bundle state) { try { @@ -442,11 +442,11 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { } } }; - initializePreference(privateUserId, Settings.SB_PRIVATE_USER_ID, + initializePreference(privateUserID, Settings.SB_PRIVATE_USER_ID, "revanced_sb_general_uuid"); - privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> { + privateUserID.setOnPreferenceChangeListener((preference1, newValue) -> { String newUUID = newValue.toString(); - if (!SponsorBlockSettings.isValidSBUserId(newUUID)) { + if (!SponsorBlockSettings.isValidSBUserID(newUUID)) { Utils.showToastLong(str("revanced_sb_general_uuid_invalid")); updateUIDelayed(); return false; @@ -456,7 +456,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { updateUIDelayed(); return true; }); - generalCategory.addPreference(privateUserId); + generalCategory.addPreference(privateUserID); Preference apiUrl = new Preference(context); initializePreference(apiUrl, Settings.SB_API_URL, diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java index 6b6d7c4deb..23101b01c9 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java @@ -30,7 +30,7 @@ import app.revanced.extension.youtube.sponsorblock.requests.SBRequester; * User skip stats. * None of the preferences here show up in search results because * a category cannot be added to another category for the search results. - * Additionally the stats must load remotely on a background thread which means the + * Additionally, the stats must load remotely on a background thread which means the * preferences are not available to collect for search when the settings first load. */ @SuppressWarnings({"unused", "deprecation"}) @@ -58,7 +58,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory { setEnabled(enabled); removeAll(); - if (!SponsorBlockSettings.userHasSBPrivateId()) { + if (!SponsorBlockSettings.userHasSBPrivateID()) { // User has never voted or created any segments. Only local stats exist. addLocalUserStats(); return; @@ -134,7 +134,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory { } else { preference.setOnPreferenceClickListener(preference1 -> { Intent i = new Intent(Intent.ACTION_VIEW); - i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserId)); + i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserID)); preference1.getContext().startActivity(i); return true; }); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java index 6e2c9e241d..ca303753aa 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java @@ -9,7 +9,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.RelativeLayout; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.ref.WeakReference; @@ -18,8 +17,11 @@ import java.util.Objects; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.ResourceType; import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController; import app.revanced.extension.youtube.sponsorblock.objects.SponsorSegment; +import app.revanced.extension.youtube.videoplayer.PlayerControlButton; import kotlin.Unit; public class SponsorBlockViewController { @@ -28,8 +30,10 @@ public class SponsorBlockViewController { private static WeakReference inlineSponsorOverlayRef = new WeakReference<>(null); private static WeakReference youtubeOverlaysLayoutRef = new WeakReference<>(null); private static WeakReference skipHighlightButtonRef = new WeakReference<>(null); - private static WeakReference skipSponsorButtonRef = new WeakReference<>(null); private static WeakReference newSegmentLayoutRef = new WeakReference<>(null); + private static WeakReference skipSponsorButtonRef = new WeakReference<>(null); + @Nullable + private static PlayerControlButton skipSponsorPlayerButton; private static boolean canShowViewElements; private static boolean newSegmentLayoutVisible; @Nullable @@ -92,6 +96,21 @@ public class SponsorBlockViewController { skipSponsorButtonRef = new WeakReference<>(Objects.requireNonNull(layout.findViewById( getResourceIdentifier(ResourceType.ID, "revanced_sb_skip_sponsor_button")))); + // Handles fading in/out with the player overlay. + skipSponsorPlayerButton = new PlayerControlButton( + layout, + "revanced_sb_skip_sponsor_button", + null, + () -> canShowViewElements && SegmentPlaybackController.currentlyInsideSkippableSegment(), + view -> { + SkipSponsorButton button = skipSponsorButtonRef.get(); + if (button != null) { + button.skipButtonClicked(); + } + }, + null + ); + NewSegmentLayout newSegmentLayout = Objects.requireNonNull(layout.findViewById( getResourceIdentifier(ResourceType.ID, "revanced_sb_new_segment_view"))); newSegmentLayoutRef = new WeakReference<>(newSegmentLayout); @@ -128,36 +147,62 @@ public class SponsorBlockViewController { } } - public static void showSkipHighlightButton(@NonNull SponsorSegment segment) { + public static void showSkipHighlightButton(SponsorSegment segment) { skipHighlight = Objects.requireNonNull(segment); NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); - // don't show highlight button if create new segment is visible + // Don't show highlight button if create new segment is visible. final boolean buttonVisibility = newSegmentLayout == null || newSegmentLayout.getVisibility() != View.VISIBLE; - updateSkipButton(skipHighlightButtonRef.get(), segment, buttonVisibility); + updateSkipButton(skipHighlightButtonRef.get(), false, segment, buttonVisibility); } - public static void showSkipSegmentButton(@NonNull SponsorSegment segment) { + + /** + * Same as {@link #setSkipSegment(SponsorSegment)} and it forcefully shows the skip button. + */ + public static void showSkipSegmentButton(SponsorSegment segment) { skipSegment = Objects.requireNonNull(segment); - updateSkipButton(skipSponsorButtonRef.get(), segment, true); + updateSkipButton(skipSponsorButtonRef.get(), true, segment, true); + } + + /** + * Sets the skip segment and updates the button, but does not forcefully show the skip button. + */ + public static void setSkipSegment(SponsorSegment segment) { + skipSegment = Objects.requireNonNull(segment); + SkipSponsorButton button = skipSponsorButtonRef.get(); + if (button != null) { + button.updateSkipButtonText(segment); + } } public static void hideSkipHighlightButton() { skipHighlight = null; - updateSkipButton(skipHighlightButtonRef.get(), null, false); + updateSkipButton(skipHighlightButtonRef.get(), false, null, false); } + public static void hideSkipSegmentButton() { - skipSegment = null; - updateSkipButton(skipSponsorButtonRef.get(), null, false); + if (!Settings.SB_AUTO_HIDE_SKIP_BUTTON.get()) { + // Must retain segment for auto hide because skip button is shown when player overlay is active. + skipSegment = null; + } + updateSkipButton(skipSponsorButtonRef.get(), true, null, false); } private static void updateSkipButton(@Nullable SkipSponsorButton button, - @Nullable SponsorSegment segment, boolean visible) { + boolean isSkipButton, + @Nullable SponsorSegment segment, + boolean visible) { if (button == null) { return; } if (segment != null) { button.updateSkipButtonText(segment); } - setViewVisibility(button, visible); + + if (isSkipButton && Settings.SB_AUTO_HIDE_SKIP_BUTTON.get()) { + setVisibilityImmediate(visible); + } else { + setGenericViewVisibility(button, visible); + } } public static void toggleNewSegmentLayoutVisibility() { @@ -168,20 +213,21 @@ public class SponsorBlockViewController { } newSegmentLayoutVisible = (newSegmentLayout.getVisibility() != View.VISIBLE); if (skipHighlight != null) { - setViewVisibility(skipHighlightButtonRef.get(), !newSegmentLayoutVisible); + setGenericViewVisibility(skipHighlightButtonRef.get(), !newSegmentLayoutVisible); } - setViewVisibility(newSegmentLayout, newSegmentLayoutVisible); + setGenericViewVisibility(newSegmentLayout, newSegmentLayoutVisible); } public static void hideNewSegmentLayout() { newSegmentLayoutVisible = false; - setViewVisibility(newSegmentLayoutRef.get(), false); + setGenericViewVisibility(newSegmentLayoutRef.get(), false); } - private static void setViewVisibility(@Nullable View view, boolean visible) { + private static void setGenericViewVisibility(@Nullable View view, boolean visible) { if (view == null) { return; } + visible &= canShowViewElements; final int desiredVisibility = visible ? View.VISIBLE : View.GONE; if (view.getVisibility() != desiredVisibility) { @@ -189,22 +235,22 @@ public class SponsorBlockViewController { } } - private static void playerTypeChanged(@NonNull PlayerType playerType) { + private static void playerTypeChanged(PlayerType playerType) { try { final boolean isWatchFullScreen = playerType == PlayerType.WATCH_WHILE_FULLSCREEN; canShowViewElements = (isWatchFullScreen || playerType == PlayerType.WATCH_WHILE_MAXIMIZED); NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); setNewSegmentLayoutMargins(newSegmentLayout, isWatchFullScreen); - setViewVisibility(newSegmentLayoutRef.get(), newSegmentLayoutVisible); + setGenericViewVisibility(newSegmentLayoutRef.get(), newSegmentLayoutVisible); SkipSponsorButton skipHighlightButton = skipHighlightButtonRef.get(); setSkipButtonMargins(skipHighlightButton, isWatchFullScreen); - setViewVisibility(skipHighlightButton, skipHighlight != null); + setGenericViewVisibility(skipHighlightButton, skipHighlight != null); SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get(); setSkipButtonMargins(skipSponsorButton, isWatchFullScreen); - setViewVisibility(skipSponsorButton, skipSegment != null); + setGenericViewVisibility(skipSponsorButton, skipSegment != null); } catch (Exception ex) { Logger.printException(() -> "playerTypeChanged failure", ex); } @@ -220,7 +266,7 @@ public class SponsorBlockViewController { setLayoutMargins(button, fullScreen, button.defaultBottomMargin, button.ctaBottomMargin); } } - private static void setLayoutMargins(@NonNull View view, boolean fullScreen, + private static void setLayoutMargins(View view, boolean fullScreen, int defaultBottomMargin, int ctaBottomMargin) { RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) view.getLayoutParams(); if (params == null) { @@ -230,4 +276,46 @@ public class SponsorBlockViewController { params.bottomMargin = fullScreen ? ctaBottomMargin : defaultBottomMargin; view.setLayoutParams(params); } + + // Additional logic to fade in/out the skip segment button when autohide skip button is active. + + /**` + * injection point. + */ + public static void setVisibilityNegatedImmediate() { + if (SegmentPlaybackController.shouldNotFadeOutPlayerOverlaySkipButton()) { + return; + } + + if (skipSponsorPlayerButton != null) { + skipSponsorPlayerButton.setVisibilityNegatedImmediate(); + } + } + + /** + * injection point. + * Only for skip button when auto hide is enbled. + */ + public static void setVisibilityImmediate(boolean visible) { + if (!visible && SegmentPlaybackController.shouldNotFadeOutPlayerOverlaySkipButton()) { + return; + } + + if (skipSponsorPlayerButton != null) { + skipSponsorPlayerButton.setVisibilityImmediate(visible); + } + } + + /** + * injection point. + */ + public static void setVisibility(boolean visible, boolean animated) { + if (!visible && SegmentPlaybackController.shouldNotFadeOutPlayerOverlaySkipButton()) { + return; + } + + if (skipSponsorPlayerButton != null) { + skipSponsorPlayerButton.setVisibility(visible, animated); + } + } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/ScreenBrightnessController.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/ScreenBrightnessController.kt index f291bcb415..2e2b6d70b7 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/ScreenBrightnessController.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/ScreenBrightnessController.kt @@ -23,7 +23,7 @@ class ScreenBrightnessController( } /** - * is the screen brightness set to device- default? + * is the screen brightness set to device-default? */ val isDefaultBrightness get() = (rawScreenBrightness == WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/VolumeKeysController.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/VolumeKeysController.kt index 5d206dfa9b..b40b9f0287 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/VolumeKeysController.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/VolumeKeysController.kt @@ -4,7 +4,7 @@ import android.view.KeyEvent import app.revanced.extension.youtube.swipecontrols.SwipeControlsHostActivity /** - * controller for custom volume button behaviour + * controller for custom volume button behavior * * @param controller main controller instance */ diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.kt index 5e863a3c58..d30ca3e458 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.kt @@ -20,7 +20,7 @@ interface SwipeControlsOverlay { fun onBrightnessChanged(brightness: Double) /** - * called when a new swipe- session has started + * called when a new swipe-session has started */ fun onEnterSwipeSession() } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoURLButton.java similarity index 86% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlButton.java rename to extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoURLButton.java index b0dc22e762..21c67ba990 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoURLButton.java @@ -5,11 +5,11 @@ import android.view.View; import androidx.annotation.Nullable; import app.revanced.extension.shared.Logger; -import app.revanced.extension.youtube.patches.CopyVideoUrlPatch; +import app.revanced.extension.youtube.patches.CopyVideoURLPatch; import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") -public class CopyVideoUrlButton { +public class CopyVideoURLButton { @Nullable private static PlayerControlButton instance; @@ -23,9 +23,9 @@ public class CopyVideoUrlButton { "revanced_copy_video_url_button", null, Settings.COPY_VIDEO_URL::get, - view -> CopyVideoUrlPatch.copyUrl(false), + view -> CopyVideoURLPatch.copyURL(false), view -> { - CopyVideoUrlPatch.copyUrl(true); + CopyVideoURLPatch.copyURL(true); return true; } ); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlTimestampButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoURLTimestampButton.java similarity index 86% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlTimestampButton.java rename to extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoURLTimestampButton.java index c702756ed3..a359779f67 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlTimestampButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoURLTimestampButton.java @@ -5,11 +5,11 @@ import android.view.View; import androidx.annotation.Nullable; import app.revanced.extension.shared.Logger; -import app.revanced.extension.youtube.patches.CopyVideoUrlPatch; +import app.revanced.extension.youtube.patches.CopyVideoURLPatch; import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") -public class CopyVideoUrlTimestampButton { +public class CopyVideoURLTimestampButton { @Nullable private static PlayerControlButton instance; @@ -23,9 +23,9 @@ public class CopyVideoUrlTimestampButton { "revanced_copy_video_url_timestamp_button", null, Settings.COPY_VIDEO_URL_TIMESTAMP::get, - view -> CopyVideoUrlPatch.copyUrl(true), + view -> CopyVideoURLPatch.copyURL(true), view -> { - CopyVideoUrlPatch.copyUrl(false); + CopyVideoURLPatch.copyURL(false); return true; } ); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/LoopVideoButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/LoopVideoButton.java index 5b8df3b5e6..249c30eb1d 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/LoopVideoButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/LoopVideoButton.java @@ -3,6 +3,7 @@ package app.revanced.extension.youtube.videoplayer; import static app.revanced.extension.shared.StringRef.str; import android.view.View; +import android.widget.ImageView; import androidx.annotation.Nullable; @@ -19,7 +20,7 @@ public class LoopVideoButton { private static final int LOOP_VIDEO_ON = Utils.getResourceIdentifierOrThrow( ResourceType.DRAWABLE, "revanced_loop_video_button_on"); private static final int LOOP_VIDEO_OFF = Utils.getResourceIdentifierOrThrow( - ResourceType.DRAWABLE,"revanced_loop_video_button_off"); + ResourceType.DRAWABLE, "revanced_loop_video_button_off"); /** * Injection point. @@ -31,14 +32,44 @@ public class LoopVideoButton { "revanced_loop_video_button", null, Settings.LOOP_VIDEO_BUTTON::get, - v -> updateButtonAppearance(), + v -> updateButtonAppearance(true, v), null ); + // Set icon when initializing button based on current setting + updateButtonAppearance(false, null); } catch (Exception ex) { Logger.printException(() -> "initializeButton failure", ex); } } + /** + * Animate button transition with fade and scale. + */ + private static void animateButtonTransition(View view, boolean newState) { + if (!(view instanceof ImageView imageView)) return; + + // Fade out. + imageView.animate() + .alpha(0.3f) + .scaleX(1.15f) + .scaleY(1.15f) + .setDuration(100) + .withEndAction(() -> { + if (instance != null) { + instance.setIcon(newState ? LOOP_VIDEO_ON : LOOP_VIDEO_OFF); + } + + // Fade in. + imageView.animate() + .alpha(1.0f) + .scaleX(1.0f) + .scaleY(1.0f) + .setDuration(100) + .start(); + }) + .start(); + } + /** * injection point. */ @@ -51,6 +82,9 @@ public class LoopVideoButton { */ public static void setVisibilityImmediate(boolean visible) { if (instance != null) instance.setVisibilityImmediate(visible); + if (visible) { + updateIconFromSettings(); + } } /** @@ -58,27 +92,47 @@ public class LoopVideoButton { */ public static void setVisibility(boolean visible, boolean animated) { if (instance != null) instance.setVisibility(visible, animated); + if (visible) { + updateIconFromSettings(); + } + } + + /** + * Update icon based on current setting value. + */ + private static void updateIconFromSettings() { + PlayerControlButton localInstance = instance; + if (localInstance == null) return; + + final boolean currentState = Settings.LOOP_VIDEO.get(); + localInstance.setIcon(currentState ? LOOP_VIDEO_ON : LOOP_VIDEO_OFF); } /** * Updates the button's appearance. */ - private static void updateButtonAppearance() { + private static void updateButtonAppearance(boolean userClickedButton, @Nullable View buttonView) { if (instance == null) return; try { Utils.verifyOnMainThread(); final boolean currentState = Settings.LOOP_VIDEO.get(); - final boolean newState = !currentState; - Settings.LOOP_VIDEO.save(newState); - instance.setIcon(newState - ? LOOP_VIDEO_ON - : LOOP_VIDEO_OFF); - Utils.showToastShort(str(newState - ? "revanced_loop_video_button_toast_on" - : "revanced_loop_video_button_toast_off")); + if (userClickedButton) { + final boolean newState = !currentState; + + Settings.LOOP_VIDEO.save(newState); + Utils.showToastShort(str(newState + ? "revanced_loop_video_button_toast_on" + : "revanced_loop_video_button_toast_off")); + + // Animate with the new state. + if (buttonView != null) animateButtonTransition(buttonView, newState); + } else { + // Initialization - just set icon based on current state. + instance.setIcon(currentState ? LOOP_VIDEO_ON : LOOP_VIDEO_OFF); + } } catch (Exception ex) { Logger.printException(() -> "updateButtonAppearance failure", ex); } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java index 248de966d8..35878a9851 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java @@ -1,5 +1,7 @@ package app.revanced.extension.youtube.videoplayer; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.Drawable; import android.view.View; import android.view.ViewPropertyAnimator; import android.widget.ImageView; @@ -56,10 +58,23 @@ public class PlayerControlButton { containerRef = new WeakReference<>(containerView); View button = Utils.getChildViewByResourceName(controlsViewGroup, buttonId); - button.setOnClickListener(onClickListener); + + // Wrap click listener to trigger animation. + button.setOnClickListener(view -> { + animateIcon(); + if (onClickListener != null) { + onClickListener.onClick(view); + } + }); + if (longClickListener != null) { - button.setOnLongClickListener(longClickListener); + // Wrap long click listener to trigger animation. + button.setOnLongClickListener(view -> { + animateIcon(); + return longClickListener.onLongClick(view); + }); } + buttonRef = new WeakReference<>(button); TextView tempTextOverlay = null; @@ -82,6 +97,23 @@ public class PlayerControlButton { }); } + /** + * Animates the button icon if it's an AnimatedVectorDrawable. + */ + public void animateIcon() { + try { + View button = buttonRef.get(); + if (button instanceof ImageView imageView) { + Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AnimatedVectorDrawable avd) { + avd.start(); + } + } + } catch (Exception ex) { + Logger.printException(() -> "animateIcon failure", ex); + } + } + public void setVisibilityNegatedImmediate() { try { Utils.verifyOnMainThread(); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java index 611c0b7495..2e456f7989 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java @@ -3,7 +3,7 @@ package app.revanced.extension.youtube.videoplayer; import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.settings.preference.CustomDialogListPreference.*; import static app.revanced.extension.youtube.patches.VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE; -import static app.revanced.extension.youtube.patches.VideoInformation.VIDEO_QUALITY_PREMIUM_NAME; +import static app.revanced.extension.youtube.patches.VideoInformation.isPremiumVideoQuality; import static app.revanced.extension.youtube.videoplayer.PlayerControlButton.fadeInDuration; import static app.revanced.extension.youtube.videoplayer.PlayerControlButton.getDialogBackgroundColor; @@ -23,16 +23,15 @@ import androidx.annotation.Nullable; import app.revanced.extension.shared.ui.Dim; import app.revanced.extension.shared.ui.SheetBottomDialog; import app.revanced.extension.youtube.shared.PlayerType; -import com.google.android.libraries.youtube.innertube.model.media.VideoQuality; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import app.revanced.extension.shared.Logger; -import app.revanced.extension.shared.ResourceType; import app.revanced.extension.shared.Utils; import app.revanced.extension.youtube.patches.VideoInformation; +import app.revanced.extension.youtube.patches.VideoInformation.VideoQualityInterface; import app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch; import app.revanced.extension.youtube.settings.Settings; import kotlin.Unit; @@ -48,7 +47,7 @@ public class VideoQualityDialogButton { private static CharSequence currentOverlayText; static { - VideoInformation.onQualityChange.addObserver((@Nullable VideoQuality quality) -> { + VideoInformation.onQualityChange.addObserver((@Nullable VideoQualityInterface quality) -> { updateButtonText(quality); return Unit.INSTANCE; }); @@ -79,7 +78,7 @@ public class VideoQualityDialogButton { }, view -> { try { - VideoQuality[] qualities = VideoInformation.getCurrentQualities(); + VideoQualityInterface[] qualities = VideoInformation.getCurrentQualities(); if (qualities == null) { Logger.printDebug(() -> "Cannot reset quality, videoQualities is null"); return true; @@ -87,7 +86,7 @@ public class VideoQualityDialogButton { // Reset to default quality. final int defaultResolution = RememberVideoQualityPatch.getDefaultQualityResolution(); - for (VideoQuality quality : qualities) { + for (VideoQualityInterface quality : qualities) { final int resolution = quality.patch_getResolution(); if (resolution != AUTOMATIC_VIDEO_QUALITY_VALUE && resolution <= defaultResolution) { Logger.printDebug(() -> "Resetting quality to: " + quality); @@ -142,7 +141,7 @@ public class VideoQualityDialogButton { /** * Updates the button text based on the current video quality. */ - public static void updateButtonText(@Nullable VideoQuality quality) { + public static void updateButtonText(@Nullable VideoQualityInterface quality) { try { Utils.verifyOnMainThread(); if (instance == null) return; @@ -164,7 +163,7 @@ public class VideoQualityDialogButton { }; text.append(qualityText); - if (quality != null && quality.patch_getQualityName().contains(VIDEO_QUALITY_PREMIUM_NAME)) { + if (quality != null && isPremiumVideoQuality(quality)) { // Underline the entire "FHD" text for 1080p Premium. text.setSpan(new UnderlineSpan(), 0, qualityText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } @@ -187,8 +186,8 @@ public class VideoQualityDialogButton { */ private static void showVideoQualityDialog(Context context) { try { - VideoQuality[] currentQualities = VideoInformation.getCurrentQualities(); - VideoQuality currentQuality = VideoInformation.getCurrentQuality(); + VideoQualityInterface[] currentQualities = VideoInformation.getCurrentQualities(); + VideoQualityInterface currentQuality = VideoInformation.getCurrentQuality(); if (currentQualities == null || currentQuality == null) { Logger.printDebug(() -> "Cannot show qualities dialog, videoQualities is null"); return; @@ -201,7 +200,7 @@ public class VideoQualityDialogButton { // -1 adjustment for automatic quality at first index. int listViewSelectedIndex = -1; - for (VideoQuality quality : currentQualities) { + for (VideoQualityInterface quality : currentQualities) { if (quality.patch_getQualityName().equals(currentQuality.patch_getQualityName())) { break; } @@ -209,7 +208,7 @@ public class VideoQualityDialogButton { } List qualityLabels = new ArrayList<>(currentQualities.length - 1); - for (VideoQuality availableQuality : currentQualities) { + for (VideoQualityInterface availableQuality : currentQualities) { if (availableQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE) { qualityLabels.add(availableQuality.patch_getQualityName()); } @@ -283,7 +282,7 @@ public class VideoQualityDialogButton { listView.setOnItemClickListener((parent, view, which, id) -> { try { final int originalIndex = which + 1; // Adjust for automatic. - VideoQuality selectedQuality = currentQualities[originalIndex]; + VideoQualityInterface selectedQuality = currentQualities[originalIndex]; RememberVideoQualityPatch.userChangedQuality(selectedQuality.patch_getResolution()); VideoInformation.changeQuality(selectedQuality); diff --git a/extensions/youtube/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/VideoQuality.java b/extensions/youtube/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/VideoQuality.java deleted file mode 100644 index 628c423abe..0000000000 --- a/extensions/youtube/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/VideoQuality.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.google.android.libraries.youtube.innertube.model.media; - -public abstract class VideoQuality implements Comparable { - public abstract String patch_getQualityName(); - - public abstract int patch_getResolution(); -} - diff --git a/patches/api/patches.api b/patches/api/patches.api index 9d48fa58ce..57f2abaa19 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -485,6 +485,10 @@ public final class app/revanced/patches/music/misc/androidauto/UnlockAndroidAuto public static final fun getUnlockAndroidAutoMediaBrowserPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/music/misc/audio/ForceOriginalAudioPatchKt { + public static final fun getForceOriginalAudioPatch ()Lapp/revanced/patcher/patch/Patch; +} + public final class app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatchKt { public static final fun getRemoveBackgroundPlaybackRestrictionsPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -539,10 +543,6 @@ public final class app/revanced/patches/music/misc/spoof/UserAgentClientSpoofPat public static final fun getUserAgentClientSpoofPatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patches/music/misc/tracks/ForceOriginalAudioPatchKt { - public static final fun getForceOriginalAudioPatch ()Lapp/revanced/patcher/patch/Patch; -} - public final class app/revanced/patches/music/playservice/VersionCheckPatchKt { public static final fun getVersionCheckPatch ()Lapp/revanced/patcher/patch/Patch; public static final fun is_7_16_or_greater ()Z @@ -551,6 +551,8 @@ public final class app/revanced/patches/music/playservice/VersionCheckPatchKt { public static final fun is_8_10_or_greater ()Z public static final fun is_8_11_or_greater ()Z public static final fun is_8_15_or_greater ()Z + public static final fun is_8_40_or_greater ()Z + public static final fun is_8_41_or_greater ()Z } public final class app/revanced/patches/myexpenses/misc/pro/UnlockProPatchKt { @@ -1479,15 +1481,11 @@ public final class app/revanced/patches/youtube/ad/general/HideAdsPatchKt { public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatchKt { - public static final fun getHideGetPremiumPatch ()Lapp/revanced/patcher/patch/Patch; -} - public final class app/revanced/patches/youtube/ad/video/VideoAdsPatchKt { public static final fun getVideoAdsPatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoUrlPatchKt { +public final class app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoURLPatchKt { public static final fun getCopyVideoURLPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -1495,6 +1493,10 @@ public final class app/revanced/patches/youtube/interaction/dialog/RemoveViewerD public static final fun getRemoveViewerDiscretionDialogPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/youtube/interaction/doubletap/AddMoreDoubleTapToSeekLengthOptionsPatchKt { + public static final fun getAddMoreDoubleTapToSeekLengthOptionsPatch ()Lapp/revanced/patcher/patch/Patch; +} + public final class app/revanced/patches/youtube/interaction/doubletap/DisableChapterSkipDoubleTapPatchKt { public static final fun getDisableDoubleTapActionsPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -1503,18 +1505,22 @@ public final class app/revanced/patches/youtube/interaction/downloads/DownloadsP public static final fun getDownloadsPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatchKt { + public static final fun getDisableHapticFeedbackPatch ()Lapp/revanced/patcher/patch/Patch; +} + public final class app/revanced/patches/youtube/interaction/seekbar/DisablePreciseSeekingGesturePatchKt { public static final fun getDisablePreciseSeekingGesturePatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patches/youtube/interaction/seekbar/EnableSeekbarTappingPatchKt { - public static final fun getEnableSeekbarTappingPatch ()Lapp/revanced/patcher/patch/Patch; -} - public final class app/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatchKt { public static final fun getEnableSlideToSeekPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/youtube/interaction/seekbar/EnableTapToSeekPatchKt { + public static final fun getEnableTapToSeekPatch ()Lapp/revanced/patcher/patch/Patch; +} + public final class app/revanced/patches/youtube/interaction/seekbar/HideSeekbarPatchKt { public static final fun getHideSeekbarPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -1523,10 +1529,6 @@ public final class app/revanced/patches/youtube/interaction/seekbar/SeekbarPatch public static final fun getSeekbarPatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patches/youtube/interaction/seekbar/SeekbarThumbnailsPatchKt { - public static final fun getSeekbarThumbnailsPatch ()Lapp/revanced/patcher/patch/Patch; -} - public final class app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatchKt { public static final fun getSwipeControlsPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -1543,12 +1545,12 @@ public final class app/revanced/patches/youtube/layout/branding/header/ChangeHea public static final fun getChangeHeaderPatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patches/youtube/layout/buttons/action/HideButtonsPatchKt { +public final class app/revanced/patches/youtube/layout/buttons/action/HideVideoActionsButtonsPatchKt { public static final fun getHideVideoActionButtonsPatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatchKt { - public static final fun getNavigationButtonsPatch ()Lapp/revanced/patcher/patch/Patch; +public final class app/revanced/patches/youtube/layout/buttons/navigation/NavigationBarPatchKt { + public static final fun getNavigationBarPatch ()Lapp/revanced/patcher/patch/Patch; } public final class app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatchKt { @@ -1559,11 +1561,15 @@ public final class app/revanced/patches/youtube/layout/formfactor/ChangeFormFact public static final fun getChangeFormFactorPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatchKt { + public static final fun getHideAutoplayPreviewPatch ()Lapp/revanced/patcher/patch/Patch; +} + public final class app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatchKt { public static final fun getHideEndScreenCardsPatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patches/youtube/layout/hide/endscreensuggestion/HideEndScreenSuggestedVideoPatchKt { +public final class app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/HideEndScreenSuggestedVideoPatchKt { public static final fun getHideEndScreenSuggestedVideoPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -1579,10 +1585,14 @@ public final class app/revanced/patches/youtube/layout/hide/infocards/HideInfoCa public static final fun getHideInfoCardsPatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patches/youtube/layout/hide/player/flyoutmenupanel/HidePlayerFlyoutMenuPatchKt { +public final class app/revanced/patches/youtube/layout/hide/player/flyoutmenu/HidePlayerFlyoutMenuPatchKt { public static final fun getHidePlayerFlyoutMenuItemsPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/youtube/layout/hide/player/popup/PlayerPopupPanelsPatchKt { + public static final fun getDisablePlayerPopupPanelsPatch ()Lapp/revanced/patcher/patch/Patch; +} + public final class app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatchKt { public static final fun getHideRelatedVideoOverlayPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -1595,7 +1605,7 @@ public final class app/revanced/patches/youtube/layout/hide/shorts/HideShortsCom public static final fun getHideShortsComponentsPatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTvPatchPopupKt { +public final class app/revanced/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTVPopupPatchKt { public static final fun getDisableSignInToTVPopupPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -1607,10 +1617,6 @@ public final class app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatc public static final fun getMiniplayerPatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patches/youtube/layout/panels/popup/PlayerPopupPanelsPatchKt { - public static final fun getDisablePlayerPopupPanelsPatch ()Lapp/revanced/patcher/patch/Patch; -} - public final class app/revanced/patches/youtube/layout/player/fullscreen/ExitFullscreenPatchKt { public static final fun getExitFullscreenPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -1637,10 +1643,6 @@ public final class app/revanced/patches/youtube/layout/returnyoutubedislike/Vote public static fun values ()[Lapp/revanced/patches/youtube/layout/returnyoutubedislike/Vote; } -public final class app/revanced/patches/youtube/layout/searchbar/WideSearchbarPatchKt { - public static final fun getWideSearchBarPatch ()Lapp/revanced/patcher/patch/Patch; -} - public final class app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatchKt { public static final fun getSeekbarColorPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -1681,6 +1683,10 @@ public final class app/revanced/patches/youtube/layout/thumbnails/BypassImageReg public static final fun getBypassImageRegionRestrictionsPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/youtube/layout/toolbar/ToolbarHookPatchKt { + public static final fun getToolbarHookPatch ()Lapp/revanced/patcher/patch/Patch; +} + public final class app/revanced/patches/youtube/misc/announcements/AnnouncementsPatchKt { public static final fun getAnnouncementsPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -1693,6 +1699,25 @@ public final class app/revanced/patches/youtube/misc/backgroundplayback/Backgrou public static final fun getRemoveBackgroundPlaybackRestrictionsPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatchKt { + public static final fun addClientVersionHook (Lapp/revanced/patches/youtube/misc/contexthook/Endpoint;Ljava/lang/String;)V + public static final fun addOSNameHook (Lapp/revanced/patches/youtube/misc/contexthook/Endpoint;Ljava/lang/String;)V + public static final fun getHookClientContextPatch ()Lapp/revanced/patcher/patch/Patch; +} + +public final class app/revanced/patches/youtube/misc/contexthook/Endpoint : java/lang/Enum { + public static final field BROWSE Lapp/revanced/patches/youtube/misc/contexthook/Endpoint; + public static final field GUIDE Lapp/revanced/patches/youtube/misc/contexthook/Endpoint; + public static final field REEL Lapp/revanced/patches/youtube/misc/contexthook/Endpoint; + public static final field SEARCH Lapp/revanced/patches/youtube/misc/contexthook/Endpoint; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getGetEndpointMethods ()[Lkotlin/jvm/functions/Function1; + public final fun getInstructions ()Ljava/lang/String; + public final fun setInstructions (Ljava/lang/String;)V + public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/youtube/misc/contexthook/Endpoint; + public static fun values ()[Lapp/revanced/patches/youtube/misc/contexthook/Endpoint; +} + public final class app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatchKt { public static final fun getEnableDebuggingPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -1705,6 +1730,15 @@ public final class app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomain public static final fun getCheckWatchHistoryDomainNameResolutionPatch ()Lapp/revanced/patcher/patch/Patch; } +public final class app/revanced/patches/youtube/misc/engagement/EngagementPanelHookPatchKt { + public static final fun addEngagementPanelIdHook (Ljava/lang/String;)V + public static final fun getEngagementPanelHookPatch ()Lapp/revanced/patcher/patch/Patch; + public static final fun getPanelControllerMethod ()Lapp/revanced/com/android/tools/smali/dexlib2/mutable/MutableMethod; + public static final fun getPanelIdIndex ()I + public static final fun getPanelIdRegister ()I + public static final fun getPanelIdSmaliInstruction ()Ljava/lang/String; +} + public final class app/revanced/patches/youtube/misc/extension/SharedExtensionPatchKt { public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/Patch; } @@ -1717,16 +1751,12 @@ public final class app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatchKt { public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patches/youtube/misc/hapticfeedback/DisableHapticFeedbackPatchKt { - public static final fun getDisableHapticFeedbackPatch ()Lapp/revanced/patcher/patch/Patch; -} - public final class app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHookKt { - public static final fun addImageUrlErrorCallbackHook (Ljava/lang/String;)V - public static final fun addImageUrlHook (Ljava/lang/String;Z)V - public static synthetic fun addImageUrlHook$default (Ljava/lang/String;ZILjava/lang/Object;)V - public static final fun addImageUrlSuccessCallbackHook (Ljava/lang/String;)V - public static final fun getCronetImageUrlHookPatch ()Lapp/revanced/patcher/patch/Patch; + public static final fun addImageURLErrorCallbackHook (Ljava/lang/String;)V + public static final fun addImageURLHook (Ljava/lang/String;Z)V + public static synthetic fun addImageURLHook$default (Ljava/lang/String;ZILjava/lang/Object;)V + public static final fun addImageURLSuccessCallbackHook (Ljava/lang/String;)V + public static final fun getCronetImageURLHookPatch ()Lapp/revanced/patcher/patch/Patch; } public final class app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatchKt { @@ -1799,14 +1829,23 @@ public final class app/revanced/patches/youtube/misc/playservice/VersionCheckPat public static final fun is_20_22_or_greater ()Z public static final fun is_20_26_or_greater ()Z public static final fun is_20_28_or_greater ()Z + public static final fun is_20_29_or_greater ()Z public static final fun is_20_30_or_greater ()Z public static final fun is_20_31_or_greater ()Z public static final fun is_20_34_or_greater ()Z public static final fun is_20_37_or_greater ()Z public static final fun is_20_39_or_greater ()Z + public static final fun is_20_40_or_greater ()Z public static final fun is_20_41_or_greater ()Z public static final fun is_20_45_or_greater ()Z public static final fun is_20_46_or_greater ()Z + public static final fun is_20_49_or_greater ()Z + public static final fun is_21_02_or_greater ()Z + public static final fun is_21_03_or_greater ()Z + public static final fun is_21_05_or_greater ()Z + public static final fun is_21_06_or_greater ()Z + public static final fun is_21_07_or_greater ()Z + public static final fun is_21_08_or_greater ()Z } public final class app/revanced/patches/youtube/misc/privacy/SanitizeSharingLinksPatchKt { @@ -1913,17 +1952,22 @@ public final class app/revanced/patches/youtube/video/videoid/VideoIdPatchKt { } public final class app/revanced/util/BytecodeUtilsKt { - public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/com/android/tools/smali/dexlib2/mutable/MutableMethod;ILjava/lang/String;)V public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/com/android/tools/smali/dexlib2/mutable/MutableMethod;ILjava/lang/String;[Lapp/revanced/patcher/extensions/ExternalLabel;)V + public static final fun cloneMutable (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;ILjava/util/List;Ljava/lang/String;I)Lapp/revanced/com/android/tools/smali/dexlib2/mutable/MutableMethod; + public static synthetic fun cloneMutable$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;ILjava/util/List;Ljava/lang/String;IILjava/lang/Object;)Lapp/revanced/com/android/tools/smali/dexlib2/mutable/MutableMethod; + public static final fun cloneMutableAndPreserveParameters (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/com/android/tools/smali/dexlib2/mutable/MutableMethod; + public static final fun cloneMutableAndPreserveParameters (Lcom/android/tools/smali/dexlib2/iface/Method;Lapp/revanced/com/android/tools/smali/dexlib2/mutable/MutableClassDef;)Lapp/revanced/com/android/tools/smali/dexlib2/mutable/MutableMethod; public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;D)Z public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;F)Z public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;J)Z - public static final fun findFreeRegister (Lcom/android/tools/smali/dexlib2/iface/Method;I[I)I public static final fun findInstructionIndicesReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)Ljava/util/List; public static final fun findInstructionIndicesReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Lkotlin/jvm/functions/Function1;)Ljava/util/List; public static final fun findInstructionIndicesReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)Ljava/util/List; public static final fun findInstructionIndicesReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Lkotlin/jvm/functions/Function1;)Ljava/util/List; public static final fun forEachInstructionAsSequence (Lapp/revanced/patcher/patch/BytecodePatchContext;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function2;)V + public static final fun getNumberOfParameterRegisters (Lcom/android/tools/smali/dexlib2/iface/Method;)I + public static final fun getNumberOfParameterRegistersLogical (Lcom/android/tools/smali/dexlib2/iface/Method;)I + public static final fun getP0Register (Lcom/android/tools/smali/dexlib2/iface/Method;)I public static final fun indexOfFirstInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;ILcom/android/tools/smali/dexlib2/Opcode;)I public static final fun indexOfFirstInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;ILkotlin/jvm/functions/Function1;)I public static final fun indexOfFirstInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)I @@ -1983,6 +2027,24 @@ public final class app/revanced/util/BytecodeUtilsKt { public static final fun traverseClassHierarchy (Lapp/revanced/patcher/patch/BytecodePatchContext;Lapp/revanced/com/android/tools/smali/dexlib2/mutable/MutableClassDef;Lkotlin/jvm/functions/Function1;)V } +public final class app/revanced/util/FreeRegisterProvider { + public final fun availableCount ()I + public final fun getAllocatedFreeRegisters ()Ljava/util/List; + public final fun getFreeRegister ()I + public final fun getMethod ()Lcom/android/tools/smali/dexlib2/iface/Method; + public final fun getUsedAndUnAvailableRegisters ()Ljava/util/List; + public final fun hasFreeRegisters ()Z +} + +public final class app/revanced/util/FreeRegisterProviderKt { + public static final fun findFreeRegister (Lcom/android/tools/smali/dexlib2/iface/Method;ILjava/util/List;)I + public static final fun findFreeRegister (Lcom/android/tools/smali/dexlib2/iface/Method;I[I)I + public static final fun getFreeRegisterProvider (Lcom/android/tools/smali/dexlib2/iface/Method;IILjava/util/List;)Lapp/revanced/util/FreeRegisterProvider; + public static final fun getFreeRegisterProvider (Lcom/android/tools/smali/dexlib2/iface/Method;II[I)Lapp/revanced/util/FreeRegisterProvider; + public static final fun getRegistersUsed (Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Ljava/util/List; + public static final fun getWriteRegister (Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Ljava/lang/Integer; +} + public final class app/revanced/util/ResourceGroup { public fun (Ljava/lang/String;[Ljava/lang/String;)V public final fun getResourceDirectoryName ()Ljava/lang/String; diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/transformation/MethodCall.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/transformation/MethodCall.kt index 0535d6f06f..db17c2f740 100644 --- a/patches/src/main/kotlin/app/revanced/patches/all/misc/transformation/MethodCall.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/transformation/MethodCall.kt @@ -35,23 +35,10 @@ interface IMethodCall { instruction: Instruction35c, instructionIndex: Int, ) { - val registers = arrayOf( - instruction.registerC, - instruction.registerD, - instruction.registerE, - instruction.registerF, - instruction.registerG, - ) - val argsNum = methodParams.size + 1 // + 1 for instance of definedClassName - if (argsNum > registers.size) { - // should never happen, but just to be sure (also for the future) a safety check - throw RuntimeException( - "Not enough registers for $definedClassName#$methodName: " + - "Required $argsNum registers, but only got ${registers.size}.", - ) + val args = with(instruction) { + arrayOf(registerC, registerD, registerE, registerF, registerG) + .take(registerCount).joinToString(", ") { "v$it" } } - - val args = registers.take(argsNum).joinToString(separator = ", ") { reg -> "v$reg" } val replacementMethod = "$methodName(${definedClassName}${methodParams.joinToString(separator = "")})$returnType" diff --git a/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt b/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt index 410a6aff3e..daa52011fb 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt @@ -9,7 +9,8 @@ import app.revanced.patches.music.misc.settings.PreferenceScreen import app.revanced.patches.music.misc.settings.settingsPatch import app.revanced.patches.shared.misc.settings.preference.SwitchPreference -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/HideVideoAdsPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/music/patches/HideVideoAdsPatch;" @Suppress("unused") val hideMusicVideoAdsPatch = bytecodePatch( @@ -26,6 +27,8 @@ val hideMusicVideoAdsPatch = bytecodePatch( "com.google.android.apps.youtube.music"( "7.29.52", "8.10.52", + "8.37.56", + "8.40.54", ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt b/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt index cc4e23c09a..96aac10c51 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt @@ -19,6 +19,8 @@ val enableExclusiveAudioPlaybackPatch = bytecodePatch( "com.google.android.apps.youtube.music"( "7.29.52", "8.10.52", + "8.37.56", + "8.40.54", ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt index aa8a0449d6..2e227b60b1 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt @@ -12,7 +12,8 @@ import app.revanced.patches.music.misc.settings.settingsPatch import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.util.findFreeRegister -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/PermanentRepeatPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/music/patches/PermanentRepeatPatch;" @Suppress("unused") val permanentRepeatPatch = bytecodePatch( @@ -29,6 +30,8 @@ val permanentRepeatPatch = bytecodePatch( "com.google.android.apps.youtube.music"( "7.29.52", "8.10.52", + "8.37.56", + "8.40.54", ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt index 0587878df9..ff6b5326a5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt @@ -29,15 +29,15 @@ private val disableSplashAnimationPatch = bytecodePatch { // causing the original YT Music logo to momentarily flash on screen as the animation starts. // // Could replace the lottie animation file with our own custom animation (app_launch.json), - // but the animation is not always the same size as the launch screen and it's still - // barely shown. Instead turn off the animation entirely (app will also launch a little faster). + // but the animation is not always the same size as the launch screen, and it's still + // barely shown. Instead, turn off the animation entirely (app will also launch a little faster). cairoSplashAnimationConfigMethod.apply { val literalIndex = indexOfFirstLiteralInstructionOrThrow( ResourceType.LAYOUT["main_activity_launch_animation"], ) val checkCastIndex = indexOfFirstInstructionOrThrow(literalIndex) { opcode == Opcode.CHECK_CAST && - getReference()?.type == "Lcom/airbnb/lottie/LottieAnimationView;" + getReference()?.type == "Lcom/airbnb/lottie/LottieAnimationView;" } val register = getInstruction(checkCastIndex).registerA @@ -73,6 +73,8 @@ val customBrandingPatch = baseCustomBrandingPatch( "com.google.android.apps.youtube.music"( "7.29.52", "8.10.52", + "8.37.56", + "8.40.54", ), ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/Fingerprints.kt index d76fb66395..1d01efcab1 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/Fingerprints.kt @@ -7,8 +7,8 @@ import app.revanced.patches.shared.misc.mapping.ResourceType internal val BytecodePatchContext.cairoSplashAnimationConfigMethod by gettingFirstMethodDeclaratively { name("onCreate") - returnType("V") definingClass(YOUTUBE_MUSIC_MAIN_ACTIVITY_CLASS_TYPE) + returnType("V") parameterTypes("Landroid/os/Bundle;") instructions(ResourceType.LAYOUT("main_activity_launch_animation")) } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/buttons/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/buttons/Fingerprints.kt index 92aa2b6356..e8e1227997 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/buttons/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/buttons/Fingerprints.kt @@ -27,9 +27,7 @@ internal val BytecodePatchContext.historyMenuItemMethodMatch by composingFirstMe Opcode.RETURN_VOID, ) literal { historyMenuItem } - custom { - immutableClassDef.methods.count() == 5 - } + custom { immutableClassDef.methods.count() == 5 || immutableClassDef.methods.count() == 4 } } internal val BytecodePatchContext.historyMenuItemOfflineTabMethodMatch by composingFirstMethod { diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/buttons/HideButtons.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/buttons/HideButtons.kt index 9c2ea8054c..17a544f89f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/buttons/HideButtons.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/buttons/HideButtons.kt @@ -31,7 +31,8 @@ internal var searchButton = -1L internal var topBarMenuItemImageView = -1L private set -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/HideButtonsPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/music/patches/HideButtonsPatch;" @Suppress("unused") val hideButtonsPatch = bytecodePatch( @@ -49,6 +50,8 @@ val hideButtonsPatch = bytecodePatch( "com.google.android.apps.youtube.music"( "7.29.52", "8.10.52", + "8.37.56", + "8.40.54", ), ) @@ -91,7 +94,11 @@ val hideButtonsPatch = bytecodePatch( arrayOf( Triple(playerOverlayChipMethod, playerOverlayChip, "hideCastButton"), Triple(searchActionViewMethod, searchButton, "hideSearchButton"), - Triple(topBarMenuItemImageViewMethod, topBarMenuItemImageView, "hideNotificationButton"), + Triple( + topBarMenuItemImageViewMethod, + topBarMenuItemImageView, + "hideNotificationButton" + ), ).forEach { (method, resourceIdLiteral, methodName) -> method.apply { val resourceIndex = indexOfFirstLiteralInstructionOrThrow(resourceIdLiteral) @@ -104,7 +111,7 @@ val hideButtonsPatch = bytecodePatch( addInstruction( targetIndex + 1, "invoke-static { v$targetRegister }, " + - "$EXTENSION_CLASS_DESCRIPTOR->$methodName(Landroid/view/View;)V", + "$EXTENSION_CLASS_DESCRIPTOR->$methodName(Landroid/view/View;)V", ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt index 7bc195419c..aa944fee58 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt @@ -15,7 +15,8 @@ import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction internal var chipCloud = -1L private set -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/HideCategoryBarPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/music/patches/HideCategoryBarPatch;" @Suppress("unused") val hideCategoryBarPatch = bytecodePatch( @@ -32,6 +33,8 @@ val hideCategoryBarPatch = bytecodePatch( "com.google.android.apps.youtube.music"( "7.29.52", "8.10.52", + "8.37.56", + "8.40.54", ), ) @@ -46,7 +49,8 @@ val hideCategoryBarPatch = bytecodePatch( chipCloudMethodMatch.let { val targetIndex = it[-1] - val targetRegister = it.method.getInstruction(targetIndex).registerA + val targetRegister = + it.method.getInstruction(targetIndex).registerA it.method.addInstruction( targetIndex + 1, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/hide/general/HideLayoutComponentsPatch.kt index 0cf6ad4c90..7c88029dca 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/hide/general/HideLayoutComponentsPatch.kt @@ -8,5 +8,12 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( lithoFilterPatch = lithoFilterPatch, settingsPatch = settingsPatch, filterClasses = setOf("Lapp/revanced/extension/shared/patches/litho/CustomFilter;"), - compatibleWithPackages = arrayOf("com.google.android.apps.youtube.music" to setOf("7.29.52", "8.10.52")), + compatibleWithPackages = arrayOf( + "com.google.android.apps.youtube.music" to setOf( + "7.29.52", + "8.10.52", + "8.37.56", + "8.40.54" + ) + ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/ChangeMiniplayerColor.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/ChangeMiniplayerColor.kt index 7a699b2675..d8f78efe2f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/ChangeMiniplayerColor.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/ChangeMiniplayerColor.kt @@ -22,7 +22,8 @@ import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/ChangeMiniplayerColorPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/music/patches/ChangeMiniplayerColorPatch;" @Suppress("unused") val changeMiniplayerColorPatch = bytecodePatch( @@ -40,6 +41,8 @@ val changeMiniplayerColorPatch = bytecodePatch( "com.google.android.apps.youtube.music"( "7.29.52", "8.10.52", + "8.37.56", + "8.40.54", ), ) @@ -71,10 +74,11 @@ val changeMiniplayerColorPatch = bytecodePatch( miniPlayerConstructorMethodMatch.immutableMethod.indexOfFirstInstructionReversedOrThrow { getReference()?.name == "getColor" } - val iPutIndex = miniPlayerConstructorMethodMatch.immutableMethod.indexOfFirstInstructionOrThrow( - colorGreyIndex, - Opcode.IPUT, - ) + val iPutIndex = + miniPlayerConstructorMethodMatch.immutableMethod.indexOfFirstInstructionOrThrow( + colorGreyIndex, + Opcode.IPUT, + ) val colorMathPlayerIPutReference = miniPlayerConstructorMethodMatch.immutableMethod .getInstruction(iPutIndex).reference diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/Fingerprints.kt index 5a3ee206e3..cd621ba5ed 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/Fingerprints.kt @@ -7,12 +7,11 @@ import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.ClassDef -internal val BytecodePatchContext.miniPlayerConstructorMethodMatch by composingFirstMethod { - returnType("V") - instructions( - ResourceType.ID("mpp_player_bottom_sheet"), - "sharedToggleMenuItemMutations"(), - ) +internal val BytecodePatchContext.miniPlayerConstructorMethodMatch by composingFirstMethod( + "sharedToggleMenuItemMutations" +) { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + instructions(ResourceType.ID("music_playback_controls")) } /** diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/Fingerprints.kt index 00984d1e01..0021d79190 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/Fingerprints.kt @@ -2,34 +2,45 @@ package app.revanced.patches.music.layout.navigationbar import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext -import app.revanced.util.containsLiteralInstruction -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstruction +import app.revanced.patches.shared.misc.mapping.ResourceType import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.Method -import com.android.tools.smali.dexlib2.iface.reference.MethodReference -internal val BytecodePatchContext.tabLayoutTextMethodMatch by composingFirstMethod("FEmusic_search") { + +internal val BytecodePatchContext.tabLayoutTextMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") parameterTypes("L") - opcodes( - Opcode.IGET, - Opcode.INVOKE_STATIC, - Opcode.MOVE_RESULT_OBJECT, - Opcode.IF_NEZ, - Opcode.SGET_OBJECT, - Opcode.INVOKE_INTERFACE, - Opcode.MOVE_RESULT, - ) - custom { - containsLiteralInstruction(text1) && - indexOfGetVisibilityInstruction(this) >= 0 - } -} + instructions( + anyOf( + "FEmusic_search"(), // 8.49 and lower. + "FEsearch"() // 8.50+ + ), + // Hide navigation label. + ResourceType.ID("text1"), + afterAtMost( + 5, + method { toString() == "Landroid/view/View;->findViewById(I)Landroid/view/View;" } + ), + afterAtMost( + 5, + allOf(Opcode.CHECK_CAST(), type("Landroid/widget/TextView;")) + ), + // Set navigation enum. + anyOf( + Opcode.SGET_OBJECT(), + Opcode.IGET_OBJECT() + ), + afterAtMost(5, allOf(Opcode.IGET(), field { type == "I" })), -internal fun indexOfGetVisibilityInstruction(method: Method) = method.indexOfFirstInstruction { - opcode == Opcode.INVOKE_VIRTUAL && - getReference()?.name == "getVisibility" + afterAtMost( + 5, + allOf( + Opcode.INVOKE_STATIC(), + method { returnType == "L" && parameterTypes.size == 1 && parameterTypes.first() == "I" }) + ), + after(Opcode.MOVE_RESULT_OBJECT()), + // Hide navigation buttons. + method("getVisibility") + ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/NavigationBarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/NavigationBarPatch.kt index f977920b34..5ff1997f61 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/NavigationBarPatch.kt @@ -2,7 +2,6 @@ package app.revanced.patches.music.layout.navigationbar import app.revanced.patcher.extensions.addInstruction import app.revanced.patcher.extensions.getInstruction -import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch import app.revanced.patches.all.misc.resources.addResources @@ -14,9 +13,6 @@ import app.revanced.patches.shared.misc.mapping.ResourceType import app.revanced.patches.shared.misc.mapping.resourceMappingPatch import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.revanced.patches.shared.misc.settings.preference.SwitchPreference -import app.revanced.util.indexOfFirstInstructionOrThrow -import app.revanced.util.indexOfFirstLiteralInstructionOrThrow -import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction @@ -24,7 +20,8 @@ import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction internal var text1 = -1L private set -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/NavigationBarPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/music/patches/NavigationBarPatch;" @Suppress("unused") val navigationBarPatch = bytecodePatch( @@ -58,6 +55,8 @@ val navigationBarPatch = bytecodePatch( "com.google.android.apps.youtube.music"( "7.29.52", "8.10.52", + "8.37.56", + "8.40.54", ), ) @@ -83,39 +82,44 @@ val navigationBarPatch = bytecodePatch( ), ) - tabLayoutTextMethodMatch.method.apply { - // Hide navigation labels. - val constIndex = indexOfFirstLiteralInstructionOrThrow(text1) - val targetIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.CHECK_CAST) - val targetParameter = getInstruction(targetIndex).reference - val targetRegister = getInstruction(targetIndex).registerA + tabLayoutTextMethodMatch.let { + it.method.apply { + // Modify in reverse order to preserve match indices. - if (!targetParameter.toString().endsWith("Landroid/widget/TextView;")) { - throw PatchException("Method signature parameter did not match: $targetParameter") + // Hide navigation buttons. + val pivotTabIndex = it[-1] + val pivotTabRegister = + getInstruction(pivotTabIndex).registerC + + addInstruction( + pivotTabIndex, + "invoke-static { v$pivotTabRegister }, " + + "${EXTENSION_CLASS_DESCRIPTOR}->hideNavigationButton(Landroid/view/View;)V" + ) + + + // Set navigation enum and hide navigation buttons. + val enumIndex = it[7] + val enumRegister = getInstruction(enumIndex).registerA + + addInstruction( + enumIndex + 1, + "invoke-static { v$enumRegister }, " + + "${EXTENSION_CLASS_DESCRIPTOR}->setLastAppNavigationEnum(Ljava/lang/Enum;)V" + ) + + + // Hide navigation labels. + val labelIndex = it[3] + val targetParameter = getInstruction(labelIndex).reference + val targetRegister = getInstruction(labelIndex).registerA + + addInstruction( + labelIndex + 1, + "invoke-static { v$targetRegister }, " + + "${EXTENSION_CLASS_DESCRIPTOR}->hideNavigationLabel(Landroid/widget/TextView;)V" + ) } - - addInstruction( - targetIndex + 1, - "invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->hideNavigationLabel(Landroid/widget/TextView;)V", - ) - - // Set navigation enum and hide navigation buttons. - val enumIndex = tabLayoutTextMethodMatch[0] + 3 - val enumRegister = getInstruction(enumIndex).registerA - val insertEnumIndex = indexOfFirstInstructionOrThrow(Opcode.AND_INT_LIT8) - 2 - - val pivotTabIndex = indexOfGetVisibilityInstruction(this) - val pivotTabRegister = getInstruction(pivotTabIndex).registerC - - addInstruction( - pivotTabIndex, - "invoke-static { v$pivotTabRegister }, $EXTENSION_CLASS_DESCRIPTOR->hideNavigationButton(Landroid/view/View;)V", - ) - - addInstruction( - insertEnumIndex, - "invoke-static { v$enumRegister }, $EXTENSION_CLASS_DESCRIPTOR->setLastAppNavigationEnum(Ljava/lang/Enum;)V", - ) } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt index d6f1c0092d..3ec4d3af44 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt @@ -13,7 +13,8 @@ import app.revanced.patches.music.misc.settings.settingsPatch import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/HideGetPremiumPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/music/patches/HideGetPremiumPatch;" @Suppress("unused") val hideGetMusicPremiumPatch = bytecodePatch( @@ -30,6 +31,8 @@ val hideGetMusicPremiumPatch = bytecodePatch( "com.google.android.apps.youtube.music"( "7.29.52", "8.10.52", + "8.37.56", + "8.40.54", ), ) @@ -56,7 +59,7 @@ val hideGetMusicPremiumPatch = bytecodePatch( addInstruction( insertIndex + 1, "invoke-virtual {v$getPremiumViewRegister, v$visibilityRegister}, " + - "Landroid/view/View;->setVisibility(I)V", + "Landroid/view/View;->setVisibility(I)V", ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/ThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/ThemePatch.kt index c6e7091c6a..39a78c3d30 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/ThemePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/ThemePatch.kt @@ -17,19 +17,23 @@ val themePatch = baseThemePatch( dependsOn( sharedExtensionPatch, baseThemeResourcePatch( - darkColorNames = THEME_DEFAULT_DARK_COLOR_NAMES + setOf( - "yt_black_pure", - "yt_black_pure_opacity80", - "ytm_color_grey_12", - "material_grey_800" - ) + getDarkColorNames = { + THEME_DEFAULT_DARK_COLOR_NAMES + setOf( + "yt_black_pure", + "yt_black_pure_opacity80", + "ytm_color_grey_12", + "material_grey_800" + ) + } ) ) compatibleWith( "com.google.android.apps.youtube.music"( "7.29.52", - "8.10.52" + "8.10.52", + "8.37.56", + "8.40.54" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/UnlockAndroidAutoMediaBrowserPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/UnlockAndroidAutoMediaBrowserPatch.kt index dd2b2eaac7..e5bb4eed22 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/UnlockAndroidAutoMediaBrowserPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/UnlockAndroidAutoMediaBrowserPatch.kt @@ -18,19 +18,25 @@ val unlockAndroidAutoMediaBrowserPatch = bytecodePatch( "com.google.android.apps.youtube.music"( "7.29.52", "8.10.52", + "8.37.56", + "8.40.54", ), ) apply { checkCertificateMethod.returnEarly(true) - searchMediaItemsConstructorMethod.immutableClassDef.getSearchMediaItemsExecuteMethod().apply { - val targetIndex = instructions.indexOfFirst { - it.opcode == Opcode.IGET_OBJECT && it.fieldReference?.type == "Ljava/lang/String;" - } + searchMediaItemsConstructorMethod.immutableClassDef.getSearchMediaItemsExecuteMethod() + .apply { + val targetIndex = instructions.indexOfFirst { + it.opcode == Opcode.IGET_OBJECT && it.fieldReference?.type == "Ljava/lang/String;" + } - val register = instructions[targetIndex].registersUsed.first() - replaceInstruction(targetIndex, "const-string v$register, \"com.google.android.apps.youtube.music\"") - } + val register = instructions[targetIndex].registersUsed.first() + replaceInstruction( + targetIndex, + "const-string v$register, \"com.google.android.apps.youtube.music\"" + ) + } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/tracks/ForceOriginalAudioPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/audio/ForceOriginalAudioPatch.kt similarity index 89% rename from patches/src/main/kotlin/app/revanced/patches/music/misc/tracks/ForceOriginalAudioPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/music/misc/audio/ForceOriginalAudioPatch.kt index cbdc7ab803..4fae08ecfb 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/tracks/ForceOriginalAudioPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/audio/ForceOriginalAudioPatch.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.music.misc.tracks +package app.revanced.patches.music.misc.audio import app.revanced.patches.music.misc.extension.sharedExtensionPatch import app.revanced.patches.music.misc.settings.PreferenceScreen @@ -20,7 +20,9 @@ val forceOriginalAudioPatch = forceOriginalAudioPatch( compatibleWith( "com.google.android.apps.youtube.music"( "7.29.52", - "8.10.52" + "8.10.52", + "8.37.56", + "8.40.54" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt index 5d8b651c18..cbe36736ec 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt @@ -19,6 +19,8 @@ val removeBackgroundPlaybackRestrictionsPatch = bytecodePatch( "com.google.android.apps.youtube.music"( "7.29.52", "8.10.52", + "8.37.56", + "8.40.54", ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/EnableDebuggingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/EnableDebuggingPatch.kt index 16e4ddd801..77edc32b42 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/EnableDebuggingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/EnableDebuggingPatch.kt @@ -3,19 +3,31 @@ package app.revanced.patches.music.misc.debugging import app.revanced.patches.music.misc.extension.sharedExtensionPatch import app.revanced.patches.music.misc.settings.PreferenceScreen import app.revanced.patches.music.misc.settings.settingsPatch +import app.revanced.patches.music.playservice.is_8_40_or_greater +import app.revanced.patches.music.playservice.is_8_41_or_greater import app.revanced.patches.shared.misc.debugging.enableDebuggingPatch @Suppress("unused") val enableDebuggingPatch = enableDebuggingPatch( - sharedExtensionPatch = sharedExtensionPatch, - settingsPatch = settingsPatch, - compatibleWithPackages = arrayOf( - "com.google.android.apps.youtube.music" to setOf( - "7.29.52", - "8.10.52" + block = { + dependsOn( + sharedExtensionPatch, + settingsPatch, ) - ), + + compatibleWith( + "com.google.android.apps.youtube.music"( + "7.29.52", + "8.10.52", + "8.37.56", + "8.40.54", + ) + ) + }, // String feature flag does not appear to be present with YT Music. - hookStringFeatureFlag = false, + hookStringFeatureFlag = { false }, + // 8.40 has changes not worth supporting. + hookLongFeatureFlag = { !is_8_40_or_greater || is_8_41_or_greater }, + hookDoubleFeatureFlag = { !is_8_40_or_greater || is_8_41_or_greater }, preferenceScreen = PreferenceScreen.MISC, ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt index aa00a581ab..f3eb9b08ee 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt @@ -13,7 +13,9 @@ val checkWatchHistoryDomainNameResolutionPatch = checkWatchHistoryDomainNameReso compatibleWith( "com.google.android.apps.youtube.music"( "7.29.52", - "8.10.52" + "8.10.52", + "8.37.56", + "8.40.54" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt index 88cfe2ca6f..d424f1685e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt @@ -32,6 +32,8 @@ val gmsCoreSupportPatch = gmsCoreSupportPatch( MUSIC_PACKAGE_NAME( "7.29.52", "8.10.52", + "8.37.56", + "8.40.54", ), ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/privacy/SanitizeSharingLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/privacy/SanitizeSharingLinksPatch.kt index e173b5c849..6aa5863fa2 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/privacy/SanitizeSharingLinksPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/privacy/SanitizeSharingLinksPatch.kt @@ -16,7 +16,9 @@ val sanitizeSharingLinksPatch = sanitizeSharingLinksPatch( compatibleWith( "com.google.android.apps.youtube.music"( "7.29.52", - "8.10.52" + "8.10.52", + "8.37.56", + "8.40.54" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/SettingsPatch.kt index 497e100112..425630b7ed 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/SettingsPatch.kt @@ -8,14 +8,18 @@ import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.music.misc.extension.sharedExtensionPatch import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME +import app.revanced.patches.music.playservice.is_8_40_or_greater +import app.revanced.patches.music.playservice.versionCheckPatch +import app.revanced.patches.shared.boldIconsFeatureFlagMethodMatch import app.revanced.patches.shared.misc.mapping.resourceMappingPatch import app.revanced.patches.shared.misc.settings.preference.* import app.revanced.patches.shared.misc.settings.settingsPatch import app.revanced.patches.youtube.misc.settings.modifyActivityForSettingsInjection import app.revanced.util.copyXmlNode import app.revanced.util.inputStreamFromBundledResource +import app.revanced.util.insertLiteralOverride -private const val GOOGLE_API_ACTIVITY_HOOK_CLASS_DESCRIPTOR = +private const val MUSIC_ACTIVITY_HOOK_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/settings/MusicActivityHook;" private val preferences = mutableSetOf() @@ -72,6 +76,7 @@ val settingsPatch = bytecodePatch( sharedExtensionPatch, settingsResourcePatch, addResourcesPatch, + versionCheckPatch ) apply { @@ -90,6 +95,12 @@ val settingsPatch = bytecodePatch( SwitchPreference("revanced_settings_search_history") ) + if (is_8_40_or_greater) { + PreferenceScreen.GENERAL.addPreferences( + SwitchPreference("revanced_settings_disable_bold_icons") + ) + } + PreferenceScreen.MISC.addPreferences( TextPreference( key = null, @@ -103,9 +114,18 @@ val settingsPatch = bytecodePatch( modifyActivityForSettingsInjection( googleApiActivityMethod.classDef, googleApiActivityMethod, - GOOGLE_API_ACTIVITY_HOOK_CLASS_DESCRIPTOR, + MUSIC_ACTIVITY_HOOK_CLASS_DESCRIPTOR, true ) + + if (is_8_40_or_greater) { + boldIconsFeatureFlagMethodMatch.let { + it.method.insertLiteralOverride( + it[0], + "$MUSIC_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useBoldIcons(Z)Z" + ) + } + } } afterDependents { diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt index f019fca848..c4c6764e54 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt @@ -31,7 +31,9 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch( compatibleWith( "com.google.android.apps.youtube.music"( "7.29.52", - "8.10.52" + "8.10.52", + "8.37.56", + "8.40.54" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/playservice/VersionCheckPatch.kt index 134121b677..003fa7b6b9 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/playservice/VersionCheckPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/playservice/VersionCheckPatch.kt @@ -20,8 +20,11 @@ var is_8_11_or_greater: Boolean by Delegates.notNull() private set var is_8_15_or_greater: Boolean by Delegates.notNull() private set +var is_8_40_or_greater: Boolean by Delegates.notNull() + private set +var is_8_41_or_greater: Boolean by Delegates.notNull() + private set -@Suppress("unused") val versionCheckPatch = resourcePatch( description = "Uses the Play Store service version to find the major/minor version of the YouTube Music target app." ) { @@ -38,5 +41,7 @@ val versionCheckPatch = resourcePatch( is_8_10_or_greater = 251099000 <= playStoreServicesVersion is_8_11_or_greater = 251199000 <= playStoreServicesVersion is_8_15_or_greater = 251530000 <= playStoreServicesVersion + is_8_40_or_greater = 254080000 <= playStoreServicesVersion + is_8_41_or_greater = 254180000 <= playStoreServicesVersion } } diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt index 035523a62d..d5dddc4da6 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt @@ -1,7 +1,14 @@ package app.revanced.patches.shared +import app.revanced.patcher.accessFlags +import app.revanced.patcher.composingFirstMethod import app.revanced.patcher.gettingFirstMethodDeclaratively +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke +import app.revanced.patcher.parameterTypes import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.returnType +import com.android.tools.smali.dexlib2.AccessFlags internal val BytecodePatchContext.castContextFetchMethod by gettingFirstMethodDeclaratively( "Error fetching CastContext." @@ -11,3 +18,14 @@ internal val BytecodePatchContext.primeMethod by gettingFirstMethodDeclaratively "com.android.vending", "com.google.android.GoogleCamera" ) + +// Flag is present in YouTube 20.23, but bold icons are missing and forcing them crashes the app. +// 20.31 is the first target with all the bold icons present. +internal val BytecodePatchContext.boldIconsFeatureFlagMethodMatch by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Z") + parameterTypes() + instructions( + 45685201L(), + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/AddBrandLicensePatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/AddBrandLicensePatch.kt index 933af78824..f64366b7eb 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/AddBrandLicensePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/AddBrandLicensePatch.kt @@ -1,8 +1,10 @@ package app.revanced.patches.shared.layout.branding +import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.rawResourcePatch import app.revanced.util.inputStreamFromBundledResource import java.nio.file.Files +import java.util.logging.Logger /** * Copies a branding license text file to the target apk. @@ -20,6 +22,8 @@ internal val addBrandLicensePatch = rawResourcePatch { val targetFile = get(brandingLicenseFileName, false).toPath() - Files.copy(inputFileStream, targetFile) + if (Files.exists(targetFile)) Logger.getLogger(this::class.java.name) + .warning("Already patched by ReVanced") + else Files.copy(inputFileStream, targetFile) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/BaseCustomBrandingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/BaseCustomBrandingPatch.kt index 750dee5fdf..35baad31cf 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/BaseCustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/BaseCustomBrandingPatch.kt @@ -3,6 +3,7 @@ package app.revanced.patches.shared.layout.branding import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod import app.revanced.patcher.extensions.addInstruction import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.firstImmutableClassDef import app.revanced.patcher.patch.* import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName import app.revanced.patches.all.misc.resources.addResources @@ -52,9 +53,11 @@ private val USER_CUSTOM_ADAPTIVE_FILE_NAMES = arrayOf( private const val USER_CUSTOM_MONOCHROME_FILE_NAME = "$LAUNCHER_ADAPTIVE_MONOCHROME_PREFIX$CUSTOM_USER_ICON_STYLE_NAME.xml" -private const val USER_CUSTOM_NOTIFICATION_ICON_FILE_NAME = "${NOTIFICATION_ICON_NAME}_$CUSTOM_USER_ICON_STYLE_NAME.xml" +private const val USER_CUSTOM_NOTIFICATION_ICON_FILE_NAME = + "${NOTIFICATION_ICON_NAME}_$CUSTOM_USER_ICON_STYLE_NAME.xml" -internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/shared/patches/CustomBrandingPatch;" +internal const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/shared/patches/CustomBrandingPatch;" /** * Shared custom branding patch for YouTube and YT Music. @@ -75,7 +78,7 @@ internal fun baseCustomBrandingPatch( ) = resourcePatch( name = "Custom branding", description = "Adds options to change the app icon and app name. " + - "Branding cannot be changed for mounted (root) installations.", + "Branding cannot be changed for mounted (root) installations.", ) { val customName by stringOption( name = "App name", @@ -116,6 +119,8 @@ internal fun baseCustomBrandingPatch( ) numberOfPresetAppNamesExtensionMethod.returnEarly(numberOfPresetAppNames) + userProvidedCustomNameExtensionMethod.returnEarly(customName != null) + userProvidedCustomIconExtensionMethod.returnEarly(customIcon != null) notificationMethod.apply { val getBuilderIndex = if (isYouTubeMusic) { @@ -128,7 +133,7 @@ internal fun baseCustomBrandingPatch( val builderCastIndex = indexOfFirstInstructionOrThrow { val reference = getReference() opcode == Opcode.CHECK_CAST && - reference?.type == "Landroid/app/Notification\$Builder;" + reference?.type == "Landroid/app/Notification\$Builder;" } indexOfFirstInstructionReversedOrThrow(builderCastIndex) { getReference()?.type == "Ljava/lang/Object;" @@ -173,11 +178,14 @@ internal fun baseCustomBrandingPatch( } apply { + val useCustomName = customName != null + val useCustomIcon = customIcon != null + addResources("shared", "layout.branding.baseCustomBrandingPatch") addResources(addResourcePatchName, "layout.branding.customBrandingPatch") preferenceScreen.addPreferences( - if (customName != null) { + if (useCustomName) { ListPreference( key = "revanced_custom_branding_name", entriesKey = "revanced_custom_branding_name_custom_entries", @@ -186,7 +194,7 @@ internal fun baseCustomBrandingPatch( } else { ListPreference("revanced_custom_branding_name") }, - if (customIcon != null) { + if (useCustomIcon) { ListPreference( key = "revanced_custom_branding_icon", entriesKey = "revanced_custom_branding_icon_custom_entries", @@ -197,9 +205,6 @@ internal fun baseCustomBrandingPatch( }, ) - val useCustomName = customName != null - val useCustomIcon = customIcon != null - iconStyleNames.forEach { style -> copyResources( "custom-branding", @@ -307,7 +312,7 @@ internal fun baseCustomBrandingPatch( activityAliasNameWithIntents, ).childNodes - // The YT application name can appear in some places along side the system + // The YT application name can appear in some places alongside the system // YouTube app, such as the settings app list and in the "open with" file picker. // Because the YouTube app cannot be completely uninstalled and only disabled, // use a custom name for this situation to disambiguate which app is which. @@ -316,6 +321,9 @@ internal fun baseCustomBrandingPatch( "@string/revanced_custom_branding_name_entry_2", ) + val enabledNameIndex = if (useCustomName) numberOfPresetAppNames else 1 // 1 indexing. + val enabledIconIndex = if (useCustomIcon) iconStyleNames.size else 0 // 0 indexing. + for (appNameIndex in 1..numberOfPresetAppNames) { fun aliasName(name: String): String = ".revanced_" + name + '_' + appNameIndex @@ -334,14 +342,14 @@ internal fun baseCustomBrandingPatch( ) // Bundled icons. - iconStyleNames.forEach { style -> + iconStyleNames.forEachIndexed { iconIndex, style -> application.appendChild( createAlias( aliasName = aliasName(style), iconMipmapName = LAUNCHER_RESOURCE_NAME_PREFIX + style, appNameIndex = appNameIndex, useCustomName = useCustomNameLabel, - enabled = false, + enabled = (appNameIndex == enabledNameIndex && iconIndex == enabledIconIndex), intentFilters, ), ) @@ -351,7 +359,7 @@ internal fun baseCustomBrandingPatch( // // Must add all aliases even if the user did not provide a custom icon of their own. // This is because if the user installs with an option, then repatches without the option, - // the alias must still exist because if it was previously enabled and then it's removed + // the alias must still exist because if it was previously enabled, and then it's removed // the app will become broken and cannot launch. Even if the app data is cleared // it still cannot be launched and the only fix is to uninstall the app. // To prevent this, always include all aliases and use dummy data if needed. @@ -361,7 +369,7 @@ internal fun baseCustomBrandingPatch( iconMipmapName = LAUNCHER_RESOURCE_NAME_PREFIX + CUSTOM_USER_ICON_STYLE_NAME, appNameIndex = appNameIndex, useCustomName = useCustomNameLabel, - enabled = false, + enabled = appNameIndex == enabledNameIndex && useCustomIcon, intentFilters, ), ) @@ -413,7 +421,7 @@ internal fun baseCustomBrandingPatch( if (customFiles.isNotEmpty() && customFiles.size != USER_CUSTOM_ADAPTIVE_FILE_NAMES.size) { throw PatchException( "Must include all required icon files " + - "but only found " + customFiles.map { it.name }, + "but only found " + customFiles.map { it.name }, ) } @@ -444,8 +452,8 @@ internal fun baseCustomBrandingPatch( if (!copiedFiles) { throw PatchException( "Expected to find directories and files: " + - USER_CUSTOM_ADAPTIVE_FILE_NAMES.contentToString() + - "\nBut none were found in the provided option file path: " + iconPathFile.absolutePath, + USER_CUSTOM_ADAPTIVE_FILE_NAMES.contentToString() + + "\nBut none were found in the provided option file path: " + iconPathFile.absolutePath, ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/Fingerprints.kt index 074bff4f28..0174c49c93 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/Fingerprints.kt @@ -5,13 +5,30 @@ import app.revanced.patcher.patch.BytecodePatchContext import com.android.tools.smali.dexlib2.AccessFlags internal val BytecodePatchContext.numberOfPresetAppNamesExtensionMethod by gettingFirstMethodDeclaratively { - name("numberOfPresetAppNames") definingClass(EXTENSION_CLASS_DESCRIPTOR) + name("numberOfPresetAppNames") accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC) returnType("I") parameterTypes() } + +internal val BytecodePatchContext.userProvidedCustomNameExtensionMethod by gettingFirstMethodDeclaratively { + definingClass(EXTENSION_CLASS_DESCRIPTOR) + name("userProvidedCustomName") + accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC) + returnType("Z") + parameterTypes() +} + +internal val BytecodePatchContext.userProvidedCustomIconExtensionMethod by gettingFirstMethodDeclaratively { + definingClass(EXTENSION_CLASS_DESCRIPTOR) + name("userProvidedCustomIcon") + accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC) + returnType("Z") + parameterTypes() +} + // A much simpler method exists that can set the small icon (contains string "414843287017"), // but that has limited usage and this one allows changing any part of the notification. internal val BytecodePatchContext.notificationMethod by gettingFirstMethodDeclaratively( diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/layout/theme/BaseThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/layout/theme/BaseThemePatch.kt index cd298fbb86..8c720c76e0 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/layout/theme/BaseThemePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/layout/theme/BaseThemePatch.kt @@ -1,20 +1,26 @@ package app.revanced.patches.shared.layout.theme -import app.revanced.patcher.patch.* +import app.revanced.patcher.patch.BytecodePatchBuilder +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption import app.revanced.util.childElementsSequence import java.util.* -internal const val THEME_COLOR_OPTION_DESCRIPTION = "Can be a hex color (#RRGGBB) or a color resource reference." +internal const val THEME_COLOR_OPTION_DESCRIPTION = + "Can be a hex color (#RRGGBB) or a color resource reference." internal val THEME_DEFAULT_DARK_COLOR_NAMES = setOf( - "yt_black0", "yt_black1", "yt_black1_opacity95", "yt_black1_opacity98", - "yt_black2", "yt_black3", "yt_black4", "yt_status_bar_background_dark", - "material_grey_850" + "yt_black0", "yt_black1", "yt_black2", "yt_black3", "yt_black4", + "yt_black1_opacity95", "yt_black1_opacity98", + "yt_status_bar_background_dark", "material_grey_850", ) internal val THEME_DEFAULT_LIGHT_COLOR_NAMES = setOf( - "yt_white1", "yt_white1_opacity95", "yt_white1_opacity98", - "yt_white2", "yt_white3", "yt_white4" + "yt_white1", "yt_white2", "yt_white3", "yt_white4", + "yt_white1_opacity95", "yt_white1_opacity98", ) /** @@ -94,8 +100,8 @@ internal fun baseThemePatch( } internal fun baseThemeResourcePatch( - darkColorNames: Set = THEME_DEFAULT_DARK_COLOR_NAMES, - lightColorNames: Set = THEME_DEFAULT_LIGHT_COLOR_NAMES, + getDarkColorNames: () -> Set = { THEME_DEFAULT_DARK_COLOR_NAMES }, + getLightColorNames: () -> Set = { THEME_DEFAULT_LIGHT_COLOR_NAMES }, lightColorReplacement: (() -> String)? = null ) = resourcePatch { apply { @@ -114,6 +120,9 @@ internal fun baseThemeResourcePatch( document("res/values/colors.xml").use { document -> val resourcesNode = document.getElementsByTagName("resources").item(0) + val darkColorNames = getDarkColorNames() + val lightColorNames = getLightColorNames() + resourcesNode.childElementsSequence().forEach { node -> val name = node.getAttribute("name") when { diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/layout/theme/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/layout/theme/Fingerprints.kt index c3c7e3d4c0..d0ef6822c0 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/layout/theme/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/layout/theme/Fingerprints.kt @@ -11,7 +11,7 @@ internal val BytecodePatchContext.lithoOnBoundsChangeMethodMatch by composingFir returnType("V") parameterTypes("Landroid/graphics/Rect;") - lateinit var methodDefiningClass: String + var methodDefiningClass = "" custom { methodDefiningClass = definingClass true @@ -23,11 +23,7 @@ internal val BytecodePatchContext.lithoOnBoundsChangeMethodMatch by composingFir field { type == "Landroid/graphics/Path;" && definingClass == methodDefiningClass }, ), afterAtMost( - 5, - method { returnType == "Z" && name == "isStateful" && definingClass == methodDefiningClass }, - ), - afterAtMost( - 5, + 10, allOf( Opcode.IGET_OBJECT(), field { type == "Landroid/graphics/Paint;" && definingClass == methodDefiningClass }, diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/audio/ForceOriginalAudioPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/audio/ForceOriginalAudioPatch.kt index 6360709519..7862271315 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/audio/ForceOriginalAudioPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/audio/ForceOriginalAudioPatch.kt @@ -14,8 +14,11 @@ import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.BasePreferenceScreen import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.util.addInstructionsAtControlFlowLabel +import app.revanced.util.cloneMutable import app.revanced.util.findMethodFromToString import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow import app.revanced.util.insertLiteralOverride import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode @@ -96,58 +99,50 @@ internal fun forceOriginalAudioPatch( ).toMutable(), ) - // Add a helper method because the isDefaultAudioTrack() has only 2 registers and 3 are needed. - val helperMethodClass = type - val helperMethodName = "patch_isDefaultAudioTrack" - val helperMethod = ImmutableMethod( - helperMethodClass, - helperMethodName, - listOf(ImmutableMethodParameter("Z", null, null)), - "Z", - AccessFlags.PRIVATE.value, - null, - null, - MutableMethodImplementation(6), - ).toMutable().apply { - addInstructionsWithLabels( - 0, - """ - iget-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean; - if-eqz v0, :call_extension - invoke-virtual { v0 }, Ljava/lang/Boolean;->booleanValue()Z - move-result v3 - return v3 - - :call_extension - invoke-virtual { p0 }, $audioTrackIdMethod - move-result-object v1 - - invoke-virtual { p0 }, $audioTrackDisplayNameMethod - move-result-object v2 - - invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->isDefaultAudioStream(ZLjava/lang/String;Ljava/lang/String;)Z - move-result v3 - - invoke-static { v3 }, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean; - move-result-object v0 - iput-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean; - return v3 - """, - ) + // Clone the method to add additional registers because the + // isDefaultAudioTrack() has only 1 or 2 registers and 3 are needed. + val clonedMethod = isDefaultAudioTrackMethod.cloneMutable( + additionalRegisters = 4 + ) + + // Replace existing method with cloned with more registers. + methods.apply { + remove(isDefaultAudioTrackMethod) + add(clonedMethod) } - methods.add(helperMethod) - // Modify isDefaultAudioTrack() to call extension helper method. - isDefaultAudioTrackMethod.apply { - val index = indexOfFirstInstructionOrThrow(Opcode.RETURN) - val register = getInstruction(index).registerA + clonedMethod.apply { + // Free registers are added + val free1 = isDefaultAudioTrackMethod.implementation!!.registerCount + 1 + val free2 = free1 + 1 + val insertIndex = indexOfFirstInstructionReversedOrThrow(Opcode.RETURN) + val originalResultRegister = + getInstruction(insertIndex).registerA - addInstructions( - index, + clonedMethod.addInstructionsAtControlFlowLabel( + insertIndex, """ - invoke-direct { p0, v$register }, $helperMethodClass->$helperMethodName(Z)Z - move-result v$register - """, + iget-object v$free1, p0, $type->$helperFieldName:Ljava/lang/Boolean; + if-eqz v$free1, :call_extension + invoke-virtual { v$free1 }, Ljava/lang/Boolean;->booleanValue()Z + move-result v$free1 + return v$free1 + + :call_extension + invoke-virtual { p0 }, $audioTrackIdMethod + move-result-object v$free1 + + invoke-virtual { p0 }, $audioTrackDisplayNameMethod + move-result-object v$free2 + + invoke-static { v$originalResultRegister, v$free1, v$free2 }, ${EXTENSION_CLASS_DESCRIPTOR}->isDefaultAudioStream(ZLjava/lang/String;Ljava/lang/String;)Z + move-result v$free1 + + invoke-static { v$free1 }, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean; + move-result-object v$free2 + iput-object v$free2, p0, $type->$helperFieldName:Ljava/lang/Boolean; + return v$free1 + """ ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/debugging/EnableDebuggingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/debugging/EnableDebuggingPatch.kt index aa8bb88bf8..5d84c52978 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/debugging/EnableDebuggingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/debugging/EnableDebuggingPatch.kt @@ -1,18 +1,27 @@ package app.revanced.patches.shared.misc.debugging +import app.revanced.patcher.accessFlags +import app.revanced.patcher.classDef import app.revanced.patcher.extensions.addInstructions -import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.firstMethodDeclaratively import app.revanced.patcher.immutableClassDef +import app.revanced.patcher.instructions +import app.revanced.patcher.method +import app.revanced.patcher.parameterTypes +import app.revanced.patcher.patch.BytecodePatchBuilder +import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.returnType import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.* import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting import app.revanced.util.* +import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/shared/patches/EnableDebuggingPatch;" @@ -21,21 +30,19 @@ private const val EXTENSION_CLASS_DESCRIPTOR = * Patch shared with YouTube and YT Music. */ internal fun enableDebuggingPatch( - sharedExtensionPatch: Patch, - settingsPatch: Patch, - vararg compatibleWithPackages: Pair>, - hookStringFeatureFlag: Boolean, + block: BytecodePatchBuilder.() -> Unit = {}, + executeBlock: BytecodePatchContext.() -> Unit = {}, + hookStringFeatureFlag: BytecodePatchBuilder.() -> Boolean, + hookLongFeatureFlag: BytecodePatchBuilder.() -> Boolean, + hookDoubleFeatureFlag: BytecodePatchBuilder.() -> Boolean, preferenceScreen: BasePreferenceScreen.Screen, + additionalDebugPreferences: List = emptyList() ) = bytecodePatch( name = "Enable debugging", description = "Adds options for debugging and exporting ReVanced logs to the clipboard.", ) { - compatibleWith(packages = compatibleWithPackages) dependsOn( - sharedExtensionPatch, - settingsPatch, - addResourcesPatch, resourcePatch { apply { copyResources( @@ -50,36 +57,44 @@ internal fun enableDebuggingPatch( "revanced_settings_arrow_left_double.xml", "revanced_settings_arrow_left_one.xml", "revanced_settings_arrow_right_double.xml", - "revanced_settings_arrow_right_one.xml", - ), + "revanced_settings_arrow_right_one.xml" + ) ) } - }, + } ) + block() + apply { + executeBlock() + addResources("shared", "misc.debugging.enableDebuggingPatch") - val preferences = setOf( + val preferences = mutableSetOf( SwitchPreference("revanced_debug"), - SwitchPreference("revanced_debug_protobuffer"), + ) + + preferences + additionalDebugPreferences + + preferences += listOf( SwitchPreference("revanced_debug_stacktrace"), SwitchPreference("revanced_debug_toast_on_error"), NonInteractivePreference( "revanced_debug_export_logs_to_clipboard", tag = "app.revanced.extension.shared.settings.preference.ExportLogToClipboardPreference", - selectable = true, + selectable = true ), NonInteractivePreference( "revanced_debug_logs_clear_buffer", tag = "app.revanced.extension.shared.settings.preference.ClearLogBufferPreference", - selectable = true, + selectable = true ), NonInteractivePreference( "revanced_debug_feature_flags_manager", tag = "app.revanced.extension.shared.settings.preference.FeatureFlagsManagerPreference", - selectable = true, - ), + selectable = true + ) ) preferenceScreen.addPreferences( @@ -87,67 +102,117 @@ internal fun enableDebuggingPatch( key = "revanced_debug_screen", sorting = Sorting.UNSORTED, preferences = preferences, - ), + ) ) - // Hook the methods that look up if a feature flag is active. - experimentalFeatureFlagParentMethod.immutableClassDef.getExperimentalBooleanFeatureFlagMethod().apply { - findInstructionIndicesReversedOrThrow(Opcode.RETURN).forEach { index -> - val register = getInstruction(index).registerA + val experimentalBooleanFeatureFlagMethodMatch = + experimentalFeatureFlagUtilMethod.immutableClassDef.experimentalBooleanFeatureFlagMethodMatch + + experimentalBooleanFeatureFlagMethodMatch.let { + it.method.apply { + // Not enough registers in the method. Clone the method and use the + // original method as an intermediate to call extension code. + + + // Copy the method. + val helperMethod = cloneMutable(name = "patch_getBooleanFeatureFlag") + + // Add the method. + it.classDef.methods.add(helperMethod) addInstructions( - index, + 0, + """ + # Invoke the copied method (helper method). + invoke-static { p0, p1, p2, p3 }, $helperMethod + move-result p0 + + # Redefine boolean in the extension. + invoke-static { p0, p1, p2 }, $EXTENSION_CLASS_DESCRIPTOR->isBooleanFeatureFlagEnabled(ZJ)Z + move-result p0 + + # Since the copied method (helper method) has already been invoked, it just returns. + return p0 """ - invoke-static { v$register, p1 }, $EXTENSION_CLASS_DESCRIPTOR->isBooleanFeatureFlagEnabled(ZLjava/lang/Long;)Z - move-result v$register - """, ) } } - experimentalFeatureFlagParentMethod.immutableClassDef.getExperimentalDoubleFeatureFlagMethod().apply { - val insertIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT_WIDE) + if (hookDoubleFeatureFlag()) + // 21.06+ doesn't have enough registers and needs to also clone. + experimentalFeatureFlagUtilMethod.immutableClassDef.getExperimentalDoubleFeatureFlagMethod() + .cloneMutableAndPreserveParameters().apply { + val helperMethod = cloneMutable(name = "patch_getDoubleFeatureFlag") - addInstructions( - insertIndex, - """ - move-result-wide v0 # Also clobbers v1 (p0) since result is wide. - invoke-static/range { v0 .. v5 }, $EXTENSION_CLASS_DESCRIPTOR->isDoubleFeatureFlagEnabled(DJD)D - move-result-wide v0 - return-wide v0 - """, - ) - } + classDef.methods.add(helperMethod) - experimentalFeatureFlagParentMethod.immutableClassDef.getExperimentalLongFeatureFlagMethod().apply { - val insertIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT_WIDE) + addInstructions( + 0, + """ + # Invoke the copied method (helper method). + invoke-static/range { p0 .. p4 }, $helperMethod + move-result-wide v0 + + # Move parameter registers to lower register range to use invoke-static/range. + move-wide v2, p1 + move-wide v4, p3 - addInstructions( - insertIndex, - """ - move-result-wide v0 - invoke-static/range { v0 .. v5 }, $EXTENSION_CLASS_DESCRIPTOR->isLongFeatureFlagEnabled(JJJ)J - move-result-wide v0 - return-wide v0 - """, - ) - } + invoke-static/range { v0 .. v5 }, ${EXTENSION_CLASS_DESCRIPTOR}->isDoubleFeatureFlagEnabled(DJD)D + move-result-wide v0 - if (hookStringFeatureFlag) { - experimentalFeatureFlagParentMethod.immutableClassDef.getExperimentalStringFeatureFlagMethod().apply { - val insertIndex = indexOfFirstInstructionReversedOrThrow(Opcode.MOVE_RESULT_OBJECT) - - addInstructions( - insertIndex, + # Since the copied method (helper method) has already been invoked, it just returns. + return-wide v0 """ - move-result-object v0 - invoke-static { v0, p1, p2, p3 }, $EXTENSION_CLASS_DESCRIPTOR->isStringFeatureFlagEnabled(Ljava/lang/String;JLjava/lang/String;)Ljava/lang/String; - move-result-object v0 - return-object v0 - """, - ) - } - } + ) + } + + if (hookLongFeatureFlag()) + experimentalFeatureFlagUtilMethod.immutableClassDef.getExperimentalLongFeatureFlagMethod() + .cloneMutableAndPreserveParameters().apply { + val helperMethod = cloneMutable(name = "patch_getLongFeatureFlag") + + classDef.methods.add(helperMethod) + + addInstructions( + 0, + """ + # Invoke the copied method (helper method). + invoke-static/range { p0 .. p4 }, $helperMethod + move-result-wide v0 + + # Move parameter registers to lower register range to use invoke-static/range. + move-wide v2, p1 + move-wide v4, p3 + + invoke-static/range { v0 .. v5 }, ${EXTENSION_CLASS_DESCRIPTOR}->isLongFeatureFlagEnabled(JJJ)J + move-result-wide v0 + + # Since the copied method (helper method) has already been invoked, it just returns. + return-wide v0 + """ + ) + } + + if (hookStringFeatureFlag()) + experimentalFeatureFlagUtilMethod.immutableClassDef.getExperimentalStringFeatureFlagMethod() + .apply { + val helperMethod = cloneMutable(name = "patch_getStringFeatureFlag") + + classDef.methods.add(helperMethod) + + addInstructions( + 0, + """ + invoke-static { p0, p1, p2, p3 }, $helperMethod + move-result-object p0 + + invoke-static { p0, p1, p2, p3 }, ${EXTENSION_CLASS_DESCRIPTOR}->isStringFeatureFlagEnabled(Ljava/lang/String;JLjava/lang/String;)Ljava/lang/String; + move-result-object p0 + + return-object p0 + """ + ) + } // There exists other experimental accessor methods for byte[] // and wrappers for obfuscated classes, but currently none of those are hooked. diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/debugging/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/debugging/Fingerprints.kt index 39611369a5..ec0b435316 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/debugging/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/debugging/Fingerprints.kt @@ -1,46 +1,55 @@ package app.revanced.patches.shared.misc.debugging +import app.revanced.patcher.ClassDefComposing import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively import app.revanced.patcher.firstMethodDeclaratively import app.revanced.patcher.accessFlags +import app.revanced.patcher.custom import app.revanced.patcher.parameterTypes import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.returnType import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.iface.ClassDef -internal val BytecodePatchContext.experimentalFeatureFlagParentMethod by gettingFirstImmutableMethodDeclaratively( +internal val BytecodePatchContext.experimentalFeatureFlagUtilMethod by gettingFirstImmutableMethodDeclaratively( "Unable to parse proto typed experiment flag: " ) { - accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) returnType("L") - parameterTypes("L", "J", "[B") + custom { + // 'public static' or 'public static final' + AccessFlags.STATIC.isSet(accessFlags) + && AccessFlags.PUBLIC.isSet(accessFlags) + // "L", "J", "[B" or "L", "J" + && parameters.let { (it.size == 2 || it.size == 3) && it[1].type == "J" } + } } -context(_: BytecodePatchContext) -internal fun ClassDef.getExperimentalBooleanFeatureFlagMethod() = firstMethodDeclaratively { - accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) +internal val ClassDef.experimentalBooleanFeatureFlagMethodMatch by ClassDefComposing.composingFirstMethod { returnType("Z") parameterTypes("L", "J", "Z") + custom { + // 'public static' or 'public static final' + AccessFlags.STATIC.isSet(accessFlags) && AccessFlags.PUBLIC.isSet(accessFlags) + } } context(_: BytecodePatchContext) internal fun ClassDef.getExperimentalDoubleFeatureFlagMethod() = firstMethodDeclaratively { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("D") - parameterTypes("J", "D") + parameterTypes("L", "J", "D") + custom { AccessFlags.STATIC.isSet(accessFlags) } } context(_: BytecodePatchContext) internal fun ClassDef.getExperimentalLongFeatureFlagMethod() = firstMethodDeclaratively { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("J") - parameterTypes("J", "J") + parameterTypes("L", "J", "J") + custom { AccessFlags.STATIC.isSet(accessFlags) } } context(_: BytecodePatchContext) internal fun ClassDef.getExperimentalStringFeatureFlagMethod() = firstMethodDeclaratively { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("Ljava/lang/String;") - parameterTypes("J", "Ljava/lang/String;") + parameterTypes("L", "J", "Ljava/lang/String;") + custom { AccessFlags.STATIC.isSet(accessFlags) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/Fingerprints.kt index 8018c9bb66..d0990f4620 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/Fingerprints.kt @@ -3,6 +3,7 @@ package app.revanced.patches.shared.misc.gms import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.iface.ClassDef internal val BytecodePatchContext.googlePlayUtilityMethod by gettingFirstMethodDeclarativelyOrNull( "This should never happen.", diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/GmsCoreSupportPatch.kt index 1ab994b61c..97d2931f40 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/GmsCoreSupportPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/GmsCoreSupportPatch.kt @@ -14,6 +14,9 @@ import app.revanced.patcher.patch.ResourcePatchContext import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch import app.revanced.patcher.patch.stringOption +import app.revanced.patcher.firstClassDef +import app.revanced.patcher.firstImmutableClassDef +import app.revanced.patcher.patch.* import app.revanced.patches.all.misc.packagename.changePackageNamePatch import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName import app.revanced.patches.all.misc.resources.addResources diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/hex/HexPatchBuilder.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/hex/HexPatchBuilder.kt index 98269bd356..ad259fcc23 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/hex/HexPatchBuilder.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/hex/HexPatchBuilder.kt @@ -95,18 +95,15 @@ class Replacement( */ private fun indexOfPatternIn(haystack: ByteArray): Int { val needle = bytes - - val haystackLength = haystack.size - 1 - val needleLength = needle.size - 1 val right = IntArray(256) { -1 } - for (i in 0 until needleLength) right[needle[i].toInt().and(0xFF)] = i + for (i in 0 until needle.size) right[needle[i].toInt().and(0xFF)] = i var skip: Int - for (i in 0..haystackLength - needleLength) { + for (i in 0..haystack.size - needle.size) { skip = 0 - for (j in needleLength - 1 downTo 0) { + for (j in needle.size - 1 downTo 0) { if (needle[j] != haystack[i + j]) { skip = max(1, j - right[haystack[i + j].toInt().and(0xFF)]) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/Fingerprints.kt index 1c3292bdea..9e128382e8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/Fingerprints.kt @@ -4,7 +4,32 @@ import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +internal val BytecodePatchContext.accessibilityIdMethodMatch by composingFirstMethod { + instructions( + allOf( + Opcode.INVOKE_INTERFACE(), + method { parameterTypes.isEmpty() && returnType == "Ljava/lang/String;" } + ), + afterAtMost(5, "primary_image"()), + ) +} + +internal fun BytecodePatchContext.getAccessibilityTextMethodMatch(accessibilityIdMethod: MethodReference) = firstMethodComposite { + returnType("V") + custom { + // 'public final synthetic' or 'public final bridge synthetic'. + AccessFlags.SYNTHETIC.isSet(accessFlags) + } + instructions( + allOf( + Opcode.INVOKE_INTERFACE(), + method { parameterTypes.isEmpty() && returnType == "Ljava/lang/String;" } + ), + afterAtMost(5, method { this == accessibilityIdMethod }) + ) +} internal val BytecodePatchContext.lithoFilterInitMethod by gettingFirstMethodDeclaratively { definingClass("/LithoFilterPatch;") accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) @@ -40,10 +65,6 @@ internal val BytecodePatchContext.protobufBufferReferenceLegacyMethod by getting opcodes(Opcode.IPUT, Opcode.INVOKE_VIRTUAL, Opcode.MOVE_RESULT, Opcode.SUB_INT_2ADDR) } -internal val BytecodePatchContext.componentContextParserMethodMatch by composingFirstMethod { - instructions("Number of bits must be positive"()) -} - internal val BytecodePatchContext.emptyComponentMethod by gettingFirstImmutableMethodDeclaratively { accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR) parameterTypes() diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/LithoFilterPatch.kt index fcc830c95c..29548aa469 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/LithoFilterPatch.kt @@ -2,22 +2,36 @@ package app.revanced.patches.shared.misc.litho.filter +import app.revanced.util.getFreeRegisterProvider import app.revanced.com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable +import app.revanced.patcher.afterAtMost +import app.revanced.patcher.allOf import app.revanced.patcher.classDef +import app.revanced.patcher.custom import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.methodReference import app.revanced.patcher.extensions.removeInstructions -import app.revanced.patcher.extensions.replaceInstruction +import app.revanced.patcher.extensions.typeReference import app.revanced.patcher.firstImmutableClassDef +import app.revanced.patcher.firstMethodComposite import app.revanced.patcher.immutableClassDef +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke +import app.revanced.patcher.method import app.revanced.patcher.patch.BytecodePatchBuilder import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.returnType import app.revanced.patches.shared.misc.extension.sharedExtensionPatch import app.revanced.util.addInstructionsAtControlFlowLabel -import app.revanced.util.findFreeRegister import app.revanced.util.findFieldFromToString +import app.revanced.util.indexOfFirstInstructionReversedOrThrow import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.immutable.value.ImmutableBooleanEncodedValue /** @@ -156,29 +170,67 @@ internal fun lithoFilterPatch( type == builderMethodDescriptor.returnType }.fields.single() - // Match all component creations methods + // Find the method call that gets the value of 'buttonViewModel.accessibilityId'. + val accessibilityIdMethod = accessibilityIdMethodMatch.let { + it.immutableMethod.getInstruction(it[0]).methodReference!! + } + + // There's a method in the same class that gets the value of 'buttonViewModel.accessibilityText'. + // As this class is abstract, another method that uses a method call is used. + val accessibilityTextMethod = getAccessibilityTextMethodMatch(accessibilityIdMethod).let { + // Find the method call that gets the value of 'buttonViewModel.accessibilityText'. + it.method.getInstruction(it[0]).methodReference + } + componentCreateMethod.apply { val insertIndex = componentCreateInsertionIndex() - val freeRegister = findFreeRegister(insertIndex) - val identifierRegister = findFreeRegister(insertIndex, freeRegister) - val pathRegister = findFreeRegister(insertIndex, freeRegister, identifierRegister) + + // Directly access the class related with the buttonViewModel from this method. + // This is within 10 lines of insertIndex. + val buttonViewModelIndex = indexOfFirstInstructionReversedOrThrow(insertIndex) { + opcode == Opcode.CHECK_CAST && + typeReference?.type == accessibilityIdMethod.definingClass + } + val buttonViewModelRegister = + getInstruction(buttonViewModelIndex).registerA + val accessibilityIdIndex = buttonViewModelIndex + 2 + + // This is an index that checks if there is accessibility-related text. + // This is within 10 lines of buttonViewModelIndex. + val nullCheckIndex = indexOfFirstInstructionReversedOrThrow( + buttonViewModelIndex, Opcode.IF_EQZ + ) + + val registerProvider = getFreeRegisterProvider( + insertIndex, 3, buttonViewModelRegister + ) + val freeRegister = registerProvider.getFreeRegister() + val identifierRegister = registerProvider.getFreeRegister() + val pathRegister = registerProvider.getFreeRegister() + + // Find a free register to store the accessibilityId and accessibilityText. + // This is before the insertion index. + val accessibilityRegisterProvider = getFreeRegisterProvider( + nullCheckIndex, + 2, + registerProvider.getUsedAndUnAvailableRegisters() + ) + val accessibilityIdRegister = accessibilityRegisterProvider.getFreeRegister() + val accessibilityTextRegister = accessibilityRegisterProvider.getFreeRegister() addInstructionsAtControlFlowLabel( insertIndex, """ move-object/from16 v$freeRegister, p2 # ConversionContext parameter - # In YT 20.41 the field is the abstract superclass. - # Check it's the actual ConversionContext just in case. + # In YouTube 20.41 the field is the abstract superclass. + # Verify it's the expected subclass just in case. instance-of v$identifierRegister, v$freeRegister, ${conversionContextToStringMethod.immutableClassDef.type} if-eqz v$identifierRegister, :unfiltered - # Get identifier and path from ConversionContext iget-object v$identifierRegister, v$freeRegister, $conversionContextIdentifierField iget-object v$pathRegister, v$freeRegister, $conversionContextPathBuilderField - - # Check if the component should be filtered. - invoke-static { v$identifierRegister, v$pathRegister }, $EXTENSION_CLASS_DESCRIPTOR->isFiltered(Ljava/lang/String;Ljava/lang/StringBuilder;)Z + invoke-static { v$identifierRegister, v$accessibilityIdRegister, v$accessibilityTextRegister, v$pathRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->isFiltered(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/StringBuilder;)Z move-result v$freeRegister if-eqz v$freeRegister, :unfiltered @@ -191,7 +243,31 @@ internal fun lithoFilterPatch( :unfiltered nop - """, + """ + ) + + // If there is text related to accessibility, get the accessibilityId and accessibilityText. + addInstructions( + accessibilityIdIndex, + """ + # Get accessibilityId + invoke-interface { v$buttonViewModelRegister }, $accessibilityIdMethod + move-result-object v$accessibilityIdRegister + + # Get accessibilityText + invoke-interface { v$buttonViewModelRegister }, $accessibilityTextMethod + move-result-object v$accessibilityTextRegister + """ + ) + + // If there is no accessibility-related text, + // both accessibilityId and accessibilityText use empty values. + addInstructions( + nullCheckIndex, + """ + const-string v$accessibilityIdRegister, "" + const-string v$accessibilityTextRegister, "" + """ ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/Fingerprints.kt index bd7a941b7f..7407326eef 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/Fingerprints.kt @@ -1,27 +1,25 @@ package app.revanced.patches.shared.misc.settings import app.revanced.patcher.accessFlags -import app.revanced.patcher.definingClass -import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively -import app.revanced.patcher.gettingFirstMethodDeclaratively +import app.revanced.patcher.firstMethodDeclaratively import app.revanced.patcher.name import app.revanced.patcher.parameterTypes import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.returnType -import app.revanced.patches.shared.misc.extension.EXTENSION_CLASS_DESCRIPTOR import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.iface.ClassDef -internal val BytecodePatchContext.themeLightColorResourceNameMethod by gettingFirstMethodDeclaratively { +context(_: BytecodePatchContext) +internal fun ClassDef.getThemeLightColorResourceNameMethod() = firstMethodDeclaratively { name("getThemeLightColorResourceName") - definingClass(EXTENSION_CLASS_DESCRIPTOR) accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC) returnType("Ljava/lang/String;") parameterTypes() } -internal val BytecodePatchContext.themeDarkColorResourceNameMethod by gettingFirstMethodDeclaratively { +context(_: BytecodePatchContext) +internal fun ClassDef.getThemeDarkColorResourceNameMethod() = firstMethodDeclaratively { name("getThemeDarkColorResourceName") - definingClass(EXTENSION_CLASS_DESCRIPTOR) accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC) returnType("Ljava/lang/String;") parameterTypes() diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/SettingsPatch.kt index efcb89c14b..52f14ae02e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/SettingsPatch.kt @@ -1,11 +1,13 @@ package app.revanced.patches.shared.misc.settings +import app.revanced.patcher.firstImmutableClassDef import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch import app.revanced.patches.all.misc.resources.addResource import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.extension.EXTENSION_CLASS_DESCRIPTOR import app.revanced.patches.shared.layout.branding.addBrandLicensePatch import app.revanced.patches.shared.misc.settings.preference.BasePreference import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory @@ -22,8 +24,8 @@ private var darkThemeColor: String? = null /** * Sets the default theme colors used in various ReVanced specific settings menus. - * By default these colors are white and black, but instead can be set to the - * same color the target app uses for it's own settings. + * By default, these colors are white and black, but instead can be set to the + * same color the target app uses for its own settings. */ fun overrideThemeColors(lightThemeColorString: String?, darkThemeColorString: String) { lightThemeColor = lightThemeColorString @@ -32,11 +34,12 @@ fun overrideThemeColors(lightThemeColorString: String?, darkThemeColorString: St private val settingsColorPatch = bytecodePatch { afterDependents { + val extensionClassDef = firstImmutableClassDef(EXTENSION_CLASS_DESCRIPTOR) if (lightThemeColor != null) { - themeLightColorResourceNameMethod.returnEarly(lightThemeColor!!) + extensionClassDef.getThemeLightColorResourceNameMethod().returnEarly(lightThemeColor!!) } if (darkThemeColor != null) { - themeDarkColorResourceNameMethod.returnEarly(darkThemeColor!!) + extensionClassDef.getThemeDarkColorResourceNameMethod().returnEarly(darkThemeColor!!) } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt index 23fec8ebc0..ba7faa001b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt @@ -28,7 +28,7 @@ internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/shared/spoof/SpoofVideoStreamsPatch;" private lateinit var buildRequestMethod: MutableMethod -private var buildRequestMethodUrlRegister = -1 +private var buildRequestMethodURLRegister = -1 internal fun spoofVideoStreamsPatch( extensionClassDescriptor: String, @@ -102,14 +102,14 @@ internal fun spoofVideoStreamsPatch( buildRequestMethod = this val newRequestBuilderIndex = buildRequestMethodMatch[0] - buildRequestMethodUrlRegister = getInstruction(newRequestBuilderIndex).registerD - val freeRegister = findFreeRegister(newRequestBuilderIndex, buildRequestMethodUrlRegister) + buildRequestMethodURLRegister = getInstruction(newRequestBuilderIndex).registerD + val freeRegister = findFreeRegister(newRequestBuilderIndex, buildRequestMethodURLRegister) addInstructions( newRequestBuilderIndex, """ move-object v$freeRegister, p1 - invoke-static { v$buildRequestMethodUrlRegister, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->fetchStreams(Ljava/lang/String;Ljava/util/Map;)V + invoke-static { v$buildRequestMethodURLRegister, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->fetchStreams(Ljava/lang/String;Ljava/util/Map;)V """, ) } @@ -164,7 +164,7 @@ internal fun spoofVideoStreamsPatch( move-result v0 if-eqz v0, :disabled - # Get video id. + # Get video ID. iget-object v2, p1, $videoDetailsClass->c:Ljava/lang/String; if-eqz v2, :disabled @@ -202,15 +202,15 @@ internal fun spoofVideoStreamsPatch( addInstructions( insertIndex, """ - invoke-static { v$buildRequestMethodUrlRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetAttRequest(Ljava/lang/String;)Ljava/lang/String; - move-result-object v$buildRequestMethodUrlRegister + invoke-static { v$buildRequestMethodURLRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetAttRequest(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$buildRequestMethodURLRegister """, ) } // endregion - // region Remove /videoplayback request body to fix playback. + // region Remove video playback request body to fix playback. // It is assumed, YouTube makes a request with a body tuned for Android. // Requesting streams intended for other platforms with a body tuned for Android could be the cause of 400 errors. // A proper fix may include modifying the request body to match the platforms expected body. diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/Fingerprints.kt index d1fa2a873b..6258569ad7 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/Fingerprints.kt @@ -2,23 +2,72 @@ package app.revanced.patches.youtube.ad.general import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext -import app.revanced.util.containsLiteralInstruction -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstructionReversed +import app.revanced.patches.shared.misc.mapping.ResourceType import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.iface.Method -import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.Opcode -internal val BytecodePatchContext.fullScreenEngagementAdContainerMethod by gettingFirstMethodDeclaratively { +internal val BytecodePatchContext.fullScreenEngagementAdContainerMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") parameterTypes() - custom { - containsLiteralInstruction(fullScreenEngagementAdContainer) && - indexOfAddListInstruction(this) >= 0 - } + instructions( + ResourceType.ID("fullscreen_engagement_ad_container"), + Opcode.IGET_BOOLEAN(), + allOf( + Opcode.INVOKE_VIRTUAL(), + method { + name == "add" && returnType == "Z" + && parameterTypes.size == 1 && parameterTypes[0] == "Ljava/lang/Object;" + } + ), + allOf( + Opcode.INVOKE_VIRTUAL(), + method { + name == "add" && returnType == "Z" + && parameterTypes.size == 1 && parameterTypes[0] == "Ljava/lang/Object;" + } + ), + allOf( + Opcode.INVOKE_VIRTUAL(), + method { name == "size" && returnType == "I" && parameterTypes.isEmpty() } + ), + ) } -internal fun indexOfAddListInstruction(method: Method) = method.indexOfFirstInstructionReversed { - getReference()?.name == "add" +internal val BytecodePatchContext.getPremiumViewMethodMatch by composingFirstMethod { + name("onMeasure") + definingClass("Lcom/google/android/apps/youtube/app/red/presenter/CompactYpcOfferModuleView;") + accessFlags(AccessFlags.PROTECTED, AccessFlags.FINAL) + returnType("V") + parameterTypes("I", "I") + opcodes( + Opcode.ADD_INT_2ADDR, + Opcode.ADD_INT_2ADDR, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ) } + +internal val BytecodePatchContext.lithoDialogBuilderMethodMatch by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes("[B", "L") + instructions( + allOf(Opcode.INVOKE_VIRTUAL(), method("show")), + ResourceType.STYLE("SlidingDialogAnimation") + ) +} + + +internal val BytecodePatchContext.playerOverlayTimelyShelfMethod by gettingFirstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes("Ljava/lang/Object;") + instructions( + "player_overlay_timely_shelf"(), + "innertube_cue_range"(), + "Null id"(), + "Null onExitActions"() + ) +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt index fb9b6756bf..c9239da9ba 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt @@ -1,10 +1,12 @@ package app.revanced.patches.youtube.ad.general +import app.revanced.patcher.extensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.fieldReference import app.revanced.patcher.extensions.getInstruction import app.revanced.patcher.extensions.instructions +import app.revanced.patcher.extensions.methodReference import app.revanced.patcher.extensions.replaceInstruction import app.revanced.patcher.extensions.wideLiteral -import app.revanced.patcher.firstMethod import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch import app.revanced.patches.all.misc.resources.addResources @@ -13,23 +15,32 @@ import app.revanced.patches.shared.misc.fix.verticalscroll.verticalScrollPatch import app.revanced.patches.shared.misc.mapping.ResourceType import app.revanced.patches.shared.misc.mapping.resourceMappingPatch import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.contexthook.Endpoint +import app.revanced.patches.youtube.misc.contexthook.addOSNameHook import app.revanced.patches.shared.misc.litho.filter.addLithoFilter -import app.revanced.patches.youtube.ad.getpremium.hideGetPremiumPatch +import app.revanced.patches.youtube.misc.contexthook.hookClientContextPatch +import app.revanced.patches.youtube.misc.engagement.addEngagementPanelIdHook +import app.revanced.patches.youtube.misc.engagement.engagementPanelHookPatch import app.revanced.patches.youtube.misc.fix.backtoexitgesture.fixBackToExitGesturePatch import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.revanced.patches.youtube.misc.playservice.is_20_14_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.addInstructionsAtControlFlowLabel +import app.revanced.util.findFreeRegister import app.revanced.util.forEachInstructionAsSequence +import app.revanced.util.indexOfFirstInstructionReversedOrThrow import app.revanced.util.injectHideViewCall import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/litho/AdsFilter;" internal var adAttributionId = -1L private set -internal var fullScreenEngagementAdContainer = -1L - private set - -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/litho/AdsFilter;" private val hideAdsResourcePatch = resourcePatch { dependsOn( @@ -37,28 +48,30 @@ private val hideAdsResourcePatch = resourcePatch { settingsPatch, resourceMappingPatch, addResourcesPatch, + hookClientContextPatch, + engagementPanelHookPatch, ) apply { addResources("youtube", "ad.general.hideAdsResourcePatch") PreferenceScreen.ADS.addPreferences( - SwitchPreference("revanced_hide_creator_store_shelf"), SwitchPreference("revanced_hide_end_screen_store_banner"), SwitchPreference("revanced_hide_fullscreen_ads"), SwitchPreference("revanced_hide_general_ads"), SwitchPreference("revanced_hide_merchandise_banners"), SwitchPreference("revanced_hide_paid_promotion_label"), + SwitchPreference("revanced_hide_player_popup_ads"), SwitchPreference("revanced_hide_self_sponsor_ads"), SwitchPreference("revanced_hide_shopping_links"), SwitchPreference("revanced_hide_view_products_banner"), - SwitchPreference("revanced_hide_web_search_results"), + SwitchPreference("revanced_hide_youtube_premium_promotions") ) addLithoFilter("Lapp/revanced/extension/youtube/patches/litho/AdsFilter;") + addEngagementPanelIdHook("$EXTENSION_CLASS_DESCRIPTOR->hidePlayerPopupAds(Ljava/lang/String;)Z") adAttributionId = ResourceType.ID["ad_attribution"] - fullScreenEngagementAdContainer = ResourceType.ID["fullscreen_engagement_ad_container"] } } @@ -68,35 +81,109 @@ val hideAdsPatch = bytecodePatch( description = "Adds options to remove general ads.", ) { dependsOn( - hideGetPremiumPatch, hideAdsResourcePatch, verticalScrollPatch, fixBackToExitGesturePatch, + versionCheckPatch ) compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) apply { + // Hide fullscreen ad + + lithoDialogBuilderMethodMatch.let { + it.method.apply { + // Find the class name of the custom dialog + val dialogClass = getInstruction(it[0]).methodReference!!.definingClass + + // The dialog can be closed after dialog.show(), + // and it is better to close the dialog after the layout of the dialog has changed + val insertIndex = indexOfFirstInstructionReversedOrThrow { + opcode == Opcode.IPUT_OBJECT && fieldReference?.type == dialogClass + } + val insertRegister = + getInstruction(insertIndex).registerA + val freeRegister = findFreeRegister(insertIndex, insertRegister) + + addInstructionsAtControlFlowLabel( + insertIndex, + """ + move-object/from16 v$freeRegister, p1 + invoke-static { v$insertRegister, v$freeRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->closeFullscreenAd(Ljava/lang/Object;[B)V + """ + ) + } + } + + // Hide get premium + + getPremiumViewMethodMatch.method.apply { + val startIndex = getPremiumViewMethodMatch[0] + val measuredWidthRegister = getInstruction(startIndex).registerA + val measuredHeightInstruction = getInstruction(startIndex + 1) + + val measuredHeightRegister = measuredHeightInstruction.registerA + val tempRegister = measuredHeightInstruction.registerB + + addInstructionsWithLabels( + startIndex + 2, + """ + # Override the internal measurement of the layout with zero values. + invoke-static {}, ${EXTENSION_CLASS_DESCRIPTOR}->hideGetPremiumView()Z + move-result v$tempRegister + if-eqz v$tempRegister, :allow + const/4 v$measuredWidthRegister, 0x0 + const/4 v$measuredHeightRegister, 0x0 + :allow + nop + # Layout width/height is then passed to a protected class method. + """, + ) + } + + // Hide player overlay view. This can be hidden with a regular litho filter + // but an empty space remains. + if (is_20_14_or_greater) { + playerOverlayTimelyShelfMethod.addInstructionsWithLabels( + 0, + """ + invoke-static {}, ${EXTENSION_CLASS_DESCRIPTOR}->hideAds()Z + move-result v0 + if-eqz v0, :show + return-void + :show + nop + """ + ) + } + + // Hide end screen store banner. - fullScreenEngagementAdContainerMethod.apply { - val addListIndex = indexOfAddListInstruction(this) - val addListInstruction = getInstruction(addListIndex) - val listRegister = addListInstruction.registerC - val objectRegister = addListInstruction.registerD + fullScreenEngagementAdContainerMethodMatch.let { + it.method.apply { + val insertIndex = it[3] + val insertInstruction = getInstruction(insertIndex) + val listRegister = insertInstruction.registerC + val objectRegister = insertInstruction.registerD - replaceInstruction( - addListIndex, - "invoke-static { v$listRegister, v$objectRegister }, $EXTENSION_CLASS_DESCRIPTOR" + - "->hideEndScreenStoreBanner(Ljava/util/List;Ljava/lang/Object;)V", - ) + replaceInstruction( + insertIndex, + "invoke-static { v$listRegister, v$objectRegister }, " + + "${EXTENSION_CLASS_DESCRIPTOR}->" + + "hideEndScreenStoreBanner(Ljava/util/List;Ljava/lang/Object;)V" + ) + } } // Hide ad views. @@ -114,7 +201,22 @@ val hideAdsPatch = bytecodePatch( return@forEachInstructionAsSequence insertIndex to viewRegister }) { method, (insertIndex, viewRegister) -> - method.injectHideViewCall(insertIndex, viewRegister, EXTENSION_CLASS_DESCRIPTOR, "hideAdAttributionView") + method.injectHideViewCall( + insertIndex, + viewRegister, + EXTENSION_CLASS_DESCRIPTOR, + "hideAdAttributionView" + ) + } + + setOf( + Endpoint.BROWSE, + Endpoint.SEARCH, + ).forEach { endpoint -> + addOSNameHook( + endpoint, + "$EXTENSION_CLASS_DESCRIPTOR->hideAds(Ljava/lang/String;)Ljava/lang/String;", + ) } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/Fingerprints.kt deleted file mode 100644 index b8ce3bbb24..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/Fingerprints.kt +++ /dev/null @@ -1,20 +0,0 @@ -package app.revanced.patches.youtube.ad.getpremium - -import app.revanced.patcher.* -import app.revanced.patcher.patch.BytecodePatchContext -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -internal val BytecodePatchContext.getPremiumViewMethodMatch by composingFirstMethod { - name("onMeasure") - definingClass("Lcom/google/android/apps/youtube/app/red/presenter/CompactYpcOfferModuleView;") - accessFlags(AccessFlags.PROTECTED, AccessFlags.FINAL) - returnType("V") - parameterTypes("I", "I") - opcodes( - Opcode.ADD_INT_2ADDR, - Opcode.ADD_INT_2ADDR, - Opcode.INVOKE_VIRTUAL, - Opcode.RETURN_VOID, - ) -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatch.kt deleted file mode 100644 index 3b19542202..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatch.kt +++ /dev/null @@ -1,66 +0,0 @@ -package app.revanced.patches.youtube.ad.getpremium - -import app.revanced.patcher.extensions.addInstructionsWithLabels -import app.revanced.patcher.extensions.getInstruction -import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patches.all.misc.resources.addResources -import app.revanced.patches.all.misc.resources.addResourcesPatch -import app.revanced.patches.shared.misc.settings.preference.SwitchPreference -import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch -import app.revanced.patches.youtube.misc.settings.PreferenceScreen -import app.revanced.patches.youtube.misc.settings.settingsPatch -import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction - -private const val EXTENSION_CLASS_DESCRIPTOR = - "Lapp/revanced/extension/youtube/patches/HideGetPremiumPatch;" - -val hideGetPremiumPatch = bytecodePatch( - description = "Hides YouTube Premium signup promotions under the video player.", -) { - dependsOn( - sharedExtensionPatch, - settingsPatch, - addResourcesPatch, - ) - - compatibleWith( - "com.google.android.youtube"( - "19.43.41", - "20.14.43", - "20.21.37", - "20.31.40", - ), - ) - - apply { - addResources("youtube", "ad.getpremium.hideGetPremiumPatch") - - PreferenceScreen.ADS.addPreferences( - SwitchPreference("revanced_hide_get_premium"), - ) - - getPremiumViewMethodMatch.let { - val startIndex = it[0] - val measuredWidthRegister = it.method.getInstruction(startIndex).registerA - val measuredHeightInstruction = it.method.getInstruction(startIndex + 1) - - val measuredHeightRegister = measuredHeightInstruction.registerA - val tempRegister = measuredHeightInstruction.registerB - - it.method.addInstructionsWithLabels( - startIndex + 2, - """ - # Override the internal measurement of the layout with zero values. - invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->hideGetPremiumView()Z - move-result v$tempRegister - if-eqz v$tempRegister, :allow - const/4 v$measuredWidthRegister, 0x0 - const/4 v$measuredHeightRegister, 0x0 - :allow - nop - # Layout width/height is then passed to a protected class method. - """, - ) - } - } -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/VideoAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/VideoAdsPatch.kt index 5ad8b5da75..f6d244f353 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/VideoAdsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/VideoAdsPatch.kt @@ -24,10 +24,12 @@ val videoAdsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoUrlPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoURLPatch.kt similarity index 86% rename from patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoUrlPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoURLPatch.kt index 18656fa5c4..a93b0a4788 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoUrlPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoURLPatch.kt @@ -12,7 +12,7 @@ import app.revanced.patches.youtube.video.information.videoInformationPatch import app.revanced.util.ResourceGroup import app.revanced.util.copyResources -private val copyVideoUrlResourcePatch = resourcePatch { +private val copyVideoURLResourcePatch = resourcePatch { dependsOn( settingsPatch, playerControlsPatch, @@ -20,7 +20,7 @@ private val copyVideoUrlResourcePatch = resourcePatch { ) apply { - addResources("youtube", "interaction.copyvideourl.copyVideoUrlResourcePatch") + addResources("youtube", "interaction.copyvideourl.copyVideoURLResourcePatch") PreferenceScreen.PLAYER.addPreferences( SwitchPreference("revanced_copy_video_url"), @@ -46,25 +46,27 @@ val copyVideoURLPatch = bytecodePatch( description = "Adds options to display buttons in the video player to copy video URLs.", ) { dependsOn( - copyVideoUrlResourcePatch, + copyVideoURLResourcePatch, playerControlsPatch, videoInformationPatch, ) compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) apply { val extensionPlayerPackage = "Lapp/revanced/extension/youtube/videoplayer" val buttonsDescriptors = listOf( - "$extensionPlayerPackage/CopyVideoUrlButton;", - "$extensionPlayerPackage/CopyVideoUrlTimestampButton;", + "$extensionPlayerPackage/CopyVideoURLButton;", + "$extensionPlayerPackage/CopyVideoURLTimestampButton;", ) buttonsDescriptors.forEach { descriptor -> diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/Fingerprints.kt index fa5c83ebe5..9de6b23d66 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/Fingerprints.kt @@ -2,6 +2,8 @@ package app.revanced.patches.youtube.interaction.dialog import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode internal val BytecodePatchContext.createDialogMethodMatch by composingFirstMethod { returnType("V") @@ -13,3 +15,28 @@ internal val BytecodePatchContext.createDialogMethodMatch by composingFirstMetho method { toString() == "Landroid/app/AlertDialog;->show()V" }, ) } + + +internal val BytecodePatchContext.createModernDialogMethodMatch by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes() + instructions( + Opcode.MOVE_RESULT(), + method { toString() == $$"Landroid/app/AlertDialog$Builder;->setIcon(I)Landroid/app/AlertDialog$Builder;" }, + method { toString() == $$"Landroid/app/AlertDialog$Builder;->create()Landroid/app/AlertDialog;" } + ) +} + +internal val BytecodePatchContext.playabilityStatusEnumMethod by gettingFirstImmutableMethod( + "OK", + "ERROR", + "UNPLAYABLE", + "LOGIN_REQUIRED", + "CONTENT_CHECK_REQUIRED", + "AGE_CHECK_REQUIRED", + "LIVE_STREAM_OFFLINE", + "FULLSCREEN_ONLY", + "GL_PLAYBACK_REQUIRED", + "AGE_VERIFICATION_REQUIRED", +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch.kt index c0d7e173e3..b57010b903 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch.kt @@ -1,15 +1,28 @@ package app.revanced.patches.youtube.interaction.dialog +import app.revanced.patcher.accessFlags +import app.revanced.patcher.custom +import app.revanced.patcher.extensions.addInstruction +import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.instructions import app.revanced.patcher.extensions.replaceInstructions +import app.revanced.patcher.firstMethodDeclaratively +import app.revanced.patcher.immutableClassDef +import app.revanced.patcher.instructions +import app.revanced.patcher.parameterTypes import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.returnType import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.backgroundPlaybackManagerShortsMethod +import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/RemoveViewerDiscretionDialogPatch;" @@ -18,7 +31,7 @@ private const val EXTENSION_CLASS_DESCRIPTOR = val removeViewerDiscretionDialogPatch = bytecodePatch( name = "Remove viewer discretion dialog", description = "Adds an option to remove the dialog that appears when opening a video that has been age-restricted " + - "by accepting it automatically. This does not bypass the age restriction.", + "by accepting it automatically. This does not bypass the age restriction.", ) { dependsOn( sharedExtensionPatch, @@ -28,10 +41,12 @@ val removeViewerDiscretionDialogPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -45,13 +60,56 @@ val removeViewerDiscretionDialogPatch = bytecodePatch( createDialogMethodMatch.let { it.method.apply { val showDialogIndex = it[-1] - val dialogRegister = getInstruction(showDialogIndex).registerC + val dialogRegister = + getInstruction(showDialogIndex).registerC replaceInstructions( showDialogIndex, - "invoke-static { v$dialogRegister }, $EXTENSION_CLASS_DESCRIPTOR->confirmDialog(Landroid/app/AlertDialog;)V", + "invoke-static { v$dialogRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->" + + "confirmDialog(Landroid/app/AlertDialog;)V", ) } } + + createModernDialogMethodMatch.let { + it.method.apply { + val showDialogIndex = it[-1] + val dialogRegister = + getInstruction(showDialogIndex).registerC + + replaceInstructions( + showDialogIndex, + "invoke-static { v$dialogRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->" + + "confirmDialog(Landroid/app/AlertDialog\$Builder;)Landroid/app/AlertDialog;", + ) + + val dialogStyleIndex = it[0] + val dialogStyleRegister = + getInstruction(dialogStyleIndex).registerA + + addInstructions( + dialogStyleIndex + 1, + """ + invoke-static { v$dialogStyleRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->disableModernDialog(Z)Z + move-result v$dialogStyleRegister + """ + ) + } + } + + backgroundPlaybackManagerShortsMethod.immutableClassDef.firstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returnType("Z") + parameterTypes(playabilityStatusEnumMethod.immutableClassDef.type) + custom { + // There's another similar method that's difficult to match uniquely, + // Instruction counter is used to identify the target method. + instructions.count() < 10 + } + }.addInstruction( + 0, + "invoke-static { p0 }, ${EXTENSION_CLASS_DESCRIPTOR}->" + + "setPlayabilityStatus(Ljava/lang/Enum;)V" + ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/AddMoreDoubleTapToSeekLengthOptionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/AddMoreDoubleTapToSeekLengthOptionsPatch.kt new file mode 100644 index 0000000000..b84afcb4d9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/AddMoreDoubleTapToSeekLengthOptionsPatch.kt @@ -0,0 +1,66 @@ +package app.revanced.patches.youtube.interaction.doubletap + +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.util.findElementByAttributeValueOrThrow +import app.revanced.util.removeFromParent +import org.w3c.dom.Element + +@Suppress("unused") +val addMoreDoubleTapToSeekLengthOptionsPatch = resourcePatch( + name = "Add more double tap to seek length options", +) { + dependsOn( + sharedExtensionPatch + ) + + compatibleWith( + "com.google.android.youtube"( + "20.14.43", + "20.21.37", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" + ) + ) + + execute { + // Values are hard coded to keep patching simple. + val doubleTapLengthOptionsString = "3, 5, 10, 15, 20, 30, 60, 120, 180, 240" + + val doubleTapLengths = doubleTapLengthOptionsString + .replace(" ", "") + .split(",") + if (doubleTapLengths.isEmpty()) throw PatchException("Invalid double-tap length elements") + + document("res/values/arrays.xml").use { document -> + fun Element.removeAllChildren() { + val children = childNodes // Calling childNodes creates a new list. + for (i in children.length - 1 downTo 0) { + children.item(i).removeFromParent() + } + } + + val values = document.childNodes.findElementByAttributeValueOrThrow( + attributeName = "name", + value = "double_tap_length_values" + ) + values.removeAllChildren() + + val entries = document.childNodes.findElementByAttributeValueOrThrow( + attributeName = "name", + value = "double_tap_length_entries" + ) + entries.removeAllChildren() + + doubleTapLengths.forEach { length -> + val item = document.createElement("item") + item.textContent = length + entries.appendChild(item) + values.appendChild(item.cloneNode(true)) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/DisableChapterSkipDoubleTapPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/DisableChapterSkipDoubleTapPatch.kt index adfbd1cdf9..fce736f556 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/DisableChapterSkipDoubleTapPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/DisableChapterSkipDoubleTapPatch.kt @@ -34,7 +34,10 @@ val disableDoubleTapActionsPatch = bytecodePatch( "com.google.android.youtube"( "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -77,12 +80,13 @@ val disableDoubleTapActionsPatch = bytecodePatch( """, ) - doubleTapInfoGetSeekSourceMethod.immutableClassDef.getDoubleTapInfoCtorMethod().addInstructions( - 0, - """ + doubleTapInfoGetSeekSourceMethod.immutableClassDef.getDoubleTapInfoCtorMethod() + .addInstructions( + 0, + """ invoke-static { p3 }, $EXTENSION_CLASS_DESCRIPTOR->disableDoubleTapChapters(Z)Z move-result p3 """, - ) + ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt index c1d1cdcbc4..337dea4666 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -55,15 +55,17 @@ private val downloadsResourcePatch = resourcePatch { } } -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/DownloadsPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/DownloadsPatch;" -internal const val BUTTON_DESCRIPTOR = "Lapp/revanced/extension/youtube/videoplayer/ExternalDownloadButton;" +internal const val BUTTON_DESCRIPTOR = + "Lapp/revanced/extension/youtube/videoplayer/ExternalDownloadButton;" @Suppress("unused") val downloadsPatch = bytecodePatch( name = "Downloads", description = "Adds support to download videos with an external downloader app " + - "using the in-app download button or a video player action button.", + "using the in-app download button or a video player action button.", ) { dependsOn( downloadsResourcePatch, @@ -73,10 +75,12 @@ val downloadsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/Fingerprints.kt index 1eac7848a8..ee7276d2a9 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/Fingerprints.kt @@ -10,7 +10,7 @@ internal val BytecodePatchContext.offlineVideoEndpointMethod by gettingFirstMeth parameterTypes( "Ljava/util/Map;", "L", - "Ljava/lang/String", // VideoId + "Ljava/lang/String", // Video ID "L", ) instructions( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatch.kt new file mode 100644 index 0000000000..9084e593d9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatch.kt @@ -0,0 +1,139 @@ +package app.revanced.patches.youtube.interaction.hapticfeedback + +import app.revanced.patcher.extensions.ExternalLabel +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.fieldReference +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.all.misc.transformation.IMethodCall +import app.revanced.patches.all.misc.transformation.filterMapInstruction35c +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR_PREFIX = + "Lapp/revanced/extension/youtube/patches/DisableHapticFeedbackPatch" + +private const val EXTENSION_CLASS_DESCRIPTOR = "$EXTENSION_CLASS_DESCRIPTOR_PREFIX;" + +@Suppress("unused") +val disableHapticFeedbackPatch = bytecodePatch( + name = "Disable haptic feedback", + description = "Adds an option to disable haptic feedback in the player for various actions.", +) { + dependsOn( + settingsPatch, + addResourcesPatch, + transformInstructionsPatch( + filterMap = { classDef, _, instruction, instructionIndex -> + filterMapInstruction35c( + EXTENSION_CLASS_DESCRIPTOR_PREFIX, + classDef, + instruction, + instructionIndex, + ) + }, + transform = { method, entry -> + val (methodType, instruction, instructionIndex) = entry + methodType.replaceInvokeVirtualWithExtension( + EXTENSION_CLASS_DESCRIPTOR, + method, + instruction, + instructionIndex, + ) + }, + ), + ) + + compatibleWith( + "com.google.android.youtube"( + "20.14.43", + "20.21.37", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" + ), + ) + + apply { + addResources("youtube", "misc.hapticfeedback.disableHapticFeedbackPatch") + + PreferenceScreen.PLAYER.addPreferences( + PreferenceScreenPreference( + "revanced_disable_haptic_feedback", + preferences = setOf( + SwitchPreference("revanced_disable_haptic_feedback_chapters"), + SwitchPreference("revanced_disable_haptic_feedback_precise_seeking"), + SwitchPreference("revanced_disable_haptic_feedback_seek_undo"), + SwitchPreference("revanced_disable_haptic_feedback_tap_and_hold"), + SwitchPreference("revanced_disable_haptic_feedback_zoom"), + ), + ), + ) + + arrayOf( + markerHapticsMethod to "disableChapterVibrate", + scrubbingHapticsMethod to "disablePreciseSeekingVibrate", + seekUndoHapticsMethod to "disableSeekUndoVibrate", + zoomHapticsMethod to "disableZoomVibrate", + ).forEach { (method, methodName) -> + method.addInstructionsWithLabels( + 0, + """ + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->$methodName()Z + move-result v0 + if-eqz v0, :vibrate + return-void + """, + ExternalLabel("vibrate", method.getInstruction(0)), + ) + } + + val vibratorField = tapAndHoldHapticsHandlerMethodMatch.let { + it.immutableMethod.getInstruction(it[-1]).fieldReference!! + } + // Function, because it can be the same method as getTapAndHoldSpeedMethodMatch. + getTapAndHoldHapticsMethodMatch(vibratorField).let { + it.method.apply { + val index = it[0] + val register = getInstruction(index).registerA + + addInstructions( + index + 1, + """ + invoke-static { v$register }, ${EXTENSION_CLASS_DESCRIPTOR}->disableTapAndHoldVibrate(Ljava/lang/Object;)Ljava/lang/Object; + move-result-object v$register + """ + ) + } + } + } +} + +@Suppress("unused") +private enum class MethodCall( + override val definedClassName: String, + override val methodName: String, + override val methodParams: Array, + override val returnType: String, +) : IMethodCall { + VibrationEffect( + "Landroid/os/Vibrator;", + "vibrate", + arrayOf("Landroid/os/VibrationEffect;"), + "V", + ), + VibrationMilliseconds( + "Landroid/os/Vibrator;", + "vibrate", + arrayOf("J"), + "V", + ), +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/hapticfeedback/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/hapticfeedback/Fingerprints.kt new file mode 100644 index 0000000000..c1f082b8ad --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/hapticfeedback/Fingerprints.kt @@ -0,0 +1,56 @@ +package app.revanced.patches.youtube.interaction.hapticfeedback + +import app.revanced.patcher.* +import app.revanced.patcher.patch.BytecodePatchContext +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +internal val BytecodePatchContext.markerHapticsMethod by gettingFirstMethodDeclaratively( + "Failed to execute markers haptics vibrate.", +) { + returnType("V") +} + +internal val BytecodePatchContext.scrubbingHapticsMethod by gettingFirstMethodDeclaratively( + "Failed to haptics vibrate for fine scrubbing.", +) { + returnType("V") +} + +internal val BytecodePatchContext.seekUndoHapticsMethod by gettingFirstMethodDeclaratively( + "Failed to execute seek undo haptics vibrate.", +) { + returnType("V") +} + +internal val BytecodePatchContext.tapAndHoldHapticsHandlerMethodMatch by composingFirstMethod { + name("") + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returnType("V") + parameterTypes("Landroid/content/Context;", "Landroid/os/Handler;") + instructions( + "vibrator"(), + allOf(Opcode.CHECK_CAST(), type("Landroid/os/Vibrator;")), + after(allOf(Opcode.IPUT_OBJECT(), field { type == "Ljava/lang/Object;" })) + ) +} + +internal fun BytecodePatchContext.getTapAndHoldHapticsMethodMatch(vibratorFieldReference: FieldReference) = + firstMethodComposite { + name("run") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes() + instructions( + allOf(Opcode.IGET_OBJECT(), field { this == vibratorFieldReference }), + allOf(Opcode.CHECK_CAST(), type("Landroid/os/Vibrator;")), + "Failed to easy seek haptics vibrate."() + ) + } + +internal val BytecodePatchContext.zoomHapticsMethod by gettingFirstMethodDeclaratively( + "Failed to haptics vibrate for video zoom", +) { + returnType("V") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch.kt index ceefafbb94..208151e344 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch.kt @@ -41,7 +41,7 @@ val enableSlideToSeekPatch = bytecodePatch( var modifiedMethods = false - // Restore the behaviour to slide to seek. + // Restore the behavior to slide to seek. val checkIndex = slideToSeekMethodMatch[0] val checkReference = slideToSeekMethodMatch.method.getInstruction(checkIndex) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSeekbarTappingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableTapToSeekPatch.kt similarity index 85% rename from patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSeekbarTappingPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableTapToSeekPatch.kt index c3e867c496..610d923c26 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSeekbarTappingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableTapToSeekPatch.kt @@ -15,9 +15,9 @@ import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/SeekbarTappingPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/TapToSeekPatch;" -val enableSeekbarTappingPatch = bytecodePatch( +val enableTapToSeekPatch = bytecodePatch( description = "Adds an option to enable tap to seek on the seekbar of the video player.", ) { dependsOn( @@ -27,14 +27,14 @@ val enableSeekbarTappingPatch = bytecodePatch( ) apply { - addResources("youtube", "interaction.seekbar.enableSeekbarTappingPatch") + addResources("youtube", "interaction.seekbar.enableTapToSeekPatch") PreferenceScreen.SEEKBAR.addPreferences( - SwitchPreference("revanced_seekbar_tapping"), + SwitchPreference("revanced_tap_to_seek"), ) // Find the required methods to tap the seekbar. - val seekbarTappingMethods = onTouchEventHandlerMethodMatch.let { + val tapToSeekMethods = onTouchEventHandlerMethodMatch.let { fun getReference(index: Int) = it.method.getInstruction(index) .reference as MethodReference @@ -44,7 +44,7 @@ val enableSeekbarTappingPatch = bytecodePatch( ) } - seekbarTappingMethodMatch.let { + tapToSeekMethodMatch.let { val insertIndex = it[-1] + 1 it.method.apply { @@ -62,13 +62,13 @@ val enableSeekbarTappingPatch = bytecodePatch( xAxisRegister, ) - val oMethod = seekbarTappingMethods[0] - val nMethod = seekbarTappingMethods[1] + val oMethod = tapToSeekMethods[0] + val nMethod = tapToSeekMethods[1] addInstructionsWithLabels( insertIndex, """ - invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->seekbarTappingEnabled()Z + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->tapToSeekEnabled()Z move-result v$freeRegister if-eqz v$freeRegister, :disabled invoke-virtual { v$thisInstanceRegister, v$xAxisRegister }, $oMethod diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/Fingerprints.kt index 38683f9acc..52e7dfb036 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/Fingerprints.kt @@ -2,10 +2,7 @@ package app.revanced.patches.youtube.interaction.seekbar import app.revanced.patcher.* import app.revanced.patcher.extensions.instructions -import app.revanced.patcher.extensions.stringReference import app.revanced.patcher.patch.BytecodePatchContext -import app.revanced.patches.youtube.misc.playservice.* -import app.revanced.util.indexOfFirstInstruction import app.revanced.util.literal import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode @@ -61,25 +58,6 @@ internal val BytecodePatchContext.disableFastForwardGestureMethodMatch by compos custom { instructions.count() > 30 } } -internal val BytecodePatchContext.customTapAndHoldMethodMatch by composingFirstMethod { - name("run") - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returnType("V") - parameterTypes() - instructions(2.0f.toRawBits().toLong()()) - custom { - // Code is found in different methods with different strings. - val findSearchLandingKey = (is_19_34_or_greater && !is_19_47_or_greater) || - (is_20_19_or_greater && !is_20_20_or_greater) || is_20_31_or_greater - - indexOfFirstInstruction { - val string = stringReference?.string - string == "Failed to easy seek haptics vibrate." || - (findSearchLandingKey && string == "search_landing_cache_key") - } >= 0 - } -} - internal val BytecodePatchContext.onTouchEventHandlerMethodMatch by composingFirstMethod { name("onTouchEvent") accessFlags(AccessFlags.PUBLIC, AccessFlags.PUBLIC) @@ -103,7 +81,7 @@ internal val BytecodePatchContext.onTouchEventHandlerMethodMatch by composingFir ) } -internal val BytecodePatchContext.seekbarTappingMethodMatch by composingFirstMethod { +internal val BytecodePatchContext.tapToSeekMethodMatch by composingFirstMethod { name("onTouchEvent") accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("Z") @@ -132,15 +110,6 @@ internal val BytecodePatchContext.slideToSeekMethodMatch by composingFirstMethod literal { 67108864 } } -internal val BytecodePatchContext.fullscreenSeekbarThumbnailsQualityMethod by gettingFirstMethodDeclaratively { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returnType("Z") - parameterTypes() - instructions( - 45399684L(), // Video stream seekbar thumbnails feature flag. - ) -} - internal val BytecodePatchContext.fullscreenLargeSeekbarFeatureFlagMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("Z") diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarPatch.kt index 6033875241..934442eeaf 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarPatch.kt @@ -6,24 +6,24 @@ import app.revanced.patcher.patch.bytecodePatch val seekbarPatch = bytecodePatch( name = "Seekbar", description = "Adds options to disable precise seeking when swiping up on the seekbar, " + - "slide to seek instead of playing at 2x speed when pressing and holding, " + - "tapping the player seekbar to seek, " + - "and hiding the video player seekbar.", + "slide to seek instead of playing at 2x speed when pressing and holding, " + + "tapping the player seekbar to seek, and hiding the video player seekbar.", ) { dependsOn( disablePreciseSeekingGesturePatch, enableSlideToSeekPatch, - enableSeekbarTappingPatch, + enableTapToSeekPatch, hideSeekbarPatch, - seekbarThumbnailsPatch, ) compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarThumbnailsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarThumbnailsPatch.kt deleted file mode 100644 index 2ffae0d6b9..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarThumbnailsPatch.kt +++ /dev/null @@ -1,71 +0,0 @@ -package app.revanced.patches.youtube.interaction.seekbar - -import app.revanced.patcher.extensions.addInstruction -import app.revanced.patcher.extensions.addInstructions -import app.revanced.patcher.extensions.instructions -import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patches.all.misc.resources.addResources -import app.revanced.patches.all.misc.resources.addResourcesPatch -import app.revanced.patches.shared.misc.settings.preference.SwitchPreference -import app.revanced.patches.youtube.layout.seekbar.fullscreenSeekbarThumbnailsMethod -import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch -import app.revanced.patches.youtube.misc.playservice.is_19_17_or_greater -import app.revanced.patches.youtube.misc.playservice.is_20_09_or_greater -import app.revanced.patches.youtube.misc.playservice.versionCheckPatch -import app.revanced.patches.youtube.misc.settings.PreferenceScreen - -private const val EXTENSION_CLASS_DESCRIPTOR = - "Lapp/revanced/extension/youtube/patches/SeekbarThumbnailsPatch;" - -val seekbarThumbnailsPatch = bytecodePatch( - description = "Adds an option to use high quality fullscreen seekbar thumbnails.", -) { - dependsOn( - sharedExtensionPatch, - addResourcesPatch, - versionCheckPatch, - ) - - apply { - if (is_20_09_or_greater) { - // High quality seekbar thumbnails is partially broken in 20.09 - // and the code is completely removed in 20.10+ - return@apply - } - - addResources("youtube", "layout.seekbar.seekbarThumbnailsPatch") - - if (is_19_17_or_greater) { - PreferenceScreen.SEEKBAR.addPreferences( - SwitchPreference("revanced_seekbar_thumbnails_high_quality"), - ) - } else { - PreferenceScreen.SEEKBAR.addPreferences( - SwitchPreference("revanced_restore_old_seekbar_thumbnails"), - SwitchPreference( - key = "revanced_seekbar_thumbnails_high_quality", - summaryOnKey = "revanced_seekbar_thumbnails_high_quality_legacy_summary_on", - summaryOffKey = "revanced_seekbar_thumbnails_high_quality_legacy_summary_on", - ), - ) - - fullscreenSeekbarThumbnailsMethod.apply { - val moveResultIndex = instructions.lastIndex - 1 - - addInstruction( - moveResultIndex, - "invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->useFullscreenSeekbarThumbnails()Z", - ) - } - } - - fullscreenSeekbarThumbnailsQualityMethod.addInstructions( - 0, - """ - invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->useHighQualityFullscreenThumbnails()Z - move-result v0 - return v0 - """, - ) - } -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt index 6c49c61618..13c90762b4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt @@ -53,7 +53,10 @@ private val swipeControlsResourcePatch = resourcePatch { SwitchPreference("revanced_swipe_save_and_restore_brightness"), SwitchPreference("revanced_swipe_lowest_value_enable_auto_brightness"), ListPreference("revanced_swipe_overlay_style"), - TextPreference("revanced_swipe_overlay_background_opacity", inputType = InputType.NUMBER), + TextPreference( + "revanced_swipe_overlay_background_opacity", + inputType = InputType.NUMBER + ), TextPreference( "revanced_swipe_overlay_progress_brightness_color", tag = "app.revanced.extension.shared.settings.preference.ColorPickerWithOpacitySliderPreference", @@ -101,10 +104,12 @@ val swipeControlsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch.kt index bc67ed397c..b5f90ac566 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch.kt @@ -8,6 +8,7 @@ import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.subtitleButtonControllerMethod private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/DisableAutoCaptionsPatch;" @@ -25,10 +26,12 @@ val disableAutoCaptionsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -54,7 +57,7 @@ val disableAutoCaptionsPatch = bytecodePatch( arrayOf( startVideoInformerMethod to 0, - storyboardRendererDecoderRecommendedLevelMethod to 1, + subtitleButtonControllerMethod to 1, ).forEach { (method, enabled) -> method.addInstructions( 0, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/Fingerprints.kt index a85542b891..04241cfc07 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/Fingerprints.kt @@ -14,23 +14,8 @@ internal val BytecodePatchContext.startVideoInformerMethod by gettingFirstMethod ) } -internal val BytecodePatchContext.storyboardRendererDecoderRecommendedLevelMethod by gettingFirstMethodDeclaratively("#-1#") { - returnType("V") - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - parameterTypes("L") -} - internal val BytecodePatchContext.subtitleTrackMethod by gettingFirstMethodDeclaratively("DISABLE_CAPTIONS_OPTION") { - definingClass("/SubtitleTrack;") accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("Z") parameterTypes() - opcodes( - Opcode.CONST_STRING, - Opcode.INVOKE_VIRTUAL, - Opcode.MOVE_RESULT_OBJECT, - Opcode.INVOKE_VIRTUAL, - Opcode.MOVE_RESULT, - Opcode.RETURN, - ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt index 3859ce093d..befd7171e9 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt @@ -26,10 +26,12 @@ val customBrandingPatch = baseCustomBrandingPatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt index 261b0a0d80..38b40e4d33 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt @@ -53,7 +53,8 @@ private val customHeaderResourceFileNames = variants.map { variant -> "${CUSTOM_HEADER_RESOURCE_NAME}_$variant.png" }.toTypedArray() -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ChangeHeaderPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/ChangeHeaderPatch;" private val changeHeaderBytecodePatch = bytecodePatch { dependsOn( @@ -106,10 +107,12 @@ val changeHeaderPatch = resourcePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideVideoActionsButtonsPatch.kt similarity index 76% rename from patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideButtonsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideVideoActionsButtonsPatch.kt index c614b2ebb9..a869906f37 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideButtonsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideVideoActionsButtonsPatch.kt @@ -16,7 +16,8 @@ import java.util.logging.Logger @Suppress("unused") val hideVideoActionButtonsPatch = resourcePatch( name = "Hide video action buttons", - description = "Adds options to hide action buttons (such as the Download button) under videos.", + description = "Adds options to hide action buttons (such as the Download button) under videos. " + + "Patching version 20.21.37 or lower can hide more player button types." ) { dependsOn( resourceMappingPatch, @@ -27,10 +28,12 @@ val hideVideoActionButtonsPatch = resourcePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - // 20.22+ does not yet support hiding all player buttons. + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -43,27 +46,20 @@ val hideVideoActionButtonsPatch = resourcePatch( SwitchPreference("revanced_hide_like_dislike_button"), SwitchPreference("revanced_hide_comments_button"), SwitchPreference("revanced_hide_save_button"), - ) + SwitchPreference("revanced_hide_remix_button"), + SwitchPreference("revanced_hide_share_button"), - if (is_20_22_or_greater) { - // FIXME: 20.22+ filtering of the action buttons doesn't work because - // the buffer is the same for all buttons. - Logger.getLogger(this::class.java.name).warning( - "\n!!!" + - "\n!!! Not all player action buttons can be set hidden when patching 20.22+" + - "\n!!! Patch 20.21.37 or lower if you want to hide player action buttons" + - "\n!!!", ) - } else { + + // 20.22+ cannot hide all action buttons because of buffer changes. + if (!is_20_22_or_greater) { preferences.addAll( listOf( SwitchPreference("revanced_hide_hype_button"), SwitchPreference("revanced_hide_ask_button"), SwitchPreference("revanced_hide_clip_button"), SwitchPreference("revanced_hide_promote_button"), - SwitchPreference("revanced_hide_remix_button"), SwitchPreference("revanced_hide_report_button"), - SwitchPreference("revanced_hide_share_button"), SwitchPreference("revanced_hide_shop_button"), SwitchPreference("revanced_hide_stop_ads_button"), SwitchPreference("revanced_hide_thanks_button"), @@ -78,6 +74,6 @@ val hideVideoActionButtonsPatch = resourcePatch( ), ) - addLithoFilter("Lapp/revanced/extension/youtube/patches/litho/ButtonsFilter;") + addLithoFilter("Lapp/revanced/extension/youtube/patches/litho/VideoActionButtonsFilter;") } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/Fingerprints.kt index 35d51ba015..4e8c86e823 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/Fingerprints.kt @@ -2,17 +2,10 @@ package app.revanced.patches.youtube.layout.buttons.navigation import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patches.shared.misc.mapping.ResourceType import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode -internal val BytecodePatchContext.addCreateButtonViewMethodMatch by composingFirstMethod { - instructions( - "Android Wear"(), - Opcode.IF_EQZ(), - after("Android Automotive"()), - ) -} - internal val BytecodePatchContext.createPivotBarMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) parameterTypes( @@ -34,6 +27,28 @@ internal val BytecodePatchContext.animatedNavigationTabsFeatureFlagMethodMatch b ) } + +internal val BytecodePatchContext.pivotBarStyleMethodMatch by composingFirstMethod { + definingClass("/PivotBar;") + returnType("V") + parameterTypes("L") + opcodes( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.XOR_INT_2ADDR + ) +} + +internal val BytecodePatchContext.pivotBarChangedMethodMatch by composingFirstMethod { + name("onConfigurationChanged") + definingClass("/PivotBar;") + returnType("V") + opcodes( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT + ) +} + internal val BytecodePatchContext.translucentNavigationStatusBarFeatureFlagMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("Z") @@ -63,3 +78,20 @@ internal val BytecodePatchContext.translucentNavigationButtonsSystemFeatureFlagM 45632194L(), // Translucent system buttons feature flag. ) } + +internal val BytecodePatchContext.setWordmarkHeaderMethod by gettingFirstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes("Landroid/widget/ImageView;") + instructions( + ResourceType.ATTR("ytPremiumWordmarkHeader"), + ResourceType.ATTR("ytWordmarkHeader") + ) +} + +internal val BytecodePatchContext.wideSearchbarLayoutMethod by gettingFirstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Landroid/view/View;") + parameterTypes("L", "L") + instructions(ResourceType.LAYOUT("action_bar_ringo")) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt new file mode 100644 index 0000000000..8abeeab8d3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -0,0 +1,253 @@ +package app.revanced.patches.youtube.layout.buttons.navigation + +import app.revanced.patcher.extensions.addInstruction +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.methodReference +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.layout.toolbar.hookToolbar +import app.revanced.patches.youtube.layout.toolbar.toolbarHookPatch +import app.revanced.patches.youtube.misc.contexthook.Endpoint +import app.revanced.patches.youtube.misc.contexthook.addOSNameHook +import app.revanced.patches.youtube.misc.contexthook.hookClientContextPatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.navigation.hookNavigationButtonCreated +import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch +import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater +import app.revanced.patches.youtube.misc.playservice.is_20_15_or_greater +import app.revanced.patches.youtube.misc.playservice.is_20_31_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.addInstructionsAtControlFlowLabel +import app.revanced.util.findInstructionIndicesReversedOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.insertLiteralOverride +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import kotlin.collections.plusAssign + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/NavigationBarPatch;" + +val navigationBarPatch = bytecodePatch( + name = "Navigation bar", + description = "Adds options to hide and change the bottom navigation bar (such as the Shorts button) " + + " and the upper navigation toolbar. Patching version 20.21.37 and lower also adds a setting to use a wide searchbar." +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + navigationBarHookPatch, + versionCheckPatch, + hookClientContextPatch, + toolbarHookPatch + ) + + compatibleWith( + "com.google.android.youtube"( + "20.14.43", + "20.21.37", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" + ), + ) + + apply { + addResources("youtube", "layout.buttons.navigation.navigationBarPatch") + + val preferences = mutableSetOf( + SwitchPreference("revanced_hide_home_button"), + SwitchPreference("revanced_hide_shorts_button"), + SwitchPreference("revanced_hide_create_button"), + SwitchPreference("revanced_hide_subscriptions_button"), + SwitchPreference("revanced_hide_notifications_button"), + SwitchPreference("revanced_switch_create_with_notifications_button"), + SwitchPreference("revanced_hide_navigation_button_labels"), + SwitchPreference("revanced_narrow_navigation_buttons"), + ) + + if (is_19_25_or_greater) { + preferences += SwitchPreference("revanced_disable_translucent_navigation_bar_light") + preferences += SwitchPreference("revanced_disable_translucent_navigation_bar_dark") + + PreferenceScreen.GENERAL_LAYOUT.addPreferences( + SwitchPreference("revanced_disable_translucent_status_bar") + ) + + if (is_20_15_or_greater) { + preferences += SwitchPreference("revanced_navigation_bar_animations") + } + } + + PreferenceScreen.GENERAL_LAYOUT.addPreferences( + PreferenceScreenPreference( + key = "revanced_navigation_buttons_screen", + sorting = Sorting.UNSORTED, + preferences = preferences + ) + ) + + + // Switch create with notifications button. + addOSNameHook( + Endpoint.GUIDE, + "${EXTENSION_CLASS_DESCRIPTOR}->switchCreateWithNotificationButton(Ljava/lang/String;)Ljava/lang/String;", + ) + + // Hide navigation button labels. + createPivotBarMethodMatch.let { + it.method.apply { + val setTextIndex = it[0] + val targetRegister = getInstruction(setTextIndex).registerC + + addInstruction( + setTextIndex, + "invoke-static { v$targetRegister }, " + + "$EXTENSION_CLASS_DESCRIPTOR->hideNavigationButtonLabels(Landroid/widget/TextView;)V", + ) + } + } + + // Hook navigation button created, in order to hide them. + hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR) + + // Force on/off translucent effect on status bar and navigation buttons. + if (is_19_25_or_greater) { + translucentNavigationStatusBarFeatureFlagMethodMatch.let { + it.method.insertLiteralOverride( + it[0], + "$EXTENSION_CLASS_DESCRIPTOR->useTranslucentNavigationStatusBar(Z)Z", + ) + } + + translucentNavigationButtonsFeatureFlagMethodMatch.let { + it.method.insertLiteralOverride( + it[0], + "$EXTENSION_CLASS_DESCRIPTOR->useTranslucentNavigationButtons(Z)Z", + ) + } + + translucentNavigationButtonsSystemFeatureFlagMethodMatch.let { + it.method.insertLiteralOverride( + it[0], + "$EXTENSION_CLASS_DESCRIPTOR->useTranslucentNavigationButtons(Z)Z", + ) + } + } + + if (is_20_15_or_greater) { + animatedNavigationTabsFeatureFlagMethodMatch.let { + it.method.insertLiteralOverride( + it[0], + "$EXTENSION_CLASS_DESCRIPTOR->useAnimatedNavigationButtons(Z)Z", + ) + } + } + + arrayOf( + pivotBarChangedMethodMatch, + pivotBarStyleMethodMatch + ).forEach { match -> + match.method.apply { + val targetIndex = match[1] + 1 + val register = getInstruction(targetIndex - 1).registerA + + addInstructions( + targetIndex, + """ + invoke-static { v$register }, ${EXTENSION_CLASS_DESCRIPTOR}->enableNarrowNavigationButton(Z)Z + move-result v$register + """ + ) + } + + } + + + // + // Toolbar. + // + + val toolbarPreferences = mutableSetOf( + SwitchPreference("revanced_hide_toolbar_create_button"), + SwitchPreference("revanced_hide_toolbar_notification_button"), + SwitchPreference("revanced_hide_toolbar_search_button") + ) + if (!is_20_31_or_greater) { + toolbarPreferences += SwitchPreference("revanced_wide_searchbar") + } + + PreferenceScreen.GENERAL_LAYOUT.addPreferences( + PreferenceScreenPreference( + key = "revanced_toolbar_screen", + sorting = Sorting.UNSORTED, + preferences = toolbarPreferences + ) + ) + + hookToolbar("${EXTENSION_CLASS_DESCRIPTOR}->hideCreateButton") + hookToolbar("${EXTENSION_CLASS_DESCRIPTOR}->hideNotificationButton") + hookToolbar("${EXTENSION_CLASS_DESCRIPTOR}->hideSearchButton") + + + // + // Wide searchbar. + // + + // YT removed the legacy text search text field all code required to use it. + // This functionality could be restored by adding a search text field to the toolbar + // with a listener that artificially clicks the toolbar search button. + if (!is_20_31_or_greater) { + // Navigate to the method that checks if the YT logo is shown beside the search bar. + val shouldShowLogoMethod = with(setWordmarkHeaderMethod) { + val invokeStaticIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_STATIC && methodReference?.returnType == "Z" + } + navigate(this).to(invokeStaticIndex).stop() + } + + shouldShowLogoMethod.apply { + findInstructionIndicesReversedOrThrow(Opcode.RETURN).forEach { index -> + val register = getInstruction(index).registerA + + addInstructionsAtControlFlowLabel( + index, + """ + invoke-static { v$register }, ${EXTENSION_CLASS_DESCRIPTOR}->enableWideSearchbar(Z)Z + move-result v$register + """ + ) + } + } + + // Fix missing left padding when using wide searchbar. + wideSearchbarLayoutMethod.apply { + findInstructionIndicesReversedOrThrow { + val reference = getReference() + reference?.definingClass == "Landroid/view/LayoutInflater;" && reference.name == "inflate" + }.forEach { inflateIndex -> + val register = + getInstruction(inflateIndex + 1).registerA + + addInstruction( + inflateIndex + 2, + "invoke-static { v$register }, " + + "${EXTENSION_CLASS_DESCRIPTOR}->setActionBar(Landroid/view/View;)V" + ) + } + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatch.kt deleted file mode 100644 index 230db65c62..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatch.kt +++ /dev/null @@ -1,150 +0,0 @@ -package app.revanced.patches.youtube.layout.buttons.navigation - -import app.revanced.patcher.extensions.addInstruction -import app.revanced.patcher.extensions.addInstructions -import app.revanced.patcher.extensions.getInstruction -import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patches.all.misc.resources.addResources -import app.revanced.patches.all.misc.resources.addResourcesPatch -import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference -import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting -import app.revanced.patches.shared.misc.settings.preference.SwitchPreference -import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch -import app.revanced.patches.youtube.misc.navigation.hookNavigationButtonCreated -import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch -import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater -import app.revanced.patches.youtube.misc.playservice.is_20_15_or_greater -import app.revanced.patches.youtube.misc.playservice.versionCheckPatch -import app.revanced.patches.youtube.misc.settings.PreferenceScreen -import app.revanced.patches.youtube.misc.settings.settingsPatch -import app.revanced.util.insertLiteralOverride -import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction - -private const val EXTENSION_CLASS_DESCRIPTOR = - "Lapp/revanced/extension/youtube/patches/NavigationButtonsPatch;" - -@Suppress("ObjectPropertyName") -val navigationButtonsPatch = bytecodePatch( - name = "Navigation buttons", - description = "Adds options to hide and change navigation buttons (such as the Shorts button).", -) { - dependsOn( - sharedExtensionPatch, - settingsPatch, - addResourcesPatch, - navigationBarHookPatch, - versionCheckPatch, - ) - - compatibleWith( - "com.google.android.youtube"( - "19.43.41", - "20.14.43", - "20.21.37", - "20.31.40", - ), - ) - - apply { - addResources("youtube", "layout.buttons.navigation.navigationButtonsPatch") - - val preferences = mutableSetOf( - SwitchPreference("revanced_hide_home_button"), - SwitchPreference("revanced_hide_shorts_button"), - SwitchPreference("revanced_hide_create_button"), - SwitchPreference("revanced_hide_subscriptions_button"), - SwitchPreference("revanced_hide_notifications_button"), - SwitchPreference("revanced_switch_create_with_notifications_button"), - SwitchPreference("revanced_hide_navigation_button_labels"), - ) - - if (is_19_25_or_greater) { - preferences += SwitchPreference("revanced_disable_translucent_navigation_bar_light") - preferences += SwitchPreference("revanced_disable_translucent_navigation_bar_dark") - - PreferenceScreen.GENERAL_LAYOUT.addPreferences( - SwitchPreference("revanced_disable_translucent_status_bar"), - ) - } - - if (is_20_15_or_greater) { - PreferenceScreen.GENERAL_LAYOUT.addPreferences( - SwitchPreference("revanced_navigation_bar_animations"), - ) - } - - PreferenceScreen.GENERAL_LAYOUT.addPreferences( - PreferenceScreenPreference( - key = "revanced_navigation_buttons_screen", - sorting = Sorting.UNSORTED, - preferences = preferences, - ), - ) - - // Switch create with notifications button. - addCreateButtonViewMethodMatch.method.apply { - val conditionalCheckIndex = addCreateButtonViewMethodMatch[1] - val conditionRegister = - getInstruction(conditionalCheckIndex).registerA - - addInstructions( - conditionalCheckIndex, - """ - invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->switchCreateWithNotificationButton()Z - move-result v$conditionRegister - """, - ) - } - - // Hide navigation button labels. - createPivotBarMethodMatch.let { - it.method.apply { - val setTextIndex = it[0] - val targetRegister = getInstruction(setTextIndex).registerC - - addInstruction( - setTextIndex, - "invoke-static { v$targetRegister }, " + - "$EXTENSION_CLASS_DESCRIPTOR->hideNavigationButtonLabels(Landroid/widget/TextView;)V", - ) - } - } - - // Hook navigation button created, in order to hide them. - hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR) - - // Force on/off translucent effect on status bar and navigation buttons. - if (is_19_25_or_greater) { - translucentNavigationStatusBarFeatureFlagMethodMatch.let { - it.method.insertLiteralOverride( - it[0], - "$EXTENSION_CLASS_DESCRIPTOR->useTranslucentNavigationStatusBar(Z)Z", - ) - } - - translucentNavigationButtonsFeatureFlagMethodMatch.let { - it.method.insertLiteralOverride( - it[0], - "$EXTENSION_CLASS_DESCRIPTOR->useTranslucentNavigationButtons(Z)Z", - ) - } - - translucentNavigationButtonsSystemFeatureFlagMethodMatch.let { - it.method.insertLiteralOverride( - it[0], - "$EXTENSION_CLASS_DESCRIPTOR->useTranslucentNavigationButtons(Z)Z", - ) - } - } - - if (is_20_15_or_greater) { - animatedNavigationTabsFeatureFlagMethodMatch.let { - it.method.insertLiteralOverride( - it[0], - "$EXTENSION_CLASS_DESCRIPTOR->useAnimatedNavigationButtons(Z)Z", - ) - } - } - } -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/Fingerprints.kt index da59a908f4..a350d883a9 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/Fingerprints.kt @@ -4,6 +4,7 @@ import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patches.shared.misc.mapping.ResourceType import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode internal val BytecodePatchContext.mediaRouteButtonMethod by gettingFirstMethodDeclaratively { name("setVisibility") @@ -30,3 +31,23 @@ internal val BytecodePatchContext.inflateControlsGroupLayoutStubMethodMatch by c method("inflate"), ) } + +internal val BytecodePatchContext.fullscreenButtonMethodMatch by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameterTypes("Landroid/view/View;") + returnType("V") + instructions( + ResourceType.ID("fullscreen_button"), + Opcode.CHECK_CAST() + ) +} + +internal val BytecodePatchContext.titleAnchorMethodMatch by composingFirstMethod { + returnType("V") + instructions( + ResourceType.ID("player_collapse_button"), + Opcode.CHECK_CAST(), + ResourceType.ID("title_anchor"), + Opcode.MOVE_RESULT_OBJECT() + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt index 4039e8a7cb..f09b64213f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt @@ -26,7 +26,7 @@ private const val EXTENSION_CLASS_DESCRIPTOR = val hidePlayerOverlayButtonsPatch = bytecodePatch( name = "Hide player overlay buttons", description = "Adds options to hide the player Cast, Autoplay, Captions, Previous & Next buttons, and the player " + - "control buttons background.", + "control buttons background.", ) { dependsOn( sharedExtensionPatch, @@ -38,10 +38,12 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -49,23 +51,26 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch( addResources("youtube", "layout.buttons.overlay.hidePlayerOverlayButtonsPatch") PreferenceScreen.PLAYER.addPreferences( - SwitchPreference("revanced_hide_player_previous_next_buttons"), - SwitchPreference("revanced_hide_cast_button"), - SwitchPreference("revanced_hide_captions_button"), SwitchPreference("revanced_hide_autoplay_button"), + SwitchPreference("revanced_hide_captions_button"), + SwitchPreference("revanced_hide_cast_button"), + SwitchPreference("revanced_hide_collapse_button"), + SwitchPreference("revanced_hide_fullscreen_button"), SwitchPreference("revanced_hide_player_control_buttons_background"), + SwitchPreference("revanced_hide_player_previous_next_buttons"), ) // region Hide player next/previous button. getLayoutConstructorMethodMatch().let { val insertIndex = it[-1] - val viewRegister = it.method.getInstruction(insertIndex).registerC + val viewRegister = + it.method.getInstruction(insertIndex).registerC it.method.addInstruction( insertIndex, "invoke-static { v$viewRegister }, $EXTENSION_CLASS_DESCRIPTOR" + - "->hidePreviousNextButtons(Landroid/view/View;)V", + "->hidePreviousNextButtons(Landroid/view/View;)V", ) } @@ -84,7 +89,7 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch( if (is_20_28_or_greater) { arrayOf( castButtonPlayerFeatureFlagMethodMatch, - castButtonActionFeatureFlagMethodMatch, + castButtonActionFeatureFlagMethodMatch, // Cast button in the feed. ).forEach { match -> match.method.insertLiteralOverride( match[0], @@ -118,14 +123,14 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch( val gotoIndex = indexOfFirstInstructionOrThrow(constIndex) { val parameterTypes = getReference()?.parameterTypes opcode == Opcode.INVOKE_VIRTUAL && - parameterTypes?.size == 2 && - parameterTypes.first() == "Landroid/view/ViewStub;" + parameterTypes?.size == 2 && + parameterTypes.first() == "Landroid/view/ViewStub;" } + 1 addInstructionsWithLabels( constIndex, """ - invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->hideAutoPlayButton()Z + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->hideAutoplayButton()Z move-result v$constRegister if-nez v$constRegister, :hidden """, @@ -135,6 +140,54 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch( // endregion + + // region Hide collapse button. + + titleAnchorMethodMatch.let { + it.method.apply { + val titleAnchorIndex = it[-1] + val titleAnchorRegister = getInstruction(titleAnchorIndex).registerA + + addInstruction( + titleAnchorIndex + 1, + "invoke-static { v$titleAnchorRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->setTitleAnchorStartMargin(Landroid/view/View;)V" + ) + + val playerCollapseButtonIndex = it[1] + val playerCollapseButtonRegister = getInstruction(playerCollapseButtonIndex).registerA + + addInstruction( + playerCollapseButtonIndex + 1, + "invoke-static { v$playerCollapseButtonRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->hideCollapseButton(Landroid/widget/ImageView;)V" + ) + } + } + + // endregion + + // region Hide fullscreen button. + + fullscreenButtonMethodMatch.let { + it.method.apply { + val castIndex = it[1] + val insertIndex = castIndex + 1 + val insertRegister = getInstruction(castIndex).registerA + + addInstructionsWithLabels( + insertIndex, + """ + invoke-static { v$insertRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->hideFullscreenButton(Landroid/widget/ImageView;)Landroid/widget/ImageView; + move-result-object v$insertRegister + if-nez v$insertRegister, :show + return-void + """, + ExternalLabel("show", getInstruction(insertIndex)) + ) + } + } + + // endregion + // region Hide player control buttons background. inflateControlsGroupLayoutStubMethodMatch.let { diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt index e4a5d1d1a8..d20f4d742d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt @@ -7,15 +7,16 @@ import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.ListPreference -import app.revanced.patches.youtube.layout.buttons.navigation.navigationButtonsPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.navigation.hookNavigationButtonCreated +import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ChangeFormFactorPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/ChangeFormFactorPatch;" @Suppress("unused") val changeFormFactorPatch = bytecodePatch( @@ -26,15 +27,17 @@ val changeFormFactorPatch = bytecodePatch( sharedExtensionPatch, settingsPatch, addResourcesPatch, - navigationButtonsPatch, + navigationBarHookPatch ) compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatch.kt new file mode 100644 index 0000000000..4417a906cc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatch.kt @@ -0,0 +1,74 @@ +package app.revanced.patches.youtube.layout.hide.autoplaypreview + +import app.revanced.patcher.extensions.ExternalLabel +import app.revanced.patcher.extensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.methodReference +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.getLayoutConstructorMethodMatch +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstResourceIdOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import kotlin.collections.first + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/HideAutoplayPreviewPatch;" + +@Suppress("unused") +val hideAutoplayPreviewPatch = bytecodePatch( + name = "Hide autoplay preview", + description = "Adds an option to hide the autoplay preview at the end of videos.", +) { + dependsOn( + settingsPatch, + sharedExtensionPatch, + resourceMappingPatch + ) + + compatibleWith( + "com.google.android.youtube"( + "20.14.43", + "20.21.37", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" + ) + ) + + apply { + addResources("youtube", "layout.hide.autoplaypreview.hideAutoplayPreviewPatch") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_hide_autoplay_preview") + ) + + getLayoutConstructorMethodMatch().method.apply { + val constIndex = indexOfFirstResourceIdOrThrow("autonav_preview_stub") + val constRegister = getInstruction(constIndex).registerA + val gotoIndex = indexOfFirstInstructionOrThrow(constIndex) { + val parameterTypes = methodReference?.parameterTypes + opcode == Opcode.INVOKE_VIRTUAL && + parameterTypes?.size == 2 && + parameterTypes.first() == "Landroid/view/ViewStub;" + } + 1 + + addInstructionsWithLabels( + constIndex, + """ + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->hideAutoplayPreview()Z + move-result v$constRegister + if-nez v$constRegister, :hidden + """, + ExternalLabel("hidden", getInstruction(gotoIndex)), + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/Fingerprints.kt index b09e058942..0f61cd5a17 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/Fingerprints.kt @@ -2,13 +2,9 @@ package app.revanced.patches.youtube.layout.hide.endscreencards import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext -import app.revanced.util.containsLiteralInstruction -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstruction import app.revanced.util.literal import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.reference.FieldReference internal val BytecodePatchContext.layoutCircleMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) @@ -33,7 +29,7 @@ internal val BytecodePatchContext.layoutIconMethodMatch by composingFirstMethod Opcode.MOVE_RESULT_OBJECT, Opcode.CHECK_CAST, - ) + ) literal { layoutIcon } } @@ -55,14 +51,18 @@ internal val BytecodePatchContext.showEndscreenCardsMethod by gettingFirstMethod accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") parameterTypes("L") + instructions( + allOf(Opcode.IPUT_OBJECT(), field { type == "Ljava/lang/String;" }), + allOf(Opcode.IGET_OBJECT(), field { type == "Ljava/lang/String;" }), + afterAtMost(7, allOf(Opcode.INVOKE_VIRTUAL(), method("ordinal"))), + 5L(), + 8L(), + 9L() + ) custom { - immutableClassDef.methods.count() == 5 && - containsLiteralInstruction(0) && - containsLiteralInstruction(5) && - containsLiteralInstruction(8) && - indexOfFirstInstruction { - val reference = getReference() - reference?.type == "Lcom/google/android/libraries/youtube/innertube/model/player/PlayerResponseModel;" - } >= 0 + immutableClassDef.methods.count() == 5 + // 'public final' or 'final' + && AccessFlags.FINAL.isSet(accessFlags) } } + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatch.kt index ca2fca5c35..e6f36cd84f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatch.kt @@ -3,6 +3,7 @@ package app.revanced.patches.youtube.layout.hide.endscreencards import app.revanced.patcher.extensions.addInstruction import app.revanced.patcher.extensions.addInstructionsWithLabels import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.immutableClassDef import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch import app.revanced.patches.all.misc.resources.addResources @@ -35,7 +36,7 @@ private val hideEndScreenCardsResourcePatch = resourcePatch { addResources("youtube", "layout.hide.endscreencards.hideEndScreenCardsResourcePatch") PreferenceScreen.PLAYER.addPreferences( - SwitchPreference("revanced_hide_endscreen_cards"), + SwitchPreference("revanced_hide_end_screen_cards"), ) fun idOf(name: String) = ResourceType.LAYOUT["endscreen_element_layout_$name"] @@ -62,10 +63,12 @@ val hideEndScreenCardsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -82,7 +85,7 @@ val hideEndScreenCardsPatch = bytecodePatch( addInstruction( insertIndex, "invoke-static { v$viewRegister }, " + - "$EXTENSION_CLASS_DESCRIPTOR->hideEndScreenCardView(Landroid/view/View;)V", + "$EXTENSION_CLASS_DESCRIPTOR->hideEndScreenCardView(Landroid/view/View;)V", ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestion/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/Fingerprints.kt similarity index 99% rename from patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestion/Fingerprints.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/Fingerprints.kt index ebb221781d..432cae1cf5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestion/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/Fingerprints.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.youtube.layout.hide.endscreensuggestion +package app.revanced.patches.youtube.layout.hide.endscreensuggestedvideo import app.revanced.patcher.* import app.revanced.patcher.extensions.instructions diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestion/HideEndScreenSuggestedVideoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/HideEndScreenSuggestedVideoPatch.kt similarity index 81% rename from patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestion/HideEndScreenSuggestedVideoPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/HideEndScreenSuggestedVideoPatch.kt index 979621b5e1..ae14785935 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestion/HideEndScreenSuggestedVideoPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/HideEndScreenSuggestedVideoPatch.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.youtube.layout.hide.endscreensuggestion +package app.revanced.patches.youtube.layout.hide.endscreensuggestedvideo import app.revanced.patcher.extensions.ExternalLabel import app.revanced.patcher.extensions.addInstructionsWithLabels @@ -31,15 +31,17 @@ val hideEndScreenSuggestedVideoPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) apply { - addResources("youtube", "layout.hide.endscreensuggestion.hideEndScreenSuggestedVideoPatch") + addResources("youtube", "layout.hide.endscreensuggestedvideo.hideEndScreenSuggestedVideoPatch") PreferenceScreen.PLAYER.addPreferences( SwitchPreference("revanced_end_screen_suggested_video"), @@ -49,18 +51,21 @@ val hideEndScreenSuggestedVideoPatch = bytecodePatch( val endScreenMethod = navigate(it.immutableMethod).to(it[-1]).stop() endScreenMethod.apply { - val autoNavStatusMethodName = autoNavConstructorMethod.immutableClassDef.getAutoNavStatusMethod().name + val autoNavStatusMethodName = + autoNavConstructorMethod.immutableClassDef.getAutoNavStatusMethod().name val invokeIndex = indexOfFirstInstructionOrThrow { val reference = methodReference reference?.name == autoNavStatusMethodName && - reference.returnType == "Z" && - reference.parameterTypes.isEmpty() + reference.returnType == "Z" && + reference.parameterTypes.isEmpty() } - val iGetObjectIndex = indexOfFirstInstructionReversedOrThrow(invokeIndex, Opcode.IGET_OBJECT) + val iGetObjectIndex = + indexOfFirstInstructionReversedOrThrow(invokeIndex, Opcode.IGET_OBJECT) val invokeReference = getInstruction(invokeIndex).reference - val iGetObjectReference = getInstruction(iGetObjectIndex).reference + val iGetObjectReference = + getInstruction(iGetObjectIndex).reference val opcodeName = getInstruction(invokeIndex).opcode.name addInstructionsWithLabels( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch.kt index 8d048b6458..00cf27fe76 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch.kt @@ -30,15 +30,20 @@ val disableFullscreenAmbientModePatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) apply { - addResources("youtube", "layout.hide.fullscreenambientmode.disableFullscreenAmbientModePatch") + addResources( + "youtube", + "layout.hide.fullscreenambientmode.disableFullscreenAmbientModePatch" + ) PreferenceScreen.PLAYER.addPreferences( SwitchPreference("revanced_disable_fullscreen_ambient_mode"), diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/Fingerprints.kt index 73180a043c..c88d7a8bec 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/Fingerprints.kt @@ -1,34 +1,96 @@ package app.revanced.patches.youtube.layout.hide.general import app.revanced.patcher.* +import app.revanced.patcher.after import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patches.shared.misc.mapping.ResourceType -import app.revanced.patches.youtube.layout.searchbar.wideSearchbarLayoutMethod import app.revanced.util.literal import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.ClassDef -/** - * 20.26+ - */ -internal val BytecodePatchContext.hideShowMoreButtonMethodMatch by composingFirstMethod { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL, AccessFlags.SYNTHETIC) + +internal val BytecodePatchContext.hideShowMoreButtonSetViewMethodMatch by composingFirstMethod { returnType("V") - parameterTypes("L", "Ljava/lang/Object;") + + var methodDefiningClass = "" + custom { + methodDefiningClass = definingClass + true + } instructions( - ResourceType.LAYOUT("expand_button_down"), - method { toString() == "Landroid/view/LayoutInflater;->inflate(ILandroid/view/ViewGroup;Z)Landroid/view/View;" }, - after(Opcode.MOVE_RESULT_OBJECT()), + ResourceType.ID("link_text_start"), + allOf( + Opcode.IPUT_OBJECT(), + field { type == "Landroid/widget/TextView;" && definingClass == methodDefiningClass }), + ResourceType.ID("expand_button_container"), + allOf( + Opcode.IPUT_OBJECT(), + field { type == "Landroid/view/View;" && definingClass == methodDefiningClass }) ) } -internal val BytecodePatchContext.hideShowMoreLegacyButtonMethodMatch by composingFirstMethod { +context(_: BytecodePatchContext) +internal fun ClassDef.getHideShowMoreButtonGetParentViewMethod() = + firstImmutableMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Landroid/view/View;") + parameterTypes() + } + +context(_: BytecodePatchContext) +internal fun ClassDef.getHideShowMoreButtonMethod() = firstMethodDeclaratively { + returnType("V") + parameterTypes("L", "Ljava/lang/Object;") + instructions( + allOf( + Opcode.INVOKE_VIRTUAL(), + method { + toString() == "Landroid/view/View;->setContentDescription(Ljava/lang/CharSequence;)V" + } + ) + ) +} + + +/** + * 20.21+ + */ +internal val BytecodePatchContext.hideSubscribedChannelsBarConstructorMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) instructions( - ResourceType.LAYOUT("expand_button_down"), - method { toString() == "Landroid/view/View;->inflate(Landroid/content/Context;ILandroid/view/ViewGroup;)Landroid/view/View;" }, - Opcode.MOVE_RESULT_OBJECT(), + ResourceType.ID("parent_container"), + afterAtMost(3, Opcode.MOVE_RESULT_OBJECT()), + afterAtMost( + 5, + allOf(Opcode.NEW_INSTANCE(), type($$"Landroid/widget/LinearLayout$LayoutParams;")) + ) + ) + custom { immutableClassDef.anyField { type == "Landroid/support/v7/widget/RecyclerView;" } } +} + +/** + * ~ 20.21 + */ +internal val BytecodePatchContext.hideSubscribedChannelsBarConstructorLegacyMethodMatch by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + instructions( + ResourceType.ID("parent_container"), + afterAtMost(3, Opcode.MOVE_RESULT_OBJECT()), + afterAtMost( + 5, + allOf(Opcode.NEW_INSTANCE(), type($$"Landroid/widget/LinearLayout$LayoutParams;")) + ) + ) +} + +internal val ClassDef.hideSubscribedChannelsBarLandscapeMethodMatch by ClassDefComposing.composingFirstMethod { + returnType("V") + parameterTypes() + instructions( + ResourceType.DIMEN("parent_view_width_in_wide_mode"), + allOf(Opcode.INVOKE_VIRTUAL(), method("getDimensionPixelSize")), + after(Opcode.MOVE_RESULT()) ) } @@ -146,3 +208,124 @@ internal val BytecodePatchContext.hideViewCountMethodMatch by composingFirstMeth Opcode.RETURN_OBJECT, ) } + +internal val BytecodePatchContext.searchBoxTypingStringMethodMatch by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes("L") + instructions( + allOf(Opcode.IGET_OBJECT(), field { type == "Ljava/util/Collection;" }), + afterAtMost(5, method { toString() == "Ljava/util/ArrayList;->(Ljava/util/Collection;)V" }), + allOf(Opcode.IGET_OBJECT(), field { type == "Ljava/lang/String;" }), + afterAtMost(5, method { toString() == "Ljava/lang/String;->isEmpty()Z" }), + ResourceType.DIMEN("suggestion_category_divider_height") + ) +} + +internal val BytecodePatchContext.searchSuggestionEndpointConstructorMethod by gettingFirstImmutableMethodDeclaratively( + "\u2026 " +) { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returnType("V") +} + +internal val ClassDef.searchSuggestionEndpointMethodMatch by ClassDefComposing.composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Z") + parameterTypes() + + var methodDefiningClass = "" + custom { + methodDefiningClass = definingClass + true + } + + instructions( + allOf( + Opcode.IGET_OBJECT(), + field { definingClass == methodDefiningClass && type == "Ljava/lang/String;" }), + allOf( + Opcode.INVOKE_STATIC(), + method { toString() == "Landroid/text/TextUtils;->isEmpty(Ljava/lang/CharSequence;)Z" }), + ) +} + +internal val BytecodePatchContext.latestVideosContentPillMethodMatch by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes("L", "Z") + instructions( + ResourceType.LAYOUT("content_pill"), + method { + toString() == "Landroid/view/LayoutInflater;->inflate(ILandroid/view/ViewGroup;Z)Landroid/view/View;" + }, + after(Opcode.MOVE_RESULT_OBJECT()) + ) +} + +internal val BytecodePatchContext.latestVideosBarMethodMatch by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes("L", "Z") + instructions( + ResourceType.LAYOUT("bar"), + method { + toString() == "Landroid/view/LayoutInflater;->inflate(ILandroid/view/ViewGroup;Z)Landroid/view/View;" + }, + after(Opcode.MOVE_RESULT_OBJECT()) + ) +} + + +internal val BytecodePatchContext.bottomSheetMenuItemBuilderMethodMatch by composingFirstMethod { + returnType("L") + parameterTypes("L") + instructions( + allOf( + Opcode.INVOKE_STATIC(), + method { + returnType == "Ljava/lang/CharSequence;" && + parameterTypes.size == 1 && parameterTypes[0].startsWith("L") + } + ), + after(Opcode.MOVE_RESULT_OBJECT()), + "Text missing for BottomSheetMenuItem."() + ) +} + +internal val BytecodePatchContext.contextualMenuItemBuilderMethodMatch by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL, AccessFlags.SYNTHETIC) + returnType("V") + parameterTypes("L", "L") + instructions( + allOf(Opcode.CHECK_CAST(), type("Landroid/widget/TextView;")), + afterAtMost( + 5, + method { toString() == "Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V" } + ), + ResourceType.DIMEN("poster_art_width_default"), + ) +} + +internal val BytecodePatchContext.channelTabBuilderMethod by gettingFirstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Landroid/view/View;") + parameterTypes( + "Ljava/lang/CharSequence;", + "Ljava/lang/CharSequence;", + "Z", + "L" + ) +} + +internal val BytecodePatchContext.channelTabRendererMethod by gettingFirstMethodDeclaratively( + "TabRenderer.content contains SectionListRenderer but the tab does not have a section list controller." +) { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes( + "L", + "Ljava/util/List;", + "I" + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt index 1abf4833c2..864583af4c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt @@ -1,6 +1,8 @@ package app.revanced.patches.youtube.layout.hide.general +import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable import app.revanced.patcher.CompositeMatch +import app.revanced.patcher.classDef import app.revanced.patcher.extensions.* import app.revanced.patcher.immutableClassDef import app.revanced.patcher.patch.resourcePatch @@ -9,9 +11,11 @@ import app.revanced.patches.shared.layout.hide.general.hideLayoutComponentsPatch import app.revanced.patches.shared.misc.mapping.ResourceType import app.revanced.patches.shared.misc.mapping.resourceMappingPatch import app.revanced.patches.shared.misc.settings.preference.* +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting +import app.revanced.patches.youtube.misc.engagement.engagementPanelHookPatch import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch -import app.revanced.patches.youtube.misc.playservice.is_20_26_or_greater +import app.revanced.patches.youtube.misc.playservice.is_20_21_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch @@ -19,12 +23,18 @@ import app.revanced.util.findFreeRegister import app.revanced.util.findInstructionIndicesReversedOrThrow import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter internal var albumCardId = -1L private set @@ -71,6 +81,7 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( hideLayoutComponentsResourcePatch, navigationBarHookPatch, versionCheckPatch, + engagementPanelHookPatch, resourceMappingPatch, ), filterClasses = setOf( @@ -82,15 +93,23 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( ), compatibleWithPackages = arrayOf( "com.google.android.youtube" to setOf( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ), ) { addResources("youtube", "layout.hide.general.hideLayoutComponentsPatch") + PreferenceScreen.ADS.addPreferences( + // Uses horizontal shelf and a buffer, which requires managing in a single place in the code + // to ensure the generic "hide horizontal shelves" doesn't hide when it should show. + SwitchPreference("revanced_hide_creator_store_shelf") + ) + PreferenceScreen.PLAYER.addPreferences( PreferenceScreenPreference( key = "revanced_hide_description_components_screen", @@ -99,15 +118,22 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( SwitchPreference("revanced_hide_ask_section"), SwitchPreference("revanced_hide_attributes_section"), SwitchPreference("revanced_hide_chapters_section"), + SwitchPreference("revanced_hide_course_progress_section"), + SwitchPreference("revanced_hide_explore_section"), + SwitchPreference("revanced_hide_explore_course_section"), + SwitchPreference("revanced_hide_explore_podcast_section"), SwitchPreference("revanced_hide_featured_links_section"), + SwitchPreference("revanced_hide_featured_places_section"), SwitchPreference("revanced_hide_featured_videos_section"), - SwitchPreference("revanced_hide_info_cards_section"), + SwitchPreference("revanced_hide_gaming_section"), SwitchPreference("revanced_hide_how_this_was_made_section"), SwitchPreference("revanced_hide_hype_points"), + SwitchPreference("revanced_hide_info_cards_section"), SwitchPreference("revanced_hide_key_concepts_section"), - SwitchPreference("revanced_hide_podcast_section"), + SwitchPreference("revanced_hide_music_section"), SwitchPreference("revanced_hide_subscribe_button"), SwitchPreference("revanced_hide_transcript_section"), + SwitchPreference("revanced_hide_quizzes_section") ), ), PreferenceScreenPreference( @@ -118,13 +144,14 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( SwitchPreference("revanced_hide_comments_channel_guidelines"), SwitchPreference("revanced_hide_comments_by_members_header"), SwitchPreference("revanced_hide_comments_section"), + SwitchPreference("revanced_hide_comments_section_in_home_feed"), SwitchPreference("revanced_hide_comments_community_guidelines"), SwitchPreference("revanced_hide_comments_create_a_short_button"), SwitchPreference("revanced_hide_comments_emoji_and_timestamp_buttons"), SwitchPreference("revanced_hide_comments_preview_comment"), SwitchPreference("revanced_hide_comments_thanks_button"), ), - sorting = PreferenceScreenPreference.Sorting.UNSORTED, + sorting = Sorting.UNSORTED, ), SwitchPreference("revanced_hide_channel_bar"), SwitchPreference("revanced_hide_channel_watermark"), @@ -132,29 +159,34 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( SwitchPreference("revanced_hide_emergency_box"), SwitchPreference("revanced_hide_info_panels"), SwitchPreference("revanced_hide_join_membership_button"), + SwitchPreference("revanced_hide_live_chat_replay_button"), SwitchPreference("revanced_hide_medical_panels"), SwitchPreference("revanced_hide_quick_actions"), SwitchPreference("revanced_hide_related_videos"), SwitchPreference("revanced_hide_subscribers_community_guidelines"), SwitchPreference("revanced_hide_timed_reactions"), + SwitchPreference("revanced_hide_video_title") ) PreferenceScreen.FEED.addPreferences( PreferenceScreenPreference( key = "revanced_hide_keyword_content_screen", - sorting = PreferenceScreenPreference.Sorting.UNSORTED, + sorting = Sorting.UNSORTED, preferences = setOf( SwitchPreference("revanced_hide_keyword_content_home"), SwitchPreference("revanced_hide_keyword_content_subscriptions"), SwitchPreference("revanced_hide_keyword_content_search"), - TextPreference("revanced_hide_keyword_content_phrases", inputType = InputType.TEXT_MULTI_LINE), + TextPreference( + "revanced_hide_keyword_content_phrases", + inputType = InputType.TEXT_MULTI_LINE + ), NonInteractivePreference( key = "revanced_hide_keyword_content_about", tag = "app.revanced.extension.shared.settings.preference.BulletPointPreference", ), NonInteractivePreference( key = "revanced_hide_keyword_content_about_whole_words", - tag = "app.revanced.extension.youtube.settings.preference.HtmlPreference", + tag = "app.revanced.extension.youtube.settings.preference.HTMLPreference", ), ), ), @@ -170,6 +202,18 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( PreferenceScreenPreference( key = "revanced_channel_screen", preferences = setOf( + PreferenceCategory( + titleKey = null, + sorting = Sorting.UNSORTED, + tag = "app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory", + preferences = setOf( + SwitchPreference("revanced_hide_channel_tab"), + TextPreference( + "revanced_hide_channel_tab_filter_strings", + inputType = InputType.TEXT_MULTI_LINE + ), + ) + ), SwitchPreference("revanced_hide_community_button"), SwitchPreference("revanced_hide_for_you_shelf"), SwitchPreference("revanced_hide_join_button"), @@ -185,6 +229,18 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( SwitchPreference("revanced_hide_community_posts"), SwitchPreference("revanced_hide_compact_banner"), SwitchPreference("revanced_hide_expandable_card"), + PreferenceCategory( + titleKey = null, + sorting = Sorting.UNSORTED, + tag = "app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory", + preferences = setOf( + SwitchPreference("revanced_hide_feed_flyout_menu"), + TextPreference( + "revanced_hide_feed_flyout_menu_filter_strings", + inputType = InputType.TEXT_MULTI_LINE + ), + ) + ), SwitchPreference("revanced_hide_floating_microphone_button"), SwitchPreference( key = "revanced_hide_horizontal_shelves", @@ -192,6 +248,7 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( ), SwitchPreference("revanced_hide_image_shelf"), SwitchPreference("revanced_hide_latest_posts"), + SwitchPreference("revanced_hide_latest_videos_button"), SwitchPreference("revanced_hide_mix_playlists"), SwitchPreference("revanced_hide_movies_section"), SwitchPreference("revanced_hide_notify_me_button"), @@ -206,7 +263,13 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( SwitchPreference("revanced_hide_doodles"), ) - // region Mix playlists + if (is_20_21_or_greater) { + PreferenceScreen.FEED.addPreferences( + SwitchPreference("revanced_hide_you_may_like_section") + ) + } + + // region Hide mix playlists parseElementFromBufferMethodMatch.let { it.method.apply { @@ -214,12 +277,18 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( val insertIndex = startIndex + 1 val byteArrayParameter = "p3" - val conversionContextRegister = getInstruction(startIndex).registerA - val returnEmptyComponentInstruction = instructions.last { it.opcode == Opcode.INVOKE_STATIC } + val conversionContextRegister = + getInstruction(startIndex).registerA + val returnEmptyComponentInstruction = + instructions.last { it.opcode == Opcode.INVOKE_STATIC } val returnEmptyComponentRegister = (returnEmptyComponentInstruction as FiveRegisterInstruction).registerC val freeRegister = - findFreeRegister(insertIndex, conversionContextRegister, returnEmptyComponentRegister) + findFreeRegister( + insertIndex, + conversionContextRegister, + returnEmptyComponentRegister + ) addInstructionsWithLabels( insertIndex, @@ -239,7 +308,7 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( // endregion - // region Watermark (legacy code for old versions of YouTube) + // region Hide watermark (legacy code for old versions of YouTube) playerOverlayMethod.immutableClassDef.getShowWatermarkMethod().apply { val index = implementation!!.instructions.size - 5 @@ -256,25 +325,94 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( // endregion - // region Show more button + // region Hide Show more button - (if (is_20_26_or_greater) hideShowMoreButtonMethodMatch else hideShowMoreLegacyButtonMethodMatch).let { + val (textViewField, buttonContainerField) = hideShowMoreButtonSetViewMethodMatch.let { + val textViewIndex = it[1] + val buttonContainerIndex = it[3] + + Pair( + it.method.getInstruction(textViewIndex).reference, + it.method.getInstruction(buttonContainerIndex).reference + ) + } + + val parentViewMethod = hideShowMoreButtonSetViewMethodMatch.immutableClassDef + .getHideShowMoreButtonGetParentViewMethod() + + hideShowMoreButtonSetViewMethodMatch.immutableClassDef.getHideShowMoreButtonMethod().apply { + val helperMethod = ImmutableMethod( + definingClass, + "patch_hideShowMoreButton", + listOf(), + "V", + AccessFlags.PRIVATE.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(7), + ).toMutable().apply { + addInstructions( + 0, + """ + move-object/from16 v0, p0 + invoke-virtual { v0 }, $parentViewMethod + move-result-object v1 + iget-object v2, v0, $buttonContainerField + iget-object v3, v0, $textViewField + invoke-static { v1, v2, v3 }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideShowMoreButton(Landroid/view/View;Landroid/view/View;Landroid/widget/TextView;)V + return-void + """ + ) + }.also(classDef.methods::add) + + findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + addInstruction(index, "invoke-direct/range { p0 .. p0 }, $helperMethod") + } + } + + // endregion + + + // region Hide Subscribed channels bar + + // Tablet + val methodMatch = if (is_20_21_or_greater) + hideSubscribedChannelsBarConstructorMethodMatch + else hideSubscribedChannelsBarConstructorLegacyMethodMatch + + methodMatch.let { it.method.apply { - val moveRegisterIndex = it[-1] - val viewRegister = getInstruction(moveRegisterIndex).registerA + val index = it[1] + val register = getInstruction(index).registerA - val insertIndex = moveRegisterIndex + 1 addInstruction( - insertIndex, - "invoke-static { v$viewRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" + - "->hideShowMoreButton(Landroid/view/View;)V", + index + 1, + "invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" + + "->hideSubscribedChannelsBar(Landroid/view/View;)V", + ) + } + } + + // Phone (landscape mode) + methodMatch.immutableClassDef.hideSubscribedChannelsBarLandscapeMethodMatch.let { + it.method.apply { + val index = it[-1] + val register = getInstruction(index).registerA + + addInstructions( + index + 1, + """ + invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideSubscribedChannelsBar(I)I + move-result v$register + """ ) } } // endregion - // region crowdfunding box + // region Hide Crowdfunding box + crowdfundingBoxMethodMatch.let { it.method.apply { val insertIndex = it[-1] @@ -283,14 +421,14 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( addInstruction( insertIndex, "invoke-static {v$objectRegister}, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" + - "->hideCrowdfundingBox(Landroid/view/View;)V", + "->hideCrowdfundingBox(Landroid/view/View;)V", ) } } // endregion - // region hide album cards + // region Hide Album cards albumCardsMethodMatch.let { it.method.apply { @@ -301,14 +439,14 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( addInstruction( insertIndex, "invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" + - "->hideAlbumCard(Landroid/view/View;)V", + "->hideAlbumCard(Landroid/view/View;)V", ) } } // endregion - // region hide floating microphone + // region Hide Floating microphone showFloatingMicrophoneButtonMethodMatch.let { it.method.apply { @@ -327,7 +465,27 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( // endregion - // region 'Yoodles' + // region Hide latest videos button + + listOf( + latestVideosContentPillMethodMatch, + latestVideosBarMethodMatch, + ).forEach { match -> + match.method.apply { + val moveIndex = match[-1] + val viewRegister = getInstruction(moveIndex).registerA + + addInstruction( + moveIndex + 1, + "invoke-static { v$viewRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" + + "->hideLatestVideosButton(Landroid/view/View;)V" + ) + } + } + + // endregion + + // region Hide 'Yoodles' yoodlesImageViewMethod.apply { findInstructionIndicesReversedOrThrow { @@ -339,14 +497,14 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( replaceInstruction( insertIndex, "invoke-static { v$imageViewRegister, v$drawableRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->" + - "setDoodleDrawable(Landroid/widget/ImageView;Landroid/graphics/drawable/Drawable;)V", + "setDoodleDrawable(Landroid/widget/ImageView;Landroid/graphics/drawable/Drawable;)V", ) } } // endregion - // region hide view count + // region Hide view count hideViewCountMethodMatch.method.apply { val startIndex = hideViewCountMethodMatch[0] @@ -356,10 +514,10 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( val applyDimensionIndex = indexOfFirstInstructionReversedOrThrow { val reference = getReference() opcode == Opcode.INVOKE_STATIC && - reference?.definingClass == "Landroid/util/TypedValue;" && - reference.returnType == "F" && - reference.name == "applyDimension" && - reference.parameterTypes == listOf("I", "F", "Landroid/util/DisplayMetrics;") + reference?.definingClass == "Landroid/util/TypedValue;" && + reference.returnType == "F" && + reference.name == "applyDimension" && + reference.parameterTypes == listOf("I", "F", "Landroid/util/DisplayMetrics;") } // A float value is passed which is used to determine subtitle text size. @@ -378,7 +536,7 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( // endregion - // region hide filter bar + // region Hide filter bar /** * Patch a [Method] with a given [instructions]. @@ -416,6 +574,179 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( relatedChipCloudMethodMatch.patch(1) { register -> "invoke-static { v$register }, " + - "$LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInRelatedVideos(Landroid/view/View;)V" + "$LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInRelatedVideos(Landroid/view/View;)V" } + + // endregion + + // region Hide You may like section + + if (is_20_21_or_greater) { + val searchSuggestionEndpointField = + searchSuggestionEndpointConstructorMethod.immutableClassDef + .searchSuggestionEndpointMethodMatch.let { + it.method.getInstruction(it[0]).fieldReference!! + } + val searchSuggestionEndpointClass = searchSuggestionEndpointField.definingClass + + searchBoxTypingStringMethodMatch.let { + it.method.apply { + // A collection of search suggestions. + // This includes trending search (also known as 'You may like' section) + // and your search history. + + val searchSuggestionCollectionField = getInstruction(it[0]).fieldReference!! + val typedStringField = getInstruction(it[2]).fieldReference!! + + val helperMethod = ImmutableMethod( + definingClass, + "patch_setSearchSuggestions", + listOf( + ImmutableMethodParameter( + parameterTypes.first().toString(), + null, + null + ) + ), + "V", + AccessFlags.PRIVATE.value or AccessFlags.FINAL.value, + annotations, + null, + MutableMethodImplementation(7), + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + move-object/from16 v0, p1 + iget-object v1, v0, $typedStringField + + # Check if the setting is enabled and if the typed string is empty. + invoke-static { v1 }, ${LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR}->hideYouMayLikeSection(Ljava/lang/String;)Z + move-result v1 + + # If the setting is disabled or the typed string is not empty, do nothing. + if-eqz v1, :ignore + + ## Get a collection of search suggestions. + iget-object v1, v0, $searchSuggestionCollectionField + + # Iterate through the collection and check if the search suggestion is the search history. + invoke-interface { v1 }, Ljava/util/Collection;->iterator()Ljava/util/Iterator; + move-result-object v2 + + :loop + invoke-interface { v2 }, Ljava/util/Iterator;->hasNext()Z + move-result v3 + if-eqz v3, :exit + invoke-interface { v2 }, Ljava/util/Iterator;->next()Ljava/lang/Object; + move-result-object v3 + instance-of v4, v3, $searchSuggestionEndpointClass + if-eqz v4, :loop + check-cast v3, $searchSuggestionEndpointClass + + # Each search suggestion has a command endpoint. + # If the search suggestion is the search history, the command includes the keyword '/delete'. + iget-object v4, v3, $searchSuggestionEndpointField + invoke-static { v3, v4 }, ${LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR}->isSearchHistory(Ljava/lang/Object;Ljava/lang/String;)Z + move-result v3 + + # If this search suggestion is the search history, do nothing. + if-nez v3, :loop + + # If this search suggestion is not the search history, remove it from the search suggestions collection. + invoke-interface { v2 }, Ljava/util/Iterator;->remove()V + goto :loop + + # Save the updated collection to a field. + :exit + iput-object v1, v0, $searchSuggestionCollectionField + + :ignore + return-void + """ + ) + }.also(it.classDef.methods::add) + + addInstruction( + 0, + "invoke-direct/range { p0 .. p1 }, $helperMethod" + ) + } + } + } + + // endregion + + // region Hide flyout menu items + + bottomSheetMenuItemBuilderMethodMatch.let { + it.method.apply { + val index = it[1] + val register = getInstruction(index).registerA + + addInstructions( + index + 1, + """ + invoke-static { v$register }, ${LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR}->hideFlyoutMenu(Ljava/lang/CharSequence;)Ljava/lang/CharSequence; + move-result-object v$register + """ + ) + } + } + + contextualMenuItemBuilderMethodMatch.let { + it.method.apply { + val index = it[1] + val targetInstruction = getInstruction(index) + + addInstruction( + index + 1, + "invoke-static { v${targetInstruction.registerC}, v${targetInstruction.registerD} }, " + + "${LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR}->hideFlyoutMenu(Landroid/widget/TextView;Ljava/lang/CharSequence;)V" + ) + } + } + + // endregion + + // region Hide channel tab + + channelTabRendererMethod.apply { + val iteratorIndex = indexOfFirstInstructionReversedOrThrow { + methodReference?.name == "hasNext" + } + + val iteratorRegister = getInstruction(iteratorIndex).registerC + val targetIndex = indexOfFirstInstructionReversedOrThrow { + val reference = methodReference + + opcode == Opcode.INVOKE_INTERFACE && + reference?.returnType == channelTabBuilderMethod.returnType && + reference.parameterTypes == channelTabBuilderMethod.parameterTypes + } + + val objectIndex = indexOfFirstInstructionReversedOrThrow( + targetIndex, + Opcode.IGET_OBJECT + ) + val objectInstruction = getInstruction(objectIndex) + val objectReference = getInstruction(objectIndex).reference + + addInstructionsWithLabels( + objectIndex + 1, + """ + invoke-static { v${objectInstruction.registerA} }, ${LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR}->hideChannelTab(Ljava/lang/String;)Z + move-result v${objectInstruction.registerA} + if-eqz v${objectInstruction.registerA}, :ignore + invoke-interface { v$iteratorRegister }, Ljava/util/Iterator;->remove()V + goto :next_iterator + :ignore + iget-object v${objectInstruction.registerA}, v${objectInstruction.registerB}, $objectReference + """, + ExternalLabel("next_iterator", getInstruction(iteratorIndex)) + ) + } + + // endregion } + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt index d42b037990..ad8a320178 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt @@ -47,10 +47,12 @@ val hideInfoCardsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -61,21 +63,21 @@ val hideInfoCardsPatch = bytecodePatch( SwitchPreference("revanced_hide_info_cards"), ) - // Edit: This old non litho code may be obsolete and no longer used by any supported versions. + // Edit: This old non-litho code may be obsolete and no longer used by any supported versions. infocardsIncognitoParentMethod.immutableClassDef.getInfocardsIncognitoMethod().apply { val invokeInstructionIndex = implementation!!.instructions.indexOfFirst { it.opcode.ordinal == Opcode.INVOKE_VIRTUAL.ordinal && - ((it as ReferenceInstruction).reference.toString() == "Landroid/view/View;->setVisibility(I)V") + ((it as ReferenceInstruction).reference.toString() == "Landroid/view/View;->setVisibility(I)V") } addInstruction( invokeInstructionIndex, "invoke-static {v${getInstruction(invokeInstructionIndex).registerC}}," + - " Lapp/revanced/extension/youtube/patches/HideInfoCardsPatch;->hideInfoCardsIncognito(Landroid/view/View;)V", + " Lapp/revanced/extension/youtube/patches/HideInfoCardsPatch;->hideInfoCardsIncognito(Landroid/view/View;)V", ) } - // Edit: This old non litho code may be obsolete and no longer used by any supported versions. + // Edit: This old non-litho code may be obsolete and no longer used by any supported versions. infocardsMethodCallMethodMatch.let { val invokeInterfaceIndex = it[-1] it.method.apply { @@ -97,7 +99,8 @@ val hideInfoCardsPatch = bytecodePatch( } // Info cards can also appear as Litho components. - val filterClassDescriptor = "Lapp/revanced/extension/youtube/patches/litho/HideInfoCardsFilter;" + val filterClassDescriptor = + "Lapp/revanced/extension/youtube/patches/litho/HideInfoCardsFilter;" addLithoFilter(filterClassDescriptor) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenupanel/HidePlayerFlyoutMenuPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenu/HidePlayerFlyoutMenuPatch.kt similarity index 90% rename from patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenupanel/HidePlayerFlyoutMenuPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenu/HidePlayerFlyoutMenuPatch.kt index 9fae036084..abbe5f023d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenupanel/HidePlayerFlyoutMenuPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenu/HidePlayerFlyoutMenuPatch.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.youtube.layout.hide.player.flyoutmenupanel +package app.revanced.patches.youtube.layout.hide.player.flyoutmenu import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.all.misc.resources.addResources @@ -25,15 +25,18 @@ val hidePlayerFlyoutMenuItemsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) apply { - val filterClassDescriptor = "Lapp/revanced/extension/youtube/patches/litho/PlayerFlyoutMenuItemsFilter;" + val filterClassDescriptor = + "Lapp/revanced/extension/youtube/patches/litho/PlayerFlyoutMenuItemsFilter;" addResources("youtube", "layout.hide.player.flyoutmenupanel.hidePlayerFlyoutMenuPatch") @@ -42,24 +45,24 @@ val hidePlayerFlyoutMenuItemsPatch = bytecodePatch( key = "revanced_hide_player_flyout", preferences = setOf( SwitchPreference("revanced_hide_player_flyout_captions"), - SwitchPreference("revanced_hide_player_flyout_additional_settings"), - SwitchPreference("revanced_hide_player_flyout_loop_video"), - SwitchPreference("revanced_hide_player_flyout_ambient_mode"), - SwitchPreference("revanced_hide_player_flyout_stable_volume"), SwitchPreference("revanced_hide_player_flyout_listen_with_youtube_music"), SwitchPreference("revanced_hide_player_flyout_help"), SwitchPreference("revanced_hide_player_flyout_speed"), SwitchPreference("revanced_hide_player_flyout_lock_screen"), SwitchPreference( key = "revanced_hide_player_flyout_audio_track", - tag = "app.revanced.extension.youtube.settings.preference.HideAudioFlyoutMenuPreference", + tag = "app.revanced.extension.youtube.settings.preference.HideAudioFlyoutMenuPreference" ), - SwitchPreference("revanced_hide_player_flyout_watch_in_vr"), - SwitchPreference("revanced_hide_player_flyout_sleep_timer"), SwitchPreference("revanced_hide_player_flyout_video_quality"), SwitchPreference("revanced_hide_player_flyout_video_quality_footer"), + SwitchPreference("revanced_hide_player_flyout_additional_settings"), + SwitchPreference("revanced_hide_player_flyout_ambient_mode"), + SwitchPreference("revanced_hide_player_flyout_stable_volume"), + SwitchPreference("revanced_hide_player_flyout_loop_video"), + SwitchPreference("revanced_hide_player_flyout_sleep_timer"), + SwitchPreference("revanced_hide_player_flyout_watch_in_vr"), ), - ), + ) ) addLithoFilter(filterClassDescriptor) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/PlayerPopupPanelsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/popup/PlayerPopupPanelsPatch.kt similarity index 78% rename from patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/PlayerPopupPanelsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/popup/PlayerPopupPanelsPatch.kt index 6e952f4461..dfebabb2f5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/PlayerPopupPanelsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/popup/PlayerPopupPanelsPatch.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.youtube.layout.panels.popup +package app.revanced.patches.youtube.layout.hide.player.popup import app.revanced.patcher.extensions.addInstructionsWithLabels import app.revanced.patcher.patch.bytecodePatch @@ -8,8 +8,10 @@ import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.getEngagementPanelControllerMethodMatch -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/DisablePlayerPopupPanelsPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/DisablePlayerPopupPanelsPatch;" @Suppress("unused") val disablePlayerPopupPanelsPatch = bytecodePatch( @@ -24,10 +26,12 @@ val disablePlayerPopupPanelsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -38,7 +42,7 @@ val disablePlayerPopupPanelsPatch = bytecodePatch( SwitchPreference("revanced_hide_player_popup_panels"), ) - engagementPanelControllerMethod.addInstructionsWithLabels( + getEngagementPanelControllerMethodMatch().method.addInstructionsWithLabels( 0, """ invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->disablePlayerPopupPanels()Z diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatch.kt index ad93bc6060..3ebbb974f4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatch.kt @@ -30,10 +30,12 @@ val hideRelatedVideoOverlayPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch.kt index 0372dfb2e0..7700ff0a57 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch.kt @@ -30,10 +30,12 @@ val disableRollingNumberAnimationsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/Fingerprints.kt index 8915cac958..d730022f92 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/Fingerprints.kt @@ -7,6 +7,21 @@ import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.ClassDef +internal val BytecodePatchContext.componentContextParserMethod by gettingFirstImmutableMethodDeclaratively { + returnType("L") + instructions( + "Failed to parse Element proto."(), + "Cannot read theme key from model."() + ) +} + +context(_: BytecodePatchContext) +internal fun ClassDef.getTreeNodeResultListMethod() = firstMethodDeclaratively { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returnType("Ljava/util/List;") + instructions(allOf(Opcode.INVOKE_STATIC(), method("nCopies"))) +} + internal val BytecodePatchContext.shortsBottomBarContainerMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") @@ -19,9 +34,6 @@ internal val BytecodePatchContext.shortsBottomBarContainerMethodMatch by composi ) } -/** - * 19.41 to 20.44. - */ context(_: BytecodePatchContext) internal fun ClassDef.getRenderBottomNavigationBarMethodMatch() = firstMethodDeclaratively { @@ -34,10 +46,10 @@ internal fun ClassDef.getRenderBottomNavigationBarMethodMatch() = firstMethodDec after(Opcode.IF_EQZ()), after(Opcode.INVOKE_INTERFACE()), Opcode.MONITOR_EXIT(), - after(Opcode.RETURN_VOID()), - after(Opcode.MOVE_EXCEPTION()), - after(Opcode.MONITOR_EXIT()), - after(Opcode.THROW()), + Opcode.RETURN_VOID(), + Opcode.MOVE_EXCEPTION(), + Opcode.MONITOR_EXIT(), + Opcode.THROW() ) } @@ -59,8 +71,7 @@ internal val BytecodePatchContext.legacyRenderBottomNavigationBarLegacyParentMet } /** - * Identical to [legacyRenderBottomNavigationBarLegacyParentMethod] - * except this has an extra parameter. + * 19.41 - 20.44 */ internal val BytecodePatchContext.renderBottomNavigationBarLegacy1941ParentMethod by gettingFirstImmutableMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) @@ -78,6 +89,9 @@ internal val BytecodePatchContext.renderBottomNavigationBarLegacy1941ParentMetho ) } +/** + * 20.45+ + */ internal val BytecodePatchContext.renderBottomNavigationBarParentMethod by gettingFirstImmutableMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("[Ljava/lang/Class;") diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt index 632934da7b..08ae72ea1b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt @@ -12,11 +12,13 @@ import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.music.shared.conversionContextToStringMethod import app.revanced.patches.shared.misc.mapping.ResourceType import app.revanced.patches.shared.misc.mapping.resourceMappingPatch import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.shared.misc.litho.filter.addLithoFilter +import app.revanced.patches.youtube.misc.engagement.engagementPanelHookPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch @@ -25,8 +27,6 @@ import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.util.* import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.MethodReference -import java.util.logging.Logger internal val hideShortsAppShortcutOption = booleanOption( name = "Hide Shorts app shortcut", @@ -54,82 +54,65 @@ private val hideShortsComponentsResourcePatch = resourcePatch { addResources("youtube", "layout.hide.shorts.hideShortsComponentsResourcePatch") - val preferences = mutableSetOf( - // Shorts player components. - // Ideally each group should be ordered similar to how they appear in the UI - - // Vertical row of buttons on right side of the screen. - SwitchPreference("revanced_hide_shorts_like_fountain"), - SwitchPreference("revanced_hide_shorts_like_button"), - SwitchPreference("revanced_hide_shorts_dislike_button"), - ) - - if (is_20_22_or_greater) { - // FIXME: The buffer is very different for 20.22+ and these current cannot be hidden. - Logger.getLogger(this::class.java.name).warning( - "\n!!!" + - "\n!!! Shorts action buttons currently cannot be set hidden when patching 20.22+" + - "\n!!! Patch 20.21.37 or lower if you want to hide Shorts action buttons" + - "\n!!!", - ) - } else { - preferences.addAll( - listOf( - SwitchPreference("revanced_hide_shorts_comments_button"), - SwitchPreference("revanced_hide_shorts_share_button"), - SwitchPreference("revanced_hide_shorts_remix_button"), - SwitchPreference("revanced_hide_shorts_sound_button"), - ), - ) - } - - preferences.addAll( - listOf( - // Upper and middle area of the player. - SwitchPreference("revanced_hide_shorts_join_button"), - SwitchPreference("revanced_hide_shorts_subscribe_button"), - SwitchPreference("revanced_hide_shorts_paused_overlay_buttons"), - - // Suggested actions. - SwitchPreference("revanced_hide_shorts_preview_comment"), - SwitchPreference("revanced_hide_shorts_save_sound_button"), - SwitchPreference("revanced_hide_shorts_use_sound_button"), - SwitchPreference("revanced_hide_shorts_use_template_button"), - SwitchPreference("revanced_hide_shorts_upcoming_button"), - SwitchPreference("revanced_hide_shorts_effect_button"), - SwitchPreference("revanced_hide_shorts_green_screen_button"), - SwitchPreference("revanced_hide_shorts_hashtag_button"), - SwitchPreference("revanced_hide_shorts_live_preview"), - SwitchPreference("revanced_hide_shorts_new_posts_button"), - SwitchPreference("revanced_hide_shorts_shop_button"), - SwitchPreference("revanced_hide_shorts_tagged_products"), - SwitchPreference("revanced_hide_shorts_search_suggestions"), - SwitchPreference("revanced_hide_shorts_super_thanks_button"), - SwitchPreference("revanced_hide_shorts_stickers"), - - // Bottom of the screen. - SwitchPreference("revanced_hide_shorts_auto_dubbed_label"), - SwitchPreference("revanced_hide_shorts_location_label"), - SwitchPreference("revanced_hide_shorts_channel_bar"), - SwitchPreference("revanced_hide_shorts_info_panel"), - SwitchPreference("revanced_hide_shorts_full_video_link_label"), - SwitchPreference("revanced_hide_shorts_video_title"), - SwitchPreference("revanced_hide_shorts_sound_metadata_label"), - SwitchPreference("revanced_hide_shorts_navigation_bar"), - ), - ) - PreferenceScreen.SHORTS.addPreferences( + SwitchPreference("revanced_hide_shorts_channel"), SwitchPreference("revanced_hide_shorts_home"), SwitchPreference("revanced_hide_shorts_search"), SwitchPreference("revanced_hide_shorts_subscriptions"), + SwitchPreference("revanced_hide_shorts_video_description"), SwitchPreference("revanced_hide_shorts_history"), PreferenceScreenPreference( key = "revanced_shorts_player_screen", sorting = PreferenceScreenPreference.Sorting.UNSORTED, - preferences = preferences, - ), + preferences = setOf( + // Shorts player components. + // Ideally each group should be ordered similar to how they appear in the UI + + // Vertical row of buttons on right side of the screen. + // Like fountain may no longer be used by YT anymore. + //SwitchPreference("revanced_hide_shorts_like_fountain"), + SwitchPreference("revanced_hide_shorts_like_button"), + SwitchPreference("revanced_hide_shorts_dislike_button"), + SwitchPreference("revanced_hide_shorts_comments_button"), + SwitchPreference("revanced_hide_shorts_share_button"), + SwitchPreference("revanced_hide_shorts_remix_button"), + SwitchPreference("revanced_hide_shorts_sound_button"), + + // Upper and middle area of the player. + SwitchPreference("revanced_hide_shorts_join_button"), + SwitchPreference("revanced_hide_shorts_subscribe_button"), + SwitchPreference("revanced_hide_shorts_paused_overlay_buttons"), + + // Suggested actions. + SwitchPreference("revanced_hide_shorts_preview_comment"), + SwitchPreference("revanced_hide_shorts_save_sound_button"), + SwitchPreference("revanced_hide_shorts_use_sound_button"), + SwitchPreference("revanced_hide_shorts_use_template_button"), + SwitchPreference("revanced_hide_shorts_upcoming_button"), + SwitchPreference("revanced_hide_shorts_effect_button"), + SwitchPreference("revanced_hide_shorts_green_screen_button"), + SwitchPreference("revanced_hide_shorts_hashtag_button"), + SwitchPreference("revanced_hide_shorts_live_preview"), + SwitchPreference("revanced_hide_shorts_new_posts_button"), + SwitchPreference("revanced_hide_shorts_shop_button"), + SwitchPreference("revanced_hide_shorts_tagged_products"), + SwitchPreference("revanced_hide_shorts_search_suggestions"), + SwitchPreference("revanced_hide_shorts_super_thanks_button"), + SwitchPreference("revanced_hide_shorts_stickers"), + + // Bottom of the screen. + SwitchPreference("revanced_hide_shorts_ai_button"), + SwitchPreference("revanced_hide_shorts_auto_dubbed_label"), + SwitchPreference("revanced_hide_shorts_location_label"), + SwitchPreference("revanced_hide_shorts_channel_bar"), + SwitchPreference("revanced_hide_shorts_info_panel"), + SwitchPreference("revanced_hide_shorts_full_video_link_label"), + SwitchPreference("revanced_hide_shorts_video_title"), + SwitchPreference("revanced_hide_shorts_sound_metadata_label"), + SwitchPreference("revanced_hide_shorts_navigation_bar"), + ), + ) ) // Verify the file has the expected node, even if the patch option is off. @@ -157,28 +140,33 @@ private val hideShortsComponentsResourcePatch = resourcePatch { } } -private const val FILTER_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/litho/ShortsFilter;" +private const val FILTER_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/litho/ShortsFilter;" @Suppress("unused") val hideShortsComponentsPatch = bytecodePatch( name = "Hide Shorts components", - description = "Adds options to hide components related to Shorts.", + description = "Adds options to hide components related to Shorts. " + + "Patching version 20.21.37 or lower can hide more Shorts player button types." ) { dependsOn( sharedExtensionPatch, lithoFilterPatch, hideShortsComponentsResourcePatch, resourceMappingPatch, + engagementPanelHookPatch, navigationBarHookPatch, versionCheckPatch, ) compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - // 20.22+ does not yet support hiding Shorts action buttons. + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -188,26 +176,66 @@ val hideShortsComponentsPatch = bytecodePatch( apply { addLithoFilter(FILTER_CLASS_DESCRIPTOR) + // region Hide sound button. + val id = ResourceType.DIMEN["reel_player_right_pivot_v2_size"] - forEachInstructionAsSequence({ _, method, instruction, index -> - if (instruction.wideLiteral != id) return@forEachInstructionAsSequence null + if (!is_21_05_or_greater) { + forEachInstructionAsSequence({ _, method, instruction, index -> + if (instruction.wideLiteral != id) return@forEachInstructionAsSequence null - val targetIndex = method.indexOfFirstInstructionOrThrow(index) { - methodReference?.name == "getDimensionPixelSize" - } + 1 + val targetIndex = method.indexOfFirstInstructionOrThrow(index) { + methodReference?.name == "getDimensionPixelSize" + } + 1 - val sizeRegister = method.getInstruction(targetIndex).registerA + val sizeRegister = method.getInstruction(targetIndex).registerA - return@forEachInstructionAsSequence targetIndex to sizeRegister - }) { method, (targetIndex, sizeRegister) -> - firstMethod(method).addInstructions( - targetIndex + 1, - """ + return@forEachInstructionAsSequence targetIndex to sizeRegister + }) { method, (targetIndex, sizeRegister) -> + firstMethod(method).addInstructions( + targetIndex + 1, + """ invoke-static { v$sizeRegister }, $FILTER_CLASS_DESCRIPTOR->getSoundButtonSize(I)I move-result v$sizeRegister """, - ) + ) + } + } + + // endregion + + // region Hide action buttons. + + if (is_20_22_or_greater) { + componentContextParserMethod.immutableClassDef.getTreeNodeResultListMethod().apply { + val conversionContextPathBuilderField = + conversionContextToStringMethod.immutableClassDef + .fields.single { field -> field.type == "Ljava/lang/StringBuilder;" } + + val insertIndex = implementation!!.instructions.lastIndex + val listRegister = getInstruction(insertIndex).registerA + + val registerProvider = getFreeRegisterProvider(insertIndex, 2) + val freeRegister = registerProvider.getFreeRegister() + val pathRegister = registerProvider.getFreeRegister() + + addInstructionsAtControlFlowLabel( + insertIndex, + """ + move-object/from16 v$freeRegister, p2 + + # In YouTube 20.41 field is the abstract superclass. + # Verify it's the expected subclass just in case. + instance-of v$pathRegister, v$freeRegister, ${conversionContextToStringMethod.immutableClassDef} + if-eqz v$pathRegister, :ignore + + iget-object v$pathRegister, v$freeRegister, $conversionContextPathBuilderField + invoke-static { v$pathRegister, v$listRegister }, ${FILTER_CLASS_DESCRIPTOR}->hideActionButtons(Ljava/lang/StringBuilder;Ljava/util/List;)V + :ignore + nop + """ + ) + } } // endregion @@ -262,7 +290,7 @@ val hideShortsComponentsPatch = bytecodePatch( // region Disable experimental Shorts flags. // Flags might be present in earlier targets, but they are not found in 19.47.53. - // If these flags are forced on, the experimental layout is still not used and + // If these flags are forced on, the experimental layout is still not used, and // it appears the features requires additional server side data to fully use. if (is_20_07_or_greater) { // Experimental Shorts player uses Android native buttons and not Litho, @@ -274,7 +302,7 @@ val hideShortsComponentsPatch = bytecodePatch( shortsExperimentalPlayerFeatureFlagMethod.returnLate(false) // Experimental UI renderer must also be disabled since it requires the - // experimental Shorts player. If this is enabled but Shorts player + // experimental Shorts player. If this is enabled but Shorts player // is disabled then the app crashes when the Shorts player is opened. renderNextUIFeatureFlagMethod.returnLate(false) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTvPatchPopup.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTVPopupPatch.kt similarity index 83% rename from patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTvPatchPopup.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTVPopupPatch.kt index bd5d5eb64f..6195af9f4e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTvPatchPopup.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTVPopupPatch.kt @@ -11,7 +11,7 @@ import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch private const val EXTENSION_CLASS_DESCRIPTOR = - "Lapp/revanced/extension/youtube/patches/DisableSignInToTvPopupPatch;" + "Lapp/revanced/extension/youtube/patches/DisableSignInToTVPopupPatch;" @Suppress("unused") val disableSignInToTVPopupPatch = bytecodePatch( @@ -27,24 +27,26 @@ val disableSignInToTVPopupPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) apply { - addResources("youtube", "layout.hide.signintotv.disableSignInToTvPopupPatch") + addResources("youtube", "layout.hide.signintotv.disableSignInToTVPopupPatch") PreferenceScreen.MISC.addPreferences( - SwitchPreference("revanced_disable_signin_to_tv_popup"), + SwitchPreference("revanced_disable_sign_in_to_tv_popup"), ) - signInToTvPopupMethod.addInstructionsWithLabels( + signInToTVPopupMethod.addInstructionsWithLabels( 0, """ - invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->disableSignInToTvPopup()Z + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->disableSignInToTVPopup()Z move-result v0 if-eqz v0, :allow_sign_in_popup const/4 v0, 0x0 diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/Fingerprints.kt index fa3846fdb4..6a549c950f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/Fingerprints.kt @@ -7,7 +7,7 @@ import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.returnType import app.revanced.patches.shared.misc.mapping.ResourceType -internal val BytecodePatchContext.signInToTvPopupMethod by gettingFirstMethodDeclaratively { +internal val BytecodePatchContext.signInToTVPopupMethod by gettingFirstMethodDeclaratively { returnType("Z") parameterTypes("Ljava/lang/String;", "Z", "L") instructions(ResourceType.STRING("mdx_seamless_tv_sign_in_drawer_fragment_title")) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch.kt index 24b240232f..b9ae184e0b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch.kt @@ -9,7 +9,8 @@ import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/HideTimestampPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/HideTimestampPatch;" @Suppress("unused") val hideTimestampPatch = bytecodePatch( @@ -24,10 +25,12 @@ val hideTimestampPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/Fingerprints.kt index 796a308636..210af8b85e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/Fingerprints.kt @@ -12,8 +12,10 @@ import com.android.tools.smali.dexlib2.iface.ClassDef internal const val MINIPLAYER_MODERN_FEATURE_KEY = 45622882L -// In later targets this feature flag does nothing and is dead code. -internal const val MINIPLAYER_MODERN_FEATURE_LEGACY_KEY = 45630429L +internal const val MINIPLAYER_MODERN_TYPE_1_FEATURE_KEY = 45623000L +internal const val MINIPLAYER_MODERN_TYPE_2_FEATURE_KEY = 45623273L +internal const val MINIPLAYER_MODERN_TYPE_3_FEATURE_KEY = 45623076L +internal const val MINIPLAYER_MODERN_TYPE_4_FEATURE_KEY = 45674402L internal const val MINIPLAYER_DOUBLE_TAP_FEATURE_KEY = 45628823L internal const val MINIPLAYER_DRAG_DROP_FEATURE_KEY = 45628752L internal const val MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY = 45658112L @@ -21,11 +23,13 @@ internal const val MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY = 45652224L internal const val MINIPLAYER_INITIAL_SIZE_FEATURE_KEY = 45640023L internal const val MINIPLAYER_DISABLED_FEATURE_KEY = 45657015L internal const val MINIPLAYER_ANIMATED_EXPAND_FEATURE_KEY = 45644360L +// In later targets this feature flag does nothing and is dead code. +internal const val MINIPLAYER_MODERN_FEATURE_LEGACY_KEY = 45630429L internal val BytecodePatchContext.miniplayerModernConstructorMethod by gettingFirstMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) instructions( - 45623000L(), // Magic number found in the constructor. + MINIPLAYER_MODERN_TYPE_1_FEATURE_KEY(), ) } @@ -148,8 +152,14 @@ internal val BytecodePatchContext.miniplayerMinimumSizeMethodMatch by composingF accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) instructions( ResourceType.DIMEN("miniplayer_max_size"), - 192L(), // Default miniplayer width constant. - 128L(), // Default miniplayer height constant. + anyOf( // Default miniplayer width constant. + 192L(), + 192.0f.toRawBits().toLong()(), // 21.03+ + ), + anyOf( // Default miniplayer height constant. + 128L(), + 128.0f.toRawBits().toLong()(), // 21.03+ + ) ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt index a2effc6a04..0ef561d714 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt @@ -53,13 +53,15 @@ private val miniplayerResourcePatch = resourcePatch { // Only required for exactly 19.16 if (!is_19_17_or_greater) { - ytOutlinePictureInPictureWhite24 = ResourceType.DRAWABLE["yt_outline_picture_in_picture_white_24"] + ytOutlinePictureInPictureWhite24 = + ResourceType.DRAWABLE["yt_outline_picture_in_picture_white_24"] ytOutlineXWhite24 = ResourceType.DRAWABLE["yt_outline_x_white_24"] } } } -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/MiniplayerPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/MiniplayerPatch;" @Suppress("unused") val miniplayerPatch = bytecodePatch( @@ -75,10 +77,12 @@ val miniplayerPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -143,7 +147,10 @@ val miniplayerPatch = bytecodePatch( } if (is_19_26_or_greater) { - preferences += TextPreference("revanced_miniplayer_width_dip", inputType = InputType.NUMBER) + preferences += TextPreference( + "revanced_miniplayer_width_dip", + inputType = InputType.NUMBER + ) } preferences += TextPreference("revanced_miniplayer_opacity", inputType = InputType.NUMBER) @@ -167,7 +174,8 @@ val miniplayerPatch = bytecodePatch( ) } - fun Method.findReturnIndicesReversed() = findInstructionIndicesReversedOrThrow(Opcode.RETURN) + fun Method.findReturnIndicesReversed() = + findInstructionIndicesReversedOrThrow(Opcode.RETURN) /** * Adds an override to force legacy tablet miniplayer to be used or not used. @@ -227,11 +235,12 @@ val miniplayerPatch = bytecodePatch( // Parts of the YT code is removed in 20.37+ and the legacy player no longer works. if (!is_20_37_or_greater) { - miniplayerDimensionsCalculatorParentMethod.immutableClassDef.getMiniplayerOverrideNoContextMethod().apply { - findReturnIndicesReversed().forEach { index -> - insertLegacyTabletMiniplayerOverride(index) + miniplayerDimensionsCalculatorParentMethod.immutableClassDef.getMiniplayerOverrideNoContextMethod() + .apply { + findReturnIndicesReversed().forEach { index -> + insertLegacyTabletMiniplayerOverride(index) + } } - } // endregion @@ -265,7 +274,11 @@ val miniplayerPatch = bytecodePatch( insertModernMiniplayerTypeOverride(iPutIndex) } else { - findReturnIndicesReversed().forEach { index -> insertModernMiniplayerOverride(index) } + findReturnIndicesReversed().forEach { index -> + insertModernMiniplayerOverride( + index + ) + } } } } @@ -355,17 +368,23 @@ val miniplayerPatch = bytecodePatch( // YT fixed this mistake in 19.17. // Fix this, by swapping the drawable resource values with each other. if (!is_19_17_or_greater) { - miniplayerModernViewParentMethod.immutableClassDef.getMiniplayerModernExpandCloseDrawablesMethod().apply { - listOf( - ytOutlinePictureInPictureWhite24 to ytOutlineXWhite24, - ytOutlineXWhite24 to ytOutlinePictureInPictureWhite24, - ).forEach { (originalResource, replacementResource) -> - val imageResourceIndex = indexOfFirstLiteralInstructionOrThrow(originalResource) - val register = getInstruction(imageResourceIndex).registerA + miniplayerModernViewParentMethod.immutableClassDef.getMiniplayerModernExpandCloseDrawablesMethod() + .apply { + listOf( + ytOutlinePictureInPictureWhite24 to ytOutlineXWhite24, + ytOutlineXWhite24 to ytOutlinePictureInPictureWhite24, + ).forEach { (originalResource, replacementResource) -> + val imageResourceIndex = + indexOfFirstLiteralInstructionOrThrow(originalResource) + val register = + getInstruction(imageResourceIndex).registerA - replaceInstruction(imageResourceIndex, "const v$register, $replacementResource") + replaceInstruction( + imageResourceIndex, + "const v$register, $replacementResource" + ) + } } - } } // endregion @@ -377,7 +396,7 @@ val miniplayerPatch = bytecodePatch( findInstructionIndicesReversedOrThrow { val reference = getReference() opcode == Opcode.INVOKE_INTERFACE && - reference?.returnType == "Z" && reference.parameterTypes.isEmpty() + reference?.returnType == "Z" && reference.parameterTypes.isEmpty() }.forEach { index -> val register = getInstruction(index + 1).registerA @@ -417,11 +436,12 @@ val miniplayerPatch = bytecodePatch( } } - miniplayerModernViewParentMethod.immutableClassDef.getMiniplayerModernAddViewListenerMethod().addInstruction( - 0, - "invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->" + - "hideMiniplayerSubTexts(Landroid/view/View;)V", - ) + miniplayerModernViewParentMethod.immutableClassDef.getMiniplayerModernAddViewListenerMethod() + .addInstruction( + 0, + "invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->" + + "hideMiniplayerSubTexts(Landroid/view/View;)V", + ) // Modern 2 has a broken overlay subtitle view that is always present. // Modern 2 uses the same overlay controls as the regular video player, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/Fingerprints.kt deleted file mode 100644 index 28c218d9d1..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/Fingerprints.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.patches.youtube.layout.panels.popup - -import app.revanced.patcher.gettingFirstMethodDeclaratively -import app.revanced.patcher.patch.BytecodePatchContext -import app.revanced.patcher.returnType - -internal val BytecodePatchContext.engagementPanelControllerMethod by gettingFirstMethodDeclaratively( - "EngagementPanelController: cannot show EngagementPanel before EngagementPanelController.init() has been called.", - "[EngagementPanel] Cannot show EngagementPanel before EngagementPanelController.init() has been called.", -) { - returnType("L") -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/ExitFullscreenPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/ExitFullscreenPatch.kt index 9f04df3c06..236a6c05fe 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/ExitFullscreenPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/ExitFullscreenPatch.kt @@ -1,5 +1,6 @@ package app.revanced.patches.youtube.layout.player.fullscreen +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch @@ -9,10 +10,9 @@ import app.revanced.patches.youtube.misc.playercontrols.playerControlsPatch import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch -import app.revanced.patches.youtube.video.information.videoEndMethod +import app.revanced.patches.youtube.video.information.playerStatusMethod import app.revanced.patches.youtube.video.information.videoInformationPatch -import app.revanced.util.addInstructionsAtControlFlowLabel -import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.Opcode @Suppress("unused") @@ -22,10 +22,12 @@ val exitFullscreenPatch = bytecodePatch( ) { compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -51,13 +53,15 @@ val exitFullscreenPatch = bytecodePatch( ListPreference("revanced_exit_fullscreen"), ) - videoEndMethod.apply { - val insertIndex = indexOfFirstInstructionReversedOrThrow(Opcode.RETURN_VOID) + playerStatusMethod.apply { + // +1 to ensure inserted after the loop logic added by the "Loop video" patch. + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.SGET_OBJECT) + 1 - addInstructionsAtControlFlowLabel( + addInstruction( insertIndex, - "invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->endOfVideoReached()V", - ) + "invoke-static/range { p1 .. p1 }, " + + "$EXTENSION_CLASS_DESCRIPTOR->endOfVideoReached(Ljava/lang/Enum;)V", + ) } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/OpenVideosFullscreenHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/OpenVideosFullscreenHookPatch.kt index 5f53f6cb7a..1828079230 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/OpenVideosFullscreenHookPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/OpenVideosFullscreenHookPatch.kt @@ -36,7 +36,7 @@ internal val openVideosFullscreenHookPatch = bytecodePatch { // Remove A/B feature call that forces what this patch already does. // Cannot use the A/B flag to accomplish the same goal because 19.50+ // Shorts fullscreen regular player does not use fullscreen - // if the player is minimized and it must be forced using other conditional check. + // if the player is minimized, and it must be forced using other conditional check. it.method.insertLiteralOverride( it[-1], false, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/OpenVideosFullscreenPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/OpenVideosFullscreenPatch.kt index c1a70e42d4..79f044a6e7 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/OpenVideosFullscreenPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/OpenVideosFullscreenPatch.kt @@ -21,12 +21,13 @@ val openVideosFullscreenPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", - "19.43.41", "19.47.53", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch.kt index 4bb4c275d0..d4dd999ef5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch.kt @@ -28,10 +28,12 @@ val customPlayerOverlayOpacityPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -45,12 +47,13 @@ val customPlayerOverlayOpacityPatch = bytecodePatch( createPlayerOverviewMethodMatch.let { it.method.apply { val viewRegisterIndex = it[-1] - val viewRegister = getInstruction(viewRegisterIndex).registerA + val viewRegister = + getInstruction(viewRegisterIndex).registerA addInstruction( viewRegisterIndex + 1, "invoke-static { v$viewRegister }, " + - "$EXTENSION_CLASS_DESCRIPTOR->changeOpacity(Landroid/widget/ImageView;)V", + "$EXTENSION_CLASS_DESCRIPTOR->changeOpacity(Landroid/widget/ImageView;)V", ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt index 7588c8d899..470cad9cb2 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt @@ -2,6 +2,7 @@ package app.revanced.patches.youtube.layout.returnyoutubedislike import app.revanced.patcher.* import app.revanced.patcher.extensions.instructions +import app.revanced.patcher.gettingFirstMethodDeclaratively import app.revanced.patcher.patch.BytecodePatchContext import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode @@ -70,7 +71,8 @@ internal val BytecodePatchContext.rollingNumberSetterMethodMatch by composingFir Opcode.IGET_OBJECT, ) - val match = indexedMatcher("RollingNumberType required properties missing! Need"(String::contains)) + val match = + indexedMatcher("RollingNumberType required properties missing! Need"(String::contains)) custom { match(instructions) } } @@ -88,15 +90,17 @@ internal val BytecodePatchContext.rollingNumberTextViewMethod by gettingFirstMet ) custom { immutableClassDef.superclass == "Landroid/support/v7/widget/AppCompatTextView;" || immutableClassDef.superclass == - "Lcom/google/android/libraries/youtube/rendering/ui/spec/typography/YouTubeAppCompatTextView;" + "Lcom/google/android/libraries/youtube/rendering/ui/spec/typography/YouTubeAppCompatTextView;" } } internal val BytecodePatchContext.textComponentConstructorMethod by gettingFirstImmutableMethodDeclaratively { - accessFlags(AccessFlags.CONSTRUCTOR, AccessFlags.PRIVATE) - instructions( - "TextComponent"(), - ) + custom { + // 20.23+ is public. + // 20.22 and lower is private. + AccessFlags.CONSTRUCTOR.isSet(accessFlags) + } + instructions("TextComponent"()) } internal val BytecodePatchContext.textComponentDataMethod by gettingFirstImmutableMethodDeclaratively { @@ -119,9 +123,35 @@ internal fun ClassDef.getTextComponentLookupMethod() = firstMethodDeclaratively instructions("…"()) } -internal val BytecodePatchContext.textComponentFeatureFlagMethod by gettingFirstMethodDeclaratively { +internal val BytecodePatchContext.textComponentFeatureFlagMethodMatch by composingFirstMethod { accessFlags(AccessFlags.FINAL) returnType("Z") parameterTypes() instructions(45675738L()) } + + +internal val BytecodePatchContext.lithoSpannableStringCreationMethodMatch by composingFirstMethod { + accessFlags(AccessFlags.PROTECTED, AccessFlags.FINAL) + returnType("V") + parameterTypes("L", "Ljava/lang/Object;", "L") + instructions( + allOf(Opcode.NEW_INSTANCE(), type("Landroid/text/SpannableString;")), + afterAtMost( + 5, + method { + toString() == "Landroid/text/SpannableString;->(Ljava/lang/CharSequence;)V" + } + ), + afterAtMost( + 5, + method { + toString() == "Landroid/text/SpannableString;->getSpans(IILjava/lang/Class;)[Ljava/lang/Object;" + } + ), + method { + name == "addOnLayoutChangeListener" && parameterTypes.size == 1 && + parameterTypes.first() == $$"Landroid/view/View$OnLayoutChangeListener;" + } + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt index bb81a31c0c..b216bcb201 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt @@ -27,15 +27,15 @@ import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId import app.revanced.patches.youtube.video.videoid.hookVideoId import app.revanced.patches.youtube.video.videoid.videoIdPatch import app.revanced.util.addInstructionsAtControlFlowLabel +import app.revanced.util.cloneMutableAndPreserveParameters import app.revanced.util.findFreeRegister import app.revanced.util.indexOfFirstInstructionOrThrow -import app.revanced.util.returnLate +import app.revanced.util.insertLiteralOverride import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction -import java.util.logging.Logger private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch;" @@ -60,11 +60,12 @@ val returnYouTubeDislikePatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", - // 20.40+ does not support yet support the Shorts player. + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -95,7 +96,7 @@ val returnYouTubeDislikePatch = bytecodePatch( hookVideoId("$EXTENSION_CLASS_DESCRIPTOR->newVideoLoaded(Ljava/lang/String;)V") - // Hook the player response video id, to start loading RYD sooner in the background. + // Hook the player response video ID, to start loading RYD sooner in the background. hookPlayerResponseVideoId("$EXTENSION_CLASS_DESCRIPTOR->preloadVideoId(Ljava/lang/String;Z)V") // endregion @@ -111,8 +112,8 @@ val returnYouTubeDislikePatch = bytecodePatch( 0, """ const/4 v0, ${vote.value} - invoke-static {v0}, $EXTENSION_CLASS_DESCRIPTOR->sendVote(I)V - """, + invoke-static { v0 }, $EXTENSION_CLASS_DESCRIPTOR->sendVote(I)V + """ ) } @@ -126,94 +127,115 @@ val returnYouTubeDislikePatch = bytecodePatch( // Find the field name of the conversion context. val conversionContextClass = conversionContextToStringMethod.immutableClassDef - val textComponentConversionContextField = textComponentConstructorMethod.immutableClassDef.fields.find { - it.type == conversionContextClass.type || - // 20.41+ uses superclass field type. - it.type == conversionContextClass.superclass - } ?: throw PatchException("Could not find conversion context field") + val textComponentConversionContextField = + textComponentConstructorMethod.immutableClassDef.fields.find { + it.type == conversionContextClass.type || + // 20.41+ uses superclass field type. + it.type == conversionContextClass.superclass + } ?: throw PatchException("Could not find conversion context field") - textComponentConstructorMethod.immutableClassDef.getTextComponentLookupMethod().apply { - // Find the instruction for creating the text data object. - val textDataClassType = textComponentDataMethod.immutableClassDef.type + val conversionContextPathBuilderField = conversionContextToStringMethod.immutableClassDef + .fields.single { field -> field.type == "Ljava/lang/StringBuilder;" } - val insertIndex: Int - val charSequenceRegister: Int + // Old pre 20.40 and lower hook. + // 21.05 clobbers p0 (this) register. + // Add additional registers so all parameters including p0 are free to use anywhere in the method. + textComponentConstructorMethod.immutableClassDef + .getTextComponentLookupMethod() + .cloneMutableAndPreserveParameters().apply { + // Find the instruction for creating the text data object. + val textDataClassType = textComponentDataMethod.immutableClassDef.type - if (is_19_33_or_greater && !is_20_10_or_greater) { - val index = indexOfFirstInstructionOrThrow { - (opcode == Opcode.INVOKE_STATIC || opcode == Opcode.INVOKE_STATIC_RANGE) && - methodReference?.returnType == textDataClassType + val insertIndex: Int + val charSequenceRegister: Int + + if (is_19_33_or_greater && !is_20_10_or_greater) { + val index = indexOfFirstInstructionOrThrow { + (opcode == Opcode.INVOKE_STATIC || opcode == Opcode.INVOKE_STATIC_RANGE) && + methodReference?.returnType == textDataClassType + } + + insertIndex = indexOfFirstInstructionOrThrow(index) { + opcode == Opcode.INVOKE_VIRTUAL && + methodReference?.parameterTypes?.firstOrNull() == "Ljava/lang/CharSequence;" + } + + charSequenceRegister = + getInstruction(insertIndex).registerD + } else { + insertIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.NEW_INSTANCE && + typeReference?.type == textDataClassType + } + + val charSequenceIndex = indexOfFirstInstructionOrThrow(insertIndex) { + opcode == Opcode.IPUT_OBJECT && + fieldReference?.type == "Ljava/lang/CharSequence;" + } + charSequenceRegister = + getInstruction(charSequenceIndex).registerA } - insertIndex = indexOfFirstInstructionOrThrow(index) { - opcode == Opcode.INVOKE_VIRTUAL && - methodReference?.parameterTypes?.firstOrNull() == "Ljava/lang/CharSequence;" - } + val conversionContext = findFreeRegister(insertIndex, charSequenceRegister) - charSequenceRegister = getInstruction(insertIndex).registerD - } else { - insertIndex = indexOfFirstInstructionOrThrow { - opcode == Opcode.NEW_INSTANCE && - typeReference?.type == textDataClassType - } - - val charSequenceIndex = indexOfFirstInstructionOrThrow(insertIndex) { - opcode == Opcode.IPUT_OBJECT && - fieldReference?.type == "Ljava/lang/CharSequence;" - } - charSequenceRegister = getInstruction(charSequenceIndex).registerA - } - - val conversionContext = findFreeRegister(insertIndex, charSequenceRegister) - - addInstructionsAtControlFlowLabel( - insertIndex, - """ + addInstructionsAtControlFlowLabel( + insertIndex, + """ # Copy conversion context. move-object/from16 v$conversionContext, p0 - iget-object v$conversionContext, v$conversionContext, $textComponentConversionContextField - invoke-static { v$conversionContext, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence; move-result-object v$charSequenceRegister :ignore nop """ - ) + ) + } + + // Hook new litho text creation code. + if (is_20_07_or_greater) { + textComponentFeatureFlagMethodMatch.let { + it.method.insertLiteralOverride( + it[0], + "$EXTENSION_CLASS_DESCRIPTOR->useNewLithoTextCreation(Z)Z" + ) + } + + lithoSpannableStringCreationMethodMatch.let { + val conversionContextField = it.immutableClassDef.type + + "->" + textComponentConversionContextField.name + + ":" + textComponentConversionContextField.type + + it.method.apply { + val insertIndex = it[1] + val charSequenceRegister = + getInstruction(insertIndex).registerD + val conversionContextPathRegister = + findFreeRegister(insertIndex, charSequenceRegister) + + addInstructions( + insertIndex, + """ + move-object/from16 v$conversionContextPathRegister, p0 + iget-object v$conversionContextPathRegister, v$conversionContextPathRegister, $conversionContextField + iget-object v$conversionContextPathRegister, v$conversionContextPathRegister, $conversionContextPathBuilderField + invoke-static { v$conversionContextPathRegister, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence; + move-result-object v$charSequenceRegister + """ + ) + } + } } // endregion // region Hook Shorts - // Filter that parses the video id from the UI + // Filter that parses the video ID from the UI addLithoFilter(FILTER_CLASS_DESCRIPTOR) - if (is_20_07_or_greater) { - // Turn off a/b flag that enables new code for creating litho spans. - // If enabled then the litho text span hook is never called. - // Target code is very obfuscated and exactly what the code does is not clear. - // Return late so debug patch logs if the flag is enabled. - if (is_20_41_or_greater) { - // TODO: Support the new non litho Shorts layout. - // Turning off this flag on later versions can break the Shorts overlay and nothing is shown. - Logger.getLogger(this::class.java.name).warning( - "\n!!!" + - "\n!!! Dislikes are not yet fully supported when patching YouTube 20.40+" + - "\n!!! Patch 20.21.37 or lower if you want to see dislikes" + - "\n!!!", - ) - - Logger.getLogger(this::class.java.name).warning( - "20.40+ Shorts player is not fully supported yet. Shorts Dislikes may not show.", - ) - } else { - textComponentFeatureFlagMethod.returnLate(false) - } - } - - // Player response video id is needed to search for the video ids in Shorts litho components. + // Player response video ID is needed to search for the video IDs in Shorts litho components. hookPlayerResponseVideoId("$FILTER_CLASS_DESCRIPTOR->newPlayerResponseVideoId(Ljava/lang/String;Z)V") // endregion @@ -224,17 +246,22 @@ val returnYouTubeDislikePatch = bytecodePatch( val insertIndex = 1 val dislikesIndex = rollingNumberSetterMethodMatch[-1] val charSequenceInstanceRegister = getInstruction(0).registerA - val charSequenceFieldReference = getInstruction(dislikesIndex).reference + val charSequenceFieldReference = + getInstruction(dislikesIndex).reference val conversionContextRegister = implementation!!.registerCount - parameters.size + 1 - val freeRegister = findFreeRegister(insertIndex, charSequenceInstanceRegister, conversionContextRegister) + val freeRegister = findFreeRegister( + insertIndex, + charSequenceInstanceRegister, + conversionContextRegister + ) addInstructions( insertIndex, """ iget-object v$freeRegister, v$charSequenceInstanceRegister, $charSequenceFieldReference - invoke-static {v$conversionContextRegister, v$freeRegister}, $EXTENSION_CLASS_DESCRIPTOR->onRollingNumberLoaded(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String; + invoke-static { v$conversionContextRegister, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->onRollingNumberLoaded(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String; move-result-object v$freeRegister iput-object v$freeRegister, v$charSequenceInstanceRegister, $charSequenceFieldReference """, @@ -249,12 +276,13 @@ val returnYouTubeDislikePatch = bytecodePatch( val endIndex = it[-1] it.method.apply { - val measuredTextWidthRegister = getInstruction(endIndex).registerA + val measuredTextWidthRegister = + getInstruction(endIndex).registerA addInstructions( endIndex + 1, """ - invoke-static {p1, v$measuredTextWidthRegister}, $EXTENSION_CLASS_DESCRIPTOR->onRollingNumberMeasured(Ljava/lang/String;F)F + invoke-static { p1, v$measuredTextWidthRegister }, $EXTENSION_CLASS_DESCRIPTOR->onRollingNumberMeasured(Ljava/lang/String;F)F move-result v$measuredTextWidthRegister """, ) @@ -272,7 +300,7 @@ val returnYouTubeDislikePatch = bytecodePatch( measureTextIndex + 1, """ move-result v$freeRegister - invoke-static {p1, v$freeRegister}, $EXTENSION_CLASS_DESCRIPTOR->onRollingNumberMeasured(Ljava/lang/String;F)F + invoke-static { p1, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->onRollingNumberMeasured(Ljava/lang/String;F)F """, ) } @@ -292,13 +320,15 @@ val returnYouTubeDislikePatch = bytecodePatch( methodReference?.name == "setText" } - val textViewRegister = getInstruction(setTextIndex).registerC - val textSpanRegister = getInstruction(setTextIndex).registerD + val textViewRegister = + getInstruction(setTextIndex).registerC + val textSpanRegister = + getInstruction(setTextIndex).registerD addInstructions( setTextIndex, """ - invoke-static {v$textViewRegister, v$textSpanRegister}, $EXTENSION_CLASS_DESCRIPTOR->updateRollingNumber(Landroid/widget/TextView;Ljava/lang/CharSequence;)Ljava/lang/CharSequence; + invoke-static { v$textViewRegister, v$textSpanRegister }, $EXTENSION_CLASS_DESCRIPTOR->updateRollingNumber(Landroid/widget/TextView;Ljava/lang/CharSequence;)Ljava/lang/CharSequence; move-result-object v$textSpanRegister """, ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/Fingerprints.kt deleted file mode 100644 index e7748db661..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/Fingerprints.kt +++ /dev/null @@ -1,34 +0,0 @@ -package app.revanced.patches.youtube.layout.searchbar - -import app.revanced.patcher.accessFlags -import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively -import app.revanced.patcher.gettingFirstMethodDeclaratively -import app.revanced.patcher.instructions -import app.revanced.patcher.parameterTypes -import app.revanced.patcher.patch.BytecodePatchContext -import app.revanced.patcher.returnType -import app.revanced.patches.shared.misc.mapping.ResourceType -import app.revanced.patches.youtube.layout.hide.general.yoodlesImageViewMethod -import com.android.tools.smali.dexlib2.AccessFlags - -internal val BytecodePatchContext.setWordmarkHeaderMethod by gettingFirstImmutableMethodDeclaratively { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returnType("V") - parameterTypes("Landroid/widget/ImageView;") - instructions( - ResourceType.ATTR("ytPremiumWordmarkHeader"), - ResourceType.ATTR("ytWordmarkHeader"), - ) -} - -/** - * Matches the same method as [yoodlesImageViewMethod]. - */ -internal val BytecodePatchContext.wideSearchbarLayoutMethod by gettingFirstMethodDeclaratively { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returnType("Landroid/view/View;") - parameterTypes("L", "L") - instructions( - ResourceType.LAYOUT("action_bar_ringo"), - ) -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/WideSearchbarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/WideSearchbarPatch.kt deleted file mode 100644 index 9ac4138abd..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/WideSearchbarPatch.kt +++ /dev/null @@ -1,105 +0,0 @@ -package app.revanced.patches.youtube.layout.searchbar - -import app.revanced.patcher.extensions.addInstruction -import app.revanced.patcher.extensions.getInstruction -import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patches.all.misc.resources.addResources -import app.revanced.patches.all.misc.resources.addResourcesPatch -import app.revanced.patches.shared.misc.mapping.resourceMappingPatch -import app.revanced.patches.shared.misc.settings.preference.SwitchPreference -import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch -import app.revanced.patches.youtube.misc.playservice.is_20_31_or_greater -import app.revanced.patches.youtube.misc.playservice.versionCheckPatch -import app.revanced.patches.youtube.misc.settings.PreferenceScreen -import app.revanced.patches.youtube.misc.settings.settingsPatch -import app.revanced.util.addInstructionsAtControlFlowLabel -import app.revanced.util.findInstructionIndicesReversedOrThrow -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstructionOrThrow -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.MethodReference -import java.util.logging.Logger - -private const val EXTENSION_CLASS_DESCRIPTOR = - "Lapp/revanced/extension/youtube/patches/WideSearchbarPatch;" - -@Suppress("unused") -val wideSearchBarPatch = bytecodePatch( - name = "Wide search bar", - description = "Adds an option to replace the search icon with a wide search bar. " + - "This will hide the YouTube logo when active.", -) { - dependsOn( - sharedExtensionPatch, - settingsPatch, - addResourcesPatch, - resourceMappingPatch, - versionCheckPatch, - ) - - compatibleWith( - "com.google.android.youtube"( - "19.43.41", - "20.14.43", - "20.21.37", - // 20.31.40+ not supported. YouTube code was removed. - ), - ) - - apply { - if (is_20_31_or_greater) { - // YT removed the legacy text search text field all code required to use it. - // This functionality could be restored by adding a search text field to the toolbar - // with a listener that artificially clicks the toolbar search button. - return@apply Logger.getLogger(this::class.java.name).warning( - "Wide searchbar is not compatible with 20.31+", - ) - } - - addResources("youtube", "layout.searchbar.wideSearchbarPatch") - - PreferenceScreen.FEED.addPreferences( - SwitchPreference("revanced_wide_searchbar"), - ) - - // Navigate to the method that checks if the YT logo is shown beside the search bar. - val shouldShowLogoMethod = with(setWordmarkHeaderMethod) { - val invokeStaticIndex = indexOfFirstInstructionOrThrow { - opcode == Opcode.INVOKE_STATIC && - getReference()?.returnType == "Z" - } - navigate(this).to(invokeStaticIndex).stop() - } - - shouldShowLogoMethod.apply { - findInstructionIndicesReversedOrThrow(Opcode.RETURN).forEach { index -> - val register = getInstruction(index).registerA - - addInstructionsAtControlFlowLabel( - index, - """ - invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->enableWideSearchbar(Z)Z - move-result v$register - """, - ) - } - } - - // Fix missing left padding when using wide searchbar. - wideSearchbarLayoutMethod.apply { - findInstructionIndicesReversedOrThrow { - val reference = getReference() - reference?.definingClass == "Landroid/view/LayoutInflater;" && - reference.name == "inflate" - }.forEach { inflateIndex -> - val register = getInstruction(inflateIndex + 1).registerA - - addInstruction( - inflateIndex + 2, - "invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->setActionBar(Landroid/view/View;)V", - ) - } - } - } -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt index 63cb2e0f1f..832e2ee57b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt @@ -112,7 +112,7 @@ internal val BytecodePatchContext.lottieAnimationViewSetAnimationIntMethod by ge parameterTypes("I") returnType("V") - lateinit var methodDefiningClass: String + var methodDefiningClass = "" custom { methodDefiningClass = definingClass true diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatch.kt index 9a67e0491c..801fc83880 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatch.kt @@ -14,6 +14,7 @@ import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater import app.revanced.patches.youtube.misc.playservice.is_19_49_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_34_or_greater +import app.revanced.patches.youtube.misc.playservice.is_21_02_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.shared.mainActivityOnCreateMethod import app.revanced.util.findInstructionIndicesReversedOrThrow @@ -152,7 +153,7 @@ val seekbarColorPatch = bytecodePatch( return@apply // 19.25 does not have a cairo launch animation. } - // Hook the splash animation to set the a seekbar color. + // Hook the splash animation to set the seekbar color. mainActivityOnCreateMethod.apply { val setAnimationIntMethodName = lottieAnimationViewSetAnimationIntMethod.name @@ -208,7 +209,7 @@ val seekbarColorPatch = bytecodePatch( factoryStreamReturnType = it.returnType } - val lottieAnimationViewSetAnimationStreamMethod = firstMethodDeclaratively { + val lottieAnimationViewSetAnimationStreamMethod = firstImmutableMethodDeclaratively { definingClass(lottieAnimationViewSetAnimationIntMethod.immutableClassDef.type) accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) parameterTypes(factoryStreamReturnType.toString()) @@ -230,13 +231,17 @@ val seekbarColorPatch = bytecodePatch( null, MutableMethodImplementation(4), ).toMutable().apply { + // 21.02+ method is private. Cannot easily change the access flags to public + // because that breaks unrelated opcode that uses invoke-direct and not invoke-virtual. + val methodOpcode = if (is_21_02_or_greater) "invoke-direct" else "invoke-virtual" + addInstructions( """ - invoke-static { p1, p2 }, $factoryStreamClass->$factoryStreamName(Ljava/io/InputStream;Ljava/lang/String;)$factoryStreamReturnType - move-result-object v0 - invoke-virtual { p0, v0}, Lcom/airbnb/lottie/LottieAnimationView;->$setAnimationStreamMethodName($factoryStreamReturnType)V - return-void - """, + invoke-static { p1, p2 }, $factoryStreamClass->$factoryStreamName(Ljava/io/InputStream;Ljava/lang/String;)$factoryStreamReturnType + move-result-object v0 + $methodOpcode { p0, v0}, Lcom/airbnb/lottie/LottieAnimationView;->$setAnimationStreamMethodName($factoryStreamReturnType)V + return-void + """ ) }, ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/Fingerprints.kt index f6b30c4650..507c7c2af8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/Fingerprints.kt @@ -19,7 +19,6 @@ internal val BytecodePatchContext.reelEnumConstructorMethodMatch by composingFir internal val BytecodePatchContext.reelPlaybackRepeatParentMethod by gettingFirstImmutableMethodDeclaratively { returnType("V") - parameterTypes("Ljava/lang/String;", "J") instructions( "Reels[%s] Playback Time: %d ms"(), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt index 988529db90..63fa21a216 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt @@ -32,7 +32,8 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ShortsAutoplayPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/ShortsAutoplayPatch;" @Suppress("ObjectPropertyName") val shortsAutoplayPatch = bytecodePatch( @@ -48,10 +49,12 @@ val shortsAutoplayPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -90,15 +93,16 @@ val shortsAutoplayPatch = bytecodePatch( ) } - val reelPlaybackRepeatMethod = reelPlaybackRepeatParentMethod.immutableClassDef.getReelPlaybackRepeatMethod() + val reelPlaybackRepeatMethod = + reelPlaybackRepeatParentMethod.immutableClassDef.getReelPlaybackRepeatMethod() reelPlaybackRepeatMethod.apply { // The behavior enums are looked up from an ordinal value to an enum type. findInstructionIndicesReversedOrThrow { val reference = getReference() reference?.definingClass == reelEnumClass && - reference.parameterTypes.firstOrNull() == "I" && - reference.returnType == reelEnumClass + reference.parameterTypes.firstOrNull() == "I" && + reference.returnType == reelEnumClass }.forEach { index -> val register = getInstruction(index + 1).registerA @@ -125,14 +129,15 @@ val shortsAutoplayPatch = bytecodePatch( // Find the first call modified by extension code above. val extensionReturnResultIndex = indexOfFirstInstructionOrThrow { opcode == Opcode.INVOKE_STATIC && - getReference()?.definingClass == EXTENSION_CLASS_DESCRIPTOR + getReference()?.definingClass == EXTENSION_CLASS_DESCRIPTOR } + 1 - val enumRegister = getInstruction(extensionReturnResultIndex).registerA + val enumRegister = + getInstruction(extensionReturnResultIndex).registerA val getReelSequenceControllerIndex = indexOfFirstInstructionOrThrow { val reference = getReference() opcode == Opcode.IGET_OBJECT && - reference?.definingClass == definingClass && - reference.type == reelSequenceControllerMethodReference.definingClass + reference?.definingClass == definingClass && + reference.type == reelSequenceControllerMethodReference.definingClass } val getReelSequenceControllerReference = getInstruction(getReelSequenceControllerIndex).reference diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/Fingerprints.kt index 7fb5ae1ca0..96f2b56032 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/Fingerprints.kt @@ -4,80 +4,13 @@ import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patches.shared.misc.mapping.ResourceType import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -/** - * Purpose of this method is not clear, and it's only used to identify - * the obfuscated name of the videoId() method in PlaybackStartDescriptor. - * 20.38 and lower. - */ -internal val BytecodePatchContext.playbackStartFeatureFlagMethodMatch by composingFirstMethod { - returnType("Z") - parameterTypes("Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;") - instructions( - method { - definingClass == "Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;" && - returnType == "Ljava/lang/String;" - }, - 45380134L(), - ) -} - -/** - * Purpose of this method is not entirely clear, and it's only used to identify - * the obfuscated name of the videoId() method in PlaybackStartDescriptor. - * 20.39+ - */ -internal val BytecodePatchContext.watchPanelVideoIdMethodMatch by composingFirstMethod { - returnType("Ljava/lang/String;") - parameterTypes() - instructions( - allOf( - Opcode.IGET_OBJECT(), - field { type == "Lcom/google/android/apps/youtube/app/common/player/queue/WatchPanelId;" }, - ), - allOf( - Opcode.CHECK_CAST(), - type("Lcom/google/android/apps/youtube/app/common/player/queue/DefaultWatchPanelId;"), - ), - method { - definingClass == "Lcom/google/android/apps/youtube/app/common/player/queue/DefaultWatchPanelId;" && - returnType == "Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;" - }, - method { - definingClass == "Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;" && - returnType == "Ljava/lang/String;" - }, - - ) -} - -// Pre 19.25 -internal val BytecodePatchContext.shortsPlaybackIntentLegacyMethodMatch by composingFirstMethod { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returnType("V") - parameterTypes( - "L", - "Ljava/util/Map;", - "J", - "Ljava/lang/String;", - "Z", - "Ljava/util/Map;", - ) - instructions( - method { returnType == "Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;" }, - // None of these strings are unique. - "com.google.android.apps.youtube.app.endpoint.flags"(), - "ReelWatchFragmentArgs"(), - "reels_fragment_descriptor"(), - ) -} +// 19.25+ internal val BytecodePatchContext.shortsPlaybackIntentMethod by gettingFirstMethodDeclaratively { accessFlags(AccessFlags.PROTECTED, AccessFlags.FINAL) returnType("V") parameterTypes( - "Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;", + "L", "Ljava/util/Map;", "J", "Ljava/lang/String;", diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/OpenShortsInRegularPlayerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/OpenShortsInRegularPlayerPatch.kt index 9b115730b4..518902cab2 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/OpenShortsInRegularPlayerPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/OpenShortsInRegularPlayerPatch.kt @@ -1,9 +1,13 @@ package app.revanced.patches.youtube.layout.shortsplayer +import app.revanced.util.findFreeRegister +import app.revanced.util.registersUsed +import app.revanced.patcher.extensions.ExternalLabel import app.revanced.patcher.extensions.addInstruction import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.extensions.addInstructionsWithLabels import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.methodReference import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch @@ -12,18 +16,20 @@ import app.revanced.patches.shared.misc.settings.preference.ListPreference import app.revanced.patches.youtube.layout.player.fullscreen.openVideosFullscreenHookPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch -import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater -import app.revanced.patches.youtube.misc.playservice.is_20_39_or_greater +import app.revanced.patches.youtube.misc.playservice.is_21_07_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.shared.mainActivityOnCreateMethod -import app.revanced.util.findFreeRegister +import app.revanced.patches.youtube.video.information.playbackStartDescriptorToStringMethodMatch +import app.revanced.util.addInstructionsAtControlFlowLabel +import app.revanced.util.findInstructionIndicesReversedOrThrow import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionReversedOrThrow import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference private const val EXTENSION_CLASS_DESCRIPTOR = @@ -46,10 +52,12 @@ val openShortsInRegularPlayerPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -64,78 +72,129 @@ val openShortsInRegularPlayerPatch = bytecodePatch( mainActivityOnCreateMethod.addInstruction( 0, "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->" + - "setMainActivity(Landroid/app/Activity;)V", + "setMainActivity(Landroid/app/Activity;)V", ) - // Find the obfuscated method name for PlaybackStartDescriptor.videoId() - val (videoIdStartMethod, videoIdIndex) = if (is_20_39_or_greater) { - watchPanelVideoIdMethodMatch.let { - it.immutableMethod to it[-1] - } - } else { - playbackStartFeatureFlagMethodMatch.let { - it.immutableMethod to it[0] - } - } - val playbackStartVideoIdMethodName = navigate(videoIdStartMethod).to(videoIdIndex).stop().name - fun extensionInstructions(playbackStartRegister: Int, freeRegister: Int) = + val playbackStartVideoIdMethod = playbackStartDescriptorToStringMethodMatch.let { + navigate(it.method).to(it[1]).original() + } + + shortsPlaybackIntentMethod.addInstructionsWithLabels( + 0, """ - invoke-virtual { v$playbackStartRegister }, Lcom/google/android/libraries/youtube/player/model/PlaybackStartDescriptor;->$playbackStartVideoIdMethodName()Ljava/lang/String; - move-result-object v$freeRegister - invoke-static { v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->openShort(Ljava/lang/String;)Z - move-result v$freeRegister - if-eqz v$freeRegister, :disabled + move-object/from16 v0, p1 + + invoke-virtual { v0 }, $playbackStartVideoIdMethod + move-result-object v1 + invoke-static { v1 }, ${EXTENSION_CLASS_DESCRIPTOR}->openShort(Ljava/lang/String;)Z + move-result v1 + if-eqz v1, :disabled return-void :disabled nop """ - - if (is_19_25_or_greater) { - shortsPlaybackIntentMethod.addInstructionsWithLabels( - 0, - """ - move-object/from16 v0, p1 - ${extensionInstructions(0, 1)} - """, - ) - } else { - shortsPlaybackIntentLegacyMethodMatch.let { - it.method.apply { - val index = it[0] - val playbackStartRegister = getInstruction(index + 1).registerA - val insertIndex = index + 2 - val freeRegister = findFreeRegister(insertIndex, playbackStartRegister) - - addInstructionsWithLabels( - insertIndex, - extensionInstructions(playbackStartRegister, freeRegister), - ) - } - } - } + ) // Fix issue with back button exiting the app instead of minimizing the player. // Without this change this issue can be difficult to reproduce, but seems to occur // most often with 'open video in regular player' and not open in fullscreen player. exitVideoPlayerMethod.apply { + // TODO: Check if this logic works for older app targets as well. + if (is_21_07_or_greater) { + findInstructionIndicesReversedOrThrow { + val methodReference = methodReference + methodReference?.name == "finish" && methodReference.parameterTypes.isEmpty() + }.forEach { index -> + val returnIndex = indexOfFirstInstructionOrThrow( + index, Opcode.RETURN_VOID + ) + + if (returnIndex == this.implementation!!.instructions.lastIndex) { + val freeRegister = findFreeRegister(index) + + // Jumps to last index + addInstructionsAtControlFlowLabel( + index, + """ + invoke-static { }, ${EXTENSION_CLASS_DESCRIPTOR}->overrideBackPressToExit()Z + move-result v$freeRegister + if-eqz v$freeRegister, :doNotCallActivityFinish + return-void + :doNotCallActivityFinish + nop + """ + ) + } else { + // Must check free register after the return index. + val freeRegister = findFreeRegister(returnIndex + 1) + + addInstructionsAtControlFlowLabel( + index, + """ + invoke-static { }, ${EXTENSION_CLASS_DESCRIPTOR}->overrideBackPressToExit()Z + move-result v$freeRegister + if-eqz v$freeRegister, :doNotCallActivityFinish + """, ExternalLabel( + "doNotCallActivityFinish", + getInstruction(returnIndex + 1) + ) + ) + } + } + return@apply + } + + // Method call for Activity.finish() - val finishIndex = indexOfFirstInstructionOrThrow { + val finishIndexFirst = indexOfFirstInstructionOrThrow { val reference = getReference() reference?.name == "finish" } - // Index of PlayerType.isWatchWhileMaximizedOrFullscreen() - val index = indexOfFirstInstructionReversedOrThrow(finishIndex, Opcode.MOVE_RESULT) - val register = getInstruction(index).registerA + // Second Activity.finish() call. Has been present since 19.x but started + // to interfere with back to exit fullscreen around 20.47. + val finishIndexSecond = indexOfFirstInstruction(finishIndexFirst + 1) { + val reference = getReference() + reference?.name == "finish" + } + val getBooleanFieldIndex = indexOfFirstInstructionReversedOrThrow(finishIndexSecond) { + opcode == Opcode.IGET_BOOLEAN + } + val booleanRegister = + getInstruction(getBooleanFieldIndex).registerA addInstructions( - index + 1, + getBooleanFieldIndex + 1, """ - invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->overrideBackPressToExit(Z)Z - move-result v$register + invoke-static { v$booleanRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->overrideBackPressToExit(Z)Z + move-result v$booleanRegister + """ + ) + + // Surround first activity.finish() and return-void with conditional check. + val returnVoidIndex = indexOfFirstInstructionOrThrow( + finishIndexFirst, Opcode.RETURN_VOID + ) + // Find free register using index after return void (new control flow path added below). + val freeRegister = findFreeRegister( + returnVoidIndex + 1, + // Exclude all registers used by only instruction we will skip over. + getInstruction(finishIndexFirst).registersUsed + ) + + addInstructionsAtControlFlowLabel( + finishIndexFirst, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->overrideBackPressToExit()Z + move-result v$freeRegister + if-eqz v$freeRegister, :doNotCallActivityFinish """, + ExternalLabel( + "doNotCallActivityFinish", + getInstruction(returnVoidIndex + 1) + ) ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt index d17441dea9..41c049d821 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt @@ -129,10 +129,12 @@ val sponsorBlockPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -145,7 +147,7 @@ val sponsorBlockPatch = bytecodePatch( hookBackgroundPlayVideoId( EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR + - "->setCurrentVideoId(Ljava/lang/String;)V", + "->setCurrentVideoId(Ljava/lang/String;)V", ) // Set seekbar draw rectangle. @@ -157,7 +159,8 @@ val sponsorBlockPatch = bytecodePatch( ) { getReference()?.type == "Landroid/graphics/Rect;" } - rectangleFieldName = getInstruction(rectangleIndex).reference as FieldReference + rectangleFieldName = + getInstruction(rectangleIndex).reference as FieldReference } } @@ -169,11 +172,12 @@ val sponsorBlockPatch = bytecodePatch( it.method.apply { // Set seekbar thickness. val thicknessIndex = it[-1] - val thicknessRegister = getInstruction(thicknessIndex).registerA + val thicknessRegister = + getInstruction(thicknessIndex).registerA addInstruction( thicknessIndex + 1, "invoke-static { v$thicknessRegister }, " + - "$EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->setSeekbarThickness(I)V", + "$EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->setSeekbarThickness(I)V", ) // Find the drawCircle call and draw the segment before it. @@ -187,8 +191,8 @@ val sponsorBlockPatch = bytecodePatch( addInstruction( drawCircleIndex, "invoke-static { v$canvasInstanceRegister, v$centerYRegister }, " + - "$EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->" + - "drawSegmentTimeBars(Landroid/graphics/Canvas;F)V", + "$EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->" + + "drawSegmentTimeBars(Landroid/graphics/Canvas;F)V", ) // Set seekbar bounds. @@ -234,7 +238,8 @@ val sponsorBlockPatch = bytecodePatch( val checkCastIndex = it[-1] it.method.apply { - val frameLayoutRegister = getInstruction(checkCastIndex).registerA + val frameLayoutRegister = + getInstruction(checkCastIndex).registerA addInstruction( checkCastIndex + 1, "invoke-static {v$frameLayoutRegister}, $EXTENSION_SPONSORBLOCK_VIEW_CONTROLLER_CLASS_DESCRIPTOR->initialize(Landroid/view/ViewGroup;)V", @@ -244,7 +249,8 @@ val sponsorBlockPatch = bytecodePatch( adProgressTextViewVisibilityMethodMatch.let { val setVisibilityIndex = it[0] - val register = it.method.getInstruction(setVisibilityIndex).registerD + val register = + it.method.getInstruction(setVisibilityIndex).registerD it.method.addInstructionsAtControlFlowLabel( setVisibilityIndex, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/Fingerprints.kt index 2d316f7623..d7842649a2 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/Fingerprints.kt @@ -6,21 +6,6 @@ import app.revanced.patches.shared.misc.mapping.ResourceType import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode -internal val BytecodePatchContext.toolBarButtonMethodMatch by composingFirstMethod { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returnType("V") - instructions( - ResourceType.ID("menu_item_view"), - allOf(Opcode.INVOKE_INTERFACE(), method { returnType == "I" }), - after(Opcode.MOVE_RESULT()), - afterAtMost(6, allOf(Opcode.IGET_OBJECT(), field { type == "Landroid/widget/ImageView;" })), - afterAtMost(8, method { name == "getDrawable" && definingClass == "Landroid/content/res/Resources;" }), - afterAtMost(4, method { name == "setImageDrawable" && definingClass == "Landroid/widget/ImageView;" }), - ) - // 20.37+ has second parameter of "Landroid/content/Context;" - custom { parameterTypes.count() in 1..2 && parameterTypes.first() == "Landroid/view/MenuItem;" } -} - internal val BytecodePatchContext.spoofAppVersionMethodMatch by composingFirstMethod( // Instead of applying a bytecode patch, it might be possible to only rely on code from the extension and // manually set the desired version string as this keyed value in the SharedPreferences. diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch.kt index cb9259325f..f55cab1921 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch.kt @@ -18,16 +18,16 @@ import app.revanced.patches.youtube.misc.playservice.is_20_14_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.getToolBarButtonMethodMatch import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/spoof/SpoofAppVersionPatch;" -@Suppress("ObjectPropertyName") val spoofAppVersionPatch = bytecodePatch( name = "Spoof app version", description = "Adds an option to trick YouTube into thinking you are running an older version of the app. " + - "This can be used to restore old UI elements and features.", + "This can be used to restore old UI elements and features.", ) { dependsOn( resourceMappingPatch, @@ -39,10 +39,12 @@ val spoofAppVersionPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -82,9 +84,11 @@ val spoofAppVersionPatch = bytecodePatch( * missing image resources. As a workaround, do not set an image in the * toolbar when the enum name is UNKNOWN. */ - toolBarButtonMethodMatch.let { + // Method is shared and indexes may no longer be correct. + getToolBarButtonMethodMatch().let { val imageResourceIndex = it[2] - val register = it.method.getInstruction(imageResourceIndex).registerA + val register = + it.method.getInstruction(imageResourceIndex).registerA val jumpIndex = it[-1] + 1 it.method.addInstructionsWithLabels( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt index ec86e1d561..c2d47290db 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt @@ -15,7 +15,8 @@ import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ChangeStartPagePatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/ChangeStartPagePatch;" @Suppress("unused") val changeStartPagePatch = bytecodePatch( @@ -30,10 +31,12 @@ val changeStartPagePatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -59,7 +62,8 @@ val changeStartPagePatch = bytecodePatch( browseIdMethodMatch.let { it.method.apply { val browseIdIndex = it[0] - val browseIdRegister = getInstruction(browseIdIndex).registerA + val browseIdRegister = + getInstruction(browseIdIndex).registerA addInstructions( browseIdIndex + 1, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/Fingerprints.kt index 6c172b1513..66b5350244 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/Fingerprints.kt @@ -11,7 +11,7 @@ internal val BytecodePatchContext.intentActionMethod by gettingFirstMethodDeclar } internal val BytecodePatchContext.browseIdMethodMatch by composingFirstMethod { - returnType("Lcom/google/android/apps/youtube/app/common/ui/navigation/PaneDescriptor;") + returnType("L") // parameterTypes() // 20.30 and earlier is no parameters. 20.31+ parameter is L. instructions( "FEwhat_to_watch"(), diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch.kt index 3b8023f26c..c727aa8267 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch.kt @@ -8,6 +8,7 @@ import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playservice.is_20_03_or_greater +import app.revanced.patches.youtube.misc.playservice.is_21_03_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch @@ -36,14 +37,22 @@ val disableResumingShortsOnStartupPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" + // This patch is obsolete with 21.03 because YT seems to have + // removed resuming Shorts functionality. + // TODO: Before adding 21.03+, merge this patch into `Hide Shorts component` ), ) apply { + // 21.03+ seems to no longer have resuming Shorts functionality. + if (is_21_03_or_greater) return@apply + addResources("youtube", "layout.startupshortsreset.disableResumingShortsOnStartupPatch") PreferenceScreen.SHORTS.addPreferences( @@ -69,8 +78,8 @@ val disableResumingShortsOnStartupPatch = bytecodePatch( userWasInShortsLegacyMethod.apply { val listenableInstructionIndex = indexOfFirstInstructionOrThrow { opcode == Opcode.INVOKE_INTERFACE && - getReference()?.definingClass == "Lcom/google/common/util/concurrent/ListenableFuture;" && - getReference()?.name == "isDone" + getReference()?.definingClass == "Lcom/google/common/util/concurrent/ListenableFuture;" && + getReference()?.name == "isDone" } val freeRegister = findFreeRegister(listenableInstructionIndex) @@ -83,7 +92,7 @@ val disableResumingShortsOnStartupPatch = bytecodePatch( return-void :show_startup_shorts_player nop - """, + """ ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/Fingerprints.kt index c89783668c..b14b0d7b19 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/Fingerprints.kt @@ -3,6 +3,7 @@ package app.revanced.patches.youtube.layout.theme import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patches.youtube.shared.YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE +import com.android.tools.smali.dexlib2.Opcode internal val BytecodePatchContext.useGradientLoadingScreenMethodMatch by composingFirstMethod { instructions(45412406L()) @@ -20,3 +21,56 @@ internal val BytecodePatchContext.splashScreenStyleMethodMatch by composingFirst ), ) } + + +/** + * Matches to the same method as [splashScreenStyleMethodMatch]. + */ +internal val BytecodePatchContext.showSplashScreen1MethodMatch by composingFirstMethod { + name("onCreate") + definingClass(YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE) + returnType("V") + parameterTypes("Landroid/os/Bundle;") + instructions( + anyOf(Opcode.CONST_4(), Opcode.CONST_16()), + afterAtMost( + 20, + method { + returnType == "V" && + parameterTypes.size == 2 && + parameterTypes[0].startsWith("L") && + parameterTypes[1] == "Ljava/lang/Runnable;" + + + }, + ), + afterAtMost(10, Opcode.APUT_OBJECT()), + afterAtMost( + 5, + method { returnType == "V" && parameterTypes.size == 1 && parameterTypes[0].startsWith("[L") }, + ), + after(Opcode.IGET_OBJECT()), + after(method { returnType == "I" && parameterTypes.isEmpty() }), + after(Opcode.MOVE_RESULT()), + after(method { returnType == "Z" && parameterTypes.size == 1 && parameterTypes[0] == "I" }), + after(Opcode.MOVE_RESULT()), + ) +} + +/** + * Matches to the same method as [splashScreenStyleMethodMatch]. + */ +internal val BytecodePatchContext.showSplashScreen2MethodMatch by composingFirstMethod { + name("onCreate") + definingClass(YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE) + returnType("V") + parameterTypes("Landroid/os/Bundle;") + instructions( + allOf( + Opcode.INVOKE_VIRTUAL(), + method { returnType == "V" && parameterTypes.size == 1 && parameterTypes[0].startsWith("[L") } + ), + Opcode.IF_NE(), + method { toString() == "Landroid/graphics/drawable/AnimatedVectorDrawable;->start()V" } + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt index dad35b727a..f1d4fb2cc5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt @@ -1,11 +1,16 @@ package app.revanced.patches.youtube.layout.theme + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.resourcePatch import app.revanced.patcher.patch.stringOption import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.layout.theme.THEME_COLOR_OPTION_DESCRIPTION +import app.revanced.patches.shared.layout.theme.THEME_DEFAULT_DARK_COLOR_NAMES +import app.revanced.patches.shared.layout.theme.THEME_DEFAULT_LIGHT_COLOR_NAMES import app.revanced.patches.shared.layout.theme.baseThemePatch import app.revanced.patches.shared.layout.theme.baseThemeResourcePatch import app.revanced.patches.shared.layout.theme.darkThemeBackgroundColorOption @@ -20,13 +25,19 @@ import app.revanced.patches.shared.misc.settings.preference.TextPreference import app.revanced.patches.youtube.layout.seekbar.seekbarColorPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playservice.is_19_47_or_greater +import app.revanced.patches.youtube.misc.playservice.is_20_02_or_greater +import app.revanced.patches.youtube.misc.playservice.is_21_06_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.util.forEachChildElement import app.revanced.util.insertLiteralOverride +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import org.w3c.dom.Element -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/theme/ThemePatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/theme/ThemePatch;" val themePatch = baseThemePatch( extensionClassDescriptor = EXTENSION_CLASS_DESCRIPTOR, @@ -147,18 +158,44 @@ val themePatch = baseThemePatch( settingsPatch, addResourcesPatch, seekbarColorPatch, + versionCheckPatch, baseThemeResourcePatch( lightColorReplacement = { lightThemeBackgroundColor!! }, + getDarkColorNames = { + THEME_DEFAULT_DARK_COLOR_NAMES + if (is_21_06_or_greater) + setOf( + // yt_ref_color_constants_baseline_black_black0 + "yt_sys_color_baseline_dark_menu_background", + // yt_ref_color_constants_baseline_black_black1 + "yt_sys_color_baseline_dark_static_black", + "yt_sys_color_baseline_dark_raised_background", + // yt_ref_color_constants_baseline_black_black3 + "yt_sys_color_baseline_dark_base_background", + "yt_sys_color_baseline_dark_static_black", + "yt_sys_color_baseline_light_inverted_background", + "yt_sys_color_baseline_light_static_black", + ) else emptySet() + }, + getLightColorNames = { + THEME_DEFAULT_LIGHT_COLOR_NAMES + if (is_21_06_or_greater) + setOf( + "yt_sys_color_baseline_light_base_background", + "yt_sys_color_baseline_light_raised_background" + ) + else emptySet() + } ), themeResourcePatch, ) compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) }, @@ -211,5 +248,39 @@ val themePatch = baseThemePatch( "$EXTENSION_CLASS_DESCRIPTOR->getLoadingScreenType(I)I", ) } + + showSplashScreen1MethodMatch.let { + it.method.apply { + val index = it[-1] + val register = getInstruction(index).registerA + + addInstructions( + index + 1, + """ + invoke-static { v$register }, ${EXTENSION_CLASS_DESCRIPTOR}->showSplashScreen(Z)Z + move-result v$register + """ + ) + } + } + + if (is_20_02_or_greater) { + showSplashScreen2MethodMatch.let { + val insertIndex = it[1] + it.method.apply { + val insertInstruction = getInstruction(insertIndex) + val registerA = insertInstruction.registerA + val registerB = insertInstruction.registerB + + addInstructions( + insertIndex, + """ + invoke-static { v$registerA, v$registerB }, ${EXTENSION_CLASS_DESCRIPTOR}->showSplashScreen(II)I + move-result v$registerA + """ + ) + } + } + } }, ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt index bc4c213cf0..6051aa2e79 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt @@ -8,10 +8,10 @@ import app.revanced.patches.shared.misc.settings.preference.NonInteractivePrefer import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.shared.misc.settings.preference.TextPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch -import app.revanced.patches.youtube.misc.imageurlhook.addImageUrlErrorCallbackHook -import app.revanced.patches.youtube.misc.imageurlhook.addImageUrlHook -import app.revanced.patches.youtube.misc.imageurlhook.addImageUrlSuccessCallbackHook -import app.revanced.patches.youtube.misc.imageurlhook.cronetImageUrlHookPatch +import app.revanced.patches.youtube.misc.imageurlhook.addImageURLErrorCallbackHook +import app.revanced.patches.youtube.misc.imageurlhook.addImageURLHook +import app.revanced.patches.youtube.misc.imageurlhook.addImageURLSuccessCallbackHook +import app.revanced.patches.youtube.misc.imageurlhook.cronetImageURLHookPatch import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch @@ -29,15 +29,17 @@ val alternativeThumbnailsPatch = bytecodePatch( settingsPatch, addResourcesPatch, navigationBarHookPatch, - cronetImageUrlHookPatch, + cronetImageURLHookPatch, ) compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -85,8 +87,8 @@ val alternativeThumbnailsPatch = bytecodePatch( ListPreference("revanced_alt_thumbnail_stills_time"), ) - addImageUrlHook(EXTENSION_CLASS_DESCRIPTOR) - addImageUrlSuccessCallbackHook(EXTENSION_CLASS_DESCRIPTOR) - addImageUrlErrorCallbackHook(EXTENSION_CLASS_DESCRIPTOR) + addImageURLHook(EXTENSION_CLASS_DESCRIPTOR) + addImageURLSuccessCallbackHook(EXTENSION_CLASS_DESCRIPTOR) + addImageURLErrorCallbackHook(EXTENSION_CLASS_DESCRIPTOR) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictionsPatch.kt index 566a9069c2..e785fcf199 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictionsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictionsPatch.kt @@ -5,8 +5,8 @@ import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch -import app.revanced.patches.youtube.misc.imageurlhook.addImageUrlHook -import app.revanced.patches.youtube.misc.imageurlhook.cronetImageUrlHookPatch +import app.revanced.patches.youtube.misc.imageurlhook.addImageURLHook +import app.revanced.patches.youtube.misc.imageurlhook.cronetImageURLHookPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch @@ -17,21 +17,23 @@ private const val EXTENSION_CLASS_DESCRIPTOR = val bypassImageRegionRestrictionsPatch = bytecodePatch( name = "Bypass image region restrictions", description = "Adds an option to use a different host for user avatar and channel images " + - "and can fix missing images that are blocked in some countries.", + "and can fix missing images that are blocked in some countries.", ) { dependsOn( sharedExtensionPatch, settingsPatch, addResourcesPatch, - cronetImageUrlHookPatch, + cronetImageURLHookPatch, ) compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -42,8 +44,8 @@ val bypassImageRegionRestrictionsPatch = bytecodePatch( SwitchPreference("revanced_bypass_image_region_restrictions"), ) - // A priority hook is not needed, as the image urls of interest are not modified + // A priority hook is not needed, as the image URLs of interest are not modified // by AlternativeThumbnails or any other patch in this repo. - addImageUrlHook(EXTENSION_CLASS_DESCRIPTOR) + addImageURLHook(EXTENSION_CLASS_DESCRIPTOR) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/toolbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/toolbar/Fingerprints.kt new file mode 100644 index 0000000000..22360b5c41 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/toolbar/Fingerprints.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.youtube.layout.toolbar + +import app.revanced.patcher.accessFlags +import app.revanced.patcher.definingClass +import app.revanced.patcher.gettingFirstMethodDeclaratively +import app.revanced.patcher.name +import app.revanced.patcher.patch.BytecodePatchContext +import com.android.tools.smali.dexlib2.AccessFlags + +internal val BytecodePatchContext.hookToolbarMethod by gettingFirstMethodDeclaratively { + name("hookToolbar") + definingClass(EXTENSION_CLASS_DESCRIPTOR) + accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/toolbar/ToolbarHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/toolbar/ToolbarHookPatch.kt new file mode 100644 index 0000000000..b01a440873 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/toolbar/ToolbarHookPatch.kt @@ -0,0 +1,74 @@ +package app.revanced.patches.youtube.layout.toolbar + +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.fieldReference +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.removeInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.shared.getToolBarButtonMethodMatch +import app.revanced.util.findFreeRegister +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ToolbarPatch;" + +internal lateinit var hookToolbar: (descriptor: String)-> Unit + +val toolbarHookPatch = bytecodePatch { + dependsOn( + sharedExtensionPatch, + resourceMappingPatch + ) + + apply { + fun indexOfGetDrawableInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.toString() == "Landroid/content/res/Resources;->getDrawable(I)Landroid/graphics/drawable/Drawable;" + } + + getToolBarButtonMethodMatch().method.apply { + val getDrawableIndex = indexOfGetDrawableInstruction(this) + val enumOrdinalIndex = indexOfFirstInstructionReversedOrThrow(getDrawableIndex) { + opcode == Opcode.INVOKE_INTERFACE && + getReference()?.returnType == "I" + } + val replaceReference = getInstruction(enumOrdinalIndex).reference + val replaceRegister = getInstruction(enumOrdinalIndex).registerC + val enumRegister = getInstruction(enumOrdinalIndex).registerD + val insertIndex = enumOrdinalIndex + 1 + val freeRegister = findFreeRegister(insertIndex, enumRegister, replaceRegister) + + val imageViewIndex = indexOfFirstInstructionOrThrow(enumOrdinalIndex) { + opcode == Opcode.IGET_OBJECT && fieldReference?.type == "Landroid/widget/ImageView;" + } + val imageViewReference = getInstruction(imageViewIndex).reference + + addInstructions( + insertIndex, + """ + iget-object v$freeRegister, p0, $imageViewReference + invoke-static {v$enumRegister, v$freeRegister}, $EXTENSION_CLASS_DESCRIPTOR->hookToolbar(Ljava/lang/Enum;Landroid/widget/ImageView;)V + invoke-interface {v$replaceRegister, v$enumRegister}, $replaceReference + """ + ) + removeInstruction(enumOrdinalIndex) + } + + hookToolbar = { descriptor -> + hookToolbarMethod.addInstructions( + 0, + "invoke-static {p0, p1}, $descriptor(Ljava/lang/String;Landroid/view/View;)V" + ) + } + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt index e50b59c3d2..11b2729a0e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt @@ -24,10 +24,12 @@ val announcementsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt index 22689c44ca..53cc5beca7 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt @@ -27,6 +27,11 @@ val pauseOnAudioInterruptPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( "20.14.43", + "20.21.37", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt index 6b39c04b5d..6f4944eaa4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt @@ -11,9 +11,11 @@ import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater +import app.revanced.patches.youtube.misc.playservice.is_20_29_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.backgroundPlaybackManagerShortsMethod import app.revanced.patches.youtube.video.information.videoInformationPatch import app.revanced.util.* import com.android.tools.smali.dexlib2.Opcode @@ -43,10 +45,12 @@ val removeBackgroundPlaybackRestrictionsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -57,7 +61,8 @@ val removeBackgroundPlaybackRestrictionsPatch = bytecodePatch( SwitchPreference("revanced_shorts_disable_background_playback"), ) - prefBackgroundAndOfflineCategoryId = ResourceType.STRING["pref_background_and_offline_category"] + prefBackgroundAndOfflineCategoryId = + ResourceType.STRING["pref_background_and_offline_category"] arrayOf( backgroundPlaybackManagerMethod to "isBackgroundPlaybackAllowed", @@ -72,7 +77,7 @@ val removeBackgroundPlaybackRestrictionsPatch = bytecodePatch( """ invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->$integrationsMethod(Z)Z move-result v$register - """, + """ ) } } @@ -106,5 +111,11 @@ val removeBackgroundPlaybackRestrictionsPatch = bytecodePatch( ) } } + + if (is_20_29_or_greater) { + // Client flag that interferes with background playback of some video types. + // Exact purpose is not clear and it's used in ~ 100 locations. + newPlayerTypeEnumFeatureFlagMethod.returnLate(false) + } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/Fingerprints.kt index 64d6a26d2d..8f1c18ad8b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/Fingerprints.kt @@ -72,13 +72,6 @@ internal val BytecodePatchContext.kidsBackgroundPlaybackPolicyControllerMethod b literal { 5 } } -internal val BytecodePatchContext.backgroundPlaybackManagerShortsMethod by gettingFirstMethodDeclaratively { - accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) - returnType("Z") - parameterTypes("L") - instructions(151635310L()) -} - internal val BytecodePatchContext.shortsBackgroundPlaybackFeatureFlagMethod by gettingFirstMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("Z") @@ -93,3 +86,7 @@ internal val BytecodePatchContext.pipInputConsumerFeatureFlagMethodMatch by comp 45638483L(), ) } + +internal val BytecodePatchContext.newPlayerTypeEnumFeatureFlagMethod by gettingFirstMethodDeclaratively { + instructions(45698813L()) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatch.kt new file mode 100644 index 0000000000..ec82e4bdbe --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatch.kt @@ -0,0 +1,177 @@ +package app.revanced.patches.youtube.misc.contexthook + +import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod +import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable +import app.revanced.patcher.accessFlags +import app.revanced.patcher.classDef +import app.revanced.patcher.extensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.fieldReference +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.methodReference +import app.revanced.patcher.firstMethodDeclaratively +import app.revanced.patcher.immutableClassDef +import app.revanced.patcher.parameterTypes +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.returnType +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.util.addInstructionsAtControlFlowLabel +import app.revanced.util.cloneMutableAndPreserveParameters +import app.revanced.util.findInstructionIndicesReversedOrThrow +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod + +private lateinit var browseIdField: FieldReference +private lateinit var clientInfoField: FieldReference +private lateinit var clientVersionField: FieldReference +private lateinit var messageLiteBuilderField: FieldReference +private lateinit var messageLiteBuilderMethod: MethodReference +private lateinit var osNameField: FieldReference + +enum class Endpoint( + vararg val getEndpointMethods: BytecodePatchContext.() -> Method, + var instructions: String = "", +) { + BROWSE( + BytecodePatchContext::browseEndpointParentMethod::get + ), + GUIDE( + BytecodePatchContext::guideEndpointConstructorMethod::get + ), + REEL( + BytecodePatchContext::reelCreateItemsEndpointConstructorMethod::get, + BytecodePatchContext::reelItemWatchEndpointConstructorMethod::get, + BytecodePatchContext::reelWatchSequenceEndpointConstructorMethod::get, + ), + SEARCH(BytecodePatchContext::searchRequestBuildParametersMethod::get) +} + +val hookClientContextPatch = bytecodePatch( + description = "Hooks the context body of the endpoint.", +) { + dependsOn(sharedExtensionPatch) + + apply { + buildDummyClientContextBodyMethodMatch.let { + it.method.apply { + val clientInfoIndex = it[-1] + val clientVersionIndex = it[2] + val messageLiteBuilderIndex = it[0] + + clientInfoField = + getInstruction(clientInfoIndex).fieldReference!! + clientVersionField = + getInstruction(clientVersionIndex).fieldReference!! + messageLiteBuilderField = + getInstruction(messageLiteBuilderIndex).fieldReference!! + } + } + + authenticationChangeListenerMethod.apply { + val messageLiteBuilderIndex = + indexOfMessageLiteBuilderReference(this, messageLiteBuilderField.definingClass) + + messageLiteBuilderMethod = + getInstruction(messageLiteBuilderIndex).methodReference!! + } + + buildClientContextBodyConstructorMethod.immutableClassDef.buildClientContextBodyMethodMatch.let { + it.method.apply { + val osNameIndex = it[1] + + osNameField = + getInstruction(osNameIndex).fieldReference!! + } + } + + browseEndpointParentMethod.immutableClassDef.browseEndpointConstructorMethodMatch.let { + it.method.apply { + val browseIdIndex = it[-1] + browseIdField = + getInstruction(browseIdIndex).fieldReference!! + } + } + } + + afterDependents { + val helperMethodName = "patch_setClientContext" + + Endpoint.entries.filter { + it.instructions.isNotEmpty() + }.forEach { endpoint -> + endpoint.getEndpointMethods.forEach { getEndpointRequestBodyParentMethod -> + getEndpointRequestBodyParentMethod().immutableClassDef.firstMethodDeclaratively { + accessFlags(AccessFlags.PROTECTED, AccessFlags.FINAL) + returnType("V") + parameterTypes() + }.cloneMutableAndPreserveParameters().let { + it.classDef.methods.add( + ImmutableMethod( + it.definingClass, + helperMethodName, + emptyList(), + "V", + AccessFlags.PRIVATE.value or AccessFlags.FINAL.value, + it.annotations, + null, + MutableMethodImplementation(5), + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + invoke-virtual { p0 }, $messageLiteBuilderMethod + move-result-object v0 + iget-object v0, v0, $messageLiteBuilderField + check-cast v0, ${clientInfoField.definingClass} + iget-object v1, v0, $clientInfoField + if-eqz v1, :ignore + """ + endpoint.instructions + + """ + :ignore + return-void + """, + ) + } + ) + + it.findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + it.addInstructionsAtControlFlowLabel( + index, + "invoke-direct/range { p0 .. p0 }, ${it.definingClass}->$helperMethodName()V" + ) + } + } + } + } + } +} + +fun addClientVersionHook(endPoint: Endpoint, descriptor: String) { + endPoint.instructions += if (endPoint == Endpoint.BROWSE) """ + iget-object v3, p0, $browseIdField + iget-object v2, v1, $clientVersionField + invoke-static { v3, v2 }, $descriptor + move-result-object v2 + iput-object v2, v1, $clientVersionField + """ else """ + iget-object v2, v1, $clientVersionField + invoke-static { v2 }, $descriptor + move-result-object v2 + iput-object v2, v1, $clientVersionField + """ +} + +fun addOSNameHook(endPoint: Endpoint, descriptor: String) { + endPoint.instructions += """ + iget-object v2, v1, $osNameField + invoke-static { v2 }, $descriptor + move-result-object v2 + iput-object v2, v1, $osNameField + """ +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/Fingerprints.kt new file mode 100644 index 0000000000..9c52762689 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/Fingerprints.kt @@ -0,0 +1,153 @@ +package app.revanced.patches.youtube.misc.contexthook + +import app.revanced.patcher.ClassDefComposing +import app.revanced.patcher.accessFlags +import app.revanced.patcher.after +import app.revanced.patcher.afterAtMost +import app.revanced.patcher.allOf +import app.revanced.patcher.composingFirstMethod +import app.revanced.patcher.custom +import app.revanced.patcher.extensions.methodReference +import app.revanced.patcher.field +import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively +import app.revanced.patcher.gettingFirstMethodDeclaratively +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke +import app.revanced.patcher.method +import app.revanced.patcher.parameterTypes +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.returnType +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.Method +import com.google.common.io.ByteArrayDataOutput + +internal const val CLIENT_INFO_CLASS_DESCRIPTOR = + $$"Lcom/google/protos/youtube/api/innertube/InnertubeContext$ClientInfo;" + +internal val BytecodePatchContext.authenticationChangeListenerMethod by gettingFirstMethodDeclaratively( + "Authentication changed while request was being made" +) { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returnType("V") + custom { indexOfMessageLiteBuilderReference(this) >= 0 } +} + +internal fun indexOfMessageLiteBuilderReference(method: Method, type: String = "L") = + method.indexOfFirstInstruction { + val reference = methodReference + opcode == Opcode.INVOKE_VIRTUAL && + reference?.parameterTypes?.isEmpty() == true && reference.returnType.startsWith(type) + } + +internal val BytecodePatchContext.buildClientContextBodyConstructorMethod by gettingFirstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returnType("V") + instructions( + "Android Wear"(), + Opcode.IF_EQZ(), + after("Android Automotive"()), + "Android"(), + after(allOf(Opcode.IPUT_OBJECT(), field())) + ) +} + +internal val ClassDef.buildClientContextBodyMethodMatch by ClassDefComposing.composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("L") + parameterTypes() + instructions( + allOf(Opcode.SGET(), field("SDK_INT")), + allOf( + Opcode.IPUT_OBJECT(), + field { definingClass == CLIENT_INFO_CLASS_DESCRIPTOR && type == "Ljava/lang/String;" } + ), + Opcode.OR_INT_LIT16() + ) +} + +internal val BytecodePatchContext.buildDummyClientContextBodyMethodMatch by composingFirstMethod { + instructions( + allOf( + Opcode.IGET_OBJECT(), + field("instance") + ), + afterAtMost(10, "10.29"()), + allOf( + Opcode.IPUT_OBJECT(), + field { definingClass == CLIENT_INFO_CLASS_DESCRIPTOR && type == "Ljava/lang/String;" } + ), + allOf( + Opcode.IPUT_OBJECT(), + field { type == CLIENT_INFO_CLASS_DESCRIPTOR } + ) + ) +} + +internal val ClassDef.browseEndpointConstructorMethodMatch by ClassDefComposing.composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returnType("V") + + var methodDefiningClass = "" + custom { + methodDefiningClass = this.definingClass + true + } + + instructions( + ""(), + after( + allOf( + Opcode.IPUT_OBJECT(), + field { definingClass == methodDefiningClass && type == "Ljava/lang/String;" } + ) + ), + ) +} + +internal val BytecodePatchContext.browseEndpointParentMethod by gettingFirstImmutableMethodDeclaratively( + "browseId" +) { + returnType("Ljava/lang/String;") +} + +internal val BytecodePatchContext.guideEndpointConstructorMethod by gettingFirstImmutableMethodDeclaratively( + "guide" +) { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returnType("V") +} + +internal val BytecodePatchContext.reelCreateItemsEndpointConstructorMethod by gettingFirstImmutableMethodDeclaratively( + "reel/create_reel_items" +) { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returnType("V") +} + +internal val BytecodePatchContext.reelItemWatchEndpointConstructorMethod by gettingFirstImmutableMethodDeclaratively( + "reel/reel_item_watch" +) { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returnType("V") +} + +internal val BytecodePatchContext.reelWatchSequenceEndpointConstructorMethod by gettingFirstImmutableMethodDeclaratively( + "reel/reel_watch_sequence" +) { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returnType("V") +} + +internal val BytecodePatchContext.searchRequestBuildParametersMethod by gettingFirstImmutableMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Ljava/lang/String;") + parameterTypes() + instructions( + "searchFormData"(), + after(allOf(Opcode.INVOKE_VIRTUAL(), method("toByteArray"))), + after(Opcode.MOVE_RESULT_OBJECT()) + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt index 73e5f30e78..3ac32bd420 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt @@ -2,21 +2,35 @@ package app.revanced.patches.youtube.misc.debugging import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch; import app.revanced.patches.shared.misc.debugging.enableDebuggingPatch +import app.revanced.patches.youtube.misc.playservice.is_20_40_or_greater +import app.revanced.patches.youtube.misc.playservice.is_20_41_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch @Suppress("unused") val enableDebuggingPatch = enableDebuggingPatch( - sharedExtensionPatch = sharedExtensionPatch, - settingsPatch = settingsPatch, - compatibleWithPackages = arrayOf( - "com.google.android.youtube" to setOf( - "19.43.41", - "20.14.43", - "20.21.37", - "20.31.40", + block = { + dependsOn( + sharedExtensionPatch, + settingsPatch, + versionCheckPatch ) - ), - hookStringFeatureFlag = true, + + compatibleWith( + "com.google.android.youtube"( + "20.14.43", + "20.21.37", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" + ) + ) + }, + hookStringFeatureFlag = { true }, + // 20.40 has changes not worth supporting. + hookLongFeatureFlag = { !is_20_40_or_greater || is_20_41_or_greater }, + hookDoubleFeatureFlag = { !is_20_40_or_greater || is_20_41_or_greater }, preferenceScreen = PreferenceScreen.MISC, ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch.kt index 934e2049bd..4b1381b280 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch.kt @@ -27,10 +27,12 @@ val spoofDeviceDimensionsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -42,19 +44,20 @@ val spoofDeviceDimensionsPatch = bytecodePatch( ) // Override the parameters containing the dimensions. - deviceDimensionsModelToStringMethod.classDef.methods.firstMethod { name == "" }.addInstructions( - 1, // Add after super call. - arrayOf( - 1 to "MinHeightOrWidth", // p1 = min height - 2 to "MaxHeightOrWidth", // p2 = max height - 3 to "MinHeightOrWidth", // p3 = min width - 4 to "MaxHeightOrWidth", // p4 = max width - ).map { (parameter, method) -> - """ + deviceDimensionsModelToStringMethod.classDef.methods.firstMethod { name == "" } + .addInstructions( + 1, // Add after super call. + arrayOf( + 1 to "MinHeightOrWidth", // p1 = min height + 2 to "MaxHeightOrWidth", // p2 = max height + 3 to "MinHeightOrWidth", // p3 = min width + 4 to "MaxHeightOrWidth", // p4 = max width + ).map { (parameter, method) -> + """ invoke-static { p$parameter }, $EXTENSION_CLASS_DESCRIPTOR->get$method(I)I move-result p$parameter """ - }.joinToString("\n") { it }, - ) + }.joinToString("\n") { it }, + ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt index 2824056d46..28125a0809 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt @@ -13,10 +13,12 @@ val checkWatchHistoryDomainNameResolutionPatch = checkWatchHistoryDomainNameReso compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/engagement/EngagementPanelHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/engagement/EngagementPanelHookPatch.kt new file mode 100644 index 0000000000..dc274c3b64 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/engagement/EngagementPanelHookPatch.kt @@ -0,0 +1,80 @@ +package app.revanced.patches.youtube.misc.engagement + +import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod +import app.revanced.patcher.extensions.addInstruction +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.fieldReference +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.shared.getEngagementPanelControllerMethodMatch +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import kotlin.properties.Delegates + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/shared/EngagementPanel;" + +var panelControllerMethod: MutableMethod by Delegates.notNull() + private set +var panelIdIndex = 0 + private set +var panelIdRegister = 0 + private set +var panelIdSmaliInstruction = "" + private set + +val engagementPanelHookPatch = bytecodePatch( + description = "Hook to get the current engagement panel state.", +) { + dependsOn(sharedExtensionPatch) + + apply { + val match = getEngagementPanelControllerMethodMatch() + match.method.apply { + val panelIdField = getInstruction(match[-1]).fieldReference + val insertIndex = match[5] + + val (freeRegister, panelRegister) = + with(getInstruction(insertIndex)) { + Pair(registerA, registerB) + } + + panelControllerMethod = this + panelIdIndex = insertIndex + panelIdRegister = freeRegister + panelIdSmaliInstruction = + "iget-object v$panelIdRegister, v$panelRegister, $panelIdField" + + addInstructions( + insertIndex, + """ + $panelIdSmaliInstruction + invoke-static { v${panelIdRegister} }, ${EXTENSION_CLASS_DESCRIPTOR}->open(Ljava/lang/String;)V + """ + ) + } + + match.immutableClassDef.getEngagementPanelUpdateMethod().addInstruction( + 0, + "invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->close()V" + ) + } +} + + +fun addEngagementPanelIdHook(descriptor: String) = panelControllerMethod.addInstructionsWithLabels( + panelIdIndex, + """ + $panelIdSmaliInstruction + invoke-static { v$panelIdRegister }, $descriptor + move-result v$panelIdRegister + if-eqz v$panelIdRegister, :shown + const/4 v$panelIdRegister, 0x0 + return-object v$panelIdRegister + :shown + nop + """ +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/engagement/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/engagement/Fingerprints.kt new file mode 100644 index 0000000000..2d199f62f6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/engagement/Fingerprints.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.youtube.misc.engagement + +import app.revanced.patcher.accessFlags +import app.revanced.patcher.allOf +import app.revanced.patcher.field +import app.revanced.patcher.firstMethodDeclaratively +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke +import app.revanced.patcher.parameterTypes +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.returnType +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.ClassDef + +context(_: BytecodePatchContext) +internal fun ClassDef.getEngagementPanelUpdateMethod() = firstMethodDeclaratively { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returnType("V") + parameterTypes("L", "Z") + instructions(allOf(Opcode.IGET_OBJECT(), field { type == "Landroid/app/Activity;" })) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/AccountCredentialsInvalidTextPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/AccountCredentialsInvalidTextPatch.kt index 6b07ea5bed..a1e094788a 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/AccountCredentialsInvalidTextPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/AccountCredentialsInvalidTextPatch.kt @@ -22,12 +22,12 @@ internal val accountCredentialsInvalidTextPatch = bytecodePatch { // If the user recently changed their account password, // the app can show "You're offline. Check your internet connection." - // even when the internet is available. For this situation + // even when the internet is available. For this situation // YouTube + MicroG shows an offline error message. // // Change the error text to inform the user to uninstall and reinstall MicroG. // The user can also fix this by deleting the MicroG account but - // MicroG accounts look almost identical to Google device accounts + // MicroG accounts look almost identical to Google device accounts, // and it's more foolproof to instead uninstall/reinstall. arrayOf( specificNetworkErrorViewControllerMethodMatch, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/Fingerprints.kt index 78ab4b951d..b434de2bbb 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/Fingerprints.kt @@ -18,8 +18,8 @@ internal val BytecodePatchContext.specificNetworkErrorViewControllerMethodMatch ) } -// It's not clear if this second class is ever used and it may be dead code, -// but it the layout image/text is identical to the network error match above. +// It's not clear if this second class is ever used, and it may be dead code, +// but it is the layout image/text is identical to the network error match above. internal val BytecodePatchContext.loadingFrameLayoutControllerMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt index e745c28381..edeeb237f9 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt @@ -36,10 +36,12 @@ val gmsCoreSupportPatch = gmsCoreSupportPatch( compatibleWith( YOUTUBE_PACKAGE_NAME( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/hapticfeedback/DisableHapticFeedbackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/hapticfeedback/DisableHapticFeedbackPatch.kt deleted file mode 100644 index 95f7b428e4..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/hapticfeedback/DisableHapticFeedbackPatch.kt +++ /dev/null @@ -1,69 +0,0 @@ -package app.revanced.patches.youtube.misc.hapticfeedback - -import app.revanced.patcher.extensions.ExternalLabel -import app.revanced.patcher.extensions.addInstructionsWithLabels -import app.revanced.patcher.extensions.getInstruction -import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patches.all.misc.resources.addResources -import app.revanced.patches.all.misc.resources.addResourcesPatch -import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference -import app.revanced.patches.shared.misc.settings.preference.SwitchPreference -import app.revanced.patches.youtube.misc.settings.PreferenceScreen -import app.revanced.patches.youtube.misc.settings.settingsPatch - -private const val EXTENSION_CLASS_DESCRIPTOR = - "Lapp/revanced/extension/youtube/patches/DisableHapticFeedbackPatch;" - -@Suppress("unused") -val disableHapticFeedbackPatch = bytecodePatch( - name = "Disable haptic feedback", - description = "Adds an option to disable haptic feedback in the player for various actions.", -) { - dependsOn( - settingsPatch, - addResourcesPatch, - ) - - compatibleWith( - "com.google.android.youtube"( - "19.43.41", - "20.14.43", - "20.21.37", - "20.31.40", - ), - ) - - apply { - addResources("youtube", "misc.hapticfeedback.disableHapticFeedbackPatch") - - PreferenceScreen.PLAYER.addPreferences( - PreferenceScreenPreference( - "revanced_disable_haptic_feedback", - preferences = setOf( - SwitchPreference("revanced_disable_haptic_feedback_chapters"), - SwitchPreference("revanced_disable_haptic_feedback_precise_seeking"), - SwitchPreference("revanced_disable_haptic_feedback_seek_undo"), - SwitchPreference("revanced_disable_haptic_feedback_zoom"), - ), - ), - ) - - arrayOf( - markerHapticsMethod to "disableChapterVibrate", - scrubbingHapticsMethod to "disablePreciseSeekingVibrate", - seekUndoHapticsMethod to "disableSeekUndoVibrate", - zoomHapticsMethod to "disableZoomVibrate", - ).forEach { (method, methodName) -> - method.addInstructionsWithLabels( - 0, - """ - invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->$methodName()Z - move-result v0 - if-eqz v0, :vibrate - return-void - """, - ExternalLabel("vibrate", method.getInstruction(0)), - ) - } - } -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/hapticfeedback/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/hapticfeedback/Fingerprints.kt deleted file mode 100644 index f37729e3d1..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/hapticfeedback/Fingerprints.kt +++ /dev/null @@ -1,28 +0,0 @@ -package app.revanced.patches.youtube.misc.hapticfeedback - -import app.revanced.patcher.* -import app.revanced.patcher.patch.BytecodePatchContext - -internal val BytecodePatchContext.markerHapticsMethod by gettingFirstMethodDeclaratively( - "Failed to execute markers haptics vibrate.", -) { - returnType("V") -} - -internal val BytecodePatchContext.scrubbingHapticsMethod by gettingFirstMethodDeclaratively( - "Failed to haptics vibrate for fine scrubbing.", -) { - returnType("V") -} - -internal val BytecodePatchContext.seekUndoHapticsMethod by gettingFirstMethodDeclaratively( - "Failed to execute seek undo haptics vibrate.", -) { - returnType("V") -} - -internal val BytecodePatchContext.zoomHapticsMethod by gettingFirstMethodDeclaratively( - "Failed to haptics vibrate for video zoom", -) { - returnType("V") -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHook.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHook.kt index 91b2192276..f1d569e459 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHook.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHook.kt @@ -17,8 +17,8 @@ import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod -private lateinit var loadImageUrlMethod: MutableMethod -private var loadImageUrlIndex = 0 +private lateinit var loadImageURLMethod: MutableMethod +private var loadImageURLIndex = 0 private lateinit var loadImageSuccessCallbackMethod: MutableMethod private var loadImageSuccessCallbackIndex = 0 @@ -26,14 +26,14 @@ private var loadImageSuccessCallbackIndex = 0 private lateinit var loadImageErrorCallbackMethod: MutableMethod private var loadImageErrorCallbackIndex = 0 -val cronetImageUrlHookPatch = bytecodePatch( - description = "Hooks Cronet image urls.", +val cronetImageURLHookPatch = bytecodePatch( + description = "Hooks Cronet image URLs.", ) { dependsOn(sharedExtensionPatch) apply { - loadImageUrlMethod = messageDigestImageUrlParentMethod.immutableClassDef.getMessageDigestImageUrlMethod() + loadImageURLMethod = messageDigestImageURLParentMethod.immutableClassDef.getMessageDigestImageURLMethod() loadImageSuccessCallbackMethod = onResponseStartedMethod.immutableClassDef.getOnSucceededMethod() loadImageErrorCallbackMethod = onResponseStartedMethod.immutableClassDef.getOnFailureMethod() @@ -72,22 +72,22 @@ val cronetImageUrlHookPatch = bytecodePatch( /** * @param highPriority If the hook should be called before all other hooks. */ -fun addImageUrlHook(targetMethodClass: String, highPriority: Boolean = false) { - loadImageUrlMethod.addInstructions( - if (highPriority) 0 else loadImageUrlIndex, +fun addImageURLHook(targetMethodClass: String, highPriority: Boolean = false) { + loadImageURLMethod.addInstructions( + if (highPriority) 0 else loadImageURLIndex, """ invoke-static { p1 }, $targetMethodClass->overrideImageURL(Ljava/lang/String;)Ljava/lang/String; move-result-object p1 """, ) - loadImageUrlIndex += 2 + loadImageURLIndex += 2 } /** * If a connection completed, which includes normal 200 responses but also includes * status 404 and other error like http responses. */ -fun addImageUrlSuccessCallbackHook(targetMethodClass: String) { +fun addImageURLSuccessCallbackHook(targetMethodClass: String) { loadImageSuccessCallbackMethod.addInstruction( loadImageSuccessCallbackIndex++, "invoke-static { p1, p2 }, $targetMethodClass->handleCronetSuccess(" + @@ -98,7 +98,7 @@ fun addImageUrlSuccessCallbackHook(targetMethodClass: String) { /** * If a connection outright failed to complete any connection. */ -fun addImageUrlErrorCallbackHook(targetMethodClass: String) { +fun addImageURLErrorCallbackHook(targetMethodClass: String) { loadImageErrorCallbackMethod.addInstruction( loadImageErrorCallbackIndex++, "invoke-static { p1, p2, p3 }, $targetMethodClass->handleCronetFailure(" + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/Fingerprints.kt index 29d69d9a80..a273132f94 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/Fingerprints.kt @@ -46,12 +46,12 @@ internal val BytecodePatchContext.requestMethod by gettingFirstMethodDeclarative } context(_: BytecodePatchContext) -internal fun ClassDef.getMessageDigestImageUrlMethod() = firstMethodDeclaratively { +internal fun ClassDef.getMessageDigestImageURLMethod() = firstMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) parameterTypes("Ljava/lang/String;", "L") } -internal val BytecodePatchContext.messageDigestImageUrlParentMethod by gettingFirstMethodDeclaratively { +internal val BytecodePatchContext.messageDigestImageURLParentMethod by gettingFirstMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("Ljava/lang/String;") parameterTypes() diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch.kt index 99f4a9f2cf..7978e8e8b4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch.kt @@ -1,5 +1,6 @@ package app.revanced.patches.youtube.misc.links +import app.revanced.patcher.CompositeMatch import app.revanced.patcher.extensions.getInstruction import app.revanced.patcher.extensions.replaceInstruction import app.revanced.patcher.patch.bytecodePatch @@ -8,11 +9,13 @@ import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playservice.is_20_37_or_greater +import app.revanced.patches.youtube.misc.playservice.is_20_49_or_greater import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/BypassURLRedirectsPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/BypassURLRedirectsPatch;" @Suppress("unused") val bypassURLRedirectsPatch = bytecodePatch( @@ -27,10 +30,12 @@ val bypassURLRedirectsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) @@ -42,20 +47,24 @@ val bypassURLRedirectsPatch = bytecodePatch( ) arrayOf( - if (is_20_37_or_greater) { - (abUriParserMethodMatch to 2) + if (is_20_49_or_greater) { + // Code has moved, and now seems to be an account URL + // and may not be anything to do with sharing links. + null to -1 + } else if (is_20_37_or_greater) { + abUriParserMethodMatch to 2 } else { - (abUriParserLegacyMethodMatch to 2) - }, - httpUriParserMethodMatch to 0, + abUriParserLegacyMethodMatch to 2 + } ).forEach { (match, index) -> - val insertIndex = match[index] - val uriStringRegister = match.method.getInstruction(insertIndex).registerC + val insertIndex = match?.get(index) ?: return@forEach + val uriStringRegister = + match.method.getInstruction(insertIndex).registerC match.method.replaceInstruction( insertIndex, "invoke-static { v$uriStringRegister }, $EXTENSION_CLASS_DESCRIPTOR->" + - "parseRedirectUri(Ljava/lang/String;)Landroid/net/Uri;", + "parseRedirectUri(Ljava/lang/String;)Landroid/net/Uri;", ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/Fingerprints.kt index bea03da447..7e268e76f5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/Fingerprints.kt @@ -34,15 +34,3 @@ internal val BytecodePatchContext.abUriParserMethodMatch by composingFirstMethod method { toString() == "Ljava/util/List;->get(I)Ljava/lang/Object;" }, ) } - -internal val BytecodePatchContext.httpUriParserMethodMatch by composingFirstMethod { - accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) - returnType("Landroid/net/Uri;") - parameterTypes("Ljava/lang/String;") - instructions( - method { toString() == "Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;" }, - "https"(), - "://"(), - "https:"(), - ) -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch.kt index 6132801071..0128184a68 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch.kt @@ -42,10 +42,12 @@ val openLinksExternallyPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt index aab6d49715..4813ed99ca 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt @@ -3,7 +3,6 @@ package app.revanced.patches.youtube.misc.litho.filter import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode internal val BytecodePatchContext.lithoComponentNameUpbFeatureFlagMethod by gettingFirstMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt index be87c62780..772e5b6efc 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt @@ -49,7 +49,7 @@ val lithoFilterPatch = lithoFilterPatch( executeBlock = { // region A/B test of new Litho native code. - // Turn off native code that handles litho component names. If this feature is on then nearly + // Turn off native code that handles litho component names. If this feature is on then nearly // all litho components have a null name and identifier/path filtering is completely broken. // // Flag was removed in 20.05. It appears a new flag might be used instead (45660109L), diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/loopvideo/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/loopvideo/Fingerprints.kt deleted file mode 100644 index 388a54e5d5..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/loopvideo/Fingerprints.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.revanced.patches.youtube.misc.loopvideo - -import app.revanced.patcher.accessFlags -import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively -import app.revanced.patcher.instructions -import app.revanced.patcher.invoke -import app.revanced.patcher.patch.BytecodePatchContext -import app.revanced.patcher.returnType -import com.android.tools.smali.dexlib2.AccessFlags - -internal val BytecodePatchContext.videoStartPlaybackMethod by gettingFirstImmutableMethodDeclaratively { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returnType("V") - instructions( - "play() called when the player wasn't loaded."(), - "play() blocked because Background Playability failed"(), - ) -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/loopvideo/LoopVideoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/loopvideo/LoopVideoPatch.kt index 1c70aa973c..e30ae91997 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/loopvideo/LoopVideoPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/loopvideo/LoopVideoPatch.kt @@ -1,5 +1,7 @@ package app.revanced.patches.youtube.misc.loopvideo +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch @@ -7,15 +9,15 @@ import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.loopvideo.button.loopVideoButtonPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen -import app.revanced.patches.youtube.video.information.videoEndMethod +import app.revanced.patches.youtube.video.information.playerStatusMethod import app.revanced.patches.youtube.video.information.videoInformationPatch -import app.revanced.util.addInstructionsAtControlFlowLabel -import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/LoopVideoPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/LoopVideoPatch;" -@Suppress("ObjectPropertyName") val loopVideoPatch = bytecodePatch( name = "Loop video", description = "Adds an option to loop videos and display loop video button in the video player.", @@ -29,36 +31,42 @@ val loopVideoPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) apply { addResources("youtube", "misc.loopvideo.loopVideoPatch") - PreferenceScreen.MISC.addPreferences( + PreferenceScreen.PLAYER.addPreferences( SwitchPreference("revanced_loop_video"), ) - videoEndMethod.apply { - // Add call to start playback again, but must not allow exit fullscreen patch call - // to be reached if the video is looped. - val insertIndex = indexOfFirstInstructionReversedOrThrow(Opcode.INVOKE_VIRTUAL) + 1 + playerStatusMethod.apply { + // Add call to start playback again, but must happen before "Exit fullscreen" patch call. + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.SGET_OBJECT) + // Since instructions are added just above Opcode.SGET_OBJECT, instead of calling findFreeRegister(), + // a register from Opcode.SGET_OBJECT is used. + val freeRegister = + getInstruction(insertIndex).registerA - addInstructionsAtControlFlowLabel( + // Since 'videoInformationPatch' is used as a dependency of this patch, + // the loop is implemented through 'VideoInformation.seekTo(0)'. + addInstructionsWithLabels( insertIndex, """ - invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->shouldLoopVideo()Z - move-result v0 - if-eqz v0, :do_not_loop - invoke-virtual { p0 }, $videoStartPlaybackMethod + invoke-static/range { p1 .. p1 }, $EXTENSION_CLASS_DESCRIPTOR->shouldLoopVideo(Ljava/lang/Enum;)Z + move-result v$freeRegister + if-eqz v$freeRegister, :do_not_loop return-void :do_not_loop nop - """, + """ ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/Fingerprints.kt index b1a3dbc82f..c8a6a5fce0 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/Fingerprints.kt @@ -8,7 +8,7 @@ import app.revanced.patcher.parameterTypes import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.returnType import app.revanced.patches.shared.misc.mapping.ResourceType -import app.revanced.patches.youtube.layout.buttons.navigation.navigationButtonsPatch +import app.revanced.patches.youtube.layout.buttons.navigation.navigationBarPatch import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.ClassDef @@ -50,12 +50,12 @@ context(_: BytecodePatchContext) internal fun ClassDef.getInitializeButtonsMethod() = firstMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") - instructions(ResourceType.LAYOUT("image_only_tab")) + instructions("FEvideo_picker"()) } /** * Extension method, used for callback into to other patches. - * Specifically, [navigationButtonsPatch]. + * Specifically, [navigationBarPatch]. */ internal val BytecodePatchContext.navigationBarHookCallbackMethod by gettingFirstMethodDeclaratively { name("navigationTabCreatedCallback") @@ -76,8 +76,8 @@ internal val BytecodePatchContext.navigationEnumMethod by gettingFirstImmutableM "TAB_ACTIVITY", "VIDEO_LIBRARY_WHITE", "INCOGNITO_CIRCLE", - "UNKNOWN", // Required to distinguish from patch extension class. ) { + definingClass { !startsWith("Lapp/revanced") } accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) } @@ -125,11 +125,11 @@ internal val BytecodePatchContext.pivotBarConstructorMethod by gettingFirstImmut } internal val BytecodePatchContext.imageEnumConstructorMethodMatch by composingFirstMethod { + definingClass { !startsWith("Lapp/revanced") } accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) instructions( "TAB_ACTIVITY_CAIRO"(), - after(Opcode.INVOKE_DIRECT()), - after(Opcode.SPUT_OBJECT()), + Opcode.SPUT_OBJECT() ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt index 8e7651635c..705539bd40 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt @@ -126,7 +126,7 @@ val navigationBarHookPatch = bytecodePatch(description = "Hooks the active navig ) } - // Hook onto back button pressed. Needed to fix race problem with + // Hook onto back button pressed. Needed to fix race problem with // Litho filtering based on navigation tab before the tab is updated. mainActivityOnBackPressedMethod.addInstruction( 0, @@ -204,14 +204,8 @@ val navigationBarHookPatch = bytecodePatch(description = "Hooks the active navig ) } - if (is_20_39_or_greater) { - return@apply Logger.getLogger(this::class.java.name).warning( - "20.39+ Navigation tab activity button selected state is not yet fixed.", - ) - } - // Fix YT bug of notification tab missing the filled icon. - if (is_19_35_or_greater && !is_20_39_or_greater) { // FIXME: 20.39+ needs this fix. + if (is_19_35_or_greater) { val cairoNotificationEnumReference = imageEnumConstructorMethodMatch.method.getInstruction(imageEnumConstructorMethodMatch[-1]).reference diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt index 973fa94ff5..aeec4f5b01 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt @@ -110,7 +110,7 @@ internal val playerControlsResourcePatch = resourcePatch { "android.support.constraint.ConstraintLayout", ).item(0).childNodes - // Copy the patch layout xml into the target layout file. + // Copy the patch layout XML into the target layout file. for (index in sourceElements.length - 1 downTo 1) { val element = sourceElements.item(index).cloneNode(true) @@ -252,7 +252,7 @@ val playerControlsPatch = bytecodePatch( visibilityMethod = playerTopControlsInflateMethodMatch.immutableClassDef.getControlsOverlayVisibilityMethod() - // Hook the fullscreen close button. Used to fix visibility + // Hook the fullscreen close button. Used to fix visibility // when seeking and other situations. overlayViewInflateMethodMatch.method.apply { val index = overlayViewInflateMethodMatch[-1] diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt index aa761dac94..f2e6b41ee3 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt @@ -8,94 +8,124 @@ import kotlin.properties.Delegates // Use notNull delegate so an exception is thrown if these fields are accessed before they are set. -@Deprecated("19.43.41 is the lowest supported version") -var is_19_17_or_greater : Boolean by Delegates.notNull() +@Deprecated("20.14.43 is the lowest supported version") +var is_19_17_or_greater: Boolean by Delegates.notNull() private set -@Deprecated("19.43.41 is the lowest supported version") -var is_19_18_or_greater : Boolean by Delegates.notNull() + +@Deprecated("20.14.43 is the lowest supported version") +var is_19_18_or_greater: Boolean by Delegates.notNull() private set -@Deprecated("19.43.41 is the lowest supported version") -var is_19_23_or_greater : Boolean by Delegates.notNull() + +@Deprecated("20.14.43 is the lowest supported version") +var is_19_23_or_greater: Boolean by Delegates.notNull() private set -@Deprecated("19.43.41 is the lowest supported version") -var is_19_25_or_greater : Boolean by Delegates.notNull() + +@Deprecated("20.14.43 is the lowest supported version") +var is_19_25_or_greater: Boolean by Delegates.notNull() private set -@Deprecated("19.43.41 is the lowest supported version") -var is_19_26_or_greater : Boolean by Delegates.notNull() + +@Deprecated("20.14.43 is the lowest supported version") +var is_19_26_or_greater: Boolean by Delegates.notNull() private set -@Deprecated("19.43.41 is the lowest supported version") -var is_19_29_or_greater : Boolean by Delegates.notNull() + +@Deprecated("20.14.43 is the lowest supported version") +var is_19_29_or_greater: Boolean by Delegates.notNull() private set -@Deprecated("19.43.41 is the lowest supported version") -var is_19_32_or_greater : Boolean by Delegates.notNull() + +@Deprecated("20.14.43 is the lowest supported version") +var is_19_32_or_greater: Boolean by Delegates.notNull() private set -@Deprecated("19.43.41 is the lowest supported version") -var is_19_33_or_greater : Boolean by Delegates.notNull() + +@Deprecated("20.14.43 is the lowest supported version") +var is_19_33_or_greater: Boolean by Delegates.notNull() private set -@Deprecated("19.43.41 is the lowest supported version") -var is_19_34_or_greater : Boolean by Delegates.notNull() + +@Deprecated("20.14.43 is the lowest supported version") +var is_19_34_or_greater: Boolean by Delegates.notNull() private set -@Deprecated("19.43.41 is the lowest supported version") -var is_19_35_or_greater : Boolean by Delegates.notNull() + +@Deprecated("20.14.43 is the lowest supported version") +var is_19_35_or_greater: Boolean by Delegates.notNull() private set -@Deprecated("19.43.41 is the lowest supported version") -var is_19_36_or_greater : Boolean by Delegates.notNull() + +@Deprecated("20.14.43 is the lowest supported version") +var is_19_36_or_greater: Boolean by Delegates.notNull() private set -@Deprecated("19.43.41 is the lowest supported version") -var is_19_41_or_greater : Boolean by Delegates.notNull() + +@Deprecated("20.14.43 is the lowest supported version") +var is_19_41_or_greater: Boolean by Delegates.notNull() private set -@Deprecated("19.43.41 is the lowest supported version") -var is_19_43_or_greater : Boolean by Delegates.notNull() + +@Deprecated("20.14.43 is the lowest supported version") +var is_19_43_or_greater: Boolean by Delegates.notNull() private set -var is_19_46_or_greater : Boolean by Delegates.notNull() +var is_19_46_or_greater: Boolean by Delegates.notNull() private set -var is_19_47_or_greater : Boolean by Delegates.notNull() +var is_19_47_or_greater: Boolean by Delegates.notNull() private set -var is_19_49_or_greater : Boolean by Delegates.notNull() +var is_19_49_or_greater: Boolean by Delegates.notNull() private set -var is_20_02_or_greater : Boolean by Delegates.notNull() +var is_20_02_or_greater: Boolean by Delegates.notNull() private set -var is_20_03_or_greater : Boolean by Delegates.notNull() +var is_20_03_or_greater: Boolean by Delegates.notNull() private set -var is_20_05_or_greater : Boolean by Delegates.notNull() +var is_20_05_or_greater: Boolean by Delegates.notNull() private set -var is_20_07_or_greater : Boolean by Delegates.notNull() +var is_20_07_or_greater: Boolean by Delegates.notNull() private set -var is_20_09_or_greater : Boolean by Delegates.notNull() +var is_20_09_or_greater: Boolean by Delegates.notNull() private set -var is_20_10_or_greater : Boolean by Delegates.notNull() +var is_20_10_or_greater: Boolean by Delegates.notNull() private set -var is_20_14_or_greater : Boolean by Delegates.notNull() +var is_20_14_or_greater: Boolean by Delegates.notNull() private set -var is_20_15_or_greater : Boolean by Delegates.notNull() +var is_20_15_or_greater: Boolean by Delegates.notNull() private set -var is_20_19_or_greater : Boolean by Delegates.notNull() +var is_20_19_or_greater: Boolean by Delegates.notNull() private set -var is_20_20_or_greater : Boolean by Delegates.notNull() +var is_20_20_or_greater: Boolean by Delegates.notNull() private set -var is_20_21_or_greater : Boolean by Delegates.notNull() +var is_20_21_or_greater: Boolean by Delegates.notNull() private set -var is_20_22_or_greater : Boolean by Delegates.notNull() +var is_20_22_or_greater: Boolean by Delegates.notNull() private set -var is_20_26_or_greater : Boolean by Delegates.notNull() +var is_20_26_or_greater: Boolean by Delegates.notNull() private set -var is_20_28_or_greater : Boolean by Delegates.notNull() +var is_20_28_or_greater: Boolean by Delegates.notNull() private set -var is_20_30_or_greater : Boolean by Delegates.notNull() +var is_20_29_or_greater: Boolean by Delegates.notNull() private set -var is_20_31_or_greater : Boolean by Delegates.notNull() +var is_20_30_or_greater: Boolean by Delegates.notNull() private set -var is_20_34_or_greater : Boolean by Delegates.notNull() +var is_20_31_or_greater: Boolean by Delegates.notNull() private set -var is_20_37_or_greater : Boolean by Delegates.notNull() +var is_20_34_or_greater: Boolean by Delegates.notNull() private set -var is_20_39_or_greater : Boolean by Delegates.notNull() +var is_20_37_or_greater: Boolean by Delegates.notNull() private set -var is_20_41_or_greater : Boolean by Delegates.notNull() +var is_20_39_or_greater: Boolean by Delegates.notNull() private set -var is_20_45_or_greater : Boolean by Delegates.notNull() +var is_20_40_or_greater: Boolean by Delegates.notNull() private set -var is_20_46_or_greater : Boolean by Delegates.notNull() +var is_20_41_or_greater: Boolean by Delegates.notNull() + private set +var is_20_45_or_greater: Boolean by Delegates.notNull() + private set +var is_20_46_or_greater: Boolean by Delegates.notNull() + private set +var is_20_49_or_greater: Boolean by Delegates.notNull() + private set +var is_21_02_or_greater: Boolean by Delegates.notNull() + private set +var is_21_03_or_greater: Boolean by Delegates.notNull() + private set +var is_21_05_or_greater : Boolean by Delegates.notNull() + private set +var is_21_06_or_greater : Boolean by Delegates.notNull() + private set +var is_21_07_or_greater : Boolean by Delegates.notNull() + private set +var is_21_08_or_greater : Boolean by Delegates.notNull() private set val versionCheckPatch = resourcePatch( @@ -137,13 +167,22 @@ val versionCheckPatch = resourcePatch( is_20_22_or_greater = 252305000 <= playStoreServicesVersion is_20_26_or_greater = 252705000 <= playStoreServicesVersion is_20_28_or_greater = 252905000 <= playStoreServicesVersion + is_20_29_or_greater = 253005000 <= playStoreServicesVersion is_20_30_or_greater = 253105000 <= playStoreServicesVersion is_20_31_or_greater = 253205000 <= playStoreServicesVersion is_20_34_or_greater = 253505000 <= playStoreServicesVersion is_20_37_or_greater = 253805000 <= playStoreServicesVersion is_20_39_or_greater = 253980000 <= playStoreServicesVersion + is_20_40_or_greater = 254105000 <= playStoreServicesVersion is_20_41_or_greater = 254205000 <= playStoreServicesVersion is_20_45_or_greater = 254605000 <= playStoreServicesVersion is_20_46_or_greater = 254705000 <= playStoreServicesVersion + is_20_49_or_greater = 255005000 <= playStoreServicesVersion + is_21_02_or_greater = 260305000 <= playStoreServicesVersion + is_21_03_or_greater = 260405000 <= playStoreServicesVersion + is_21_05_or_greater = 260605000 <= playStoreServicesVersion + is_21_06_or_greater = 260705000 <= playStoreServicesVersion + is_21_07_or_greater = 260805000 <= playStoreServicesVersion + is_21_08_or_greater = 260905000 <= playStoreServicesVersion } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/SanitizeSharingLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/SanitizeSharingLinksPatch.kt index c3cf0e93d0..a8c9549578 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/SanitizeSharingLinksPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/SanitizeSharingLinksPatch.kt @@ -15,10 +15,12 @@ val sanitizeSharingLinksPatch = sanitizeSharingLinksPatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/Fingerprints.kt index 37b3b50806..6467516580 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/Fingerprints.kt @@ -29,14 +29,3 @@ internal val BytecodePatchContext.cairoFragmentConfigMethodMatch by composingFir afterAtMost(10, Opcode.MOVE_RESULT()), ) } - -// Flag is present in 20.23, but bold icons are missing and forcing them crashes the app. -// 20.31 is the first target with all the bold icons present. -internal val BytecodePatchContext.boldIconsFeatureFlagMethodMatch by composingFirstMethod { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returnType("Z") - parameterTypes() - instructions( - 45685201L(), - ) -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt index 38b200d4b3..3eb9d2eb15 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt @@ -11,6 +11,7 @@ import app.revanced.patcher.patch.resourcePatch import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.boldIconsFeatureFlagMethodMatch import app.revanced.patches.shared.misc.mapping.resourceMappingPatch import app.revanced.patches.shared.misc.settings.overrideThemeColors import app.revanced.patches.shared.misc.settings.preference.* @@ -270,7 +271,7 @@ val settingsPatch = bytecodePatch( } /** - * Modifies the activity to show ReVanced settings instead of it's original purpose. + * Modifies the activity to show ReVanced settings instead of its original purpose. */ internal fun modifyActivityForSettingsInjection( activityOnCreateClass: MutableClassDef, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt index 428d55af73..980e64888e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt @@ -33,10 +33,12 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch( block = { compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt index 0397f26992..05edd856cd 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt @@ -1,8 +1,8 @@ package app.revanced.patches.youtube.shared -import app.revanced.patcher.ClassDefComposing import app.revanced.patcher.accessFlags import app.revanced.patcher.after +import app.revanced.patcher.afterAtMost import app.revanced.patcher.allOf import app.revanced.patcher.composingFirstMethod import app.revanced.patcher.custom @@ -20,13 +20,13 @@ import app.revanced.patcher.opcodes import app.revanced.patcher.parameterTypes import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.returnType -import app.revanced.patcher.type import app.revanced.patches.shared.misc.mapping.ResourceType import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.ClassDef -internal const val YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE = "Lcom/google/android/apps/youtube/app/watchwhile/MainActivity;" +internal const val YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE = + "Lcom/google/android/apps/youtube/app/watchwhile/MainActivity;" internal val BytecodePatchContext.conversionContextToStringMethod by gettingFirstImmutableMethodDeclaratively( ", widthConstraint=", @@ -40,6 +40,31 @@ internal val BytecodePatchContext.conversionContextToStringMethod by gettingFirs instructions("ConversionContext{"(String::startsWith)) // Partial string match. } +internal val BytecodePatchContext.backgroundPlaybackManagerShortsMethod by gettingFirstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returnType("Z") + parameterTypes("L") + instructions(151635310L()) +} + +internal fun BytecodePatchContext.getEngagementPanelControllerMethodMatch() = firstMethodComposite { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returnType("L") + parameterTypes("L", "L", "Z", "Z") + instructions( + "EngagementPanelController: cannot show EngagementPanel before EngagementPanelController.init() has been called."(), + method { toString() == "Lj$/util/Optional;->orElse(Ljava/lang/Object;)Ljava/lang/Object;" }, + method { toString() == "Lj$/util/Optional;->orElse(Ljava/lang/Object;)Ljava/lang/Object;" }, + afterAtMost(4, Opcode.CHECK_CAST()), + after(Opcode.IF_EQZ()), + after(Opcode.IGET_OBJECT()), + 45615449L(), + method { toString() == "Ljava/util/ArrayDeque;->iterator()Ljava/util/Iterator;" }, + afterAtMost(10, allOf(Opcode.IGET_OBJECT(), field { type == "Ljava/lang/String;" })) + ) +} + + internal fun BytecodePatchContext.getLayoutConstructorMethodMatch() = firstMethodComposite { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") @@ -53,7 +78,8 @@ internal fun BytecodePatchContext.getLayoutConstructorMethodMatch() = firstMetho ResourceType.ID("player_control_next_button_touch_area"), method { parameterTypes.size == 2 && - parameterTypes.zip(methodParameterTypePrefixes).all { (a, b) -> a.startsWith(b) } + parameterTypes.zip(methodParameterTypePrefixes) + .all { (a, b) -> a.startsWith(b) } }, ) } @@ -100,8 +126,8 @@ internal val BytecodePatchContext.rollingNumberTextViewAnimationUpdateMethodMatc ) custom { immutableClassDef.superclass == "Landroid/support/v7/widget/AppCompatTextView;" || - immutableClassDef.superclass == - "Lcom/google/android/libraries/youtube/rendering/ui/spec/typography/YouTubeAppCompatTextView;" + immutableClassDef.superclass == + "Lcom/google/android/libraries/youtube/rendering/ui/spec/typography/YouTubeAppCompatTextView;" } } @@ -110,9 +136,6 @@ internal val BytecodePatchContext.seekbarMethod by gettingFirstImmutableMethodDe instructions("timed_markers_width"()) } -/** - * Matches to _mutable_ class found in [seekbarMethod]. - */ internal fun ClassDef.getSeekbarOnDrawMethodMatch() = firstMethodComposite { name("onDraw") instructions( @@ -124,7 +147,7 @@ internal fun ClassDef.getSeekbarOnDrawMethodMatch() = firstMethodComposite { internal val BytecodePatchContext.subtitleButtonControllerMethod by gettingFirstMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") - parameterTypes("Lcom/google/android/libraries/youtube/player/subtitles/model/SubtitleTrack;") + parameterTypes("L") instructions( ResourceType.STRING("accessibility_captions_unavailable"), ResourceType.STRING("accessibility_captions_button_name"), @@ -136,9 +159,32 @@ internal val BytecodePatchContext.videoQualityChangedMethodMatch by composingFir returnType("L") parameterTypes("L") instructions( - allOf(Opcode.NEW_INSTANCE(), type("Lcom/google/android/libraries/youtube/innertube/model/media/VideoQuality;")), - Opcode.IGET_OBJECT(), + allOf(Opcode.IGET(), field { type == "I" }), + after(2L()), + after(Opcode.IF_NE()), + after(Opcode.NEW_INSTANCE()), // Obfuscated VideoQuality. + afterAtMost(6, Opcode.IGET_OBJECT()), Opcode.CHECK_CAST(), after(allOf(Opcode.IGET(), field { type == "I" })), // Video resolution (human-readable). ) } + + +internal fun BytecodePatchContext.getToolBarButtonMethodMatch() = firstMethodComposite { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + instructions( + ResourceType.ID("menu_item_view"), + allOf(Opcode.INVOKE_INTERFACE(), method { returnType == "I" }), + after(Opcode.MOVE_RESULT()), + afterAtMost(6, allOf(Opcode.IGET_OBJECT(), field { type == "Landroid/widget/ImageView;" })), + afterAtMost( + 8, + method { name == "getDrawable" && definingClass == "Landroid/content/res/Resources;" }), + afterAtMost( + 4, + method { name == "setImageDrawable" && definingClass == "Landroid/widget/ImageView;" }), + ) + // 20.37+ has second parameter of "Landroid/content/Context;" + custom { parameterTypes.count() in 1..2 && parameterTypes.first() == "Landroid/view/MenuItem;" } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt index 90fbbe740f..2289a65dbd 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt @@ -20,10 +20,12 @@ val forceOriginalAudioPatch = forceOriginalAudioPatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/codecs/DisableVideoCodecsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/codecs/DisableVideoCodecsPatch.kt index a7f027fd73..7e3360dde0 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/codecs/DisableVideoCodecsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/codecs/DisableVideoCodecsPatch.kt @@ -14,7 +14,8 @@ import app.revanced.util.getReference import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/DisableVideoCodecsPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/DisableVideoCodecsPatch;" @Suppress("unused") val disableVideoCodecsPatch = bytecodePatch( @@ -55,10 +56,12 @@ val disableVideoCodecsPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt index 9f3b8e52cb..544cad4d0e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt @@ -7,33 +7,47 @@ import app.revanced.patches.youtube.shared.videoQualityChangedMethodMatch import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.ClassDef -import org.stringtemplate.v4.compiler.Bytecode internal val BytecodePatchContext.createVideoPlayerSeekbarMethod by gettingFirstImmutableMethodDeclaratively { returnType("V") instructions("timed_markers_width"()) } -internal val BytecodePatchContext.onPlaybackSpeedItemClickMethod by gettingFirstMethodDeclaratively { + +internal val BytecodePatchContext.onPlaybackSpeedItemClickParentMethod by gettingFirstImmutableMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returnType("L") + parameterTypes("L", "Ljava/lang/String;") + instructions( + method("getSupportFragmentManager"), + after(Opcode.MOVE_RESULT_OBJECT()), + after(method { returnType.startsWith("L") && parameterTypes.size == 1 && parameterTypes.first() == "Ljava/lang/String;" }), + after(Opcode.MOVE_RESULT_OBJECT()), + after(Opcode.IF_EQZ()), + after(Opcode.CHECK_CAST()) + ) + custom { immutableClassDef.methods.count() == 8 } +} + +/** + * Resolves using the method found in [onPlaybackSpeedItemClickParentMethod]. + */ + +context(_: BytecodePatchContext) +internal fun ClassDef.getOnPlaybackSpeedItemClickMethod() = firstMethodDeclaratively { name("onItemClick") accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") parameterTypes("L", "L", "I", "J") - instructions( - allOf( - Opcode.IGET_OBJECT(), - field { type == "Lcom/google/android/libraries/youtube/innertube/model/player/PlayerResponseModel;" }, - ), - ) } internal val BytecodePatchContext.playerControllerSetTimeReferenceMethodMatch by - composingFirstMethod("Media progress reported outside media playback: ") { - opcodes( - Opcode.INVOKE_DIRECT_RANGE, - Opcode.IGET_OBJECT, - ) - } +composingFirstMethod("Media progress reported outside media playback: ") { + opcodes( + Opcode.INVOKE_DIRECT_RANGE, + Opcode.IGET_OBJECT, + ) +} internal val BytecodePatchContext.playVideoCheckVideoStreamingDataResponseMethod by gettingFirstImmutableMethodDeclaratively { instructions("playVideo called on player response with no videoStreamingData."()) @@ -43,7 +57,14 @@ internal val BytecodePatchContext.playVideoCheckVideoStreamingDataResponseMethod * Matched using class found in [playVideoCheckVideoStreamingDataResponseMethod]. */ internal fun ClassDef.getSeekMethod() = firstImmutableMethodDeclaratively { - instructions("Attempting to seek during an ad"()) + instructions( + anyOf( + // 20.xx + "Attempting to seek during an ad"(), + // 21.02+ + "currentPositionMs."() + ) + ) } internal val ClassDef.videoLengthMethodMatch by ClassDefComposing.composingFirstMethod { @@ -116,17 +137,33 @@ internal fun ClassDef.getSeekRelativeMethod() = firstMethodDeclaratively { ) } -internal val BytecodePatchContext.videoEndMethodMatch by composingFirstMethod { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returnType("Z") - parameterTypes("J", "L") - instructions( - method { parameterTypes.isEmpty() && returnType == "V" }, - afterAtMost(5, 45368273L()), - "Attempting to seek when video is not playing"(), - ) +internal val BytecodePatchContext.playerStatusEnumMethod by gettingFirstImmutableMethodDeclaratively( + "NEW", + "PLAYBACK_PENDING", + "PLAYBACK_LOADED", + "PLAYBACK_INTERRUPTED", + "INTERSTITIAL_REQUESTED", + "INTERSTITIAL_PLAYING", + "VIDEO_PLAYING", + "ENDED", +) { + accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) } +context(context: BytecodePatchContext) +internal fun ClassDef.getPlayerStatusMethod() = + firstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes(context.playerStatusEnumMethod.immutableClassDef.type) + instructions( + // The opcode for the first index of the method is sget-object. + // Even in sufficiently old versions, such as YT 17.34, the opcode for the first index is sget-object. + Opcode.SGET_OBJECT(), + method { name == "plus" && definingClass == "Lj$/time/Instant;" }, + ) + } + /** * Matches with the class found in [videoQualityChangedMethodMatch]. */ @@ -146,14 +183,11 @@ internal val BytecodePatchContext.playbackSpeedClassMethod by gettingFirstMethod opcodes(Opcode.RETURN_OBJECT) } -internal const val YOUTUBE_VIDEO_QUALITY_CLASS_TYPE = - "Lcom/google/android/libraries/youtube/innertube/model/media/VideoQuality;" - /** * YouTube 20.19 and lower. */ internal val BytecodePatchContext.videoQualityLegacyMethod by gettingFirstMethodDeclaratively { - definingClass(YOUTUBE_VIDEO_QUALITY_CLASS_TYPE) + definingClass("Lcom/google/android/libraries/youtube/innertube/model/media/VideoQuality;") accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) parameterTypes( "I", // Resolution. @@ -163,8 +197,21 @@ internal val BytecodePatchContext.videoQualityLegacyMethod by gettingFirstMethod ) } + +internal val BytecodePatchContext.playbackStartDescriptorToStringMethodMatch by composingFirstMethod { + name("toString") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Ljava/lang/String;") + instructions( + method { toString() == "Ljava/util/Locale;->getDefault()Ljava/util/Locale;" }, + // First method call after Locale is the video ID. + method { returnType == "Ljava/lang/String;" && parameterTypes.isEmpty() }, + "PlaybackStartDescriptor:"(String::startsWith) + ) +} + +// Class name is un-obfuscated in targets before 21.01. internal val BytecodePatchContext.videoQualityMethod by gettingFirstMethodDeclaratively { - definingClass(YOUTUBE_VIDEO_QUALITY_CLASS_TYPE) accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) parameterTypes( "I", // Resolution. diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt index b8bd0a4f57..26117af4df 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt @@ -11,6 +11,7 @@ import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playservice.is_20_19_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_20_or_greater +import app.revanced.patches.youtube.misc.playservice.is_20_49_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.shared.videoQualityChangedMethodMatch import app.revanced.patches.youtube.video.playerresponse.Hook @@ -24,9 +25,11 @@ import app.revanced.util.addInstructionsAtControlFlowLabel import app.revanced.util.addStaticFieldToExtension import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.patcher.patch.PatchException import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction @@ -42,9 +45,11 @@ import com.android.tools.smali.dexlib2.util.MethodUtil private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/VideoInformation;" private const val EXTENSION_PLAYER_INTERFACE = - "Lapp/revanced/extension/youtube/patches/VideoInformation\$PlaybackController;" + $$"Lapp/revanced/extension/youtube/patches/VideoInformation$PlaybackController;" private const val EXTENSION_VIDEO_QUALITY_MENU_INTERFACE = - "Lapp/revanced/extension/youtube/patches/VideoInformation\$VideoQualityMenuInterface;" + $$"Lapp/revanced/extension/youtube/patches/VideoInformation$VideoQualityMenuInterface;" +internal const val EXTENSION_VIDEO_QUALITY_INTERFACE = + $$"Lapp/revanced/extension/youtube/patches/VideoInformation$VideoQualityInterface;" private lateinit var playerInitMethod: MutableMethod private var playerInitInsertIndex = -1 @@ -57,7 +62,7 @@ private var mdxInitInsertRegister = -1 private lateinit var timeMethod: MutableMethod private var timeInitInsertIndex = 2 -// Old speed menu, where speeds are entries in a list. Method is also used by the player speed button. +// Old speed menu, where speeds are entries in a list. Method is also used by the player speed button. private lateinit var legacySpeedSelectionInsertMethod: MutableMethod private var legacySpeedSelectionInsertIndex = -1 private var legacySpeedSelectionValueRegister = -1 @@ -71,11 +76,13 @@ private var speedSelectionValueRegister = -1 private lateinit var setPlaybackSpeedMethod: MutableMethod private var setPlaybackSpeedMethodIndex = -1 -internal lateinit var videoEndMethod: MutableMethod +internal lateinit var playerStatusMethod: MutableMethod // Used by other patches. internal lateinit var setPlaybackSpeedContainerClassFieldReference: FieldReference private set +internal lateinit var setPlaybackSpeedContainerClassFieldReferenceClassType: ClassDef + private set internal lateinit var setPlaybackSpeedClassFieldReference: FieldReference private set internal lateinit var setPlaybackSpeedMethodReference: MethodReference @@ -123,7 +130,7 @@ val videoInformationPatch = bytecodePatch( mdxInitInsertIndex = initThisIndex + 1 // Hook the MDX director for use through the extension. - onCreateHookMdx(EXTENSION_CLASS_DESCRIPTOR, "initializeMdx") + onCreateHookMDX(EXTENSION_CLASS_DESCRIPTOR, "initializeMDX") addSeekInterfaceMethods( classDef, @@ -150,11 +157,10 @@ val videoInformationPatch = bytecodePatch( } } - videoEndMethod = navigate(videoEndMethodMatch.immutableMethod) - .to(videoEndMethodMatch[0]).stop() + playerStatusMethod = playerInitMethod.immutableClassDef.getPlayerStatusMethod() /* - * Inject call for video ids + * Inject call for video IDs */ val videoIdMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->setVideoId(Ljava/lang/String;)V" hookVideoId(videoIdMethodDescriptor) @@ -162,8 +168,8 @@ val videoInformationPatch = bytecodePatch( hookPlayerResponseVideoId( "$EXTENSION_CLASS_DESCRIPTOR->setPlayerResponseVideoId(Ljava/lang/String;Z)V", ) - // Call before any other video id hooks, - // so they can use VideoInformation and check if the video id is for a Short. + // Call before any other video ID hooks, + // so they can use VideoInformation and check if the video ID is for a Short. addPlayerResponseMethodHook( Hook.ProtoBufferParameterBeforeVideoId( "$EXTENSION_CLASS_DESCRIPTOR->" + @@ -185,7 +191,7 @@ val videoInformationPatch = bytecodePatch( /* * Hook the user playback speed selection. */ - onPlaybackSpeedItemClickMethod.apply { + onPlaybackSpeedItemClickParentMethod.immutableClassDef.getOnPlaybackSpeedItemClickMethod().apply { val speedSelectionValueInstructionIndex = indexOfFirstInstructionOrThrow(Opcode.IGET) legacySpeedSelectionInsertMethod = this @@ -193,20 +199,49 @@ val videoInformationPatch = bytecodePatch( legacySpeedSelectionValueRegister = getInstruction(speedSelectionValueInstructionIndex).registerA - setPlaybackSpeedMethodReference = - getInstruction(speedSelectionValueInstructionIndex + 2).reference as MethodReference - setPlaybackSpeedClassFieldReference = - getInstruction(speedSelectionValueInstructionIndex + 1).reference as FieldReference - setPlaybackSpeedContainerClassFieldReference = - getInstruction(indexOfFirstInstructionOrThrow(Opcode.IF_EQZ) - 1).reference as FieldReference + setPlaybackSpeedMethodReference = getInstruction( + indexOfFirstInstructionOrThrow(speedSelectionValueInstructionIndex) { + val reference = getReference() + reference?.parameterTypes?.size == 1 && reference.parameterTypes.first() == "F" + } + ).reference as MethodReference - setPlaybackSpeedMethod = - firstClassDef(setPlaybackSpeedMethodReference.definingClass) - .methods.first { it.name == setPlaybackSpeedMethodReference.name } + setPlaybackSpeedContainerClassFieldReference = getInstruction( + indexOfFirstInstructionOrThrow(Opcode.IF_EQZ) - 1 + ).reference as FieldReference + + if (is_20_49_or_greater) { + // Only one class implements the interface. Patcher currently does not have a + // 'first' accessor for looking up classes, so do it ourselves to verify + // we're using the expected class type. + var fieldReferenceType: ClassDef? = null + classDefs.forEach { classDef -> + if (classDef.interfaces.contains(setPlaybackSpeedContainerClassFieldReference.type)) { + if (fieldReferenceType != null) { + throw PatchException("Found more than one playback speed interface: $classDef") + } + fieldReferenceType = classDef + } + } + setPlaybackSpeedContainerClassFieldReferenceClassType = fieldReferenceType!! + } else { + setPlaybackSpeedContainerClassFieldReferenceClassType = + classDefs[setPlaybackSpeedContainerClassFieldReference.type]!! + } + + setPlaybackSpeedClassFieldReference = getInstruction( + indexOfFirstInstructionOrThrow(speedSelectionValueInstructionIndex) { + getReference()?.type?.startsWith("L") == true + } + ).reference as FieldReference + + setPlaybackSpeedMethod = firstClassDef( + setPlaybackSpeedMethodReference.definingClass + ).methods.first { it.name == setPlaybackSpeedMethodReference.name } setPlaybackSpeedMethodIndex = 0 // Add override playback speed method. - onPlaybackSpeedItemClickMethod.classDef.methods.add( + classDef.methods.add( ImmutableMethod( definingClass, "overridePlaybackSpeed", @@ -224,16 +259,19 @@ val videoInformationPatch = bytecodePatch( if-lez v0, :ignore # Get the container class field. - iget-object v0, v2, $setPlaybackSpeedContainerClassFieldReference - + iget-object v0, v2, $setPlaybackSpeedContainerClassFieldReference + # For some reason, in YouTube 19.44.39 this value is sometimes null. if-eqz v0, :ignore + + # Required cast for 20.49+ + check-cast v0, $setPlaybackSpeedContainerClassFieldReferenceClassType # Get the field from its class. iget-object v1, v0, $setPlaybackSpeedClassFieldReference # Invoke setPlaybackSpeed on that class. - invoke-virtual {v1, v3}, $setPlaybackSpeedMethodReference + invoke-virtual { v1, v3 }, $setPlaybackSpeedMethodReference :ignore return-void @@ -286,7 +324,10 @@ val videoInformationPatch = bytecodePatch( } } + val videoQualityClassType : String (if (is_20_19_or_greater) videoQualityMethod else videoQualityLegacyMethod).apply { + videoQualityClassType = immutableClassDef.type + // Fix bad data used by YouTube. val nameRegister = if (is_20_20_or_greater) "p3" else "p2" addInstructions( @@ -299,6 +340,9 @@ val videoInformationPatch = bytecodePatch( // Add methods to access obfuscated quality fields. classDef.apply { + // Add interface and helper methods to allow extension code to call obfuscated methods. + interfaces.add(EXTENSION_VIDEO_QUALITY_INTERFACE) + methods.add( ImmutableMethod( type, @@ -370,7 +414,7 @@ val videoInformationPatch = bytecodePatch( type, "patch_setQuality", listOf( - ImmutableMethodParameter(YOUTUBE_VIDEO_QUALITY_CLASS_TYPE, null, null), + ImmutableMethodParameter(EXTENSION_VIDEO_QUALITY_INTERFACE, null, null), ), "V", AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, @@ -379,12 +423,13 @@ val videoInformationPatch = bytecodePatch( MutableMethodImplementation(2), ).toMutable().apply { val setQualityMenuIndexMethod = methods.single { method -> - method.parameterTypes.firstOrNull() == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE + method.parameterTypes.firstOrNull() == videoQualityClassType } addInstructions( 0, """ + check-cast p1, $videoQualityClassType invoke-virtual { p0, p1 }, $setQualityMenuIndexMethod return-void """, @@ -400,7 +445,7 @@ val videoInformationPatch = bytecodePatch( iget-object v0, p0, $onItemClickListenerClassReference iget-object v0, v0, $setQualityFieldReference - invoke-static { p1, v0, p2 }, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality([$YOUTUBE_VIDEO_QUALITY_CLASS_TYPE${EXTENSION_VIDEO_QUALITY_MENU_INTERFACE}I)I + invoke-static { p1, v0, p2 }, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality([$EXTENSION_VIDEO_QUALITY_INTERFACE${EXTENSION_VIDEO_QUALITY_MENU_INTERFACE}I)I move-result p2 """, ) @@ -471,9 +516,9 @@ private fun MutableMethod.insertTimeHook(insertIndex: Int, descriptor: String) = insert(insertIndex, "p1, p2", descriptor) /** - * Hook the player controller. Called when a video is opened or the current video is changed. + * Hook the player controller. Called when a video is opened or the current video is changed. * - * Note: This hook is called very early and is called before the video id, video time, video length, + * Note: This hook is called very early and is called before the video ID, video time, video length, * and many other data fields are set. * * @param targetMethodClass The descriptor for the class to invoke when the player controller is created. @@ -492,7 +537,7 @@ internal fun onCreateHook(targetMethodClass: String, targetMethodName: String) = * @param targetMethodClass The descriptor for the class to invoke when the player controller is created. * @param targetMethodName The name of the static method to invoke when the player controller is created. */ -internal fun onCreateHookMdx(targetMethodClass: String, targetMethodName: String) = +internal fun onCreateHookMDX(targetMethodClass: String, targetMethodName: String) = mdxInitMethod.insert( mdxInitInsertIndex++, "v$mdxInitInsertRegister", diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt index cd78c70575..3a981345ad 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt @@ -23,7 +23,7 @@ internal val BytecodePatchContext.playerParameterBuilderMethod by gettingFirstMe "Ljava/lang/String;", "Ljava/lang/String;", "L", - "Z", // Appears to indicate if the video id is being opened or is currently playing. + "Z", // Appears to indicate if the video ID is being opened or is currently playing. "Z", "Z", "Lj$/time/Duration;", @@ -49,7 +49,7 @@ internal val BytecodePatchContext.playerParameterBuilder2026Method by gettingFir "Ljava/lang/String;", "Ljava/lang/String;", "L", - "Z", // Appears to indicate if the video id is being opened or is currently playing. + "Z", // Appears to indicate if the video ID is being opened or is currently playing. "Z", "Z", "Lj$/time/Duration;", @@ -76,7 +76,7 @@ internal val BytecodePatchContext.playerParameterBuilder2015Method by gettingFir "Ljava/lang/String;", "Ljava/lang/String;", "L", - "Z", // Appears to indicate if the video id is being opened or is currently playing. + "Z", // Appears to indicate if the video ID is being opened or is currently playing. "Z", "Z", ) @@ -102,7 +102,7 @@ internal val BytecodePatchContext.playerParameterBuilder2010Method by gettingFir "Ljava/lang/String;", "Ljava/lang/String;", "L", - "Z", // Appears to indicate if the video id is being opened or is currently playing. + "Z", // Appears to indicate if the video ID is being opened or is currently playing. "Z", "Z", "Z", @@ -128,7 +128,7 @@ internal val BytecodePatchContext.playerParameterBuilder2002Method by gettingFir "Ljava/lang/String;", "Ljava/lang/String;", "L", - "Z", // Appears to indicate if the video id is being opened or is currently playing. + "Z", // Appears to indicate if the video ID is being opened or is currently playing. "Z", "Z", "Z", @@ -154,7 +154,7 @@ internal val BytecodePatchContext.playerParameterBuilder1925Method by gettingFir "Ljava/lang/String;", "Ljava/lang/String;", "L", - "Z", // Appears to indicate if the video id is being opened or is currently playing. + "Z", // Appears to indicate if the video ID is being opened or is currently playing. "Z", "Z", ) @@ -178,7 +178,7 @@ internal val BytecodePatchContext.playerParameterBuilderLegacyMethod by gettingF "Ljava/lang/String;", "Ljava/lang/String;", "L", - "Z", // Appears to indicate if the video id is being opened or is currently playing. + "Z", // Appears to indicate if the video ID is being opened or is currently playing. "Z", "Z", ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt index f10565959c..614fd9c0f3 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt @@ -1,9 +1,18 @@ package app.revanced.patches.youtube.video.quality import app.revanced.patcher.accessFlags +import app.revanced.patcher.afterAtMost +import app.revanced.patcher.allOf import app.revanced.patcher.composingFirstMethod +import app.revanced.patcher.custom +import app.revanced.patcher.definingClass +import app.revanced.patcher.field import app.revanced.patcher.firstMethodDeclaratively import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively +import app.revanced.patcher.gettingFirstMethodDeclaratively +import app.revanced.patcher.immutableClassDef +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke import app.revanced.patcher.name import app.revanced.patcher.opcodes import app.revanced.patcher.parameterTypes @@ -14,6 +23,43 @@ import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.ClassDef +internal val BytecodePatchContext.currentVideoFormatToStringMethod by gettingFirstImmutableMethodDeclaratively( + "currentVideoFormat=" +) { + name("toString") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Ljava/lang/String;") + parameterTypes() +} + +internal val BytecodePatchContext.defaultOverflowOverlayOnClickMethodMatch by composingFirstMethod { + name("onClick") + definingClass("Lcom/google/android/libraries/youtube/player/features/overlay/overflow/ui/DefaultOverflowOverlay;") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes("Landroid/view/View;") + + var methodDefiningClass = "" + custom { + methodDefiningClass = definingClass + true + } + + instructions( + Opcode.IF_NE(), + afterAtMost(2, allOf(Opcode.IGET_OBJECT(), field { definingClass == methodDefiningClass })) + ) +} + +internal val BytecodePatchContext.hidePremiumVideoQualityGetArrayMethod by gettingFirstMethodDeclaratively { + name("apply") + definingClass("Lapp/revanced/extension/youtube/patches/playback/quality/HidePremiumVideoQualityPatch") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Ljava/lang/Object;") + parameterTypes("I") + custom { AccessFlags.SYNTHETIC.isSet(immutableClassDef.accessFlags) } +} + internal val BytecodePatchContext.videoQualityItemOnClickParentMethod by gettingFirstImmutableMethodDeclaratively( "VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT", ) { diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/HidePremiumVideoQualityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/HidePremiumVideoQualityPatch.kt new file mode 100644 index 0000000000..3bac14e576 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/HidePremiumVideoQualityPatch.kt @@ -0,0 +1,97 @@ +package app.revanced.patches.youtube.video.quality + +import app.revanced.patcher.accessFlags +import app.revanced.patcher.afterAtMost +import app.revanced.patcher.allOf +import app.revanced.patcher.custom +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.fieldReference +import app.revanced.patcher.field +import app.revanced.patcher.firstMethodComposite +import app.revanced.patcher.immutableClassDef +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.returnType +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.video.information.EXTENSION_VIDEO_QUALITY_INTERFACE +import app.revanced.patches.youtube.video.information.videoInformationPatch +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/playback/quality/HidePremiumVideoQualityPatch;" + +internal val hidePremiumVideoQualityPatch = bytecodePatch { + dependsOn( + sharedExtensionPatch, + settingsPatch, + videoInformationPatch, + addResourcesPatch + ) + + apply { + addResources("youtube", "video.quality.hidePremiumVideoQualityPatch") + + settingsMenuVideoQualityGroup.add( + SwitchPreference("revanced_hide_premium_video_quality") + ) + + // Class name is obfuscated in 21.02+. + val videoQualityArrayFieldType = defaultOverflowOverlayOnClickMethodMatch.let { + it.method.getInstruction(it[-1]).fieldReference!!.type + } + + // To avoid ClassCastException, declare the new array + // as original video quality class instead of EXTENSION_VIDEO_QUALITY_INTERFACE. + hidePremiumVideoQualityGetArrayMethod.addInstructions( + 0, + """ + new-array p1, p1, $videoQualityArrayFieldType + return-object p1 + """ + ) + + fun ClassDef.getCurrentVideoFormatConstructorMethodMatch() = firstMethodComposite { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returnType("V") + + var methodDefiningClass = "" + custom { + methodDefiningClass = definingClass + true + } + + instructions( + allOf( + Opcode.IPUT_OBJECT(), + field { type == videoQualityArrayFieldType && definingClass == methodDefiningClass } + ) + ) + } + + currentVideoFormatToStringMethod.immutableClassDef.getCurrentVideoFormatConstructorMethodMatch() + .let { + it.method.apply { + val index = it[-1] + val register = getInstruction(index).registerA + + addInstructions( + index, + """ + invoke-static/range { v$register .. v$register }, $EXTENSION_CLASS_DESCRIPTOR->hidePremiumVideoQuality([$EXTENSION_VIDEO_QUALITY_INTERFACE)[Ljava/lang/Object; + move-result-object v$register + check-cast v$register, $videoQualityArrayFieldType + """ + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt index 9b38ead454..15db089e5f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -72,7 +72,7 @@ val rememberVideoQualityPatch = bytecodePatch { // Inject a call to remember the user selected quality for regular videos. videoQualityChangedMethodMatch.let { match -> - val index = match[3] + val index = match[-1] val register = match.method.getInstruction(index).registerA match.method.addInstruction( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt index bc5639098b..eac5428f97 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt @@ -7,7 +7,7 @@ import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPref import app.revanced.patches.youtube.misc.settings.PreferenceScreen /** - * Video quality settings. Used to organize all speed related settings together. + * Video quality settings. Used to organize all speed related settings together. */ internal val settingsMenuVideoQualityGroup = mutableSetOf() @@ -19,15 +19,18 @@ val videoQualityPatch = bytecodePatch( dependsOn( rememberVideoQualityPatch, advancedVideoQualityMenuPatch, + hidePremiumVideoQualityPatch, videoQualityDialogButtonPatch, ) compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt index cecb755e0f..16b54823e2 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt @@ -10,7 +10,7 @@ import app.revanced.patches.youtube.video.speed.custom.customPlaybackSpeedPatch import app.revanced.patches.youtube.video.speed.remember.rememberPlaybackSpeedPatch /** - * Speed menu settings. Used to organize all speed related settings together. + * Speed menu settings. Used to organize all speed related settings together. */ internal val settingsMenuVideoSpeedGroup = mutableSetOf() @@ -18,7 +18,7 @@ internal val settingsMenuVideoSpeedGroup = mutableSetOf() val playbackSpeedPatch = bytecodePatch( name = "Playback speed", description = "Adds options to customize available playback speeds, set a default playback speed, " + - "and show a speed dialog button in the video player.", + "and show a speed dialog button in the video player.", ) { dependsOn( customPlaybackSpeedPatch, @@ -28,10 +28,12 @@ val playbackSpeedPatch = bytecodePatch( compatibleWith( "com.google.android.youtube"( - "19.43.41", "20.14.43", "20.21.37", - "20.31.40", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45" ) ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt index 5f3273718f..606c78c458 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt @@ -17,7 +17,6 @@ import app.revanced.patches.shared.misc.settings.preference.InputType import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.shared.misc.settings.preference.TextPreference import app.revanced.patches.shared.misc.litho.filter.addLithoFilter -import app.revanced.patches.youtube.interaction.seekbar.customTapAndHoldMethodMatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch import app.revanced.patches.youtube.misc.playservice.is_19_47_or_greater @@ -98,13 +97,15 @@ internal val customPlaybackSpeedPatch = bytecodePatch( // Apply changes from last index to first to preserve indexes. val originalArrayFetchIndex = it[5] - val originalArrayFetchDestination = getInstruction(it[5]).registerA + val originalArrayFetchDestination = + getInstruction(it[5]).registerA replaceInstruction( originalArrayFetchIndex, "sget-object v$originalArrayFetchDestination, $playbackSpeedsArrayType", ) - val arrayLengthConstDestination = getInstruction(it[3]).registerA + val arrayLengthConstDestination = + getInstruction(it[3]).registerA val newArrayIndex = it[4] addInstructions( newArrayIndex, @@ -115,7 +116,8 @@ internal val customPlaybackSpeedPatch = bytecodePatch( ) val sizeCallIndex = it[0] + 1 - val sizeCallResultRegister = getInstruction(sizeCallIndex).registerA + val sizeCallResultRegister = + getInstruction(sizeCallIndex).registerA replaceInstruction(sizeCallIndex, "const/4 v$sizeCallResultRegister, 0x0") } } @@ -170,17 +172,19 @@ internal val customPlaybackSpeedPatch = bytecodePatch( // region Custom tap and hold 2x speed. if (is_19_47_or_greater) { - customTapAndHoldMethodMatch.let { + // Function, because it can be the same method as getTapAndHoldHapticsMethodMatch. + getTapAndHoldSpeedMethodMatch().let { it.method.apply { - val index = it[0] - val register = getInstruction(index).registerA + val speedIndex = it[-1] + val speedRegister = + getInstruction(speedIndex).registerA addInstructions( - index + 1, + speedIndex + 1, + """ + invoke-static { }, ${EXTENSION_CLASS_DESCRIPTOR}->getTapAndHoldSpeed()F + move-result v$speedRegister """ - invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->tapAndHoldSpeed()F - move-result v$register - """, ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt index c2db863a55..e431b77c37 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt @@ -35,14 +35,14 @@ internal val BytecodePatchContext.serverSideMaxSpeedFeatureFlagMethod by getting internal val BytecodePatchContext.speedArrayGeneratorMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) returnType("[L") - parameterTypes("Lcom/google/android/libraries/youtube/innertube/model/player/PlayerResponseModel;") + parameterTypes("L") instructions( method { name == "size" && returnType == "I" }, allOf(Opcode.NEW_INSTANCE(), type("Ljava/text/DecimalFormat;")), "0.0#"(), 7L(), Opcode.NEW_ARRAY(), - field { definingClass.endsWith("/PlayerConfigModel;") && type == "[F" }, + field { type == "[F" }, ) } @@ -52,8 +52,9 @@ internal val BytecodePatchContext.speedArrayGeneratorMethodMatch by composingFir internal val BytecodePatchContext.speedLimiterMethod by gettingFirstMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") - parameterTypes("F", "Lcom/google/android/libraries/youtube/innertube/model/media/PlayerConfigModel;") + parameterTypes("F", "L") instructions( + "setPlaybackRate"(), 0.25f.toRawBits().toLong()(), 4.0f.toRawBits().toLong()(), ) @@ -77,3 +78,29 @@ internal val BytecodePatchContext.speedLimiterLegacyMethod by gettingFirstMethod Opcode.INVOKE_STATIC, ) } + + +internal fun BytecodePatchContext.getTapAndHoldSpeedMethodMatch() = firstMethodComposite { + name("run") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes() + instructions( + allOf( + Opcode.IGET_OBJECT(), + field { type == "Landroid/os/Handler;" } + ), + allOf( + Opcode.INVOKE_VIRTUAL(), + method { toString() == "Landroid/os/Handler;->removeCallbacks(Ljava/lang/Runnable;)V" } + ), + allOf( + Opcode.INVOKE_VIRTUAL(), + method { returnType == "Z" && parameterTypes.isEmpty() } + ), + Opcode.IF_EQZ(), + allOf(Opcode.IGET_BOOLEAN(), field { type == "Z" }), + Opcode.IF_NEZ(), + 2.0f.toRawBits().toLong()() + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt index 315da7fa0a..ca2035fcd6 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt @@ -74,7 +74,10 @@ internal val rememberPlaybackSpeedPatch = bytecodePatch { iget-object v1, p0, $onItemClickListenerClassFieldReference # Get the container class field. - iget-object v1, v1, $setPlaybackSpeedContainerClassFieldReference + iget-object v1, v1, $setPlaybackSpeedContainerClassFieldReference + + # Required cast for 20.49+ + check-cast v1, $setPlaybackSpeedContainerClassFieldReferenceClassType # Get the field from its class. iget-object v2, v1, $setPlaybackSpeedClassFieldReference diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt index 535f38abc6..526485fb54 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt @@ -15,11 +15,10 @@ internal fun ClassDef.getVideoIdMethodMatch() = firstMethodComposite { returnType("V") parameterTypes("L") instructions( - method { - definingClass == "Lcom/google/android/libraries/youtube/innertube/model/player/PlayerResponseModel;" && - returnType == "Ljava/lang/String;" - }, - Opcode.MOVE_RESULT_OBJECT(), + allOf(Opcode.INVOKE_INTERFACE(), method { returnType == "Ljava/lang/String;" }), + after(Opcode.MOVE_RESULT_OBJECT()), // videoId + afterAtMost(6, method { toString() == "Ljava/util/Map;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;" }), + after(Opcode.RETURN_VOID()) ) } @@ -28,10 +27,7 @@ internal val BytecodePatchContext.videoIdBackgroundPlayMethodMatch by composingF returnType("V") parameterTypes("L") instructions( - method { - definingClass == "Lcom/google/android/libraries/youtube/innertube/model/player/PlayerResponseModel;" && - returnType == "Ljava/lang/String;" - }, + method { returnType == "Ljava/lang/String;" }, Opcode.MOVE_RESULT_OBJECT(), Opcode.IPUT_OBJECT(), Opcode.MONITOR_EXIT(), @@ -41,7 +37,7 @@ internal val BytecodePatchContext.videoIdBackgroundPlayMethodMatch by composingF ) custom { immutableClassDef.methods.count() == 17 || // 20.39 and lower. - immutableClassDef.methods.count() == 16 // 20.40+ + immutableClassDef.methods.count() == 16 // 20.40+ } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/VideoIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/VideoIdPatch.kt index 76465dd9e9..ac016b3a6b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/VideoIdPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/VideoIdPatch.kt @@ -12,13 +12,13 @@ import app.revanced.patches.youtube.video.playerresponse.playerResponseMethodHoo import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction /** - * Hooks the new video id when the video changes. + * Hooks the new video ID when the video changes. * * Supports all videos (regular videos and Shorts). * * _Does not function if playing in the background with no video visible_. * - * Be aware, this can be called multiple times for the same video id. + * Be aware, this can be called multiple times for the same video ID. * * @param methodDescriptor which method to call. Params have to be `Ljava/lang/String;` */ @@ -35,7 +35,7 @@ fun hookVideoId( * * _Does not support Shorts_. * - * Be aware, the hook can be called multiple times for the same video id. + * Be aware, the hook can be called multiple times for the same video ID. * * @param methodDescriptor which method to call. Params have to be `Ljava/lang/String;` */ @@ -47,10 +47,10 @@ fun hookBackgroundPlayVideoId( ) /** - * Hooks the video id of every video when loaded. + * Hooks the video ID of every video when loaded. * Supports all videos and functions in all situations. * - * First parameter is the video id. + * First parameter is the video ID. * Second parameter is if the video is a Short AND it is being opened or is currently playing. * * Hook is always called off the main thread. @@ -58,14 +58,14 @@ fun hookBackgroundPlayVideoId( * This hook is called as soon as the player response is parsed, * and called before many other hooks are updated such as [playerTypeHookPatch]. * - * Note: The video id returned here may not be the current video that's being played. + * Note: The video ID returned here may not be the current video that's being played. * It's common for multiple Shorts to load at once in preparation * for the user swiping to the next Short. * * For most use cases, you probably want to use * [hookVideoId] or [hookBackgroundPlayVideoId] instead. * - * Be aware, this can be called multiple times for the same video id. + * Be aware, this can be called multiple times for the same video ID. * * @param methodDescriptor which method to call. Params must be `Ljava/lang/String;Z` */ @@ -84,7 +84,7 @@ private var backgroundPlaybackInsertIndex = 0 private lateinit var backgroundPlaybackMethod: MutableMethod val videoIdPatch = bytecodePatch( - description = "Hooks to detect when the video id changes.", + description = "Hooks to detect when the video ID changes.", ) { dependsOn( sharedExtensionPatch, @@ -95,9 +95,9 @@ val videoIdPatch = bytecodePatch( videoIdParentMethodMatch.immutableClassDef.getVideoIdMethodMatch().let { videoIdMethod = it.method - val index = it[0] - videoIdRegister = it.method.getInstruction(index + 1).registerA - videoIdInsertIndex = index + 2 + val index = it[1] + videoIdRegister = it.method.getInstruction(index).registerA + videoIdInsertIndex = index + 1 } videoIdBackgroundPlayMethodMatch.let { diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 8ffe3e029f..5694608dfe 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -4,15 +4,13 @@ import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableClassDef import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableField import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableField.Companion.toMutable import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod +import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable import app.revanced.patcher.* import app.revanced.patcher.extensions.* import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.PatchException import app.revanced.patches.shared.misc.mapping.ResourceType import app.revanced.patches.shared.misc.mapping.resourceMappingPatch -import app.revanced.util.InstructionUtils.Companion.branchOpcodes -import app.revanced.util.InstructionUtils.Companion.returnOpcodes -import app.revanced.util.InstructionUtils.Companion.writeOpcodes import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode.* @@ -20,6 +18,7 @@ import com.android.tools.smali.dexlib2.analysis.reflection.util.ReflectionUtils import com.android.tools.smali.dexlib2.formatter.DexFormatter import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.MethodParameter import com.android.tools.smali.dexlib2.iface.instruction.* import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference @@ -27,158 +26,17 @@ import com.android.tools.smali.dexlib2.iface.reference.Reference import com.android.tools.smali.dexlib2.iface.reference.StringReference import com.android.tools.smali.dexlib2.iface.value.* import com.android.tools.smali.dexlib2.immutable.ImmutableField +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation import com.android.tools.smali.dexlib2.immutable.value.* import java.util.* -import kotlin.collections.ArrayDeque - -/** - * Starting from and including the instruction at index [startIndex], - * finds the next register that is wrote to and not read from. If a return instruction - * is encountered, then the lowest unused register is returned. - * - * This method can return a non 4-bit register, and the calling code may need to temporarily - * swap register contents if a 4-bit register is required. - * - * @param startIndex Inclusive starting index. - * @param registersToExclude Registers to exclude, and consider as used. For most use cases, - * all registers used in injected code should be specified. - * @throws IllegalArgumentException If a branch or conditional statement is encountered - * before a suitable register is found. - */ -fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude: Int): Int { - if (implementation == null) { - throw IllegalArgumentException("Method has no implementation: $this") - } - if (startIndex < 0 || startIndex >= instructions.count()) { - throw IllegalArgumentException("startIndex out of bounds: $startIndex") - } - - // Highest 4-bit register available, exclusive. Ideally return a free register less than this. - val maxRegister4Bits = 16 - var bestFreeRegisterFound: Int? = null - val usedRegisters = registersToExclude.toMutableSet() - - for (i in startIndex until instructions.count()) { - val instruction = getInstruction(i) - val instructionRegisters = instruction.registersUsed - - val writeRegister = instruction.writeRegister - if (writeRegister != null) { - if (writeRegister !in usedRegisters) { - // Verify the register is only used for write and not also as a parameter. - // If the instruction uses the write register once then it's not also a read register. - if (instructionRegisters.count { register -> register == writeRegister } == 1) { - if (writeRegister < maxRegister4Bits) { - // Found an ideal register. - return writeRegister - } - - // Continue searching for a 4-bit register if available. - if (bestFreeRegisterFound == null || writeRegister < bestFreeRegisterFound) { - bestFreeRegisterFound = writeRegister - } - } - } - } - - usedRegisters.addAll(instructionRegisters) - - if (instruction.isBranchInstruction) { - if (bestFreeRegisterFound != null) { - return bestFreeRegisterFound - } - // This method is simple and does not follow branching. - throw IllegalArgumentException( - "Encountered a branch statement before " + - "a free register could be found from startIndex: $startIndex", - ) - } - - if (instruction.isReturnInstruction) { - // Use lowest register that hasn't been encountered. - val freeRegister = (0 until implementation!!.registerCount).find { - it !in usedRegisters - } - if (freeRegister != null) { - return freeRegister - } - if (bestFreeRegisterFound != null) { - return bestFreeRegisterFound - } - - // Somehow every method register was read from before any register was wrote to. - // In practice this never occurs. - throw IllegalArgumentException( - "Could not find a free register from startIndex: " + - "$startIndex excluding: $registersToExclude", - ) - } - } - - // Some methods can have array payloads at the end of the method after a return statement. - // But in normal usage this cannot be reached since a branch or return statement - // will be encountered before the end of the method. - throw IllegalArgumentException("Start index is outside the range of normal control flow: $startIndex") -} - -/** - * @return The registers used by this instruction. - */ -internal val Instruction.registersUsed: List - get() = when (this) { - is FiveRegisterInstruction -> { - when (registerCount) { - 0 -> listOf() - 1 -> listOf(registerC) - 2 -> listOf(registerC, registerD) - 3 -> listOf(registerC, registerD, registerE) - 4 -> listOf(registerC, registerD, registerE, registerF) - else -> listOf(registerC, registerD, registerE, registerF, registerG) - } - } - - is ThreeRegisterInstruction -> listOf(registerA, registerB, registerC) - - is TwoRegisterInstruction -> listOf(registerA, registerB) - - is OneRegisterInstruction -> listOf(registerA) - - is RegisterRangeInstruction -> (startRegister until (startRegister + registerCount)).toList() - - else -> emptyList() - } - -/** - * @return The register that is written to by this instruction, - * or NULL if this is not a write opcode. - */ -internal val Instruction.writeRegister: Int? - get() { - if (this.opcode !in writeOpcodes) { - return null - } - if (this !is OneRegisterInstruction) { - throw IllegalStateException("Not a write instruction: $this") - } - return registerA - } - -/** - * @return If this instruction is an unconditional or conditional branch opcode. - */ -internal val Instruction.isBranchInstruction: Boolean - get() = this.opcode in branchOpcodes - -/** - * @return If this instruction returns or throws. - */ -internal val Instruction.isReturnInstruction: Boolean - get() = this.opcode in returnOpcodes +import kotlin.apply +import kotlin.collections.remove /** * Find the instruction index used for a toString() StringBuilder write of a given String name. * - * @param fieldName The name of the field to find. Partial matches are allowed. + * @param fieldName The name of the field to find. Partial matches are allowed. */ private fun Method.findInstructionIndexFromToString(fieldName: String): Int { val stringIndex = indexOfFirstInstruction { @@ -231,7 +89,7 @@ private fun Method.findInstructionIndexFromToString(fieldName: String): Int { /** * Find the method used for a toString() StringBuilder write of a given String name. * - * @param fieldName The name of the field to find. Partial matches are allowed. + * @param fieldName The name of the field to find. Partial matches are allowed. */ context(context: BytecodePatchContext) internal fun Method.findMethodFromToString(fieldName: String): MutableMethod { @@ -242,7 +100,7 @@ internal fun Method.findMethodFromToString(fieldName: String): MutableMethod { /** * Find the field used for a toString() StringBuilder write of a given String name. * - * @param fieldName The name of the field to find. Partial matches are allowed. + * @param fieldName The name of the field to find. Partial matches are allowed. */ internal fun Method.findFieldFromToString(fieldName: String): FieldReference { val methodUsageIndex = findInstructionIndexFromToString(fieldName) @@ -285,25 +143,6 @@ fun MutableMethod.injectHideViewCall( "invoke-static { v$viewRegister }, $classDescriptor->$targetMethod(Landroid/view/View;)V", ) -/** - * Inserts instructions at a given index, using the existing control flow label at that index. - * Inserted instructions can have it's own control flow labels as well. - * - * Effectively this changes the code from: - * :label - * (original code) - * - * Into: - * :label - * (patch code) - * (original code) - */ -// TODO: delete this on next major version bump. -fun MutableMethod.addInstructionsAtControlFlowLabel( - insertIndex: Int, - instructions: String, -) = addInstructionsAtControlFlowLabel(insertIndex, instructions, *arrayOf()) - /** * Inserts instructions at a given index, using the existing control flow label at that index. * Inserted instructions can have it's own control flow labels as well. @@ -819,8 +658,197 @@ private fun MutableMethod.checkReturnType(expectedTypes: Iterable>) { check(expectedTypes.any { returnTypeJava == it.name }) { "Actual return type $returnTypeJava is not contained in expected types: $expectedTypes" } + } +/** + * Additional registers effectively take the place of the pX parameters (p0, p1, p2, etc.) + * and contain the original contents of the method parameters. + * Added registers always start at index: `originalMethod.implementation!!.registerCount` of the + * original uncloned method. + * + * **Fingerprint match indexes will be increased positively by [numberOfParameterRegistersLogical]**. + */ +context(_: BytecodePatchContext) +fun Method.cloneMutableAndPreserveParameters() = + cloneMutableAndPreserveParameters(classDef) + +/** + * Additional registers effectively take the place of the pX parameters (p0, p1, p2, etc.) + * and contain the original contents of the method parameters. + * Added registers always start at index: `originalMethod.implementation!!.registerCount` of the + * original uncloned method. + * + * **Fingerprint match indexes will be increased positively by [numberOfParameterRegistersLogical]**. + */ +fun Method.cloneMutableAndPreserveParameters(mutableClassDef : MutableClassDef) : MutableMethod { + check (!AccessFlags.STATIC.isSet(accessFlags) || parameters.isNotEmpty()) { + "Static methods have no parameter registers to preserve" + } + + val clonedMethod = cloneMutable( + additionalRegisters = numberOfParameterRegisters + ) + + // Replace existing method with cloned with more registers. + mutableClassDef.methods.apply { + remove(this@cloneMutableAndPreserveParameters) + add(clonedMethod) + } + + return clonedMethod +} + +// Adapted from BiliRoamingX: +// https://github.com/BiliRoamingX/BiliRoamingX/blob/ae58109f3acdd53ec2d2b3fb439c2a2ef1886221/patches/src/main/kotlin/app/revanced/patches/bilibili/utils/Extenstions.kt#L51 +/** + * Additional registers effectively take the place of the pX parameters (p0, p1, p2, etc.) + * and contain the original contents of the method parameters. + * Added registers always start at index: `originalMethod.implementation!!.registerCount` of the + * original uncloned method. + * + * **Fingerprint match indexes will be increased positively by [additionalRegisters]**. + */ +fun Method.cloneMutable( + name: String = this.name, + accessFlags: Int = this.accessFlags, + parameters: List = this.parameters, + returnType: String = this.returnType, + additionalRegisters: Int = 0, +): MutableMethod { + check(additionalRegisters >= 0) { + "Additional registers cannot be negative" + } + + val implementationExists = implementation != null + val oldFirstParameterRegister = if (implementationExists) p0Register else 0 + + val clonedImplementation = implementation?.let { + ImmutableMethodImplementation( + it.registerCount + additionalRegisters, + it.instructions, + it.tryBlocks, + it.debugItems, + ) + } + + return ImmutableMethod( + definingClass, + name, + parameters, + returnType, + accessFlags, + annotations, + hiddenApiRestrictions, + clonedImplementation + ).toMutable().apply { + var insertIndex = 0 + var addedInstructions = 0 + val isNotStatic = !AccessFlags.STATIC.isSet(accessFlags) + + if (implementationExists && additionalRegisters > 0 && (parameters.isNotEmpty() || isNotStatic)) { + var destReg = oldFirstParameterRegister + var pReg = 0 + + // Handle `this`. + if (isNotStatic) { + addInstructions(insertIndex++, "move-object/from16 v$destReg, p$pReg") + addedInstructions++ + destReg += 1 + pReg += 1 + } + + // Handle method parameters. + for (parameter in parameters) { + val opcode = when (parameter.type) { + "J", "D" -> "move-wide/from16" + else -> { + if (parameter.type.startsWith('L') || parameter.type.startsWith('[')) { + "move-object/from16" + } else { + "move/from16" + } + } + } + + addInstructions(insertIndex++, "$opcode v$destReg, p$pReg") + addedInstructions++ + + val width = if (opcode.startsWith("move-wide")) 2 else 1 + destReg += width + pReg += width + } + + if (addedInstructions != numberOfParameterRegistersLogical) { + throw IllegalStateException( + "Added instructions do not match additional registers " + + "addedInstructions: $addedInstructions " + + "numberOfParameterRegistersLogical: $numberOfParameterRegistersLogical" + ) + } + } + } +} + +/** + * @return The number of registers for all parameters, including p0. + * This includes 2 registers for each wide parameter. + */ +val Method.numberOfParameterRegisters: Int + get() { + var count = 0 + + if (!AccessFlags.STATIC.isSet(accessFlags)) { + count += 1 + } + + for (param in parameters) { + count += when (param.type) { + "J", "D" -> 2 // wide + else -> 1 // normal + } + } + + return count + } + +/** + * @return The number of parameter registers, including p0 as 'this' if method is not static. + * This differs from [numberOfParameterRegisters] in that long/double parameters are counted only once each. + */ +val Method.numberOfParameterRegistersLogical: Int + get() = parameters.count() + if (AccessFlags.STATIC.isSet(accessFlags)) { + 0 + } else { + 1 + } + +/** + * @return the actual register number of p0 for this method. + * Throws if the method has no implementation. + */ +val Method.p0Register: Int + get() { + val impl = implementation ?: throw IllegalStateException("Method has no implementation: $this") + var paramRegs = 0 + + // Count explicit parameters (wide types take 2 registers). + for (type in this.parameterTypes) { + paramRegs += if (type == "J" || type == "D") 2 else 1 + } + + // Add implicit 'this' for non-static methods. + if (!AccessFlags.STATIC.isSet(this.accessFlags)) { + paramRegs += 1 + } + + val totalRegs = impl.registerCount + + return totalRegs - paramRegs + } + +private const val RETURN_TYPE_MISMATCH = "Mismatch between override type and Method return type" + /** * Overrides the first instruction of a method with returning the default value for the type (or `void`). * None of the method code will ever execute. @@ -1253,61 +1281,3 @@ fun MutablePredicateList.literal(literalSupplier: () -> Long) { custom { containsLiteralInstruction(literalSupplier()) } } -private class InstructionUtils { - companion object { - val branchOpcodes: EnumSet = EnumSet.of( - GOTO, GOTO_16, GOTO_32, - IF_EQ, IF_NE, IF_LT, IF_GE, IF_GT, IF_LE, - IF_EQZ, IF_NEZ, IF_LTZ, IF_GEZ, IF_GTZ, IF_LEZ, - PACKED_SWITCH_PAYLOAD, SPARSE_SWITCH_PAYLOAD, - ) - - val returnOpcodes: EnumSet = EnumSet.of( - RETURN_VOID, - RETURN, - RETURN_WIDE, - RETURN_OBJECT, - RETURN_VOID_NO_BARRIER, - THROW, - ) - - val writeOpcodes: EnumSet = EnumSet.of( - ARRAY_LENGTH, - INSTANCE_OF, - NEW_INSTANCE, NEW_ARRAY, - MOVE, MOVE_FROM16, MOVE_16, MOVE_WIDE, MOVE_WIDE_FROM16, MOVE_WIDE_16, MOVE_OBJECT, - MOVE_OBJECT_FROM16, MOVE_OBJECT_16, MOVE_RESULT, MOVE_RESULT_WIDE, MOVE_RESULT_OBJECT, MOVE_EXCEPTION, - CONST, CONST_4, CONST_16, CONST_HIGH16, CONST_WIDE_16, CONST_WIDE_32, - CONST_WIDE, CONST_WIDE_HIGH16, CONST_STRING, CONST_STRING_JUMBO, - IGET, IGET_WIDE, IGET_OBJECT, IGET_BOOLEAN, IGET_BYTE, IGET_CHAR, IGET_SHORT, - IGET_VOLATILE, IGET_WIDE_VOLATILE, IGET_OBJECT_VOLATILE, - SGET, SGET_WIDE, SGET_OBJECT, SGET_BOOLEAN, SGET_BYTE, SGET_CHAR, SGET_SHORT, - SGET_VOLATILE, SGET_WIDE_VOLATILE, SGET_OBJECT_VOLATILE, - AGET, AGET_WIDE, AGET_OBJECT, AGET_BOOLEAN, AGET_BYTE, AGET_CHAR, AGET_SHORT, - // Arithmetic and logical operations. - ADD_DOUBLE_2ADDR, ADD_DOUBLE, ADD_FLOAT_2ADDR, ADD_FLOAT, ADD_INT_2ADDR, - ADD_INT_LIT8, ADD_INT, ADD_LONG_2ADDR, ADD_LONG, ADD_INT_LIT16, - AND_INT_2ADDR, AND_INT_LIT8, AND_INT_LIT16, AND_INT, AND_LONG_2ADDR, AND_LONG, - DIV_DOUBLE_2ADDR, DIV_DOUBLE, DIV_FLOAT_2ADDR, DIV_FLOAT, DIV_INT_2ADDR, - DIV_INT_LIT16, DIV_INT_LIT8, DIV_INT, DIV_LONG_2ADDR, DIV_LONG, - DOUBLE_TO_FLOAT, DOUBLE_TO_INT, DOUBLE_TO_LONG, - FLOAT_TO_DOUBLE, FLOAT_TO_INT, FLOAT_TO_LONG, - INT_TO_BYTE, INT_TO_CHAR, INT_TO_DOUBLE, INT_TO_FLOAT, INT_TO_LONG, INT_TO_SHORT, - LONG_TO_DOUBLE, LONG_TO_FLOAT, LONG_TO_INT, - MUL_DOUBLE_2ADDR, MUL_DOUBLE, MUL_FLOAT_2ADDR, MUL_FLOAT, MUL_INT_2ADDR, - MUL_INT_LIT16, MUL_INT_LIT8, MUL_INT, MUL_LONG_2ADDR, MUL_LONG, - NEG_DOUBLE, NEG_FLOAT, NEG_INT, NEG_LONG, - NOT_INT, NOT_LONG, - OR_INT_2ADDR, OR_INT_LIT16, OR_INT_LIT8, OR_INT, OR_LONG_2ADDR, OR_LONG, - REM_DOUBLE_2ADDR, REM_DOUBLE, REM_FLOAT_2ADDR, REM_FLOAT, REM_INT_2ADDR, - REM_INT_LIT16, REM_INT_LIT8, REM_INT, REM_LONG_2ADDR, REM_LONG, - RSUB_INT_LIT8, RSUB_INT, - SHL_INT_2ADDR, SHL_INT_LIT8, SHL_INT, SHL_LONG_2ADDR, SHL_LONG, - SHR_INT_2ADDR, SHR_INT_LIT8, SHR_INT, SHR_LONG_2ADDR, SHR_LONG, - SUB_DOUBLE_2ADDR, SUB_DOUBLE, SUB_FLOAT_2ADDR, SUB_FLOAT, SUB_INT_2ADDR, - SUB_INT, SUB_LONG_2ADDR, SUB_LONG, - USHR_INT_2ADDR, USHR_INT_LIT8, USHR_INT, USHR_LONG_2ADDR, USHR_LONG, - XOR_INT_2ADDR, XOR_INT_LIT16, XOR_INT_LIT8, XOR_INT, XOR_LONG_2ADDR, XOR_LONG, - ) - } -} diff --git a/patches/src/main/kotlin/app/revanced/util/FreeRegisterProvider.kt b/patches/src/main/kotlin/app/revanced/util/FreeRegisterProvider.kt new file mode 100644 index 0000000000..c309e05368 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/util/FreeRegisterProvider.kt @@ -0,0 +1,575 @@ +package app.revanced.util + +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.instructions +import app.revanced.util.FreeRegisterProvider.Companion.conditionalBranchOpcodes +import app.revanced.util.FreeRegisterProvider.Companion.logFreeRegisterSearch +import app.revanced.util.FreeRegisterProvider.Companion.returnOpcodes +import app.revanced.util.FreeRegisterProvider.Companion.switchOpcodes +import app.revanced.util.FreeRegisterProvider.Companion.unconditionalBranchOpcodes +import app.revanced.util.FreeRegisterProvider.Companion.writeOpcodes +import com.android.tools.smali.dexlib2.Format +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.Opcode.* +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.Instruction +import com.android.tools.smali.dexlib2.iface.instruction.OffsetInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ThreeRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import java.util.EnumSet +import java.util.LinkedList + +/** + * Finds free registers at a specific index in a method. + * Allows allocating multiple free registers for a given index. + * + * If you only need a single free register, instead use [findFreeRegister]. + * + * @param index Index you need a use a free register at. + * @param numberOfFreeRegistersNeeded The maximum number of free registers you may get using + * [FreeRegisterProvider.getFreeRegister]. + * @param registersToExclude Registers to exclude, and consider as used. For most use cases, + * all registers used in injected code should be specified. + * + * @throws IllegalArgumentException If no free registers can be found at the given index. + * This includes unusual method indexes that read from every register + * before any registers are wrote to, or if a switch statement is + * encountered before any free registers are found. + */ +fun Method.getFreeRegisterProvider(index: Int, numberOfFreeRegistersNeeded: Int, registersToExclude: List) = + FreeRegisterProvider(this, index, numberOfFreeRegistersNeeded, registersToExclude) + +/** + * Finds free registers at a specific index in a method. + * Allows allocating multiple free registers for a given index. + * + * If you only need a single free register, instead use [findFreeRegister]. + * + * @param index Index you need a use a free register at. + * @param numberOfFreeRegistersNeeded The minimum free registers to find. + * @param registersToExclude Registers to exclude, and consider as used. For most use cases, + * all registers used in injected code should be specified. + * + * @throws IllegalArgumentException If no free registers can be found at the given index. + * This includes unusual method indexes that read from every register + * before any registers are wrote to, or if a switch statement is + * encountered before any free registers are found. + */ +fun Method.getFreeRegisterProvider(index: Int, numberOfFreeRegistersNeeded: Int, vararg registersToExclude: Int) = + FreeRegisterProvider(this, index, numberOfFreeRegistersNeeded, *registersToExclude) + +class FreeRegisterProvider internal constructor( + val method: Method, + index: Int, + numberOfFreeRegistersNeeded: Int, + registersToExclude: List +) { + internal constructor( + method: Method, + index: Int, + numberOfFreeRegistersNeeded: Int, + vararg registersToExclude: Int + ) : this(method, index, numberOfFreeRegistersNeeded, registersToExclude.toList()) + + private var freeRegisters: MutableList = LinkedList( + method.findFreeRegisters(index, numberOfFreeRegistersNeeded, registersToExclude) + ) + + private val originallyExcludedRegisters = registersToExclude + private val allocatedFreeRegisters = mutableListOf() + + /** + * Returns a free register and removes it from the available list. + * + * @return A free register number + * @throws IllegalStateException if no free registers are available + */ + fun getFreeRegister(): Int { + if (freeRegisters.isEmpty()) { + throw IllegalStateException("No free registers available") + } + val register = freeRegisters.removeFirst() + allocatedFreeRegisters.add(register) + return register + } + + /** + * Returns all registers that have been allocated via [getFreeRegister]. + * This does not include the originally excluded registers. + * + * @return List of registers that have been allocated, in allocation order + */ + fun getAllocatedFreeRegisters(): List = allocatedFreeRegisters.toList() + + /** + * Returns all registers that are considered "in use" or unsafe to use. This includes the + * excluded registers originally passed in, all registers that are unsuitable to use + * (registers are read from by the original code), and all free registers previously provided + * by this class using [getFreeRegister]. + * + * @return List of all registers that are unsafe to use at this time. + */ + fun getUsedAndUnAvailableRegisters(): List { + val allRegisters = 0 until method.implementation!!.registerCount + return (allocatedFreeRegisters + originallyExcludedRegisters + (allRegisters - freeRegisters.toSet())) + .distinct() + } + + /** + * @return The number of free registers still available. + */ + fun availableCount(): Int = freeRegisters.size + + /** + * Checks if there are any free registers available. + */ + fun hasFreeRegisters(): Boolean = freeRegisters.isNotEmpty() + + internal companion object { + val conditionalBranchOpcodes: EnumSet = EnumSet.of( + IF_EQ, IF_NE, IF_LT, IF_GE, IF_GT, IF_LE, + IF_EQZ, IF_NEZ, IF_LTZ, IF_GEZ, IF_GTZ, IF_LEZ + ) + + val unconditionalBranchOpcodes: EnumSet = EnumSet.of( + GOTO, GOTO_16, GOTO_32 + ) + + val switchOpcodes: EnumSet = EnumSet.of( + PACKED_SWITCH, SPARSE_SWITCH + ) + + val returnOpcodes: EnumSet = EnumSet.of( + RETURN_VOID, RETURN, RETURN_WIDE, RETURN_OBJECT, RETURN_VOID_NO_BARRIER, + THROW + ) + + val writeOpcodes: EnumSet = EnumSet.of( + ARRAY_LENGTH, + INSTANCE_OF, + NEW_INSTANCE, NEW_ARRAY, + MOVE, MOVE_FROM16, MOVE_16, MOVE_WIDE, MOVE_WIDE_FROM16, MOVE_WIDE_16, MOVE_OBJECT, + MOVE_OBJECT_FROM16, MOVE_OBJECT_16, MOVE_RESULT, MOVE_RESULT_WIDE, MOVE_RESULT_OBJECT, MOVE_EXCEPTION, + CONST, CONST_4, CONST_16, CONST_HIGH16, CONST_WIDE_16, CONST_WIDE_32, + CONST_WIDE, CONST_WIDE_HIGH16, CONST_STRING, CONST_STRING_JUMBO, + IGET, IGET_WIDE, IGET_OBJECT, IGET_BOOLEAN, IGET_BYTE, IGET_CHAR, IGET_SHORT, + IGET_VOLATILE, IGET_WIDE_VOLATILE, IGET_OBJECT_VOLATILE, + SGET, SGET_WIDE, SGET_OBJECT, SGET_BOOLEAN, SGET_BYTE, SGET_CHAR, SGET_SHORT, + SGET_VOLATILE, SGET_WIDE_VOLATILE, SGET_OBJECT_VOLATILE, + AGET, AGET_WIDE, AGET_OBJECT, AGET_BOOLEAN, AGET_BYTE, AGET_CHAR, AGET_SHORT, + // Arithmetic and logical operations. + ADD_DOUBLE_2ADDR, ADD_DOUBLE, ADD_FLOAT_2ADDR, ADD_FLOAT, ADD_INT_2ADDR, + ADD_INT_LIT8, ADD_INT, ADD_LONG_2ADDR, ADD_LONG, ADD_INT_LIT16, + AND_INT_2ADDR, AND_INT_LIT8, AND_INT_LIT16, AND_INT, AND_LONG_2ADDR, AND_LONG, + DIV_DOUBLE_2ADDR, DIV_DOUBLE, DIV_FLOAT_2ADDR, DIV_FLOAT, DIV_INT_2ADDR, + DIV_INT_LIT16, DIV_INT_LIT8, DIV_INT, DIV_LONG_2ADDR, DIV_LONG, + DOUBLE_TO_FLOAT, DOUBLE_TO_INT, DOUBLE_TO_LONG, + FLOAT_TO_DOUBLE, FLOAT_TO_INT, FLOAT_TO_LONG, + INT_TO_BYTE, INT_TO_CHAR, INT_TO_DOUBLE, INT_TO_FLOAT, INT_TO_LONG, INT_TO_SHORT, + LONG_TO_DOUBLE, LONG_TO_FLOAT, LONG_TO_INT, + MUL_DOUBLE_2ADDR, MUL_DOUBLE, MUL_FLOAT_2ADDR, MUL_FLOAT, MUL_INT_2ADDR, + MUL_INT_LIT16, MUL_INT_LIT8, MUL_INT, MUL_LONG_2ADDR, MUL_LONG, + NEG_DOUBLE, NEG_FLOAT, NEG_INT, NEG_LONG, + NOT_INT, NOT_LONG, + OR_INT_2ADDR, OR_INT_LIT16, OR_INT_LIT8, OR_INT, OR_LONG_2ADDR, OR_LONG, + REM_DOUBLE_2ADDR, REM_DOUBLE, REM_FLOAT_2ADDR, REM_FLOAT, REM_INT_2ADDR, + REM_INT_LIT16, REM_INT_LIT8, REM_INT, REM_LONG_2ADDR, REM_LONG, + RSUB_INT_LIT8, RSUB_INT, + SHL_INT_2ADDR, SHL_INT_LIT8, SHL_INT, SHL_LONG_2ADDR, SHL_LONG, + SHR_INT_2ADDR, SHR_INT_LIT8, SHR_INT, SHR_LONG_2ADDR, SHR_LONG, + SUB_DOUBLE_2ADDR, SUB_DOUBLE, SUB_FLOAT_2ADDR, SUB_FLOAT, SUB_INT_2ADDR, + SUB_INT, SUB_LONG_2ADDR, SUB_LONG, + USHR_INT_2ADDR, USHR_INT_LIT8, USHR_INT, USHR_LONG_2ADDR, USHR_LONG, + XOR_INT_2ADDR, XOR_INT_LIT16, XOR_INT_LIT8, XOR_INT, XOR_LONG_2ADDR, XOR_LONG, + ) + + /** + * For debugging and development. + */ + internal const val logFreeRegisterSearch = false + } +} + +/** + * Starting from and including the instruction at index [index], + * finds the next register that is written to and not read from. If a return instruction + * is encountered, then the lowest unused register is returned. + * + * This method should work for all situations including inserting at a branch statement, + * but this may not work if the index is at or just before a switch statement or if the branch + * paths have no common free registers. + * + * If you need multiple free registers, then instead use [Method.getFreeRegisterProvider]. + * + * @param index Index you need a use a free register at. + * @param registersToExclude Registers to exclude, and consider as used. For most use cases, + * all registers used in injected code should be specified. + * @return The lowest register number (usually a 4-bit register) that is free at the given index. + * @throws IllegalArgumentException If no free registers can be found at the given index. + * This includes unusual method indexes that read from every register + * before any registers are wrote to, or if a switch statement is + * encountered before any free registers are found, or if the index is + * at/before a branch statement and the method has an unusually high + * amount of branching where no common free registers exist in both branch paths. + */ +fun Method.findFreeRegister( + index: Int, + vararg registersToExclude: Int +) = findFreeRegisters( + startIndex = index, + numberOfFreeRegistersNeeded = 1, + registersToExclude = registersToExclude.toList() +).first() + +/** + * Starting from and including the instruction at index [index], + * finds the next register that is written to and not read from. If a return instruction + * is encountered, then the lowest unused register is returned. + * + * This method should work for all situations including inserting at a branch statement, + * but this may not work if the index is at or just before a switch statement or if the branch + * paths have no common free registers. + * + * If you need multiple free registers, then instead use [Method.getFreeRegisterProvider]. + * + * @param index Index you need a use a free register at. + * @param registersToExclude Registers to exclude, and consider as used. For most use cases, + * all registers used in injected code should be specified. + * @return The lowest register number (usually a 4-bit register) that is free at the given index. + * @throws IllegalArgumentException If no free registers can be found at the given index. + * This includes unusual method indexes that read from every register + * before any registers are wrote to, or if a switch statement is + * encountered before any free registers are found, or if the index is + * at/before a branch statement and the method has an unusually high + * amount of branching where no common free registers exist in both branch paths. + */ +fun Method.findFreeRegister( + index: Int, + registersToExclude: List +) = findFreeRegisters( + startIndex = index, + numberOfFreeRegistersNeeded = 1, + registersToExclude = registersToExclude +).first() + +private fun Method.findFreeRegisters( + startIndex: Int, + numberOfFreeRegistersNeeded: Int, + registersToExclude: List +): List { + if (logFreeRegisterSearch) println("Searching startIndex: $startIndex method: $this") + + val freeRegisters = findFreeRegistersInternal( + startIndex = startIndex, + numberOfFreeRegistersNeeded = numberOfFreeRegistersNeeded, + currentDepth = 0, + foundFreeRegistersAtIndex = mutableMapOf(), + registersToExclude = registersToExclude, + offsetArray = buildInstructionOffsetArray() + ) + + if (freeRegisters.isEmpty()) { + // Should only happen if a switch statement is encountered before enough free registers are found. + throw IllegalArgumentException("Could not find a free register from startIndex: " + + "$startIndex excluding: $registersToExclude") + } + + if (logFreeRegisterSearch) println("Final free registers found: $freeRegisters") + + // Use 4-bit registers first, but keep sorting stable among 4-bit vs not 4-bit. + return freeRegisters.sortedWith { first, second -> + val firstIsFourBit = first < 16 + val secondIsFourBit = second < 16 + if (firstIsFourBit == secondIsFourBit) { + return@sortedWith 0 + } + + if (firstIsFourBit) -1 else 1 + } +} + +/** + * Returns all free registers found starting from [startIndex].Follows branches up to [maxDepth]. + * + * @param startIndex Inclusive starting index. + * @param numberOfFreeRegistersNeeded The minimum free registers to ensure will be returned. + * @param currentDepth Current branching depth. Value of zero means no branching has been followed yet. + * @param foundFreeRegistersAtIndex Map of instruction index to list of free registers previously found. + * @param registersToExclude Registers to exclude from consideration. + * @param offsetArray Map from instruction index to code offset. + * @return List of all free registers found. + */ +private fun Method.findFreeRegistersInternal( + startIndex: Int, + numberOfFreeRegistersNeeded: Int, + currentDepth: Int, + foundFreeRegistersAtIndex: MutableMap?>, + registersToExclude: List, + offsetArray: IntArray +): List { + check(implementation != null) { + "Method has no implementation: $this" + } + check(startIndex > 0 && startIndex < instructions.count()) { + "startIndex out of bounds: $startIndex methodInstructionCount: ${instructions.count()}" + } + check(numberOfFreeRegistersNeeded > 0) { + "numberOfFreeRegistersNeeded must be greater than zero: $numberOfFreeRegistersNeeded" + } + + fun Collection.numberOf4BitRegisters() = this.count { it < 16 } + + foundFreeRegistersAtIndex[startIndex]?.let { + // Recursive call to a branch index that has already been explored. + return (it - registersToExclude.toSet()).toList() + } + + val usedRegisters = registersToExclude.toMutableSet() + val freeRegisters = mutableSetOf() + foundFreeRegistersAtIndex[startIndex] = freeRegisters + + for (i in startIndex until instructions.count()) { + val instruction = getInstruction(i) + val instructionRegisters = instruction.registersUsed + + // Check for write-only register. + val writeRegister = instruction.writeRegister + if (writeRegister != null && writeRegister !in usedRegisters) { + // Check if this register is ONLY written to (not also read) + // Count occurrences of writeRegister in instructionRegisters. + val occurrences = instructionRegisters.count { it == writeRegister } + // If it appears only once, it's write-only (to write). + // If it appears more than once, it's also read. + if (occurrences <= 1) { + if (logFreeRegisterSearch) println("Found free register at $i: $writeRegister " + + "opcode: " + instruction.opcode + " reference: " + (instruction.getReference())) + freeRegisters.add(writeRegister) + // If the requested number of free registers is found and this is not a branch, + // then no additional searching is needed. + // But if this is a branch, then this all free registers should be found + // because the intersection of free registers from different branches may be + // less than the requested number of registers. + if (currentDepth == 0 && freeRegisters.numberOf4BitRegisters() >= numberOfFreeRegistersNeeded) { + return freeRegisters.toList() + } + } + } + + // Mark all registers used by this instruction as "used". + usedRegisters.addAll(instructionRegisters) + + // If we hit a return, all unused registers on this path are free. + if (instruction.isReturnInstruction) { + val allRegisters = (0 until implementation!!.registerCount).toList() + val unusedRegisters = allRegisters - usedRegisters + freeRegisters.addAll(unusedRegisters) + if (logFreeRegisterSearch) println("Encountered return index: $i and found: $freeRegisters") + return freeRegisters.toList() + } + + if (instruction.isSwitchInstruction) { + // For now, do not handle the complexity of a switch statement and handle as a leaf node. + if (logFreeRegisterSearch) println("Encountered switch index: $i opcode: " + instruction.opcode) + return freeRegisters.toList() + } + + if (instruction.isUnconditionalBranchInstruction) { + if (logFreeRegisterSearch) println("encountered unconditional branch index: $i opcode: " + instruction.opcode) + + // Continue searching from the go-to index. + return (freeRegisters + findFreeRegistersInternal( + startIndex = getBranchTargetInstructionIndex(instruction, i, offsetArray), + numberOfFreeRegistersNeeded = numberOfFreeRegistersNeeded, + currentDepth = currentDepth, // Same depth since it's a continuation of single path. + foundFreeRegistersAtIndex = foundFreeRegistersAtIndex, + registersToExclude = usedRegisters.toList(), + offsetArray = offsetArray + )).toList() + } + + if (instruction.isConditionalBranchInstruction) { + if (logFreeRegisterSearch) println("encountered conditional branch index: $i opcode: " + instruction.opcode) + val usedRegistersList = usedRegisters.toList() + + val branchFreeRegisters = findFreeRegistersInternal( + startIndex = getBranchTargetInstructionIndex(instruction, i, offsetArray), + numberOfFreeRegistersNeeded = numberOfFreeRegistersNeeded, + currentDepth = currentDepth + 1, + foundFreeRegistersAtIndex = foundFreeRegistersAtIndex, + registersToExclude = usedRegistersList, + offsetArray = offsetArray + ) + if (logFreeRegisterSearch) println("branch registers: $branchFreeRegisters") + + val fallThruFreeRegisters = findFreeRegistersInternal( + startIndex = i + 1, + numberOfFreeRegistersNeeded = numberOfFreeRegistersNeeded, + currentDepth = currentDepth + 1, + foundFreeRegistersAtIndex = foundFreeRegistersAtIndex, + registersToExclude = usedRegistersList, + offsetArray = offsetArray + ) + if (logFreeRegisterSearch) println("fall thru registers: $fallThruFreeRegisters") + + return (freeRegisters + branchFreeRegisters.intersect(fallThruFreeRegisters.toSet())).toList() + } + } + + // A return or branch instruction will be encountered before all instructions can be iterated. + // Some methods have switch payload instructions after the last actual instruction, + // but these cannot be reached through normal control flow. + throw IllegalArgumentException("Start index is outside normal control flow: $startIndex") +} + +private fun Method.buildInstructionOffsetArray(): IntArray { + val instructionCount = instructions.count() + val offsetArray = IntArray(instructionCount) { -1 } + var currentOffset = 0 + + for (i in 0 until instructionCount) { + val instruction = getInstruction(i) + val format = instruction.opcode.format + + if (!format.isPayloadFormat) { + offsetArray[i] = currentOffset + + // Get size in bytes from format. + val sizeInBytes = format.size + val sizeInCodeUnits = when { + sizeInBytes > 0 -> sizeInBytes / 2 // Normal instruction + format == Format.UnresolvedOdexInstruction -> 1 // Default size + else -> 1 // Fallback for any other edge case + } + + currentOffset += sizeInCodeUnits + } + // Skip payloads + } + + return offsetArray +} + +/** + * Returns an instruction index for a given branch instruction. + * + * @param instruction The branch instruction + * @param index Current instruction index + * @param offsetArray Array mapping instruction index to code offset. + */ +private fun Method.getBranchTargetInstructionIndex( + instruction: Instruction, + index: Int, + offsetArray: IntArray +): Int { + check (index >0 && index < offsetArray.size) { + "Invalid index: $index" + } + val currentOffset = offsetArray[index] + + return when (instruction.opcode) { + GOTO, GOTO_16, GOTO_32, + IF_EQ, IF_NE, IF_LT, IF_GE, IF_GT, IF_LE, + IF_EQZ, IF_NEZ, IF_LTZ, IF_GEZ, IF_GTZ, IF_LEZ -> { + val offset = (instruction as OffsetInstruction).codeOffset + val targetOffset = currentOffset + offset + // Find the instruction index at this offset. + findInstructionIndexByOffset(targetOffset, offsetArray) + } + // These need special handling - they jump to payloads + // which then have their own target lists. + // PACKED_SWITCH, SPARSE_SWITCH -> // TODO? + else -> throw IllegalStateException("Unsupported opcode: ${instruction.opcode}") + } +} + +/** + * Finds the instruction index for a given code offset. + * + * @param targetOffset Target code offset in 16-bit units + * @param offsetArray Array mapping instruction index to code offset (-1 for payloads) + * @return Instruction index at the target offset, or null if not found + */ +private fun Method.findInstructionIndexByOffset( + targetOffset: Int, + offsetArray: IntArray +): Int { + // Simple linear search using indexOfFirst + val index = offsetArray.indexOfFirst { it == targetOffset } + if (index >= 0) { + return index + } + + // Should never happen. + // Code has been tested on hundreds of random methods on all instruction indices, + // but maybe some weird code exists that this has overlooked. + throw IllegalArgumentException("Could not find exact instruction offset for method: " + + "$this at offset: $targetOffset." + ) +} + +/** + * @return The registers used by this instruction. + */ +val Instruction.registersUsed: List + get() = when (this) { + is FiveRegisterInstruction -> { + when (registerCount) { + 0 -> listOf() + 1 -> listOf(registerC) + 2 -> listOf(registerC, registerD) + 3 -> listOf(registerC, registerD, registerE) + 4 -> listOf(registerC, registerD, registerE, registerF) + else -> listOf(registerC, registerD, registerE, registerF, registerG) + } + } + + is ThreeRegisterInstruction -> listOf(registerA, registerB, registerC) + is TwoRegisterInstruction -> listOf(registerA, registerB) + is OneRegisterInstruction -> listOf(registerA) + is RegisterRangeInstruction -> (startRegister until (startRegister + registerCount)).toList() + else -> emptyList() + } + +/** + * @return The register that is written to by this instruction, + * or NULL if this is not a write opcode. + */ +val Instruction.writeRegister: Int? + get() { + if (this.opcode !in writeOpcodes) { + return null + } + if (this !is OneRegisterInstruction) { + throw IllegalStateException("Not a write instruction: $this") + } + return registerA + } + +/** + * This differs from [isUnconditionalBranchInstruction] in that it does not include unconditional goto. + * + * @return If this instruction is a conditional branch (multiple branch paths). + */ +internal val Instruction.isConditionalBranchInstruction: Boolean + get() = this.opcode in conditionalBranchOpcodes + +/** + * @return If this instruction is a GOTO opcode. + */ +internal val Instruction.isUnconditionalBranchInstruction: Boolean + get() = this.opcode in unconditionalBranchOpcodes + +/** + * @return If this instruction is a switch opcode. + */ +internal val Instruction.isSwitchInstruction: Boolean + get() = this.opcode in switchOpcodes + +/** + * @return If this instruction returns or throws. + */ +internal val Instruction.isReturnInstruction: Boolean + get() = this.opcode in returnOpcodes diff --git a/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt index b2d8ba287f..c1d76a8bb3 100644 --- a/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt @@ -76,12 +76,16 @@ fun ResourcePatchContext.copyResources( for (resourceGroup in resources) { resourceGroup.resources.forEach { resource -> + // Create the target directory if it doesn't exist. + Files.createDirectories(targetResourceDirectory.resolve(resourceGroup.resourceDirectoryName).toPath()) + val resourceFile = "${resourceGroup.resourceDirectoryName}/$resource" val stream = inputStreamFromBundledResource(sourceResourceDirectory, resourceFile) - if (stream == null) { - throw IllegalArgumentException("Could not find resource: $resourceFile " + - "in directory: $sourceResourceDirectory") - } + ?: throw IllegalArgumentException( + "Could not find resource: $resourceFile " + + "in directory: $sourceResourceDirectory" + ) + Files.copy( stream, targetResourceDirectory.resolve(resourceFile).toPath(), @@ -105,8 +109,8 @@ class ResourceGroup(val resourceDirectoryName: String, vararg val resources: Str /** * Iterate through the children of a node by its tag. - * @param resource The xml resource. - * @param targetTag The target xml node. + * @param resource The XML resource. + * @param targetTag The target XML node. * @param callback The callback to call when iterating over the nodes. */ fun ResourcePatchContext.iterateXmlNodeChildren( diff --git a/patches/src/main/kotlin/app/revanced/util/resource/StringResource.kt b/patches/src/main/kotlin/app/revanced/util/resource/StringResource.kt index c0956257c3..ddd3e6e528 100644 --- a/patches/src/main/kotlin/app/revanced/util/resource/StringResource.kt +++ b/patches/src/main/kotlin/app/revanced/util/resource/StringResource.kt @@ -10,7 +10,6 @@ import java.util.logging.Logger * * @param name The name of the string. * @param value The value of the string. - * @param formatted If the string is formatted. Defaults to `true`. */ class StringResource( name: String, diff --git a/patches/src/main/resources/addresources/values-bs-rBA/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/strings.xml index 3a40645452..76d0bdac9e 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/strings.xml @@ -75,7 +75,7 @@ Second \"item\" text" - + diff --git a/patches/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml index 097a7d9412..0be0a47ed0 100644 --- a/patches/src/main/resources/addresources/values/arrays.xml +++ b/patches/src/main/resources/addresources/values/arrays.xml @@ -303,23 +303,19 @@ @string/revanced_spoof_app_version_target_entry_1 @string/revanced_spoof_app_version_target_entry_2 @string/revanced_spoof_app_version_target_entry_3 - @string/revanced_spoof_app_version_target_entry_4 20.13.41 20.05.46 19.35.36 - 19.01.34 @string/revanced_spoof_app_version_target_entry_2 @string/revanced_spoof_app_version_target_entry_3 - @string/revanced_spoof_app_version_target_entry_4 20.05.46 19.35.36 - 19.01.34 19.01.34 @@ -350,8 +346,10 @@ @string/revanced_splash_screen_animation_style_entry_1 @string/revanced_splash_screen_animation_style_entry_2 + @string/revanced_splash_screen_animation_style_entry_3 + DISABLED FPS_60_ONE_SECOND FPS_60_BLACK_AND_WHITE diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 38d270a1d0..4a990f5fda 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -184,6 +184,7 @@ You will not be notified of any unexpected events." Flags saved Flags reset Flags copied to clipboard + Restart the app to use this feature Log protocol buffer Debug logs include proto buffer Debug logs do not include proto buffer @@ -237,6 +238,9 @@ However, enabling this will also log some user data such as your IP address."Shorts background play is enabled + Hide creator store shelf + Creator store shelf under video player is hidden + Creator store shelf under video player is shown Hide album cards Album cards are hidden Album cards are shown @@ -246,6 +250,9 @@ However, enabling this will also log some user data such as your IP address."Hide chips shelf Chips shelf is hidden Chips shelf is shown + Hide comments section under videos in Home feed + Comments section under videos in Home feed is hidden + Comments section under videos in Home feed is shown Hide community posts Community posts are hidden Community posts are shown @@ -274,21 +281,26 @@ However, enabling this will also log some user data such as your IP address."Hide latest posts Latest posts are hidden Latest posts are shown + + Hide \'Latest videos\' button + Latest videos button is hidden + Latest videos button is shown Hide mix playlists Mix playlists are hidden Mix playlists are shown Hide movies section Movies section is hidden Movies section is shown - + Hide \'Notify me\' button Notify me button is hidden Notify me button is shown Hide Playables Playables are hidden Playables are shown - Hide \'Show more\' button Show more button in search results is hidden @@ -306,13 +318,16 @@ However, enabling this will also log some user data such as your IP address."Hide visual spacer Visual spacer is hidden Visual spacer is shown + Hide web search results + Web search results are hidden + Web search results are shown + Hide \'You may like\' section + \'You may like\' section is hidden + \'You may like\' section is shown Hide YouTube Doodles YouTube Doodles animation on the logo is hidden YouTube Doodles animation on the logo is shown - "YouTube Doodles are shown a few days each year. - -If a Doodle is currently showing in your region and this hide setting is on, then the filter bar below the search bar will also be hidden." Hide channel bar Channel bar is hidden Channel bar is shown @@ -329,10 +344,15 @@ If a Doodle is currently showing in your region and this hide setting is on, the Info panels are hidden Info panels are shown + This button usually appears in the video player for certain videos. --> Hide Join button Join button is hidden Join button is shown + + Hide \'Live chat replay\' button + Live chat replay button in player overlay is hidden + Live chat replay button in player overlay is shown Hide medical panels Medical panels are hidden Medical panels are shown @@ -348,6 +368,9 @@ If a Doodle is currently showing in your region and this hide setting is on, the Hide timed reactions Timed reactions are hidden Timed reactions are shown + Hide video title + Video title in player overlay is hidden + Video title in player overlay is shown Hide \'AI-generated video summary\' AI-generated video summary section is hidden AI-generated video summary section is shown @@ -360,33 +383,62 @@ If a Doodle is currently showing in your region and this hide setting is on, the Hide Chapters Chapters section is hidden Chapters section is shown - Hide \'How this content was made\' - How this content was made section is hidden - How this content was made section is shown - Hide Hype points - Hype points are hidden - Hype points are shown + Hide \'Course progress\' + Course progress section is hidden + Course progress section is shown + Hide Explore + Explore this course and Explore the podcast sections are hidden + Explore this course and Explore the podcast sections are shown + Hide \'Explore this course\' + Explore this course section is hidden + Explore this course section is shown + Hide \'Explore the podcast\' + Explore the podcast section is hidden + Explore the podcast section is shown Hide \'Explore the podcast\' Explore the podcast section is hidden Explore the podcast section is shown Hide Featured links Featured links section is hidden Featured links section is shown + Hide \'Featured places\' + Featured places section is hidden + Featured places section is shown Hide Featured videos Featured videos section is hidden Featured videos section is shown + Enable feed flyout menu filter + Feed flyout menu filter is enabled + Feed flyout menu filter is disabled + Feed flyout menu filter + Enter the flyout menu names to filter by, one per line + Hide Gaming + Gaming section is hidden + Gaming section is shown + Hide \'How this content was made\' + How this content was made section is hidden + How this content was made section is shown + Hide Hype points + Hype points are hidden + Hype points are shown Hide Info cards Info cards section is hidden Info cards section is shown Hide \'Key concepts\' Key concepts section is hidden Key concepts section is shown + Hide Music + Music section is hidden + Music section is shown Hide Subscribe button Subscribe button is hidden Subscribe button is shown Hide Transcript Transcript section is hidden Transcript section is shown + Hide Quizzes + Quizzes section is hidden + Quizzes section is shown Video description Hide or show video description components @@ -407,6 +459,11 @@ If a Doodle is currently showing in your region and this hide setting is on, the Channel page Hide or show channel page components + Enable channel tab filter + Channel tab filter is enabled + Channel tab filter is disabled + Channel tab filter + Enter the channel tab names to filter by, one per line Hide Community button Community button is hidden @@ -471,15 +528,15 @@ If a Doodle is currently showing in your region and this hide setting is on, the View count is shown in feed and search results "Limitations: -• Shorts shelves, channel pages, and search results may still show view counts -• This feature does not work with automotive form factor" +• Shorts shelves, channel pages, and search results may still show view counts. +• This feature does not work with automotive form factor." Hide upload time Upload time is hidden in feed and search results Upload time is shown in feed and search results "Limitations: -• Shorts shelves, channel pages, and search results may still show upload times -• This feature does not work with automotive form factor" +• Shorts shelves, channel pages, and search results may still show upload times. +• This feature does not work with automotive form factor." Hide keyword content Hide feed and search videos using keyword filters Hide Home videos by keywords @@ -502,7 +559,7 @@ Words with uppercase letters in the middle must be entered with the casing (ie: About keyword filtering "Home/Subscriptions/Search results are filtered to hide content that matches keyword phrases -Limitations +Limitations: • Shorts cannot be hidden by channel name • Some UI components may not be hidden • Searching for a keyword may show no results" @@ -517,19 +574,12 @@ Limitations Keyword will hide all videos: %s - Hide creator store shelf - Creator store shelf under video player is hidden - Creator store shelf under video player is shown Hide end screen store banner End screen store banner is hidden End screen store banner is shown Hide fullscreen ads - "Fullscreen ads are hidden - -This feature is only available for older devices" + Fullscreen ads are hidden Fullscreen ads are shown - - Hide fullscreen ads only works with older devices Hide general ads General ads are hidden General ads are shown @@ -539,6 +589,9 @@ This feature is only available for older devices" Hide paid promotion label Paid promotion label is hidden Paid promotion label is shown + Hide player popup ads + Player popup ads are hidden + Player popup ads are shown Hide self sponsored cards Self sponsored cards are hidden Self sponsored cards are shown @@ -546,23 +599,18 @@ This feature is only available for older devices" Shopping links in video description are hidden Shopping links in video description are shown Hide view products banner - View products banner in video overlay is hidden - View products banner in video overlay is shown - Hide web search results - Web search results are hidden - Web search results are shown - - - Hide YouTube Premium promotions - YouTube Premium promotions under video player are hidden - YouTube Premium promotions under video player are shown + View products banner in player overlay is hidden + View products banner in player overlay is shown + Hide YouTube Premium promotions + YouTube Premium promotions are hidden + YouTube Premium promotions are shown Hide video ads Video ads are hidden Video ads are shown - + URL copied to clipboard URL with timestamp copied Show copy video URL button @@ -578,10 +626,10 @@ This feature is only available for older devices" Dialog will be shown This does not bypass the age restriction. It just accepts it automatically. - - Disable \'Sign in to TV\' popup - Sign in to TV popup is disabled - Sign in to TV popup is enabled + + Disable \'Sign in to TV\' popup + Sign in to TV popup is disabled + Sign in to TV popup is enabled Disable double tap chapter skip @@ -614,10 +662,10 @@ Verify the package name is correct and the app is installed" Precise seeking gesture is disabled Precise seeking gesture is enabled - - Enable tap to seek - Tap to seek is enabled - Tap to seek is disabled + + Enable tap to seek + Tap to seek is enabled + Tap to seek is disabled Enable brightness gesture @@ -698,7 +746,7 @@ Adjust volume by swiping vertically on the right side of the screen" Comments button is hidden Comments button is shown + This button usually appears on live streamed videos. --> Hide Report Report button is hidden Report button is shown @@ -711,7 +759,7 @@ Adjust volume by swiping vertically on the right side of the screen" Download button is hidden Download button is shown + This button usually appears on videos uploaded by the logged-in user. --> Hide Hype Hype button is hidden Hype button is shown @@ -724,7 +772,7 @@ Adjust volume by swiping vertically on the right side of the screen" Thanks button is hidden Thanks button is shown + This button usually appears if the user IP is from a specific region such as the USA or EU. --> Hide Ask Ask button is hidden Ask button is shown @@ -732,6 +780,7 @@ Adjust volume by swiping vertically on the right side of the screen" Hide Clip Clip button is hidden Clip button is shown + Hiding may not work for some user accounts. Hide Shop Shop button is hidden @@ -741,7 +790,7 @@ Adjust volume by swiping vertically on the right side of the screen" Save button is hidden Save button is shown - + Navigation buttons Hide or change navigation bar buttons @@ -765,29 +814,41 @@ Adjust volume by swiping vertically on the right side of the screen" Notifications button is shown Switch Create with Notifications - "Create button is switched with Notifications button - -Note: Enabling this also forcibly hides video ads" + Create button is switched with Notifications button Create button is not switched with Notifications button - "Disabling this setting will also disable Shorts ad blocking. - -If changing this setting does not take effect, try switching to Incognito mode." + If changing this setting does not take effect, try switching to Incognito mode. Hide navigation button labels Labels are hidden Labels are shown + Enable narrow navigation buttons + Spacing between navigation buttons is narrow + Spacing between navigation buttons is normal Enable navigation bar animations Navigation transitions are animated Navigation transitions are not animated Disable translucent status bar Status bar is opaque Status bar is opaque or translucent - On some devices, enabling this feature can change the system navigation bar to transparent. + "Limitations: +• A black bar may appear at the top of the video player. +• On some devices, enabling this feature can change the system navigation bar to transparent." Disable light translucent bar Light mode navigation bar is opaque Light mode navigation bar is opaque or translucent Disable dark translucent bar Dark mode navigation bar is opaque Dark mode navigation bar is opaque or translucent + Toolbar + Hide or change toolbar components + Hide Create button + Create button is hidden + Create button is shown + Hide Notifications button + Notifications button is hidden + Notifications button is shown + Hide Search button + Search button is hidden + Search button is shown. Flyout menu @@ -862,6 +923,12 @@ To show the Audio track menu, change \'Spoof video streams\' to \'Android No SDK Hide Cast button Cast button is hidden Cast button is shown + Hide Collapse button + Collapse button is hidden + Collapse button is shown + Hide Fullscreen button + Fullscreen button is hidden + Fullscreen button is shown Hide player controls background Player controls background is hidden Player controls background is shown @@ -870,9 +937,9 @@ To show the Audio track menu, change \'Spoof video streams\' to \'Android No SDK Previous & Next buttons are shown - Hide end screen cards - End screen cards are hidden - End screen cards are shown + Hide end screen cards + End screen cards are hidden + End screen cards are shown Disable Ambient mode in fullscreen @@ -904,6 +971,9 @@ To show the Audio track menu, change \'Spoof video streams\' to \'Android No SDK Shorts player Hide or show Shorts player components + Hide Shorts in channel page + Hidden in channel page + Shown in channel page Hide Shorts in Home feed Hidden in Home feed and related videos @@ -915,9 +985,18 @@ To show the Audio track menu, change \'Spoof video streams\' to \'Android No SDK Hide Shorts in Subscriptions feed Hidden in Subscriptions feed Shown in Subscriptions feed + Hide Shorts in video description + Hidden in video description + Shown in video description Hide Shorts in watch history Hidden in watch history Shown in watch history + Hide AI button + AI button is hidden + AI button is shown + Hide \'Auto-dubbed\' label + Auto-dubbed label is hidden + Auto-dubbed label is shown Hide \'Auto-dubbed\' label Auto-dubbed label is hidden Auto-dubbed label is shown @@ -979,6 +1058,7 @@ To show the Audio track menu, change \'Spoof video streams\' to \'Android No SDK Hide \'Use this template\' button Use this template button is hidden Use this template button is shown + Hide Like button fountain animation Like button fountain animation is hidden Like button fountain animation is shown @@ -1021,7 +1101,12 @@ To show the Audio track menu, change \'Spoof video streams\' to \'Android No SDK Navigation bar is hidden Navigation bar is shown - + + Hide autoplay preview + Autoplay preview is hidden + Autoplay preview is shown + + Hide end screen suggested video "End screen suggested video is hidden when autoplay is turned off @@ -1117,21 +1202,6 @@ Limitation: Dislikes may not appear in incognito mode" Wide search bar is enabled Wide search bar is disabled - - Enable high quality thumbnails - Seekbar thumbnails are high quality - Seekbar thumbnails are medium quality - Fullscreen seekbar thumbnails are high quality - Fullscreen seekbar thumbnails are medium quality - "This will also restore thumbnails on livestreams that do not have seekbar thumbnails. - -Seekbar thumbnails will use the same quality as the current video. - -This feature works best with a video quality of 720p or lower and when using a very fast internet connection." - Restore old seekbar thumbnails - Seekbar thumbnails will appear above the seekbar - Seekbar thumbnails will appear in fullscreen - SponsorBlock Enable SponsorBlock @@ -1233,7 +1303,7 @@ Your user id is like a password and it should never be shared. Tangent / Jokes Tangential scenes or jokes that are not required to understand the main content of the video. Does not include sections providing context or background details Music: Non-Music Section - Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category + Only for use in music videos. Sections of music videos without music, that are not already covered by another category Skip Highlight Skip sponsor @@ -1276,8 +1346,8 @@ Your user id is like a password and it should never be shared. SponsorBlock is temporarily down Unable to submit segment (status: %1$d %2$s) Unable to submit segment. Rate Limited (too many from the same user or IP) - Can\'t submit the segment: %s - "Can\'t submit the segment. + Cannot submit the segment: %s + "Cannot submit the segment. Already exists" Segment submitted successfully @@ -1374,7 +1444,6 @@ If later turned off, it is recommended to clear the app data to prevent UI bugs. 20.13.41 - Restore non collapsed video action bar 20.05.46 - Restore transcript functionality 19.35.36 - Restore old Shorts player icons - 19.01.34 - Restore old navigation icons Change start page @@ -1485,8 +1554,9 @@ Swipe to expand or close" Loading screen will have a gradient background Loading screen will have a solid background Splash screen style - Color - Black and white + Disabled + Color + Black and white Enable custom seekbar color Custom seekbar color is shown Original seekbar color is shown @@ -1602,6 +1672,9 @@ Enabling this can unlock higher video qualities" Disable seek undo haptics Seek undo haptics is disabled Seek undo haptics is enabled + Disable tap and hold haptics + Tap and hold haptics is disabled + Tap and hold haptics is enabled Disable zoom haptics Zoom haptics is disabled Zoom haptics is enabled @@ -1697,6 +1770,11 @@ Limitations: Advanced video quality menu is shown Advanced video quality menu is not shown + + Hide Premium quality options + Premium quality options are hidden + Premium quality options are shown + Enable slide to seek Slide to seek is enabled @@ -1732,6 +1810,7 @@ Video playback with AV1 may stutter or drop frames." Music + About Ads General diff --git a/patches/src/main/resources/copyvideourl/drawable/revanced_yt_copy.xml b/patches/src/main/resources/copyvideourl/drawable/revanced_yt_copy.xml index 1a2ab36293..967bd75071 100644 --- a/patches/src/main/resources/copyvideourl/drawable/revanced_yt_copy.xml +++ b/patches/src/main/resources/copyvideourl/drawable/revanced_yt_copy.xml @@ -1,6 +1,7 @@ + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/copyvideourl/drawable/revanced_yt_copy_timestamp.xml b/patches/src/main/resources/copyvideourl/drawable/revanced_yt_copy_timestamp.xml index de5835b5be..879ee6a663 100644 --- a/patches/src/main/resources/copyvideourl/drawable/revanced_yt_copy_timestamp.xml +++ b/patches/src/main/resources/copyvideourl/drawable/revanced_yt_copy_timestamp.xml @@ -1,7 +1,8 @@ + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/downloads/drawable/revanced_yt_download_button.xml b/patches/src/main/resources/downloads/drawable/revanced_yt_download_button.xml index 18d0944b07..32898a39f1 100644 --- a/patches/src/main/resources/downloads/drawable/revanced_yt_download_button.xml +++ b/patches/src/main/resources/downloads/drawable/revanced_yt_download_button.xml @@ -1,6 +1,7 @@ + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_rectangle.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_rectangle.xml index 2791f1990a..a4c0785a92 100644 --- a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_rectangle.xml +++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_rectangle.xml @@ -1,9 +1,45 @@ - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button_rectangle.xml b/patches/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button_rectangle.xml index 2791f1990a..a4c0785a92 100644 --- a/patches/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button_rectangle.xml +++ b/patches/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button_rectangle.xml @@ -1,9 +1,45 @@ - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index edfcf40ede..24d395c4ae 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,12 @@ pluginManagement { } } +dependencyResolutionManagement { + repositories { + mavenLocal() + } +} + plugins { id("app.revanced.patches") version "1.0.0-dev.9" }