diff --git a/CHANGELOG.md b/CHANGELOG.md index 675036e0..b69aab43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,199 @@ +# [0.108.0-dev.24](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.23...v0.108.0-dev.24) (2023-05-22) + + +### Bug Fixes + +* **youtube/return-youtube-dislike:** fix dislikes not showing for video opened from feed autoplay ([#408](https://github.com/revanced/revanced-integrations/issues/408)) ([307315c](https://github.com/revanced/revanced-integrations/commit/307315c43c68a47c983384351a617f5c5f508b4f)) + +# [0.108.0-dev.23](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.22...v0.108.0-dev.23) (2023-05-21) + + +### Features + +* **reddit:** add `sanitize-sharing-links` patch ([#407](https://github.com/revanced/revanced-integrations/issues/407)) ([191cc71](https://github.com/revanced/revanced-integrations/commit/191cc711de1ecbf6632fc27d32ee4f0c81413c57)) + +# [0.108.0-dev.22](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.21...v0.108.0-dev.22) (2023-05-20) + + +### Bug Fixes + +* **twitter:** correctly resolve to integrations methods ([cd93917](https://github.com/revanced/revanced-integrations/commit/cd93917148e2f7695effb15183f53b84ddb9800a)) + + +### Features + +* **twitter/hide-recommended-users:** hide "Who to follow" ([c7cabc0](https://github.com/revanced/revanced-integrations/commit/c7cabc0b5799464ed75d290dfae5fcd2faa4fc94)) + +# [0.108.0-dev.21](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.20...v0.108.0-dev.21) (2023-05-19) + + +### Bug Fixes + +* **youtube/settings:** fix non functional back button in settings ([#404](https://github.com/revanced/revanced-integrations/issues/404)) ([0c55d70](https://github.com/revanced/revanced-integrations/commit/0c55d70370dad9275dfb5bc3817f71d4290f5a13)) + +# [0.108.0-dev.20](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.19...v0.108.0-dev.20) (2023-05-19) + + +### Features + +* **youtube/copy-video-url:** add tap and hold functionality to copy video url buttons ([#403](https://github.com/revanced/revanced-integrations/issues/403)) ([80689ef](https://github.com/revanced/revanced-integrations/commit/80689eff5b2deb971feb1fc59e987ef835506bae)) + +# [0.108.0-dev.19](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.18...v0.108.0-dev.19) (2023-05-19) + + +### Features + +* **youtube:** support version `18.19.35` ([b47a781](https://github.com/revanced/revanced-integrations/commit/b47a781ba710e6fb66e144ef95cdd51af358e4de)) + +# [0.108.0-dev.18](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.17...v0.108.0-dev.18) (2023-05-18) + + +### Features + +* add capability to filter from protobuf buffer ([5652c32](https://github.com/revanced/revanced-integrations/commit/5652c323455b58f6760d4938c79d704c22fd546c)) +* **youtube/hide-shorts-components:** hide navigation bar ([ac13d10](https://github.com/revanced/revanced-integrations/commit/ac13d1030561905a81059ad0db31a749833a31cd)) +* **youtube:** add `hide-shorts-components` patch ([5ec90db](https://github.com/revanced/revanced-integrations/commit/5ec90db28a46e8f5d79f4793c141a7411a2da05d)) + +# [0.108.0-dev.17](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.16...v0.108.0-dev.17) (2023-05-16) + + +### Bug Fixes + +* **youtube/sponsorblock:** fix toast shown when scrubbing thru a paused video ([#401](https://github.com/revanced/revanced-integrations/issues/401)) ([7da5673](https://github.com/revanced/revanced-integrations/commit/7da56738a14a36fbf66f05d28fd886baaafbee3f)) + +# [0.108.0-dev.16](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.15...v0.108.0-dev.16) (2023-05-16) + + +### Features + +* **youtube:** add options to disable toasts on connection errors ([#402](https://github.com/revanced/revanced-integrations/issues/402)) ([ae18edd](https://github.com/revanced/revanced-integrations/commit/ae18edd047d7979307bc28f28db17bae2c5cc226)) + +# [0.108.0-dev.15](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.14...v0.108.0-dev.15) (2023-05-15) + + +### Features + +* **youtube:** import / export of revanced settings ([#388](https://github.com/revanced/revanced-integrations/issues/388)) ([c3f08d8](https://github.com/revanced/revanced-integrations/commit/c3f08d8d7e8116496611b85508fbd54bb3a71992)) + +# [0.108.0-dev.14](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.13...v0.108.0-dev.14) (2023-05-14) + + +### Bug Fixes + +* **youtube/return-youtube-dislikes:** fix temporarily frozen video after opening a shorts ([#396](https://github.com/revanced/revanced-integrations/issues/396)) ([6a94bd2](https://github.com/revanced/revanced-integrations/commit/6a94bd2237be9cde6256c83fcec72b3f0de83496)) + +# [0.108.0-dev.13](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.12...v0.108.0-dev.13) (2023-05-13) + + +### Bug Fixes + +* **youtube/remember-video-quality:** do not show 'auto' in video resolution picker if a default quality is set ([#400](https://github.com/revanced/revanced-integrations/issues/400)) ([e30d120](https://github.com/revanced/revanced-integrations/commit/e30d1201c992f4896a0b7106230377d78506cd6f)) + +# [0.108.0-dev.12](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.11...v0.108.0-dev.12) (2023-05-13) + + +### Bug Fixes + +* **youtube/swipe-controls:** restart when "press to swipe" preference is changed ([#399](https://github.com/revanced/revanced-integrations/issues/399)) ([a3d754c](https://github.com/revanced/revanced-integrations/commit/a3d754c209e443135759850c7634708b23330a7c)) + +# [0.108.0-dev.11](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.10...v0.108.0-dev.11) (2023-05-12) + + +### Features + +* **twitch:** add `auto-claim-channel-points` patch ([#398](https://github.com/revanced/revanced-integrations/issues/398)) ([d7f050b](https://github.com/revanced/revanced-integrations/commit/d7f050ba2ff513c91cccbf0095fc7756dbb47400)) + +# [0.108.0-dev.10](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.9...v0.108.0-dev.10) (2023-05-12) + + +### Features + +* **youtube:** add `hide-filter-bar` patch ([9649c3d](https://github.com/revanced/revanced-integrations/commit/9649c3dbc8406c3639c4fff9dd179d6d29886e60)) + +# [0.108.0-dev.9](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.8...v0.108.0-dev.9) (2023-05-11) + + +### Features + +* **youtube/video-speed:** change custom video speeds inside app settings ([#393](https://github.com/revanced/revanced-integrations/issues/393)) ([b42790f](https://github.com/revanced/revanced-integrations/commit/b42790fbca0f6c854d41871834fd6266dd2ea106)) + +# [0.108.0-dev.8](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.7...v0.108.0-dev.8) (2023-05-11) + + +### Bug Fixes + +* **youtube/theme:** apply custom seekbar color to video thumbnails ([#391](https://github.com/revanced/revanced-integrations/issues/391)) ([ae99408](https://github.com/revanced/revanced-integrations/commit/ae994086360b45340ed1ed896c35917d785bb4f9)) + +# [0.108.0-dev.7](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.6...v0.108.0-dev.7) (2023-05-10) + + +### Bug Fixes + +* **youtube/hide-ads:** don't filter for `reels_player_overlay` ([415c194](https://github.com/revanced/revanced-integrations/commit/415c1948fccdc8eb27b76b043996017c5c56eac3)) + +# [0.108.0-dev.6](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.5...v0.108.0-dev.6) (2023-05-09) + + +### Bug Fixes + +* **youtube/spoof-app-version:** restore watch history preview ([#394](https://github.com/revanced/revanced-integrations/issues/394)) ([4c7f737](https://github.com/revanced/revanced-integrations/commit/4c7f737913a0c3690f8230c51f6dd217e8b04c7a)) + +# [0.108.0-dev.5](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.4...v0.108.0-dev.5) (2023-05-09) + + +### Bug Fixes + +* **youtube/remember-video-quality:** fix default video quality/speed being applied when resuming app ([#392](https://github.com/revanced/revanced-integrations/issues/392)) ([c97d1b7](https://github.com/revanced/revanced-integrations/commit/c97d1b7ee5be6a0f097f2995321608bc74f5822c)) + +# [0.108.0-dev.4](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.3...v0.108.0-dev.4) (2023-05-07) + + +### Features + +* **youtube/hide-player-overlay:** make it toggleable in settings ([#382](https://github.com/revanced/revanced-integrations/issues/382)) ([1b4aa0f](https://github.com/revanced/revanced-integrations/commit/1b4aa0fcc6b89acd4156e93685b1da7519aa7148)) + +# [0.108.0-dev.3](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.2...v0.108.0-dev.3) (2023-05-07) + + +### Features + +* **youtube:** `hide-load-more-button` patch ([#389](https://github.com/revanced/revanced-integrations/issues/389)) ([7da9d44](https://github.com/revanced/revanced-integrations/commit/7da9d440eedfc895b49aac40498f0279156ad117)) + +# [0.108.0-dev.2](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.1...v0.108.0-dev.2) (2023-05-05) + + +### Bug Fixes + +* **youtube/theme:** fix app crash if user clears seekbar color ([#390](https://github.com/revanced/revanced-integrations/issues/390)) ([e2f5290](https://github.com/revanced/revanced-integrations/commit/e2f52905dc445f881666c06877c3a69306335dcb)) + +# [0.108.0-dev.1](https://github.com/revanced/revanced-integrations/compare/v0.107.1-dev.3...v0.108.0-dev.1) (2023-05-03) + + +### Features + +* **youtube/settings:** add reset button to edit preference dialog ([#383](https://github.com/revanced/revanced-integrations/issues/383)) ([cb5a4d0](https://github.com/revanced/revanced-integrations/commit/cb5a4d0c9b3b340928695fcb1d10b164a6dcef27)) + +## [0.107.1-dev.3](https://github.com/revanced/revanced-integrations/compare/v0.107.1-dev.2...v0.107.1-dev.3) (2023-05-03) + + +### Bug Fixes + +* **youtube/theme:** fix toast shown on fresh app install ([#381](https://github.com/revanced/revanced-integrations/issues/381)) ([2dc431f](https://github.com/revanced/revanced-integrations/commit/2dc431f1bf54c12dfc45c4511a0b0792e214be4f)) + +## [0.107.1-dev.2](https://github.com/revanced/revanced-integrations/compare/v0.107.1-dev.1...v0.107.1-dev.2) (2023-05-03) + + +### Bug Fixes + +* **youtube/sponsorblock:** fix skip button in wrong location when full screen and comments visible ([#387](https://github.com/revanced/revanced-integrations/issues/387)) ([486b79b](https://github.com/revanced/revanced-integrations/commit/486b79b4e4927d4c05cfb4d5222a1d74fe60e327)) + +## [0.107.1-dev.1](https://github.com/revanced/revanced-integrations/compare/v0.107.0...v0.107.1-dev.1) (2023-05-02) + + +### Bug Fixes + +* **youtube/return-youtube-dislike:** fix potential error toast when using old UI layout ([#384](https://github.com/revanced/revanced-integrations/issues/384)) ([6c36bee](https://github.com/revanced/revanced-integrations/commit/6c36beeda139156bfbb5a17bc89aa63c25afa83c)) + # [0.107.0](https://github.com/revanced/revanced-integrations/compare/v0.106.0...v0.107.0) (2023-05-02) diff --git a/README.md b/README.md index 17a90cb3..4e37581a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -# ReVanced Integrations +# 🔩 ReVanced Integrations + +The official ReVanced Integrations containing classes to be merged by ReVanced Patcher. + +## ❓ How to use debugging: -# How to use debugging: - Usage on Windows: ```adb logcat | findstr "revanced" > log.txt``` - Usage on Linux: ```adb logcat | grep --line-buffered "revanced" > log.txt``` This will write the log to a file called log.txt which you can view then. - diff --git a/app/src/main/java/app/revanced/integrations/adremover/AdRemoverAPI.java b/app/src/main/java/app/revanced/integrations/adremover/AdRemoverAPI.java deleted file mode 100644 index 63dfed33..00000000 --- a/app/src/main/java/app/revanced/integrations/adremover/AdRemoverAPI.java +++ /dev/null @@ -1,42 +0,0 @@ -package app.revanced.integrations.adremover; - - -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.Toolbar; - -import app.revanced.integrations.utils.LogHelper; - -public class AdRemoverAPI { - - /** - * Removes Reels and Home ads - * - * @param view - */ - //ToDo: refactor this - public static void HideViewWithLayout1dp(View view) { - if (view instanceof LinearLayout) { - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(1, 1); - view.setLayoutParams(layoutParams); - } else if (view instanceof FrameLayout) { - FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(1, 1); - view.setLayoutParams(layoutParams2); - } else if (view instanceof RelativeLayout) { - RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(1, 1); - view.setLayoutParams(layoutParams3); - } else if (view instanceof Toolbar) { - Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(1, 1); - view.setLayoutParams(layoutParams4); - } else if (view instanceof ViewGroup) { - ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(1, 1); - view.setLayoutParams(layoutParams5); - } else { - LogHelper.printDebug(() -> "HideViewWithLayout1dp - Id: " + view.getId() + " Type: " + view.getClass().getName()); - } - } - -} diff --git a/app/src/main/java/app/revanced/integrations/patches/AutoRepeatPatch.java b/app/src/main/java/app/revanced/integrations/patches/AutoRepeatPatch.java index 52c29aea..28cf5372 100644 --- a/app/src/main/java/app/revanced/integrations/patches/AutoRepeatPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/AutoRepeatPatch.java @@ -5,6 +5,6 @@ import app.revanced.integrations.settings.SettingsEnum; public class AutoRepeatPatch { //Used by app.revanced.patches.youtube.layout.autorepeat.patch.AutoRepeatPatch public static boolean shouldAutoRepeat() { - return SettingsEnum.PREFERRED_AUTO_REPEAT.getBoolean(); + return SettingsEnum.AUTO_REPEAT.getBoolean(); } } diff --git a/app/src/main/java/app/revanced/integrations/patches/ButtonsPatch.java b/app/src/main/java/app/revanced/integrations/patches/ButtonsPatch.java deleted file mode 100644 index b9897c10..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/ButtonsPatch.java +++ /dev/null @@ -1,31 +0,0 @@ -package app.revanced.integrations.patches; - -import app.revanced.integrations.settings.SettingsEnum; - -final class ButtonsPatch extends Filter { - private final BlockRule actionBarRule; - - public ButtonsPatch() { - actionBarRule = new BlockRule(null, "video_action_bar"); - pathRegister.registerAll( - new BlockRule(SettingsEnum.HIDE_LIKE_DISLIKE_BUTTON, "|like_button", "dislike_button"), - new BlockRule(SettingsEnum.HIDE_DOWNLOAD_BUTTON, "download_button"), - new BlockRule(SettingsEnum.HIDE_PLAYLIST_BUTTON, "save_to_playlist_button"), - new BlockRule(SettingsEnum.HIDE_CLIP_BUTTON, "|clip_button.eml|"), - new BlockRule(SettingsEnum.HIDE_ACTION_BUTTONS, "ContainerType|video_action_button", "|CellType|CollectionType|CellType|ContainerType|button.eml|") - ); - } - - private boolean canHideActionBar() { - for (BlockRule rule : pathRegister) if (!rule.isEnabled()) return false; - return true; - } - - @Override - public boolean filter(final String path, final String identifier) { - // If everything is hidden, then also hide the video bar itself. - if (canHideActionBar() && actionBarRule.check(identifier).isBlocked()) return true; - - return pathRegister.contains(path); - } -} diff --git a/app/src/main/java/app/revanced/integrations/patches/CommentsPatch.java b/app/src/main/java/app/revanced/integrations/patches/CommentsPatch.java deleted file mode 100644 index 41a1887c..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/CommentsPatch.java +++ /dev/null @@ -1,31 +0,0 @@ -package app.revanced.integrations.patches; - -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; - -final class CommentsPatch extends Filter { - - public CommentsPatch() { - var comments = new BlockRule(SettingsEnum.HIDE_COMMENTS_SECTION, "video_metadata_carousel", "_comments"); - var previewComment = new BlockRule( - SettingsEnum.HIDE_PREVIEW_COMMENT, - "|carousel_item", - "comments_entry_point_teaser", - "comments_entry_point_simplebox" - ); - - this.pathRegister.registerAll( - comments, - previewComment - ); - } - - @Override - boolean filter(String path, String _identifier) { - if (!pathRegister.contains(path)) return false; - - LogHelper.printDebug(() -> "Blocked: " + path); - - return true; - } -} diff --git a/app/src/main/java/app/revanced/integrations/patches/CopyVideoUrlPatch.java b/app/src/main/java/app/revanced/integrations/patches/CopyVideoUrlPatch.java index 023c2bd6..f21e4889 100644 --- a/app/src/main/java/app/revanced/integrations/patches/CopyVideoUrlPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/CopyVideoUrlPatch.java @@ -2,22 +2,46 @@ package app.revanced.integrations.patches; import static app.revanced.integrations.utils.StringRef.str; +import android.os.Build; + import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; public class CopyVideoUrlPatch { - public static void copyUrl(Boolean withTimestamp) { + + public static void copyUrl(boolean withTimestamp) { try { - String url = String.format("https://youtu.be/%s", VideoInformation.getVideoId()); - if (withTimestamp) { - long seconds = VideoInformation.getVideoTime() / 1000; - url += String.format("?t=%s", seconds); + StringBuilder builder = new StringBuilder("https://youtu.be/"); + builder.append(VideoInformation.getVideoId()); + final long currentVideoTimeInSeconds = VideoInformation.getVideoTime() / 1000; + if (withTimestamp && currentVideoTimeInSeconds > 0) { + final long hour = currentVideoTimeInSeconds / (60 * 60); + final long minute = (currentVideoTimeInSeconds / 60) % 60; + final long second = currentVideoTimeInSeconds % 60; + builder.append("?t="); + if (hour > 0) { + builder.append(hour).append("h"); + } + if (minute > 0) { + builder.append(minute).append("m"); + } + if (second > 0) { + builder.append(second).append("s"); + } } - ReVancedUtils.setClipboard(url); - ReVancedUtils.showToastShort(str("share_copy_url_success")); + ReVancedUtils.setClipboard(builder.toString()); + // Do not show a toast if using Android 13+ as it shows it's 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)) { + ReVancedUtils.showToastShort(withTimestamp && currentVideoTimeInSeconds > 0 + ? str("revanced_share_copy_url_timestamp_success") + : str("revanced_share_copy_url_success")); + } } catch (Exception e) { LogHelper.printException(() -> "Failed to generate video url", e); } } + } diff --git a/app/src/main/java/app/revanced/integrations/patches/DisableAutoCaptionsPatch.java b/app/src/main/java/app/revanced/integrations/patches/DisableAutoCaptionsPatch.java index e3d83a6e..0e61b261 100644 --- a/app/src/main/java/app/revanced/integrations/patches/DisableAutoCaptionsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/DisableAutoCaptionsPatch.java @@ -4,10 +4,13 @@ import app.revanced.integrations.settings.SettingsEnum; public class DisableAutoCaptionsPatch { + /** + * Used by injected code. Do not delete. + */ public static boolean captionsButtonDisabled; public static boolean autoCaptionsEnabled() { - return SettingsEnum.CAPTIONS_ENABLED.getBoolean(); + return SettingsEnum.AUTO_CAPTIONS.getBoolean(); } } diff --git a/app/src/main/java/app/revanced/integrations/patches/DisableStartupShortsPlayerPatch.java b/app/src/main/java/app/revanced/integrations/patches/DisableStartupShortsPlayerPatch.java index d0ee7d93..aa8c4cf3 100644 --- a/app/src/main/java/app/revanced/integrations/patches/DisableStartupShortsPlayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/DisableStartupShortsPlayerPatch.java @@ -5,6 +5,6 @@ import app.revanced.integrations.settings.SettingsEnum; public class DisableStartupShortsPlayerPatch { //Used by app.revanced.patches.youtube.layout.startupshortsreset.patch.DisableShortsOnStartupPatch public static boolean disableStartupShortsPlayer() { - return SettingsEnum.DISABLE_STARTUP_SHORTS_PLAYER.getBoolean(); + return SettingsEnum.DISABLE_RESUMING_SHORTS_PLAYER.getBoolean(); } } diff --git a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java index 75717de9..e69de29b 100644 --- a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java @@ -1,191 +0,0 @@ -package app.revanced.integrations.patches; - -import android.view.View; -import app.revanced.integrations.adremover.AdRemoverAPI; -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; - -public final class GeneralAdsPatch extends Filter { - private final String[] IGNORE = { - "home_video_with_context", - "related_video_with_context", - "comment_thread", // skip blocking anything in the comments - "|comment.", // skip blocking anything in the comments replies - "library_recent_shelf", - }; - - private final BlockRule custom = new CustomBlockRule( - SettingsEnum.ADREMOVER_CUSTOM_ENABLED, - SettingsEnum.ADREMOVER_CUSTOM_REMOVAL - ); - - public GeneralAdsPatch() { - var communityPosts = new BlockRule(SettingsEnum.ADREMOVER_COMMUNITY_POSTS_REMOVAL, "post_base_wrapper"); - var communityGuidelines = new BlockRule(SettingsEnum.ADREMOVER_COMMUNITY_GUIDELINES_REMOVAL, "community_guidelines"); - var subscribersCommunityGuidelines = new BlockRule(SettingsEnum.ADREMOVER_SUBSCRIBERS_COMMUNITY_GUIDELINES_REMOVAL, "sponsorships_comments_upsell"); - var channelMemberShelf = new BlockRule(SettingsEnum.ADREMOVER_CHANNEL_MEMBER_SHELF_REMOVAL, "member_recognition_shelf"); - var compactBanner = new BlockRule(SettingsEnum.ADREMOVER_COMPACT_BANNER_REMOVAL, "compact_banner"); - var inFeedSurvey = new BlockRule(SettingsEnum.ADREMOVER_FEED_SURVEY_REMOVAL, "in_feed_survey", "slimline_survey"); - var medicalPanel = new BlockRule(SettingsEnum.ADREMOVER_MEDICAL_PANEL_REMOVAL, "medical_panel"); - var paidContent = new BlockRule(SettingsEnum.ADREMOVER_PAID_CONTENT_REMOVAL, "paid_content_overlay"); - var merchandise = new BlockRule(SettingsEnum.ADREMOVER_MERCHANDISE_REMOVAL, "product_carousel"); - var infoPanel = new BlockRule(SettingsEnum.ADREMOVER_INFO_PANEL_REMOVAL, "publisher_transparency_panel", "single_item_information_panel"); - var latestPosts = new BlockRule(SettingsEnum.ADREMOVER_HIDE_LATEST_POSTS, "post_shelf"); - var channelGuidelines = new BlockRule(SettingsEnum.ADREMOVER_HIDE_CHANNEL_GUIDELINES, "channel_guidelines_entry_banner"); - var audioTrackButton = new BlockRule(SettingsEnum.HIDE_AUDIO_TRACK_BUTTON, "multi_feed_icon_button"); - var artistCard = new BlockRule(SettingsEnum.HIDE_ARTIST_CARDS, "official_card"); - var selfSponsor = new BlockRule(SettingsEnum.ADREMOVER_SELF_SPONSOR_REMOVAL, "cta_shelf_card"); - var chapterTeaser = new BlockRule(SettingsEnum.ADREMOVER_CHAPTER_TEASER_REMOVAL, "expandable_metadata", "macro_markers_carousel"); - var viewProducts = new BlockRule(SettingsEnum.ADREMOVER_VIEW_PRODUCTS, "product_item", "products_in_video"); - var webLinkPanel = new BlockRule(SettingsEnum.ADREMOVER_WEB_SEARCH_RESULTS, "web_link_panel"); - var channelBar = new BlockRule(SettingsEnum.ADREMOVER_CHANNEL_BAR, "channel_bar"); - var relatedVideos = new BlockRule(SettingsEnum.ADREMOVER_RELATED_VIDEOS, "fullscreen_related_videos"); - var quickActions = new BlockRule(SettingsEnum.ADREMOVER_QUICK_ACTIONS, "quick_actions"); - var imageShelf = new BlockRule(SettingsEnum.ADREMOVER_IMAGE_SHELF, "image_shelf"); - var graySeparator = new BlockRule(SettingsEnum.ADREMOVER_GRAY_SEPARATOR, - "cell_divider" // layout residue (gray line above the buttoned ad), - ); - var buttonedAd = new BlockRule(SettingsEnum.ADREMOVER_BUTTONED_REMOVAL, - "_buttoned_layout", - "full_width_square_image_layout", - "_ad_with", - "video_display_button_group_layout", - "landscape_image_wide_button_layout" - ); - var generalAds = new BlockRule( - SettingsEnum.ADREMOVER_GENERAL_ADS_REMOVAL, - "ads_video_with_context", - "banner_text_icon", - "square_image_layout", - "watch_metadata_app_promo", - "video_display_full_layout", - "hero_promo_image", - "statement_banner", - "carousel_footered_layout", - "text_image_button_layout", - "primetime_promo", - "product_details", - "full_width_portrait_image_layout", - "brand_video_shelf" - ); - var movieAds = new BlockRule( - SettingsEnum.ADREMOVER_MOVIE_REMOVAL, - "browsy_bar", - "compact_movie", - "horizontal_movie_shelf", - "movie_and_show_upsell_card", - "compact_tvfilm_item", - "offer_module_root" - ); - - this.pathRegister.registerAll( - generalAds, - buttonedAd, - channelBar, - communityPosts, - paidContent, - latestPosts, - movieAds, - chapterTeaser, - communityGuidelines, - quickActions, - relatedVideos, - compactBanner, - inFeedSurvey, - viewProducts, - medicalPanel, - merchandise, - infoPanel, - channelGuidelines, - audioTrackButton, - artistCard, - selfSponsor, - webLinkPanel, - imageShelf, - subscribersCommunityGuidelines, - channelMemberShelf - ); - - var carouselAd = new BlockRule(SettingsEnum.ADREMOVER_GENERAL_ADS_REMOVAL, - "carousel_ad" - ); - var shorts = new BlockRule(SettingsEnum.ADREMOVER_SHORTS_REMOVAL, - "reels_player_overlay", - "shorts_shelf", - "inline_shorts", - "shorts_grid" - ); - - this.identifierRegister.registerAll( - shorts, - graySeparator, - carouselAd - ); - } - - public boolean filter(final String path, final String identifier) { - BlockResult result; - - if (custom.isEnabled() && custom.check(path).isBlocked()) - result = BlockResult.CUSTOM; - else if (ReVancedUtils.containsAny(path, IGNORE)) - result = BlockResult.IGNORED; - else if (pathRegister.contains(path) || identifierRegister.contains(identifier)) - result = BlockResult.DEFINED; - else - result = BlockResult.UNBLOCKED; - - LogHelper.printDebug(() -> String.format("%s (ID: %s): %s", result.message, identifier, path)); - - return result.filter; - } - - private enum BlockResult { - UNBLOCKED(false, "Unblocked"), - IGNORED(false, "Ignored"), - DEFINED(true, "Blocked"), - CUSTOM(true, "Custom"); - - final Boolean filter; - final String message; - - BlockResult(boolean filter, String message) { - this.filter = filter; - this.message = message; - } - } - - /** - * Hide a view. - * - * @param condition The setting to check for hiding the view. - * @param view The view to hide. - */ - private static void hideView(SettingsEnum condition, View view) { - if (!condition.getBoolean()) return; - - LogHelper.printDebug(() -> "Hiding view with setting: " + condition); - - AdRemoverAPI.HideViewWithLayout1dp(view); - } - - /** - * Hide the view, which shows ads in the homepage. - * - * @param view The view, which shows ads. - */ - public static void hideAdAttributionView(View view) { - hideView(SettingsEnum.ADREMOVER_GENERAL_ADS_REMOVAL, view); - } - - /** - * Hide the view, which shows reels in the homepage. - * - * @param view The view, which shows reels. - */ - public static void hideReelView(View view) { - hideView(SettingsEnum.ADREMOVER_SHORTS_REMOVAL, view); - } - -} diff --git a/app/src/main/java/app/revanced/integrations/patches/HDRAutoBrightnessPatch.java b/app/src/main/java/app/revanced/integrations/patches/HDRAutoBrightnessPatch.java index 330b3cfd..86271d5d 100644 --- a/app/src/main/java/app/revanced/integrations/patches/HDRAutoBrightnessPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/HDRAutoBrightnessPatch.java @@ -21,7 +21,7 @@ public class HDRAutoBrightnessPatch { */ public static float getHDRBrightness(float original) { // do nothing if disabled - if (!SettingsEnum.USE_HDR_AUTO_BRIGHTNESS.getBoolean()) { + if (!SettingsEnum.HDR_AUTO_BRIGHTNESS.getBoolean()) { return original; } diff --git a/app/src/main/java/app/revanced/integrations/patches/HideAlbumCardsPatch.java b/app/src/main/java/app/revanced/integrations/patches/HideAlbumCardsPatch.java index 455e7741..b0f42e89 100644 --- a/app/src/main/java/app/revanced/integrations/patches/HideAlbumCardsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/HideAlbumCardsPatch.java @@ -2,13 +2,12 @@ package app.revanced.integrations.patches; import android.view.View; -import app.revanced.integrations.adremover.AdRemoverAPI; import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.ReVancedUtils; public class HideAlbumCardsPatch { - //Used by app.revanced.patches.youtube.layout.hidealbumcards.patch.HideAlbumCardsPatch - public static void hideAlbumCards(View view) { + public static void hideAlbumCard(View view) { if (!SettingsEnum.HIDE_ALBUM_CARDS.getBoolean()) return; - AdRemoverAPI.HideViewWithLayout1dp(view); + ReVancedUtils.hideViewByLayoutParams(view); } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/patches/HideBreakingNewsPatch.java b/app/src/main/java/app/revanced/integrations/patches/HideBreakingNewsPatch.java index b5efae2b..465c2836 100644 --- a/app/src/main/java/app/revanced/integrations/patches/HideBreakingNewsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/HideBreakingNewsPatch.java @@ -2,13 +2,28 @@ package app.revanced.integrations.patches; import android.view.View; -import app.revanced.integrations.adremover.AdRemoverAPI; import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.ReVancedUtils; public class HideBreakingNewsPatch { - //Used by app.revanced.patches.youtube.layout.homepage.breakingnews.patch.BreakingNewsPatch + + /** + * When spoofing to app versions older than 17.30.35, the watch history preview bar uses + * the same layout components as the breaking news shelf. + * + * Breaking news does not appear to be present in these older versions anyways. + */ + private static boolean isSpoofingOldVersionWithHorizontalCardListWatchHistory() { + return SettingsEnum.SPOOF_APP_VERSION.getBoolean() + && SettingsEnum.SPOOF_APP_VERSION_TARGET.getString().compareTo("17.30.35") < 0; + } + + /** + * Injection point. + */ public static void hideBreakingNews(View view) { - if (!SettingsEnum.HIDE_BREAKING_NEWS.getBoolean()) return; - AdRemoverAPI.HideViewWithLayout1dp(view); + if (!SettingsEnum.HIDE_BREAKING_NEWS.getBoolean() + || isSpoofingOldVersionWithHorizontalCardListWatchHistory()) return; + ReVancedUtils.hideViewByLayoutParams(view); } } diff --git a/app/src/main/java/app/revanced/integrations/patches/HideCrowdfundingBoxPatch.java b/app/src/main/java/app/revanced/integrations/patches/HideCrowdfundingBoxPatch.java index 747784e8..c8b87339 100644 --- a/app/src/main/java/app/revanced/integrations/patches/HideCrowdfundingBoxPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/HideCrowdfundingBoxPatch.java @@ -2,13 +2,13 @@ package app.revanced.integrations.patches; import android.view.View; -import app.revanced.integrations.adremover.AdRemoverAPI; import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.ReVancedUtils; public class HideCrowdfundingBoxPatch { //Used by app.revanced.patches.youtube.layout.hidecrowdfundingbox.patch.HideCrowdfundingBoxPatch public static void hideCrowdfundingBox(View view) { if (!SettingsEnum.HIDE_CROWDFUNDING_BOX.getBoolean()) return; - AdRemoverAPI.HideViewWithLayout1dp(view); + ReVancedUtils.hideViewByLayoutParams(view); } } diff --git a/app/src/main/java/app/revanced/integrations/patches/HideFilterBarPatch.java b/app/src/main/java/app/revanced/integrations/patches/HideFilterBarPatch.java new file mode 100644 index 00000000..24d117dc --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/HideFilterBarPatch.java @@ -0,0 +1,25 @@ +package app.revanced.integrations.patches; + +import android.view.View; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.ReVancedUtils; + +public final class HideFilterBarPatch { + public static int hideInFeed(final int height) { + if (SettingsEnum.HIDE_FILTER_BAR_FEED_IN_FEED.getBoolean()) return 0; + + return height; + } + + public static void hideInRelatedVideos(final View chipView) { + if (!SettingsEnum.HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS.getBoolean()) return; + + ReVancedUtils.hideViewByLayoutParams(chipView); + } + + public static int hideInSearch(final int height) { + if (SettingsEnum.HIDE_FILTER_BAR_FEED_IN_SEARCH.getBoolean()) return 0; + + return height; + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/HideLoadMoreButtonPatch.java b/app/src/main/java/app/revanced/integrations/patches/HideLoadMoreButtonPatch.java new file mode 100644 index 00000000..ab77cf2a --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/HideLoadMoreButtonPatch.java @@ -0,0 +1,13 @@ +package app.revanced.integrations.patches; + +import android.view.View; + +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.ReVancedUtils; + +public class HideLoadMoreButtonPatch { + public static void hideLoadMoreButton(View view){ + if(!SettingsEnum.HIDE_LOAD_MORE_BUTTON.getBoolean()) return; + ReVancedUtils.hideViewByLayoutParams(view); + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/HidePlayerOverlayPatch.java b/app/src/main/java/app/revanced/integrations/patches/HidePlayerOverlayPatch.java new file mode 100644 index 00000000..e734fb24 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/HidePlayerOverlayPatch.java @@ -0,0 +1,12 @@ +package app.revanced.integrations.patches; + +import android.widget.ImageView; + +import app.revanced.integrations.settings.SettingsEnum; + +public class HidePlayerOverlayPatch { + public static void hidePlayerOverlay(ImageView view) { + if (!SettingsEnum.HIDE_PLAYER_OVERLAY.getBoolean()) return; + view.setImageResource(android.R.color.transparent); + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/HideShortsCommentsButtonPatch.java b/app/src/main/java/app/revanced/integrations/patches/HideShortsCommentsButtonPatch.java deleted file mode 100644 index 597212ad..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/HideShortsCommentsButtonPatch.java +++ /dev/null @@ -1,13 +0,0 @@ -package app.revanced.integrations.patches; - -import android.view.View; - -import app.revanced.integrations.settings.SettingsEnum; - -public class HideShortsCommentsButtonPatch { - //Used by app.revanced.patches.youtube.layout.comments.patch.CommentsPatch - public static void hideShortsCommentsButton(View view) { - if (!SettingsEnum.HIDE_SHORTS_COMMENTS_BUTTON.getBoolean()) return; - view.setVisibility(View.GONE); - } -} diff --git a/app/src/main/java/app/revanced/integrations/patches/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/patches/LithoFilterPatch.java deleted file mode 100644 index 2d8dafb3..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/LithoFilterPatch.java +++ /dev/null @@ -1,140 +0,0 @@ -package app.revanced.integrations.patches; - -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.Spliterator; -import java.util.function.Consumer; - -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; - -class BlockRule { - final static class BlockResult { - private final boolean blocked; - private final SettingsEnum setting; - - public BlockResult(final SettingsEnum setting, final boolean blocked) { - this.setting = setting; - this.blocked = blocked; - } - - public SettingsEnum getSetting() { - return setting; - } - - public boolean isBlocked() { - return blocked; - } - } - - protected final SettingsEnum setting; - private final String[] blocks; - - /** - * Initialize a new rule for components. - * - * @param setting The setting which controls the blocking of this component. - * @param blocks The rules to block the component on. - */ - public BlockRule(final SettingsEnum setting, final String... blocks) { - this.setting = setting; - this.blocks = blocks; - } - - public boolean isEnabled() { - return setting.getBoolean(); - } - - public BlockResult check(final String string) { - return new BlockResult(setting, string != null && ReVancedUtils.containsAny(string, blocks)); - } -} - -final class CustomBlockRule extends BlockRule { - /** - * Initialize a new rule for components. - * - * @param setting The setting which controls the blocking of the components. - * @param filter The setting which contains the list of component names. - */ - public CustomBlockRule(final SettingsEnum setting, final SettingsEnum filter) { - super(setting, filter.getString().split(",")); - } -} - - -abstract class Filter { - final protected LithoBlockRegister pathRegister = new LithoBlockRegister(); - final protected LithoBlockRegister identifierRegister = new LithoBlockRegister(); - - abstract boolean filter(final String path, final String identifier); -} - -final class LithoBlockRegister implements Iterable { - private final ArrayList blocks = new ArrayList<>(); - - public void registerAll(BlockRule... blocks) { - this.blocks.addAll(Arrays.asList(blocks)); - } - - @NonNull - @Override - public Iterator iterator() { - return blocks.iterator(); - } - - @RequiresApi(api = Build.VERSION_CODES.N) - @Override - public void forEach(@NonNull Consumer action) { - blocks.forEach(action); - } - - @RequiresApi(api = Build.VERSION_CODES.N) - @NonNull - @Override - public Spliterator spliterator() { - return blocks.spliterator(); - } - - public boolean contains(String path) { - for (var rule : this) { - if (!rule.isEnabled()) continue; - - var result = rule.check(path); - if (result.isBlocked()) { - return true; - } - } - - return false; - } -} - -public final class LithoFilterPatch { - private static final Filter[] filters = new Filter[]{ - new GeneralAdsPatch(), - new ButtonsPatch(), - new CommentsPatch(), - }; - - public static boolean filter(final StringBuilder pathBuilder, final String identifier) { - var path = pathBuilder.toString(); - if (path.isEmpty()) return false; - - LogHelper.printDebug(() -> String.format("Searching (ID: %s): %s", identifier, path)); - - for (var filter : filters) { - if (filter.filter(path, identifier)) return true; - } - - return false; - } -} - diff --git a/app/src/main/java/app/revanced/integrations/patches/OpenLinksExternallyPatch.java b/app/src/main/java/app/revanced/integrations/patches/OpenLinksExternallyPatch.java index 4f5ce40b..1640c7bf 100644 --- a/app/src/main/java/app/revanced/integrations/patches/OpenLinksExternallyPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/OpenLinksExternallyPatch.java @@ -13,7 +13,7 @@ public class OpenLinksExternallyPatch { * @return The new, default service to open links with or the original service. */ public static String enableExternalBrowser(String original) { - if (SettingsEnum.ENABLE_EXTERNAL_BROWSER.getBoolean()) original = ""; + if (SettingsEnum.EXTERNAL_BROWSER.getBoolean()) original = ""; return original; } } diff --git a/app/src/main/java/app/revanced/integrations/patches/PlayerOverlaysHookPatch.java b/app/src/main/java/app/revanced/integrations/patches/PlayerOverlaysHookPatch.java index 95b30835..4f1969df 100644 --- a/app/src/main/java/app/revanced/integrations/patches/PlayerOverlaysHookPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/PlayerOverlaysHookPatch.java @@ -15,7 +15,7 @@ import app.revanced.integrations.shared.PlayerOverlays; @SuppressWarnings("unused") public class PlayerOverlaysHookPatch { /** - * Hook into YouTubePlayerOverlaysLayout.onFinishInflate() method + * Injection point. * * @param thisRef reference to the view * @smali YouTubePlayerOverlaysLayout_onFinishInflateHook(Ljava / lang / Object ;)V diff --git a/app/src/main/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java b/app/src/main/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java index 2f46f008..49296e5c 100644 --- a/app/src/main/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java @@ -3,32 +3,25 @@ package app.revanced.integrations.patches; import androidx.annotation.Nullable; import app.revanced.integrations.shared.PlayerType; -import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.shared.VideoState; -/** - * Hook receiver class for 'player-type-hook' patch - * - * @usedBy app.revanced.patches.youtube.misc.playertype.patch.PlayerTypeHookPatch - * @smali Lapp/revanced/integrations/patches/PlayerTypeHookPatch; - */ @SuppressWarnings("unused") public class PlayerTypeHookPatch { /** - * Hook into YouTubePlayerOverlaysLayout.updatePlayerLayout() method - * - * @param type the new player type - * @smali YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(Ljava/lang/Object;)V + * Injection point. */ - public static void YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(@Nullable Object type) { - if (type == null) return; + public static void setPlayerType(@Nullable Enum youTubePlayerType) { + if (youTubePlayerType == null) return; - // update current player type - final PlayerType newType = PlayerType.safeParseFromString(type.toString()); - if (newType == null) { - LogHelper.printException(() -> "Unknown PlayerType encountered: " + type); - } else { - PlayerType.setCurrent(newType); - LogHelper.printDebug(() -> "PlayerType was updated to: " + newType); - } + PlayerType.setFromString(youTubePlayerType.name()); + } + + /** + * Injection point. + */ + public static void setVideoState(@Nullable Enum youTubeVideoState) { + if (youTubeVideoState == null) return; + + VideoState.setFromString(youTubeVideoState.name()); } } diff --git a/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java b/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java index 93f0020d..21014e7f 100644 --- a/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java @@ -2,26 +2,42 @@ package app.revanced.integrations.patches; import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote; +import android.graphics.Rect; +import android.os.Build; import android.text.Editable; import android.text.Spannable; +import android.text.SpannableString; import android.text.Spanned; import android.text.TextWatcher; +import android.view.View; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.shared.PlayerType; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; +/** + * Handles all interaction of UI patch components. + * + * Does not handle creating dislike spans or anything to do with {@link ReturnYouTubeDislikeApi}. + */ public class ReturnYouTubeDislikePatch { + @Nullable + private static String currentVideoId; + + /** * Resource identifier of old UI dislike button. */ @@ -60,7 +76,7 @@ public class ReturnYouTubeDislikePatch { if (oldUIReplacementSpan == null || oldUIReplacementSpan.toString().equals(s.toString())) { return; } - s.replace(0, s.length(), oldUIReplacementSpan); + s.replace(0, s.length(), oldUIReplacementSpan); // Causes a recursive call back into this listener } }; @@ -80,12 +96,15 @@ public class ReturnYouTubeDislikePatch { * * Used when spoofing the older app versions of {@link SpoofAppVersionPatch}. */ - public static void setOldUILayoutDislikes(int buttonViewResourceId, @NonNull TextView textView) { + public static void setOldUILayoutDislikes(int buttonViewResourceId, @Nullable TextView textView) { try { if (!SettingsEnum.RYD_ENABLED.getBoolean() - || buttonViewResourceId != OLD_UI_DISLIKE_BUTTON_RESOURCE_ID) { + || buttonViewResourceId != OLD_UI_DISLIKE_BUTTON_RESOURCE_ID + || textView == null) { return; } + LogHelper.printDebug(() -> "setOldUILayoutDislikes"); + if (oldUIOriginalSpan == null) { // Use value of the first instance, as it appears TextViews can be recycled // and might contain dislikes previously added by the patch. @@ -96,23 +115,19 @@ public class ReturnYouTubeDislikePatch { textView.removeTextChangedListener(oldUiTextWatcher); textView.addTextChangedListener(oldUiTextWatcher); + /** + * If the patch is changed to include the dislikes button as a parameter to this method, + * then if the button is already selected the dislikes could be adjusted using + * {@link ReturnYouTubeDislike#setUserVote(Vote)} + */ + updateOldUIDislikesTextView(); + } catch (Exception ex) { LogHelper.printException(() -> "setOldUILayoutDislikes failure", ex); } } - /** - * Injection point. - */ - public static void newVideoLoaded(@NonNull String videoId) { - try { - if (!SettingsEnum.RYD_ENABLED.getBoolean()) return; - ReturnYouTubeDislike.newVideoLoaded(videoId); - } catch (Exception ex) { - LogHelper.printException(() -> "newVideoLoaded failure", ex); - } - } /** * Injection point. @@ -157,21 +172,153 @@ public class ReturnYouTubeDislikePatch { return original; } + + /** + * Replacement text to use for "Dislikes" while RYD is fetching. + */ + private static final Spannable SHORTS_LOADING_SPAN = new SpannableString("-"); + + /** + * Dislikes TextViews used by Shorts. + * + * Multiple TextViews are loaded at once (for the prior and next videos to swipe to). + * Keep track of all of them, and later pick out the correct one based on their on screen position. + */ + private static final List> shortsTextViewRefs = new ArrayList<>(); + + private static void clearRemovedShortsTextViews() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + shortsTextViewRefs.removeIf(ref -> ref.get() == null); + return; + } + throw new IllegalStateException(); // YouTube requires Android N or greater + } + + /** + * Injection point. Called when a Shorts dislike is updated. + * Handles update asynchronously, otherwise Shorts video will be frozen while the UI thread is blocked. + * + * @return if RYD is enabled and the TextView was updated + */ + public static boolean setShortsDislikes(@NonNull View likeDislikeView) { + try { + if (!SettingsEnum.RYD_ENABLED.getBoolean() || !SettingsEnum.RYD_SHORTS.getBoolean()) { + return false; + } + LogHelper.printDebug(() -> "setShortsDislikes"); + + TextView textView = (TextView) likeDislikeView; + textView.setText(SHORTS_LOADING_SPAN); // Change 'Dislike' text to the loading text + shortsTextViewRefs.add(new WeakReference<>(textView)); + + if (likeDislikeView.isSelected() && isShortTextViewOnScreen(textView)) { + LogHelper.printDebug(() -> "Shorts dislike is already selected"); + ReturnYouTubeDislike.setUserVote(Vote.DISLIKE); + } + + // For the first short played, the shorts dislike hook is called after the video id hook. + // But for most other times this hook is called before the video id (which is not ideal). + // Must update the TextViews here, and also after the videoId changes. + updateOnScreenShortsTextViews(false); + + return true; + } catch (Exception ex) { + LogHelper.printException(() -> "setShortsDislikes failure", ex); + return false; + } + } + + /** + * @param forceUpdate if false, then only update the 'loading text views. + * If true, update all on screen text views. + */ + private static void updateOnScreenShortsTextViews(boolean forceUpdate) { + try { + clearRemovedShortsTextViews(); + if (shortsTextViewRefs.isEmpty()) { + return; + } + + LogHelper.printDebug(() -> "updateShortsTextViews"); + String videoId = VideoInformation.getVideoId(); + + Runnable update = () -> { + Spanned shortsDislikesSpan = ReturnYouTubeDislike.getDislikeSpanForShort(SHORTS_LOADING_SPAN); + ReVancedUtils.runOnMainThreadNowOrLater(() -> { + if (!videoId.equals(VideoInformation.getVideoId())) { + // User swiped to new video before fetch completed + LogHelper.printDebug(() -> "Ignoring stale dislikes data for short: " + videoId); + return; + } + + // Update text views that appear to be visible on screen. + // Only 1 will be the actual textview for the current Short, + // but discarded and not yet garbage collected views can remain. + // So must set the dislike span on all views that match. + for (WeakReference textViewRef : shortsTextViewRefs) { + TextView textView = textViewRef.get(); + if (textView == null) { + continue; + } + if (isShortTextViewOnScreen(textView) + && (forceUpdate || textView.getText().toString().equals(SHORTS_LOADING_SPAN.toString()))) { + LogHelper.printDebug(() -> "Setting Shorts TextView to: " + shortsDislikesSpan); + textView.setText(shortsDislikesSpan); + } + } + }); + }; + if (ReturnYouTubeDislike.fetchCompleted()) { + update.run(); // Network call is completed, no need to wait on background thread. + } else { + ReVancedUtils.runOnBackgroundThread(update); + } + } catch (Exception ex) { + LogHelper.printException(() -> "updateVisibleShortsTextViews failure", ex); + } + } + + /** + * Check if a view is within the screen bounds. + */ + private static boolean isShortTextViewOnScreen(@NonNull View view) { + final int[] location = new int[2]; + view.getLocationInWindow(location); + if (location[0] <= 0 && location[1] <= 0) { // Lower bound + return false; + } + Rect windowRect = new Rect(); + view.getWindowVisibleDisplayFrame(windowRect); // Upper bound + return location[0] < windowRect.width() && location[1] < windowRect.height(); + } + /** * Injection point. - * - * Called when a Shorts dislike Spanned is created. */ - public static Spanned onShortsComponentCreated(Spanned original) { + public static void newVideoLoaded(@NonNull String videoId) { try { - if (!SettingsEnum.RYD_ENABLED.getBoolean()) { - return original; + if (!SettingsEnum.RYD_ENABLED.getBoolean()) return; + + if (!videoId.equals(currentVideoId)) { + currentVideoId = videoId; + + final boolean noneHiddenOrMinimized = PlayerType.getCurrent().isNoneHiddenOrMinimized(); + if (noneHiddenOrMinimized && !SettingsEnum.RYD_SHORTS.getBoolean()) { + ReturnYouTubeDislike.setCurrentVideoId(null); + return; + } + + ReturnYouTubeDislike.newVideoLoaded(videoId); + + if (noneHiddenOrMinimized) { + // Shorts TextView hook can be called out of order with the video id hook. + // Must manually update again here. + updateOnScreenShortsTextViews(true); + } } - return ReturnYouTubeDislike.getDislikeSpanForShort(original); } catch (Exception ex) { - LogHelper.printException(() -> "onShortsComponentCreated failure", ex); + LogHelper.printException(() -> "newVideoLoaded failure", ex); } - return original; } /** @@ -186,10 +333,14 @@ public class ReturnYouTubeDislikePatch { if (!SettingsEnum.RYD_ENABLED.getBoolean()) { return; } + if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrMinimized()) { + return; + } for (Vote v : Vote.values()) { if (v.value == vote) { ReturnYouTubeDislike.sendVote(v); + updateOldUIDislikesTextView(); return; } diff --git a/app/src/main/java/app/revanced/integrations/patches/SeekbarTappingPatch.java b/app/src/main/java/app/revanced/integrations/patches/SeekbarTappingPatch.java index 062cfd11..3ac73c04 100644 --- a/app/src/main/java/app/revanced/integrations/patches/SeekbarTappingPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/SeekbarTappingPatch.java @@ -2,11 +2,8 @@ package app.revanced.integrations.patches; import app.revanced.integrations.settings.SettingsEnum; -public class SeekbarTappingPatch { - - //Used by app.revanced.patches.youtube.interaction.seekbar.patch.EnableSeekbarTappingPatch - public static boolean isTapSeekingEnabled() { - return SettingsEnum.TAP_SEEKING_ENABLED.getBoolean(); +public final class SeekbarTappingPatch { + public static boolean seekbarTappingEnabled() { + return SettingsEnum.SEEKBAR_TAPPING.getBoolean(); } - } diff --git a/app/src/main/java/app/revanced/integrations/patches/SpoofSignatureVerificationPatch.java b/app/src/main/java/app/revanced/integrations/patches/SpoofSignatureVerificationPatch.java index 95b948d5..874781f5 100644 --- a/app/src/main/java/app/revanced/integrations/patches/SpoofSignatureVerificationPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/SpoofSignatureVerificationPatch.java @@ -60,7 +60,7 @@ public class SpoofSignatureVerificationPatch { */ public static String overrideProtobufParameter(String originalValue) { try { - if (!SettingsEnum.SIGNATURE_SPOOFING.getBoolean()) { + if (!SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.getBoolean()) { return originalValue; } @@ -101,11 +101,11 @@ public class SpoofSignatureVerificationPatch { } LogHelper.printDebug(() -> "YouTube HTTP status code: " + responseCode); - if (SettingsEnum.SIGNATURE_SPOOFING.getBoolean()) { + if (SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.getBoolean()) { return; // already enabled } - SettingsEnum.SIGNATURE_SPOOFING.saveValue(true); + SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.saveValue(true); ReVancedUtils.showToastLong("Spoofing app signature to prevent playback issues"); // it would be great if the video could be forcefully reloaded, but currently there is no code to do this @@ -130,7 +130,7 @@ public class SpoofSignatureVerificationPatch { * @param sd function is not entirely clear */ public static int[] getSubtitleWindowSettingsOverride(int ap, int ah, int av, boolean vs, boolean sd) { - final boolean signatureSpoofing = SettingsEnum.SIGNATURE_SPOOFING.getBoolean(); + final boolean signatureSpoofing = SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.getBoolean(); if (SettingsEnum.DEBUG.getBoolean()) { if (ap != lastAp || ah != lastAh || av != lastAv || vs != lastVs || sd != lastSd) { LogHelper.printDebug(() -> "video: " + VideoInformation.getVideoId() + " spoof: " + signatureSpoofing diff --git a/app/src/main/java/app/revanced/integrations/patches/VideoAdsPatch.java b/app/src/main/java/app/revanced/integrations/patches/VideoAdsPatch.java index 076fcae7..487817fd 100644 --- a/app/src/main/java/app/revanced/integrations/patches/VideoAdsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/VideoAdsPatch.java @@ -7,8 +7,7 @@ public class VideoAdsPatch { // Used by app.revanced.patches.youtube.ad.general.video.patch.VideoAdsPatch // depends on Whitelist patch (still needs to be written) public static boolean shouldShowAds() { - return !SettingsEnum.VIDEO_ADS_REMOVAL.getBoolean(); // TODO && Whitelist.shouldShowAds(); - + return !SettingsEnum.HIDE_VIDEO_ADS.getBoolean(); // TODO && Whitelist.shouldShowAds(); } } diff --git a/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java b/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java index 13149541..2a321953 100644 --- a/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java +++ b/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java @@ -4,8 +4,10 @@ import androidx.annotation.NonNull; import java.lang.ref.WeakReference; import java.lang.reflect.Method; +import java.util.Objects; import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch; +import app.revanced.integrations.shared.VideoState; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; @@ -16,7 +18,7 @@ public final class VideoInformation { private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; private static final String SEEK_METHOD_NAME = "seekTo"; - private static WeakReference playerController; + private static WeakReference playerControllerRef; private static Method seekMethod; @NonNull @@ -30,17 +32,17 @@ public final class VideoInformation { /** * Injection point. - * Sets a reference to the YouTube playback controller. * - * @param thisRef Reference to the player controller object. + * @param playerController player controller object. */ - public static void playerController_onCreateHook(final Object thisRef) { - playerController = new WeakReference<>(thisRef); - videoLength = 0; - videoTime = -1; - + public static void initialize(@NonNull Object playerController) { try { - seekMethod = thisRef.getClass().getMethod(SEEK_METHOD_NAME, Long.TYPE); + playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController)); + videoTime = -1; + videoLength = 0; + playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED; + + seekMethod = playerController.getClass().getMethod(SEEK_METHOD_NAME, Long.TYPE); seekMethod.setAccessible(true); } catch (Exception ex) { LogHelper.printException(() -> "Failed to initialize", ex); @@ -56,7 +58,6 @@ public final class VideoInformation { if (!videoId.equals(newlyLoadedVideoId)) { LogHelper.printDebug(() -> "New video id: " + newlyLoadedVideoId); videoId = newlyLoadedVideoId; - playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED; } } @@ -124,7 +125,7 @@ public final class VideoInformation { try { LogHelper.printDebug(() -> "Seeking to " + millisecond); - return (Boolean) seekMethod.invoke(playerController.get(), millisecond); + return (Boolean) seekMethod.invoke(playerControllerRef.get(), millisecond); } catch (Exception ex) { LogHelper.printException(() -> "Failed to seek", ex); return false; @@ -183,7 +184,12 @@ public final class VideoInformation { * @return If the playback is at the end of the video. * * If video is playing in the background with no video visible, - * this always returns false (even if the video is actually at the end) + * this always returns false (even if the video is actually at the end). + * + * This is equivalent to checking for {@link VideoState#ENDED}, + * but can give a more up to date result for code calling from some hooks. + * + * @see VideoState */ public static boolean isAtEndOfVideo() { return videoTime > 0 && videoLength > 0 && videoTime >= videoLength; diff --git a/app/src/main/java/app/revanced/integrations/patches/components/AdsFilter.java b/app/src/main/java/app/revanced/integrations/patches/components/AdsFilter.java new file mode 100644 index 00000000..527a3a28 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/components/AdsFilter.java @@ -0,0 +1,265 @@ +package app.revanced.integrations.patches.components; + + +import android.view.View; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + + +public final class AdsFilter extends Filter { + private final String[] exceptions; + + private final CustomFilterGroup custom; + + public AdsFilter() { + exceptions = new String[]{ + "home_video_with_context", + "related_video_with_context", + "comment_thread", // skip filtering anything in the comments + "|comment.", // skip filtering anything in the comments replies + "library_recent_shelf", + }; + + custom = new CustomFilterGroup( + SettingsEnum.CUSTOM_FILTER, + SettingsEnum.CUSTOM_FILTER_STRINGS + ); + + final var communityPosts = new StringFilterGroup( + SettingsEnum.HIDE_COMMUNITY_POSTS, + "post_base_wrapper" + ); + + final var communityGuidelines = new StringFilterGroup( + SettingsEnum.HIDE_COMMUNITY_GUIDELINES, + "community_guidelines" + ); + + final var subscribersCommunityGuidelines = new StringFilterGroup( + SettingsEnum.HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES, + "sponsorships_comments_upsell" + ); + + + final var channelMemberShelf = new StringFilterGroup( + SettingsEnum.HIDE_CHANNEL_MEMBER_SHELF, + "member_recognition_shelf" + ); + + final var compactBanner = new StringFilterGroup( + SettingsEnum.HIDE_COMPACT_BANNER, + "compact_banner" + ); + + final var inFeedSurvey = new StringFilterGroup( + SettingsEnum.HIDE_FEED_SURVEY, + "in_feed_survey", + "slimline_survey" + ); + + final var medicalPanel = new StringFilterGroup( + SettingsEnum.HIDE_MEDICAL_PANELS, + "medical_panel" + ); + + final var paidContent = new StringFilterGroup( + SettingsEnum.HIDE_PAID_CONTENT, + "paid_content_overlay" + ); + + final var merchandise = new StringFilterGroup( + SettingsEnum.HIDE_MERCHANDISE_BANNERS, + "product_carousel" + ); + + final var infoPanel = new StringFilterGroup( + SettingsEnum.HIDE_HIDE_INFO_PANELS, + "publisher_transparency_panel", + "single_item_information_panel" + ); + + final var latestPosts = new StringFilterGroup( + SettingsEnum.HIDE_HIDE_LATEST_POSTS, + "post_shelf" + ); + + final var channelGuidelines = new StringFilterGroup( + SettingsEnum.HIDE_HIDE_CHANNEL_GUIDELINES, + "channel_guidelines_entry_banner" + ); + + final var audioTrackButton = new StringFilterGroup( + SettingsEnum.HIDE_AUDIO_TRACK_BUTTON, + "multi_feed_icon_button" + ); + + final var artistCard = new StringFilterGroup( + SettingsEnum.HIDE_ARTIST_CARDS, + "official_card" + ); + + final var selfSponsor = new StringFilterGroup( + SettingsEnum.HIDE_SELF_SPONSOR, + "cta_shelf_card" + ); + + final var chapterTeaser = new StringFilterGroup( + SettingsEnum.HIDE_CHAPTER_TEASER, + "expandable_metadata", + "macro_markers_carousel" + ); + + final var viewProducts = new StringFilterGroup( + SettingsEnum.HIDE_PRODUCTS_BANNER, + "product_item", + "products_in_video" + ); + + final var webLinkPanel = new StringFilterGroup( + SettingsEnum.HIDE_WEB_SEARCH_RESULTS, + "web_link_panel" + ); + + final var channelBar = new StringFilterGroup( + SettingsEnum.HIDE_CHANNEL_BAR, + "channel_bar" + ); + + final var relatedVideos = new StringFilterGroup( + SettingsEnum.HIDE_RELATED_VIDEOS, + "fullscreen_related_videos" + ); + + final var quickActions = new StringFilterGroup( + SettingsEnum.HIDE_QUICK_ACTIONS, + "quick_actions" + ); + + final var imageShelf = new StringFilterGroup( + SettingsEnum.HIDE_IMAGE_SHELF, + "image_shelf" + ); + + final var graySeparator = new StringFilterGroup( + SettingsEnum.HIDE_GRAY_SEPARATOR, + "cell_divider" // layout residue (gray line above the buttoned ad), + ); + + final var buttonedAd = new StringFilterGroup( + SettingsEnum.HIDE_BUTTONED_ADS, + "_buttoned_layout", + "full_width_square_image_layout", + "_ad_with", + "video_display_button_group_layout", + "landscape_image_wide_button_layout" + ); + + final var generalAds = new StringFilterGroup( + SettingsEnum.HIDE_GENERAL_ADS, + "ads_video_with_context", + "banner_text_icon", + "square_image_layout", + "watch_metadata_app_promo", + "video_display_full_layout", + "hero_promo_image", + "statement_banner", + "carousel_footered_layout", + "text_image_button_layout", + "primetime_promo", + "product_details", + "full_width_portrait_image_layout", + "brand_video_shelf" + ); + + final var movieAds = new StringFilterGroup( + SettingsEnum.HIDE_MOVIES_SECTION, + "browsy_bar", + "compact_movie", + "horizontal_movie_shelf", + "movie_and_show_upsell_card", + "compact_tvfilm_item", + "offer_module_root" + ); + + this.pathFilterGroups.addAll( + generalAds, + buttonedAd, + channelBar, + communityPosts, + paidContent, + latestPosts, + movieAds, + chapterTeaser, + communityGuidelines, + quickActions, + relatedVideos, + compactBanner, + inFeedSurvey, + viewProducts, + medicalPanel, + merchandise, + infoPanel, + channelGuidelines, + audioTrackButton, + artistCard, + selfSponsor, + webLinkPanel, + imageShelf, + subscribersCommunityGuidelines, + channelMemberShelf + ); + + final var carouselAd = new StringFilterGroup( + SettingsEnum.HIDE_GENERAL_ADS, + "carousel_ad" + ); + + this.identifierFilterGroups.addAll( + graySeparator, + carouselAd + ); + } + + @Override + public boolean isFiltered(final String path, final String identifier, final byte[] _protobufBufferArray) { + FilterResult result; + + if (custom.isEnabled() && custom.check(path).isFiltered()) + result = FilterResult.CUSTOM; + else if (ReVancedUtils.containsAny(path, exceptions)) + result = FilterResult.EXCEPTION; + else if (pathFilterGroups.contains(path) || identifierFilterGroups.contains(identifier)) + result = FilterResult.FILTERED; + else + result = FilterResult.UNFILTERED; + + LogHelper.printDebug(() -> String.format("%s (ID: %s): %s", result.message, identifier, path)); + + return result.filter; + } + + private enum FilterResult { + UNFILTERED(false, "Unfiltered"), + EXCEPTION(false, "Exception"), + FILTERED(true, "Filtered"), + CUSTOM(true, "Custom"); + + final Boolean filter; + final String message; + + FilterResult(boolean filter, String message) { + this.filter = filter; + this.message = message; + } + } + + /** + * Hide the view, which shows ads in the homepage. + * + * @param view The view, which shows ads. + */ + public static void hideAdAttributionView(View view) { + ReVancedUtils.hideViewBy1dpUnderCondition(SettingsEnum.HIDE_GENERAL_ADS, view); + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/components/ButtonsFilter.java b/app/src/main/java/app/revanced/integrations/patches/components/ButtonsFilter.java new file mode 100644 index 00000000..42a4bdf2 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/components/ButtonsFilter.java @@ -0,0 +1,54 @@ +package app.revanced.integrations.patches.components; + +import app.revanced.integrations.settings.SettingsEnum; + +final class ButtonsFilter extends Filter { + private final StringFilterGroup actionBarRule; + + public ButtonsFilter() { + actionBarRule = new StringFilterGroup( + null, + "video_action_bar" + ); + + pathFilterGroups.addAll( + new StringFilterGroup( + SettingsEnum.HIDE_LIKE_DISLIKE_BUTTON, + "|like_button", + "dislike_button" + ), + new StringFilterGroup( + SettingsEnum.HIDE_DOWNLOAD_BUTTON, + "download_button" + ), + new StringFilterGroup( + SettingsEnum.HIDE_PLAYLIST_BUTTON, + "save_to_playlist_button" + ), + new StringFilterGroup( + SettingsEnum.HIDE_CLIP_BUTTON, + "|clip_button.eml|" + ), + new StringFilterGroup( + SettingsEnum.HIDE_ACTION_BUTTONS, + "ContainerType|video_action_button", + "|CellType|CollectionType|CellType|ContainerType|button.eml|" + ) + ); + } + + private boolean isEveryFilterGroupEnabled() { + for (StringFilterGroup rule : pathFilterGroups) + if (!rule.isEnabled()) return false; + + return true; + } + + @Override + public boolean isFiltered(final String path, final String identifier, final byte[] _protobufBufferArray) { + if (isEveryFilterGroupEnabled()) + if (actionBarRule.check(identifier).isFiltered()) return true; + + return super.isFiltered(path, identifier, _protobufBufferArray); + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/components/CommentsFilter.java b/app/src/main/java/app/revanced/integrations/patches/components/CommentsFilter.java new file mode 100644 index 00000000..22a9cba6 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/components/CommentsFilter.java @@ -0,0 +1,26 @@ +package app.revanced.integrations.patches.components; + +import app.revanced.integrations.settings.SettingsEnum; + +final class CommentsFilter extends Filter { + + public CommentsFilter() { + var comments = new StringFilterGroup( + SettingsEnum.HIDE_COMMENTS_SECTION, + "video_metadata_carousel", + "_comments" + ); + + var previewComment = new StringFilterGroup( + SettingsEnum.HIDE_PREVIEW_COMMENT, + "|carousel_item", + "comments_entry_point_teaser", + "comments_entry_point_simplebox" + ); + + this.pathFilterGroups.addAll( + comments, + previewComment + ); + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java new file mode 100644 index 00000000..e6798766 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java @@ -0,0 +1,263 @@ +package app.revanced.integrations.patches.components; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.function.Consumer; + +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + +abstract class FilterGroup { + final static class FilterGroupResult { + private final boolean filtered; + private final SettingsEnum setting; + + public FilterGroupResult(final SettingsEnum setting, final boolean filtered) { + this.setting = setting; + this.filtered = filtered; + } + + public SettingsEnum getSetting() { + return setting; + } + + public boolean isFiltered() { + return filtered; + } + } + + protected final SettingsEnum setting; + protected final T[] filters; + + /** + * Initialize a new filter group. + * + * @param setting The associated setting. + * @param filters The filters. + */ + @SafeVarargs + public FilterGroup(final SettingsEnum setting, final T... filters) { + this.setting = setting; + this.filters = filters; + } + + public boolean isEnabled() { + return setting.getBoolean(); + } + + public abstract FilterGroupResult check(final T stack); +} + +class StringFilterGroup extends FilterGroup { + + /** + * {@link FilterGroup#FilterGroup(SettingsEnum, Object[])} + */ + public StringFilterGroup(final SettingsEnum setting, final String... filters) { + super(setting, filters); + } + + @Override + public FilterGroupResult check(final String string) { + return new FilterGroupResult(setting, string != null && ReVancedUtils.containsAny(string, filters)); + } +} + +final class CustomFilterGroup extends StringFilterGroup { + + /** + * {@link FilterGroup#FilterGroup(SettingsEnum, Object[])} + */ + public CustomFilterGroup(final SettingsEnum setting, final SettingsEnum filter) { + super(setting, filter.getString().split(",")); + } +} + +class ByteArrayFilterGroup extends FilterGroup { + // Modified implementation from https://stackoverflow.com/a/1507813 + private int indexOf(final byte[] data, final byte[] pattern) { + // Computes the failure function using a boot-strapping process, + // where the pattern is matched against itself. + + final int[] failure = new int[pattern.length]; + + int j = 0; + for (int i = 1; i < pattern.length; i++) { + while (j > 0 && pattern[j] != pattern[i]) { + j = failure[j - 1]; + } + if (pattern[j] == pattern[i]) { + j++; + } + failure[i] = j; + } + + // Finds the first occurrence of the pattern in the byte array using + // KMP matching algorithm. + + j = 0; + if (data.length == 0) return -1; + + for (int i = 0; i < data.length; i++) { + while (j > 0 && pattern[j] != data[i]) { + j = failure[j - 1]; + } + if (pattern[j] == data[i]) { + j++; + } + if (j == pattern.length) { + return i - pattern.length + 1; + } + } + return -1; + } + + /** + * {@link FilterGroup#FilterGroup(SettingsEnum, Object[])} + */ + public ByteArrayFilterGroup(final SettingsEnum setting, final byte[]... filters) { + super(setting, filters); + } + + @Override + public FilterGroupResult check(final byte[] bytes) { + var matched = false; + for (byte[] filter : filters) { + if (indexOf(bytes, filter) == -1) continue; + + matched = true; + break; + } + + final var filtered = matched; + return new FilterGroupResult(setting, filtered); + } +} + +final class ByteArrayAsStringFilterGroup extends ByteArrayFilterGroup { + + /** + * {@link ByteArrayFilterGroup#ByteArrayFilterGroup(SettingsEnum, byte[]...)} + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public ByteArrayAsStringFilterGroup(SettingsEnum setting, String... filters) { + super(setting, Arrays.stream(filters).map(String::getBytes).toArray(byte[][]::new)); + } +} + +abstract class FilterGroupList> implements Iterable { + private final ArrayList filterGroups = new ArrayList<>(); + + @SafeVarargs + protected final void addAll(final T... filterGroups) { + this.filterGroups.addAll(Arrays.asList(filterGroups)); + } + + @NonNull + @Override + public Iterator iterator() { + return filterGroups.iterator(); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public void forEach(@NonNull Consumer action) { + filterGroups.forEach(action); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @NonNull + @Override + public Spliterator spliterator() { + return filterGroups.spliterator(); + } + + protected boolean contains(final V stack) { + for (T filterGroup : this) { + if (!filterGroup.isEnabled()) continue; + + var result = filterGroup.check(stack); + if (result.isFiltered()) { + return true; + } + } + + return false; + } +} + +final class StringFilterGroupList extends FilterGroupList { +} + +final class ByteArrayFilterGroupList extends FilterGroupList { +} + +abstract class Filter { + final protected StringFilterGroupList pathFilterGroups = new StringFilterGroupList(); + final protected StringFilterGroupList identifierFilterGroups = new StringFilterGroupList(); + final protected ByteArrayFilterGroupList protobufBufferFilterGroups = new ByteArrayFilterGroupList(); + + /** + * Check if the given path, identifier or protobuf buffer is filtered by any {@link FilterGroup}. + * + * @return True if filtered, false otherwise. + */ + boolean isFiltered(final String path, final String identifier, final byte[] protobufBufferArray) { + if (pathFilterGroups.contains(path)) { + LogHelper.printDebug(() -> String.format("Filtered path: %s", path)); + return true; + } + + if (identifierFilterGroups.contains(identifier)) { + LogHelper.printDebug(() -> String.format("Filtered identifier: %s", identifier)); + return true; + } + + if (protobufBufferFilterGroups.contains(protobufBufferArray)) { + LogHelper.printDebug(() -> "Filtered from protobuf-buffer"); + return true; + } + + return false; + } +} + +@RequiresApi(api = Build.VERSION_CODES.N) +@SuppressWarnings("unused") +public final class LithoFilterPatch { + private static final Filter[] filters = new Filter[]{ + new AdsFilter(), + new ButtonsFilter(), + new CommentsFilter(), + new ShortsFilter() + }; + + @SuppressWarnings("unused") + public static boolean filter(final StringBuilder pathBuilder, final String identifier, final ByteBuffer protobufBuffer) { + var path = pathBuilder.toString(); + // It is assumed that protobufBuffer is empty as well in this case. + if (path.isEmpty()) return false; + + LogHelper.printDebug(() -> String.format( + "Searching (ID: %s, Buffer-size: %s): %s", + identifier, protobufBuffer.remaining(), path + )); + + var protobufBufferArray = protobufBuffer.array(); + + // check if any filter-group + for (var filter : filters) + if (filter.isFiltered(path, identifier, protobufBufferArray)) return true; + + return false; + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/patches/components/ShortsFilter.java new file mode 100644 index 00000000..43ab563f --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/components/ShortsFilter.java @@ -0,0 +1,92 @@ +package app.revanced.integrations.patches.components; + +import static app.revanced.integrations.utils.ReVancedUtils.hideViewBy1dpUnderCondition; +import static app.revanced.integrations.utils.ReVancedUtils.hideViewUnderCondition; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.view.View; + +import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar; + +import app.revanced.integrations.settings.SettingsEnum; + +public final class ShortsFilter extends Filter { + public static PivotBar pivotBar; + @SuppressLint("StaticFieldLeak") + + private final StringFilterGroup reelChannelBar = new StringFilterGroup( + null, + "reel_channel_bar" + ); + + public ShortsFilter() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return; + + final var thanksButton = new StringFilterGroup( + SettingsEnum.HIDE_SHORTS_THANKS_BUTTON, + "suggested_action" + ); + + final var subscribeButton = new StringFilterGroup( + SettingsEnum.HIDE_SHORTS_SUBSCRIBE_BUTTON, + "subscribe_button" + ); + + final var joinButton = new StringFilterGroup( + SettingsEnum.HIDE_SHORTS_JOIN_BUTTON, + "sponsor_button" + ); + + final var shorts = new StringFilterGroup( + SettingsEnum.HIDE_SHORTS, + "shorts_shelf", + "inline_shorts", + "shorts_grid" + ); + + this.pathFilterGroups.addAll(joinButton, subscribeButton); + this.identifierFilterGroups.addAll(shorts, thanksButton); + } + + @Override + boolean isFiltered(final String path, final String identifier, final byte[] protobufBufferArray) { + // Filter the path only when reelChannelBar is visible. + if (reelChannelBar.check(path).isFiltered()) + if (this.pathFilterGroups.contains(path)) return true; + + return this.identifierFilterGroups.contains(identifier); + } + + public static void hideShortsShelf(final View shortsShelfView) { + hideViewBy1dpUnderCondition(SettingsEnum.HIDE_SHORTS, shortsShelfView); + } + + // Additional components that have to be hidden by setting their visibility + + public static void hideShortsCommentsButton(final View commentsButtonView) { + hideViewUnderCondition(SettingsEnum.HIDE_SHORTS_COMMENTS_BUTTON, commentsButtonView); + } + + public static void hideShortsRemixButton(final View remixButtonView) { + hideViewUnderCondition(SettingsEnum.HIDE_SHORTS_REMIX_BUTTON, remixButtonView); + } + + public static void hideShortsShareButton(final View shareButtonView) { + hideViewUnderCondition(SettingsEnum.HIDE_SHORTS_SHARE_BUTTON, shareButtonView); + } + + public static void hideNavigationBar() { + if (!SettingsEnum.HIDE_SHORTS_NAVIGATION_BAR.getBoolean()) return; + if (pivotBar == null) return; + + pivotBar.setVisibility(View.GONE); + } + + public static View hideNavigationBar(final View navigationBarView) { + if (SettingsEnum.HIDE_SHORTS_NAVIGATION_BAR.getBoolean()) + return null; // Hides the navigation bar. + + return navigationBarView; + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldQualityLayoutPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldQualityLayoutPatch.java index 670cde2f..71899a7a 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldQualityLayoutPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldQualityLayoutPatch.java @@ -10,7 +10,7 @@ import app.revanced.integrations.utils.LogHelper; public class OldQualityLayoutPatch { public static void showOldQualityMenu(ListView listView) { - if (!SettingsEnum.OLD_STYLE_VIDEO_QUALITY_PLAYER_SETTINGS.getBoolean()) return; + if (!SettingsEnum.SHOW_OLD_VIDEO_MENU.getBoolean()) return; listView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { @Override diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java index df85317d..d1ef08eb 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java @@ -2,7 +2,6 @@ package app.revanced.integrations.patches.playback.quality; import static app.revanced.integrations.utils.ReVancedUtils.NetworkType; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.reflect.Field; @@ -20,12 +19,10 @@ public class RememberVideoQualityPatch { private static final SettingsEnum mobileQualitySetting = SettingsEnum.VIDEO_QUALITY_DEFAULT_MOBILE; private static boolean qualityNeedsUpdating; - @Nullable - private static String currentVideoId; /** * If the user selected a new quality from the flyout menu, - * and {@link SettingsEnum#VIDEO_QUALITY_REMEMBER_LAST_SELECTED} is enabled. + * and {@link SettingsEnum#REMEMBER_VIDEO_QUALITY_LAST_SELECTED} is enabled. */ private static boolean userChangedDefaultQuality; @@ -91,7 +88,7 @@ public class RememberVideoQualityPatch { } } } - LogHelper.printDebug(() -> "VideoId: " + currentVideoId + " videoQualities: " + videoQualities); + LogHelper.printDebug(() -> "videoQualities: " + videoQualities); } if (userChangedDefaultQuality) { @@ -113,15 +110,25 @@ public class RememberVideoQualityPatch { } i++; } + + // If the desired quality index is equal to the original index, + // then the video is already set to the desired default quality. + // + // The method could return here, but the UI video quality flyout will still + // show 'Auto' (ie: Auto (480p)) + // It appears that "Auto" picks the resolution on video load, + // and it does not appear to change the resolution during playback. + // + // To prevent confusion, set the video index anyways (even if it matches the existing index) + // As that will force the UI picker to not display "Auto" which may confuse the user. if (qualityIndexToUse == originalQualityIndex) { LogHelper.printDebug(() -> "Video is already preferred quality: " + preferredQuality); - return originalQualityIndex; + } else { + final int qualityToUseLog = qualityToUse; + LogHelper.printDebug(() -> "Quality changed from: " + + videoQualities.get(originalQualityIndex) + " to: " + qualityToUseLog); } - final int qualityToUseLog = qualityToUse; - LogHelper.printDebug(() -> "Quality changed from: " - + videoQualities.get(originalQualityIndex) + " to: " + qualityToUseLog); - Method m = qInterface.getClass().getMethod(qIndexMethod, Integer.TYPE); m.invoke(qInterface, qualityToUse); return qualityIndexToUse; @@ -135,7 +142,7 @@ public class RememberVideoQualityPatch { * Injection point. */ public static void userChangedQuality(int selectedQuality) { - if (!SettingsEnum.VIDEO_QUALITY_REMEMBER_LAST_SELECTED.getBoolean()) return; + if (!SettingsEnum.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.getBoolean()) return; userSelectedQualityIndex = selectedQuality; userChangedDefaultQuality = true; @@ -144,25 +151,9 @@ public class RememberVideoQualityPatch { /** * Injection point. */ - public static void newVideoStarted(@NonNull String videoId) { - // The same videoId can be passed in multiple times for a single video playback. - // Such as closing and opening the app, and sometimes when turning off/on the device screen. - // - // Known limitation, if: - // 1. a default video quality exists, and remember quality is turned off - // 2. user opens a video - // 3. user changes the video quality - // 4. user turns off then on the device screen (or does anything else that triggers the video id hook) - // result: the video quality of the current video will revert back to the saved default - // - // qualityNeedsUpdating could be set only when the videoId changes - // but then if the user closes and re-opens the same video the default video quality will not be applied. - LogHelper.printDebug(() -> "newVideoStarted: " + videoId); + public static void newVideoStarted(Object ignoredPlayerController) { + LogHelper.printDebug(() -> "newVideoStarted"); qualityNeedsUpdating = true; - - if (!videoId.equals(currentVideoId)) { - currentVideoId = videoId; - videoQualities = null; - } + videoQualities = null; } } diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java index 6692dc85..df80c1f5 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java @@ -1,12 +1,103 @@ package app.revanced.integrations.patches.playback.speed; +import android.preference.ListPreference; + +import androidx.annotation.NonNull; + +import java.util.Arrays; + +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + public class CustomVideoSpeedPatch { /** - * Default playback speeds offered by YouTube. - * Values are also used by {@link RememberPlaybackSpeedPatch}. - * - * If custom video speed is applied, - * then this array is overwritten by the patch with custom speeds + * Maximum playback speed, exclusive value. Custom speeds must be less than this value. */ - public static final float[] videoSpeeds = {0.25f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f}; + public static final float MAXIMUM_PLAYBACK_SPEED = 10; + + /** + * Custom playback speeds. + */ + public static float[] customVideoSpeeds; + + /** + * Minimum value of {@link #customVideoSpeeds} + */ + public static float minVideoSpeed; + + /** + * Maxium value of {@link #customVideoSpeeds} + */ + public static float maxVideoSpeed; + + /** + * PreferenceList entries and values, of all available playback speeds. + */ + private static String[] preferenceListEntries, preferenceListEntryValues; + + static { + loadSpeeds(); + } + + private static void resetCustomSpeeds(@NonNull String toastMessage) { + ReVancedUtils.showToastLong(toastMessage); + SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.saveValue(SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.defaultValue); + } + + private static void loadSpeeds() { + try { + String[] speedStrings = SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.getString().split("\\s+"); + Arrays.sort(speedStrings); + if (speedStrings.length == 0) { + throw new IllegalArgumentException(); + } + customVideoSpeeds = new float[speedStrings.length]; + for (int i = 0, length = speedStrings.length; i < length; i++) { + final float speed = Float.parseFloat(speedStrings[i]); + if (speed <= 0 || arrayContains(customVideoSpeeds, speed)) { + throw new IllegalArgumentException(); + } + if (speed >= MAXIMUM_PLAYBACK_SPEED) { + resetCustomSpeeds("Custom speeds must be less than " + MAXIMUM_PLAYBACK_SPEED + + ". Using default values."); + loadSpeeds(); + return; + } + minVideoSpeed = Math.min(minVideoSpeed, speed); + maxVideoSpeed = Math.max(maxVideoSpeed, speed); + customVideoSpeeds[i] = speed; + } + } catch (Exception ex) { + LogHelper.printInfo(() -> "parse error", ex); + resetCustomSpeeds("Invalid custom video speeds. Using default values."); + loadSpeeds(); + } + } + + private static boolean arrayContains(float[] array, float value) { + for (float arrayValue : array) { + if (arrayValue == value) return true; + } + return false; + } + + /** + * Initialize a settings preference list with the available playback speeds. + */ + public static void initializeListPreference(ListPreference preference) { + if (preferenceListEntries == null) { + preferenceListEntries = new String[customVideoSpeeds.length]; + preferenceListEntryValues = new String[customVideoSpeeds.length]; + int i = 0; + for (float speed : customVideoSpeeds) { + String speedString = String.valueOf(speed); + preferenceListEntries[i] = speedString + "x"; + preferenceListEntryValues[i] = speedString; + i++; + } + } + preference.setEntries(preferenceListEntries); + preference.setEntryValues(preferenceListEntryValues); + } } diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java index 4ccd457f..f557f1a6 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java @@ -1,33 +1,17 @@ package app.revanced.integrations.patches.playback.speed; -import android.preference.ListPreference; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import app.revanced.integrations.patches.VideoInformation; import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; public final class RememberPlaybackSpeedPatch { - /** - * PreferenceList entries and values, of all available playback speeds. - */ - private static String[] preferenceListEntries, preferenceListEntryValues; - - @Nullable - private static String currentVideoId; - /** * Injection point. - * Called when a new video loads. */ - public static void newVideoLoaded(@NonNull String videoId) { - if (videoId.equals(currentVideoId)) { - return; - } - currentVideoId = videoId; + public static void newVideoStarted(Object ignoredPlayerController) { + LogHelper.printDebug(() -> "newVideoStarted"); VideoInformation.overridePlaybackSpeed(SettingsEnum.PLAYBACK_SPEED_DEFAULT.getFloat()); } @@ -38,7 +22,7 @@ public final class RememberPlaybackSpeedPatch { * @param playbackSpeed The playback speed the user selected */ public static void userSelectedPlaybackSpeed(float playbackSpeed) { - if (SettingsEnum.PLAYBACK_SPEED_REMEMBER_LAST_SELECTED.getBoolean()) { + if (SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.getBoolean()) { SettingsEnum.PLAYBACK_SPEED_DEFAULT.saveValue(playbackSpeed); ReVancedUtils.showToastLong("Changed default speed to: " + playbackSpeed + "x"); } @@ -52,26 +36,4 @@ public final class RememberPlaybackSpeedPatch { return VideoInformation.getPlaybackSpeed(); } - /** - * Initialize a settings preference list. - * - * Normally this is done during patching by creating a static xml preference list, - * but the available playback speeds differ depending if {@link CustomVideoSpeedPatch} is applied or not. - */ - public static void initializeListPreference(ListPreference preference) { - if (preferenceListEntries == null) { - float[] videoSpeeds = CustomVideoSpeedPatch.videoSpeeds; - preferenceListEntries = new String[videoSpeeds.length]; - preferenceListEntryValues = new String[videoSpeeds.length]; - int i = 0; - for (float speed : videoSpeeds) { - String speedString = String.valueOf(speed); - preferenceListEntries[i] = speedString + "x"; - preferenceListEntryValues[i] = speedString; - i++; - } - } - preference.setEntries(preferenceListEntries); - preference.setEntryValues(preferenceListEntryValues); - } } diff --git a/app/src/main/java/app/revanced/integrations/patches/theme/ProgressBarDrawable.java b/app/src/main/java/app/revanced/integrations/patches/theme/ProgressBarDrawable.java new file mode 100644 index 00000000..890fc44c --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/theme/ProgressBarDrawable.java @@ -0,0 +1,47 @@ +package app.revanced.integrations.patches.theme; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import app.revanced.integrations.patches.HideSeekbarPatch; +import app.revanced.integrations.settings.SettingsEnum; + +/** + * Used by {@link SeekbarColorPatch} change the color of the seekbar. + * and {@link HideSeekbarPatch} to hide the seekbar of the feed and watch history. + */ +public class ProgressBarDrawable extends Drawable { + + private final Paint paint = new Paint(); + + @Override + public void draw(@NonNull Canvas canvas) { + if (SettingsEnum.HIDE_SEEKBAR.getBoolean()) { + return; + } + paint.setColor(SeekbarColorPatch.getCustomSeekbarColor()); + canvas.drawRect(getBounds(), paint); + } + + @Override + public void setAlpha(int alpha) { + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + paint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + +} diff --git a/app/src/main/java/app/revanced/integrations/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/patches/theme/SeekbarColorPatch.java new file mode 100644 index 00000000..1505732d --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/theme/SeekbarColorPatch.java @@ -0,0 +1,111 @@ +package app.revanced.integrations.patches.theme; + +import android.graphics.Color; + +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + +public final class SeekbarColorPatch { + + /** + * Default color of seekbar. + */ + private static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000; + + /** + * Default YouTube seekbar color brightness. + */ + private static final float ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS; + + /** + * Color value of {@link SettingsEnum#SEEKBAR_COLOR} + */ + private static int customSeekbarColor; + + /** + * Custom seekbar hue, saturation, and brightness values. + */ + private static final float[] customSeekbarColorHSV = new float[3]; + + static { + float[] hsv = new float[3]; + Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv); + ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2]; + + loadCustomSeekbarColorHSV(); + } + + private static void loadCustomSeekbarColorHSV() { + try { + customSeekbarColor = Color.parseColor(SettingsEnum.SEEKBAR_COLOR.getString()); + Color.colorToHSV(customSeekbarColor, customSeekbarColorHSV); + } catch (Exception ex) { + ReVancedUtils.showToastShort("Invalid seekbar color value. Using default value."); + SettingsEnum.SEEKBAR_COLOR.saveValue(SettingsEnum.SEEKBAR_COLOR.defaultValue); + loadCustomSeekbarColorHSV(); + } + } + + public static int getCustomSeekbarColor() { + return customSeekbarColor; + } + + /** + * Injection point. + * + * Overrides color when seekbar is clicked, and all Litho components that use the YouTube seekbar color. + */ + public static int getSeekbarColorOverride(int colorValue) { + return colorValue == ORIGINAL_SEEKBAR_COLOR + ? getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR) + : colorValue; + } + + /** + * Injection point. + * + * If {@link SettingsEnum#HIDE_SEEKBAR} is enabled, this returns a fully transparent color. + * + * Otherwise the original color is changed to the custom seekbar color, while retaining + * the brightness and alpha changes of the parameter value compared to the original seekbar color. + */ + public static int getSeekbarColorValue(int originalColor) { + try { + if (SettingsEnum.HIDE_SEEKBAR.getBoolean()) { + return 0x00000000; + } + if (customSeekbarColor == ORIGINAL_SEEKBAR_COLOR) { + return originalColor; // Nothing to do + } + final int alphaDifference = Color.alpha(originalColor) - Color.alpha(ORIGINAL_SEEKBAR_COLOR); + + // The seekbar uses the same color but different brightness for different situations. + float[] hsv = new float[3]; + Color.colorToHSV(originalColor, hsv); + final float brightnessDifference = hsv[2] - ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS; + + // Apply the brightness difference to the custom seekbar color. + hsv[0] = customSeekbarColorHSV[0]; + hsv[1] = customSeekbarColorHSV[1]; + hsv[2] = clamp(customSeekbarColorHSV[2] + brightnessDifference, 0, 1); + + final int replacementAlpha = clamp(Color.alpha(customSeekbarColor) + alphaDifference, 0, 255); + final int replacementColor = Color.HSVToColor(replacementAlpha, hsv); + LogHelper.printDebug(() -> String.format("Original color: #%08X replacement color: #%08X", + originalColor, replacementColor)); + return replacementColor; + } catch (Exception ex) { + LogHelper.printException(() -> "getSeekbarColorValue failure", ex); + return originalColor; + } + } + + static int clamp(int value, int lower, int upper) { + return Math.max(lower, Math.min(value, upper)); + } + + static float clamp(float value, float lower, float upper) { + return Math.max(lower, Math.min(value, upper)); + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/theme/ThemePatch.java b/app/src/main/java/app/revanced/integrations/patches/theme/ThemePatch.java deleted file mode 100644 index 78d9f7e7..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/theme/ThemePatch.java +++ /dev/null @@ -1,30 +0,0 @@ -package app.revanced.integrations.patches.theme; - -import android.graphics.Color; -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.ReVancedUtils; - -public final class ThemePatch { - public static final int DEFAULT_SEEKBAR_COLOR = 0xffff0000; - - public static final int ORIGINAL_SEEKBAR_CLICKED_COLOR = -65536; - - private static void resetSeekbarColor() { - ReVancedUtils.showToastShort("Invalid seekbar color value. Using default value."); - SettingsEnum.SEEKBAR_COLOR.saveValue("#" + Integer.toHexString(DEFAULT_SEEKBAR_COLOR)); - } - - public static int getSeekbarClickedColorValue(final int colorValue) { - // YouTube uses a specific color when the seekbar is clicked. Override in that case. - return colorValue == ORIGINAL_SEEKBAR_CLICKED_COLOR ? getSeekbarColorValue() : colorValue; - } - - public static int getSeekbarColorValue() { - try { - return Color.parseColor(SettingsEnum.SEEKBAR_COLOR.getString()); - } catch (IllegalArgumentException exception) { - resetSeekbarColor(); - return DEFAULT_SEEKBAR_COLOR; - } - } -} diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java index 5bee15ea..fc577f53 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java @@ -25,8 +25,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.text.NumberFormat; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import java.util.Objects; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -45,13 +48,47 @@ import app.revanced.integrations.utils.ThemeHelper; * Because Litho creates spans using multiple threads, this entire class supports multithreading as well. */ public class ReturnYouTubeDislike { + + /** + * Simple wrapper to cache a Future. + */ + private static class RYDCachedFetch { + /** + * How long to retain cached RYD fetches. + */ + static final long CACHE_TIMEOUT_MILLISECONDS = 4 * 60 * 1000; // 4 Minutes + + @NonNull + final Future future; + final String videoId; + final long timeFetched; + RYDCachedFetch(@NonNull Future future, @NonNull String videoId) { + this.future = Objects.requireNonNull(future); + this.videoId = Objects.requireNonNull(videoId); + this.timeFetched = System.currentTimeMillis(); + } + + boolean isExpired(long now) { + return (now - timeFetched) > CACHE_TIMEOUT_MILLISECONDS; + } + + boolean futureInProgressOrFinishedSuccessfully() { + try { + return !future.isDone() || future.get(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH, TimeUnit.MILLISECONDS) != null; + } catch (ExecutionException | InterruptedException | TimeoutException ex) { + LogHelper.printInfo(() -> "failed to lookup cache", ex); // will never happen + } + return false; + } + } + /** * Maximum amount of time to block the UI from updates while waiting for network call to complete. * * Must be less than 5 seconds, as per: * https://developer.android.com/topic/performance/vitals/anr */ - private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE = 4000; + private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH = 4000; /** * Unique placeholder character, used to detect if a segmented span already has dislikes added to it. @@ -59,6 +96,12 @@ public class ReturnYouTubeDislike { */ private static final char MIDDLE_SEPARATOR_CHARACTER = '\u2009'; // 'narrow space' character + /** + * Cached lookup of RYD fetches. + */ + @GuardedBy("videoIdLockObject") + private static final Map futureCache = new HashMap<>(); + /** * Used to send votes, one by one, in the same order the user created them. */ @@ -85,6 +128,13 @@ public class ReturnYouTubeDislike { @GuardedBy("videoIdLockObject") private static Future voteFetchFuture; + /** + * Optional current vote status of the UI. Used to apply a user vote that was done on a previous video viewing. + */ + @Nullable + @GuardedBy("videoIdLockObject") + private static Vote userVote; + /** * Original dislike span, before modifications. */ @@ -135,13 +185,25 @@ public class ReturnYouTubeDislike { } } - private static void setCurrentVideoId(@Nullable String videoId) { + public static void setCurrentVideoId(@Nullable String videoId) { synchronized (videoIdLockObject) { if (videoId == null && currentVideoId != null) { LogHelper.printDebug(() -> "Clearing data"); } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + final long now = System.currentTimeMillis(); + futureCache.values().removeIf(value -> { + final boolean expired = value.isExpired(now); + if (expired) LogHelper.printDebug(() -> "Removing expired fetch: " + value.videoId); + return expired; + }); + } else { + throw new IllegalStateException(); // YouTube requires Android N or greater + } currentVideoId = videoId; dislikeDataIsShort = false; + userVote = null; voteFetchFuture = null; originalDislikeSpan = null; replacementLikeDislikeSpan = null; @@ -154,7 +216,7 @@ public class ReturnYouTubeDislike { public static void clearCache() { synchronized (videoIdLockObject) { if (replacementLikeDislikeSpan != null) { - LogHelper.printDebug(() -> "Clearing cache"); + LogHelper.printDebug(() -> "Clearing replacement spans"); } replacementLikeDislikeSpan = null; } @@ -177,12 +239,6 @@ public class ReturnYouTubeDislike { public static void newVideoLoaded(@NonNull String videoId) { Objects.requireNonNull(videoId); - PlayerType currentPlayerType = PlayerType.getCurrent(); - if (currentPlayerType == PlayerType.INLINE_MINIMAL) { - LogHelper.printDebug(() -> "Ignoring inline playback of video: " + videoId); - setCurrentVideoId(null); - return; - } synchronized (videoIdLockObject) { if (videoId.equals(currentVideoId)) { return; // already loaded @@ -192,17 +248,23 @@ public class ReturnYouTubeDislike { setCurrentVideoId(null); return; } + PlayerType currentPlayerType = PlayerType.getCurrent(); LogHelper.printDebug(() -> "New video loaded: " + videoId + " playerType: " + currentPlayerType); setCurrentVideoId(videoId); // If a Short is opened while a regular video is on screen, this will incorrectly set this as false. // But this check is needed to fix unusual situations of opening/closing the app // while both a regular video and a short are on screen. - dislikeDataIsShort = PlayerType.getCurrent().isNoneOrHidden(); + dislikeDataIsShort = currentPlayerType.isNoneHiddenOrMinimized(); - // No need to wrap the call in a try/catch, - // as any exceptions are propagated out in the later Future#Get call. + RYDCachedFetch entry = futureCache.get(videoId); + if (entry != null && entry.futureInProgressOrFinishedSuccessfully()) { + LogHelper.printDebug(() -> "Using cached RYD fetch: "+ entry.videoId); + voteFetchFuture = entry.future; + return; + } voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId)); + futureCache.put(videoId, new RYDCachedFetch(voteFetchFuture, videoId)); } } @@ -240,13 +302,28 @@ public class ReturnYouTubeDislike { @NonNull private static Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton) { try { + Future fetchFuture = getVoteFetchFuture(); + if (fetchFuture == null) { + LogHelper.printDebug(() -> "fetch future not available (user enabled RYD while video was playing?)"); + return oldSpannable; + } + // Absolutely cannot be holding any lock during get(). + RYDVoteData votingData = fetchFuture.get(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH, TimeUnit.MILLISECONDS); + if (votingData == null) { + LogHelper.printDebug(() -> "Cannot add dislike to UI (RYD data not available)"); + return oldSpannable; + } + + // Must check against existing replacements, after the fetch, + // otherwise concurrent threads can create the same replacement same multiple times. + // Also do the replacement comparison and creation in a single synchronized block. synchronized (videoIdLockObject) { - if (replacementLikeDislikeSpan != null) { - if (spansHaveEqualTextAndColor(replacementLikeDislikeSpan, oldSpannable)) { + if (originalDislikeSpan != null && replacementLikeDislikeSpan != null) { + if (spansHaveEqualTextAndColor(oldSpannable, replacementLikeDislikeSpan)) { LogHelper.printDebug(() -> "Ignoring previously created dislikes span"); return oldSpannable; } - if (spansHaveEqualTextAndColor(Objects.requireNonNull(originalDislikeSpan), oldSpannable)) { + if (spansHaveEqualTextAndColor(oldSpannable, originalDislikeSpan)) { LogHelper.printDebug(() -> "Replacing span with previously created dislike span"); return replacementLikeDislikeSpan; } @@ -258,31 +335,19 @@ public class ReturnYouTubeDislike { return oldSpannable; } oldSpannable = originalDislikeSpan; - } else { - originalDislikeSpan = oldSpannable; // most up to date original } - } - // Must block the current thread until fetching is done. - // There's no known way to edit the text after creation yet. - Future fetchFuture = getVoteFetchFuture(); - if (fetchFuture == null) { - LogHelper.printDebug(() -> "fetch future not available (user enabled RYD while video was playing?)"); - return oldSpannable; - } - RYDVoteData votingData = fetchFuture.get(MAX_MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE, TimeUnit.MILLISECONDS); - if (votingData == null) { - LogHelper.printDebug(() -> "Cannot add dislike to UI (RYD data not available)"); - return oldSpannable; - } + // No replacement span exist, create it now. - SpannableString replacement = createDislikeSpan(oldSpannable, isSegmentedButton, votingData); - synchronized (videoIdLockObject) { - replacementLikeDislikeSpan = replacement; + if (userVote != null) { + votingData.updateUsingVote(userVote); + } + originalDislikeSpan = oldSpannable; + replacementLikeDislikeSpan = createDislikeSpan(oldSpannable, isSegmentedButton, votingData); + LogHelper.printDebug(() -> "Replaced: '" + originalDislikeSpan + "' with: '" + replacementLikeDislikeSpan + "'"); + + return replacementLikeDislikeSpan; } - final Spanned oldSpannableLogging = oldSpannable; - LogHelper.printDebug(() -> "Replaced: '" + oldSpannableLogging + "' with: '" + replacement + "'"); - return replacement; } catch (TimeoutException e) { LogHelper.printDebug(() -> "UI timed out while waiting for fetch votes to complete"); // show no toast } catch (Exception e) { @@ -291,13 +356,22 @@ public class ReturnYouTubeDislike { return oldSpannable; } + /** + * @return if the RYD fetch call has completed. + */ + public static boolean fetchCompleted() { + Future future = getVoteFetchFuture(); + return future != null && future.isDone(); + } + public static void sendVote(@NonNull Vote vote) { ReVancedUtils.verifyOnMainThread(); Objects.requireNonNull(vote); try { // Must make a local copy of videoId, since it may change between now and when the vote thread runs. String videoIdToVoteFor = getCurrentVideoId(); - if (videoIdToVoteFor == null || dislikeDataIsShort != PlayerType.getCurrent().isNoneOrHidden()) { + if (videoIdToVoteFor == null || + (SettingsEnum.RYD_SHORTS.getBoolean() && dislikeDataIsShort != PlayerType.getCurrent().isNoneHiddenOrMinimized())) { // User enabled RYD after starting playback of a video. // Or shorts was loaded with regular video present, then shorts was closed, // and then user voted on the now visible original video. @@ -317,27 +391,48 @@ public class ReturnYouTubeDislike { } }); - clearCache(); // UI needs updating - - // Update the downloaded vote data. - Future future = getVoteFetchFuture(); - if (future == null) { - LogHelper.printException(() -> "Cannot update UI dislike count - vote fetch is null"); - return; - } - // The future should always be completed before user can like/dislike, but use a timeout just in case. - RYDVoteData voteData = future.get(MAX_MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE, TimeUnit.MILLISECONDS); - if (voteData == null) { - // RYD fetch failed - LogHelper.printDebug(() -> "Cannot update UI (vote data not available)"); - return; - } - voteData.updateUsingVote(vote); + setUserVote(vote); } catch (Exception ex) { LogHelper.printException(() -> "Error trying to send vote", ex); } } + public static void setUserVote(@NonNull Vote vote) { + Objects.requireNonNull(vote); + try { + LogHelper.printDebug(() -> "setUserVote: " + vote); + + // Update the downloaded vote data. + Future future = getVoteFetchFuture(); + if (future != null && future.isDone()) { + RYDVoteData voteData; + try { + voteData = future.get(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH, TimeUnit.MILLISECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException ex) { + // Should never happen + LogHelper.printInfo(() -> "Could not update vote data", ex); + return; + } + if (voteData == null) { + // RYD fetch failed + LogHelper.printDebug(() -> "Cannot update UI (vote data not available)"); + return; + } + + voteData.updateUsingVote(vote); + } // Else, vote will be applied after vote data is received + + synchronized (videoIdLockObject) { + if (userVote != vote) { + userVote = vote; + clearCache(); // UI needs updating + } + } + } catch (Exception ex) { + LogHelper.printException(() -> "setUserVote failure", ex); + } + } + /** * Must call off main thread, as this will make a network call if user is not yet registered. * @@ -363,6 +458,7 @@ public class ReturnYouTubeDislike { /** * @param isSegmentedButton If UI is using the segmented single UI component for both like and dislike. */ + @NonNull private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton, @NonNull RYDVoteData voteData) { if (!isSegmentedButton) { // Simple replacement of 'dislike' with a number/percentage. @@ -393,7 +489,7 @@ public class ReturnYouTubeDislike { } SpannableStringBuilder builder = new SpannableStringBuilder(); - final boolean compactLayout = SettingsEnum.RYD_USE_COMPACT_LAYOUT.getBoolean(); + final boolean compactLayout = SettingsEnum.RYD_COMPACT_LAYOUT.getBoolean(); final int separatorColor = ThemeHelper.isDarkTheme() ? 0x29AAAAAA // transparent dark gray : 0xFFD9D9D9; // light gray @@ -477,12 +573,12 @@ public class ReturnYouTubeDislike { private static SpannableString newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) { return newSpanUsingStylingOfAnotherSpan(sourceStyling, - SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean() + SettingsEnum.RYD_DISLIKE_PERCENTAGE.getBoolean() ? formatDislikePercentage(voteData.getDislikePercentage()) : formatDislikeCount(voteData.getDislikeCount())); } - private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull String newSpanText) { + private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) { SpannableString destination = new SpannableString(newSpanText); Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class); for (Object span : spans) { diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/RYDVoteData.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/RYDVoteData.java index 969063e8..149f7ffd 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/RYDVoteData.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/RYDVoteData.java @@ -82,15 +82,12 @@ public final class RYDVoteData { public void updateUsingVote(Vote vote) { if (vote == Vote.LIKE) { - LogHelper.printDebug(() -> "Increasing like count"); likeCount = fetchedLikeCount + 1; dislikeCount = fetchedDislikeCount; } else if (vote == Vote.DISLIKE) { - LogHelper.printDebug(() -> "Increasing dislike count"); likeCount = fetchedLikeCount; dislikeCount = fetchedDislikeCount + 1; } else if (vote == Vote.LIKE_REMOVE) { - LogHelper.printDebug(() -> "Resetting like/dislike to fetched values"); likeCount = fetchedLikeCount; dislikeCount = fetchedDislikeCount; } else { diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java index dcc5bf9d..0965f493 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -5,11 +5,13 @@ import static app.revanced.integrations.utils.StringRef.str; import android.util.Base64; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; +import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.ProtocolException; @@ -22,6 +24,7 @@ import java.util.Objects; import app.revanced.integrations.requests.Requester; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; @@ -219,6 +222,15 @@ public class ReturnYouTubeDislikeApi { } } + private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) { + if (SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.getBoolean()) { + ReVancedUtils.showToastShort(toastMessage); + } + if (ex != null) { + LogHelper.printInfo(() -> toastMessage, ex); + } + } + /** * @return NULL if fetch failed, or if a rate limit is in effect. */ @@ -272,12 +284,13 @@ public class ReturnYouTubeDislikeApi { LogHelper.printDebug(() -> "Video has no like/dislikes (video is a YouTube Story?): " + videoId); return null; // do not updated connection statistics } else { - LogHelper.printException(() -> "Failed to fetch votes for video: " + videoId + " response code was: " + responseCode, - null, str("revanced_ryd_failure_connection_status_code", responseCode)); - connection.disconnect(); // something went wrong, might as well disconnect + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); } + connection.disconnect(); // something went wrong, might as well disconnect } catch (SocketTimeoutException ex) { // connection timed out, response timeout, or some other network error - LogHelper.printException(() -> "Failed to fetch votes", ex, str("revanced_ryd_failure_connection_timeout")); + handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex); + } catch (IOException ex) { + handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex); } catch (Exception ex) { // should never happen LogHelper.printException(() -> "Failed to fetch votes", ex, str("revanced_ryd_failure_generic", ex.getMessage())); @@ -318,11 +331,14 @@ public class ReturnYouTubeDislikeApi { String solution = solvePuzzle(challenge, difficulty); return confirmRegistration(userId, solution); } - LogHelper.printException(() -> "Failed to register new user: " + userId - + " response code was: " + responseCode); // failed attempt, and ok to log userId + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); connection.disconnect(); + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + } catch (IOException ex) { + handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex); } catch (Exception ex) { - LogHelper.printException(() -> "Failed to register user", ex); + LogHelper.printException(() -> "Failed to register user", ex); // should never happen } return null; } @@ -351,19 +367,23 @@ public class ReturnYouTubeDislikeApi { connection.disconnect(); // disconnect, as no more connections will be made for a little while return null; } + String result = null; if (responseCode == HTTP_STATUS_CODE_SUCCESS) { - String result = Requester.parseJson(connection); + result = Requester.parseJson(connection); if (result.equalsIgnoreCase("true")) { LogHelper.printDebug(() -> "Registration confirmation successful"); return userId; } - LogHelper.printException(() -> "Failed to confirm registration for user: " + userId - + " solution: " + solution + " response string was: " + result); - } else { - LogHelper.printException(() -> "Failed to confirm registration for user: " + userId - + " solution: " + solution + " response code was: " + responseCode); } + final String resultLog = result == null ? "(no response)" : result; + LogHelper.printInfo(() -> "Failed to confirm registration for user: " + userId + + " solution: " + solution + " responseCode: " + responseCode + " responseString: " + resultLog); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); connection.disconnect(); // something went wrong, might as well disconnect + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + } catch (IOException ex) { + handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"), ex); } catch (Exception ex) { LogHelper.printException(() -> "Failed to confirm registration for user: " + userId + "solution: " + solution, ex); @@ -405,10 +425,16 @@ public class ReturnYouTubeDislikeApi { String solution = solvePuzzle(challenge, difficulty); return confirmVote(videoId, userId, solution); } - LogHelper.printException(() -> "Failed to send vote for video: " + videoId - + " vote: " + vote + " response code was: " + responseCode); + LogHelper.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote + + " response code was: " + responseCode); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); connection.disconnect(); // something went wrong, might as well disconnect + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + } catch (IOException ex) { + handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex); } catch (Exception ex) { + // should never happen LogHelper.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex); } return false; @@ -438,23 +464,26 @@ public class ReturnYouTubeDislikeApi { connection.disconnect(); // disconnect, as no more connections will be made for a little while return false; } - + String result = null; if (responseCode == HTTP_STATUS_CODE_SUCCESS) { - String result = Requester.parseJson(connection); + result = Requester.parseJson(connection); if (result.equalsIgnoreCase("true")) { LogHelper.printDebug(() -> "Vote confirm successful for video: " + videoId); return true; } - LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId - + " solution: " + solution + " response string was: " + result); - } else { - LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId - + " solution: " + solution + " response code was: " + responseCode); } + final String resultLog = result == null ? "(no response)" : result; + LogHelper.printInfo(() -> "Failed to confirm vote for video: " + videoId + + " solution: " + solution + " responseCode: " + responseCode + " responseString: " + resultLog); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); connection.disconnect(); // something went wrong, might as well disconnect + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + } catch (IOException ex) { + handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"), ex); } catch (Exception ex) { LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId - + " solution: " + solution, ex); + + " solution: " + solution, ex); // should never happen } return false; } @@ -503,7 +532,7 @@ public class ReturnYouTubeDislikeApi { } // should never be reached - throw new IllegalStateException("Failed to solve puzzle challenge: " + challenge + " of difficulty: " + difficulty); + throw new IllegalStateException("Failed to solve puzzle challenge: " + challenge + " difficulty: " + difficulty); } // https://stackoverflow.com/a/157202 @@ -519,9 +548,8 @@ public class ReturnYouTubeDislikeApi { private static int countLeadingZeroes(byte[] uInt8View) { int zeroes = 0; - int value; for (byte b : uInt8View) { - value = b & 0xFF; + int value = b & 0xFF; if (value == 0) { zeroes += 8; } else { diff --git a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java index 4c5db2ac..9584929d 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -1,69 +1,90 @@ package app.revanced.integrations.settings; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static app.revanced.integrations.settings.SettingsEnum.ReturnType.BOOLEAN; +import static app.revanced.integrations.settings.SettingsEnum.ReturnType.FLOAT; +import static app.revanced.integrations.settings.SettingsEnum.ReturnType.INTEGER; +import static app.revanced.integrations.settings.SettingsEnum.ReturnType.LONG; +import static app.revanced.integrations.settings.SettingsEnum.ReturnType.STRING; +import static app.revanced.integrations.settings.SharedPrefCategory.RETURN_YOUTUBE_DISLIKE; +import static app.revanced.integrations.settings.SharedPrefCategory.SPONSOR_BLOCK; +import static app.revanced.integrations.utils.StringRef.str; + +import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import app.revanced.integrations.utils.StringRef; -import app.revanced.integrations.patches.theme.ThemePatch; +import app.revanced.integrations.sponsorblock.SponsorBlockSettings; +import app.revanced.integrations.utils.ReVancedUtils; +import app.revanced.integrations.utils.StringRef; +import app.revanced.integrations.utils.LogHelper; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; -import static app.revanced.integrations.settings.SettingsEnum.ReturnType.*; -import static app.revanced.integrations.settings.SharedPrefCategory.RETURN_YOUTUBE_DISLIKE; -import static app.revanced.integrations.settings.SharedPrefCategory.SPONSOR_BLOCK; -import static java.lang.Boolean.FALSE; -import static java.lang.Boolean.TRUE; public enum SettingsEnum { - //Download Settings - DOWNLOADS_BUTTON_SHOWN("revanced_downloads_enabled", BOOLEAN, TRUE), - DOWNLOADS_PACKAGE_NAME("revanced_downloads_package_name", STRING, "org.schabi.newpipe" /* NewPipe */, parents(DOWNLOADS_BUTTON_SHOWN)), + // External downloader + EXTERNAL_DOWNLOADER("revanced_external_downloader", BOOLEAN, TRUE), + EXTERNAL_DOWNLOADER_PACKAGE_NAME("revanced_external_downloader_name", STRING, + "org.schabi.newpipe" /* NewPipe */, parents(EXTERNAL_DOWNLOADER)), - // Copy video URL settings - COPY_VIDEO_URL_BUTTON_SHOWN("revanced_copy_video_url_enabled", BOOLEAN, TRUE), - COPY_VIDEO_URL_TIMESTAMP_BUTTON_SHOWN("revanced_copy_video_url_timestamp_enabled", BOOLEAN, TRUE), + // Copy video URL + COPY_VIDEO_URL("revanced_copy_video_url", BOOLEAN, FALSE), + COPY_VIDEO_URL_TIMESTAMP("revanced_copy_video_url_timestamp", BOOLEAN, TRUE), - // Video settings - OLD_STYLE_VIDEO_QUALITY_PLAYER_SETTINGS("revanced_use_old_style_quality_settings", BOOLEAN, TRUE), - VIDEO_QUALITY_REMEMBER_LAST_SELECTED("revanced_remember_video_quality_last_selected", BOOLEAN, TRUE), - VIDEO_QUALITY_DEFAULT_WIFI("revanced_default_video_quality_wifi", INTEGER, -2), - VIDEO_QUALITY_DEFAULT_MOBILE("revanced_default_video_quality_mobile", INTEGER, -2), - PLAYBACK_SPEED_REMEMBER_LAST_SELECTED("revanced_remember_playback_speed_last_selected", BOOLEAN, TRUE), - PLAYBACK_SPEED_DEFAULT("revanced_default_playback_speed", FLOAT, 1.0f), + // Video + HDR_AUTO_BRIGHTNESS("revanced_hdr_auto_brightness", BOOLEAN, TRUE), + SHOW_OLD_VIDEO_MENU("revanced_show_old_video_menu", BOOLEAN, TRUE), + REMEMBER_VIDEO_QUALITY_LAST_SELECTED("revanced_remember_video_quality_last_selected", BOOLEAN, TRUE), + VIDEO_QUALITY_DEFAULT_WIFI("revanced_video_quality_default_wifi", INTEGER, -2), + VIDEO_QUALITY_DEFAULT_MOBILE("revanced_video_quality_default_mobile", INTEGER, -2), + REMEMBER_PLAYBACK_SPEED_LAST_SELECTED("revanced_remember_playback_speed_last_selected", BOOLEAN, TRUE), + PLAYBACK_SPEED_DEFAULT("revanced_playback_speed_default", FLOAT, 1.0f), + CUSTOM_PLAYBACK_SPEEDS("revanced_custom_playback_speeds", STRING, + "0.25\n0.5\n0.75\n0.9\n0.95\n1.0\n1.05\n1.1\n1.25\n1.5\n1.75\n2.0\n3.0\n4.0\n5.0", true), - // TODO: Unused currently - // Whitelist settings - //ENABLE_WHITELIST("revanced_whitelist_ads_enabled", BOOLEAN, FALSE), + // Whitelist + //WHITELIST("revanced_whitelist_ads", BOOLEAN, FALSE), // TODO: Unused currently - // Ad settings - ADREMOVER_BUTTONED_REMOVAL("revanced_adremover_buttoned", BOOLEAN, TRUE), - ADREMOVER_CHANNEL_BAR("revanced_hide_channel_bar", BOOLEAN, FALSE), - ADREMOVER_CHANNEL_MEMBER_SHELF_REMOVAL("revanced_adremover_channel_member_shelf_removal", BOOLEAN, TRUE), - ADREMOVER_CHAPTER_TEASER_REMOVAL("revanced_adremover_chapter_teaser", BOOLEAN, TRUE), - ADREMOVER_COMMUNITY_GUIDELINES_REMOVAL("revanced_adremover_community_guidelines", BOOLEAN, TRUE), - ADREMOVER_COMMUNITY_POSTS_REMOVAL("revanced_adremover_community_posts_removal", BOOLEAN, FALSE), - ADREMOVER_COMPACT_BANNER_REMOVAL("revanced_adremover_compact_banner_removal", BOOLEAN, TRUE), - ADREMOVER_CUSTOM_ENABLED("revanced_adremover_custom_enabled", BOOLEAN, FALSE), - ADREMOVER_CUSTOM_REMOVAL("revanced_adremover_custom_strings", STRING, "", true, parents(ADREMOVER_CUSTOM_ENABLED)), - ADREMOVER_EMERGENCY_BOX_REMOVAL("revanced_adremover_emergency_box_removal", BOOLEAN, TRUE), - ADREMOVER_FEED_SURVEY_REMOVAL("revanced_adremover_feed_survey", BOOLEAN, TRUE), - ADREMOVER_GENERAL_ADS_REMOVAL("revanced_adremover_ad_removal", BOOLEAN, TRUE), - ADREMOVER_GRAY_SEPARATOR("revanced_adremover_separator", BOOLEAN, TRUE), - ADREMOVER_HIDE_CHANNEL_GUIDELINES("revanced_adremover_hide_channel_guidelines", BOOLEAN, TRUE), - ADREMOVER_HIDE_LATEST_POSTS("revanced_adremover_hide_latest_posts", BOOLEAN, TRUE), - ADREMOVER_IMAGE_SHELF("revanced_hide_image_shelf", BOOLEAN, TRUE), - ADREMOVER_INFO_PANEL_REMOVAL("revanced_adremover_info_panel", BOOLEAN, TRUE), - ADREMOVER_MEDICAL_PANEL_REMOVAL("revanced_adremover_medical_panel", BOOLEAN, TRUE), - ADREMOVER_MERCHANDISE_REMOVAL("revanced_adremover_merchandise", BOOLEAN, TRUE), - ADREMOVER_MOVIE_REMOVAL("revanced_adremover_movie", BOOLEAN, TRUE), - ADREMOVER_PAID_CONTENT_REMOVAL("revanced_adremover_paid_content", BOOLEAN, TRUE), - ADREMOVER_QUICK_ACTIONS("revanced_hide_quick_actions", BOOLEAN, FALSE), - ADREMOVER_RELATED_VIDEOS("revanced_hide_related_videos", BOOLEAN, FALSE), - ADREMOVER_SELF_SPONSOR_REMOVAL("revanced_adremover_self_sponsor", BOOLEAN, TRUE), - ADREMOVER_SHORTS_REMOVAL("revanced_adremover_shorts", BOOLEAN, TRUE, true), - ADREMOVER_SUBSCRIBERS_COMMUNITY_GUIDELINES_REMOVAL("revanced_adremover_subscribers_community_guidelines_removal", BOOLEAN, TRUE), - ADREMOVER_VIEW_PRODUCTS("revanced_adremover_view_products", BOOLEAN, TRUE), - ADREMOVER_WEB_SEARCH_RESULTS("revanced_adremover_web_search_result", BOOLEAN, TRUE), - VIDEO_ADS_REMOVAL("revanced_video_ads_removal", BOOLEAN, TRUE, true), + // Ads + HIDE_BUTTONED_ADS("revanced_hide_buttoned_ads", BOOLEAN, TRUE), + HIDE_GENERAL_ADS("revanced_hide_general_ads", BOOLEAN, TRUE), + HIDE_HIDE_LATEST_POSTS("revanced_hide_latest_posts_ads", BOOLEAN, TRUE), + HIDE_PAID_CONTENT("revanced_hide_paid_content_ads", BOOLEAN, TRUE), + HIDE_SELF_SPONSOR("revanced_hide_self_sponsor_ads", BOOLEAN, TRUE), + HIDE_VIDEO_ADS("revanced_hide_video_ads", BOOLEAN, TRUE, true), + CUSTOM_FILTER("revanced_custom_filter", BOOLEAN, FALSE), + CUSTOM_FILTER_STRINGS("revanced_custom_filter_strings", STRING, "", true, parents(CUSTOM_FILTER)), + + // Layout + HIDE_CHANNEL_BAR("revanced_hide_channel_bar", BOOLEAN, FALSE), + HIDE_CHANNEL_MEMBER_SHELF("revanced_hide_channel_member_shelf", BOOLEAN, TRUE), + HIDE_CHAPTER_TEASER("revanced_hide_chapter_teaser", BOOLEAN, TRUE), + HIDE_COMMUNITY_GUIDELINES("revanced_hide_community_guidelines", BOOLEAN, TRUE), + HIDE_COMMUNITY_POSTS("revanced_hide_community_posts", BOOLEAN, FALSE), + HIDE_COMPACT_BANNER("revanced_hide_compact_banner", BOOLEAN, TRUE), + HIDE_EMERGENCY_BOX("revanced_hide_emergency_box", BOOLEAN, TRUE), + HIDE_FEED_SURVEY("revanced_hide_feed_survey", BOOLEAN, TRUE), + HIDE_GRAY_SEPARATOR("revanced_hide_gray_separator", BOOLEAN, TRUE), + HIDE_HIDE_CHANNEL_GUIDELINES("revanced_hide_channel_guidelines", BOOLEAN, TRUE), + HIDE_IMAGE_SHELF("revanced_hide_image_shelf", BOOLEAN, TRUE), + HIDE_HIDE_INFO_PANELS("revanced_hide_info_panels", BOOLEAN, TRUE), + HIDE_MEDICAL_PANELS("revanced_hide_medical_panels", BOOLEAN, TRUE), + HIDE_MERCHANDISE_BANNERS("revanced_hide_merchandise_banners", BOOLEAN, TRUE), + HIDE_MOVIES_SECTION("revanced_hide_movies_section", BOOLEAN, TRUE), + HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES("revanced_hide_subscribers_community_guidelines", BOOLEAN, TRUE), + HIDE_PRODUCTS_BANNER("revanced_hide_products_banner", BOOLEAN, TRUE), + HIDE_WEB_SEARCH_RESULTS("revanced_hide_web_search_results", BOOLEAN, TRUE), + HIDE_QUICK_ACTIONS("revanced_hide_quick_actions", BOOLEAN, FALSE), + HIDE_RELATED_VIDEOS("revanced_hide_related_videos", BOOLEAN, FALSE), // Action buttons HIDE_LIKE_DISLIKE_BUTTON("revanced_hide_like_dislike_button", BOOLEAN, FALSE), @@ -72,8 +93,8 @@ public enum SettingsEnum { HIDE_CLIP_BUTTON("revanced_hide_clip_button", BOOLEAN, FALSE, "revanced_hide_clip_button_user_dialog_message"), HIDE_ACTION_BUTTONS("revanced_hide_action_buttons", BOOLEAN, FALSE), - // Layout settings - DISABLE_STARTUP_SHORTS_PLAYER("revanced_startup_shorts_player_enabled", BOOLEAN, FALSE), + // Layout + DISABLE_RESUMING_SHORTS_PLAYER("revanced_disable_resuming_shorts_player", BOOLEAN, FALSE), HIDE_ALBUM_CARDS("revanced_hide_album_cards", BOOLEAN, FALSE, true), HIDE_ARTIST_CARDS("revanced_hide_artist_cards", BOOLEAN, FALSE), HIDE_AUDIO_TRACK_BUTTON("revanced_hide_audio_track_button", BOOLEAN, FALSE), @@ -83,87 +104,253 @@ public enum SettingsEnum { HIDE_CAST_BUTTON("revanced_hide_cast_button", BOOLEAN, TRUE, true), HIDE_COMMENTS_SECTION("revanced_hide_comments_section", BOOLEAN, FALSE, true), HIDE_CREATE_BUTTON("revanced_hide_create_button", BOOLEAN, TRUE, true), - SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON("revanced_switch_create_with_notifications_button", BOOLEAN, TRUE, true), HIDE_CROWDFUNDING_BOX("revanced_hide_crowdfunding_box", BOOLEAN, FALSE, true), HIDE_EMAIL_ADDRESS("revanced_hide_email_address", BOOLEAN, FALSE), HIDE_ENDSCREEN_CARDS("revanced_hide_endscreen_cards", BOOLEAN, TRUE), HIDE_FLOATING_MICROPHONE_BUTTON("revanced_hide_floating_microphone_button", BOOLEAN, TRUE, true), HIDE_FULLSCREEN_PANELS("revanced_hide_fullscreen_panels", BOOLEAN, TRUE), HIDE_GET_PREMIUM("revanced_hide_get_premium", BOOLEAN, TRUE), - HIDE_INFO_CARDS("revanced_hide_infocards", BOOLEAN, TRUE), + HIDE_INFO_CARDS("revanced_hide_info_cards", BOOLEAN, TRUE), + HIDE_LOAD_MORE_BUTTON("revanced_hide_load_more_button", BOOLEAN, TRUE, true), HIDE_PLAYER_BUTTONS("revanced_hide_player_buttons", BOOLEAN, FALSE), + HIDE_PLAYER_OVERLAY("revanced_hide_player_overlay", BOOLEAN, FALSE, true), HIDE_PREVIEW_COMMENT("revanced_hide_preview_comment", BOOLEAN, FALSE, true), - HIDE_SEEKBAR("revanced_hide_seekbar", BOOLEAN, FALSE), + HIDE_SEEKBAR("revanced_hide_seekbar", BOOLEAN, FALSE, true), HIDE_HOME_BUTTON("revanced_hide_home_button", BOOLEAN, FALSE, true), HIDE_SHORTS_BUTTON("revanced_hide_shorts_button", BOOLEAN, TRUE, true), HIDE_SUBSCRIPTIONS_BUTTON("revanced_hide_subscriptions_button", BOOLEAN, FALSE, true), - HIDE_SHORTS_COMMENTS_BUTTON("revanced_hide_shorts_comments_button", BOOLEAN, FALSE), HIDE_TIMESTAMP("revanced_hide_timestamp", BOOLEAN, FALSE), HIDE_VIDEO_WATERMARK("revanced_hide_video_watermark", BOOLEAN, TRUE), HIDE_WATCH_IN_VR("revanced_hide_watch_in_vr", BOOLEAN, FALSE, true), - PLAYER_POPUP_PANELS("revanced_player_popup_panels_enabled", BOOLEAN, FALSE), + PLAYER_POPUP_PANELS("revanced_hide_player_popup_panels", BOOLEAN, FALSE), + SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON("revanced_switch_create_with_notifications_button", BOOLEAN, TRUE, true), SPOOF_APP_VERSION("revanced_spoof_app_version", BOOLEAN, FALSE, true, "revanced_spoof_app_version_user_dialog_message"), SPOOF_APP_VERSION_TARGET("revanced_spoof_app_version_target", STRING, "17.30.35", true, parents(SPOOF_APP_VERSION)), USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true), WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true), - SEEKBAR_COLOR("revanced_seekbar_color", STRING, Integer.toHexString(ThemePatch.DEFAULT_SEEKBAR_COLOR), true), + SEEKBAR_COLOR("revanced_seekbar_color", STRING, "#FF0000", true), + HIDE_FILTER_BAR_FEED_IN_FEED("revanced_hide_filter_bar_feed_in_feed", BOOLEAN, FALSE, true), + HIDE_FILTER_BAR_FEED_IN_SEARCH("revanced_hide_filter_bar_feed_in_search", BOOLEAN, FALSE, true), + HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS("revanced_hide_filter_bar_feed_in_related_videos", BOOLEAN, FALSE, true), + HIDE_SHORTS_JOIN_BUTTON("revanced_hide_shorts_join_button", BOOLEAN, FALSE), + HIDE_SHORTS_SUBSCRIBE_BUTTON("revanced_hide_shorts_subscribe_button", BOOLEAN, FALSE), + HIDE_SHORTS_THANKS_BUTTON("revanced_hide_shorts_thanks_button", BOOLEAN, FALSE), + HIDE_SHORTS_COMMENTS_BUTTON("revanced_hide_shorts_comments_button", BOOLEAN, FALSE), + HIDE_SHORTS_REMIX_BUTTON("revanced_hide_shorts_remix_button", BOOLEAN, FALSE), + HIDE_SHORTS_SHARE_BUTTON("revanced_hide_shorts_share_button", BOOLEAN, FALSE), + HIDE_SHORTS_NAVIGATION_BAR("revanced_hide_shorts_navigation_bar", BOOLEAN, TRUE, true), + HIDE_SHORTS("revanced_hide_shorts", BOOLEAN, FALSE, true), - // Misc. Settings - SIGNATURE_SPOOFING("revanced_spoof_signature_verification", BOOLEAN, TRUE, "revanced_spoof_signature_verification_user_dialog_message"), - CAPTIONS_ENABLED("revanced_autocaptions_enabled", BOOLEAN, FALSE), + // Misc + AUTO_CAPTIONS("revanced_auto_captions", BOOLEAN, FALSE), DISABLE_ZOOM_HAPTICS("revanced_disable_zoom_haptics", BOOLEAN, TRUE), - ENABLE_EXTERNAL_BROWSER("revanced_enable_external_browser", BOOLEAN, TRUE, true), - PREFERRED_AUTO_REPEAT("revanced_pref_auto_repeat", BOOLEAN, FALSE), - TAP_SEEKING_ENABLED("revanced_enable_tap_seeking", BOOLEAN, TRUE), - USE_HDR_AUTO_BRIGHTNESS("revanced_pref_hdr_autobrightness", BOOLEAN, TRUE), + EXTERNAL_BROWSER("revanced_external_browser", BOOLEAN, TRUE, true), + AUTO_REPEAT("revanced_auto_repeat", BOOLEAN, FALSE), + SEEKBAR_TAPPING("revanced_seekbar_tapping", BOOLEAN, TRUE), + SPOOF_SIGNATURE_VERIFICATION("revanced_spoof_signature_verification", BOOLEAN, TRUE, "revanced_spoof_signature_verification_user_dialog_message"), // Swipe controls - ENABLE_SWIPE_BRIGHTNESS("revanced_enable_swipe_brightness", BOOLEAN, TRUE), - ENABLE_SWIPE_VOLUME("revanced_enable_swipe_volume", BOOLEAN, TRUE), - ENABLE_PRESS_TO_SWIPE("revanced_enable_press_to_swipe", BOOLEAN, FALSE, - parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)), - ENABLE_SWIPE_HAPTIC_FEEDBACK("revanced_enable_swipe_haptic_feedback", BOOLEAN, TRUE, - parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)), - SWIPE_MAGNITUDE_THRESHOLD("revanced_swipe_magnitude_threshold", FLOAT, 30f, - parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)), + SWIPE_BRIGHTNESS("revanced_swipe_brightness", BOOLEAN, TRUE), + SWIPE_VOLUME("revanced_swipe_volume", BOOLEAN, TRUE), + SWIPE_PRESS_TO_ENGAGE("revanced_swipe_press_to_engage", BOOLEAN, FALSE, true, + parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)), + SWIPE_HAPTIC_FEEDBACK("revanced_swipe_haptic_feedback", BOOLEAN, TRUE, + parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)), + SWIPE_MAGNITUDE_THRESHOLD("revanced_swipe_threshold", INTEGER, 30, + parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)), SWIPE_OVERLAY_BACKGROUND_ALPHA("revanced_swipe_overlay_background_alpha", INTEGER, 127, - parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)), - SWIPE_OVERLAY_TEXT_SIZE("revanced_swipe_overlay_text_size", FLOAT, 22f, - parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)), + parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)), + SWIPE_OVERLAY_TEXT_SIZE("revanced_swipe_text_overlay_size", INTEGER, 22, + parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)), SWIPE_OVERLAY_TIMEOUT("revanced_swipe_overlay_timeout", LONG, 500L, - parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)), + parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)), - // Debug settings - DEBUG("revanced_debug_enabled", BOOLEAN, FALSE), - DEBUG_STACKTRACE("revanced_debug_stacktrace_enabled", BOOLEAN, FALSE, parents(DEBUG)), - DEBUG_SHOW_TOAST_ON_ERROR("revanced_debug_toast_on_error_enabled", BOOLEAN, TRUE, "revanced_debug_toast_on_error_user_dialog_message"), + // Debugging + DEBUG("revanced_debug", BOOLEAN, FALSE), + DEBUG_STACKTRACE("revanced_debug_stacktrace", BOOLEAN, FALSE, parents(DEBUG)), + DEBUG_TOAST_ON_ERROR("revanced_debug_toast_on_error", BOOLEAN, TRUE, "revanced_debug_toast_on_error_user_dialog_message"), - // ReturnYoutubeDislike settings + // ReturnYoutubeDislike RYD_ENABLED("ryd_enabled", BOOLEAN, TRUE, RETURN_YOUTUBE_DISLIKE), - RYD_USER_ID("ryd_userId", STRING, "", RETURN_YOUTUBE_DISLIKE), - RYD_SHOW_DISLIKE_PERCENTAGE("ryd_show_dislike_percentage", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)), - RYD_USE_COMPACT_LAYOUT("ryd_use_compact_layout", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)), + RYD_USER_ID("ryd_user_id", STRING, "", RETURN_YOUTUBE_DISLIKE), + RYD_SHORTS("ryd_shorts", BOOLEAN, TRUE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)), + RYD_DISLIKE_PERCENTAGE("ryd_dislike_percentage", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)), + RYD_COMPACT_LAYOUT("ryd_compact_layout", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)), + RYD_TOAST_ON_CONNECTION_ERROR("ryd_toast_on_connection_error", BOOLEAN, TRUE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)), - // SponsorBlock settings - SB_ENABLED("sb-enabled", BOOLEAN, TRUE, SPONSOR_BLOCK), - SB_VOTING_ENABLED("sb-voting-enabled", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)), - SB_CREATE_NEW_SEGMENT_ENABLED("sb-new-segment-enabled", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)), - SB_USE_COMPACT_SKIP_BUTTON("sb-use-compact-skip-button", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)), - SB_AUTO_HIDE_SKIP_BUTTON("sb-auto-hide-skip-segment-button", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)), - SB_SHOW_TOAST_ON_SKIP("show-toast", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)), - SB_TRACK_SKIP_COUNT("count-skips", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)), - SB_UUID("uuid", STRING, "", SPONSOR_BLOCK), - SB_ADJUST_NEW_SEGMENT_STEP("new-segment-step-accuracy", INTEGER, 150, SPONSOR_BLOCK, parents(SB_ENABLED)), - SB_MIN_DURATION("sb-min-duration", FLOAT, 0F, SPONSOR_BLOCK, parents(SB_ENABLED)), - SB_SEEN_GUIDELINES("sb-seen-gl", BOOLEAN, FALSE, SPONSOR_BLOCK), - SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED("sb-skipped-segments", INTEGER, 0, SPONSOR_BLOCK), - SB_SKIPPED_SEGMENTS_TIME_SAVED("sb-skipped-segments-time", LONG, 0L, SPONSOR_BLOCK), - SB_SHOW_TIME_WITHOUT_SEGMENTS("sb-length-without-segments", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)), - SB_IS_VIP("sb-is-vip", BOOLEAN, FALSE, SPONSOR_BLOCK), - SB_LAST_VIP_CHECK("sb-last-vip-check", LONG, 0L, SPONSOR_BLOCK), - SB_API_URL("sb-api-host-url", STRING, "https://sponsor.ajay.app", SPONSOR_BLOCK); + // SponsorBlock + SB_ENABLED("sb_enabled", BOOLEAN, TRUE, SPONSOR_BLOCK), + SB_PRIVATE_USER_ID("sb_private_user_id_Do_Not_Share", STRING, "", SPONSOR_BLOCK), /** Do not use directly, instead use {@link SponsorBlockSettings} */ + DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING("uuid", STRING, "", SPONSOR_BLOCK), // Delete sometime in 2024 + SB_CREATE_NEW_SEGMENT_STEP("sb_create_new_segment_step", INTEGER, 150, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_VOTING_BUTTON("sb_voting_button", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_CREATE_NEW_SEGMENT("sb_create_new_segment", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_COMPACT_SKIP_BUTTON("sb_compact_skip_button", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_AUTO_HIDE_SKIP_BUTTON("sb_auto_hide_skip_button", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_TOAST_ON_SKIP("sb_toast_on_skip", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_TOAST_ON_CONNECTION_ERROR("sb_toast_on_connection_error", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_TRACK_SKIP_COUNT("sb_track_skip_count", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_SEGMENT_MIN_DURATION("sb_min_segment_duration", FLOAT, 0F, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_VIDEO_LENGTH_WITHOUT_SEGMENTS("sb_video_length_without_segments", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_API_URL("sb_api_url", STRING, "https://sponsor.ajay.app", SPONSOR_BLOCK), + SB_USER_IS_VIP("sb_user_is_vip", BOOLEAN, FALSE, SPONSOR_BLOCK), + // SB settings not exported + SB_LAST_VIP_CHECK("sb_last_vip_check", LONG, 0L, SPONSOR_BLOCK), + SB_HIDE_EXPORT_WARNING("sb_hide_export_warning", BOOLEAN, FALSE, SPONSOR_BLOCK), + SB_SEEN_GUIDELINES("sb_seen_guidelines", BOOLEAN, FALSE, SPONSOR_BLOCK), + SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS("sb_local_time_saved_number_segments", INTEGER, 0, SPONSOR_BLOCK), + SB_LOCAL_TIME_SAVED_MILLISECONDS("sb_local_time_saved_milliseconds", LONG, 0L, SPONSOR_BLOCK), - private static SettingsEnum[] parents(SettingsEnum ... parents) { + // + // TODO: eventually, delete these + // + @Deprecated + DEPRECATED_ADREMOVER_BUTTONED_REMOVAL("revanced_adremover_buttoned", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_ADREMOVER_GENERAL_ADS_REMOVAL("revanced_adremover_ad_removal", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_ADREMOVER_PAID_CONTENT("revanced_adremover_paid_content", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_ADREMOVER_HIDE_LATEST_POSTS("revanced_adremover_hide_latest_posts", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_ADREMOVER_SELF_SPONSOR("revanced_adremover_self_sponsor", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_ADREMOVER_CUSTOM_ENABLED("revanced_adremover_custom_enabled", BOOLEAN, FALSE), + @Deprecated + DEPRECATED_ADREMOVER_CUSTOM_REMOVAL("revanced_adremover_custom_strings", STRING, "", true), + @Deprecated + DEPRECATED_REMOVE_VIDEO_ADS("revanced_video_ads_removal", BOOLEAN, TRUE, true), + + @Deprecated + DEPRECATED_HIDE_CHANNEL_MEMBER_SHELF("revanced_adremover_channel_member_shelf_removal", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_CHAPTER_TEASER("revanced_adremover_chapter_teaser", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_COMMUNITY_GUIDELINES("revanced_adremover_community_guidelines", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_COMMUNITY_POSTS("revanced_adremover_community_posts_removal", BOOLEAN, FALSE), + @Deprecated + DEPRECATED_HIDE_COMPACT_BANNER("revanced_adremover_compact_banner_removal", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_EMERGENCY_BOX("revanced_adremover_emergency_box_removal", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_FEED_SURVEY_REMOVAL("revanced_adremover_feed_survey", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_GRAY_SEPARATOR("revanced_adremover_separator", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_HIDE_CHANNEL_GUIDELINES("revanced_adremover_hide_channel_guidelines", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_INFO_PANEL_REMOVAL("revanced_adremover_info_panel", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_MEDICAL_PANEL_REMOVAL("revanced_adremover_medical_panel", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_MERCHANDISE_REMOVAL("revanced_adremover_merchandise", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_MOVIE_REMOVAL("revanced_adremover_movie", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES_REMOVAL("revanced_adremover_subscribers_community_guidelines_removal", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_VIEW_PRODUCTS("revanced_adremover_view_products", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_WEB_SEARCH_RESULTS("revanced_adremover_web_search_result", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HIDE_SHORTS("revanced_adremover_shorts", BOOLEAN, TRUE, true), + @Deprecated + DEPRECATED_HIDE_INFO_CARDS("revanced_hide_infocards", BOOLEAN, TRUE), + + @Deprecated + DEPRECATED_DISABLE_RESUMING_SHORTS_PLAYER("revanced_disable_startup_shorts_player", BOOLEAN, FALSE), + + @Deprecated + DEPRECATED_ETERNAL_DOWNLOADER("revanced_downloads_enabled", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_EXTERNAL_DOWNLOADER_PACKAGE_NAME("revanced_downloads_package_name", STRING, "org.schabi.newpipe"), + + @Deprecated + DEPRECATED_SHOW_OLD_VIDEO_MENU("revanced_use_old_style_quality_settings", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_VIDEO_QUALITY_DEFAULT_WIFI("revanced_default_video_quality_wifi", INTEGER, -2), + @Deprecated + DEPRECATED_VIDEO_QUALITY_DEFAULT_MOBILE("revanced_default_video_quality_mobile", INTEGER, -2), + @Deprecated + DEPRECATED_PLAYBACK_SPEED_DEFAULT("revanced_default_playback_speed", FLOAT, 1.0f), + + @Deprecated + DEPRECATED_COPY_VIDEO_URL("revanced_copy_video_url_enabled", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_COPY_VIDEO_URL_TIMESTAMP("revanced_copy_video_url_timestamp_enabled", BOOLEAN, TRUE), + + @Deprecated + DEPRECATED_AUTO_CAPTIONS("revanced_autocaptions_enabled", BOOLEAN, FALSE), + @Deprecated + DEPRECATED_PLAYER_POPUP_PANELS("revanced_player_popup_panels_enabled", BOOLEAN, FALSE), + @Deprecated + DEPRECATED_SWIPE_BRIGHTNESS("revanced_enable_swipe_brightness", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_SWIPE_VOLUME("revanced_enable_swipe_volume", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_PRESS_TO_SWIPE("revanced_enable_press_to_swipe", BOOLEAN, FALSE), + @Deprecated + DEPRECATED_SWIPE_HAPTIC_FEEDBACK("revanced_enable_swipe_haptic_feedback", BOOLEAN, TRUE), + + @Deprecated + DEPRECATED_DEBUG("revanced_debug_enabled", BOOLEAN, FALSE), + @Deprecated + DEPRECATED_DEBUG_STACKTRACE("revanced_debug_stacktrace_enabled", BOOLEAN, FALSE), + @Deprecated + DEPRECATED_DEBUG_TOAST_ON_ERROR("revanced_debug_toast_on_error_enabled", BOOLEAN, TRUE), + + @Deprecated + DEPRECATED_EXTERNAL_BROWSER("revanced_enable_external_browser", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_AUTO_REPEAT("revanced_pref_auto_repeat", BOOLEAN, FALSE), + @Deprecated + DEPRECATED_TAP_SEEKING("revanced_enable_tap_seeking", BOOLEAN, TRUE), + @Deprecated + DEPRECATED_HDR_AUTO_BRIGHTNESS("revanced_pref_hdr_autobrightness", BOOLEAN, TRUE), + + @Deprecated + DEPRECATED_RYD_USER_ID("ryd_userId", STRING, "", RETURN_YOUTUBE_DISLIKE), + @Deprecated + DEPRECATED_RYD_DISLIKE_PERCENTAGE("ryd_show_dislike_percentage", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE), + @Deprecated + DEPRECATED_RYD_COMPACT_LAYOUT("ryd_use_compact_layout", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE), + + @Deprecated + DEPRECATED_SB_ENABLED("sb-enabled", BOOLEAN, TRUE, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_VOTING_BUTTON("sb-voting-enabled", BOOLEAN, FALSE, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_CREATE_NEW_SEGMENT("sb-new-segment-enabled", BOOLEAN, FALSE, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_COMPACT_SKIP_BUTTON("sb-use-compact-skip-button", BOOLEAN, FALSE, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_MIN_DURATION("sb-min-duration", FLOAT, 0F, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_VIDEO_LENGTH_WITHOUT_SEGMENTS("sb-length-without-segments", BOOLEAN, TRUE, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_API_URL("sb-api-host-url", STRING, "https://sponsor.ajay.app", SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_TOAST_ON_SKIP("show-toast", BOOLEAN, TRUE, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_AUTO_HIDE_SKIP_BUTTON("sb-auto-hide-skip-segment-button", BOOLEAN, TRUE, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_TRACK_SKIP_COUNT("count-skips", BOOLEAN, TRUE, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_ADJUST_NEW_SEGMENT_STEP("new-segment-step-accuracy", INTEGER, 150, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_LAST_VIP_CHECK("sb-last-vip-check", LONG, 0L, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_IS_VIP("sb-is-vip", BOOLEAN, FALSE, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS("sb-skipped-segments", INTEGER, 0, SPONSOR_BLOCK), + @Deprecated + DEPRECATED_SB_LOCAL_TIME_SAVED_MILLISECONDS("sb-skipped-segments-time", LONG, 0L, SPONSOR_BLOCK); + // + // TODO END + // + + private static SettingsEnum[] parents(SettingsEnum... parents) { return parents; } @@ -206,26 +393,32 @@ public enum SettingsEnum { SettingsEnum(String path, ReturnType returnType, Object defaultValue) { this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, false, null, null); } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, boolean rebootApp) { - this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, null,null); + this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, null, null); } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, String userDialogMessage) { this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, false, userDialogMessage, null); } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, SettingsEnum[] parents) { this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, false, null, parents); } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, boolean rebootApp, String userDialogMessage) { this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, userDialogMessage, null); } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, boolean rebootApp, SettingsEnum[] parents) { this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, null, parents); } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, boolean rebootApp, String userDialogMessage, SettingsEnum[] parents) { this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, userDialogMessage, parents); @@ -234,20 +427,24 @@ public enum SettingsEnum { SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName) { this(path, returnType, defaultValue, prefName, false, null, null); } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName, boolean rebootApp) { this(path, returnType, defaultValue, prefName, rebootApp, null, null); } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName, String userDialogMessage) { this(path, returnType, defaultValue, prefName, false, userDialogMessage, null); } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName, SettingsEnum[] parents) { this(path, returnType, defaultValue, prefName, false, null, parents); } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName, - boolean rebootApp, @Nullable String userDialogMessage, @Nullable SettingsEnum[] parents) { + boolean rebootApp, @Nullable String userDialogMessage, @Nullable SettingsEnum[] parents) { this.path = Objects.requireNonNull(path); this.returnType = Objects.requireNonNull(returnType); this.value = this.defaultValue = Objects.requireNonNull(defaultValue); @@ -273,22 +470,126 @@ public enum SettingsEnum { } } + private static final Map pathToSetting = new HashMap<>(2* values().length); + static { loadAllSettings(); + + for (SettingsEnum setting : values()) { + pathToSetting.put(setting.path, setting); + } } @Nullable public static SettingsEnum settingFromPath(@NonNull String str) { - for (SettingsEnum setting : values()) { - if (setting.path.equals(str)) return setting; - } - return null; + return pathToSetting.get(str); } private static void loadAllSettings() { for (SettingsEnum setting : values()) { setting.load(); } + + // + // TODO: eventually delete this + // renamed settings with new path names, but otherwise the new and old settings are identical + // + SettingsEnum[][] renamedSettings = { + // TODO: do _not_ delete this SB private user id migration property until sometime in 2024. + // This is the only setting that cannot be reconfigured if lost, + // and more time should be given for users who rarely upgrade. + {DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING, SB_PRIVATE_USER_ID}, + + // TODO: delete the rest of these migration settings. When to delete? Anytime. + {DEPRECATED_ADREMOVER_BUTTONED_REMOVAL, HIDE_BUTTONED_ADS}, + {DEPRECATED_ADREMOVER_GENERAL_ADS_REMOVAL, HIDE_GENERAL_ADS}, + {DEPRECATED_ADREMOVER_HIDE_LATEST_POSTS, HIDE_HIDE_LATEST_POSTS}, + {DEPRECATED_ADREMOVER_PAID_CONTENT, HIDE_PAID_CONTENT}, + {DEPRECATED_ADREMOVER_SELF_SPONSOR, HIDE_SELF_SPONSOR}, + {DEPRECATED_REMOVE_VIDEO_ADS, HIDE_VIDEO_ADS}, + {DEPRECATED_ADREMOVER_CUSTOM_ENABLED, CUSTOM_FILTER}, + {DEPRECATED_ADREMOVER_CUSTOM_REMOVAL, CUSTOM_FILTER_STRINGS}, + + {DEPRECATED_HIDE_CHANNEL_MEMBER_SHELF, HIDE_CHANNEL_MEMBER_SHELF}, + {DEPRECATED_HIDE_CHAPTER_TEASER, HIDE_CHAPTER_TEASER}, + {DEPRECATED_HIDE_COMMUNITY_GUIDELINES, HIDE_COMMUNITY_GUIDELINES}, + {DEPRECATED_HIDE_COMMUNITY_POSTS, HIDE_COMMUNITY_POSTS}, + {DEPRECATED_HIDE_COMPACT_BANNER, HIDE_COMPACT_BANNER}, + {DEPRECATED_HIDE_EMERGENCY_BOX, HIDE_EMERGENCY_BOX}, + {DEPRECATED_HIDE_FEED_SURVEY_REMOVAL, HIDE_FEED_SURVEY}, + {DEPRECATED_HIDE_GRAY_SEPARATOR, HIDE_GRAY_SEPARATOR}, + {DEPRECATED_HIDE_HIDE_CHANNEL_GUIDELINES, HIDE_HIDE_CHANNEL_GUIDELINES}, + {DEPRECATED_HIDE_INFO_PANEL_REMOVAL, HIDE_HIDE_INFO_PANELS}, + {DEPRECATED_HIDE_MEDICAL_PANEL_REMOVAL, HIDE_MEDICAL_PANELS}, + {DEPRECATED_HIDE_MERCHANDISE_REMOVAL, HIDE_MERCHANDISE_BANNERS}, + {DEPRECATED_HIDE_MOVIE_REMOVAL, HIDE_MOVIES_SECTION}, + {DEPRECATED_HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES_REMOVAL, HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES}, + {DEPRECATED_HIDE_VIEW_PRODUCTS, HIDE_PRODUCTS_BANNER}, + {DEPRECATED_HIDE_WEB_SEARCH_RESULTS, HIDE_WEB_SEARCH_RESULTS}, + {DEPRECATED_HIDE_SHORTS, HIDE_SHORTS}, + {DEPRECATED_DISABLE_RESUMING_SHORTS_PLAYER, DISABLE_RESUMING_SHORTS_PLAYER}, + {DEPRECATED_HIDE_INFO_CARDS, HIDE_INFO_CARDS}, + + {DEPRECATED_ETERNAL_DOWNLOADER, EXTERNAL_DOWNLOADER}, + {DEPRECATED_EXTERNAL_DOWNLOADER_PACKAGE_NAME, EXTERNAL_DOWNLOADER_PACKAGE_NAME}, + {DEPRECATED_COPY_VIDEO_URL, COPY_VIDEO_URL}, + {DEPRECATED_COPY_VIDEO_URL_TIMESTAMP, COPY_VIDEO_URL_TIMESTAMP}, + + {DEPRECATED_SHOW_OLD_VIDEO_MENU, SHOW_OLD_VIDEO_MENU}, + {DEPRECATED_VIDEO_QUALITY_DEFAULT_WIFI, VIDEO_QUALITY_DEFAULT_WIFI}, + {DEPRECATED_VIDEO_QUALITY_DEFAULT_MOBILE, VIDEO_QUALITY_DEFAULT_MOBILE}, + {DEPRECATED_PLAYBACK_SPEED_DEFAULT, PLAYBACK_SPEED_DEFAULT}, + + {DEPRECATED_AUTO_CAPTIONS, AUTO_CAPTIONS}, + {DEPRECATED_PLAYER_POPUP_PANELS, PLAYER_POPUP_PANELS}, + {DEPRECATED_SWIPE_BRIGHTNESS, SWIPE_BRIGHTNESS}, + {DEPRECATED_SWIPE_VOLUME, SWIPE_VOLUME}, + {DEPRECATED_PRESS_TO_SWIPE, SWIPE_PRESS_TO_ENGAGE}, + {DEPRECATED_SWIPE_HAPTIC_FEEDBACK, SWIPE_HAPTIC_FEEDBACK}, + + {DEPRECATED_DEBUG, DEBUG}, + {DEPRECATED_DEBUG_STACKTRACE, DEBUG_STACKTRACE}, + {DEPRECATED_DEBUG_TOAST_ON_ERROR, DEBUG_TOAST_ON_ERROR}, + + {DEPRECATED_EXTERNAL_BROWSER, EXTERNAL_BROWSER}, + {DEPRECATED_AUTO_REPEAT, AUTO_REPEAT}, + {DEPRECATED_TAP_SEEKING, SEEKBAR_TAPPING}, + {DEPRECATED_HDR_AUTO_BRIGHTNESS, HDR_AUTO_BRIGHTNESS}, + + {DEPRECATED_RYD_USER_ID, RYD_USER_ID}, + {DEPRECATED_RYD_DISLIKE_PERCENTAGE, RYD_DISLIKE_PERCENTAGE}, + {DEPRECATED_RYD_COMPACT_LAYOUT, RYD_COMPACT_LAYOUT}, + + {DEPRECATED_SB_ENABLED, SB_ENABLED}, + {DEPRECATED_SB_VOTING_BUTTON, SB_VOTING_BUTTON}, + {DEPRECATED_SB_CREATE_NEW_SEGMENT, SB_CREATE_NEW_SEGMENT}, + {DEPRECATED_SB_COMPACT_SKIP_BUTTON, SB_COMPACT_SKIP_BUTTON}, + {DEPRECATED_SB_MIN_DURATION, SB_SEGMENT_MIN_DURATION}, + {DEPRECATED_SB_VIDEO_LENGTH_WITHOUT_SEGMENTS, SB_VIDEO_LENGTH_WITHOUT_SEGMENTS}, + {DEPRECATED_SB_API_URL, SB_API_URL}, + {DEPRECATED_SB_TOAST_ON_SKIP, SB_TOAST_ON_SKIP}, + {DEPRECATED_SB_AUTO_HIDE_SKIP_BUTTON, SB_AUTO_HIDE_SKIP_BUTTON}, + {DEPRECATED_SB_TRACK_SKIP_COUNT, SB_TRACK_SKIP_COUNT}, + {DEPRECATED_SB_ADJUST_NEW_SEGMENT_STEP, SB_CREATE_NEW_SEGMENT_STEP}, + {DEPRECATED_SB_LAST_VIP_CHECK, SB_LAST_VIP_CHECK}, + {DEPRECATED_SB_IS_VIP, SB_USER_IS_VIP}, + {DEPRECATED_SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS, SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS}, + {DEPRECATED_SB_LOCAL_TIME_SAVED_MILLISECONDS, SB_LOCAL_TIME_SAVED_MILLISECONDS}, + }; + for (SettingsEnum[] oldNewSetting : renamedSettings) { + SettingsEnum oldSetting = oldNewSetting[0]; + SettingsEnum newSetting = oldNewSetting[1]; + + if (!oldSetting.isSetToDefault()) { + LogHelper.printInfo(() -> "Migrating old setting of '" + oldSetting.value + + "' from: " + oldSetting + " into replacement setting: " + newSetting); + newSetting.saveValue(oldSetting.value); + oldSetting.saveValue(oldSetting.defaultValue); // reset old value + } + } + // + // TODO end + // } private void load() { @@ -315,10 +616,10 @@ public enum SettingsEnum { /** * Sets, but does _not_ persistently save the value. - * + *

