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