diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index fbee1c7937..438c1ef850 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,4 +1,4 @@ -name: PR to main +name: Open a PR to main on: push: @@ -7,7 +7,7 @@ on: workflow_dispatch: env: - MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main` + MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main` jobs: pull-request: @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Open pull request uses: repo-sync/pull-request@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4fd0e1417..d3ff3d7641 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Make sure the release step uses its own credentials: # https://github.com/cycjimmy/semantic-release-action#private-packages diff --git a/CHANGELOG.md b/CHANGELOG.md index a28a03e043..6657e8bdb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,202 @@ +# [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) + + +### Features + +* **YouTube:** Add `Bypass URL redirects` patch ([9109653](https://github.com/ReVanced/revanced-integrations/commit/91096532eedf396920d69932638f667cbf850cbe)) + +# [0.118.0-dev.15](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.14...v0.118.0-dev.15) (2023-09-27) + + +### Features + +* **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)) + +# [0.118.0-dev.14](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.13...v0.118.0-dev.14) (2023-09-27) + + +### Bug Fixes + +* **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)) + +# [0.118.0-dev.13](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.12...v0.118.0-dev.13) (2023-09-27) + + +### Bug Fixes + +* **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)) + +# [0.118.0-dev.12](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.11...v0.118.0-dev.12) (2023-09-26) + + +### Bug Fixes + +* **YouTube - Client spoof:** Show seekbar thumbnail for age restricted and paid videos ([01019b0](https://github.com/ReVanced/revanced-integrations/commit/01019b09c1c106ed814b994dd8af558a18873c1d)) + +# [0.118.0-dev.11](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.10...v0.118.0-dev.11) (2023-09-26) + + +### Bug Fixes + +* **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)) + +# [0.118.0-dev.10](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.9...v0.118.0-dev.10) (2023-09-26) + + +### Bug Fixes + +* **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)) + +# [0.118.0-dev.9](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.8...v0.118.0-dev.9) (2023-09-25) + + +### Bug Fixes + +* **YouTube - Client spoof:** Removed unused code ([#480](https://github.com/ReVanced/revanced-integrations/issues/480)) ([e6903bf](https://github.com/ReVanced/revanced-integrations/commit/e6903bff95b485d21773537bbcc162411b616618)) + +# [0.118.0-dev.8](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.7...v0.118.0-dev.8) (2023-09-25) + + +### Bug Fixes + +* **YouTube - Client spoof:** Display seekbar thumbnails in high quality ([f71c1a0](https://github.com/ReVanced/revanced-integrations/commit/f71c1a0c156b2320e06dd98b3e5b276560d438aa)) + +# [0.118.0-dev.7](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.6...v0.118.0-dev.7) (2023-09-25) + + +### Bug Fixes + +* Remove parameter from route ([4b0925e](https://github.com/ReVanced/revanced-integrations/commit/4b0925e33762c02e95ef9b1aadcae1038af71a50)) + +# [0.118.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.5...v0.118.0-dev.6) (2023-09-25) + + +### 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.5](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.4...v0.118.0-dev.5) (2023-09-25) + + +### Bug Fixes + +* **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)) + +# [0.118.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.3...v0.118.0-dev.4) (2023-09-23) + + +### Bug Fixes + +* **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)) + +# [0.118.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.2...v0.118.0-dev.3) (2023-09-19) + + +### Bug Fixes + +* **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)) + +# [0.118.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v0.118.0-dev.1...v0.118.0-dev.2) (2023-09-14) + + +### Features + +* **Twitch - Block embedded ads:** Switch from `ttv.lol` to `luminous.dev` ([2c34180](https://github.com/ReVanced/revanced-integrations/commit/2c3418041cf19ae4c1c7b67eda8398578384b753)) + +# [0.118.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.117.2-dev.3...v0.118.0-dev.1) (2023-09-14) + + +### 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)) + +## [0.117.2-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v0.117.2-dev.2...v0.117.2-dev.3) (2023-09-09) + + +### Bug Fixes + +* **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)) + +## [0.117.2-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v0.117.2-dev.1...v0.117.2-dev.2) (2023-09-07) + + +### Bug Fixes + +* **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)) + +## [0.117.2-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.117.1...v0.117.2-dev.1) (2023-09-07) + + +### Bug Fixes + +* **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)) + ## [0.117.1](https://github.com/ReVanced/revanced-integrations/compare/v0.117.0...v0.117.1) (2023-09-03) diff --git a/app/src/main/java/app/revanced/integrations/patches/BypassURLRedirectsPatch.java b/app/src/main/java/app/revanced/integrations/patches/BypassURLRedirectsPatch.java new file mode 100644 index 0000000000..dd062e0372 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/BypassURLRedirectsPatch.java @@ -0,0 +1,29 @@ +package app.revanced.integrations.patches; + +import android.net.Uri; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; + +public class BypassURLRedirectsPatch { + private static final String YOUTUBE_REDIRECT_PATH = "/redirect"; + + /** + * Convert the YouTube redirect URI string to the redirect query URI. + * + * @param uri The YouTube redirect URI string. + * @return The redirect query URI. + */ + public static Uri parseRedirectUri(String uri) { + final var parsed = Uri.parse(uri); + + if (SettingsEnum.BYPASS_URL_REDIRECTS.getBoolean() && parsed.getPath().equals(YOUTUBE_REDIRECT_PATH)) { + var query = Uri.parse(Uri.decode(parsed.getQueryParameter("q"))); + + LogHelper.printDebug(() -> "Bypassing YouTube redirect URI: " + query); + + return query; + } + + return parsed; + } +} 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 a3d2fc3314..2c3b3d0fe1 100644 --- a/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java @@ -1,25 +1,13 @@ package app.revanced.integrations.patches; -import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote; - import android.graphics.Rect; import android.os.Build; -import android.text.Editable; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.Spanned; -import android.text.TextWatcher; +import android.text.*; import android.view.View; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - +import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; import app.revanced.integrations.settings.SettingsEnum; @@ -27,6 +15,13 @@ 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.concurrent.atomic.AtomicReference; + +import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote; + /** * Handles all interaction of UI patch components. * @@ -154,6 +149,8 @@ public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, } String conversionContextString = conversionContext.toString(); + LogHelper.printDebug(() -> "conversionContext: " + conversionContextString); + final boolean isSegmentedButton; if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) { isSegmentedButton = true; diff --git a/app/src/main/java/app/revanced/integrations/patches/SpoofSignatureVerificationPatch.java b/app/src/main/java/app/revanced/integrations/patches/SpoofSignatureVerificationPatch.java deleted file mode 100644 index 37c7a51dfa..0000000000 --- a/app/src/main/java/app/revanced/integrations/patches/SpoofSignatureVerificationPatch.java +++ /dev/null @@ -1,95 +0,0 @@ -package app.revanced.integrations.patches; - -import static app.revanced.integrations.utils.ReVancedUtils.containsAny; - -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; - -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.shared.PlayerType; -import app.revanced.integrations.utils.LogHelper; - -public class SpoofSignatureVerificationPatch { - /** - * Enable/disable all workarounds that are required due to signature spoofing. - */ - private static final boolean WORKAROUND = true; - - /** - * Protobuf parameters used for autoplay in scrim. - * Prepend this parameter to mute video playback (for autoplay in feed) - */ - private static final String PROTOBUF_PARAMETER_SCRIM = "SAFgAXgB"; - - /** - * Protobuf parameter also used by - * yt-dlp - *
- * Known issue: captions are positioned on upper area in the player. - */ - private static final String PROTOBUF_PLAYER_PARAMS = "CgIQBg=="; - - /** - * Target Protobuf parameters. - */ - private static final String[] PROTOBUF_PARAMETER_TARGETS = { - "YAHI", // Autoplay in feed - "SAFg" // Autoplay in scrim - }; - - /** - * Injection point. - * - * @param originalValue originalValue protobuf parameter - */ - public static String overrideProtobufParameter(String originalValue) { - try { - if (!SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.getBoolean()) { - return originalValue; - } - - LogHelper.printDebug(() -> "Original protobuf parameter value: " + originalValue); - - if (!WORKAROUND) return PROTOBUF_PLAYER_PARAMS; - - var isPlayingVideo = originalValue.contains(PROTOBUF_PLAYER_PARAMS); - if (isPlayingVideo) return originalValue; - - boolean isPlayingFeed = containsAny(originalValue, PROTOBUF_PARAMETER_TARGETS) && PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL; - if (isPlayingFeed) { - // Videos in feed won't autoplay with sound. - return PROTOBUF_PARAMETER_SCRIM + PROTOBUF_PLAYER_PARAMS; - } else { - // Spoof the parameter to prevent playback issues. - return PROTOBUF_PLAYER_PARAMS; - } - } catch (Exception ex) { - LogHelper.printException(() -> "overrideProtobufParameter failure", ex); - } - - return originalValue; - } - - /** - * Injection point. - */ - public static boolean getSeekbarThumbnailOverrideValue() { - return SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.getBoolean(); - } - - /** - * Injection point. - * - * @param view seekbar thumbnail view. Includes both shorts and regular videos. - */ - public static void seekbarImageViewCreated(ImageView view) { - if (SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.getBoolean()) { - view.setVisibility(View.GONE); - // Also hide the border around the thumbnail (otherwise a 1 pixel wide bordered frame is visible). - ViewGroup parentLayout = (ViewGroup) view.getParent(); - parentLayout.setPadding(0, 0, 0, 0); - } - } - -} 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/HideInfoCardsFilterPatch.java b/app/src/main/java/app/revanced/integrations/patches/components/HideInfoCardsFilterPatch.java new file mode 100644 index 0000000000..fc1f9a1e9f --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/components/HideInfoCardsFilterPatch.java @@ -0,0 +1,15 @@ +package app.revanced.integrations.patches.components; + +import app.revanced.integrations.settings.SettingsEnum; + +public final class HideInfoCardsFilterPatch extends Filter { + + public HideInfoCardsFilterPatch() { + identifierFilterGroupList.addAll( + new StringFilterGroup( + SettingsEnum.HIDE_INFO_CARDS, + "info_card_teaser_overlay.eml" + ) + ); + } +} 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 67811a8991..39be57c924 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 @@ -7,6 +7,7 @@ import androidx.annotation.RequiresApi; import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.StringTrieSearch; @RequiresApi(api = Build.VERSION_CODES.N) @@ -18,6 +19,7 @@ public final class LayoutComponentsFilter extends Filter { SettingsEnum.HIDE_MIX_PLAYLISTS, "&list=" ); + private final StringFilterGroup searchResultShelfHeader; @RequiresApi(api = Build.VERSION_CODES.N) public LayoutComponentsFilter() { @@ -92,8 +94,16 @@ public LayoutComponentsFilter() { "channel_guidelines_entry_banner" ); + // The player audio track button does the exact same function as the audio track flyout menu option. + // But if the copy url button is shown, these button clashes and the the audio button does not work. + // Previously this was a setting to show/hide the player button. + // But it was decided it's simpler to always hide this button because: + // - it doesn't work with copy video url feature + // - the button is rare + // - always hiding makes the ReVanced settings simpler and easier to understand + // - nobody is going to notice the redundant button is always hidden final var audioTrackButton = new StringFilterGroup( - SettingsEnum.HIDE_AUDIO_TRACK_BUTTON, + null, "multi_feed_icon_button" ); @@ -137,6 +147,27 @@ 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" + ); + + final var 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" @@ -147,27 +178,30 @@ public LayoutComponentsFilter() { communityPosts, paidContent, latestPosts, - chapters, 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 ); } @@ -177,6 +211,9 @@ public boolean isFiltered(@Nullable String identifier, String path, byte[] proto 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); } @@ -187,6 +224,11 @@ public boolean isFiltered(@Nullable String identifier, String path, byte[] proto * Called from a different place then the other filters. */ public static boolean filterMixPlaylists(final byte[] bytes) { - return mixPlaylists.check(bytes).isFiltered(); + final boolean isMixPlaylistFiltered = mixPlaylists.check(bytes).isFiltered(); + + if (isMixPlaylistFiltered) + LogHelper.printDebug(() -> "Filtered mix playlist"); + + return isMixPlaylistFiltered; } } 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 90aa678105..7cfe060b1d 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 @@ -88,7 +88,7 @@ public FilterGroupResult check(final String string) { final class CustomFilterGroup extends StringFilterGroup { public CustomFilterGroup(final SettingsEnum setting, final SettingsEnum filter) { - super(setting, filter.getString().split(",")); + super(setting, filter.getString().split("\\s+")); } } @@ -292,10 +292,10 @@ abstract class Filter { boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) { if (SettingsEnum.DEBUG.getBoolean()) { - if (pathFilterGroupList == matchedList) { - LogHelper.printDebug(() -> getClass().getSimpleName() + " Filtered path: " + path); - } else if (identifierFilterGroupList == matchedList) { + if (matchedList == identifierFilterGroupList) { LogHelper.printDebug(() -> getClass().getSimpleName() + " Filtered identifier: " + identifier); + } else { + LogHelper.printDebug(() -> getClass().getSimpleName() + " Filtered path: " + path); } } return true; 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 b6e1f3facb..6b03173b07 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 @@ -1,31 +1,29 @@ package app.revanced.integrations.patches.components; -import static app.revanced.integrations.utils.ReVancedUtils.hideViewBy1dpUnderCondition; -import static app.revanced.integrations.utils.ReVancedUtils.hideViewUnderCondition; - +import android.os.Build; import android.view.View; - import androidx.annotation.Nullable; - +import androidx.annotation.RequiresApi; +import app.revanced.integrations.settings.SettingsEnum; import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar; -import app.revanced.integrations.settings.SettingsEnum; +import static app.revanced.integrations.utils.ReVancedUtils.hideViewBy1dpUnderCondition; +import static app.revanced.integrations.utils.ReVancedUtils.hideViewUnderCondition; +@RequiresApi(api = Build.VERSION_CODES.N) public final class ShortsFilter extends Filter { - private static final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml"; 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 soundButton; private final StringFilterGroup infoPanel; - private final StringFilterGroup shortsShelfHeader; + private final StringFilterGroup shelfHeader; + + private final StringFilterGroup videoActionButton; + private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList(); public ShortsFilter() { - // Home / subscription feed components. - var thanksButton = new StringFilterGroup( - SettingsEnum.HIDE_SHORTS_THANKS_BUTTON, - "suggested_action" - ); var shorts = new StringFilterGroup( SettingsEnum.HIDE_SHORTS, "shorts_shelf", @@ -33,14 +31,22 @@ public ShortsFilter() { "shorts_grid", "shorts_video_cell", "shorts_pivot_item" + ); + // Feed Shorts shelf header. // Use a different filter group for this pattern, as it requires an additional check after matching. - shortsShelfHeader = new StringFilterGroup( + shelfHeader = new StringFilterGroup( SettingsEnum.HIDE_SHORTS, "shelf_header.eml" ); - identifierFilterGroupList.addAll(shorts, shortsShelfHeader, thanksButton); + // Home / subscription feed components. + var thanksButton = new StringFilterGroup( + SettingsEnum.HIDE_SHORTS_THANKS_BUTTON, + "suggested_action" + ); + + identifierFilterGroupList.addAll(shorts, shelfHeader, thanksButton); // Shorts player components. var joinButton = new StringFilterGroup( @@ -49,8 +55,10 @@ public ShortsFilter() { ); var subscribeButton = new StringFilterGroup( SettingsEnum.HIDE_SHORTS_SUBSCRIBE_BUTTON, - "subscribe_button" + "subscribe_button", + "shorts_paused_state" ); + channelBar = new StringFilterGroup( SettingsEnum.HIDE_SHORTS_CHANNEL_BAR, REEL_CHANNEL_BAR_PATH @@ -59,11 +67,37 @@ public ShortsFilter() { SettingsEnum.HIDE_SHORTS_SOUND_BUTTON, "reel_pivot_button" ); + infoPanel = new StringFilterGroup( SettingsEnum.HIDE_SHORTS_INFO_PANEL, "shorts_info_panel_overview" ); - pathFilterGroupList.addAll(joinButton, subscribeButton, channelBar, soundButton, infoPanel); + + videoActionButton = new StringFilterGroup( + null, + "ContainerType|shorts_video_action_button" + ); + + pathFilterGroupList.addAll( + joinButton, subscribeButton, channelBar, soundButton, infoPanel, videoActionButton + ); + + var shortsCommentButton = new ByteArrayAsStringFilterGroup( + SettingsEnum.HIDE_SHORTS_COMMENTS_BUTTON, + "reel_comment_button" + ); + + var shortsShareButton = new ByteArrayAsStringFilterGroup( + SettingsEnum.HIDE_SHORTS_SHARE_BUTTON, + "reel_share_button" + ); + + var shortsRemixButton = new ByteArrayAsStringFilterGroup( + SettingsEnum.HIDE_SHORTS_REMIX_BUTTON, + "reel_remix_button" + ); + + videoActionButtonGroupList.addAll(shortsCommentButton, shortsShareButton, shortsRemixButton); } @Override @@ -72,27 +106,34 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff if (matchedList == pathFilterGroupList) { // Always filter if matched. if (matchedGroup == soundButton || matchedGroup == infoPanel || matchedGroup == channelBar) - return super.isFiltered(path, identifier, protobufBufferArray, matchedList, matchedGroup, matchedIndex); + 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); + 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; - } else if (matchedGroup == shortsShelfHeader) { + } 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. if (matchedIndex != 0) return false; } // Super class handles logging. - return super.isFiltered(path, identifier, protobufBufferArray, matchedList, matchedGroup, matchedIndex); + return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex); } public static void hideShortsShelf(final View shortsShelfView) { hideViewBy1dpUnderCondition(SettingsEnum.HIDE_SHORTS, shortsShelfView); } - // Additional components that have to be hidden by setting their visibility + // region Hide the buttons in older versions of YouTube. New versions use Litho. public static void hideShortsCommentsButton(final View commentsButtonView) { hideViewUnderCondition(SettingsEnum.HIDE_SHORTS_COMMENTS_BUTTON, commentsButtonView); @@ -106,6 +147,8 @@ public static void hideShortsShareButton(final View shareButtonView) { hideViewUnderCondition(SettingsEnum.HIDE_SHORTS_SHARE_BUTTON, shareButtonView); } + // endregion + public static void hideNavigationBar() { if (!SettingsEnum.HIDE_SHORTS_NAVIGATION_BAR.getBoolean()) return; if (pivotBar == null) return; diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldVideoQualityMenuPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldVideoQualityMenuPatch.java index 70defb4c36..0d914ab3d7 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldVideoQualityMenuPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldVideoQualityMenuPatch.java @@ -3,71 +3,44 @@ import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.LinearLayout; import android.widget.ListView; -import androidx.annotation.NonNull; - import app.revanced.integrations.patches.components.VideoQualityMenuFilterPatch; import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.utils.LogHelper; -import com.facebook.litho.ComponentHost; -import kotlin.Deprecated; -// This patch contains the logic to show the old video quality menu. -// Two methods are required, because the quality menu is a RecyclerView in the new YouTube version -// and a ListView in the old one. +/** + * This patch contains the logic to show the old video quality menu. + * Two methods are required, because the quality menu is a RecyclerView in the new YouTube version + * and a ListView in the old one. + */ public final class OldVideoQualityMenuPatch { - public static void onFlyoutMenuCreate(final LinearLayout linearLayout) { + /** + * Injection point. + */ + public static void onFlyoutMenuCreate(RecyclerView recyclerView) { if (!SettingsEnum.SHOW_OLD_VIDEO_QUALITY_MENU.getBoolean()) return; - // The quality menu is a RecyclerView with 3 children. The third child is the "Advanced" quality menu. - addRecyclerListener(linearLayout, 3, 2, recyclerView -> { - // Check if the current view is the quality menu. - if (VideoQualityMenuFilterPatch.isVideoQualityMenuVisible) { - VideoQualityMenuFilterPatch.isVideoQualityMenuVisible = false; - linearLayout.setVisibility(View.GONE); + recyclerView.getViewTreeObserver().addOnDrawListener(() -> { + try { + // Check if the current view is the quality menu. + if (VideoQualityMenuFilterPatch.isVideoQualityMenuVisible) { + VideoQualityMenuFilterPatch.isVideoQualityMenuVisible = false; + ((ViewGroup) recyclerView.getParent().getParent().getParent()).setVisibility(View.GONE); - // Click the "Advanced" quality menu to show the "old" quality menu. - ((ComponentHost) recyclerView.getChildAt(0)).getChildAt(3).performClick(); - LogHelper.printDebug(() -> "Advanced quality menu in new type of quality menu clicked"); + // Click the "Advanced" quality menu to show the "old" quality menu. + ((ViewGroup) recyclerView.getChildAt(0)).getChildAt(3).performClick(); + } + } catch (Exception ex) { + LogHelper.printException(() -> "onFlyoutMenuCreate failure", ex); } }); } - public static void addRecyclerListener(@NonNull LinearLayout linearLayout, - int expectedLayoutChildCount, int recyclerViewIndex, - @NonNull RecyclerViewGlobalLayoutListener listener) { - if (linearLayout.getChildCount() != expectedLayoutChildCount) return; - - var layoutChild = linearLayout.getChildAt(recyclerViewIndex); - if (!(layoutChild instanceof RecyclerView)) return; - final var recyclerView = (RecyclerView) layoutChild; - - recyclerView.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - try { - listener.recyclerOnGlobalLayout(recyclerView); - } catch (Exception ex) { - LogHelper.printException(() -> "addRecyclerListener failure", ex); - } finally { - // Remove the listener because it will be added again. - recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } - } - } - ); - } - - public interface RecyclerViewGlobalLayoutListener { - void recyclerOnGlobalLayout(@NonNull RecyclerView recyclerView); - } - - @Deprecated(message = "This patch is deprecated because the quality menu is not a ListView anymore") + /** + * Injection point. Only used if spoofing to an old app version. + */ public static void showOldVideoQualityMenu(final ListView listView) { if (!SettingsEnum.SHOW_OLD_VIDEO_QUALITY_MENU.getBoolean()) return; diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomPlaybackSpeedPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomPlaybackSpeedPatch.java index a65a2530f6..5e3185787c 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomPlaybackSpeedPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomPlaybackSpeedPatch.java @@ -1,23 +1,23 @@ package app.revanced.integrations.patches.playback.speed; import android.preference.ListPreference; +import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; + import androidx.annotation.NonNull; + +import java.util.Arrays; + import app.revanced.integrations.patches.components.PlaybackSpeedMenuFilterPatch; import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; -import com.facebook.litho.ComponentHost; - -import java.util.Arrays; - -import static app.revanced.integrations.patches.playback.quality.OldVideoQualityMenuPatch.addRecyclerListener; public class CustomPlaybackSpeedPatch { /** * Maximum playback speed, exclusive value. Custom speeds must be less than this value. + * Limit is required otherwise double digit speeds show up out of order in the UI selector. */ public static final float MAXIMUM_PLAYBACK_SPEED = 10; @@ -26,16 +26,6 @@ public class CustomPlaybackSpeedPatch { */ public static float[] customPlaybackSpeeds; - /** - * Minimum value of {@link #customPlaybackSpeeds} - */ - public static float minPlaybackSpeed; - - /** - * Maxium value of {@link #customPlaybackSpeeds} - */ - public static float maxPlaybackSpeed; - /** * PreferenceList entries and values, of all available playback speeds. */ @@ -69,8 +59,6 @@ private static void loadCustomSpeeds() { loadCustomSpeeds(); return; } - minPlaybackSpeed = Math.min(minPlaybackSpeed, speed); - maxPlaybackSpeed = Math.max(maxPlaybackSpeed, speed); customPlaybackSpeeds[i] = speed; } } catch (Exception ex) { @@ -106,25 +94,37 @@ public static void initializeListPreference(ListPreference preference) { preference.setEntryValues(preferenceListEntryValues); } - /* - * To reduce copy and paste between two similar code paths. + /** + * Injection point. */ - public static void onFlyoutMenuCreate(final LinearLayout linearLayout) { - // The playback rate menu is a RecyclerView with 2 children. The third child is the "Advanced" quality menu. - addRecyclerListener(linearLayout, 2, 1, recyclerView -> { - if (PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible) { - PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible = false; - - if (recyclerView.getChildCount() == 1 && recyclerView.getChildAt(0) instanceof ComponentHost) { - linearLayout.setVisibility(View.GONE); - - // Close the new Playback speed menu and instead show the old one. + public static void onFlyoutMenuCreate(RecyclerView recyclerView) { + recyclerView.getViewTreeObserver().addOnDrawListener(() -> { + try { + // For some reason, the custom playback speed flyout panel is activated when the user opens the share panel. (A/B tests) + // Check the child count of playback speed flyout panel to prevent this issue. + // Child count of playback speed flyout panel is always 8. + if (PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible + && ((ViewGroup) recyclerView.getChildAt(0)).getChildCount() == 8) { + PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible = false; + ViewGroup parentView3rd = (ViewGroup) recyclerView.getParent().getParent().getParent(); + ViewGroup parentView4th = (ViewGroup) parentView3rd.getParent(); + + // Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView. + // This only shows in phone layout. + parentView4th.getChildAt(0).performClick(); + + // In tablet layout there is no Dismiss View, instead we just hide all two parent views. + parentView3rd.setVisibility(View.GONE); + parentView4th.setVisibility(View.GONE); + + // This works without issues for both tablet and phone layouts, + // So no code is needed to check whether the current device is a tablet or phone. + + // Close the new Playback speed menu and show the old one. showOldPlaybackSpeedMenu(); - - // DismissView [R.id.touch_outside] is the 1st ChildView of the 3rd ParentView. - ((ViewGroup) linearLayout.getParent().getParent().getParent()) - .getChildAt(0).performClick(); } + } catch (Exception ex) { + LogHelper.printException(() -> "onFlyoutMenuCreate failure", ex); } }); } 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 new file mode 100644 index 0000000000..16a284d049 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofSignaturePatch.java @@ -0,0 +1,153 @@ +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 java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +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 { + /** + * Parameter (also used by + * yt-dlp) + * to fix playback issues. + */ + private static final String INCOGNITO_PARAMETERS = "CgIQBg=="; + + /** + * Parameters causing playback issues. + */ + private static final String[] AUTOPLAY_PARAMETERS = { + "YAHI", // Autoplay in feed. + "SAFg" // Autoplay in scrim. + }; + + /** + * Parameter used for autoplay in scrim. + * Prepend this parameter to mute video playback (for autoplay in feed). + */ + private static final String SCRIM_PARAMETER = "SAFgAXgB"; + + /** + * Parameters used in YouTube Shorts. + */ + private static final String SHORTS_PLAYER_PARAMETERS = "8AEB"; + + /** + * Last video id loaded. Used to prevent reloading the same spec multiple times. + */ + private static volatile String lastPlayerResponseVideoId; + + private static volatile Future rendererFuture; + + @Nullable + private static StoryboardRenderer getRenderer() { + if (rendererFuture != null) { + try { + return rendererFuture.get(5000, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + LogHelper.printDebug(() -> "Could not get renderer (get timed out)"); + } catch (ExecutionException | InterruptedException ex) { + // Should never happen. + LogHelper.printException(() -> "Could not get renderer", ex); + } + } + return null; + } + + /** + * Injection point. + * + * Called off the main thread, and called multiple times for each video. + * + * @param parameters Original protobuf parameter value. + */ + public static String spoofParameter(String parameters) { + LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters); + + if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) return parameters; + + // Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops) + // For this reason, the player parameters of a clip are usually very long (150~300 characters). + // Clips are 60 seconds or less in length, so no spoofing. + var isClip = parameters.length() > 150; + if (isClip) return parameters; + + // Shorts do not need to be spoofed. + if (parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) return parameters; + + boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL && containsAny(parameters, AUTOPLAY_PARAMETERS); + if (isPlayingFeed) return SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean() ? + // Prepend the scrim parameter to mute videos in feed. + SCRIM_PARAMETER + INCOGNITO_PARAMETERS : + // In order to prevent videos that are auto-played in feed to be added to history, + // only spoof the parameter if the video is not playing in the feed. + // This will cause playback issues in the feed, but it's better than manipulating the history. + parameters; + + fetchStoryboardRenderer(); + + return INCOGNITO_PARAMETERS; + } + + private static void fetchStoryboardRenderer() { + String videoId = VideoInformation.getPlayerResponseVideoId(); + if (!videoId.equals(lastPlayerResponseVideoId)) { + rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId)); + lastPlayerResponseVideoId = videoId; + } + // 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(); + } + + /** + * Injection point. + */ + public static boolean getSeekbarThumbnailOverrideValue() { + return SettingsEnum.SPOOF_SIGNATURE.getBoolean(); + } + + /** + * Injection point. + * Called from background threads and from the main thread. + */ + @Nullable + public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) { + if (SettingsEnum.SPOOF_SIGNATURE.getBoolean()) { + StoryboardRenderer renderer = getRenderer(); + if (renderer != null) return renderer.getSpec(); + } + + return originalStoryboardRendererSpec; + } + + /** + * Injection point. + */ + public static int getRecommendedLevel(int originalLevel) { + if (SettingsEnum.SPOOF_SIGNATURE.getBoolean()) { + StoryboardRenderer renderer = getRenderer(); + if (renderer != null) { + Integer recommendedLevel = renderer.getRecommendedLevel(); + if (recommendedLevel != null) return recommendedLevel; + } + } + + 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 new file mode 100644 index 0000000000..d0e70988bf --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/StoryboardRenderer.java @@ -0,0 +1,39 @@ +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; + @Nullable + private final Integer recommendedLevel; + + public StoryboardRenderer(String spec, @Nullable Integer recommendedLevel) { + this.spec = spec; + this.recommendedLevel = recommendedLevel; + } + + @NonNull + public String getSpec() { + return spec; + } + + /** + * @return Recommended image quality level, or NULL if no recommendation exists. + */ + @Nullable + public Integer getRecommendedLevel() { + return recommendedLevel; + } + + @NotNull + @Override + public String toString() { + return "StoryboardRenderer{" + + "spec='" + spec + '\'' + + ", recommendedLevel=" + recommendedLevel + + '}'; + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/patches/spoof/requests/PlayerRoutes.java new file mode 100644 index 0000000000..db3971ba6a --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/requests/PlayerRoutes.java @@ -0,0 +1,89 @@ +package app.revanced.integrations.patches.spoof.requests; + +import app.revanced.integrations.requests.Requester; +import app.revanced.integrations.requests.Route; +import app.revanced.integrations.utils.LogHelper; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; + +final class PlayerRoutes { + private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/"; + static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route( + Route.Method.POST, + "player" + + "?fields=storyboards.playerStoryboardSpecRenderer," + + "storyboards.playerLiveStoryboardSpecRenderer," + + "playabilityStatus.status" + ).compile(); + + static final String ANDROID_INNER_TUBE_BODY; + static final String TV_EMBED_INNER_TUBE_BODY; + + static { + JSONObject innerTubeBody = new JSONObject(); + + try { + JSONObject context = new JSONObject(); + + JSONObject client = new JSONObject(); + client.put("clientName", "ANDROID"); + client.put("clientVersion", "18.37.36"); + client.put("androidSdkVersion", 34); + + context.put("client", client); + + innerTubeBody.put("context", context); + innerTubeBody.put("videoId", "%s"); + } catch (JSONException e) { + LogHelper.printException(() -> "Failed to create innerTubeBody", e); + } + + ANDROID_INNER_TUBE_BODY = innerTubeBody.toString(); + + JSONObject tvEmbedInnerTubeBody = new JSONObject(); + + try { + JSONObject context = new JSONObject(); + + JSONObject client = new JSONObject(); + client.put("clientName", "TVHTML5_SIMPLY_EMBEDDED_PLAYER"); + client.put("clientVersion", "2.0"); + client.put("platform", "TV"); + client.put("clientScreen", "EMBED"); + + JSONObject thirdParty = new JSONObject(); + thirdParty.put("embedUrl", "https://www.youtube.com/watch?v=%s"); + + context.put("thirdParty", thirdParty); + context.put("client", client); + + tvEmbedInnerTubeBody.put("context", context); + tvEmbedInnerTubeBody.put("videoId", "%s"); + } catch (JSONException e) { + LogHelper.printException(() -> "Failed to create tvEmbedInnerTubeBody", e); + } + + TV_EMBED_INNER_TUBE_BODY = tvEmbedInnerTubeBody.toString(); + } + + private PlayerRoutes() { + } + + /** @noinspection SameParameterValue*/ + static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route) throws IOException { + var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); + connection.setRequestProperty("User-Agent", "com.google.android.youtube/18.37.36 (Linux; U; Android 12; GB) gzip"); + connection.setRequestProperty("X-Goog-Api-Format-Version", "2"); + connection.setRequestProperty("Content-Type", "application/json"); + + connection.setUseCaches(false); + connection.setDoOutput(true); + + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + return connection; + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..61828a04bb --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/requests/StoryboardRendererRequester.java @@ -0,0 +1,119 @@ +package app.revanced.integrations.patches.spoof.requests; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import app.revanced.integrations.patches.spoof.StoryboardRenderer; +import app.revanced.integrations.requests.Requester; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.*; + +public class StoryboardRendererRequester { + private StoryboardRendererRequester() { + } + + @Nullable + private static JSONObject fetchPlayerResponse(@NonNull String requestBody) { + try { + ReVancedUtils.verifyOffMainThread(); + Objects.requireNonNull(requestBody); + + final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8); + + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STORYBOARD_SPEC_RENDERER); + connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) return Requester.parseJSONObject(connection); + + LogHelper.printException(() -> "API not available: " + responseCode); + connection.disconnect(); + } catch (SocketTimeoutException ex) { + LogHelper.printException(() -> "API timed out", ex); + } catch (Exception ex) { + LogHelper.printException(() -> "Failed to fetch storyboard URL", ex); + } + + return null; + } + + private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) { + try { + return playerResponse.getJSONObject("playabilityStatus").getString("status").equals("OK"); + } catch (JSONException e) { + LogHelper.printDebug(() -> "Failed to get playabilityStatus for response: " + playerResponse); + } + + return false; + } + + /** + * Fetches the storyboardRenderer from the innerTubeBody. + * @param innerTubeBody The innerTubeBody to use to fetch the storyboardRenderer. + * @return StoryboardRenderer or null if playabilityStatus is not OK. + */ + @Nullable + private static StoryboardRenderer getStoryboardRendererUsingBody(@NonNull String innerTubeBody) { + final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody); + if (playerResponse != null && isPlayabilityStatusOk(playerResponse)) + return getStoryboardRendererUsingResponse(playerResponse); + + return null; + } + + @Nullable + private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull JSONObject playerResponse) { + try { + final JSONObject storyboards = playerResponse.getJSONObject("storyboards"); + final String storyboardsRendererTag = storyboards.has("playerLiveStoryboardSpecRenderer") + ? "playerLiveStoryboardSpecRenderer" + : "playerStoryboardSpecRenderer"; + + final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag); + StoryboardRenderer renderer = new StoryboardRenderer( + rendererElement.getString("spec"), + rendererElement.has("recommendedLevel") + ? rendererElement.getInt("recommendedLevel") + : null + ); + + LogHelper.printDebug(() -> "Fetched: " + renderer); + + return renderer; + } catch (JSONException e) { + LogHelper.printException(() -> "Failed to get storyboardRenderer", e); + } + + return null; + } + + @Nullable + public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) { + try { + Objects.requireNonNull(videoId); + + var renderer = getStoryboardRendererUsingBody(String.format(ANDROID_INNER_TUBE_BODY, videoId)); + if (renderer == null) { + LogHelper.printDebug(() -> videoId + " not available using Android client"); + renderer = getStoryboardRendererUsingBody(String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId)); + if (renderer == null) { + LogHelper.printDebug(() -> videoId + " not available using TV embedded client"); + } + } + + return renderer; + } catch (Exception ex) { + LogHelper.printException(() -> "Failed to fetch storyboard URL", ex); + } + + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/requests/Requester.java b/app/src/main/java/app/revanced/integrations/requests/Requester.java index 55b777d9bc..c756dfe855 100644 --- a/app/src/main/java/app/revanced/integrations/requests/Requester.java +++ b/app/src/main/java/app/revanced/integrations/requests/Requester.java @@ -16,7 +16,11 @@ private Requester() { } public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException { - String url = apiUrl + route.compile(params).getCompiledRoute(); + return getConnectionFromCompiledRoute(apiUrl, route.compile(params)); + } + + public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException { + String url = apiUrl + route.getCompiledRoute(); HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestMethod(route.getMethod().name()); connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";revanced"); 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 7fcd9d0f3b..21f72b9d39 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java @@ -279,7 +279,7 @@ public static Spanned getDislikesSpanForRegularVideo(@NonNull Spanned original, // 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 getDislikeSpanForContext(), as data loaded is for prior short"); + LogHelper.printDebug(() -> "Ignoring dislike span, as data loaded is for prior short"); return original; } return waitForFetchAndUpdateReplacementSpan(original, isSegmentedButton); 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 c7e6c432f4..581f934057 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -36,8 +36,6 @@ public enum SettingsEnum { // Video HDR_AUTO_BRIGHTNESS("revanced_hdr_auto_brightness", BOOLEAN, TRUE), SHOW_OLD_VIDEO_QUALITY_MENU("revanced_show_old_video_quality_menu", BOOLEAN, TRUE), - @Deprecated - DEPRECATED_SHOW_OLD_VIDEO_QUALITY_MENU("revanced_show_old_video_menu", BOOLEAN, TRUE), REMEMBER_VIDEO_QUALITY_LAST_SELECTED("revanced_remember_video_quality_last_selected", BOOLEAN, TRUE), VIDEO_QUALITY_DEFAULT_WIFI("revanced_video_quality_default_wifi", INTEGER, -2), VIDEO_QUALITY_DEFAULT_MOBILE("revanced_video_quality_default_mobile", INTEGER, -2), @@ -67,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), @@ -97,7 +99,6 @@ public enum SettingsEnum { DISABLE_RESUMING_SHORTS_PLAYER("revanced_disable_resuming_shorts_player", BOOLEAN, FALSE), HIDE_ALBUM_CARDS("revanced_hide_album_cards", BOOLEAN, FALSE, true), HIDE_ARTIST_CARDS("revanced_hide_artist_cards", BOOLEAN, FALSE), - HIDE_AUDIO_TRACK_BUTTON("revanced_hide_audio_track_button", BOOLEAN, FALSE), HIDE_AUTOPLAY_BUTTON("revanced_hide_autoplay_button", BOOLEAN, TRUE, true), HIDE_BREAKING_NEWS("revanced_hide_breaking_news", BOOLEAN, TRUE, true), HIDE_CAPTIONS_BUTTON("revanced_hide_captions_button", BOOLEAN, FALSE), @@ -168,8 +169,11 @@ 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), - SPOOF_SIGNATURE_VERIFICATION("revanced_spoof_signature_verification", BOOLEAN, TRUE, true, - "revanced_spoof_signature_verification_user_dialog_message"), + 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, + parents(SPOOF_SIGNATURE)), + BYPASS_URL_REDIRECTS("revanced_bypass_url_redirects", BOOLEAN, TRUE), // Swipe controls SWIPE_BRIGHTNESS("revanced_swipe_brightness", BOOLEAN, TRUE), @@ -368,13 +372,20 @@ private static void loadAllSettings() { // region Migration - // TODO: do _not_ delete this SB private user id migration property until sometime in 2024. + // Do _not_ delete this SB private user id migration property until sometime in 2024. // This is the only setting that cannot be reconfigured if lost, // and more time should be given for users who rarely upgrade. migrateOldSettingToNew(DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING, SB_PRIVATE_USER_ID); - // TODO: delete DEPRECATED_SHOW_OLD_VIDEO_QUALITY_MENU (When? anytime). - migrateOldSettingToNew(DEPRECATED_SHOW_OLD_VIDEO_QUALITY_MENU, SHOW_OLD_VIDEO_QUALITY_MENU); + // This migration may need to remain here for a while. + // Older online guides will still reference using commas, + // and this code will automatically convert anything the user enters to newline format, + // and also migrate any imported older settings that using commas. + String componentsToFilter = SettingsEnum.CUSTOM_FILTER_STRINGS.getString(); + if (componentsToFilter.contains(",")) { + LogHelper.printInfo(() -> "Migrating custom filter strings to new line format"); + SettingsEnum.CUSTOM_FILTER_STRINGS.saveValue(componentsToFilter.replace(",", "\n")); + } // endregion } 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/utils/ReVancedUtils.java b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java index 564de2c384..940dd50819 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 { diff --git a/app/src/main/java/app/revanced/tudortmund/lockscreen/ShowOnLockscreenPatch.java b/app/src/main/java/app/revanced/tudortmund/lockscreen/ShowOnLockscreenPatch.java new file mode 100644 index 0000000000..2260de24d7 --- /dev/null +++ b/app/src/main/java/app/revanced/tudortmund/lockscreen/ShowOnLockscreenPatch.java @@ -0,0 +1,46 @@ +package app.revanced.tudortmund.lockscreen; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.view.Display; +import android.view.Window; +import androidx.appcompat.app.AppCompatActivity; + +import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; +import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; + +public class ShowOnLockscreenPatch { + /** + * @noinspection deprecation + */ + public static Window getWindow(AppCompatActivity activity, float brightness) { + Window window = activity.getWindow(); + + if (brightness >= 0) { + // High brightness set, therefore show on lockscreen. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) activity.setShowWhenLocked(true); + else window.addFlags(FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD); + } else { + // Ignore brightness reset when the screen is turned off. + DisplayManager displayManager = (DisplayManager) activity.getSystemService(Context.DISPLAY_SERVICE); + + boolean isScreenOn = false; + for (Display display : displayManager.getDisplays()) { + if (display.getState() == Display.STATE_OFF) continue; + + isScreenOn = true; + break; + } + + if (isScreenOn) { + // Hide on lockscreen. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) activity.setShowWhenLocked(false); + else window.clearFlags(FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD); + } + } + + return window; + } + +} 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/app/src/main/java/app/revanced/twitch/adblock/LuminousService.java b/app/src/main/java/app/revanced/twitch/adblock/LuminousService.java new file mode 100644 index 0000000000..34acece3f9 --- /dev/null +++ b/app/src/main/java/app/revanced/twitch/adblock/LuminousService.java @@ -0,0 +1,46 @@ +package app.revanced.twitch.adblock; + +import app.revanced.twitch.utils.LogHelper; +import app.revanced.twitch.utils.ReVancedUtils; +import okhttp3.HttpUrl; +import okhttp3.Request; + +public class LuminousService implements IAdblockService { + @Override + public String friendlyName() { + return ReVancedUtils.getString("revanced_proxy_luminous"); + } + + @Override + public Integer maxAttempts() { + return 2; + } + + @Override + public Boolean isAvailable() { + return true; + } + + @Override + public Request rewriteHlsRequest(Request originalRequest) { + var type = IAdblockService.isVod(originalRequest) ? "vod" : "playlist"; + var url = HttpUrl.parse("https://eu.luminous.dev/" + + type + + "/" + + IAdblockService.channelName(originalRequest) + + ".m3u8" + + "%3Fallow_source%3Dtrue%26allow_audio_only%3Dtrue%26fast_bread%3Dtrue" + ); + + if (url == null) { + LogHelper.error("Failed to parse rewritten URL"); + return null; + } + + // Overwrite old request + return new Request.Builder() + .get() + .url(url) + .build(); + } +} diff --git a/app/src/main/java/app/revanced/twitch/adblock/PurpleAdblockService.java b/app/src/main/java/app/revanced/twitch/adblock/PurpleAdblockService.java index 5b2db973b2..07e47dca30 100644 --- a/app/src/main/java/app/revanced/twitch/adblock/PurpleAdblockService.java +++ b/app/src/main/java/app/revanced/twitch/adblock/PurpleAdblockService.java @@ -1,14 +1,13 @@ package app.revanced.twitch.adblock; -import java.util.HashMap; -import java.util.Map; - import app.revanced.twitch.api.RetrofitClient; import app.revanced.twitch.utils.LogHelper; import app.revanced.twitch.utils.ReVancedUtils; import okhttp3.HttpUrl; import okhttp3.Request; -import okhttp3.ResponseBody; + +import java.util.HashMap; +import java.util.Map; public class PurpleAdblockService implements IAdblockService { private final Map tunnels = new HashMap<>() {{ @@ -36,9 +35,13 @@ public Boolean isAvailable() { if (!response.isSuccessful()) { LogHelper.error("PurpleAdBlock tunnel $tunnel returned an error: HTTP code %d", response.code()); LogHelper.debug(response.message()); - if (response.errorBody() != null) { - LogHelper.debug(((ResponseBody) response.errorBody()).string()); + + try (var errorBody = response.errorBody()) { + if (errorBody != null) { + LogHelper.debug(errorBody.string()); + } } + success = false; } } catch (Exception ex) { diff --git a/app/src/main/java/app/revanced/twitch/adblock/TTVLolService.java b/app/src/main/java/app/revanced/twitch/adblock/TTVLolService.java deleted file mode 100644 index 6aa31d823f..0000000000 --- a/app/src/main/java/app/revanced/twitch/adblock/TTVLolService.java +++ /dev/null @@ -1,70 +0,0 @@ -package app.revanced.twitch.adblock; - -import java.util.ArrayList; -import java.util.Random; - -import app.revanced.twitch.utils.LogHelper; -import app.revanced.twitch.utils.ReVancedUtils; -import okhttp3.HttpUrl; -import okhttp3.Request; - -public class TTVLolService implements IAdblockService { - @Override - public String friendlyName() { - return ReVancedUtils.getString("revanced_proxy_ttv_lol"); - } - - // TTV.lol is sometimes unstable - @Override - public Integer maxAttempts() { - return 4; - } - - @Override - public Boolean isAvailable() { - return true; - } - - @Override - public Request rewriteHlsRequest(Request originalRequest) { - - var type = "vod"; - if (!IAdblockService.isVod(originalRequest)) - type = "playlist"; - - var url = HttpUrl.parse("https://api.ttv.lol/" + - type + "/" + - IAdblockService.channelName(originalRequest) + - ".m3u8" + nextQuery() - ); - - if (url == null) { - LogHelper.error("Failed to parse rewritten URL"); - return null; - } - - // Overwrite old request - return new Request.Builder() - .get() - .url(url) - .addHeader("X-Donate-To", "https://ttv.lol/donate") - .build(); - } - - private String nextQuery() { - return SAMPLE_QUERY.replace("", generateSessionId()); - } - - private String generateSessionId() { - final var chars = "abcdef0123456789".toCharArray(); - - var sessionId = new ArrayList(); - for (int i = 0; i < 32; i++) - sessionId.add(chars[randomSource.nextInt(16)]); - - return sessionId.toString(); - } - - private final Random randomSource = new Random(); - private final String SAMPLE_QUERY = "%3Fallow_source%3Dtrue%26fast_bread%3Dtrue%26allow_audio_only%3Dtrue%26p%3D0%26play_session_id%3D%26player_backend%3Dmediaplayer%26warp%3Dfalse%26force_preroll%3Dfalse%26mobile_cellular%3Dfalse"; -} diff --git a/app/src/main/java/app/revanced/twitch/api/RequestInterceptor.java b/app/src/main/java/app/revanced/twitch/api/RequestInterceptor.java index 34cda6f6ac..f571c23954 100644 --- a/app/src/main/java/app/revanced/twitch/api/RequestInterceptor.java +++ b/app/src/main/java/app/revanced/twitch/api/RequestInterceptor.java @@ -1,21 +1,20 @@ package app.revanced.twitch.api; -import static app.revanced.twitch.adblock.IAdblockService.channelName; -import static app.revanced.twitch.adblock.IAdblockService.isVod; - import androidx.annotation.NonNull; - -import java.io.IOException; - import app.revanced.twitch.adblock.IAdblockService; +import app.revanced.twitch.adblock.LuminousService; import app.revanced.twitch.adblock.PurpleAdblockService; -import app.revanced.twitch.adblock.TTVLolService; import app.revanced.twitch.settings.SettingsEnum; import app.revanced.twitch.utils.LogHelper; import app.revanced.twitch.utils.ReVancedUtils; import okhttp3.Interceptor; import okhttp3.Response; +import java.io.IOException; + +import static app.revanced.twitch.adblock.IAdblockService.channelName; +import static app.revanced.twitch.adblock.IAdblockService.isVod; + public class RequestInterceptor implements Interceptor { private IAdblockService activeService = null; @@ -87,8 +86,8 @@ public Response intercept(@NonNull Chain chain) throws IOException { private void updateActiveService() { var current = SettingsEnum.BLOCK_EMBEDDED_ADS.getString(); - if (current.equals(ReVancedUtils.getString("key_revanced_proxy_ttv_lol")) && !(activeService instanceof TTVLolService)) - activeService = new TTVLolService(); + if (current.equals(ReVancedUtils.getString("key_revanced_proxy_luminous")) && !(activeService instanceof LuminousService)) + activeService = new LuminousService(); else if (current.equals(ReVancedUtils.getString("key_revanced_proxy_purpleadblock")) && !(activeService instanceof PurpleAdblockService)) activeService = new PurpleAdblockService(); else if (current.equals(ReVancedUtils.getString("key_revanced_proxy_disabled"))) diff --git a/app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java b/app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java index 6e1833873e..6a2f187e84 100644 --- a/app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java @@ -1,23 +1,21 @@ package app.revanced.twitch.settings; -import static java.lang.Boolean.FALSE; -import static java.lang.Boolean.TRUE; -import static app.revanced.twitch.settings.SettingsEnum.ReturnType.BOOLEAN; -import static app.revanced.twitch.settings.SettingsEnum.ReturnType.STRING; - import android.content.Context; import android.content.SharedPreferences; - import androidx.annotation.NonNull; - import app.revanced.twitch.utils.LogHelper; import app.revanced.twitch.utils.ReVancedUtils; +import static app.revanced.twitch.settings.SettingsEnum.ReturnType.BOOLEAN; +import static app.revanced.twitch.settings.SettingsEnum.ReturnType.STRING; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + public enum SettingsEnum { /* Ads */ BLOCK_VIDEO_ADS("revanced_block_video_ads", BOOLEAN, TRUE), BLOCK_AUDIO_ADS("revanced_block_audio_ads", BOOLEAN, TRUE), - BLOCK_EMBEDDED_ADS("revanced_block_embedded_ads", STRING, "ttv-lol"), + BLOCK_EMBEDDED_ADS("revanced_block_embedded_ads", STRING, "luminous"), /* Chat */ SHOW_DELETED_MESSAGES("revanced_show_deleted_messages", STRING, "cross-out"), diff --git a/dummy/src/main/java/com/facebook/litho/ComponentHost.java b/dummy/src/main/java/com/facebook/litho/ComponentHost.java deleted file mode 100644 index ea2cde3fd2..0000000000 --- a/dummy/src/main/java/com/facebook/litho/ComponentHost.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.facebook.litho; - -import android.content.Context; -import android.support.v7.widget.RecyclerView; - -public final class ComponentHost extends RecyclerView { - public ComponentHost(Context context) { - super(context); - } -} 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 f547f97e49..848e696fac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 0.117.1 +version = 0.118.0-dev.24