* This intentionally is a static method, to deter accidental usage * when {@link #saveValue(Object)} was intended. - * + *

* This method is only to be used by the Settings preference code. */ public static void setValue(@NonNull SettingsEnum setting, @NonNull String newValue) { @@ -343,11 +644,12 @@ public enum SettingsEnum { throw new IllegalStateException(setting.name()); } } + /** * This method is only to be used by the Settings preference code. */ public static void setValue(@NonNull SettingsEnum setting, @NonNull Boolean newValue) { - Objects.requireNonNull(newValue); + setting.returnType.validate(newValue); setting.value = newValue; } @@ -355,7 +657,8 @@ public enum SettingsEnum { * Sets the value, and persistently saves it. */ public void saveValue(@NonNull Object newValue) { - Objects.requireNonNull(newValue); + returnType.validate(newValue); + value = newValue; // Must set before saving to preferences (otherwise importing fails to update UI correctly). switch (returnType) { case BOOLEAN: sharedPref.saveBoolean(path, (boolean) newValue); @@ -375,12 +678,11 @@ public enum SettingsEnum { default: throw new IllegalStateException(name()); } - value = newValue; } /** * @return if this setting can be configured and used. - * + *

* Not to be confused with {@link #getBoolean()} */ public boolean isAvailable() { @@ -393,6 +695,13 @@ public enum SettingsEnum { return false; } + /** + * @return if the currently set value is the same as {@link #defaultValue} + */ + public boolean isSetToDefault() { + return value.equals(defaultValue); + } + public boolean getBoolean() { return (Boolean) value; } @@ -422,11 +731,174 @@ public enum SettingsEnum { return value; } + /** + * This could be yet another field, + * for now use a simple switch statement since this method is not used outside this class. + */ + private boolean includeWithImportExport() { + switch (this) { + case RYD_USER_ID: // Not useful to export, no reason to include it. + case SB_LAST_VIP_CHECK: + case SB_HIDE_EXPORT_WARNING: + case SB_SEEN_GUIDELINES: + case SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS: + case SB_LOCAL_TIME_SAVED_MILLISECONDS: + return false; + } + return true; + } + + // Begin import / export + + /** + * If a setting path has this prefix, then remove it before importing/exporting. + */ + private static final String OPTIONAL_REVANCED_SETTINGS_PREFIX = "revanced_"; + + /** + * The path, minus any 'revanced' prefix to keep json concise. + */ + private String getImportExportKey() { + if (path.startsWith(OPTIONAL_REVANCED_SETTINGS_PREFIX)) { + return path.substring(OPTIONAL_REVANCED_SETTINGS_PREFIX.length()); + } + return path; + } + + private static SettingsEnum[] valuesSortedForExport() { + SettingsEnum[] sorted = values(); + Arrays.sort(sorted, (SettingsEnum o1, SettingsEnum o2) -> { + // Organize SponsorBlock settings last. + final boolean o1IsSb = o1.sharedPref == SPONSOR_BLOCK; + final boolean o2IsSb = o2.sharedPref == SPONSOR_BLOCK; + if (o1IsSb != o2IsSb) { + return o1IsSb ? 1 : -1; + } + return o1.path.compareTo(o2.path); + }); + return sorted; + } + + @NonNull + public static String exportJSON(@Nullable Context alertDialogContext) { + try { + JSONObject json = new JSONObject(); + for (SettingsEnum setting : valuesSortedForExport()) { + String importExportKey = setting.getImportExportKey(); + if (json.has(importExportKey)) { + throw new IllegalArgumentException("duplicate key found: " + importExportKey); + } + final boolean exportDefaultValues = false; // Enable to see what all settings looks like in the UI. + if (setting.includeWithImportExport() && (!setting.isSetToDefault() | exportDefaultValues)) { + json.put(importExportKey, setting.getObjectValue()); + } + } + SponsorBlockSettings.exportCategoriesToFlatJson(alertDialogContext, json); + + if (json.length() == 0) { + return ""; + } + String export = json.toString(0); + // Remove the outer JSON braces to make the output more compact, + // and leave less chance of the user forgetting to copy it + return export.substring(2, export.length() - 2); + } catch (JSONException e) { + LogHelper.printException(() -> "Export failure", e); // should never happen + return ""; + } + } + + /** + * @return if any settings that require a reboot were changed. + */ + public static boolean importJSON(@NonNull String settingsJsonString) { + try { + if (!settingsJsonString.matches("[\\s\\S]*\\{")) { + settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces + } + JSONObject json = new JSONObject(settingsJsonString); + + boolean rebootSettingChanged = false; + int numberOfSettingsImported = 0; + for (SettingsEnum setting : values()) { + String key = setting.getImportExportKey(); + if (json.has(key)) { + Object value; + switch (setting.returnType) { + case BOOLEAN: + value = json.getBoolean(key); + break; + case INTEGER: + value = json.getInt(key); + break; + case LONG: + value = json.getLong(key); + break; + case FLOAT: + value = (float) json.getDouble(key); + break; + case STRING: + value = json.getString(key); + break; + default: + throw new IllegalStateException(); + } + if (!setting.getObjectValue().equals(value)) { + rebootSettingChanged |= setting.rebootApp; + setting.saveValue(value); + } + numberOfSettingsImported++; + } else if (setting.includeWithImportExport() && !setting.isSetToDefault()) { + LogHelper.printDebug(() -> "Resetting to default: " + setting); + rebootSettingChanged |= setting.rebootApp; + setting.saveValue(setting.defaultValue); + } + } + numberOfSettingsImported += SponsorBlockSettings.importCategoriesFromFlatJson(json); + + ReVancedUtils.showToastLong(numberOfSettingsImported == 0 + ? str("revanced_settings_import_reset") + : str("revanced_settings_import_success", numberOfSettingsImported)); + + return rebootSettingChanged; + } catch (JSONException | IllegalArgumentException ex) { + ReVancedUtils.showToastLong(str("revanced_settings_import_failure_parse", ex.getMessage())); + LogHelper.printInfo(() -> "", ex); + } catch (Exception ex) { + LogHelper.printException(() -> "Import failure: " + ex.getMessage(), ex); // should never happen + } + return false; + } + + // End import / export + public enum ReturnType { BOOLEAN, INTEGER, - STRING, LONG, FLOAT, + STRING; + + public void validate(@Nullable Object obj) throws IllegalArgumentException { + if (!matches(obj)) { + throw new IllegalArgumentException("'" + obj + "' does not match:" + this); + } + } + + public boolean matches(@Nullable Object obj) { + switch (this) { + case BOOLEAN: + return obj instanceof Boolean; + case INTEGER: + return obj instanceof Integer; + case LONG: + return obj instanceof Long; + case FLOAT: + return obj instanceof Float; + case STRING: + return obj instanceof String; + } + return false; + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java b/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java index 4bf180ed..bded5469 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java +++ b/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java @@ -8,6 +8,7 @@ import androidx.annotation.Nullable; import java.util.Objects; +import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; /** @@ -35,6 +36,11 @@ public enum SharedPrefCategory { preferences = Objects.requireNonNull(ReVancedUtils.getContext()).getSharedPreferences(prefName, Context.MODE_PRIVATE); } + private void removeConflictingPreferenceKeyValue(@NonNull String key) { + LogHelper.printException(() -> "Found conflicting preference: " + key); + preferences.edit().remove(key).apply(); + } + private void saveObjectAsString(@NonNull String key, @Nullable Object value) { preferences.edit().putString(key, (value == null ? null : value.toString())).apply(); } @@ -91,7 +97,14 @@ public enum SharedPrefCategory { } return _default; } catch (ClassCastException ex) { - return preferences.getInt(key, _default); // old data, previously stored as primitive + try { + // Old data previously stored as primitive. + return preferences.getInt(key, _default); + } catch (ClassCastException ex2) { + // Value stored is a completely different type (should never happen). + removeConflictingPreferenceKeyValue(key); + return _default; + } } } @@ -104,7 +117,12 @@ public enum SharedPrefCategory { } return _default; } catch (ClassCastException ex) { - return preferences.getLong(key, _default); + try { + return preferences.getLong(key, _default); + } catch (ClassCastException ex2) { + removeConflictingPreferenceKeyValue(key); + return _default; + } } } @@ -117,7 +135,12 @@ public enum SharedPrefCategory { } return _default; } catch (ClassCastException ex) { - return preferences.getFloat(key, _default); + try { + return preferences.getFloat(key, _default); + } catch (ClassCastException ex2) { + removeConflictingPreferenceKeyValue(key); + return _default; + } } } diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ImportExportPreference.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ImportExportPreference.java new file mode 100644 index 00000000..ff6397e8 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ImportExportPreference.java @@ -0,0 +1,97 @@ +package app.revanced.integrations.settingsmenu; + +import static app.revanced.integrations.utils.StringRef.str; + +import android.app.AlertDialog; +import android.content.Context; +import android.os.Build; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.text.InputType; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.EditText; + +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + +public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener { + + private String existingSettings; + + private void init() { + setSelectable(true); + + EditText editText = getEditText(); + editText.setTextIsSelectable(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + editText.setAutofillHints((String) null); + } + editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 7); // Use a smaller font to reduce text wrap. + + setOnPreferenceClickListener(this); + } + + public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + public ImportExportPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + public ImportExportPreference(Context context) { + super(context); + init(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + try { + // Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened. + existingSettings = SettingsEnum.exportJSON(getContext()); + getEditText().setText(existingSettings); + } catch (Exception ex) { + LogHelper.printException(() -> "showDialog failure", ex); + } + return true; + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + try { + // Show the user the settings in JSON format. + builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> { + ReVancedUtils.setClipboard(getEditText().getText().toString()); + }).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> { + importSettings(getEditText().getText().toString()); + }); + } catch (Exception ex) { + LogHelper.printException(() -> "onPrepareDialogBuilder failure", ex); + } + } + + private void importSettings(String replacementSettings) { + try { + if (replacementSettings.equals(existingSettings)) { + return; + } + ReVancedSettingsFragment.settingImportInProgress = true; + final boolean rebootNeeded = SettingsEnum.importJSON(replacementSettings); + if (rebootNeeded) { + ReVancedSettingsFragment.showRebootDialog(getContext()); + } + } catch (Exception ex) { + LogHelper.printException(() -> "importSettings failure", ex); + } finally { + ReVancedSettingsFragment.settingImportInProgress = false; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingActivity.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingActivity.java index cda92cb9..e7cba140 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingActivity.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingActivity.java @@ -1,17 +1,18 @@ package app.revanced.integrations.settingsmenu; +import static app.revanced.integrations.utils.ReVancedUtils.getChildView; +import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier; + +import android.annotation.SuppressLint; +import android.app.Activity; import android.preference.PreferenceFragment; -import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.TextView; -import androidx.annotation.Nullable; - -import com.google.android.libraries.social.licenses.LicenseActivity; +import java.util.Objects; import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; import app.revanced.integrations.utils.ThemeHelper; public class ReVancedSettingActivity { @@ -19,76 +20,68 @@ public class ReVancedSettingActivity { /** * Injection point. */ - public static void setTheme(LicenseActivity base) { - final var whiteTheme = "Theme.YouTube.Settings"; - final var darkTheme = "Theme.YouTube.Settings.Dark"; - - final var theme = ThemeHelper.isDarkTheme() ? darkTheme : whiteTheme; - - LogHelper.printDebug(() -> "Using theme: " + theme); - base.setTheme(ReVancedUtils.getResourceIdentifier(theme, "style")); - } - - /** - * Injection point. - */ - public static void initializeSettings(LicenseActivity base) { - base.setContentView(ReVancedUtils.getResourceIdentifier("revanced_settings_with_toolbar", "layout")); - - PreferenceFragment preferenceFragment; - String preferenceIdentifier; - - String dataString = base.getIntent().getDataString(); - if (dataString.equalsIgnoreCase("sponsorblock_settings")) { - preferenceIdentifier = "sb_settings"; - preferenceFragment = new SponsorBlockSettingsFragment(); - } else if (dataString.equalsIgnoreCase("ryd_settings")) { - preferenceIdentifier = "revanced_ryd_settings_title"; - preferenceFragment = new ReturnYouTubeDislikeSettingsFragment(); - } else { - preferenceIdentifier = "revanced_settings"; - preferenceFragment = new ReVancedSettingsFragment(); - } - + public static void initializeSettings(Activity licenseActivity) { try { - TextView toolbar = getTextView((ViewGroup) base.findViewById(ReVancedUtils.getResourceIdentifier("toolbar", "id"))); - if (toolbar == null) { - // FIXME - // https://github.com/revanced/revanced-patches/issues/1384 - LogHelper.printDebug(() -> "Could not find toolbar"); - } else { - toolbar.setText(preferenceIdentifier); + ThemeHelper.setActivityTheme(licenseActivity); + licenseActivity.setContentView( + getResourceIdentifier("revanced_settings_with_toolbar", "layout")); + setBackButton(licenseActivity); + + PreferenceFragment fragment; + String toolbarTitleResourceName; + String dataString = licenseActivity.getIntent().getDataString(); + switch (dataString) { + case "sponsorblock_settings": + toolbarTitleResourceName = "revanced_sponsorblock_settings_title"; + fragment = new SponsorBlockSettingsFragment(); + break; + case "ryd_settings": + toolbarTitleResourceName = "revanced_ryd_settings_title"; + fragment = new ReturnYouTubeDislikeSettingsFragment(); + break; + case "revanced_settings": + toolbarTitleResourceName = "revanced_settings_title"; + fragment = new ReVancedSettingsFragment(); + break; + default: + LogHelper.printException(() -> "Unknown setting: " + dataString); + return; } - } catch (Exception e) { - LogHelper.printException(() -> "Could not set Toolbar title", e); + + setToolbarTitle(licenseActivity, toolbarTitleResourceName); + licenseActivity.getFragmentManager() + .beginTransaction() + .replace(getResourceIdentifier("revanced_settings_fragments", "id"), fragment) + .commit(); + } catch (Exception ex) { + LogHelper.printException(() -> "onCreate failure", ex); } - - base.getFragmentManager().beginTransaction().replace(ReVancedUtils.getResourceIdentifier("revanced_settings_fragments", "id"), preferenceFragment).commit(); } + private static void setToolbarTitle(Activity activity, String toolbarTitleResourceName) { + ViewGroup toolbar = activity.findViewById(getToolbarResourceId()); + TextView toolbarTextView = Objects.requireNonNull(getChildView(toolbar, view -> view instanceof TextView)); + toolbarTextView.setText(getResourceIdentifier(toolbarTitleResourceName, "string")); + } - @Nullable - public static T getView(Class typeClass, ViewGroup viewGroup) { - if (viewGroup == null) { - return null; + @SuppressLint("UseCompatLoadingForDrawables") + private static void setBackButton(Activity activity) { + ViewGroup toolbar = activity.findViewById(getToolbarResourceId()); + ImageButton imageButton = Objects.requireNonNull(getChildView(toolbar, view -> view instanceof ImageButton)); + final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme() + ? "yt_outline_arrow_left_white_24" + : "yt_outline_arrow_left_black_24", + "drawable"); + imageButton.setImageDrawable(activity.getResources().getDrawable(backButtonResource)); + imageButton.setOnClickListener(view -> activity.onBackPressed()); + } + + private static int getToolbarResourceId() { + final int toolbarResourceId = getResourceIdentifier("revanced_toolbar", "id"); + if (toolbarResourceId == 0) { + throw new IllegalStateException("Could not find back button resource"); } - int childCount = viewGroup.getChildCount(); - for (int i = 0; i < childCount; i++) { - View childAt = viewGroup.getChildAt(i); - if (childAt.getClass() == typeClass) { - return (T) childAt; - } - } - return null; + return toolbarResourceId; } - @Nullable - public static ImageButton getImageButton(ViewGroup viewGroup) { - return getView(ImageButton.class, viewGroup); - } - - @Nullable - public static TextView getTextView(ViewGroup viewGroup) { - return getView(TextView.class, viewGroup); - } } diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java index 16b427d2..9bbc8387 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java @@ -24,13 +24,40 @@ import androidx.annotation.Nullable; import com.google.android.apps.youtube.app.application.Shell_HomeActivity; -import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch; +import app.revanced.integrations.patches.playback.speed.CustomVideoSpeedPatch; import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.settings.SharedPrefCategory; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; public class ReVancedSettingsFragment extends PreferenceFragment { + /** + * Indicates that if a preference changes, + * to apply the change from the Setting to the UI component. + */ + static boolean settingImportInProgress; + + private static void reboot(@NonNull Context activity) { + final int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; + PendingIntent intent = PendingIntent.getActivity(activity, 0, + new Intent(activity, Shell_HomeActivity.class), intentFlags); + AlarmManager systemService = (AlarmManager) activity.getSystemService(Context.ALARM_SERVICE); + systemService.setExact(AlarmManager.ELAPSED_REALTIME, 1500L, intent); + Process.killProcess(Process.myPid()); + } + + static void showRebootDialog(@NonNull Context activity) { + String positiveButton = str("in_app_update_restart_button"); + String negativeButton = str("sign_in_cancel"); + new AlertDialog.Builder(activity).setMessage(str("pref_refresh_config")) + .setPositiveButton(positiveButton, (dialog, id) -> { + reboot(activity); + }) + .setNegativeButton(negativeButton, null) + .setCancelable(false) + .show(); + } + /** * Used to prevent showing reboot dialog, if user cancels a setting user dialog. */ @@ -42,33 +69,53 @@ public class ReVancedSettingsFragment extends PreferenceFragment { if (setting == null) { return; } - Preference pref = this.findPreference(str); - LogHelper.printDebug(() -> "Setting " + setting.name() + " was changed. Preference " + str + ": " + pref); + Preference pref = findPreference(str); + LogHelper.printDebug(() -> setting.name() + ": " + " setting value:" + setting.getObjectValue() + " pref:" + pref); + if (pref == null) { + return; + } if (pref instanceof SwitchPreference) { SwitchPreference switchPref = (SwitchPreference) pref; - SettingsEnum.setValue(setting, switchPref.isChecked()); + if (settingImportInProgress) { + switchPref.setChecked(setting.getBoolean()); + } else { + SettingsEnum.setValue(setting, switchPref.isChecked()); + } } else if (pref instanceof EditTextPreference) { - String editText = ((EditTextPreference) pref).getText(); - SettingsEnum.setValue(setting, editText); + EditTextPreference editPreference = (EditTextPreference) pref; + if (settingImportInProgress) { + editPreference.getEditText().setText(setting.getObjectValue().toString()); + } else { + SettingsEnum.setValue(setting, editPreference.getText()); + } } else if (pref instanceof ListPreference) { ListPreference listPref = (ListPreference) pref; - SettingsEnum.setValue(setting, listPref.getValue()); + if (settingImportInProgress) { + listPref.setValue(setting.getObjectValue().toString()); + } else { + SettingsEnum.setValue(setting, listPref.getValue()); + } updateListPreferenceSummary((ListPreference) pref, setting); } else { LogHelper.printException(() -> "Setting cannot be handled: " + pref.getClass() + " " + pref); return; } + enableDisablePreferences(); + + if (settingImportInProgress) { + return; + } + if (!showingUserDialogMessage) { if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) { showSettingUserDialogConfirmation(getActivity(), (SwitchPreference) pref, setting); } else if (setting.rebootApp) { - rebootDialog(getActivity()); + showRebootDialog(getActivity()); } } - enableDisablePreferences(); } catch (Exception ex) { LogHelper.printException(() -> "OnSharedPreferenceChangeListener failure", ex); } @@ -88,20 +135,24 @@ public class ReVancedSettingsFragment extends PreferenceFragment { // if the preference was included, then initialize it based on the available playback speed Preference defaultSpeedPreference = findPreference(SettingsEnum.PLAYBACK_SPEED_DEFAULT.path); if (defaultSpeedPreference instanceof ListPreference) { - RememberPlaybackSpeedPatch.initializeListPreference((ListPreference) defaultSpeedPreference); + CustomVideoSpeedPatch.initializeListPreference((ListPreference) defaultSpeedPreference); } - // set the summary text for any ListPreferences + // Set current value from SettingsEnum for (SettingsEnum setting : SettingsEnum.values()) { Preference preference = findPreference(setting.path); - if (preference instanceof ListPreference) { + if (preference instanceof SwitchPreference) { + ((SwitchPreference) preference).setChecked(setting.getBoolean()); + } else if (preference instanceof EditTextPreference) { + ((EditTextPreference) preference).setText(setting.getObjectValue().toString()); + } else if (preference instanceof ListPreference) { updateListPreferenceSummary((ListPreference) preference, setting); } } preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener); } catch (Exception ex) { - LogHelper.printException(() -> "onActivityCreated() error", ex); + LogHelper.printException(() -> "onActivityCreated() failure", ex); } } @@ -120,34 +171,18 @@ public class ReVancedSettingsFragment extends PreferenceFragment { } } + /** + * Sets summary text to the currently selected list option. + */ private void updateListPreferenceSummary(ListPreference listPreference, SettingsEnum setting) { - final int entryIndex = listPreference.findIndexOfValue(setting.getObjectValue().toString()); + String objectStringValue = setting.getObjectValue().toString(); + final int entryIndex = listPreference.findIndexOfValue(objectStringValue); if (entryIndex >= 0) { listPreference.setSummary(listPreference.getEntries()[entryIndex]); + listPreference.setValue(objectStringValue); } } - private void reboot(@NonNull Activity activity) { - final int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - PendingIntent intent = PendingIntent.getActivity(activity, 0, - new Intent(activity, Shell_HomeActivity.class), intentFlags); - AlarmManager systemService = (AlarmManager) activity.getSystemService(Context.ALARM_SERVICE); - systemService.setExact(AlarmManager.ELAPSED_REALTIME, 1500L, intent); - Process.killProcess(Process.myPid()); - } - - private void rebootDialog(@NonNull Activity activity) { - String positiveButton = str("in_app_update_restart_button"); - String negativeButton = str("sign_in_cancel"); - new AlertDialog.Builder(activity).setMessage(str("pref_refresh_config")) - .setPositiveButton(positiveButton, (dialog, id) -> { - reboot(activity); - }) - .setNegativeButton(negativeButton, null) - .setCancelable(false) - .show(); - } - private void showSettingUserDialogConfirmation(@NonNull Activity activity, SwitchPreference switchPref, SettingsEnum setting) { showingUserDialogMessage = true; new AlertDialog.Builder(activity) @@ -155,7 +190,7 @@ public class ReVancedSettingsFragment extends PreferenceFragment { .setMessage(setting.userDialogMessage.toString()) .setPositiveButton(android.R.string.ok, (dialog, id) -> { if (setting.rebootApp) { - rebootDialog(activity); + showRebootDialog(activity); } }) .setNegativeButton(android.R.string.cancel, (dialog, id) -> { diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ResettableEditTextPreference.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ResettableEditTextPreference.java new file mode 100644 index 00000000..f4a47e8a --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ResettableEditTextPreference.java @@ -0,0 +1,63 @@ +package app.revanced.integrations.settingsmenu; + +import static app.revanced.integrations.utils.StringRef.str; + +import android.app.AlertDialog; +import android.content.Context; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.util.AttributeSet; +import android.widget.Button; +import android.widget.EditText; + +import java.util.Objects; + +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; + +public class ResettableEditTextPreference extends EditTextPreference { + + public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ResettableEditTextPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ResettableEditTextPreference(Context context) { + super(context); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + super.onPrepareDialogBuilder(builder); + SettingsEnum setting = SettingsEnum.settingFromPath(getKey()); + if (setting != null) { + builder.setNeutralButton(str("revanced_settings_reset"), null); + } + } + + @Override + protected void showDialog(Bundle state) { + super.showDialog(state); + + // Override the button click listener to prevent dismissing the dialog. + Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL); + if (button == null) { + return; + } + button.setOnClickListener(v -> { + try { + SettingsEnum setting = Objects.requireNonNull(SettingsEnum.settingFromPath(getKey())); + String defaultStringValue = setting.defaultValue.toString(); + EditText editText = getEditText(); + editText.setText(defaultStringValue); + editText.setSelection(defaultStringValue.length()); // move cursor to end of text + } catch (Exception ex) { + LogHelper.printException(() -> "reset failure", ex); + } + }); + } +} diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java index 6a8a0bb2..dadc85be 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java @@ -19,6 +19,11 @@ import app.revanced.integrations.settings.SharedPrefCategory; public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { + /** + * If dislikes are shown on Shorts. + */ + private SwitchPreference shortsPreference; + /** * If dislikes are shown as percentage. */ @@ -29,9 +34,16 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { */ private SwitchPreference compactLayoutPreference; + /** + * If segmented like/dislike button uses smaller compact layout. + */ + private SwitchPreference toastOnRYDNotAvailable; + private void updateUIState() { - percentagePreference.setEnabled(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.isAvailable()); - compactLayoutPreference.setEnabled(SettingsEnum.RYD_USE_COMPACT_LAYOUT.isAvailable()); + shortsPreference.setEnabled(SettingsEnum.RYD_SHORTS.isAvailable()); + percentagePreference.setEnabled(SettingsEnum.RYD_DISLIKE_PERCENTAGE.isAvailable()); + compactLayoutPreference.setEnabled(SettingsEnum.RYD_COMPACT_LAYOUT.isAvailable()); + toastOnRYDNotAvailable.setEnabled(SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.isAvailable()); } @Override @@ -58,13 +70,25 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { }); preferenceScreen.addPreference(enabledPreference); + shortsPreference = new SwitchPreference(context); + shortsPreference.setChecked(SettingsEnum.RYD_SHORTS.getBoolean()); + shortsPreference.setTitle(str("revanced_ryd_shorts_title")); + shortsPreference.setSummaryOn(str("revanced_ryd_shorts_summary_on")); + shortsPreference.setSummaryOff(str("revanced_ryd_shorts_summary_off")); + shortsPreference.setOnPreferenceChangeListener((pref, newValue) -> { + SettingsEnum.RYD_SHORTS.saveValue(newValue); + updateUIState(); + return true; + }); + preferenceScreen.addPreference(shortsPreference); + percentagePreference = new SwitchPreference(context); - percentagePreference.setChecked(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean()); + percentagePreference.setChecked(SettingsEnum.RYD_DISLIKE_PERCENTAGE.getBoolean()); percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title")); percentagePreference.setSummaryOn(str("revanced_ryd_dislike_percentage_summary_on")); percentagePreference.setSummaryOff(str("revanced_ryd_dislike_percentage_summary_off")); percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> { - SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.saveValue(newValue); + SettingsEnum.RYD_DISLIKE_PERCENTAGE.saveValue(newValue); ReturnYouTubeDislike.clearCache(); updateUIState(); return true; @@ -72,18 +96,30 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { preferenceScreen.addPreference(percentagePreference); compactLayoutPreference = new SwitchPreference(context); - compactLayoutPreference.setChecked(SettingsEnum.RYD_USE_COMPACT_LAYOUT.getBoolean()); + compactLayoutPreference.setChecked(SettingsEnum.RYD_COMPACT_LAYOUT.getBoolean()); compactLayoutPreference.setTitle(str("revanced_ryd_compact_layout_title")); compactLayoutPreference.setSummaryOn(str("revanced_ryd_compact_layout_summary_on")); compactLayoutPreference.setSummaryOff(str("revanced_ryd_compact_layout_summary_off")); compactLayoutPreference.setOnPreferenceChangeListener((pref, newValue) -> { - SettingsEnum.RYD_USE_COMPACT_LAYOUT.saveValue(newValue); + SettingsEnum.RYD_COMPACT_LAYOUT.saveValue(newValue); ReturnYouTubeDislike.clearCache(); updateUIState(); return true; }); preferenceScreen.addPreference(compactLayoutPreference); + toastOnRYDNotAvailable = new SwitchPreference(context); + toastOnRYDNotAvailable.setChecked(SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.getBoolean()); + toastOnRYDNotAvailable.setTitle(str("ryd_toast_on_connection_error_title")); + toastOnRYDNotAvailable.setSummaryOn(str("ryd_toast_on_connection_error_summary_on")); + toastOnRYDNotAvailable.setSummaryOff(str("ryd_toast_on_connection_error_summary_off")); + toastOnRYDNotAvailable.setOnPreferenceChangeListener((pref, newValue) -> { + SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.saveValue(newValue); + updateUIState(); + return true; + }); + preferenceScreen.addPreference(toastOnRYDNotAvailable); + updateUIState(); diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java index 9a9e0141..85bac3ab 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java @@ -20,6 +20,7 @@ import android.preference.PreferenceScreen; import android.preference.SwitchPreference; import android.text.Html; import android.text.InputType; +import android.util.TypedValue; import android.widget.EditText; import androidx.annotation.NonNull; @@ -51,6 +52,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { private SwitchPreference showSkipToast; private SwitchPreference trackSkips; private SwitchPreference showTimeWithoutSegments; + private SwitchPreference toastOnConnectionError; private EditTextPreference newSegmentStep; private EditTextPreference minSegmentDuration; @@ -67,41 +69,44 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { if (!enabled) { SponsorBlockViewController.hideAll(); SegmentPlaybackController.setCurrentVideoId(null); - } else if (!SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean()) { + } else if (!SettingsEnum.SB_CREATE_NEW_SEGMENT.getBoolean()) { SponsorBlockViewController.hideNewSegmentLayout(); } // voting and add new segment buttons automatically shows/hides themselves sbEnabled.setChecked(enabled); - addNewSegment.setChecked(SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean()); + addNewSegment.setChecked(SettingsEnum.SB_CREATE_NEW_SEGMENT.getBoolean()); addNewSegment.setEnabled(enabled); - votingEnabled.setChecked(SettingsEnum.SB_VOTING_ENABLED.getBoolean()); + votingEnabled.setChecked(SettingsEnum.SB_VOTING_BUTTON.getBoolean()); votingEnabled.setEnabled(enabled); - compactSkipButton.setChecked(SettingsEnum.SB_USE_COMPACT_SKIP_BUTTON.getBoolean()); + compactSkipButton.setChecked(SettingsEnum.SB_COMPACT_SKIP_BUTTON.getBoolean()); compactSkipButton.setEnabled(enabled); autoHideSkipSegmentButton.setChecked(SettingsEnum.SB_AUTO_HIDE_SKIP_BUTTON.getBoolean()); autoHideSkipSegmentButton.setEnabled(enabled); - showSkipToast.setChecked(SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean()); + showSkipToast.setChecked(SettingsEnum.SB_TOAST_ON_SKIP.getBoolean()); showSkipToast.setEnabled(enabled); + toastOnConnectionError.setChecked(SettingsEnum.SB_TOAST_ON_CONNECTION_ERROR.getBoolean()); + toastOnConnectionError.setEnabled(enabled); + trackSkips.setChecked(SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean()); trackSkips.setEnabled(enabled); - showTimeWithoutSegments.setChecked(SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()); + showTimeWithoutSegments.setChecked(SettingsEnum.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.getBoolean()); showTimeWithoutSegments.setEnabled(enabled); - newSegmentStep.setText(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getObjectValue().toString()); + newSegmentStep.setText(SettingsEnum.SB_CREATE_NEW_SEGMENT_STEP.getObjectValue().toString()); newSegmentStep.setEnabled(enabled); - minSegmentDuration.setText(SettingsEnum.SB_MIN_DURATION.getObjectValue().toString()); + minSegmentDuration.setText(SettingsEnum.SB_SEGMENT_MIN_DURATION.getObjectValue().toString()); minSegmentDuration.setEnabled(enabled); - privateUserId.setText(SettingsEnum.SB_UUID.getString()); + privateUserId.setText(SettingsEnum.SB_PRIVATE_USER_ID.getString()); privateUserId.setEnabled(enabled); apiUrl.setEnabled(enabled); @@ -171,7 +176,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { votingEnabled.setSummaryOff(str("sb_enable_voting_sum_off")); category.addPreference(votingEnabled); votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> { - SettingsEnum.SB_VOTING_ENABLED.saveValue(newValue); + SettingsEnum.SB_VOTING_BUTTON.saveValue(newValue); updateUI(); return true; }); @@ -182,7 +187,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { compactSkipButton.setSummaryOff(str("sb_enable_compact_skip_button_sum_off")); category.addPreference(compactSkipButton); compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> { - SettingsEnum.SB_USE_COMPACT_SKIP_BUTTON.saveValue(newValue); + SettingsEnum.SB_COMPACT_SKIP_BUTTON.saveValue(newValue); updateUI(); return true; }); @@ -207,7 +212,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { return false; }); showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> { - SettingsEnum.SB_SHOW_TOAST_ON_SKIP.saveValue(newValue); + SettingsEnum.SB_TOAST_ON_SKIP.saveValue(newValue); updateUI(); return true; }); @@ -218,7 +223,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { showTimeWithoutSegments.setSummaryOn(str("sb_general_time_without_sum_on")); showTimeWithoutSegments.setSummaryOff(str("sb_general_time_without_sum_off")); showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> { - SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(newValue); + SettingsEnum.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.saveValue(newValue); updateUI(); return true; }); @@ -247,7 +252,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { .setCancelable(false) .show(); } - SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.saveValue(newValue); + SettingsEnum.SB_CREATE_NEW_SEGMENT.saveValue(newValue); updateUI(); return true; }); @@ -262,7 +267,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { ReVancedUtils.showToastLong(str("sb_general_adjusting_invalid")); return false; } - SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.saveValue(newAdjustmentValue); + SettingsEnum.SB_CREATE_NEW_SEGMENT_STEP.saveValue(newAdjustmentValue); return true; }); category.addPreference(newSegmentStep); @@ -282,6 +287,17 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { screen.addPreference(category); category.setTitle(str("sb_general")); + toastOnConnectionError = new SwitchPreference(context); + toastOnConnectionError.setTitle(str("sb_toast_on_connection_error_title")); + toastOnConnectionError.setSummaryOn(str("sb_toast_on_connection_error_summary_on")); + toastOnConnectionError.setSummaryOff(str("sb_toast_on_connection_error_summary_off")); + toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> { + SettingsEnum.SB_TOAST_ON_CONNECTION_ERROR.saveValue(newValue); + updateUI(); + return true; + }); + category.addPreference(toastOnConnectionError); + trackSkips = new SwitchPreference(context); trackSkips.setTitle(str("sb_general_skipcount")); trackSkips.setSummaryOn(str("sb_general_skipcount_sum_on")); @@ -298,7 +314,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { minSegmentDuration.setSummary(str("sb_general_min_duration_sum")); minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> { - SettingsEnum.SB_MIN_DURATION.saveValue(Float.valueOf(newValue.toString())); + SettingsEnum.SB_SEGMENT_MIN_DURATION.saveValue(Float.valueOf(newValue.toString())); return true; }); category.addPreference(minSegmentDuration); @@ -312,7 +328,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { ReVancedUtils.showToastLong(str("sb_general_uuid_invalid")); return false; } - SettingsEnum.SB_UUID.saveValue(newUUID); + SettingsEnum.SB_PRIVATE_USER_ID.saveValue(newUUID); fetchAndDisplayStats(); return true; }); @@ -351,9 +367,22 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { }); category.addPreference(apiUrl); - importExport = new EditTextPreference(context); + importExport = new EditTextPreference(context) { + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + builder.setNeutralButton(str("sb_settings_copy"), (dialog, which) -> { + ReVancedUtils.setClipboard(getEditText().getText().toString()); + }); + } + }; importExport.setTitle(str("sb_settings_ie")); importExport.setSummary(str("sb_settings_ie_sum")); + importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_FLAG_MULTI_LINE + | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + importExport.getEditText().setAutofillHints((String) null); + } + importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); importExport.setOnPreferenceClickListener(preference1 -> { importExport.getEditText().setText(SponsorBlockSettings.exportSettings()); return true; @@ -419,6 +448,12 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { private void fetchAndDisplayStats() { try { statsCategory.removeAll(); + if (!SponsorBlockSettings.userHasSBPrivateId()) { + // User has never voted or created any segments. No stats to show. + addLocalUserStats(); + return; + } + Preference loadingPlaceholderPreference = new Preference(this.getActivity()); loadingPlaceholderPreference.setEnabled(false); statsCategory.addPreference(loadingPlaceholderPreference); @@ -428,6 +463,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { UserStats stats = SBRequester.retrieveUserStats(); ReVancedUtils.runOnMainThread(() -> { // get back on main thread to modify UI elements addUserStats(loadingPlaceholderPreference, stats); + addLocalUserStats(); }); }); } else { @@ -450,7 +486,8 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { statsCategory.removeAll(); Context context = statsCategory.getContext(); - { + if (stats.totalSegmentCountIncludingIgnored > 0) { + // If user has not created any segments, there's no reason to set a username. EditTextPreference preference = new EditTextPreference(context); statsCategory.addPreference(preference); String userName = stats.userName; @@ -482,7 +519,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { statsCategory.addPreference(preference); String formatted = statsNumberOfSegmentsSkippedFormatter.format(stats.segmentCount); preference.setTitle(fromHtml(str("sb_stats_submissions", formatted))); - if (stats.segmentCount == 0) { + if (stats.totalSegmentCountIncludingIgnored == 0) { preference.setSelectable(false); } else { preference.setOnPreferenceClickListener(preference1 -> { @@ -512,7 +549,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { String stats_saved; String stats_saved_sum; - if (stats.segmentCount == 0) { + if (stats.totalSegmentCountIncludingIgnored == 0) { stats_saved = str("sb_stats_saved_zero"); stats_saved_sum = str("sb_stats_saved_sum_zero"); } else { @@ -528,34 +565,34 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { return false; }); } - - { - // time the user saved by using SB - Preference preference = new Preference(context); - statsCategory.addPreference(preference); - - Runnable updateStatsSelfSaved = () -> { - String formatted = statsNumberOfSegmentsSkippedFormatter.format(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getInt()); - preference.setTitle(fromHtml(str("sb_stats_self_saved", formatted))); - String formattedSaved = SponsorBlockUtils.getTimeSavedString(SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getLong() / 1000); - preference.setSummary(fromHtml(str("sb_stats_self_saved_sum", formattedSaved))); - }; - updateStatsSelfSaved.run(); - preference.setOnPreferenceClickListener(preference1 -> { - new AlertDialog.Builder(preference1.getContext()) - .setTitle(str("sb_stats_self_saved_reset_title")) - .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { - SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.defaultValue); - SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.defaultValue); - updateStatsSelfSaved.run(); - }) - .setNegativeButton(android.R.string.no, null).show(); - return true; - }); - } } catch (Exception ex) { - LogHelper.printException(() -> "fetchAndDisplayStats failure", ex); + LogHelper.printException(() -> "addUserStats failure", ex); } } + private void addLocalUserStats() { + // time the user saved by using SB + Preference preference = new Preference(statsCategory.getContext()); + statsCategory.addPreference(preference); + + Runnable updateStatsSelfSaved = () -> { + String formatted = statsNumberOfSegmentsSkippedFormatter.format(SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.getInt()); + preference.setTitle(fromHtml(str("sb_stats_self_saved", formatted))); + String formattedSaved = SponsorBlockUtils.getTimeSavedString(SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.getLong() / 1000); + preference.setSummary(fromHtml(str("sb_stats_self_saved_sum", formattedSaved))); + }; + updateStatsSelfSaved.run(); + preference.setOnPreferenceClickListener(preference1 -> { + new AlertDialog.Builder(preference1.getContext()) + .setTitle(str("sb_stats_self_saved_reset_title")) + .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { + SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.saveValue(SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.defaultValue); + SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.saveValue(SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.defaultValue); + updateStatsSelfSaved.run(); + }) + .setNegativeButton(android.R.string.no, null).show(); + return true; + }); + } + } diff --git a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt index 2cf2c4a9..4992170f 100644 --- a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt +++ b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt @@ -1,44 +1,63 @@ package app.revanced.integrations.shared import app.revanced.integrations.utils.Event +import app.revanced.integrations.utils.LogHelper /** * WatchWhile player type */ -@Suppress("unused") enum class PlayerType { - NONE, // includes Shorts and Stories playback - HIDDEN, // A Shorts or Stories, if a regular video is minimized and a Short/Story is then opened + /** + * Includes Shorts and Stories playback. + */ + NONE, + /** + * A Shorts or Stories, if a regular video is minimized and a Short/Story is then opened. + */ + HIDDEN, + /** + * When spoofing to an old version of YouTube, and watching a short with a regular video in the background, + * the type will be this (and not [HIDDEN]). + */ WATCH_WHILE_MINIMIZED, WATCH_WHILE_MAXIMIZED, WATCH_WHILE_FULLSCREEN, WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN, WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED, + /** + * When opening a short while a regular video is minimized, the type can momentarily be this. + */ WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED, WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED, - INLINE_MINIMAL, // home feed video playback + /** + * Home feed video playback. + */ + INLINE_MINIMAL, VIRTUAL_REALITY_FULLSCREEN, WATCH_WHILE_PICTURE_IN_PICTURE; companion object { - /** - * safely parse from a string - * - * @param name the name to find - * @return the enum constant, or null if not found - */ + + private val nameToPlayerType = values().associateBy { it.name } + @JvmStatic - fun safeParseFromString(name: String): PlayerType? { - return values().firstOrNull { it.name == name } + fun setFromString(enumName: String) { + val newType = nameToPlayerType[enumName] + if (newType == null) { + LogHelper.printException { "Unknown PlayerType encountered: $enumName" } + } else if (current != newType) { + LogHelper.printDebug { "PlayerType changed to: $newType" } + current = newType + } } /** - * the current player type, as reported by [app.revanced.integrations.patches.PlayerTypeHookPatch.YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX] + * The current player type. */ @JvmStatic var current get() = currentPlayerType - set(value) { + private set(value) { currentPlayerType = value onChange(currentPlayerType) } @@ -53,11 +72,30 @@ enum class PlayerType { } /** - * Check if the current player type is [NONE] or [HIDDEN] + * Check if the current player type is [NONE] or [HIDDEN]. + * Useful to check if a short is currently playing. * - * @return True, if nothing, a Short, or a Story is playing. + * Does not include the first moment after a short is opened when a regular video is minimized on screen, + * or while watching a short with a regular video present on a spoofed old version of YouTube. + * To include those situations instead use [isNoneHiddenOrMinimized]. */ fun isNoneOrHidden(): Boolean { return this == NONE || this == HIDDEN } + + /** + * Check if the current player type is [NONE], [HIDDEN], [WATCH_WHILE_MINIMIZED], [WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED]. + * + * Useful to check if a Short is being played, + * although can return false positive if the player is minimized. + * + * @return If nothing, a Short, a Story, + * or a regular video is minimized video or sliding off screen to a dismissed or hidden state. + */ + fun isNoneHiddenOrMinimized(): Boolean { + return this == NONE || this == HIDDEN + || this == WATCH_WHILE_MINIMIZED + || this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED + } + } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/shared/VideoState.kt b/app/src/main/java/app/revanced/integrations/shared/VideoState.kt new file mode 100644 index 00000000..977b9181 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/VideoState.kt @@ -0,0 +1,48 @@ +package app.revanced.integrations.shared + +import app.revanced.integrations.utils.LogHelper +import app.revanced.integrations.patches.VideoInformation + +/** + * VideoState playback state. + */ +enum class VideoState { + NEW, + PLAYING, + PAUSED, + RECOVERABLE_ERROR, + UNRECOVERABLE_ERROR, + /** + * @see [VideoInformation.isAtEndOfVideo] + */ + ENDED; + + companion object { + + private val nameToVideoState = values().associateBy { it.name } + + @JvmStatic + fun setFromString(enumName: String) { + val state = nameToVideoState[enumName] + if (state == null) { + LogHelper.printException { "Unknown VideoState encountered: $enumName" } + } else if (currentVideoState != state) { + LogHelper.printDebug { "VideoState changed to: $state" } + currentVideoState = state + } + } + + /** + * Depending on which hook this is called from, + * this value may not be up to date with the actual playback state. + */ + @JvmStatic + var current: VideoState? + get() = currentVideoState + private set(value) { + currentVideoState = value + } + + private var currentVideoState : VideoState? = null + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java index 918e657e..e6bb328d 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java @@ -1,25 +1,15 @@ package app.revanced.integrations.sponsorblock; -import static app.revanced.integrations.utils.StringRef.str; - import android.graphics.Canvas; import android.graphics.Rect; import android.text.TextUtils; import android.util.TypedValue; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; - import app.revanced.integrations.patches.VideoInformation; import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.shared.PlayerType; +import app.revanced.integrations.shared.VideoState; import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour; import app.revanced.integrations.sponsorblock.objects.SegmentCategory; import app.revanced.integrations.sponsorblock.objects.SponsorSegment; @@ -28,6 +18,11 @@ import app.revanced.integrations.sponsorblock.ui.SponsorBlockViewController; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; +import java.lang.reflect.Field; +import java.util.*; + +import static app.revanced.integrations.utils.StringRef.str; + /** * Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video. * @@ -38,12 +33,12 @@ public class SegmentPlaybackController { * Length of time to show a skip button for a highlight segment, * or a regular segment if {@link SettingsEnum#SB_AUTO_HIDE_SKIP_BUTTON} is enabled. * - * Because Effectively, this value is rounded up to the next second. + * Effectively this value is rounded up to the next second. */ private static final long DURATION_TO_SHOW_SKIP_BUTTON = 3800; /* - * Highlight segments have zero length, as they are a point in time. + * Highlight segments have zero length as they are a point in time. * Draw them on screen using a fixed width bar. * Value is independent of device dpi. */ @@ -102,9 +97,9 @@ public class SegmentPlaybackController { @Nullable private static String timeWithoutSegments; - private static float sponsorBarLeft = 1f; - private static float sponsorBarRight = 1f; - private static float sponsorBarThickness = 2f; + private static int sponsorBarAbsoluteLeft; + private static int sponsorAbsoluteBarRight; + private static int sponsorBarThickness; @Nullable static SponsorSegment[] getSegments() { @@ -177,7 +172,7 @@ public class SegmentPlaybackController { * Injection point. * Initializes SponsorBlock when the video player starts playing a new video. */ - public static void initialize(Object _o) { + public static void initialize(Object ignoredPlayerController) { try { ReVancedUtils.verifyOnMainThread(); SponsorBlockSettings.initialize(); @@ -235,7 +230,7 @@ public class SegmentPlaybackController { SponsorSegment[] segments = SBRequester.getSegments(videoId); ReVancedUtils.runOnMainThread(()-> { - if (!videoId.equals(SegmentPlaybackController.currentVideoId)) { + if (!videoId.equals(currentVideoId)) { // user changed videos before get segments network call could complete LogHelper.printDebug(() -> "Ignoring segments for prior video: " + videoId); return; @@ -522,17 +517,21 @@ public class SegmentPlaybackController { return; } + final boolean videoIsPaused = VideoState.getCurrent() == VideoState.PAUSED; if (!userManuallySkipped) { // check for any smaller embedded segments, and count those as autoskipped - final boolean showSkipToast = SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean(); - for (final SponsorSegment otherSegment : segments) { + final boolean showSkipToast = SettingsEnum.SB_TOAST_ON_SKIP.getBoolean(); + for (final SponsorSegment otherSegment : Objects.requireNonNull(segments)) { if (segmentToSkip.end < otherSegment.start) { break; // no other segments can be contained } if (otherSegment == segmentToSkip || (otherSegment.category != SegmentCategory.HIGHLIGHT && segmentToSkip.containsSegment(otherSegment))) { otherSegment.didAutoSkipped = true; - if (showSkipToast) { + // Do not show a toast if the user is scrubbing thru a paused video. + // Cannot do this video state check in setTime or earlier in this method, as the video state may not be up to date. + // So instead, only hide toasts because all other skip logic done while paused causes no harm. + if (showSkipToast && !videoIsPaused) { showSkippedSegmentToast(otherSegment); } } @@ -542,7 +541,7 @@ public class SegmentPlaybackController { if (segmentToSkip.category == SegmentCategory.UNSUBMITTED) { removeUnsubmittedSegments(); SponsorBlockUtils.setNewSponsorSegmentPreviewed(); - } else { + } else if (!videoIsPaused) { SponsorBlockUtils.sendViewRequestAsync(segmentToSkip); } } catch (Exception ex) { @@ -599,20 +598,6 @@ public class SegmentPlaybackController { } } - /** - * Injection point. - */ - public static void setSponsorBarAbsoluteLeft(final Rect rect) { - setSponsorBarAbsoluteLeft(rect.left); - } - - public static void setSponsorBarAbsoluteLeft(final float left) { - if (sponsorBarLeft != left) { - LogHelper.printDebug(() -> String.format("setSponsorBarAbsoluteLeft: left=%.2f", left)); - sponsorBarLeft = left; - } - } - /** * Injection point */ @@ -620,42 +605,36 @@ public class SegmentPlaybackController { try { Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect"); field.setAccessible(true); - Rect rect = (Rect) field.get(self); - if (rect == null) { - LogHelper.printException(() -> "Could not find sponsorblock rect"); - } else { - setSponsorBarAbsoluteLeft(rect.left); - setSponsorBarAbsoluteRight(rect.right); - } + Rect rect = (Rect) Objects.requireNonNull(field.get(self)); + setSponsorBarAbsoluteLeft(rect); + setSponsorBarAbsoluteRight(rect); } catch (Exception ex) { LogHelper.printException(() -> "setSponsorBarRect failure", ex); } } - /** - * Injection point. - */ - public static void setSponsorBarAbsoluteRight(final Rect rect) { - setSponsorBarAbsoluteRight(rect.right); + private static void setSponsorBarAbsoluteLeft(Rect rect) { + final int left = rect.left; + if (sponsorBarAbsoluteLeft != left) { + LogHelper.printDebug(() -> "setSponsorBarAbsoluteLeft: " + left); + sponsorBarAbsoluteLeft = left; + } } - public static void setSponsorBarAbsoluteRight(final float right) { - if (sponsorBarRight != right) { - LogHelper.printDebug(() -> String.format("setSponsorBarAbsoluteRight: right=%.2f", right)); - sponsorBarRight = right; + private static void setSponsorBarAbsoluteRight(Rect rect) { + final int right = rect.right; + if (sponsorAbsoluteBarRight != right) { + LogHelper.printDebug(() -> "setSponsorBarAbsoluteRight: " + right); + sponsorAbsoluteBarRight = right; } } /** * Injection point */ - public static void setSponsorBarThickness(final int thickness) { - setSponsorBarThickness((float) thickness); - } - - public static void setSponsorBarThickness(final float thickness) { + public static void setSponsorBarThickness(int thickness) { if (sponsorBarThickness != thickness) { - LogHelper.printDebug(() -> String.format("setSponsorBarThickness: %.2f", thickness)); + LogHelper.printDebug(() -> "setSponsorBarThickness: " + thickness); sponsorBarThickness = thickness; } } @@ -665,7 +644,7 @@ public class SegmentPlaybackController { */ public static String appendTimeWithoutSegments(String totalTime) { try { - if (SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean() + if (SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.getBoolean() && !TextUtils.isEmpty(totalTime) && !TextUtils.isEmpty(timeWithoutSegments)) { // Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages return "\u202D" + totalTime + timeWithoutSegments; // u202D = left to right override @@ -679,7 +658,7 @@ public class SegmentPlaybackController { private static void calculateTimeWithoutSegments() { final long currentVideoLength = VideoInformation.getVideoLength(); - if (!SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean() || currentVideoLength <= 0 + if (!SettingsEnum.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.getBoolean() || currentVideoLength <= 0 || segments == null || segments.length == 0) { timeWithoutSegments = null; return; @@ -736,25 +715,23 @@ public class SegmentPlaybackController { */ public static void drawSponsorTimeBars(final Canvas canvas, final float posY) { try { - if (sponsorBarThickness < 0.1) return; if (segments == null) return; final long videoLength = VideoInformation.getVideoLength(); if (videoLength <= 0) return; - final float thicknessDiv2 = sponsorBarThickness / 2; - final float top = posY - thicknessDiv2; + final int thicknessDiv2 = sponsorBarThickness / 2; // rounds down + final float top = posY - (sponsorBarThickness - thicknessDiv2); final float bottom = posY + thicknessDiv2; - final float absoluteLeft = sponsorBarLeft; - final float absoluteRight = sponsorBarRight; + final float videoMillisecondsToPixels = (1f / videoLength) * (sponsorAbsoluteBarRight - sponsorBarAbsoluteLeft); + final float leftPadding = sponsorBarAbsoluteLeft; - final float tmp1 = (1f / videoLength) * (absoluteRight - absoluteLeft); for (SponsorSegment segment : segments) { - final float left = segment.start * tmp1 + absoluteLeft; + final float left = leftPadding + segment.start * videoMillisecondsToPixels; final float right; if (segment.category == SegmentCategory.HIGHLIGHT) { right = left + getHighlightSegmentTimeBarScreenWidth(); } else { - right = segment.end * tmp1 + absoluteLeft; + right = leftPadding + segment.end * videoMillisecondsToPixels; } canvas.drawRect(left, top, right, bottom, segment.category.paint); } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java index 5565ac5b..595a037e 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java @@ -2,10 +2,13 @@ package app.revanced.integrations.sponsorblock; import static app.revanced.integrations.utils.StringRef.str; +import android.app.AlertDialog; +import android.content.Context; import android.content.SharedPreferences; import android.util.Patterns; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; @@ -21,6 +24,10 @@ import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; public class SponsorBlockSettings { + /** + * Minimum length a SB user id must be, as set by SB API. + */ + private static final int SB_PRIVATE_USER_ID_MINIMUM_LENGTH = 30; public static void importSettings(@NonNull String json) { ReVancedUtils.verifyOnMainThread(); @@ -66,43 +73,43 @@ public class SponsorBlockSettings { } editor.apply(); - String userID = settingsJson.getString("userID"); - if (!isValidSBUserId(userID)) { - throw new IllegalArgumentException("userId is blank"); + 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)) { + SettingsEnum.SB_PRIVATE_USER_ID.saveValue(userID); + } } - SettingsEnum.SB_UUID.saveValue(userID); - - SettingsEnum.SB_IS_VIP.saveValue(settingsJson.getBoolean("isVip")); - SettingsEnum.SB_SHOW_TOAST_ON_SKIP.saveValue(!settingsJson.getBoolean("dontShowNotice")); + SettingsEnum.SB_USER_IS_VIP.saveValue(settingsJson.getBoolean("isVip")); + SettingsEnum.SB_TOAST_ON_SKIP.saveValue(!settingsJson.getBoolean("dontShowNotice")); SettingsEnum.SB_TRACK_SKIP_COUNT.saveValue(settingsJson.getBoolean("trackViewCount")); + SettingsEnum.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.saveValue(settingsJson.getBoolean("showTimeWithSkips")); String serverAddress = settingsJson.getString("serverAddress"); - if (!isValidSBServerAddress(serverAddress)) { - throw new IllegalArgumentException(str("sb_api_url_invalid")); + if (isValidSBServerAddress(serverAddress)) { // Old versions of ReVanced exported wrong url format + SettingsEnum.SB_API_URL.saveValue(serverAddress); } - SettingsEnum.SB_API_URL.saveValue(serverAddress); - SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(settingsJson.getBoolean("showTimeWithSkips")); - final float minDuration = (float)settingsJson.getDouble("minDuration"); + final float minDuration = (float) settingsJson.getDouble("minDuration"); if (minDuration < 0) { throw new IllegalArgumentException("invalid minDuration: " + minDuration); } - SettingsEnum.SB_MIN_DURATION.saveValue(minDuration); + SettingsEnum.SB_SEGMENT_MIN_DURATION.saveValue(minDuration); - try { + if (settingsJson.has("skipCount")) { // Value not exported in old versions of ReVanced int skipCount = settingsJson.getInt("skipCount"); if (skipCount < 0) { throw new IllegalArgumentException("invalid skipCount: " + skipCount); } - SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(skipCount); + SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.saveValue(skipCount); + } + if (settingsJson.has("minutesSaved")) { final double minutesSaved = settingsJson.getDouble("minutesSaved"); if (minutesSaved < 0) { throw new IllegalArgumentException("invalid minutesSaved: " + minutesSaved); } - SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue((long)(minutesSaved * 60 * 1000)); - } catch (JSONException ex) { - // ignore. values were not exported in prior versions of ReVanced + SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.saveValue((long) (minutesSaved * 60 * 1000)); } ReVancedUtils.showToastLong(str("sb_settings_import_successful")); @@ -136,15 +143,17 @@ public class SponsorBlockSettings { categorySelectionsArray.put(behaviorObject); } } - json.put("userID", SettingsEnum.SB_UUID.getString()); - json.put("isVip", SettingsEnum.SB_IS_VIP.getBoolean()); + if (SponsorBlockSettings.userHasSBPrivateId()) { + json.put("userID", SettingsEnum.SB_PRIVATE_USER_ID.getString()); + } + json.put("isVip", SettingsEnum.SB_USER_IS_VIP.getBoolean()); json.put("serverAddress", SettingsEnum.SB_API_URL.getString()); - json.put("dontShowNotice", !SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean()); - json.put("showTimeWithSkips", SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()); - json.put("minDuration", SettingsEnum.SB_MIN_DURATION.getFloat()); + json.put("dontShowNotice", !SettingsEnum.SB_TOAST_ON_SKIP.getBoolean()); + json.put("showTimeWithSkips", SettingsEnum.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.getBoolean()); + json.put("minDuration", SettingsEnum.SB_SEGMENT_MIN_DURATION.getFloat()); json.put("trackViewCount", SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean()); - json.put("skipCount", SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getInt()); - json.put("minutesSaved", SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getLong() / (60f * 1000)); + json.put("skipCount", SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.getInt()); + json.put("minutesSaved", SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.getLong() / (60f * 1000)); json.put("categorySelections", categorySelectionsArray); json.put("barTypes", barTypesObject); @@ -152,13 +161,59 @@ public class SponsorBlockSettings { return json.toString(2); } catch (Exception ex) { LogHelper.printInfo(() -> "failed to export settings", ex); // use info level, as we are showing our own toast - ReVancedUtils.showToastLong(str("sb_settings_export_failed")); + ReVancedUtils.showToastLong(str("sb_settings_export_failed", ex)); return ""; } } + /** + * Export the categories using flatten json (no embedded dictionaries or arrays). + */ + public static void exportCategoriesToFlatJson(@Nullable Context dialogContext, + @NonNull JSONObject json) throws JSONException { + ReVancedUtils.verifyOnMainThread(); + initialize(); + + // If user has a SponsorBlock user id then show a warning. + if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateId() + && !SettingsEnum.SB_HIDE_EXPORT_WARNING.getBoolean()) { + new AlertDialog.Builder(dialogContext) + .setMessage(str("sb_settings_revanced_export_user_id_warning")) + .setNeutralButton(str("sb_settings_revanced_export_user_id_warning_dismiss"), + (dialog, which) -> SettingsEnum.SB_HIDE_EXPORT_WARNING.saveValue(true)) + .setPositiveButton(android.R.string.ok, null) + .setCancelable(false) + .show(); + } + + for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) { + category.exportToFlatJSON(json); + } + } + + /** + * Import the categories using flatten json (no embedded dictionaries or arrays). + * + * @return the number of settings imported + */ + public static int importCategoriesFromFlatJson(JSONObject json) throws JSONException { + ReVancedUtils.verifyOnMainThread(); + initialize(); + + int numberOfImportedSettings = 0; + SharedPreferences.Editor editor = SharedPrefCategory.SPONSOR_BLOCK.preferences.edit(); + for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) { + numberOfImportedSettings += category.importFromFlatJSON(json, editor); + } + editor.apply(); + + SegmentCategory.updateEnabledCategories(); + + return numberOfImportedSettings; + } + public static boolean isValidSBUserId(@NonNull String userId) { - return !userId.isEmpty(); + return !userId.isEmpty() && userId.length() >= SB_PRIVATE_USER_ID_MINIMUM_LENGTH; } /** @@ -180,6 +235,29 @@ public class SponsorBlockSettings { return true; } + /** + * @return if the user has ever voted, created a segment, or imported existing SB settings. + */ + public static boolean userHasSBPrivateId() { + return !SettingsEnum.SB_PRIVATE_USER_ID.getString().isEmpty(); + } + + /** + * Use this only if a user id is required (creating segments, voting). + */ + @NonNull + public static String getSBPrivateUserID() { + String uuid = SettingsEnum.SB_PRIVATE_USER_ID.getString(); + if (uuid.isEmpty()) { + uuid = (UUID.randomUUID().toString() + + UUID.randomUUID().toString() + + UUID.randomUUID().toString()) + .replace("-", ""); + SettingsEnum.SB_PRIVATE_USER_ID.saveValue(uuid); + } + return uuid; + } + private static boolean initialized; public static void initialize() { @@ -188,15 +266,6 @@ public class SponsorBlockSettings { } initialized = true; - String uuid = SettingsEnum.SB_UUID.getString(); - if (uuid.isEmpty()) { - uuid = (UUID.randomUUID().toString() + - UUID.randomUUID().toString() + - UUID.randomUUID().toString()) - .replace("-", ""); - SettingsEnum.SB_UUID.saveValue(uuid); - } - SegmentCategory.loadFromPreferences(); } } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java index 6f411586..37020fce 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java @@ -172,7 +172,7 @@ public class SponsorBlockUtils { for (int i = 0; i < voteOptions.length; i++) { SegmentVote voteOption = voteOptions[i]; String title = voteOption.title.toString(); - if (SettingsEnum.SB_IS_VIP.getBoolean() && segment.isLocked && voteOption.shouldHighlight) { + if (SettingsEnum.SB_USER_IS_VIP.getBoolean() && segment.isLocked && voteOption.shouldHighlight) { items[i] = Html.fromHtml(String.format("%s", LOCKED_COLOR, title)); } else { items[i] = title; @@ -214,20 +214,18 @@ public class SponsorBlockUtils { private static void submitNewSegment() { try { ReVancedUtils.verifyOnMainThread(); - final String uuid = SettingsEnum.SB_UUID.getString(); final long start = newSponsorSegmentStartMillis; final long end = newSponsorSegmentEndMillis; final String videoId = VideoInformation.getVideoId(); final long videoLength = VideoInformation.getVideoLength(); final SegmentCategory segmentCategory = newUserCreatedSegmentCategory; - if (start < 0 || end < 0 || start >= end || videoLength <= 0 || videoId.isEmpty() - || segmentCategory == null || uuid.isEmpty()) { + if (start < 0 || end < 0 || start >= end || videoLength <= 0 || videoId.isEmpty() || segmentCategory == null) { LogHelper.printException(() -> "invalid parameters"); return; } clearUnsubmittedSegmentTimes(); ReVancedUtils.runOnBackgroundThread(() -> { - SBRequester.submitSegments(uuid, videoId, segmentCategory.key, start, end, videoLength); + SBRequester.submitSegments(videoId, segmentCategory.key, start, end, videoLength); SegmentPlaybackController.executeDownloadSegments(videoId); }); } catch (Exception e) { @@ -380,9 +378,9 @@ public class SponsorBlockUtils { return; } segment.recordedAsSkipped = true; - final long totalTimeSkipped = SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getLong() + segment.length(); - SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue(totalTimeSkipped); - SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getInt() + 1); + final long totalTimeSkipped = SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.getLong() + segment.length(); + SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.saveValue(totalTimeSkipped); + SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.saveValue(SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.getInt() + 1); if (SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean()) { ReVancedUtils.runOnBackgroundThread(() -> SBRequester.sendSegmentSkippedViewedRequest(segment)); diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java index 6ed56fce..ed34bf10 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java @@ -16,6 +16,9 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.json.JSONException; +import org.json.JSONObject; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -59,6 +62,11 @@ public enum SegmentCategory { private static final StringRef skipSponsorTextCompact = sf("sb_skip_button_compact"); private static final StringRef skipSponsorTextCompactHighlight = sf("sb_skip_button_compact_highlight"); + /** + * Prefix to use when serializing to flat JSON layout used with ReVanced import/export. + */ + private static final String FLAT_JSON_IMPORT_EXPORT_PREFIX = "sb_"; + private static final SegmentCategory[] categoriesWithoutHighlights = new SegmentCategory[]{ SPONSOR, SELF_PROMO, @@ -189,6 +197,8 @@ public enum SegmentCategory { */ @NonNull public CategoryBehaviour behaviour; + @NonNull + public final CategoryBehaviour defaultBehaviour; SegmentCategory(String key, StringRef title, StringRef description, StringRef skipButtonText, @@ -213,7 +223,7 @@ public enum SegmentCategory { this.skippedToastBeginning = Objects.requireNonNull(skippedToastBeginning); this.skippedToastMiddle = Objects.requireNonNull(skippedToastMiddle); this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd); - this.behaviour = Objects.requireNonNull(defaultBehavior); + this.behaviour = this.defaultBehaviour = Objects.requireNonNull(defaultBehavior); this.color = this.defaultColor = defaultColor; this.paint = new Paint(); setColor(defaultColor); @@ -231,10 +241,13 @@ public enum SegmentCategory { } String behaviorString = preferences.getString(key, null); - if (behaviorString != null) { + if (behaviorString == null) { + behaviour = defaultBehaviour; + } else { CategoryBehaviour preferenceBehavior = CategoryBehaviour.byStringKey(behaviorString); if (preferenceBehavior == null) { LogHelper.printException(() -> "Unknown behavior: " + behaviorString); // should never happen + behaviour = defaultBehaviour; } else { behaviour = preferenceBehavior; } @@ -253,6 +266,50 @@ public enum SegmentCategory { editor.putString(key, behaviour.key); } + private String getFlatJsonBehaviorKey() { + return FLAT_JSON_IMPORT_EXPORT_PREFIX + key; + } + private String getFlatJsonColorKey() { + return FLAT_JSON_IMPORT_EXPORT_PREFIX + key + COLOR_PREFERENCE_KEY_SUFFIX; + } + + public void exportToFlatJSON(JSONObject json) throws JSONException { + if (behaviour != defaultBehaviour) { + json.put(getFlatJsonBehaviorKey(), behaviour.key); + } + if (color != defaultColor) { + json.put(getFlatJsonColorKey(), colorString()); + } + } + + /** + * Calling code is responsible for calling {@link #updateEnabledCategories()} and {@link SharedPreferences.Editor#apply()} + */ + public int importFromFlatJSON(JSONObject json, SharedPreferences.Editor editor) throws JSONException { + int numberOfSettingsImported = 0; + String behaviorKey = getFlatJsonBehaviorKey(); + if (json.has(behaviorKey)) { + String behaviorString = json.getString(behaviorKey); + CategoryBehaviour importedBehavior = CategoryBehaviour.byStringKey(behaviorString); + if (importedBehavior == null) { + throw new IllegalArgumentException("unknown behavior: " + behaviorString); + } + behaviour = importedBehavior; + numberOfSettingsImported++; + } else { + behaviour = defaultBehaviour; + } + String colorKey = getFlatJsonColorKey(); + if (json.has(colorKey)) { + setColor(json.getString(colorKey)); + numberOfSettingsImported++; + } else { + color = defaultColor; + } + save(editor); + return numberOfSettingsImported; + } + /** * @return HTML color format string */ @@ -300,7 +357,7 @@ public enum SegmentCategory { */ @NonNull StringRef getSkipButtonText(long segmentStartTime, long videoLength) { - if (SettingsEnum.SB_USE_COMPACT_SKIP_BUTTON.getBoolean()) { + if (SettingsEnum.SB_COMPACT_SKIP_BUTTON.getBoolean()) { return (this == SegmentCategory.HIGHLIGHT) ? skipSponsorTextCompactHighlight : skipSponsorTextCompact; diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/UserStats.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/UserStats.java index 663fc6ae..ae5bdbc4 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/UserStats.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/UserStats.java @@ -17,7 +17,12 @@ public class UserStats { * "User reputation". Unclear how SB determines this value. */ public final float reputation; + /** + * {@link #segmentCount} plus {@link #ignoredSegmentCount} + */ + public final int totalSegmentCountIncludingIgnored; public final int segmentCount; + public final int ignoredSegmentCount; public final int viewCount; public final double minutesSaved; @@ -26,6 +31,8 @@ public class UserStats { userName = json.getString("userName"); reputation = (float)json.getDouble("reputation"); segmentCount = json.getInt("segmentCount"); + ignoredSegmentCount = json.getInt("ignoredSegmentCount"); + totalSegmentCountIncludingIgnored = segmentCount + ignoredSegmentCount; viewCount = json.getInt("viewCount"); minutesSaved = json.getDouble("minutesSaved"); } @@ -38,6 +45,7 @@ public class UserStats { + ", userName='" + userName + '\'' + ", reputation=" + reputation + ", segmentCount=" + segmentCount + + ", ignoredSegmentCount=" + ignoredSegmentCount + ", viewCount=" + viewCount + ", minutesSaved=" + minutesSaved + '}'; diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java b/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java index a2985f4d..6bb69917 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java @@ -20,7 +20,7 @@ import java.util.concurrent.TimeUnit; import app.revanced.integrations.requests.Requester; import app.revanced.integrations.requests.Route; import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour; +import app.revanced.integrations.sponsorblock.SponsorBlockSettings; import app.revanced.integrations.sponsorblock.objects.SegmentCategory; import app.revanced.integrations.sponsorblock.objects.SponsorSegment; import app.revanced.integrations.sponsorblock.objects.SponsorSegment.SegmentVote; @@ -49,6 +49,15 @@ public class SBRequester { private SBRequester() { } + private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) { + if (SettingsEnum.SB_TOAST_ON_CONNECTION_ERROR.getBoolean()) { + ReVancedUtils.showToastShort(toastMessage); + } + if (ex != null) { + LogHelper.printInfo(() -> toastMessage, ex); + } + } + @NonNull public static SponsorSegment[] getSegments(@NonNull String videoId) { ReVancedUtils.verifyOffMainThread(); @@ -59,7 +68,7 @@ public class SBRequester { if (responseCode == HTTP_STATUS_CODE_SUCCESS) { JSONArray responseArray = Requester.parseJSONArray(connection); - final long minSegmentDuration = (long) (SettingsEnum.SB_MIN_DURATION.getFloat() * 1000); + final long minSegmentDuration = (long) (SettingsEnum.SB_SEGMENT_MIN_DURATION.getFloat() * 1000); for (int i = 0, length = responseArray.length(); i < length; i++) { JSONObject obj = (JSONObject) responseArray.get(i); JSONArray segment = obj.getJSONArray("segment"); @@ -88,14 +97,16 @@ public class SBRequester { // no segments are found. a normal response LogHelper.printDebug(() -> "No segments found for video: " + videoId); } else { - LogHelper.printException(() -> "getSegments failed with response code: " + responseCode, - null, str("sb_sponsorblock_connection_failure_status", responseCode)); + handleConnectionError(str("sb_sponsorblock_connection_failure_status", responseCode), null); connection.disconnect(); // something went wrong, might as well disconnect } } catch (SocketTimeoutException ex) { - LogHelper.printException(() -> "Failed to get segments", ex, str("sb_sponsorblock_connection_failure_timeout")); + handleConnectionError(str("sb_sponsorblock_connection_failure_timeout"), ex); + } catch (IOException ex) { + handleConnectionError(str("sb_sponsorblock_connection_failure_generic"), ex); } catch (Exception ex) { - LogHelper.printException(() -> "Failed to get segments", ex, str("sb_sponsorblock_connection_failure_generic")); + // Should never happen + LogHelper.printException(() -> "getSegments failure", ex); } // Crude debug tests to verify random features @@ -127,15 +138,16 @@ public class SBRequester { return segments.toArray(new SponsorSegment[0]); } - public static void submitSegments(@NonNull String userPrivateId, @NonNull String videoId, @NonNull String category, + public static void submitSegments(@NonNull String videoId, @NonNull String category, long startTime, long endTime, long videoLength) { ReVancedUtils.verifyOffMainThread(); try { + 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, userPrivateId, videoId, category, start, end, duration); + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, privateUserId, videoId, category, start, end, duration); final int responseCode = connection.getResponseCode(); final String messageToToast; @@ -161,7 +173,10 @@ public class SBRequester { } ReVancedUtils.showToastLong(messageToToast); } catch (SocketTimeoutException ex) { + // Always show, even if show connection toasts is turned off ReVancedUtils.showToastLong(str("sb_submit_failed_timeout")); + } catch (IOException ex) { + ReVancedUtils.showToastLong(str("sb_submit_failed_unknown_error", 0, ex.getMessage())); } catch (Exception ex) { LogHelper.printException(() -> "failed to submit segments", ex); } @@ -196,7 +211,7 @@ public class SBRequester { ReVancedUtils.runOnBackgroundThread(() -> { try { String segmentUuid = segment.UUID; - String uuid = SettingsEnum.SB_UUID.getString(); + String uuid = SponsorBlockSettings.getSBPrivateUserID(); HttpURLConnection connection = (voteOption == SegmentVote.CATEGORY_CHANGE) ? getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_CATEGORY, uuid, segmentUuid, categoryToVoteFor.key) : getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_QUALITY, uuid, segmentUuid, String.valueOf(voteOption.apiVoteType)); @@ -216,7 +231,9 @@ public class SBRequester { break; } } catch (SocketTimeoutException ex) { - LogHelper.printException(() -> "failed to vote for segment", ex, str("sb_vote_failed_timeout")); + ReVancedUtils.showToastShort(str("sb_vote_failed_timeout")); + } catch (IOException ex) { + ReVancedUtils.showToastShort(str("sb_vote_failed_unknown_error", 0, ex.getMessage())); } catch (Exception ex) { LogHelper.printException(() -> "failed to vote for segment", ex); // should never happen } @@ -230,7 +247,7 @@ public class SBRequester { public static UserStats retrieveUserStats() { ReVancedUtils.verifyOffMainThread(); try { - UserStats stats = new UserStats(getJSONObject(SBRoutes.GET_USER_STATS, SettingsEnum.SB_UUID.getString())); + UserStats stats = new UserStats(getJSONObject(SBRoutes.GET_USER_STATS, SponsorBlockSettings.getSBPrivateUserID())); LogHelper.printDebug(() -> "user stats: " + stats); return stats; } catch (IOException ex) { @@ -248,7 +265,7 @@ public class SBRequester { public static String setUsername(@NonNull String username) { ReVancedUtils.verifyOffMainThread(); try { - HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SettingsEnum.SB_UUID.getString(), username); + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SponsorBlockSettings.getSBPrivateUserID(), username); final int responseCode = connection.getResponseCode(); String responseMessage = connection.getResponseMessage(); if (responseCode == HTTP_STATUS_CODE_SUCCESS) { @@ -262,15 +279,18 @@ 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. + } long now = System.currentTimeMillis(); if (now < (SettingsEnum.SB_LAST_VIP_CHECK.getLong() + TimeUnit.DAYS.toMillis(3))) { return; } ReVancedUtils.runOnBackgroundThread(() -> { try { - JSONObject json = getJSONObject(SBRoutes.IS_USER_VIP, SettingsEnum.SB_UUID.getString()); + JSONObject json = getJSONObject(SBRoutes.IS_USER_VIP, SponsorBlockSettings.getSBPrivateUserID()); boolean vip = json.getBoolean("vip"); - SettingsEnum.SB_IS_VIP.saveValue(vip); + SettingsEnum.SB_USER_IS_VIP.saveValue(vip); SettingsEnum.SB_LAST_VIP_CHECK.saveValue(now); } catch (IOException ex) { LogHelper.printInfo(() -> "Failed to check VIP (network error)", ex); // info, so no error toast is shown diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRoutes.java b/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRoutes.java index 6bf90a94..5fbe688a 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRoutes.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRoutes.java @@ -9,7 +9,7 @@ class SBRoutes { static final Route IS_USER_VIP = new Route(GET, "/api/isUserVIP?userID={user_id}"); static final Route GET_SEGMENTS = new Route(GET, "/api/skipSegments?videoID={video_id}&categories={categories}"); static final Route VIEWED_SEGMENT = new Route(POST, "/api/viewedVideoSponsorTime?UUID={segment_id}"); - static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userID\",\"userName\",\"reputation\",\"segmentCount\",\"viewCount\",\"minutesSaved\"]"); + static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userID\",\"userName\",\"reputation\",\"segmentCount\",\"ignoredSegmentCount\",\"viewCount\",\"minutesSaved\"]"); static final Route CHANGE_USERNAME = new Route(POST, "/api/setUsername?userID={user_id}&username={username}"); static final Route SUBMIT_SEGMENTS = new Route(POST, "/api/skipSegments?userID={user_id}&videoID={video_id}&category={category}&startTime={start_time}&endTime={end_time}&videoDuration={duration}"); static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&type={type}"); diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/CreateSegmentButtonController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/CreateSegmentButtonController.java index 432cf4eb..83528d09 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/CreateSegmentButtonController.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/CreateSegmentButtonController.java @@ -3,52 +3,35 @@ package app.revanced.integrations.sponsorblock.ui; import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier; import android.view.View; -import android.view.animation.Animation; import android.widget.ImageView; -import android.widget.RelativeLayout; import java.lang.ref.WeakReference; +import java.util.Objects; import app.revanced.integrations.patches.VideoInformation; import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; +import app.revanced.integrations.videoplayer.BottomControlButton; public class CreateSegmentButtonController { private static WeakReference buttonReference = new WeakReference<>(null); - private static Animation fadeIn; - private static Animation fadeOut; private static boolean isShowing; /** * injection point */ - public static void initialize(Object viewStub) { + public static void initialize(View youtubeControlsLayout) { try { LogHelper.printDebug(() -> "initializing new segment button"); - - RelativeLayout youtubeControlsLayout = (RelativeLayout) viewStub; - String buttonIdentifier = "sb_sponsorblock_button"; - ImageView imageView = youtubeControlsLayout.findViewById(getResourceIdentifier(buttonIdentifier, "id")); - if (imageView == null) { - LogHelper.printException(() -> "Couldn't find imageView with \"" + buttonIdentifier + "\""); - return; - } + ImageView imageView = Objects.requireNonNull(youtubeControlsLayout.findViewById( + getResourceIdentifier("sb_sponsorblock_button", "id"))); + imageView.setVisibility(View.GONE); imageView.setOnClickListener(v -> { - LogHelper.printDebug(() -> "New segment button clicked"); SponsorBlockViewController.toggleNewSegmentLayoutVisibility(); }); - buttonReference = new WeakReference<>(imageView); - // Animations - if (fadeIn == null) { - fadeIn = ReVancedUtils.getResourceAnimation("fade_in"); - fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast")); - fadeOut = ReVancedUtils.getResourceAnimation("fade_out"); - fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled")); - } - isShowing = true; - changeVisibilityImmediate(false); + buttonReference = new WeakReference<>(imageView); } catch (Exception ex) { LogHelper.printException(() -> "initialize failure", ex); } @@ -86,7 +69,7 @@ public class CreateSegmentButtonController { return; } if (!immediate) { - iView.startAnimation(fadeIn); + iView.startAnimation(BottomControlButton.getButtonFadeIn()); } iView.setVisibility(View.VISIBLE); return; @@ -95,7 +78,7 @@ public class CreateSegmentButtonController { if (iView.getVisibility() == View.VISIBLE) { iView.clearAnimation(); if (!immediate) { - iView.startAnimation(fadeOut); + iView.startAnimation(BottomControlButton.getButtonFadeOut()); } iView.setVisibility(View.GONE); } @@ -105,7 +88,7 @@ public class CreateSegmentButtonController { } private static boolean shouldBeShown() { - return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean() + return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_CREATE_NEW_SEGMENT.getBoolean() && !VideoInformation.isAtEndOfVideo(); } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/NewSegmentLayout.java b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/NewSegmentLayout.java index e1e09182..970462af 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/NewSegmentLayout.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/NewSegmentLayout.java @@ -53,14 +53,14 @@ public final class NewSegmentLayout extends FrameLayout { initializeButton( context, "sb_new_segment_rewind", - () -> VideoInformation.seekToRelative(-SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt()), + () -> VideoInformation.seekToRelative(-SettingsEnum.SB_CREATE_NEW_SEGMENT_STEP.getInt()), "Rewind button clicked" ); initializeButton( context, "sb_new_segment_forward", - () -> VideoInformation.seekToRelative(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt()), + () -> VideoInformation.seekToRelative(SettingsEnum.SB_CREATE_NEW_SEGMENT_STEP.getInt()), "Forward button clicked" ); diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java index fcbaf2da..ac3a3909 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java @@ -51,7 +51,7 @@ public class SponsorBlockViewController { /** * Injection point. */ - public static void initialize(Object obj) { + public static void initialize(ViewGroup viewGroup) { try { LogHelper.printDebug(() -> "initializing"); @@ -64,7 +64,6 @@ public class SponsorBlockViewController { LayoutInflater.from(context).inflate(getResourceIdentifier("inline_sponsor_overlay", "layout"), layout); inlineSponsorOverlayRef = new WeakReference<>(layout); - ViewGroup viewGroup = (ViewGroup) obj; viewGroup.addView(layout); viewGroup.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { @Override @@ -214,7 +213,7 @@ public class SponsorBlockViewController { // the buttons automatically set themselves to visible when appropriate, // but if buttons are showing when the end of the video is reached then they need // to be forcefully hidden - if (!SettingsEnum.PREFERRED_AUTO_REPEAT.getBoolean()) { + if (!SettingsEnum.AUTO_REPEAT.getBoolean()) { CreateSegmentButtonController.hide(); VotingButtonController.hide(); } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/VotingButtonController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/VotingButtonController.java index 0a5c499a..e0e4d555 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/VotingButtonController.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/VotingButtonController.java @@ -3,11 +3,10 @@ package app.revanced.integrations.sponsorblock.ui; import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier; import android.view.View; -import android.view.animation.Animation; import android.widget.ImageView; -import android.widget.RelativeLayout; import java.lang.ref.WeakReference; +import java.util.Objects; import app.revanced.integrations.patches.VideoInformation; import app.revanced.integrations.settings.SettingsEnum; @@ -15,40 +14,26 @@ import app.revanced.integrations.sponsorblock.SegmentPlaybackController; import app.revanced.integrations.sponsorblock.SponsorBlockUtils; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; +import app.revanced.integrations.videoplayer.BottomControlButton; public class VotingButtonController { private static WeakReference buttonReference = new WeakReference<>(null); - private static Animation fadeIn; - private static Animation fadeOut; private static boolean isShowing; /** * injection point */ - public static void initialize(Object viewStub) { + public static void initialize(View youtubeControlsLayout) { try { LogHelper.printDebug(() -> "initializing voting button"); - RelativeLayout controlsLayout = (RelativeLayout) viewStub; - String buttonResourceName = "sb_voting_button"; - ImageView imageView = controlsLayout.findViewById(getResourceIdentifier(buttonResourceName, "id")); - if (imageView == null) { - LogHelper.printException(() -> "Couldn't find imageView with \"" + buttonResourceName + "\""); - return; - } + ImageView imageView = Objects.requireNonNull(youtubeControlsLayout.findViewById( + getResourceIdentifier("sb_voting_button", "id"))); + imageView.setVisibility(View.GONE); imageView.setOnClickListener(v -> { SponsorBlockUtils.onVotingClicked(v.getContext()); }); - buttonReference = new WeakReference<>(imageView); - // Animations - if (fadeIn == null) { - fadeIn = ReVancedUtils.getResourceAnimation("fade_in"); - fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast")); - fadeOut = ReVancedUtils.getResourceAnimation("fade_out"); - fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled")); - } - isShowing = true; - changeVisibilityImmediate(false); + buttonReference = new WeakReference<>(imageView); } catch (Exception ex) { LogHelper.printException(() -> "Unable to set RelativeLayout", ex); } @@ -86,7 +71,7 @@ public class VotingButtonController { return; } if (!immediate) { - iView.startAnimation(fadeIn); + iView.startAnimation(BottomControlButton.getButtonFadeIn()); } iView.setVisibility(View.VISIBLE); return; @@ -95,7 +80,7 @@ public class VotingButtonController { if (iView.getVisibility() == View.VISIBLE) { iView.clearAnimation(); if (!immediate) { - iView.startAnimation(fadeOut); + iView.startAnimation(BottomControlButton.getButtonFadeOut()); } iView.setVisibility(View.GONE); } @@ -105,7 +90,7 @@ public class VotingButtonController { } private static boolean shouldBeShown() { - return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_VOTING_ENABLED.getBoolean() + return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_VOTING_BUTTON.getBoolean() && SegmentPlaybackController.videoHasSegments() && !VideoInformation.isAtEndOfVideo(); } @@ -116,7 +101,6 @@ public class VotingButtonController { ReVancedUtils.verifyOnMainThread(); View v = buttonReference.get(); if (v == null) { - LogHelper.printDebug(() -> "Cannot hide voting button (value is null)"); return; } v.setVisibility(View.GONE); diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsConfigurationProvider.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsConfigurationProvider.kt index 5f9fc916..ebb1fe59 100644 --- a/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsConfigurationProvider.kt +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsConfigurationProvider.kt @@ -24,13 +24,13 @@ class SwipeControlsConfigurationProvider( * should swipe controls for volume be enabled? */ val enableVolumeControls: Boolean - get() = SettingsEnum.ENABLE_SWIPE_VOLUME.boolean + get() = SettingsEnum.SWIPE_VOLUME.boolean /** * should swipe controls for volume be enabled? */ val enableBrightnessControl: Boolean - get() = SettingsEnum.ENABLE_SWIPE_BRIGHTNESS.boolean + get() = SettingsEnum.SWIPE_BRIGHTNESS.boolean /** * is the video player currently in fullscreen mode? @@ -52,14 +52,14 @@ class SwipeControlsConfigurationProvider( * should press-to-swipe be enabled? */ val shouldEnablePressToSwipe: Boolean - get() = SettingsEnum.ENABLE_PRESS_TO_SWIPE.boolean + get() = SettingsEnum.SWIPE_PRESS_TO_ENGAGE.boolean /** * threshold for swipe detection * this may be called rapidly in onScroll, so we have to load it once and then leave it constant */ - val swipeMagnitudeThreshold: Float - get() = SettingsEnum.SWIPE_MAGNITUDE_THRESHOLD.float + val swipeMagnitudeThreshold: Int + get() = SettingsEnum.SWIPE_MAGNITUDE_THRESHOLD.int //endregion //region overlay adjustments @@ -68,7 +68,7 @@ class SwipeControlsConfigurationProvider( * should the overlay enable haptic feedback? */ val shouldEnableHapticFeedback: Boolean - get() = SettingsEnum.ENABLE_SWIPE_HAPTIC_FEEDBACK.boolean + get() = SettingsEnum.SWIPE_HAPTIC_FEEDBACK.boolean /** * how long the overlay should be shown on changes @@ -79,8 +79,8 @@ class SwipeControlsConfigurationProvider( /** * text size for the overlay, in sp */ - val overlayTextSize: Float - get() = SettingsEnum.SWIPE_OVERLAY_TEXT_SIZE.float + val overlayTextSize: Int + get() = SettingsEnum.SWIPE_OVERLAY_TEXT_SIZE.int /** * get the background color for text on the overlay, as a color int diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsOverlayLayout.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsOverlayLayout.kt index b80ed822..9127c130 100644 --- a/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsOverlayLayout.kt +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsOverlayLayout.kt @@ -74,7 +74,7 @@ class SwipeControlsOverlayLayout( setColor(config.overlayTextBackgroundColor) } setTextColor(config.overlayForegroundColor) - setTextSize(TypedValue.COMPLEX_UNIT_SP, config.overlayTextSize) + setTextSize(TypedValue.COMPLEX_UNIT_SP, config.overlayTextSize.toFloat()) compoundDrawablePadding = compoundIconPadding visibility = GONE } diff --git a/app/src/main/java/app/revanced/integrations/utils/LogHelper.java b/app/src/main/java/app/revanced/integrations/utils/LogHelper.java index 19e3c127..b0f96635 100644 --- a/app/src/main/java/app/revanced/integrations/utils/LogHelper.java +++ b/app/src/main/java/app/revanced/integrations/utils/LogHelper.java @@ -125,7 +125,7 @@ public class LogHelper { } else { Log.e(logMessage, messageString, ex); } - if (SettingsEnum.DEBUG_SHOW_TOAST_ON_ERROR.getBoolean()) { + if (SettingsEnum.DEBUG_TOAST_ON_ERROR.getBoolean()) { String toastMessageToDisplay = (userToastMessage != null) ? userToastMessage : outerClassSimpleName + ": " + messageString; diff --git a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java index b92d4bd5..564de2c3 100644 --- a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java +++ b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java @@ -6,9 +6,15 @@ import android.content.res.Resources; import android.net.ConnectivityManager; import android.os.Handler; import android.os.Looper; +import android.view.View; +import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.Toast; +import android.widget.Toolbar; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -22,6 +28,8 @@ import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import app.revanced.integrations.settings.SettingsEnum; + public class ReVancedUtils { @SuppressLint("StaticFieldLeak") @@ -30,6 +38,35 @@ public class ReVancedUtils { private ReVancedUtils() { } // utility class + /** + * Hide a view by setting its layout height and width to 1dp. + * + * @param condition The setting to check for hiding the view. + * @param view The view to hide. + */ + public static void hideViewBy1dpUnderCondition(SettingsEnum condition, View view) { + if (!condition.getBoolean()) return; + + LogHelper.printDebug(() -> "Hiding view with setting: " + condition); + + hideViewByLayoutParams(view); + } + + /** + * Hide a view by setting its visibility to GONE. + * + * @param condition The setting to check for hiding the view. + * @param view The view to hide. + */ + public static void hideViewUnderCondition(SettingsEnum condition, View view) { + if (!condition.getBoolean()) return; + + LogHelper.printDebug(() -> "Hiding view with setting: " + condition); + + view.setVisibility(View.GONE); + } + + /** * General purpose pool for network calls and other background tasks. * All tasks run at max thread priority. @@ -97,6 +134,24 @@ public class ReVancedUtils { return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen")); } + /** + * @return The first child view that matches the filter. + */ + @Nullable + public static T getChildView(@NonNull ViewGroup viewGroup, @NonNull MatchFilter filter) { + for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) { + View childAt = viewGroup.getChildAt(i); + if (filter.matches(childAt)) { + return (T) childAt; + } + } + return null; + } + + public interface MatchFilter { + boolean matches(T object); + } + public static Context getContext() { if (context != null) { return context; @@ -117,6 +172,7 @@ public class ReVancedUtils { @Nullable private static Boolean isRightToLeftTextLayout; + /** * If the device language uses right to left text layout (hebrew, arabic, etc) */ @@ -239,6 +295,31 @@ public class ReVancedUtils { || (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER; } + /** + * Hide a view by setting its layout params to 1x1 + * @param view The view to hide. + */ + public static void hideViewByLayoutParams(View view) { + if (view instanceof LinearLayout) { + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(1, 1); + view.setLayoutParams(layoutParams); + } else if (view instanceof FrameLayout) { + FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(1, 1); + view.setLayoutParams(layoutParams2); + } else if (view instanceof RelativeLayout) { + RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(1, 1); + view.setLayoutParams(layoutParams3); + } else if (view instanceof Toolbar) { + Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(1, 1); + view.setLayoutParams(layoutParams4); + } else if (view instanceof ViewGroup) { + ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(1, 1); + view.setLayoutParams(layoutParams5); + } else { + LogHelper.printDebug(() -> "Hidden view with id " + view.getId()); + } + } + public enum NetworkType { NONE, MOBILE, diff --git a/app/src/main/java/app/revanced/integrations/utils/ThemeHelper.java b/app/src/main/java/app/revanced/integrations/utils/ThemeHelper.java index cbb10394..99fe57d3 100644 --- a/app/src/main/java/app/revanced/integrations/utils/ThemeHelper.java +++ b/app/src/main/java/app/revanced/integrations/utils/ThemeHelper.java @@ -1,20 +1,15 @@ package app.revanced.integrations.utils; +import android.app.Activity; + public class ThemeHelper { private static int themeValue; - public static void setTheme(int value) { - if (themeValue != value) { - themeValue = value; - LogHelper.printDebug(() -> "Theme value: " + themeValue); - } - } - public static void setTheme(Object value) { final int newOrdinalValue = ((Enum) value).ordinal(); if (themeValue != newOrdinalValue) { themeValue = newOrdinalValue; - LogHelper.printDebug(() -> "Theme value: " + themeValue); + LogHelper.printDebug(() -> "Theme value: " + newOrdinalValue); } } @@ -22,4 +17,11 @@ public class ThemeHelper { return themeValue == 1; } + public static void setActivityTheme(Activity activity) { + final var theme = isDarkTheme() + ? "Theme.YouTube.Settings.Dark" + : "Theme.YouTube.Settings"; + activity.setTheme(ReVancedUtils.getResourceIdentifier(theme, "style")); + } + } diff --git a/app/src/main/java/app/revanced/integrations/videoplayer/BottomControlButton.java b/app/src/main/java/app/revanced/integrations/videoplayer/BottomControlButton.java index fe27fc8f..dc30ad5c 100644 --- a/app/src/main/java/app/revanced/integrations/videoplayer/BottomControlButton.java +++ b/app/src/main/java/app/revanced/integrations/videoplayer/BottomControlButton.java @@ -4,35 +4,52 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.widget.ImageView; + import androidx.annotation.NonNull; -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; +import androidx.annotation.Nullable; import java.lang.ref.WeakReference; import java.util.Objects; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + public abstract class BottomControlButton { - private static final Animation fadeIn = ReVancedUtils.getResourceAnimation("fade_in"); - private static final Animation fadeOut = ReVancedUtils.getResourceAnimation("fade_out"); + private static final Animation fadeIn; + private static final Animation fadeOut; + private final WeakReference buttonRef; private final SettingsEnum setting; protected boolean isVisible; static { // TODO: check if these durations are correct. + fadeIn = ReVancedUtils.getResourceAnimation("fade_in"); fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast")); + + fadeOut = ReVancedUtils.getResourceAnimation("fade_out"); fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled")); } + @NonNull + public static Animation getButtonFadeIn() { + return fadeIn; + } + + @NonNull + public static Animation getButtonFadeOut() { + return fadeOut; + } + public BottomControlButton(@NonNull ViewGroup bottomControlsViewGroup, @NonNull String imageViewButtonId, - @NonNull SettingsEnum booleanSetting, @NonNull View.OnClickListener onClickListener) { + @NonNull SettingsEnum booleanSetting, @NonNull View.OnClickListener onClickListener, + @Nullable View.OnLongClickListener longClickListener) { LogHelper.printDebug(() -> "Initializing button: " + imageViewButtonId); if (booleanSetting.returnType != SettingsEnum.ReturnType.BOOLEAN) { throw new IllegalArgumentException(); } - setting = booleanSetting; // Create the button. @@ -40,6 +57,9 @@ public abstract class BottomControlButton { ReVancedUtils.getResourceIdentifier(imageViewButtonId, "id") )); imageView.setOnClickListener(onClickListener); + if (longClickListener != null) { + imageView.setOnLongClickListener(longClickListener); + } imageView.setVisibility(View.GONE); buttonRef = new WeakReference<>(imageView); diff --git a/app/src/main/java/app/revanced/integrations/videoplayer/CopyVideoUrlButton.java b/app/src/main/java/app/revanced/integrations/videoplayer/CopyVideoUrlButton.java index 23bcd151..90eed361 100644 --- a/app/src/main/java/app/revanced/integrations/videoplayer/CopyVideoUrlButton.java +++ b/app/src/main/java/app/revanced/integrations/videoplayer/CopyVideoUrlButton.java @@ -1,5 +1,6 @@ package app.revanced.integrations.videoplayer; +import android.view.View; import android.view.ViewGroup; import androidx.annotation.Nullable; @@ -16,17 +17,21 @@ public class CopyVideoUrlButton extends BottomControlButton { super( viewGroup, "copy_video_url_button", - SettingsEnum.COPY_VIDEO_URL_BUTTON_SHOWN, - view -> CopyVideoUrlPatch.copyUrl(false) + SettingsEnum.COPY_VIDEO_URL, + view -> CopyVideoUrlPatch.copyUrl(false), + view -> { + CopyVideoUrlPatch.copyUrl(true); + return true; + } ); } /** * Injection point. */ - public static void initializeButton(Object obj) { + public static void initializeButton(View view) { try { - instance = new CopyVideoUrlButton((ViewGroup) obj); + instance = new CopyVideoUrlButton((ViewGroup) view); } catch (Exception ex) { LogHelper.printException(() -> "initializeButton failure", ex); } diff --git a/app/src/main/java/app/revanced/integrations/videoplayer/CopyVideoUrlTimestampButton.java b/app/src/main/java/app/revanced/integrations/videoplayer/CopyVideoUrlTimestampButton.java index 3a94344d..a3d290ad 100644 --- a/app/src/main/java/app/revanced/integrations/videoplayer/CopyVideoUrlTimestampButton.java +++ b/app/src/main/java/app/revanced/integrations/videoplayer/CopyVideoUrlTimestampButton.java @@ -1,5 +1,6 @@ package app.revanced.integrations.videoplayer; +import android.view.View; import android.view.ViewGroup; import androidx.annotation.Nullable; @@ -16,15 +17,19 @@ public class CopyVideoUrlTimestampButton extends BottomControlButton { super( bottomControlsViewGroup, "copy_video_url_timestamp_button", - SettingsEnum.COPY_VIDEO_URL_TIMESTAMP_BUTTON_SHOWN, - view -> CopyVideoUrlPatch.copyUrl(true) + SettingsEnum.COPY_VIDEO_URL_TIMESTAMP, + view -> CopyVideoUrlPatch.copyUrl(true), + view -> { + CopyVideoUrlPatch.copyUrl(false); + return true; + } ); } /** * Injection point. */ - public static void initializeButton(Object bottomControlsViewGroup) { + public static void initializeButton(View bottomControlsViewGroup) { try { instance = new CopyVideoUrlTimestampButton((ViewGroup) bottomControlsViewGroup); } catch (Exception ex) { diff --git a/app/src/main/java/app/revanced/integrations/videoplayer/DownloadButton.java b/app/src/main/java/app/revanced/integrations/videoplayer/DownloadButton.java index dcb581a2..28f1733e 100644 --- a/app/src/main/java/app/revanced/integrations/videoplayer/DownloadButton.java +++ b/app/src/main/java/app/revanced/integrations/videoplayer/DownloadButton.java @@ -21,17 +21,18 @@ public class DownloadButton extends BottomControlButton { super( viewGroup, "download_button", - SettingsEnum.DOWNLOADS_BUTTON_SHOWN, - DownloadButton::onDownloadClick + SettingsEnum.EXTERNAL_DOWNLOADER, + DownloadButton::onDownloadClick, + null ); } /** * Injection point. */ - public static void initializeButton(Object obj) { + public static void initializeButton(View view) { try { - instance = new DownloadButton((ViewGroup) obj); + instance = new DownloadButton((ViewGroup) view); } catch (Exception ex) { LogHelper.printException(() -> "initializeButton failure", ex); } @@ -48,7 +49,7 @@ public class DownloadButton extends BottomControlButton { LogHelper.printDebug(() -> "Download button clicked"); final var context = view.getContext(); - var downloaderPackageName = SettingsEnum.DOWNLOADS_PACKAGE_NAME.getString(); + var downloaderPackageName = SettingsEnum.EXTERNAL_DOWNLOADER_PACKAGE_NAME.getString(); boolean packageEnabled = false; try { diff --git a/app/src/main/java/app/revanced/reddit/patches/SanitizeUrlQueryPatch.java b/app/src/main/java/app/revanced/reddit/patches/SanitizeUrlQueryPatch.java new file mode 100644 index 00000000..44de897e --- /dev/null +++ b/app/src/main/java/app/revanced/reddit/patches/SanitizeUrlQueryPatch.java @@ -0,0 +1,25 @@ +package app.revanced.reddit.patches; + +import app.revanced.integrations.utils.LogHelper; + +import java.net.MalformedURLException; +import java.net.URL; + +public final class SanitizeUrlQueryPatch { + /** + * Strip query parameters from a given URL string. + * + * @param urlString URL string to strip query parameters from. + * @return URL string without query parameters if possible, otherwise the original string. + */ + public static String stripQueryParameters(final String urlString) { + try { + final var url = new URL(urlString); + + return url.getProtocol() + "://" + url.getHost() + url.getPath(); + } catch (MalformedURLException e) { + LogHelper.printException(() -> "Can not parse URL", e); + return urlString; + } + } +} diff --git a/app/src/main/java/app/revanced/twitch/patches/AutoClaimChannelPointsPatch.java b/app/src/main/java/app/revanced/twitch/patches/AutoClaimChannelPointsPatch.java new file mode 100644 index 00000000..dc6c7500 --- /dev/null +++ b/app/src/main/java/app/revanced/twitch/patches/AutoClaimChannelPointsPatch.java @@ -0,0 +1,9 @@ +package app.revanced.twitch.patches; + +import app.revanced.twitch.settings.SettingsEnum; + +public class AutoClaimChannelPointsPatch { + public static boolean shouldAutoClaim() { + return SettingsEnum.AUTO_CLAIM_CHANNEL_POINTS.getBoolean(); + } +} diff --git a/app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java b/app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java index 9103f654..6e183387 100644 --- a/app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java @@ -21,6 +21,7 @@ public enum SettingsEnum { /* Chat */ SHOW_DELETED_MESSAGES("revanced_show_deleted_messages", STRING, "cross-out"), + AUTO_CLAIM_CHANNEL_POINTS("revanced_auto_claim_channel_points", BOOLEAN, TRUE), /* Misc */ DEBUG_MODE("revanced_debug_mode", BOOLEAN, FALSE, true); @@ -153,6 +154,14 @@ public enum SettingsEnum { return (String) value; } + /** + * @return the value of this setting as as generic object type. + */ + @NonNull + public Object getObjectValue() { + return value; + } + public enum ReturnType { BOOLEAN, INTEGER, diff --git a/app/src/main/java/app/revanced/twitch/settingsmenu/ReVancedSettingsFragment.java b/app/src/main/java/app/revanced/twitch/settingsmenu/ReVancedSettingsFragment.java index 1da3eacd..ed76480b 100644 --- a/app/src/main/java/app/revanced/twitch/settingsmenu/ReVancedSettingsFragment.java +++ b/app/src/main/java/app/revanced/twitch/settingsmenu/ReVancedSettingsFragment.java @@ -88,7 +88,23 @@ public class ReVancedSettingsFragment extends PreferenceFragment { ) ); - // Sync all preferences with UI + // TODO: for a developer that uses Twitch: remove duplicated settings data + // 1. remove all default values from the Patches Setting preferences (SwitchPreference, TextPreference, ListPreference) + // 2. enable this code and verify the default is applied + if (false) { + for (SettingsEnum setting : SettingsEnum.values()) { + Preference pref = this.findPreference(setting.path); + if (pref instanceof SwitchPreference) { + ((SwitchPreference) pref).setChecked(setting.getBoolean()); + } else if (pref instanceof EditTextPreference) { + ((EditTextPreference) pref).setText(setting.getObjectValue().toString()); + } else if (pref instanceof ListPreference) { + ((ListPreference) pref).setValue(setting.getObjectValue().toString()); + } + } + } + // TODO: remove this line. On load the UI should apply the values from Settings using the code above. + // It should not apply the UI values to the Settings here syncPreference(null); this.registered = true; diff --git a/app/src/main/java/app/revanced/twitter/patches/hook/json/JsonHookPatch.kt b/app/src/main/java/app/revanced/twitter/patches/hook/json/JsonHookPatch.kt index 9817b6aa..d8c47d71 100644 --- a/app/src/main/java/app/revanced/twitter/patches/hook/json/JsonHookPatch.kt +++ b/app/src/main/java/app/revanced/twitter/patches/hook/json/JsonHookPatch.kt @@ -1,5 +1,6 @@ package app.revanced.twitter.patches.hook.json +import app.revanced.twitter.patches.hook.patch.dummy.DummyHook import app.revanced.twitter.utils.json.JsonUtils.parseJson import app.revanced.twitter.utils.stream.StreamUtils import org.json.JSONException @@ -7,8 +8,9 @@ import java.io.IOException import java.io.InputStream object JsonHookPatch { + // Additional hooks added by corresponding patch. private val hooks = buildList { - // Modified by corresponding patch. + add(DummyHook) } @JvmStatic diff --git a/app/src/main/java/app/revanced/twitter/patches/hook/patch/dummy/DummyHook.kt b/app/src/main/java/app/revanced/twitter/patches/hook/patch/dummy/DummyHook.kt new file mode 100644 index 00000000..56a7a08e --- /dev/null +++ b/app/src/main/java/app/revanced/twitter/patches/hook/patch/dummy/DummyHook.kt @@ -0,0 +1,14 @@ +package app.revanced.twitter.patches.hook.patch.dummy + +import app.revanced.twitter.patches.hook.json.BaseJsonHook +import app.revanced.twitter.patches.hook.json.JsonHookPatch +import org.json.JSONObject + +/** + * Dummy hook to reserve a register in [JsonHookPatch.hooks] list. + */ +object DummyHook : BaseJsonHook() { + override fun apply(json: JSONObject) { + // Do nothing. + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/twitter/patches/hook/twifucker/TwiFucker.kt b/app/src/main/java/app/revanced/twitter/patches/hook/twifucker/TwiFucker.kt index c79254e2..9d47bc8f 100644 --- a/app/src/main/java/app/revanced/twitter/patches/hook/twifucker/TwiFucker.kt +++ b/app/src/main/java/app/revanced/twitter/patches/hook/twifucker/TwiFucker.kt @@ -51,7 +51,7 @@ internal object TwiFucker { private fun JSONObject.dataCheckAndRemove() { dataGetInstructions()?.forEach { instruction -> - instruction.instructionCheckAndRemove() + instruction.instructionCheckAndRemove { it.entriesRemoveAnnoyance() } } } @@ -107,9 +107,9 @@ internal object TwiFucker { private fun JSONObject.instructionGetAddEntries(): JSONArray? = optJSONObject("addEntries")?.optJSONArray("entries") - private fun JSONObject.instructionCheckAndRemove() { - instructionTimelineAddEntries()?.entriesRemoveAnnoyance() - instructionGetAddEntries()?.entriesRemoveAnnoyance() + private fun JSONObject.instructionCheckAndRemove(action: (JSONArray) -> Unit) { + instructionTimelineAddEntries()?.let(action) + instructionGetAddEntries()?.let(action) } // entries @@ -164,14 +164,57 @@ internal object TwiFucker { entriesRemoveTweetDetailRelatedTweets() } + private fun JSONObject.entryIsWhoToFollow(): Boolean = optString("entryId").let { + it.startsWith("whoToFollow-") || it.startsWith("who-to-follow-") || it.startsWith("connect-module-") + } + + private fun JSONObject.itemContainsPromotedUser(): Boolean = + optJSONObject("item")?.optJSONObject("content") + ?.has("userPromotedMetadata") == true || optJSONObject("item")?.optJSONObject("content") + ?.optJSONObject("user") + ?.has("userPromotedMetadata") == true || optJSONObject("item")?.optJSONObject("content") + ?.optJSONObject("user")?.has("promotedMetadata") == true + + fun JSONArray.entriesRemoveWhoToFollow() { + val entryRemoveIndex = mutableListOf() + forEachIndexed { entryIndex, entry -> + if (!entry.entryIsWhoToFollow()) return@forEachIndexed + + Log.d("revanced", "Handle whoToFollow $entryIndex $entry") + entryRemoveIndex.add(entryIndex) + + val items = entry.entryGetContentItems() + val userRemoveIndex = mutableListOf() + items?.forEachIndexed { index, item -> + item.itemContainsPromotedUser().let { + if (it) { + Log.d("revanced", "Handle whoToFollow promoted user $index $item") + userRemoveIndex.add(index) + } + } + } + for (i in userRemoveIndex.reversed()) { + items?.remove(i) + } + } + for (i in entryRemoveIndex.reversed()) { + remove(i) + } + } + fun hideRecommendedUsers(json: JSONObject) { + json.filterInstructions { it.entriesRemoveWhoToFollow() } json.jsonCheckAndRemoveRecommendedUsers() } fun hidePromotedAds(json: JSONObject) { - json.jsonGetInstructions()?.forEach { instruction -> - instruction.instructionCheckAndRemove() - } + json.filterInstructions { it.entriesRemoveAnnoyance() } json.jsonGetData()?.dataCheckAndRemove() } + + private fun JSONObject.filterInstructions(action: (JSONArray) -> Unit) { + jsonGetInstructions()?.forEach { instruction -> + instruction.instructionCheckAndRemove(action) + } + } } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 6c175194..823b6b3e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.0.0") + classpath("com.android.tools.build:gradle:8.0.1") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20") // NOTE: Do not place your application dependencies here; they belong diff --git a/dummy/src/main/java/com/google/android/libraries/social/licenses/LicenseActivity.java b/dummy/src/main/java/com/google/android/libraries/social/licenses/LicenseActivity.java deleted file mode 100644 index 059a45c9..00000000 --- a/dummy/src/main/java/com/google/android/libraries/social/licenses/LicenseActivity.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.google.android.libraries.social.licenses; - -import android.app.Activity; - -// Dummy class -public final class LicenseActivity extends Activity { } - diff --git a/dummy/src/main/java/com/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar.java b/dummy/src/main/java/com/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar.java new file mode 100644 index 00000000..f275effd --- /dev/null +++ b/dummy/src/main/java/com/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar.java @@ -0,0 +1,10 @@ +package com.google.android.libraries.youtube.rendering.ui.pivotbar; + +import android.content.Context; +import android.widget.HorizontalScrollView; + +public class PivotBar extends HorizontalScrollView { + public PivotBar(Context context) { + super(context); + } +} diff --git a/gradle.properties b/gradle.properties index 03bc21d4..4559bea1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs = -Xmx2048m android.useAndroidX = true -version = 0.107.0 +version = 0.108.0-dev.24