diff --git a/CHANGELOG.md b/CHANGELOG.md index ea31817b81..3efb9c6e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,236 @@ +# [0.120.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v0.120.0-dev.1...v0.120.0-dev.2) (2023-10-14) + + +### Bug Fixes + +* **YouTube - Minimized playback:** Fix pip incorrectly showing if app is minimized immediately after opening a Short ([7d02774](https://github.com/ReVanced/revanced-integrations/commit/7d02774ea192510e692e90ae55a86e25ee321926)) + +# [0.120.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.119.3-dev.2...v0.120.0-dev.1) (2023-10-13) + + +### Features + +* **YouTube - Theme:** Disable gradient loading screen ([fd09e46](https://github.com/ReVanced/revanced-integrations/commit/fd09e46d01c820632cfe440dac34f5cd957e793d)) + +## [0.119.3-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v0.119.3-dev.1...v0.119.3-dev.2) (2023-10-13) + + +### Bug Fixes + +* **YouTube - Hide Layout components:** Exempt expandable chips from exceptions ([#498](https://github.com/ReVanced/revanced-integrations/issues/498)) ([6f79746](https://github.com/ReVanced/revanced-integrations/commit/6f79746d788f196f3aa63b8e7c24b7f15ecd3f50)) + +## [0.119.3-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.119.2...v0.119.3-dev.1) (2023-10-13) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Hide new channel watermark component ([9670bd3](https://github.com/ReVanced/revanced-integrations/commit/9670bd305b3b9bbbc900af3b64152aaac125ec14)) + +## [0.119.2](https://github.com/ReVanced/revanced-integrations/compare/v0.119.1...v0.119.2) (2023-10-12) + + +### Bug Fixes + +* **YouTube - ReturnYouTubeDislike:** Fix dislikes not showing on Shorts ([#495](https://github.com/ReVanced/revanced-integrations/issues/495)) ([9b2add7](https://github.com/ReVanced/revanced-integrations/commit/9b2add7553488436c63fa14bd62966dfb92705bf)) + +## [0.119.2-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.119.1...v0.119.2-dev.1) (2023-10-12) + + +### Bug Fixes + +* **YouTube - ReturnYouTubeDislike:** Fix dislikes not showing on Shorts ([#495](https://github.com/ReVanced/revanced-integrations/issues/495)) ([9b2add7](https://github.com/ReVanced/revanced-integrations/commit/9b2add7553488436c63fa14bd62966dfb92705bf)) + +## [0.119.1](https://github.com/ReVanced/revanced-integrations/compare/v0.119.0...v0.119.1) (2023-10-09) + + +### Bug Fixes + +* **YouTube - Hide shorts components:** Do not hide subscribe button outside of Shorts ([1479d6b](https://github.com/ReVanced/revanced-integrations/commit/1479d6bc2668758ea55f9d640684547f710099f0)) + +## [0.119.1-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.119.0...v0.119.1-dev.1) (2023-10-08) + + +### Bug Fixes + +* **YouTube - Hide shorts components:** Do not hide subscribe button outside of Shorts ([1479d6b](https://github.com/ReVanced/revanced-integrations/commit/1479d6bc2668758ea55f9d640684547f710099f0)) + +# [0.119.0](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0...v0.119.0) (2023-10-08) + + +### Bug Fixes + +* **YouTube - Hide shorts components:** Add filter to filter group list ([30788ba](https://github.com/ReVanced/revanced-integrations/commit/30788ba1a3e8d666b55a37e9389246e4bfa039cb)) +* **YouTube - Hide shorts components:** Do not prevent filtering components ([6eb3017](https://github.com/ReVanced/revanced-integrations/commit/6eb301776e202cf1d923f96cc83ddf6f0430b211)) +* **YouTube - Hide shorts components:** Hide the subscribe button when paused ([6730aaf](https://github.com/ReVanced/revanced-integrations/commit/6730aaf2b864b747a6b77411cdaceee653b36218)) +* **YouTube - ReturnYouTubeDislike:** Do not retry API call if same fetch recently failed ([#493](https://github.com/ReVanced/revanced-integrations/issues/493)) ([486c894](https://github.com/ReVanced/revanced-integrations/commit/486c894257e91dedc04b38191e0e01e38c66b5c4)) + + +### Features + +* **YouTube - Hide shorts components:** Hide subscribe button when paused separately from subscribe button ([3ac869f](https://github.com/ReVanced/revanced-integrations/commit/3ac869fa6ae095f074481d98d9a4eea207eda00d)) +* **YouTube - Return YouTube Dislike:** Support version `18.37.36` ([#490](https://github.com/ReVanced/revanced-integrations/issues/490)) ([245c3b3](https://github.com/ReVanced/revanced-integrations/commit/245c3b35373313d49cc5b1c2fd8e9deebb6258a5)) +* **YouTube:** Add `Disable fine scrubbing gesture` patch ([4498f39](https://github.com/ReVanced/revanced-integrations/commit/4498f39b8c4900fccdc359d0c687c26db23526c3)) + +# [0.119.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v0.119.0-dev.5...v0.119.0-dev.6) (2023-10-07) + + +### Bug Fixes + +* **YouTube - ReturnYouTubeDislike:** Do not retry API call if same fetch recently failed ([#493](https://github.com/ReVanced/revanced-integrations/issues/493)) ([486c894](https://github.com/ReVanced/revanced-integrations/commit/486c894257e91dedc04b38191e0e01e38c66b5c4)) + +# [0.119.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v0.119.0-dev.4...v0.119.0-dev.5) (2023-10-07) + + +### Features + +* **YouTube - Return YouTube Dislike:** Support version `18.37.36` ([#490](https://github.com/ReVanced/revanced-integrations/issues/490)) ([245c3b3](https://github.com/ReVanced/revanced-integrations/commit/245c3b35373313d49cc5b1c2fd8e9deebb6258a5)) + +# [0.119.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v0.119.0-dev.3...v0.119.0-dev.4) (2023-10-07) + + +### Features + +* **YouTube:** Add `Disable fine scrubbing gesture` patch ([4498f39](https://github.com/ReVanced/revanced-integrations/commit/4498f39b8c4900fccdc359d0c687c26db23526c3)) + +# [0.119.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v0.119.0-dev.2...v0.119.0-dev.3) (2023-10-06) + + +### Bug Fixes + +* **YouTube - Hide shorts components:** Add filter to filter group list ([30788ba](https://github.com/ReVanced/revanced-integrations/commit/30788ba1a3e8d666b55a37e9389246e4bfa039cb)) + +# [0.119.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v0.119.0-dev.1...v0.119.0-dev.2) (2023-10-06) + + +### Bug Fixes + +* **YouTube - Hide shorts components:** Do not prevent filtering components ([6eb3017](https://github.com/ReVanced/revanced-integrations/commit/6eb301776e202cf1d923f96cc83ddf6f0430b211)) + +# [0.119.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.118.1-dev.1...v0.119.0-dev.1) (2023-10-05) + + +### Features + +* **YouTube - Hide shorts components:** Hide subscribe button when paused separately from subscribe button ([3ac869f](https://github.com/ReVanced/revanced-integrations/commit/3ac869fa6ae095f074481d98d9a4eea207eda00d)) + +## [0.118.1-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0...v0.118.1-dev.1) (2023-10-05) + + +### Bug Fixes + +* **YouTube - Hide shorts components:** Hide the subscribe button when paused ([6730aaf](https://github.com/ReVanced/revanced-integrations/commit/6730aaf2b864b747a6b77411cdaceee653b36218)) + +# [0.118.0](https://github.com/ReVanced/revanced-integrations/compare/v0.117.1...v0.118.0) (2023-10-04) + + +### Bug Fixes + +* Do not always hide the component ([3d0fc1d](https://github.com/ReVanced/revanced-integrations/commit/3d0fc1d610cdf50bb7cc4687d899e0acbf3fb83e)) +* Remove parameter from route ([4b0925e](https://github.com/ReVanced/revanced-integrations/commit/4b0925e33762c02e95ef9b1aadcae1038af71a50)) +* **YouTube - Client spoof:** Display seekbar thumbnails in high quality ([f71c1a0](https://github.com/ReVanced/revanced-integrations/commit/f71c1a0c156b2320e06dd98b3e5b276560d438aa)) +* **YouTube - Client spoof:** Do not record feed videos to history by default ([#478](https://github.com/ReVanced/revanced-integrations/issues/478)) ([ef1cca0](https://github.com/ReVanced/revanced-integrations/commit/ef1cca02c165d9c24e64b43fae375ae57bf90a52)) +* **YouTube - Client spoof:** fix occasionally frozen video playback ([#486](https://github.com/ReVanced/revanced-integrations/issues/486)) ([b0b6ff6](https://github.com/ReVanced/revanced-integrations/commit/b0b6ff6a82820d4578c5cfc5f69ae7aaaac49c7f)) +* **YouTube - Client spoof:** fix storyboard fetched out of order ([#481](https://github.com/ReVanced/revanced-integrations/issues/481)) ([8398774](https://github.com/ReVanced/revanced-integrations/commit/83987747e67541cd44221ede8c4020baba36c7b8)) +* **YouTube - Client spoof:** Fix toast shown for live streams ([#489](https://github.com/ReVanced/revanced-integrations/issues/489)) ([27f49df](https://github.com/ReVanced/revanced-integrations/commit/27f49dfd1e8fbfd3e28270da91ad437df8a54761)) +* **YouTube - Client spoof:** fix toast shown if opening paid or age restricted video ([#482](https://github.com/ReVanced/revanced-integrations/issues/482)) ([e72b65b](https://github.com/ReVanced/revanced-integrations/commit/e72b65b599353715a6467463226abc603bc850f7)) +* **YouTube - Client spoof:** Removed unused code ([#480](https://github.com/ReVanced/revanced-integrations/issues/480)) ([e6903bf](https://github.com/ReVanced/revanced-integrations/commit/e6903bff95b485d21773537bbcc162411b616618)) +* **YouTube - Client spoof:** Restore clipping videos functionality ([2cd1738](https://github.com/ReVanced/revanced-integrations/commit/2cd1738d2494add13c48b64ccc9aad2432b2d8e3)) +* **YouTube - Client spoof:** Restore seekbar thumbnails ([978f630](https://github.com/ReVanced/revanced-integrations/commit/978f630c0267ec2b0d9bb9b5b0b3cdc9abef65ec)) +* **YouTube - Client spoof:** Show seekbar thumbnail for age restricted and paid videos ([01019b0](https://github.com/ReVanced/revanced-integrations/commit/01019b09c1c106ed814b994dd8af558a18873c1d)) +* **YouTube - Custom filter:** Use new lines between components instead of commas ([#475](https://github.com/ReVanced/revanced-integrations/issues/475)) ([17ed396](https://github.com/ReVanced/revanced-integrations/commit/17ed39673954a5b571bc1654be20afc235682ca4)) +* **YouTube - Hide info cards:** Fix info cards not hiding for some users ([#487](https://github.com/ReVanced/revanced-integrations/issues/487)) ([00c4c40](https://github.com/ReVanced/revanced-integrations/commit/00c4c4025bc27495e490fdb231ac803881c9887f)) +* **YouTube - Hide layout components:** Always hide redundant 'player audio track' button ([#473](https://github.com/ReVanced/revanced-integrations/issues/473)) ([d86851b](https://github.com/ReVanced/revanced-integrations/commit/d86851baf1ef1993f5ba9543a4a3fe8d50c3a199)) +* **YouTube - Hide layout components:** Do not hide chapters in feed unexpectedly ([bedb02e](https://github.com/ReVanced/revanced-integrations/commit/bedb02e4f6122f3dcdc106648648eec4d6a3cbe5)) +* **YouTube - Hide shorts components:** Hide subscribe button in paused state ([9685070](https://github.com/ReVanced/revanced-integrations/commit/9685070eda5b448eb33324b4bfabd4c7eae42f9f)) +* **YouTube - ReturnYouTubeDislike:** Add debug logging to litho text ([#476](https://github.com/ReVanced/revanced-integrations/issues/476)) ([e3b8e8b](https://github.com/ReVanced/revanced-integrations/commit/e3b8e8be41796d0300c8421e28e5b8cf43ffb25e)) +* **YouTube - ReturnYouTubeDislike:** Revert support for 18.37.36 ([#488](https://github.com/ReVanced/revanced-integrations/issues/488)) ([165b061](https://github.com/ReVanced/revanced-integrations/commit/165b061fa9c5fd48b0dbb9540fd6ea6a9ffaf312)) +* **YouTube - SponsorBlock:** Adjust import/export UI text ([#491](https://github.com/ReVanced/revanced-integrations/issues/491)) ([4215be4](https://github.com/ReVanced/revanced-integrations/commit/4215be4250d195ecf89b041c96834be56c164f34)) +* **YouTube - Video Id:** Fix video id not showing the currently playing video ([#484](https://github.com/ReVanced/revanced-integrations/issues/484)) ([da923a3](https://github.com/ReVanced/revanced-integrations/commit/da923a38a06baf8c30d38211e8354be4edb0ad51)) +* **YouTube:** fix old quality and custom speed not working on tablets ([#477](https://github.com/ReVanced/revanced-integrations/issues/477)) ([2352fa5](https://github.com/ReVanced/revanced-integrations/commit/2352fa542658035c5f4400fb5892217887710e4d)) + + +### Features + +* **TU Dortmund:** Add `Show on lockscreen` patch ([#472](https://github.com/ReVanced/revanced-integrations/issues/472)) ([526d66f](https://github.com/ReVanced/revanced-integrations/commit/526d66f6a91e0ed907db609a4adaa97f3239898b)) +* **Twitch - Block embedded ads:** Switch from `ttv.lol` to `luminous.dev` ([2c34180](https://github.com/ReVanced/revanced-integrations/commit/2c3418041cf19ae4c1c7b67eda8398578384b753)) +* **YouTube - Hide layout components:** Disable hiding search result shelf header by default ([b280de3](https://github.com/ReVanced/revanced-integrations/commit/b280de31957631180f07057cc7e466bd26e2f7fb)) +* **YouTube - Hide layout components:** Hide "Join" button ([e225468](https://github.com/ReVanced/revanced-integrations/commit/e2254681cd77481376e4c3f8c556db510fdfce6c)) +* **YouTube - Hide layout components:** Hide "Notify me" button ([b87d806](https://github.com/ReVanced/revanced-integrations/commit/b87d8066597a2c989480de47561007844964a0e4)) +* **YouTube - Hide layout components:** Hide search result shelf header ([93a3045](https://github.com/ReVanced/revanced-integrations/commit/93a30453d9693e015b1f58a12f85cf355770a4ca)) +* **YouTube - Hide layout components:** Hide timed reactions ([b472aee](https://github.com/ReVanced/revanced-integrations/commit/b472aeeed7904f6b6d537dfbddda1a97c7ddcd5e)) +* **YouTube:** Add `Bypass URL redirects` patch ([9109653](https://github.com/ReVanced/revanced-integrations/commit/91096532eedf396920d69932638f667cbf850cbe)) +* **YouTube:** Bump compatibility to `18.37.36` ([#483](https://github.com/ReVanced/revanced-integrations/issues/483)) ([5dadb0d](https://github.com/ReVanced/revanced-integrations/commit/5dadb0d523f2b1eb4216d43770af37a156c8a477)) + + +### Performance Improvements + +* Only request required fields ([d20b768](https://github.com/ReVanced/revanced-integrations/commit/d20b768bc23d167d9f0d2c651c75b3f92944e731)) +* Remove unnecessary api key parameter ([ba5e7d8](https://github.com/ReVanced/revanced-integrations/commit/ba5e7d870ee88ad45c233d914e1e2795de920cb2)) + +# [0.118.0-dev.24](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.23...v0.118.0-dev.24) (2023-10-03) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Do not hide chapters in feed unexpectedly ([bedb02e](https://github.com/ReVanced/revanced-integrations/commit/bedb02e4f6122f3dcdc106648648eec4d6a3cbe5)) + +# [0.118.0-dev.23](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.22...v0.118.0-dev.23) (2023-10-02) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Adjust import/export UI text ([#491](https://github.com/ReVanced/revanced-integrations/issues/491)) ([4215be4](https://github.com/ReVanced/revanced-integrations/commit/4215be4250d195ecf89b041c96834be56c164f34)) + +# [0.118.0-dev.22](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.21...v0.118.0-dev.22) (2023-10-02) + + +### Bug Fixes + +* Do not always hide the component ([3d0fc1d](https://github.com/ReVanced/revanced-integrations/commit/3d0fc1d610cdf50bb7cc4687d899e0acbf3fb83e)) + + +### Features + +* **YouTube - Hide layout components:** Disable hiding search result shelf header by default ([b280de3](https://github.com/ReVanced/revanced-integrations/commit/b280de31957631180f07057cc7e466bd26e2f7fb)) + +# [0.118.0-dev.21](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.20...v0.118.0-dev.21) (2023-10-02) + + +### Features + +* **YouTube - Hide layout components:** Hide search result shelf header ([93a3045](https://github.com/ReVanced/revanced-integrations/commit/93a30453d9693e015b1f58a12f85cf355770a4ca)) + +# [0.118.0-dev.20](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.19...v0.118.0-dev.20) (2023-10-01) + + +### Bug Fixes + +* **YouTube - Hide shorts components:** Hide subscribe button in paused state ([9685070](https://github.com/ReVanced/revanced-integrations/commit/9685070eda5b448eb33324b4bfabd4c7eae42f9f)) + +# [0.118.0-dev.19](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.18...v0.118.0-dev.19) (2023-10-01) + + +### Features + +* **YouTube - Hide layout components:** Hide "Join" button ([e225468](https://github.com/ReVanced/revanced-integrations/commit/e2254681cd77481376e4c3f8c556db510fdfce6c)) +* **YouTube - Hide layout components:** Hide "Notify me" button ([b87d806](https://github.com/ReVanced/revanced-integrations/commit/b87d8066597a2c989480de47561007844964a0e4)) +* **YouTube - Hide layout components:** Hide timed reactions ([b472aee](https://github.com/ReVanced/revanced-integrations/commit/b472aeeed7904f6b6d537dfbddda1a97c7ddcd5e)) + +# [0.118.0-dev.18](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.17...v0.118.0-dev.18) (2023-09-28) + + +### Bug Fixes + +* **YouTube - ReturnYouTubeDislike:** Revert support for 18.37.36 ([#488](https://github.com/ReVanced/revanced-integrations/issues/488)) ([165b061](https://github.com/ReVanced/revanced-integrations/commit/165b061fa9c5fd48b0dbb9540fd6ea6a9ffaf312)) + +# [0.118.0-dev.17](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.16...v0.118.0-dev.17) (2023-09-28) + + +### Bug Fixes + +* **YouTube - Client spoof:** Fix toast shown for live streams ([#489](https://github.com/ReVanced/revanced-integrations/issues/489)) ([27f49df](https://github.com/ReVanced/revanced-integrations/commit/27f49dfd1e8fbfd3e28270da91ad437df8a54761)) +* **YouTube - Video Id:** Fix video id not showing the currently playing video ([#484](https://github.com/ReVanced/revanced-integrations/issues/484)) ([da923a3](https://github.com/ReVanced/revanced-integrations/commit/da923a38a06baf8c30d38211e8354be4edb0ad51)) + # [0.118.0-dev.16](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.15...v0.118.0-dev.16) (2023-09-28) diff --git a/app/src/main/java/app/revanced/integrations/patches/BrandingWaterMarkPatch.java b/app/src/main/java/app/revanced/integrations/patches/BrandingWaterMarkPatch.java deleted file mode 100644 index e729e153fd..0000000000 --- a/app/src/main/java/app/revanced/integrations/patches/BrandingWaterMarkPatch.java +++ /dev/null @@ -1,11 +0,0 @@ -package app.revanced.integrations.patches; - -import app.revanced.integrations.settings.SettingsEnum; - -public class BrandingWaterMarkPatch { - - // Used by: app.revanced.patches.youtube.layout.watermark.patch.HideWatermarkPatch - public static boolean isBrandingWatermarkShown() { - return SettingsEnum.HIDE_VIDEO_WATERMARK.getBoolean() == false; - } -} diff --git a/app/src/main/java/app/revanced/integrations/patches/DisableFineScrubbingGesturePatch.java b/app/src/main/java/app/revanced/integrations/patches/DisableFineScrubbingGesturePatch.java new file mode 100644 index 0000000000..72a6859065 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/DisableFineScrubbingGesturePatch.java @@ -0,0 +1,18 @@ +package app.revanced.integrations.patches; + +import android.view.MotionEvent; +import android.view.VelocityTracker; +import app.revanced.integrations.settings.SettingsEnum; + +public final class DisableFineScrubbingGesturePatch { + /** + * Disables the fine scrubbing gesture. + * @param tracker The velocity tracker that is used to determine the gesture. + * @param event The motion event that is used to determine the gesture. + */ + public static void disableGesture(VelocityTracker tracker, MotionEvent event) { + if (SettingsEnum.DISABLE_FINE_SCRUBBING_GESTURE.getBoolean()) return; + + tracker.addMovement(event); + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/MinimizedPlaybackPatch.java b/app/src/main/java/app/revanced/integrations/patches/MinimizedPlaybackPatch.java index e7f8af3608..6fcae6bfc5 100644 --- a/app/src/main/java/app/revanced/integrations/patches/MinimizedPlaybackPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/MinimizedPlaybackPatch.java @@ -5,7 +5,7 @@ public class MinimizedPlaybackPatch { public static boolean isPlaybackNotShort() { - return !PlayerType.getCurrent().isNoneOrHidden(); + return !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized(); } public static boolean overrideMinimizedPlaybackAvailable() { 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 383d3185cb..7bbcb8b852 100644 --- a/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java @@ -1,36 +1,90 @@ package app.revanced.integrations.patches; +import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote; + import android.graphics.Rect; import android.os.Build; -import android.text.*; +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 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; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; -import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote; +import app.revanced.integrations.patches.components.ReturnYouTubeDislikeFilterPatch; +import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; +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}. + * Known limitation: + * Litho based Shorts player can experience temporarily frozen video playback if the RYD fetch takes too long. + * + * Temporary work around: + * Enable app spoofing to version 18.20.39 or older, as that uses a non litho Shorts player. + * + * Permanent fix (yet to be implemented), either of: + * - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes asynchronously. + * - Find a way to force Litho to rebuild it's component tree + * (and use that hook to force the shorts dislikes to update after the fetch is completed). */ public class ReturnYouTubeDislikePatch { + /** + * RYD data for the current video on screen. + */ @Nullable - private static String currentVideoId; + private static volatile ReturnYouTubeDislike currentVideoData; + /** + * The last litho based Shorts loaded. + * May be the same value as {@link #currentVideoData}, but usually is the next short to swipe to. + */ + @Nullable + private static volatile ReturnYouTubeDislike lastLithoShortsVideoData; + + /** + * Because the litho Shorts spans are created after {@link ReturnYouTubeDislikeFilterPatch} + * detects the video ids, after the user votes the litho will update + * but {@link #lastLithoShortsVideoData} is not the correct data to use. + * If this is true, then instead use {@link #currentVideoData}. + */ + private static volatile boolean lithoShortsShouldUseCurrentData; + + /** + * Last video id prefetched. Field is prevent prefetching the same video id multiple times in a row. + */ + @Nullable + private static volatile String lastPrefetchedVideoId; + + public static void onRYDStatusChange(boolean rydEnabled) { + if (!rydEnabled) { + // Must remove all values to protect against using stale data + // if the user enables RYD while a video is on screen. + currentVideoData = null; + lastLithoShortsVideoData = null; + lithoShortsShouldUseCurrentData = false; + } + } + + + // + // 17.x non litho regular video player. + // /** * Resource identifier of old UI dislike button. @@ -59,7 +113,7 @@ public class ReturnYouTubeDislikePatch { /** * Old UI dislikes can be set multiple times by YouTube. - * To prevent it from reverting changes made here, this listener overrides any future changes YouTube makes. + * To prevent reverting changes made here, this listener overrides any future changes YouTube makes. */ private static final TextWatcher oldUiTextWatcher = new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int count, int after) { @@ -75,11 +129,15 @@ public void afterTextChanged(Editable s) { }; private static void updateOldUIDislikesTextView() { + ReturnYouTubeDislike videoData = currentVideoData; + if (videoData == null) { + return; + } TextView oldUITextView = oldUITextViewRef.get(); if (oldUITextView == null) { return; } - oldUIReplacementSpan = ReturnYouTubeDislike.getDislikesSpanForRegularVideo(oldUIOriginalSpan, false); + oldUIReplacementSpan = videoData.getDislikesSpanForRegularVideo(oldUIOriginalSpan, false); if (!oldUIReplacementSpan.equals(oldUITextView.getText())) { oldUITextView.setText(oldUIReplacementSpan); } @@ -88,7 +146,7 @@ private static void updateOldUIDislikesTextView() { /** * Injection point. Called on main thread. * - * Used when spoofing the older app versions of {@link SpoofAppVersionPatch}. + * Used when spoofing to 16.x and 17.x versions. */ public static void setOldUILayoutDislikes(int buttonViewResourceId, @Nullable TextView textView) { try { @@ -123,6 +181,10 @@ public static void setOldUILayoutDislikes(int buttonViewResourceId, @Nullable Te } + // + // Litho player for both regular videos and Shorts. + // + /** * Injection point. * @@ -148,18 +210,47 @@ public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, } String conversionContextString = conversionContext.toString(); + // Remove this log statement after the a/b new litho dislikes is fixed. LogHelper.printDebug(() -> "conversionContext: " + conversionContextString); final Spanned replacement; if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) { - replacement = ReturnYouTubeDislike.getDislikesSpanForRegularVideo((Spannable) original, true); - } else if (conversionContextString.contains("|dislike_button.eml|") ) { - // This code path is basically dead because it's only used when spoofing between 17.09.xx and 17.30.xx - // but spoofing to that range gives a broken UI layout. - // Keep this check here anyways just in case the old litho layout is somehow still used. - replacement = ReturnYouTubeDislike.getDislikesSpanForRegularVideo((Spannable) original, false); + // Regular video + ReturnYouTubeDislike videoData = currentVideoData; + if (videoData == null) { + return original; // User enabled RYD while a video was on screen. + } + replacement = videoData.getDislikesSpanForRegularVideo((Spannable) original, true); + // When spoofing between 17.09.xx and 17.30.xx the UI is the old layout but uses litho + // and the dislikes is "|dislike_button.eml|" + // but spoofing to that range gives a broken UI layout so no point checking for that. } else if (conversionContextString.contains("|shorts_dislike_button.eml|")) { - replacement = ReturnYouTubeDislike.getDislikeSpanForShort((Spannable) original); + // Litho Shorts player. + if (!SettingsEnum.RYD_SHORTS.getBoolean()) { + // Must clear the current video here, otherwise if the user opens a regular video + // then opens a litho short (while keeping the regular video on screen), then closes the short, + // the original video may show the incorrect dislike value. + currentVideoData = null; + return original; + } + ReturnYouTubeDislike videoData = lastLithoShortsVideoData; + if (videoData == null) { + // The Shorts litho video id filter did not detect the video id. + // This is normal if in incognito mode, but otherwise is not normal. + LogHelper.printDebug(() -> "Cannot modify Shorts litho span, data is null"); + return original; + } + // Use the correct dislikes data after voting. + if (lithoShortsShouldUseCurrentData) { + lithoShortsShouldUseCurrentData = false; + videoData = currentVideoData; + if (videoData == null) { + LogHelper.printException(() -> "currentVideoData is null"); // Should never happen + return original; + } + LogHelper.printDebug(() -> "Using current video data for litho span"); + } + replacement = videoData.getDislikeSpanForShort((Spannable) original); } else { return original; } @@ -173,6 +264,10 @@ public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, } + // + // Non litho Shorts player. + // + /** * Replacement text to use for "Dislikes" while RYD is fetching. */ @@ -187,18 +282,16 @@ public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, private static final List> shortsTextViewRefs = new ArrayList<>(); private static void clearRemovedShortsTextViews() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // YouTube requires Android N or greater 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. + * Injection point. Called when a Shorts dislike is updated. Always on main thread. * 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 + * @return if RYD is enabled and the TextView was updated. */ public static boolean setShortsDislikes(@NonNull View likeDislikeView) { try { @@ -208,21 +301,22 @@ public static boolean setShortsDislikes(@NonNull View likeDislikeView) { if (!SettingsEnum.RYD_SHORTS.getBoolean()) { // Must clear the data here, in case a new video was loaded while PlayerType // suggested the video was not a short (can happen when spoofing to an old app version). - ReturnYouTubeDislike.setCurrentVideoId(null); + currentVideoData = null; return false; } LogHelper.printDebug(() -> "setShortsDislikes"); TextView textView = (TextView) likeDislikeView; - textView.setText(SHORTS_LOADING_SPAN); // Change 'Dislike' text to the loading text + 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); + ReturnYouTubeDislike videoData = currentVideoData; + if (videoData != null) videoData.setUserVote(Vote.DISLIKE); } - // For the first short played, the shorts dislike hook is called after the video id hook. + // 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); @@ -244,13 +338,17 @@ private static void updateOnScreenShortsTextViews(boolean forceUpdate) { if (shortsTextViewRefs.isEmpty()) { return; } + ReturnYouTubeDislike videoData = currentVideoData; + if (videoData == null) { + return; + } LogHelper.printDebug(() -> "updateShortsTextViews"); - String videoId = VideoInformation.getVideoId(); Runnable update = () -> { - Spanned shortsDislikesSpan = ReturnYouTubeDislike.getDislikeSpanForShort(SHORTS_LOADING_SPAN); + Spanned shortsDislikesSpan = videoData.getDislikeSpanForShort(SHORTS_LOADING_SPAN); ReVancedUtils.runOnMainThreadNowOrLater(() -> { + String videoId = videoData.getVideoId(); if (!videoId.equals(VideoInformation.getVideoId())) { // User swiped to new video before fetch completed LogHelper.printDebug(() -> "Ignoring stale dislikes data for short: " + videoId); @@ -274,13 +372,13 @@ private static void updateOnScreenShortsTextViews(boolean forceUpdate) { } }); }; - if (ReturnYouTubeDislike.fetchCompleted()) { + if (videoData.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); + LogHelper.printException(() -> "updateOnScreenShortsTextViews failure", ex); } } @@ -298,35 +396,107 @@ private static boolean isShortTextViewOnScreen(@NonNull View view) { return location[0] < windowRect.width() && location[1] < windowRect.height(); } + + // + // Video Id and voting hooks (all players). + // + /** - * Injection point. + * Injection point. Uses 'playback response' video id hook to preload RYD. + */ + public static void preloadVideoId(@NonNull String videoId) { + if (!SettingsEnum.RYD_ENABLED.getBoolean()) { + return; + } + if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneOrHidden()) { + return; + } + if (videoId.equals(lastPrefetchedVideoId)) { + return; + } + lastPrefetchedVideoId = videoId; + LogHelper.printDebug(() -> "Prefetching RYD for video: " + videoId); + ReturnYouTubeDislike.getFetchForVideoId(videoId); + } + + /** + * Injection point. Uses 'current playing' video id hook. Always called on main thread. */ public static void newVideoLoaded(@NonNull String videoId) { + newVideoLoaded(videoId, false); + } + + /** + * Called both on and off main thread. + * + * @param isShortsLithoVideoId If the video id is from {@link ReturnYouTubeDislikeFilterPatch}. + * if true, then the video id can be null indicating the filter did + * not find any video id. + */ + public static void newVideoLoaded(@Nullable String videoId, boolean isShortsLithoVideoId) { try { if (!SettingsEnum.RYD_ENABLED.getBoolean()) return; - if (!videoId.equals(currentVideoId)) { - currentVideoId = videoId; + PlayerType currentPlayerType = PlayerType.getCurrent(); + final boolean isNoneHiddenOrSlidingMinimized = currentPlayerType.isNoneHiddenOrSlidingMinimized(); + if (isNoneHiddenOrSlidingMinimized && !SettingsEnum.RYD_SHORTS.getBoolean()) { + // Must clear here, otherwise the wrong data can be used for a minimized regular video. + currentVideoData = null; + return; + } - final boolean noneHiddenOrMinimized = PlayerType.getCurrent().isNoneOrHidden(); - if (noneHiddenOrMinimized && !SettingsEnum.RYD_SHORTS.getBoolean()) { - ReturnYouTubeDislike.setCurrentVideoId(null); + if (isShortsLithoVideoId) { + // Litho Shorts video. + if (videoIdIsSame(lastLithoShortsVideoData, videoId)) { + return; + } + if (videoId == null) { + // Litho filter did not detect the video id. App is in incognito mode, + // or the proto buffer structure was changed and the video id is no longer present. + // Must clear both currently playing and last litho data otherwise the + // next regular video may use the wrong data. + LogHelper.printDebug(() -> "Litho filter did not find any video ids"); + currentVideoData = null; + lastLithoShortsVideoData = null; + lithoShortsShouldUseCurrentData = false; + return; + } + ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(videoId); + videoData.setVideoIdIsShort(true); + lastLithoShortsVideoData = videoData; + lithoShortsShouldUseCurrentData = false; + } else { + Objects.requireNonNull(videoId); + // All other playback (including non-litho Shorts). + if (videoIdIsSame(currentVideoData, videoId)) { return; } + currentVideoData = ReturnYouTubeDislike.getFetchForVideoId(videoId); + // Pre-emptively set the data to short status. + // Required to prevent Shorts data from being used on a minimized video in incognito mode. + if (isNoneHiddenOrSlidingMinimized) { + currentVideoData.setVideoIdIsShort(true); + } + } - ReturnYouTubeDislike.newVideoLoaded(videoId); + LogHelper.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType + + " isShortsLithoHook: " + isShortsLithoVideoId); - if (noneHiddenOrMinimized) { - // Shorts TextView hook can be called out of order with the video id hook. - // Must manually update again here. - updateOnScreenShortsTextViews(true); - } + // Current video id hook can be called out of order with the non litho Shorts text view hook. + // Must manually update again here. + if (!isShortsLithoVideoId && isNoneHiddenOrSlidingMinimized) { + updateOnScreenShortsTextViews(true); } } catch (Exception ex) { LogHelper.printException(() -> "newVideoLoaded failure", ex); } } + private static boolean videoIdIsSame(@Nullable ReturnYouTubeDislike fetch, @Nullable String videoId) { + return (fetch == null && videoId == null) + || (fetch != null && fetch.getVideoId().equals(videoId)); + } + /** * Injection point. * @@ -339,15 +509,27 @@ public static void sendVote(int vote) { if (!SettingsEnum.RYD_ENABLED.getBoolean()) { return; } - if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrMinimized()) { + final boolean isNoneHiddenOrMinimized = PlayerType.getCurrent().isNoneHiddenOrMinimized(); + if (isNoneHiddenOrMinimized && !SettingsEnum.RYD_SHORTS.getBoolean()) { return; } + ReturnYouTubeDislike videoData = currentVideoData; + if (videoData == null) { + LogHelper.printDebug(() -> "Cannot send vote, as current video data is null"); + return; // User enabled RYD while a regular video was minimized. + } for (Vote v : Vote.values()) { if (v.value == vote) { - ReturnYouTubeDislike.sendVote(v); + videoData.sendVote(v); + + if (isNoneHiddenOrMinimized) { + if (lastLithoShortsVideoData != null) { + lithoShortsShouldUseCurrentData = true; + } + updateOldUIDislikesTextView(); + } - updateOldUIDislikesTextView(); return; } } 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 ad5bcd1686..ee9034b0b3 100644 --- a/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java +++ b/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java @@ -1,16 +1,15 @@ package app.revanced.integrations.patches; 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; +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.util.Objects; + /** * Hooking class for the current playing video. */ @@ -25,6 +24,10 @@ public final class VideoInformation { private static String videoId = ""; private static long videoLength = 0; private static long videoTime = -1; + + @NonNull + private static volatile String playerResponseVideoId = ""; + /** * The current playback speed */ @@ -61,6 +64,18 @@ public static void setVideoId(@NonNull String newlyLoadedVideoId) { } } + /** + * Injection point. Called off the main thread. + * + * @param videoId The id of the last video loaded. + */ + public static void setPlayerResponseVideoId(@NonNull String videoId) { + if (!playerResponseVideoId.equals(videoId)) { + LogHelper.printDebug(() -> "New player response video id: " + videoId); + playerResponseVideoId = videoId; + } + } + /** * Injection point. * Called when user selects a playback speed. @@ -141,6 +156,22 @@ public static String getVideoId() { return videoId; } + /** + * Differs from {@link #videoId} as this is the video id for the + * last player response received, which may not be the current video playing. + * + * If Shorts are loading the background, this commonly will be + * different from the Short that is currently on screen. + * + * For most use cases, you should instead use {@link #getVideoId()}. + * + * @return The id of the last video loaded. Empty string if not set yet. + */ + @NonNull + public static String getPlayerResponseVideoId() { + return playerResponseVideoId; + } + /** * @return The current playback speed. */ diff --git a/app/src/main/java/app/revanced/integrations/patches/components/LayoutComponentsFilter.java b/app/src/main/java/app/revanced/integrations/patches/components/LayoutComponentsFilter.java index 7c227ba78c..a5882d2210 100644 --- a/app/src/main/java/app/revanced/integrations/patches/components/LayoutComponentsFilter.java +++ b/app/src/main/java/app/revanced/integrations/patches/components/LayoutComponentsFilter.java @@ -19,14 +19,18 @@ public final class LayoutComponentsFilter extends Filter { SettingsEnum.HIDE_MIX_PLAYLISTS, "&list=" ); + private final StringFilterGroup searchResultShelfHeader; + private final StringFilterGroup inFeedSurvey; + private final StringFilterGroup notifyMe; + private final StringFilterGroup expandableMetadata; @RequiresApi(api = Build.VERSION_CODES.N) public LayoutComponentsFilter() { exceptions.addPatterns( "home_video_with_context", "related_video_with_context", - "comment_thread", // skip filtering anything in the comments - "|comment.", // skip filtering anything in the comments replies + "comment_thread", // Whitelist comments + "|comment.", // Whitelist comment replies "library_recent_shelf" ); @@ -61,7 +65,7 @@ public LayoutComponentsFilter() { "compact_banner" ); - final var inFeedSurvey = new StringFilterGroup( + inFeedSurvey = new StringFilterGroup( SettingsEnum.HIDE_FEED_SURVEY, "in_feed_survey", "slimline_survey" @@ -111,7 +115,7 @@ public LayoutComponentsFilter() { "official_card" ); - final var expandableMetadata = new StringFilterGroup( + expandableMetadata = new StringFilterGroup( SettingsEnum.HIDE_EXPANDABLE_CHIP, "inline_expander" ); @@ -146,53 +150,91 @@ public LayoutComponentsFilter() { "cell_divider" // layout residue (gray line above the buttoned ad), ); + final var timedReactions = new StringFilterGroup( + SettingsEnum.HIDE_TIMED_REACTIONS, + "emoji_control_panel", + "timed_reaction" + ); + + searchResultShelfHeader = new StringFilterGroup( + SettingsEnum.HIDE_SEARCH_RESULT_SHELF_HEADER, + "shelf_header.eml" + ); + + notifyMe = new StringFilterGroup( + SettingsEnum.HIDE_NOTIFY_ME_BUTTON, + "set_reminder_button" + ); + + final var joinMembership = new StringFilterGroup( + SettingsEnum.HIDE_JOIN_MEMBERSHIP_BUTTON, + "compact_sponsor_button" + ); + final var chipsShelf = new StringFilterGroup( SettingsEnum.HIDE_CHIPS_SHELF, "chips_shelf" ); + final var channelWatermark = new StringFilterGroup( + SettingsEnum.HIDE_VIDEO_CHANNEL_WATERMARK, + "featured_channel_watermark_overlay" + ); + this.pathFilterGroupList.addAll( channelBar, communityPosts, paidContent, latestPosts, - chapters, + channelWatermark, communityGuidelines, quickActions, expandableMetadata, relatedVideos, compactBanner, inFeedSurvey, + joinMembership, medicalPanel, + notifyMe, infoPanel, + subscribersCommunityGuidelines, channelGuidelines, audioTrackButton, artistCard, + timedReactions, imageShelf, - subscribersCommunityGuidelines, channelMemberShelf, custom ); this.identifierFilterGroupList.addAll( graySeparator, - chipsShelf + chipsShelf, + chapters ); } @Override public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) { + + // The groups are excluded from the filter due to the exceptions list below. + // Filter them separately here. + if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata) + return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex); + if (matchedGroup != custom && exceptions.matches(path)) return false; // Exceptions are not filtered. + // TODO: This also hides the feed Shorts shelf header + if (matchedGroup == searchResultShelfHeader && matchedIndex != 0) return false; + return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex); } /** * Injection point. - * * Called from a different place then the other filters. */ public static boolean filterMixPlaylists(final byte[] bytes) { @@ -203,4 +245,8 @@ public static boolean filterMixPlaylists(final byte[] bytes) { return isMixPlaylistFiltered; } + + public static boolean showWatermark() { + return !SettingsEnum.HIDE_VIDEO_CHANNEL_WATERMARK.getBoolean(); + } } diff --git a/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java index 7cfe060b1d..5099a4df28 100644 --- a/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; + import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.utils.*; @@ -13,12 +14,24 @@ abstract class FilterGroup { final static class FilterGroupResult { - SettingsEnum setting; - boolean filtered; + private SettingsEnum setting; + private int matchedIndex; + private int matchedLength; + // In the future it might be useful to include which pattern matched, + // but for now that is not needed. + + FilterGroupResult() { + this(null, -1, 0); + } - FilterGroupResult(SettingsEnum setting, boolean filtered) { + FilterGroupResult(SettingsEnum setting, int matchedIndex, int matchedLength) { + setValues(setting, matchedIndex, matchedLength); + } + + public void setValues(SettingsEnum setting, int matchedIndex, int matchedLength) { this.setting = setting; - this.filtered = filtered; + this.matchedIndex = matchedIndex; + this.matchedLength = matchedLength; } /** @@ -30,7 +43,21 @@ public SettingsEnum getSetting() { } public boolean isFiltered() { - return filtered; + return matchedIndex >= 0; + } + + /** + * Matched index of first pattern that matched, or -1 if nothing matched. + */ + public int getMatchedIndex() { + return matchedIndex; + } + + /** + * Length of the matched filter pattern. + */ + public int getMatchedLength() { + return matchedLength; } } @@ -81,7 +108,21 @@ public StringFilterGroup(final SettingsEnum setting, final String... filters) { @Override public FilterGroupResult check(final String string) { - return new FilterGroupResult(setting, isEnabled() && ReVancedUtils.containsAny(string, filters)); + int matchedIndex = -1; + int matchedLength = 0; + if (isEnabled()) { + for (String pattern : filters) { + if (!string.isEmpty()) { + final int indexOf = pattern.indexOf(string); + if (indexOf >= 0) { + matchedIndex = indexOf; + matchedLength = pattern.length(); + break; + } + } + } + } + return new FilterGroupResult(setting, matchedIndex, matchedLength); } } @@ -155,19 +196,22 @@ private synchronized void buildFailurePatterns() { @Override public FilterGroupResult check(final byte[] bytes) { - var matched = false; + int matchedLength = 0; + int matchedIndex = -1; if (isEnabled()) { if (failurePatterns == null) { buildFailurePatterns(); // Lazy load. } for (int i = 0, length = filters.length; i < length; i++) { - if (indexOf(bytes, filters[i], failurePatterns[i]) >= 0) { - matched = true; + byte[] filter = filters[i]; + matchedIndex = indexOf(bytes, filter, failurePatterns[i]); + if (matchedIndex >= 0) { + matchedLength = filter.length; break; } } } - return new FilterGroupResult(setting, matched); + return new FilterGroupResult(setting, matchedIndex, matchedLength); } } @@ -204,11 +248,10 @@ protected final synchronized void buildSearch() { continue; } for (V pattern : group.filters) { - search.addPattern(pattern, (textSearched, matchedStartIndex, callbackParameter) -> { + search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { if (group.isEnabled()) { FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter; - result.setting = group.setting; - result.filtered = true; + result.setValues(group.setting, matchedStartIndex, matchedLength); return true; } return false; @@ -241,9 +284,10 @@ protected FilterGroup.FilterGroupResult check(V stack) { if (search == null) { buildSearch(); // Lazy load. } - FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(null, false); + FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(); search.matches(stack, result); return result; + } protected abstract TrieSearch createSearchGraph(); @@ -399,7 +443,7 @@ private static void filterGroupLists(TrieSearch pathSearchTree, continue; } for (T pattern : group.filters) { - pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex, callbackParameter) -> { + pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { if (!group.isEnabled()) return false; LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter; return filter.isFiltered(parameters.identifier, parameters.path, parameters.protoBuffer, diff --git a/app/src/main/java/app/revanced/integrations/patches/components/ReturnYouTubeDislikeFilterPatch.java b/app/src/main/java/app/revanced/integrations/patches/components/ReturnYouTubeDislikeFilterPatch.java new file mode 100644 index 0000000000..b52fd486fc --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/components/ReturnYouTubeDislikeFilterPatch.java @@ -0,0 +1,133 @@ +package app.revanced.integrations.patches.components; + +import android.os.Build; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; + +import app.revanced.integrations.patches.ReturnYouTubeDislikePatch; +import app.revanced.integrations.patches.VideoInformation; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.TrieSearch; + +/** + * Searches for video id's in the proto buffer of Shorts dislike. + * + * Because multiple litho dislike spans are created in the background + * (and also anytime litho refreshes the components, which is somewhat arbitrary), + * that makes the value of {@link VideoInformation#getVideoId()} and {@link VideoInformation#getPlayerResponseVideoId()} + * unreliable to determine which video id a Shorts litho span belongs to. + * + * But the correct video id does appear in the protobuffer just before a Shorts litho span is created. + * + * Once a way to asynchronously update litho text is found, this strategy will no longer be needed. + */ +@RequiresApi(api = Build.VERSION_CODES.N) +public final class ReturnYouTubeDislikeFilterPatch extends Filter { + + /** + * Last unique video id's loaded. Value is ignored and Map is treated as a Set. + * Cannot use {@link LinkedHashSet} because it's missing #removeEldestEntry(). + */ + @GuardedBy("itself") + private static final Map lastVideoIds = new LinkedHashMap<>() { + /** + * Number of video id's to keep track of for searching thru the buffer. + * A minimum value of 3 should be sufficient, but check a few more just in case. + */ + private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 5; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK; + } + }; + + /** + * Injection point. + */ + public static void newPlayerResponseVideoId(String videoId) { + try { + if (!SettingsEnum.RYD_SHORTS.getBoolean()) { + return; + } + synchronized (lastVideoIds) { + if (lastVideoIds.put(videoId, Boolean.TRUE) == null) { + LogHelper.printDebug(() -> "New video id: " + videoId); + } + } + } catch (Exception ex) { + LogHelper.printException(() -> "newPlayerResponseVideoId failure", ex); + } + } + + private final ByteArrayFilterGroupList videoIdFilterGroup = new ByteArrayFilterGroupList(); + + public ReturnYouTubeDislikeFilterPatch() { + pathFilterGroupList.addAll( + new StringFilterGroup(SettingsEnum.RYD_SHORTS, "|shorts_dislike_button.eml|") + ); + // After the dislikes icon name is some binary data and then the video id for that specific short. + videoIdFilterGroup.addAll( + // Video was previously disliked before video was opened. + new ByteArrayAsStringFilterGroup(null, "ic_right_dislike_on_shadowed"), + // Video was not already disliked. + new ByteArrayAsStringFilterGroup(null, "ic_right_dislike_off_shadowed") + ); + } + + @Override + public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) { + FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray); + if (result.isFiltered()) { + String matchedVideoId = findVideoId(protobufBufferArray); + // Matched video will be null if in incognito mode. + // Must pass a null id to correctly clear out the current video data. + // Otherwise if a Short is opened in non-incognito, then incognito is enabled and another Short is opened, + // the new incognito Short will show the old prior data. + ReturnYouTubeDislikePatch.newVideoLoaded(matchedVideoId, true); + } + + return false; + } + + @Nullable + private String findVideoId(byte[] protobufBufferArray) { + synchronized (lastVideoIds) { + for (String videoId : lastVideoIds.keySet()) { + if (byteArrayContainsString(protobufBufferArray, videoId)) { + return videoId; + } + } + return null; + } + } + + /** + * This could use {@link TrieSearch}, but since the video ids are constantly changing + * the overhead of updating the Trie might negate the search performance gain. + */ + private static boolean byteArrayContainsString(@NonNull byte[] array, @NonNull String text) { + for (int i = 0, lastArrayStartIndex = array.length - text.length(); i <= lastArrayStartIndex; i++) { + boolean found = true; + for (int j = 0, textLength = text.length(); j < textLength; j++) { + if (array[i + j] != (byte) text.charAt(j)) { + found = false; + break; + } + } + if (found) { + return true; + } + } + return false; + } +} \ No newline at end of file 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 index e253d9173f..7ab5a54296 100644 --- a/app/src/main/java/app/revanced/integrations/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/patches/components/ShortsFilter.java @@ -10,12 +10,15 @@ import static app.revanced.integrations.utils.ReVancedUtils.hideViewBy1dpUnderCondition; import static app.revanced.integrations.utils.ReVancedUtils.hideViewUnderCondition; +/** @noinspection unused*/ @RequiresApi(api = Build.VERSION_CODES.N) public final class ShortsFilter extends Filter { public static PivotBar pivotBar; // Set by patch. private final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml"; private final StringFilterGroup channelBar; + private final StringFilterGroup subscribeButton; + private final StringFilterGroup subscribeButtonPaused; private final StringFilterGroup soundButton; private final StringFilterGroup infoPanel; private final StringFilterGroup shelfHeader; @@ -53,15 +56,22 @@ public ShortsFilter() { SettingsEnum.HIDE_SHORTS_JOIN_BUTTON, "sponsor_button" ); - var subscribeButton = new StringFilterGroup( + + subscribeButton = new StringFilterGroup( SettingsEnum.HIDE_SHORTS_SUBSCRIBE_BUTTON, "subscribe_button" ); + subscribeButtonPaused = new StringFilterGroup( + SettingsEnum.HIDE_SHORTS_SUBSCRIBE_BUTTON_PAUSED, + "shorts_paused_state" + ); + channelBar = new StringFilterGroup( SettingsEnum.HIDE_SHORTS_CHANNEL_BAR, REEL_CHANNEL_BAR_PATH ); + soundButton = new StringFilterGroup( SettingsEnum.HIDE_SHORTS_SOUND_BUTTON, "reel_pivot_button" @@ -78,7 +88,8 @@ public ShortsFilter() { ); pathFilterGroupList.addAll( - joinButton, subscribeButton, channelBar, soundButton, infoPanel, videoActionButton + joinButton, subscribeButton, subscribeButtonPaused, + channelBar, soundButton, infoPanel, videoActionButton ); var shortsCommentButton = new ByteArrayAsStringFilterGroup( @@ -104,20 +115,28 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) { if (matchedList == pathFilterGroupList) { // Always filter if matched. - if (matchedGroup == soundButton || matchedGroup == infoPanel || matchedGroup == channelBar) - return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex); + if (matchedGroup == soundButton || + matchedGroup == infoPanel || + matchedGroup == channelBar || + matchedGroup == subscribeButtonPaused + ) return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex); // Video action buttons (comment, share, remix) have the same path. if (matchedGroup == videoActionButton) { - if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) - return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex); + if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) return super.isFiltered( + identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex + ); return false; } // Filter other path groups from pathFilterGroupList, only when reelChannelBar is visible // to avoid false positives. - if (!path.startsWith(REEL_CHANNEL_BAR_PATH)) - return false; + if (path.startsWith(REEL_CHANNEL_BAR_PATH)) + if (matchedGroup == subscribeButton) return super.isFiltered( + identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex + ); + + return false; } else if (matchedGroup == shelfHeader) { // Because the header is used in watch history and possibly other places, check for the index, // which is 0 when the shelf header is used for Shorts. diff --git a/app/src/main/java/app/revanced/integrations/patches/SpoofAppVersionPatch.java b/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofAppVersionPatch.java similarity index 87% rename from app/src/main/java/app/revanced/integrations/patches/SpoofAppVersionPatch.java rename to app/src/main/java/app/revanced/integrations/patches/spoof/SpoofAppVersionPatch.java index 4967bb9519..c43d603127 100644 --- a/app/src/main/java/app/revanced/integrations/patches/SpoofAppVersionPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofAppVersionPatch.java @@ -1,4 +1,4 @@ -package app.revanced.integrations.patches; +package app.revanced.integrations.patches.spoof; import app.revanced.integrations.settings.SettingsEnum; diff --git a/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofSignaturePatch.java b/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofSignaturePatch.java index 69f00abfd6..16a284d049 100644 --- a/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofSignaturePatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofSignaturePatch.java @@ -1,19 +1,20 @@ package app.revanced.integrations.patches.spoof; +import static app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer; +import static app.revanced.integrations.utils.ReVancedUtils.containsAny; + import androidx.annotation.Nullable; -import app.revanced.integrations.patches.VideoInformation; -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.shared.PlayerType; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import static app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer; -import static app.revanced.integrations.utils.ReVancedUtils.containsAny; +import app.revanced.integrations.patches.VideoInformation; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.shared.PlayerType; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; /** @noinspection unused*/ public class SpoofSignaturePatch { @@ -46,7 +47,7 @@ public class SpoofSignaturePatch { /** * Last video id loaded. Used to prevent reloading the same spec multiple times. */ - private static volatile String currentVideoId; + private static volatile String lastPlayerResponseVideoId; private static volatile Future rendererFuture; @@ -83,7 +84,6 @@ public static String spoofParameter(String parameters) { var isClip = parameters.length() > 150; if (isClip) return parameters; - // Shorts do not need to be spoofed. if (parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) return parameters; @@ -96,18 +96,23 @@ public static String spoofParameter(String parameters) { // This will cause playback issues in the feed, but it's better than manipulating the history. parameters; - String videoId = VideoInformation.getVideoId(); - if (!videoId.equals(currentVideoId)) { + fetchStoryboardRenderer(); + + return INCOGNITO_PARAMETERS; + } + + private static void fetchStoryboardRenderer() { + String videoId = VideoInformation.getPlayerResponseVideoId(); + if (!videoId.equals(lastPlayerResponseVideoId)) { rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId)); - currentVideoId = videoId; + lastPlayerResponseVideoId = videoId; } - // Occasionally when a new video is opened the video will be frozen a few seconds while the audio plays. + // Block until the fetch is completed. Without this, occasionally when a new video is opened + // the video will be frozen a few seconds while the audio plays. // This is because the main thread is calling to get the storyboard but the fetch is not completed. // To prevent this, call get() here and block until the fetch is completed. // So later when the main thread calls to get the renderer it will never block as the future is done. getRenderer(); - - return INCOGNITO_PARAMETERS; } /** @@ -123,23 +128,26 @@ public static boolean getSeekbarThumbnailOverrideValue() { */ @Nullable public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) { - if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) return originalStoryboardRendererSpec; - - StoryboardRenderer renderer = getRenderer(); - if (renderer == null) return originalStoryboardRendererSpec; + if (SettingsEnum.SPOOF_SIGNATURE.getBoolean()) { + StoryboardRenderer renderer = getRenderer(); + if (renderer != null) return renderer.getSpec(); + } - return renderer.getSpec(); + return originalStoryboardRendererSpec; } /** * Injection point. */ public static int getRecommendedLevel(int originalLevel) { - if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) return originalLevel; - - StoryboardRenderer renderer = getRenderer(); - if (renderer == null) return originalLevel; + if (SettingsEnum.SPOOF_SIGNATURE.getBoolean()) { + StoryboardRenderer renderer = getRenderer(); + if (renderer != null) { + Integer recommendedLevel = renderer.getRecommendedLevel(); + if (recommendedLevel != null) return recommendedLevel; + } + } - return renderer.getRecommendedLevel(); + return originalLevel; } } diff --git a/app/src/main/java/app/revanced/integrations/patches/spoof/StoryboardRenderer.java b/app/src/main/java/app/revanced/integrations/patches/spoof/StoryboardRenderer.java index c7ce43e57d..d0e70988bf 100644 --- a/app/src/main/java/app/revanced/integrations/patches/spoof/StoryboardRenderer.java +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/StoryboardRenderer.java @@ -1,13 +1,16 @@ package app.revanced.integrations.patches.spoof; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.jetbrains.annotations.NotNull; public final class StoryboardRenderer { private final String spec; - private final int recommendedLevel; + @Nullable + private final Integer recommendedLevel; - public StoryboardRenderer(String spec, int recommendedLevel) { + public StoryboardRenderer(String spec, @Nullable Integer recommendedLevel) { this.spec = spec; this.recommendedLevel = recommendedLevel; } @@ -17,7 +20,11 @@ public String getSpec() { return spec; } - public int getRecommendedLevel() { + /** + * @return Recommended image quality level, or NULL if no recommendation exists. + */ + @Nullable + public Integer getRecommendedLevel() { return recommendedLevel; } diff --git a/app/src/main/java/app/revanced/integrations/patches/spoof/requests/StoryboardRendererRequester.java b/app/src/main/java/app/revanced/integrations/patches/spoof/requests/StoryboardRendererRequester.java index 3138630570..61828a04bb 100644 --- a/app/src/main/java/app/revanced/integrations/patches/spoof/requests/StoryboardRendererRequester.java +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/requests/StoryboardRendererRequester.java @@ -49,7 +49,7 @@ private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) try { return playerResponse.getJSONObject("playabilityStatus").getString("status").equals("OK"); } catch (JSONException e) { - LogHelper.printException(() -> "Failed to get playabilityStatus", e); + LogHelper.printDebug(() -> "Failed to get playabilityStatus for response: " + playerResponse); } return false; @@ -80,7 +80,9 @@ private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull JS final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag); StoryboardRenderer renderer = new StoryboardRenderer( rendererElement.getString("spec"), - rendererElement.getInt("recommendedLevel") + rendererElement.has("recommendedLevel") + ? rendererElement.getInt("recommendedLevel") + : null ); LogHelper.printDebug(() -> "Fetched: " + renderer); diff --git a/app/src/main/java/app/revanced/integrations/patches/theme/ThemeLithoComponentsPatch.java b/app/src/main/java/app/revanced/integrations/patches/theme/ThemePatch.java similarity index 90% rename from app/src/main/java/app/revanced/integrations/patches/theme/ThemeLithoComponentsPatch.java rename to app/src/main/java/app/revanced/integrations/patches/theme/ThemePatch.java index da9f3963c4..e9796db250 100644 --- a/app/src/main/java/app/revanced/integrations/patches/theme/ThemeLithoComponentsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/theme/ThemePatch.java @@ -1,9 +1,10 @@ package app.revanced.integrations.patches.theme; +import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.utils.ReVancedUtils; import app.revanced.integrations.utils.ThemeHelper; -public class ThemeLithoComponentsPatch { +public class ThemePatch { // color constants used in relation with litho components private static final int[] WHITE_VALUES = { -1, // comments chip background @@ -40,6 +41,10 @@ public static int getValue(int originalValue) { return originalValue; } + public static boolean gradientLoadingScreenEnabled() { + return SettingsEnum.GRADIENT_LOADING_SCREEN.getBoolean(); + } + private static int getBlackColor() { if (blackColor == 0) blackColor = ReVancedUtils.getResourceColor("yt_black1"); return blackColor; 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 21f72b9d39..da8cd810a9 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java @@ -45,40 +45,21 @@ import app.revanced.integrations.utils.ThemeHelper; /** + * Handles fetching and creation/replacing of RYD dislike text spans. + * * 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(); - } + public enum Vote { + LIKE(1), + DISLIKE(-1), + LIKE_REMOVE(0); - boolean isExpired(long now) { - return (now - timeFetched) > CACHE_TIMEOUT_MILLISECONDS; - } + public final int value; - 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; + Vote(int value) { + this.value = value; } } @@ -88,67 +69,35 @@ boolean futureInProgressOrFinishedSuccessfully() { * 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_WAITING_FOR_FETCH = 4000; - - /** - * Unique placeholder character, used to detect if a segmented span already has dislikes added to it. - * Can be any almost any non-visible character. - */ - private static final char MIDDLE_SEPARATOR_CHARACTER = '\u2009'; // 'narrow space' character + private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH = 4500; /** - * Cached lookup of RYD fetches. + * How long to retain successful RYD fetches. */ - @GuardedBy("videoIdLockObject") - private static final Map futureCache = new HashMap<>(); + private static final long CACHE_TIMEOUT_SUCCESS_MILLISECONDS = 7 * 60 * 1000; // 7 Minutes /** - * Used to send votes, one by one, in the same order the user created them. + * How long to retain unsuccessful RYD fetches, + * and also the minimum time before retrying again. */ - private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor(); + private static final long CACHE_TIMEOUT_FAILURE_MILLISECONDS = 2 * 60 * 1000; // 2 Minutes /** - * Used to guard {@link #currentVideoId} and {@link #voteFetchFuture}. - */ - private static final Object videoIdLockObject = new Object(); - - @Nullable - @GuardedBy("videoIdLockObject") - private static String currentVideoId; - - /** - * If {@link #currentVideoId} and the RYD data is for the last shorts loaded. - */ - private static volatile boolean dislikeDataIsShort; - - /** - * Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes. - */ - @Nullable - @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. + * Unique placeholder character, used to detect if a segmented span already has dislikes added to it. + * Can be any almost any non-visible character. */ - @Nullable - @GuardedBy("videoIdLockObject") - private static Vote userVote; + private static final char MIDDLE_SEPARATOR_CHARACTER = '\u2009'; // 'narrow space' character /** - * Original dislike span, before modifications. + * Cached lookup of all video ids. */ - @Nullable - @GuardedBy("videoIdLockObject") - private static Spanned originalDislikeSpan; + @GuardedBy("itself") + private static final Map fetchCache = new HashMap<>(); /** - * Replacement like/dislike span that includes formatted dislikes. - * Used to prevent recreating the same span multiple times. + * Used to send votes, one by one, in the same order the user created them. */ - @Nullable - @GuardedBy("videoIdLockObject") - private static SpannableString replacementLikeDislikeSpan; + private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor(); /** * For formatting dislikes as number. @@ -162,298 +111,61 @@ boolean futureInProgressOrFinishedSuccessfully() { @GuardedBy("ReturnYouTubeDislike.class") private static NumberFormat dislikePercentageFormatter; - public enum Vote { - LIKE(1), - DISLIKE(-1), - LIKE_REMOVE(0); - - public final int value; - - Vote(int value) { - this.value = value; - } - } + // Used for segmented dislike spans in Litho regular player. + private static final Rect leftSeparatorBounds; + private static final Rect middleSeparatorBounds; - private ReturnYouTubeDislike() { - } // only static methods + static { + DisplayMetrics dp = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getDisplayMetrics(); - public static void onEnabledChange(boolean enabled) { - if (!enabled) { - // Must clear old values, to protect against using stale data - // if the user re-enables RYD while watching a video. - setCurrentVideoId(null); - } + leftSeparatorBounds = new Rect(0, 0, + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp), + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 18, dp)); + final int middleSeparatorSize = + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp); + middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize); } - 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; - } - } + private final String videoId; /** - * Should be called after a user dislikes, or if the user changes settings for dislikes appearance. + * Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes. + * Absolutely cannot be holding any lock during calls to {@link Future#get()}. */ - public static void clearCache() { - synchronized (videoIdLockObject) { - if (replacementLikeDislikeSpan != null) { - LogHelper.printDebug(() -> "Clearing replacement spans"); - } - replacementLikeDislikeSpan = null; - } - } - - @Nullable - private static String getCurrentVideoId() { - synchronized (videoIdLockObject) { - return currentVideoId; - } - } - - @Nullable - private static Future getVoteFetchFuture() { - synchronized (videoIdLockObject) { - return voteFetchFuture; - } - } - - public static void newVideoLoaded(@NonNull String videoId) { - Objects.requireNonNull(videoId); - - synchronized (videoIdLockObject) { - if (videoId.equals(currentVideoId)) { - return; // already loaded - } - if (!ReVancedUtils.isNetworkConnected()) { // must do network check after verifying it's a new video id - LogHelper.printDebug(() -> "Network not connected, ignoring video: " + videoId); - 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 = currentPlayerType.isNoneOrHidden(); - - 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)); - } - } + private final Future future; /** - * @return the replacement span containing dislikes, or the original span if RYD is not available. + * Time this instance and the future was created. */ - @NonNull - public static Spanned getDislikesSpanForRegularVideo(@NonNull Spanned original, boolean isSegmentedButton) { - if (dislikeDataIsShort) { - // user: - // 1, opened a video - // 2. opened a short (without closing the regular video) - // 3. closed the short - // 4. regular video is now present, but the videoId and RYD data is still for the short - LogHelper.printDebug(() -> "Ignoring dislike span, as data loaded is for prior short"); - return original; - } - return waitForFetchAndUpdateReplacementSpan(original, isSegmentedButton); - } + private final long timeFetched; /** - * Called when a Shorts dislike Spannable is created. + * If this instance was previously used for a Short. */ - @NonNull - public static Spanned getDislikeSpanForShort(@NonNull Spanned original) { - dislikeDataIsShort = true; // it's now certain the video and data are a short - return waitForFetchAndUpdateReplacementSpan(original, false); - } - - // Alternatively, this could check if the span contains one of the custom created spans, but this is simple and quick. - private static boolean isPreviouslyCreatedSegmentedSpan(@NonNull Spanned span) { - return span.toString().indexOf(MIDDLE_SEPARATOR_CHARACTER) != -1; - } - - @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 (originalDislikeSpan != null && replacementLikeDislikeSpan != null) { - if (spansHaveEqualTextAndColor(oldSpannable, replacementLikeDislikeSpan)) { - LogHelper.printDebug(() -> "Ignoring previously created dislikes span"); - return oldSpannable; - } - if (spansHaveEqualTextAndColor(oldSpannable, originalDislikeSpan)) { - LogHelper.printDebug(() -> "Replacing span with previously created dislike span"); - return replacementLikeDislikeSpan; - } - } - if (isSegmentedButton && isPreviouslyCreatedSegmentedSpan(oldSpannable)) { - // need to recreate using original, as oldSpannable has prior outdated dislike values - if (originalDislikeSpan == null) { - LogHelper.printDebug(() -> "Cannot add dislikes - original span is null"); // should never happen - return oldSpannable; - } - oldSpannable = originalDislikeSpan; - } - - // No replacement span exist, create it now. - - if (userVote != null) { - votingData.updateUsingVote(userVote); - } - originalDislikeSpan = oldSpannable; - replacementLikeDislikeSpan = createDislikeSpan(oldSpannable, isSegmentedButton, votingData); - LogHelper.printDebug(() -> "Replaced: '" + originalDislikeSpan + "' with: '" + replacementLikeDislikeSpan + "'"); - - return replacementLikeDislikeSpan; - } - } catch (TimeoutException e) { - LogHelper.printDebug(() -> "UI timed out while waiting for fetch votes to complete"); // show no toast - } catch (Exception e) { - LogHelper.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", e); // should never happen - } - return oldSpannable; - } + @GuardedBy("this") + private boolean isShort; /** - * @return if the RYD fetch call has completed. + * Optional current vote status of the UI. Used to apply a user vote that was done on a previous video viewing. */ - 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 || - (SettingsEnum.RYD_SHORTS.getBoolean() && dislikeDataIsShort != PlayerType.getCurrent().isNoneOrHidden())) { - // 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. - // Cannot send a vote, because the loaded videoId is for the wrong video. - ReVancedUtils.showToastLong(str("revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted")); - return; - } - - voteSerialExecutor.execute(() -> { - try { // must wrap in try/catch to properly log exceptions - String userId = getUserId(); - if (userId != null) { - ReturnYouTubeDislikeApi.sendVote(videoIdToVoteFor, userId, vote); - } - } catch (Exception ex) { - LogHelper.printException(() -> "Failed to send vote", ex); - } - }); - - 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); - } - } + @Nullable + @GuardedBy("this") + private Vote userVote; /** - * Must call off main thread, as this will make a network call if user is not yet registered. - * - * @return ReturnYouTubeDislike user ID. If user registration has never happened - * and the network call fails, this returns NULL. + * Original dislike span, before modifications. */ @Nullable - private static String getUserId() { - ReVancedUtils.verifyOffMainThread(); - - String userId = SettingsEnum.RYD_USER_ID.getString(); - if (!userId.isEmpty()) { - return userId; - } + @GuardedBy("this") + private Spanned originalDislikeSpan; - userId = ReturnYouTubeDislikeApi.registerAsNewUser(); - if (userId != null) { - SettingsEnum.RYD_USER_ID.saveValue(userId); - } - return userId; - } + /** + * Replacement like/dislike span that includes formatted dislikes. + * Used to prevent recreating the same span multiple times. + */ + @Nullable + @GuardedBy("this") + private SpannableString replacementLikeDislikeSpan; /** * @param isSegmentedButton If UI is using the segmented single UI component for both like and dislike. @@ -493,13 +205,9 @@ private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, final int separatorColor = ThemeHelper.isDarkTheme() ? 0x29AAAAAA // transparent dark gray : 0xFFD9D9D9; // light gray - DisplayMetrics dp = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getDisplayMetrics(); if (!compactLayout) { // left separator - final Rect leftSeparatorBounds = new Rect(0, 0, - (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp), - (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 18, dp)); String leftSeparatorString = ReVancedUtils.isRightToLeftTextLayout() ? "\u200F " // u200F = right to left character : "\u200E "; // u200E = left to right character @@ -520,8 +228,6 @@ private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, ? " " + MIDDLE_SEPARATOR_CHARACTER + " " : " \u2009" + MIDDLE_SEPARATOR_CHARACTER + "\u2009 "; // u2009 = 'narrow space' character final int shapeInsertionIndex = middleSeparatorString.length() / 2; - final int middleSeparatorSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp); - final Rect middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize); Spannable middleSeparatorSpan = new SpannableString(middleSeparatorString); ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape()); shapeDrawable.getPaint().setColor(separatorColor); @@ -536,6 +242,11 @@ private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, return new SpannableString(builder); } + // Alternatively, this could check if the span contains one of the custom created spans, but this is simple and quick. + private static boolean isPreviouslyCreatedSegmentedSpan(@NonNull Spanned span) { + return span.toString().indexOf(MIDDLE_SEPARATOR_CHARACTER) != -1; + } + /** * Correctly handles any unicode numbers (such as Arabic numbers). * @@ -603,7 +314,7 @@ private static String formatDislikeCount(long dislikeCount) { } } - // will never be reached, as the oldest supported YouTube app requires Android N or greater + // Will never be reached, as the oldest supported YouTube app requires Android N or greater. return String.valueOf(dislikeCount); } @@ -622,6 +333,238 @@ private static String formatDislikePercentage(float dislikePercentage) { return dislikePercentageFormatter.format(dislikePercentage); } } + + @NonNull + public static ReturnYouTubeDislike getFetchForVideoId(@Nullable String videoId) { + Objects.requireNonNull(videoId); + synchronized (fetchCache) { + // Remove any expired entries. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + final long now = System.currentTimeMillis(); + fetchCache.values().removeIf(value -> { + final boolean expired = value.isExpired(now); + if (expired) + LogHelper.printDebug(() -> "Removing expired fetch: " + value.videoId); + return expired; + }); + } + + ReturnYouTubeDislike fetch = fetchCache.get(videoId); + if (fetch == null) { + fetch = new ReturnYouTubeDislike(videoId); + fetchCache.put(videoId, fetch); + } + return fetch; + } + } + + /** + * Should be called if the user changes settings for dislikes appearance. + */ + public static void clearAllUICaches() { + synchronized (fetchCache) { + for (ReturnYouTubeDislike fetch : fetchCache.values()) { + fetch.clearUICache(); + } + } + } + + private ReturnYouTubeDislike(@NonNull String videoId) { + this.videoId = Objects.requireNonNull(videoId); + this.timeFetched = System.currentTimeMillis(); + this.future = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId)); + } + + private boolean isExpired(long now) { + final long timeSinceCreation = now - timeFetched; + if (timeSinceCreation < CACHE_TIMEOUT_FAILURE_MILLISECONDS) { + return false; // Not expired, even if the API call failed. + } + if (timeSinceCreation > CACHE_TIMEOUT_SUCCESS_MILLISECONDS) { + return true; // Always expired. + } + // Only expired if the fetch failed (API null response). + return (!fetchCompleted() || getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH) == null); + } + + @Nullable + public RYDVoteData getFetchData(long maxTimeToWait) { + try { + return future.get(maxTimeToWait, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + LogHelper.printDebug(() -> "Waited but future was not complete after: " + maxTimeToWait + "ms"); + } catch (ExecutionException | InterruptedException ex) { + LogHelper.printException(() -> "Future failure ", ex); // will never happen + } + return null; + } + + /** + * @return if the RYD fetch call has completed. + */ + public boolean fetchCompleted() { + return future.isDone(); + } + + private synchronized void clearUICache() { + if (replacementLikeDislikeSpan != null) { + LogHelper.printDebug(() -> "Clearing replacement span for: " + videoId); + } + replacementLikeDislikeSpan = null; + } + + @NonNull + public String getVideoId() { + return videoId; + } + + /** + * Pre-emptively set this as a Short. + */ + public synchronized void setVideoIdIsShort(boolean isShort) { + this.isShort = isShort; + } + + /** + * @return the replacement span containing dislikes, or the original span if RYD is not available. + */ + @NonNull + public synchronized Spanned getDislikesSpanForRegularVideo(@NonNull Spanned original, boolean isSegmentedButton) { + return waitForFetchAndUpdateReplacementSpan(original, isSegmentedButton, false); + } + + /** + * Called when a Shorts dislike Spannable is created. + */ + @NonNull + public synchronized Spanned getDislikeSpanForShort(@NonNull Spanned original) { + return waitForFetchAndUpdateReplacementSpan(original, false, true); + } + + @NonNull + private Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned original, + boolean isSegmentedButton, + boolean spanIsForShort) { + try { + RYDVoteData votingData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH); + if (votingData == null) { + LogHelper.printDebug(() -> "Cannot add dislike to UI (RYD data not available)"); + return original; + } + + synchronized (this) { + if (spanIsForShort) { + // Cannot set this to false if span is not for a Short. + // When spoofing to an old version and a Short is opened while a regular video + // is on screen, this instance can be loaded for the minimized regular video. + // But this Shorts data won't be displayed for that call + // and when it is un-minimized it will reload again and the load will be ignored. + isShort = true; + } else if (isShort) { + // user: + // 1, opened a video + // 2. opened a short (without closing the regular video) + // 3. closed the short + // 4. regular video is now present, but the videoId and RYD data is still for the short + LogHelper.printDebug(() -> "Ignoring regular video dislike span," + + " as data loaded was previously used for a Short: " + videoId); + return original; + } + + if (originalDislikeSpan != null && replacementLikeDislikeSpan != null) { + if (spansHaveEqualTextAndColor(original, replacementLikeDislikeSpan)) { + LogHelper.printDebug(() -> "Ignoring previously created dislikes span of data: " + videoId); + return original; + } + if (spansHaveEqualTextAndColor(original, originalDislikeSpan)) { + LogHelper.printDebug(() -> "Replacing span with previously created dislike span of data: " + videoId); + return replacementLikeDislikeSpan; + } + } + if (isSegmentedButton && isPreviouslyCreatedSegmentedSpan(original)) { + // need to recreate using original, as original has prior outdated dislike values + if (originalDislikeSpan == null) { + // Should never happen. + LogHelper.printDebug(() -> "Cannot add dislikes - original span is null. videoId: " + videoId); + return original; + } + original = originalDislikeSpan; + } + + // No replacement span exist, create it now. + + if (userVote != null) { + votingData.updateUsingVote(userVote); + } + originalDislikeSpan = original; + replacementLikeDislikeSpan = createDislikeSpan(original, isSegmentedButton, votingData); + LogHelper.printDebug(() -> "Replaced: '" + originalDislikeSpan + "' with: '" + + replacementLikeDislikeSpan + "'" + " using video: " + videoId); + + return replacementLikeDislikeSpan; + } + } catch (Exception e) { + LogHelper.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", e); // should never happen + } + return original; + } + + public void sendVote(@NonNull Vote vote) { + ReVancedUtils.verifyOnMainThread(); + Objects.requireNonNull(vote); + try { + if (isShort != PlayerType.getCurrent().isNoneOrHidden()) { + // Shorts was loaded with regular video present, then Shorts was closed. + // and then user voted on the now visible original video. + // Cannot send a vote, because this instance is for the wrong video. + ReVancedUtils.showToastLong(str("revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted")); + return; + } + + setUserVote(vote); + + voteSerialExecutor.execute(() -> { + try { // Must wrap in try/catch to properly log exceptions. + ReturnYouTubeDislikeApi.sendVote(videoId, vote); + } catch (Exception ex) { + LogHelper.printException(() -> "Failed to send vote", ex); + } + }); + } catch (Exception ex) { + LogHelper.printException(() -> "Error trying to send vote", ex); + } + } + + /** + * Sets the current user vote value, and does not send the vote to the RYD API. + * + * Only used to set value if thumbs up/down is already selected on video load. + */ + public void setUserVote(@NonNull Vote vote) { + Objects.requireNonNull(vote); + try { + LogHelper.printDebug(() -> "setUserVote: " + vote); + + synchronized (this) { + userVote = vote; + clearUICache(); + } + + if (future.isDone()) { + // Update the fetched vote data. + RYDVoteData voteData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH); + 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 fetch completes. + + } catch (Exception ex) { + LogHelper.printException(() -> "setUserVote failure", ex); + } + } } class VerticallyCenteredImageSpan extends ImageSpan { 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 149f7ffd74..faf6161950 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 @@ -7,8 +7,6 @@ import org.json.JSONException; import org.json.JSONObject; -import app.revanced.integrations.utils.LogHelper; - /** * ReturnYouTubeDislike API estimated like/dislike/view counts. * @@ -81,17 +79,21 @@ public float getDislikePercentage() { } public void updateUsingVote(Vote vote) { - if (vote == Vote.LIKE) { - likeCount = fetchedLikeCount + 1; - dislikeCount = fetchedDislikeCount; - } else if (vote == Vote.DISLIKE) { - likeCount = fetchedLikeCount; - dislikeCount = fetchedDislikeCount + 1; - } else if (vote == Vote.LIKE_REMOVE) { - likeCount = fetchedLikeCount; - dislikeCount = fetchedDislikeCount; - } else { - throw new IllegalStateException(); + switch (vote) { + case LIKE: + likeCount = fetchedLikeCount + 1; + dislikeCount = fetchedDislikeCount; + break; + case DISLIKE: + likeCount = fetchedLikeCount; + dislikeCount = fetchedDislikeCount + 1; + break; + case LIKE_REMOVE: + likeCount = fetchedLikeCount; + dislikeCount = fetchedDislikeCount; + break; + default: + throw new IllegalStateException(); } updatePercentages(); } 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 0965f493d0..f10ed8210f 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 @@ -35,10 +35,10 @@ public class ReturnYouTubeDislikeApi { private static final int API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS = 2000; /** - * {@link #fetchVotes(String)} HTTP read timeout - * To locally debug and force timeouts, change this to a very small number (ie: 100) + * {@link #fetchVotes(String)} HTTP read timeout. + * To locally debug and force timeouts, change this to a very small number (ie: 100) */ - private static final int API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS = 4000; + private static final int API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS = 5000; /** * Default connection and response timeout for voting and registration. @@ -391,13 +391,37 @@ private static String confirmRegistration(String userId, String solution) { return null; } - public static boolean sendVote(String videoId, String userId, ReturnYouTubeDislike.Vote vote) { + /** + * Must call off main thread, as this will make a network call if user is not yet registered. + * + * @return ReturnYouTubeDislike user ID. If user registration has never happened + * and the network call fails, this returns NULL. + */ + @Nullable + private static String getUserId() { + ReVancedUtils.verifyOffMainThread(); + + String userId = SettingsEnum.RYD_USER_ID.getString(); + if (!userId.isEmpty()) { + return userId; + } + + userId = registerAsNewUser(); + if (userId != null) { + SettingsEnum.RYD_USER_ID.saveValue(userId); + } + return userId; + } + + public static boolean sendVote(String videoId, ReturnYouTubeDislike.Vote vote) { ReVancedUtils.verifyOffMainThread(); Objects.requireNonNull(videoId); - Objects.requireNonNull(userId); Objects.requireNonNull(vote); try { + String userId = getUserId(); + if (userId == null) return false; + if (checkIfRateLimitInEffect("sendVote")) { return false; } 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 d93ab548b2..cd37b78edf 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -65,6 +65,10 @@ public enum SettingsEnum { 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_TIMED_REACTIONS("revanced_hide_timed_reactions", BOOLEAN, TRUE), + HIDE_SEARCH_RESULT_SHELF_HEADER("revanced_hide_search_result_shelf_header", BOOLEAN, FALSE), + HIDE_NOTIFY_ME_BUTTON("revanced_hide_notify_me_button", BOOLEAN, TRUE), + HIDE_JOIN_MEMBERSHIP_BUTTON("revanced_hide_join_membership_button", 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), @@ -119,6 +123,7 @@ public enum SettingsEnum { HIDE_SUBSCRIPTIONS_BUTTON("revanced_hide_subscriptions_button", BOOLEAN, FALSE, true), HIDE_TIMESTAMP("revanced_hide_timestamp", BOOLEAN, FALSE), HIDE_VIDEO_WATERMARK("revanced_hide_video_watermark", BOOLEAN, TRUE), + HIDE_VIDEO_CHANNEL_WATERMARK("revanced_hide_channel_watermark", BOOLEAN, TRUE), 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"), @@ -126,13 +131,15 @@ public enum SettingsEnum { USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true), TABLET_LAYOUT("revanced_tablet_layout", BOOLEAN, FALSE, true, "revanced_tablet_layout_user_dialog_message"), WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true), + GRADIENT_LOADING_SCREEN("revanced_gradient_loading_screen", BOOLEAN, FALSE), SEEKBAR_CUSTOM_COLOR("revanced_seekbar_custom_color", BOOLEAN, TRUE, true), SEEKBAR_CUSTOM_COLOR_VALUE("revanced_seekbar_custom_color_value", STRING, "#FF0000", true, parents(SEEKBAR_CUSTOM_COLOR)), 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, TRUE), - HIDE_SHORTS_SUBSCRIBE_BUTTON("revanced_hide_shorts_subscribe_button", BOOLEAN, FALSE), + HIDE_SHORTS_SUBSCRIBE_BUTTON("revanced_hide_shorts_subscribe_button", BOOLEAN, TRUE), + HIDE_SHORTS_SUBSCRIBE_BUTTON_PAUSED("revanced_hide_shorts_subscribe_button_paused", BOOLEAN, FALSE), HIDE_SHORTS_THANKS_BUTTON("revanced_hide_shorts_thanks_button", BOOLEAN, TRUE), HIDE_SHORTS_COMMENTS_BUTTON("revanced_hide_shorts_comments_button", BOOLEAN, FALSE), HIDE_SHORTS_REMIX_BUTTON("revanced_hide_shorts_remix_button", BOOLEAN, TRUE), @@ -165,6 +172,7 @@ public enum SettingsEnum { EXTERNAL_BROWSER("revanced_external_browser", BOOLEAN, TRUE, true), AUTO_REPEAT("revanced_auto_repeat", BOOLEAN, FALSE), SEEKBAR_TAPPING("revanced_seekbar_tapping", BOOLEAN, TRUE), + DISABLE_FINE_SCRUBBING_GESTURE("revanced_disable_fine_scrubbing_gesture", BOOLEAN, TRUE), SPOOF_SIGNATURE("revanced_spoof_signature_verification_enabled", BOOLEAN, TRUE, true, "revanced_spoof_signature_verification_enabled_user_dialog_message"), SPOOF_SIGNATURE_IN_FEED("revanced_spoof_signature_in_feed_enabled", BOOLEAN, FALSE, false, @@ -368,6 +376,8 @@ private static void loadAllSettings() { // region Migration + migrateOldSettingToNew(HIDE_VIDEO_WATERMARK, HIDE_VIDEO_CHANNEL_WATERMARK); + // 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. 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 dadc85beb7..45fffc2cef 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java @@ -12,6 +12,7 @@ import android.preference.PreferenceScreen; import android.preference.SwitchPreference; +import app.revanced.integrations.patches.ReturnYouTubeDislikePatch; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; import app.revanced.integrations.settings.SettingsEnum; @@ -19,6 +20,10 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { + private static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER = + SettingsEnum.SPOOF_APP_VERSION.getBoolean() + && SettingsEnum.SPOOF_APP_VERSION_TARGET.getString().compareTo("18.33.40") <= 0; + /** * If dislikes are shown on Shorts. */ @@ -63,7 +68,7 @@ public void onCreate(Bundle savedInstanceState) { enabledPreference.setOnPreferenceChangeListener((pref, newValue) -> { final boolean rydIsEnabled = (Boolean) newValue; SettingsEnum.RYD_ENABLED.saveValue(rydIsEnabled); - ReturnYouTubeDislike.onEnabledChange(rydIsEnabled); + ReturnYouTubeDislikePatch.onRYDStatusChange(rydIsEnabled); updateUIState(); return true; @@ -73,7 +78,11 @@ public void onCreate(Bundle savedInstanceState) { 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")); + String shortsSummary = str("revanced_ryd_shorts_summary_on", + IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER + ? "" + : "\n\n" + str("revanced_ryd_shorts_summary_disclaimer")); + shortsPreference.setSummaryOn(shortsSummary); shortsPreference.setSummaryOff(str("revanced_ryd_shorts_summary_off")); shortsPreference.setOnPreferenceChangeListener((pref, newValue) -> { SettingsEnum.RYD_SHORTS.saveValue(newValue); @@ -89,7 +98,7 @@ public void onCreate(Bundle savedInstanceState) { percentagePreference.setSummaryOff(str("revanced_ryd_dislike_percentage_summary_off")); percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> { SettingsEnum.RYD_DISLIKE_PERCENTAGE.saveValue(newValue); - ReturnYouTubeDislike.clearCache(); + ReturnYouTubeDislike.clearAllUICaches(); updateUIState(); return true; }); @@ -102,7 +111,7 @@ public void onCreate(Bundle savedInstanceState) { compactLayoutPreference.setSummaryOff(str("revanced_ryd_compact_layout_summary_off")); compactLayoutPreference.setOnPreferenceChangeListener((pref, newValue) -> { SettingsEnum.RYD_COMPACT_LAYOUT.saveValue(newValue); - ReturnYouTubeDislike.clearCache(); + ReturnYouTubeDislike.clearAllUICaches(); updateUIState(); return true; }); 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 e1de088de3..456bcdc8a9 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java @@ -72,7 +72,7 @@ private void updateUI() { } else if (!SettingsEnum.SB_CREATE_NEW_SEGMENT.getBoolean()) { SponsorBlockViewController.hideNewSegmentLayout(); } - // voting and add new segment buttons automatically shows/hides themselves + // Voting and add new segment buttons automatically shows/hide themselves. sbEnabled.setChecked(enabled); @@ -109,6 +109,12 @@ private void updateUI() { privateUserId.setText(SettingsEnum.SB_PRIVATE_USER_ID.getString()); privateUserId.setEnabled(enabled); + // If the user has a private user id, then include a subtext that mentions not to share it. + String exportSummarySubText = SponsorBlockSettings.userHasSBPrivateId() + ? str("sb_settings_ie_sum_warning") + : ""; + importExport.setSummary(str("sb_settings_ie_sum", exportSummarySubText)); + apiUrl.setEnabled(enabled); importExport.setEnabled(enabled); segmentCategory.setEnabled(enabled); @@ -329,6 +335,7 @@ private void addGeneralCategory(final Context context, PreferenceScreen screen) return false; } SettingsEnum.SB_PRIVATE_USER_ID.saveValue(newUUID); + updateUI(); fetchAndDisplayStats(); return true; }); @@ -375,7 +382,7 @@ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { } }; importExport.setTitle(str("sb_settings_ie")); - importExport.setSummary(str("sb_settings_ie_sum")); + // Summary is set in updateUI() importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); 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 a8dec9ccec..e2777c867c 100644 --- a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt +++ b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt @@ -17,6 +17,8 @@ enum class PlayerType { */ HIDDEN, /** + * A regular video is minimized. + * * When spoofing to 16.x YouTube and watching a short with a regular video in the background, * the type can be this (and not [HIDDEN]). */ @@ -26,7 +28,9 @@ enum class PlayerType { 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. + * Player is either sliding to [HIDDEN] state because a Short was opened while a regular video is on screen. + * OR + * The user has swiped a minimized player away to be closed (and no Short is being opened). */ WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED, WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED, @@ -84,20 +88,38 @@ enum class PlayerType { return this == NONE || this == HIDDEN } + /** + * Check if the current player type is + * [NONE], [HIDDEN], [WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED]. + * + * Useful to check if a Short is being played or opened. + * + * Usually covers all use cases with no false positives, except if called from some hooks + * when spoofing to an old version this will return false even + * though a Short is being opened or is on screen (see [isNoneHiddenOrMinimized]). + * + * @return If nothing, a Short, or a regular video is sliding off screen to a dismissed or hidden state. + */ + fun isNoneHiddenOrSlidingMinimized(): Boolean { + return isNoneOrHidden() || this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED + } + /** * Check if the current player type is * [NONE], [HIDDEN], [WATCH_WHILE_MINIMIZED], [WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED]. * * Useful to check if a Short is being played, - * although will return false positive if a regular video is opened and minimized (and no short is playing). + * although will return false positive if a regular video is + * opened and minimized (and a Short is not playing or being opened). * - * @return If nothing, a Short, - * or a regular video is minimized video or sliding off screen to a dismissed or hidden state. + * Typically used to detect if a Short is playing when the player cannot be in a minimized state, + * such as the user interacting with a button or element of the player. + * + * @return If nothing, a Short, a regular video is sliding off screen to a dismissed or hidden state, + * a regular video is minimized (and a new video is not being opened). */ fun isNoneHiddenOrMinimized(): Boolean { - return this == NONE || this == HIDDEN - || this == WATCH_WHILE_MINIMIZED - || this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED + return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED } } \ No newline at end of file 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 564de2c384..286a03bf23 100644 --- a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java +++ b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java @@ -10,25 +10,15 @@ 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 android.widget.*; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import app.revanced.integrations.settings.SettingsEnum; import java.text.Bidi; import java.util.Locale; import java.util.Objects; -import java.util.concurrent.Callable; -import java.util.concurrent.Future; -import java.util.concurrent.SynchronousQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import app.revanced.integrations.settings.SettingsEnum; +import java.util.concurrent.*; public class ReVancedUtils { @@ -93,9 +83,17 @@ public static Future submitOnBackgroundThread(@NonNull Callable call) } public static boolean containsAny(@NonNull String value, @NonNull String... targets) { - for (String string : targets) - if (!string.isEmpty() && value.contains(string)) return true; - return false; + return indexOfFirstFound(value, targets) >= 0; + } + + public static int indexOfFirstFound(@NonNull String value, @NonNull String... targets) { + for (String string : targets) { + if (!string.isEmpty()) { + final int indexOf = value.indexOf(string); + if (indexOf >= 0) return indexOf; + } + } + return -1; } /** diff --git a/app/src/main/java/app/revanced/integrations/utils/TrieSearch.java b/app/src/main/java/app/revanced/integrations/utils/TrieSearch.java index 6458f07cb5..8b9fecb8f4 100644 --- a/app/src/main/java/app/revanced/integrations/utils/TrieSearch.java +++ b/app/src/main/java/app/revanced/integrations/utils/TrieSearch.java @@ -23,11 +23,12 @@ public interface TriePatternMatchedCallback { * * @param textSearched Text that was searched. * @param matchedStartIndex Start index of the search text, where the pattern was matched. + * @param matchedLength Length of the match. * @param callbackParameter Optional parameter passed into {@link TrieSearch#matches(Object, Object)}. * @return True, if the search should stop here. * If false, searching will continue to look for other matches. */ - boolean patternMatched(T textSearched, int matchedStartIndex, Object callbackParameter); + boolean patternMatched(T textSearched, int matchedStartIndex, int matchedLength, Object callbackParameter); } /** @@ -64,8 +65,8 @@ boolean matches(TrieNode enclosingNode, // Used only for the get character me return false; } } - return callback == null - || callback.patternMatched(searchText, searchTextIndex - patternStartIndex, callbackParameter); + return callback == null || callback.patternMatched(searchText, + searchTextIndex - patternStartIndex, patternLength, callbackParameter); } } @@ -161,7 +162,7 @@ private boolean matches(T searchText, int searchTextLength, int searchTextIndex, if (callback == null) { return true; // No callback and all matches are valid. } - if (callback.patternMatched(searchText, matchStartIndex, callbackParameter)) { + if (callback.patternMatched(searchText, matchStartIndex, currentMatchLength, callbackParameter)) { return true; // Callback confirmed the match. } } diff --git a/app/src/main/java/app/revanced/tumblr/patches/TimelineFilterPatch.java b/app/src/main/java/app/revanced/tumblr/patches/TimelineFilterPatch.java new file mode 100644 index 0000000000..1a4d50eff3 --- /dev/null +++ b/app/src/main/java/app/revanced/tumblr/patches/TimelineFilterPatch.java @@ -0,0 +1,32 @@ +package app.revanced.tumblr.patches; + +import com.tumblr.rumblr.model.TimelineObject; +import com.tumblr.rumblr.model.Timelineable; + +import java.util.HashSet; +import java.util.List; + +public final class TimelineFilterPatch { + private static final HashSet blockedObjectTypes = new HashSet<>(); + + static { + // This dummy gets removed by the TimelineFilterPatch and in its place, + // equivalent instructions with a different constant string + // will be inserted for each Timeline object type filter. + // Modifying this line may break the patch. + blockedObjectTypes.add("BLOCKED_OBJECT_DUMMY"); + } + + // Calls to this method are injected where the list of Timeline objects is first received. + // We modify the list filter out elements that we want to hide. + public static void filterTimeline(final List> timelineObjects) { + final var iterator = timelineObjects.iterator(); + while (iterator.hasNext()) { + var timelineElement = iterator.next(); + if (timelineElement == null) continue; + + String elementType = timelineElement.getData().getTimelineObjectType().toString(); + if (blockedObjectTypes.contains(elementType)) iterator.remove(); + } + } +} diff --git a/dummy/src/main/java/com/tumblr/rumblr/model/TimelineObject.java b/dummy/src/main/java/com/tumblr/rumblr/model/TimelineObject.java new file mode 100644 index 0000000000..8bb2c885d4 --- /dev/null +++ b/dummy/src/main/java/com/tumblr/rumblr/model/TimelineObject.java @@ -0,0 +1,8 @@ +package com.tumblr.rumblr.model; + +public class TimelineObject { + public final T getData() { + throw new UnsupportedOperationException("Stub"); + } + +} diff --git a/dummy/src/main/java/com/tumblr/rumblr/model/TimelineObjectType.java b/dummy/src/main/java/com/tumblr/rumblr/model/TimelineObjectType.java new file mode 100644 index 0000000000..f9b7d7abca --- /dev/null +++ b/dummy/src/main/java/com/tumblr/rumblr/model/TimelineObjectType.java @@ -0,0 +1,4 @@ +package com.tumblr.rumblr.model; + +public enum TimelineObjectType { +} diff --git a/dummy/src/main/java/com/tumblr/rumblr/model/Timelineable.java b/dummy/src/main/java/com/tumblr/rumblr/model/Timelineable.java new file mode 100644 index 0000000000..bf84887def --- /dev/null +++ b/dummy/src/main/java/com/tumblr/rumblr/model/Timelineable.java @@ -0,0 +1,5 @@ +package com.tumblr.rumblr.model; + +public interface Timelineable { + TimelineObjectType getTimelineObjectType(); +} diff --git a/gradle.properties b/gradle.properties index 507cb29c63..4b4fb3abb7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 0.118.0-dev.16 +version = 0.120.0-dev.2