From 01f084d87af6a2b1bc0581b1adbb6dfdfff75d82 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 20 Aug 2024 00:16:42 -0400 Subject: [PATCH 01/29] fix(YouTube - SponsorBlock): Handle if the user enters an invalid number into any SB settings --- .../SponsorBlockPreferenceFragment.java | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java index 371ef389..05e0dbaf 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java @@ -257,13 +257,19 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment { newSegmentStep.setSummary(str("revanced_sb_general_adjusting_sum")); newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> { - final int newAdjustmentValue = Integer.parseInt(newValue.toString()); - if (newAdjustmentValue == 0) { - Utils.showToastLong(str("revanced_sb_general_adjusting_invalid")); - return false; + try { + final int newAdjustmentValue = Integer.parseInt(newValue.toString()); + if (newAdjustmentValue != 0) { + Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue); + return true; + } + } catch (NumberFormatException ex) { + Logger.printInfo(() -> "Invalid new segment step", ex); } - Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue); - return true; + + Utils.showToastLong(str("revanced_sb_general_adjusting_invalid")); + updateUI(); + return false; }); category.addPreference(newSegmentStep); @@ -309,8 +315,17 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment { minSegmentDuration.setSummary(str("revanced_sb_general_min_duration_sum")); minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> { - Settings.SB_SEGMENT_MIN_DURATION.save(Float.valueOf(newValue.toString())); - return true; + try { + Float minTimeDuration = Float.valueOf(newValue.toString()); + Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration); + return true; + } catch (NumberFormatException ex) { + Logger.printInfo(() -> "Invalid minimum segment duration", ex); + } + + Utils.showToastLong(str("revanced_sb_general_min_duration_invalid")); + updateUI(); + return false; }); category.addPreference(minSegmentDuration); @@ -323,6 +338,7 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment { Utils.showToastLong(str("revanced_sb_general_uuid_invalid")); return false; } + Settings.SB_PRIVATE_USER_ID.save(newUUID); updateUI(); fetchAndDisplayStats(); From 7a58ae5d0e7ab595cde78f4ea0071b4f02707037 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 20 Aug 2024 04:20:24 +0000 Subject: [PATCH 02/29] chore(release): 1.13.1-dev.1 [skip ci] ## [1.13.1-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.13.0...v1.13.1-dev.1) (2024-08-20) ### Bug Fixes * **YouTube - SponsorBlock:** Handle if the user enters an invalid number into any SB settings ([01f084d](https://github.com/ReVanced/revanced-integrations/commit/01f084d87af6a2b1bc0581b1adbb6dfdfff75d82)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d885484..70e33616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [1.13.1-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.13.0...v1.13.1-dev.1) (2024-08-20) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Handle if the user enters an invalid number into any SB settings ([01f084d](https://github.com/ReVanced/revanced-integrations/commit/01f084d87af6a2b1bc0581b1adbb6dfdfff75d82)) + # [1.13.0](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0...v1.13.0) (2024-08-15) diff --git a/gradle.properties b/gradle.properties index 79ce6376..0b892916 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.13.0 +version = 1.13.1-dev.1 From 6be257a7a66aaa67c187d71530d6773c06a41993 Mon Sep 17 00:00:00 2001 From: Bceez <153008658+Bceez@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:16:43 +0200 Subject: [PATCH 03/29] fix(YouTube - Hide layout components): Hide new kind of community post (#678) --- .../youtube/patches/components/LayoutComponentsFilter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java index 4d7358d4..a0ffde0b 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java @@ -81,7 +81,8 @@ public final class LayoutComponentsFilter extends Filter { Settings.HIDE_COMMUNITY_POSTS, "post_base_wrapper", "image_post_root.eml", - "text_post_root.eml" + "text_post_root.eml", + "images_post_root.eml" ); final var communityGuidelines = new StringFilterGroup( From 08d9f612a6943869c30c478e63a7c2b43554e9c2 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 20 Aug 2024 06:20:04 +0000 Subject: [PATCH 04/29] chore(release): 1.13.1-dev.2 [skip ci] ## [1.13.1-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.13.1-dev.1...v1.13.1-dev.2) (2024-08-20) ### Bug Fixes * **YouTube - Hide layout components:** Hide new kind of community post ([#678](https://github.com/ReVanced/revanced-integrations/issues/678)) ([6be257a](https://github.com/ReVanced/revanced-integrations/commit/6be257a7a66aaa67c187d71530d6773c06a41993)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70e33616..af1a18ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [1.13.1-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.13.1-dev.1...v1.13.1-dev.2) (2024-08-20) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Hide new kind of community post ([#678](https://github.com/ReVanced/revanced-integrations/issues/678)) ([6be257a](https://github.com/ReVanced/revanced-integrations/commit/6be257a7a66aaa67c187d71530d6773c06a41993)) + ## [1.13.1-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.13.0...v1.13.1-dev.1) (2024-08-20) diff --git a/gradle.properties b/gradle.properties index 0b892916..85d798ef 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.13.1-dev.1 +version = 1.13.1-dev.2 From 2c471f39c229af940b7c0890a228bdf01bdc8c39 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 22 Aug 2024 13:47:15 -0400 Subject: [PATCH 05/29] feat(YouTube - Spoof client): Allow forcing AVC codec with iOS (#679) --- .../patches/spoof/SpoofClientPatch.java | 156 ++++++++++++------ .../youtube/settings/Settings.java | 8 +- 2 files changed, 109 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 2b29dd97..14e5e2f1 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -1,19 +1,25 @@ package app.revanced.integrations.youtube.patches.spoof; +import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowAV1; +import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowVP9; + import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.net.Uri; import android.os.Build; + +import org.chromium.net.ExperimentalUrlRequest; + import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.settings.Setting; import app.revanced.integrations.youtube.patches.BackgroundPlaybackPatch; import app.revanced.integrations.youtube.settings.Settings; -import org.chromium.net.ExperimentalUrlRequest; @SuppressWarnings("unused") public class SpoofClientPatch { private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get(); - private static final ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_USE_IOS.get() ? ClientType.IOS : ClientType.ANDROID_VR; - private static final boolean SPOOFING_TO_IOS = SPOOF_CLIENT_ENABLED && SPOOF_CLIENT_TYPE == ClientType.IOS; + private static final ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get(); + private static final boolean SPOOF_IOS = SPOOF_CLIENT_ENABLED && SPOOF_CLIENT_TYPE == ClientType.IOS; /** * Any unreachable ip address. Used to intentionally fail requests. @@ -81,7 +87,7 @@ public class SpoofClientPatch { * Injection point. */ public static String getClientVersion(String originalClientVersion) { - return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.version : originalClientVersion; + return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.appVersion : originalClientVersion; } /** @@ -96,7 +102,7 @@ public class SpoofClientPatch { * Fix video qualities missing, if spoofing to iOS by using the correct client OS version. */ public static String getOsVersion(String originalOsVersion) { - return SPOOFING_TO_IOS ? ClientType.IOS.osVersion : originalOsVersion; + return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.osVersion : originalOsVersion; } /** @@ -119,7 +125,7 @@ public class SpoofClientPatch { * Return true to force create the playback speed menu. */ public static boolean forceCreatePlaybackSpeedMenu(boolean original) { - return SPOOFING_TO_IOS || original; + return SPOOF_IOS || original; } /** @@ -128,7 +134,7 @@ public class SpoofClientPatch { * Return true to force enable audio background play. */ public static boolean overrideBackgroundAudioPlayback() { - return SPOOFING_TO_IOS && BackgroundPlaybackPatch.playbackIsNotShort(); + return SPOOF_IOS && BackgroundPlaybackPatch.playbackIsNotShort(); } /** @@ -136,36 +142,97 @@ public class SpoofClientPatch { * Fix video qualities missing, if spoofing to iOS by using the correct iOS user-agent. */ public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, String url) { - if (SPOOFING_TO_IOS) { + if (SPOOF_CLIENT_ENABLED) { String path = Uri.parse(url).getPath(); if (path != null && path.contains("player")) { - return builder.addHeader("User-Agent", ClientType.IOS.userAgent).build(); + return builder.addHeader("User-Agent", SPOOF_CLIENT_TYPE.userAgent).build(); } } return builder.build(); } - private enum ClientType { + // Must check for device features in a separate class and cannot place this code inside + // the Patch or ClientType enum due to cyclic Setting references. + static class DeviceHardwareSupport { + private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding(); + private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding(); + + private static boolean deviceHasVP9HardwareDecoding() { + MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + + for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { + final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + ? codecInfo.isHardwareAccelerated() + : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. + if (isHardwareAccelerated && !codecInfo.isEncoder()) { + for (String type : codecInfo.getSupportedTypes()) { + if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { + Logger.printDebug(() -> "Device supports VP9 hardware decoding."); + return true; + } + } + } + } + + Logger.printDebug(() -> "Device does not support VP9 hardware decoding."); + return false; + } + + private static boolean deviceHasAV1HardwareDecoding() { + // It appears all devices with hardware AV1 are also Android 10 or newer. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + + for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { + if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) { + for (String type : codecInfo.getSupportedTypes()) { + if (type.equalsIgnoreCase("video/av01")) { + Logger.printDebug(() -> "Device supports AV1 hardware decoding."); + return true; + } + } + } + } + } + + Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); + return false; + } + + static boolean allowVP9() { + return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_CLIENT_IOS_FORCE_AVC.get(); + } + + static boolean allowAV1() { + return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; + } + } + + public enum ClientType { // https://dumps.tadiphone.dev/dumps/oculus/eureka + IOS(5, + // iPhone 15 supports AV1 hardware decoding. + // Only use if this Android device also has hardware decoding. + allowAV1() + ? "iPhone16,2" // 15 Pro Max + : "iPhone11,4", // XS Max + // iOS 14+ forces VP9. + allowVP9() + ? "17.5.1.21F90" + : "13.7.17H35", + allowVP9() + ? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)" + : "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)", + // Version number should be a valid iOS release. + // https://www.ipa4fun.com/history/185230 + "19.10.7" + ), ANDROID_VR(28, "Quest 3", - "1.56.21", "12", - "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip" - ), - // 11,4 = iPhone XS Max. - // 16,2 = iPhone 15 Pro Max. - // Since the 15 supports AV1 hardware decoding, only spoof that device if this - // Android device also has hardware decoding. - // - // Version number should be a valid iOS release. - // https://www.ipa4fun.com/history/185230 - IOS(5, - deviceHasAV1HardwareDecoding() ? "iPhone16,2" : "iPhone11,4", - "19.10.7", - "17.5.1.21F90", - "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)" + "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", + "1.56.21" ); /** @@ -179,11 +246,6 @@ public class SpoofClientPatch { */ final String model; - /** - * App version. - */ - final String version; - /** * Device OS version. */ @@ -194,36 +256,24 @@ public class SpoofClientPatch { */ final String userAgent; - ClientType(int id, String model, String version, String osVersion, String userAgent) { + /** + * App version. + */ + final String appVersion; + + ClientType(int id, String model, String osVersion, String userAgent, String appVersion) { this.id = id; this.model = model; - this.version = version; this.osVersion = osVersion; this.userAgent = userAgent; + this.appVersion = appVersion; } } - private static boolean deviceHasAV1HardwareDecoding() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - - for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) { - String[] supportedTypes = codecInfo.getSupportedTypes(); - for (String type : supportedTypes) { - if (type.equalsIgnoreCase("video/av01")) { - MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(type); - if (capabilities != null) { - Logger.printDebug(() -> "Device supports AV1 hardware decoding."); - return true; - } - } - } - } - } + public static final class ForceiOSAVCAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return Settings.SPOOF_CLIENT.get() && Settings.SPOOF_CLIENT_TYPE.get() == ClientType.IOS; } - - Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); - return false; } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 47ee02c8..8708d579 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -6,6 +6,7 @@ import static app.revanced.integrations.shared.settings.Setting.*; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3; +import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; import java.util.Arrays; @@ -20,6 +21,7 @@ import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.Stil import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption; import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailStillTime; import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch; +import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch; import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings; @SuppressWarnings("deprecation") @@ -253,8 +255,10 @@ public class Settings extends BaseSettings { "revanced_spoof_device_dimensions_user_dialog_message"); public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE); public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE); - public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true, "revanced_spoof_client_user_dialog_message"); - public static final BooleanSetting SPOOF_CLIENT_USE_IOS = new BooleanSetting("revanced_spoof_client_use_ios", TRUE, true, parent(SPOOF_CLIENT)); + public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true,"revanced_spoof_client_user_dialog_message"); + public static final BooleanSetting SPOOF_CLIENT_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_client_ios_force_avc", FALSE, true, + "revanced_spoof_client_ios_force_avc_user_dialog_message", new SpoofClientPatch.ForceiOSAVCAvailability()); + public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", ClientType.IOS, true, parent(SPOOF_CLIENT)); @Deprecated public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", ""); public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1); From db8133207854454a9b37d4e07d0ffe5974990012 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 22 Aug 2024 17:50:39 +0000 Subject: [PATCH 06/29] chore(release): 1.14.0-dev.1 [skip ci] # [1.14.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.13.1-dev.2...v1.14.0-dev.1) (2024-08-22) ### Features * **YouTube - Spoof client:** Allow forcing AVC codec with iOS ([#679](https://github.com/ReVanced/revanced-integrations/issues/679)) ([2c471f3](https://github.com/ReVanced/revanced-integrations/commit/2c471f39c229af940b7c0890a228bdf01bdc8c39)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af1a18ad..fd99c70f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.14.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.13.1-dev.2...v1.14.0-dev.1) (2024-08-22) + + +### Features + +* **YouTube - Spoof client:** Allow forcing AVC codec with iOS ([#679](https://github.com/ReVanced/revanced-integrations/issues/679)) ([2c471f3](https://github.com/ReVanced/revanced-integrations/commit/2c471f39c229af940b7c0890a228bdf01bdc8c39)) + ## [1.13.1-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.13.1-dev.1...v1.13.1-dev.2) (2024-08-20) diff --git a/gradle.properties b/gradle.properties index 85d798ef..96e93f7c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.13.1-dev.2 +version = 1.14.0-dev.1 From 5314dd90d16dc8565331c4cddce114956d85a173 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:38:44 -0400 Subject: [PATCH 07/29] feat(YouTube - Keyword filter): Add syntax to match whole keywords and not substrings (#681) Co-authored-by: oSumAtrIX --- .../components/KeywordContentFilter.java | 250 +++++++++++++++--- .../settings/preference/HtmlPreference.java | 35 +++ 2 files changed, 253 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java index 4e0e6f58..3185036c 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java @@ -2,6 +2,7 @@ package app.revanced.integrations.youtube.patches.components; import static app.revanced.integrations.shared.StringRef.str; import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; +import static java.lang.Character.UnicodeBlock.*; import android.os.Build; @@ -10,9 +11,8 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import app.revanced.integrations.shared.Logger; @@ -26,7 +26,7 @@ import app.revanced.integrations.youtube.shared.PlayerType; /** *
- * Allows hiding home feed and search results based on keywords and/or channel names.
+ * Allows hiding home feed and search results based on video title keywords and/or channel names.
  *
  * Limitations:
  * - Searching for a keyword phrase will give no search results.
@@ -41,19 +41,14 @@ import app.revanced.integrations.youtube.shared.PlayerType;
  *   (ie: "mr beast" automatically filters "Mr Beast" and "MR BEAST").
  * - Keywords present in the layout or video data cannot be used as filters, otherwise all videos
  *   will always be hidden.  This patch checks for some words of these words.
+ * - When using whole word syntax, some keywords may need additional pluralized variations.
  */
 @SuppressWarnings("unused")
 @RequiresApi(api = Build.VERSION_CODES.N)
 final class KeywordContentFilter extends Filter {
 
     /**
-     * Minimum keyword/phrase length to prevent excessively broad content filtering.
-     */
-    private static final int MINIMUM_KEYWORD_LENGTH = 3;
-
-    /**
-     * Strings found in the buffer for every videos.
-     * Full strings should be specified, as they are compared using {@link String#contains(CharSequence)}.
+     * Strings found in the buffer for every videos.  Full strings should be specified.
      *
      * This list does not include every common buffer string, and this can be added/changed as needed.
      * Words must be entered with the exact casing as found in the buffer.
@@ -88,7 +83,7 @@ final class KeywordContentFilter extends Filter {
             "search_vwc_description_transition_key",
             "g-high-recZ",
             // Text and litho components found in the buffer that belong to path filters.
-            "metadata.eml",
+            "expandable_metadata.eml",
             "thumbnail.eml",
             "avatar.eml",
             "overflow_button.eml",
@@ -107,7 +102,8 @@ final class KeywordContentFilter extends Filter {
             "search_video_with_context.eml",
             "video_with_context.eml", // Subscription tab videos.
             "related_video_with_context.eml",
-            "video_lockup_with_attachment.eml", // A/B test for subscribed video.
+            // A/B test for subscribed video, and sometimes when tablet layout is enabled.
+            "video_lockup_with_attachment.eml",
             "compact_video.eml",
             "inline_shorts",
             "shorts_video_cell",
@@ -139,6 +135,12 @@ final class KeywordContentFilter extends Filter {
             "overflow_button.eml"
     );
 
+    /**
+     * Minimum keyword/phrase length to prevent excessively broad content filtering.
+     * Only applies when not using whole word syntax.
+     */
+    private static final int MINIMUM_KEYWORD_LENGTH = 3;
+
     /**
      * Threshold for {@link #filteredVideosPercentage}
      * that indicates all or nearly all videos have been filtered.
@@ -150,6 +152,8 @@ final class KeywordContentFilter extends Filter {
 
     private static final long ALL_VIDEOS_FILTERED_BACKOFF_MILLISECONDS = 60 * 1000; // 60 seconds
 
+    private static final int UTF8_MAX_BYTE_COUNT = 4;
+
     /**
      * Rolling average of how many videos were filtered by a keyword.
      * Used to detect if a keyword passes the initial check against {@link #STRINGS_IN_EVERY_BUFFER}
@@ -216,23 +220,167 @@ final class KeywordContentFilter extends Filter {
                 capitalizeNext = false;
             }
         }
+
         return new String(codePoints, 0, codePoints.length);
     }
 
     /**
-     * @return If the phrase will will hide all videos. Not an exhaustive check.
+     * @return If the string contains any characters from languages that do not use spaces between words.
      */
-    private static boolean phrasesWillHideAllVideos(@NonNull String[] phrases) {
-        for (String commonString : STRINGS_IN_EVERY_BUFFER) {
-            if (Utils.containsAny(commonString, phrases)) {
+    private static boolean isLanguageWithNoSpaces(String text) {
+        for (int i = 0, length = text.length(); i < length;) {
+            final int codePoint = text.codePointAt(i);
+
+            Character.UnicodeBlock block = Character.UnicodeBlock.of(codePoint);
+            if (block == CJK_UNIFIED_IDEOGRAPHS // Chinese and Kanji
+                    || block == HIRAGANA // Japanese Hiragana
+                    || block == KATAKANA // Japanese Katakana
+                    || block == THAI
+                    || block == LAO
+                    || block == MYANMAR
+                    || block == KHMER
+                    || block == TIBETAN) {
                 return true;
             }
+
+            i += Character.charCount(codePoint);
         }
+
         return false;
     }
 
+    /**
+     * @return If the phrase will hide all videos. Not an exhaustive check.
+     */
+    private static boolean phrasesWillHideAllVideos(@NonNull String[] phrases, boolean matchWholeWords) {
+        for (String phrase : phrases) {
+            for (String commonString : STRINGS_IN_EVERY_BUFFER) {
+                if (matchWholeWords) {
+                    byte[] commonStringBytes = commonString.getBytes(StandardCharsets.UTF_8);
+                    int matchIndex = 0;
+                    while (true) {
+                        matchIndex = commonString.indexOf(phrase, matchIndex);
+                        if (matchIndex < 0) break;
+
+                        if (keywordMatchIsWholeWord(commonStringBytes, matchIndex, phrase.length())) {
+                            return true;
+                        }
+
+                        matchIndex++;
+                    }
+                } else if (Utils.containsAny(commonString, phrases)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @return If the start and end indexes are not surrounded by other letters.
+     *         If the indexes are surrounded by numbers/symbols/punctuation it is considered a whole word.
+     */
+    private static boolean keywordMatchIsWholeWord(byte[] text, int keywordStartIndex, int keywordLength) {
+        final Integer codePointBefore = getUtf8CodePointBefore(text, keywordStartIndex);
+        if (codePointBefore != null && Character.isLetter(codePointBefore)) {
+            return false;
+        }
+
+        final Integer codePointAfter = getUtf8CodePointAt(text, keywordStartIndex + keywordLength);
+        //noinspection RedundantIfStatement
+        if (codePointAfter != null && Character.isLetter(codePointAfter)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @return The UTF8 character point immediately before the index,
+     *         or null if the bytes before the index is not a valid UTF8 character.
+     */
+    @Nullable
+    private static Integer getUtf8CodePointBefore(byte[] data, int index) {
+        int characterByteCount = 0;
+        while (--index >= 0 && ++characterByteCount <= UTF8_MAX_BYTE_COUNT) {
+            if (isValidUtf8(data, index, characterByteCount)) {
+                return decodeUtf8ToCodePoint(data, index, characterByteCount);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * @return The UTF8 character point at the index,
+     *         or null if the index holds no valid UTF8 character.
+     */
+    @Nullable
+    private static Integer getUtf8CodePointAt(byte[] data, int index) {
+        int characterByteCount = 0;
+        final int dataLength = data.length;
+        while (index + characterByteCount < dataLength && ++characterByteCount <= UTF8_MAX_BYTE_COUNT) {
+            if (isValidUtf8(data, index, characterByteCount)) {
+                return decodeUtf8ToCodePoint(data, index, characterByteCount);
+            }
+        }
+
+        return null;
+    }
+
+    public static boolean isValidUtf8(byte[] data, int startIndex, int numberOfBytes) {
+        switch (numberOfBytes) {
+            case 1: // 0xxxxxxx (ASCII)
+                return (data[startIndex] & 0x80) == 0;
+            case 2: // 110xxxxx, 10xxxxxx
+                return (data[startIndex] & 0xE0) == 0xC0
+                        && (data[startIndex + 1] & 0xC0) == 0x80;
+            case 3: // 1110xxxx, 10xxxxxx, 10xxxxxx
+                return (data[startIndex] & 0xF0) == 0xE0
+                        && (data[startIndex + 1] & 0xC0) == 0x80
+                        && (data[startIndex + 2] & 0xC0) == 0x80;
+            case 4: // 11110xxx, 10xxxxxx, 10xxxxxx, 10xxxxxx
+                return (data[startIndex] & 0xF8) == 0xF0
+                        && (data[startIndex + 1] & 0xC0) == 0x80
+                        && (data[startIndex + 2] & 0xC0) == 0x80
+                        && (data[startIndex + 3] & 0xC0) == 0x80;
+        }
+
+        throw new IllegalArgumentException("numberOfBytes: " + numberOfBytes);
+    }
+
+    public static int decodeUtf8ToCodePoint(byte[] data, int startIndex, int numberOfBytes) {
+        switch (numberOfBytes) {
+            case 1:
+                return data[startIndex];
+            case 2:
+                return ((data[startIndex] & 0x1F) << 6) |
+                        (data[startIndex + 1] & 0x3F);
+            case 3:
+                return ((data[startIndex] & 0x0F) << 12) |
+                        ((data[startIndex + 1] & 0x3F) << 6) |
+                        (data[startIndex + 2] & 0x3F);
+            case 4:
+                return ((data[startIndex] & 0x07) << 18) |
+                        ((data[startIndex + 1] & 0x3F) << 12) |
+                        ((data[startIndex + 2] & 0x3F) << 6) |
+                        (data[startIndex + 3] & 0x3F);
+        }
+        throw new IllegalArgumentException("numberOfBytes: " + numberOfBytes);
+    }
+
+    private static boolean phraseUsesWholeWordSyntax(String phrase) {
+        return phrase.startsWith("\"") && phrase.endsWith("\"");
+    }
+
+    private static String stripWholeWordSyntax(String phrase) {
+        return phrase.substring(1, phrase.length() - 1);
+    }
+
     private synchronized void parseKeywords() { // Must be synchronized since Litho is multi-threaded.
         String rawKeywords = Settings.HIDE_KEYWORD_CONTENT_PHRASES.get();
+
         //noinspection StringEquality
         if (rawKeywords == lastKeywordPhrasesParsed) {
             Logger.printDebug(() -> "Using previously initialized search");
@@ -243,20 +391,33 @@ final class KeywordContentFilter extends Filter {
         String[] split = rawKeywords.split("\n");
         if (split.length != 0) {
             // Linked Set so log statement are more organized and easier to read.
-            Set keywords = new LinkedHashSet<>(10 * split.length);
+            // Map is: Phrase -> isWholeWord
+            Map keywords = new LinkedHashMap<>(10 * split.length);
 
             for (String phrase : split) {
-                // Remove any trailing white space the user may have accidentally included.
+                // Remove any trailing spaces the user may have accidentally included.
                 phrase = phrase.stripTrailing();
                 if (phrase.isBlank()) continue;
 
-                if (phrase.length() < MINIMUM_KEYWORD_LENGTH) {
+                final boolean wholeWordMatching;
+                if (phraseUsesWholeWordSyntax(phrase)) {
+                    if (phrase.length() == 2) {
+                        continue; // Empty "" phrase
+                    }
+                    phrase = stripWholeWordSyntax(phrase);
+                    wholeWordMatching = true;
+                } else if (phrase.length() < MINIMUM_KEYWORD_LENGTH && !isLanguageWithNoSpaces(phrase)) {
+                    // Allow phrases of 1 and 2 characters if using a
+                    // language that does not use spaces between words.
+
                     // Do not reset the setting. Keep the invalid keywords so the user can fix the mistake.
                     Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_length", phrase, MINIMUM_KEYWORD_LENGTH));
                     continue;
+                } else {
+                    wholeWordMatching = false;
                 }
 
-                // Add common casing that might appear.
+                // Common casing that might appear.
                 //
                 // This could be simplified by adding case insensitive search to the prefix search,
                 // which is very simple to add to StringTreSearch for Unicode and ByteTrieSearch for ASCII.
@@ -265,7 +426,7 @@ final class KeywordContentFilter extends Filter {
                 // UTF-8 characters can be different byte lengths, which does
                 // not allow comparing two different byte arrays using simple plain array indexes.
                 //
-                // Instead add all common case variations of the words.
+                // Instead use all common case variations of the words.
                 String[] phraseVariations = {
                         phrase,
                         phrase.toLowerCase(),
@@ -273,20 +434,45 @@ final class KeywordContentFilter extends Filter {
                         capitalizeAllFirstLetters(phrase),
                         phrase.toUpperCase()
                 };
-                if (phrasesWillHideAllVideos(phraseVariations)) {
-                    Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_common", phrase));
+
+                if (phrasesWillHideAllVideos(phraseVariations, wholeWordMatching)) {
+                    String toastMessage;
+                    // If whole word matching is off, but would pass with on, then show a different toast.
+                    if (!wholeWordMatching && !phrasesWillHideAllVideos(phraseVariations, true)) {
+                        toastMessage = "revanced_hide_keyword_toast_invalid_common_whole_word_required";
+                    } else {
+                        toastMessage = "revanced_hide_keyword_toast_invalid_common";
+                    }
+
+                    Utils.showToastLong(str(toastMessage, phrase));
                     continue;
                 }
 
-                keywords.addAll(Arrays.asList(phraseVariations));
+                for (String variation : phraseVariations) {
+                    // Check if the same phrase is declared both with and without quotes.
+                    Boolean existing = keywords.get(variation);
+                    if (existing == null) {
+                        keywords.put(variation, wholeWordMatching);
+                    } else if (existing != wholeWordMatching) {
+                        Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_conflicting", phrase));
+                        break;
+                    }
+                }
             }
 
-            for (String keyword : keywords) {
-                // Use a callback to get the keyword that matched.
-                // TrieSearch could have this built in, but that's slightly more complicated since
-                // the strings are stored as a byte array and embedded in the search tree.
+            for (Map.Entry entry : keywords.entrySet()) {
+                String keyword = entry.getKey();
+                //noinspection ExtractMethodRecommender
+                final boolean isWholeWord = entry.getValue();
+
                 TrieSearch.TriePatternMatchedCallback callback =
-                        (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
+                        (textSearched, startIndex, matchLength, callbackParameter) -> {
+                            if (isWholeWord && !keywordMatchIsWholeWord(textSearched, startIndex, matchLength)) {
+                                return false;
+                            }
+
+                            Logger.printDebug(() -> (isWholeWord ? "Matched whole keyword: '"
+                                    : "Matched keyword: '") + keyword + "'");
                             // noinspection unchecked
                             ((MutableReference) callbackParameter).value = keyword;
                             return true;
@@ -295,7 +481,7 @@ final class KeywordContentFilter extends Filter {
                 search.addPattern(stringBytes, callback);
             }
 
-            Logger.printDebug(() -> "Search using: (" + search.getEstimatedMemorySize() + " KB) keywords: " + keywords);
+            Logger.printDebug(() -> "Search using: (" + search.getEstimatedMemorySize() + " KB) keywords: " + keywords.keySet());
         }
 
         bufferSearch = search;
@@ -382,7 +568,7 @@ final class KeywordContentFilter extends Filter {
         // Field is intentionally compared using reference equality.
         //noinspection StringEquality
         if (Settings.HIDE_KEYWORD_CONTENT_PHRASES.get() != lastKeywordPhrasesParsed) {
-            // User changed the keywords.
+            // User changed the keywords or whole word setting.
             parseKeywords();
         }
 
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java
new file mode 100644
index 00000000..96d29645
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java
@@ -0,0 +1,35 @@
+package app.revanced.integrations.youtube.settings.preference;
+
+import static android.text.Html.FROM_HTML_MODE_COMPACT;
+
+import android.content.Context;
+import android.os.Build;
+import android.preference.Preference;
+import android.text.Html;
+import android.util.AttributeSet;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * Allows using basic html for the summary text.
+ */
+@SuppressWarnings({"unused", "deprecation"})
+@RequiresApi(api = Build.VERSION_CODES.O)
+public class HtmlPreference extends Preference {
+    {
+        setSummary(Html.fromHtml(getSummary().toString(), FROM_HTML_MODE_COMPACT));
+    }
+
+    public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+    public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+    public HtmlPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+    public HtmlPreference(Context context) {
+        super(context);
+    }
+}

From 55c278dc08079ac59615792efaeea01789a88abb Mon Sep 17 00:00:00 2001
From: semantic-release-bot 
Date: Fri, 30 Aug 2024 21:41:55 +0000
Subject: [PATCH 08/29] chore(release): 1.14.0-dev.2 [skip ci]

# [1.14.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.1...v1.14.0-dev.2) (2024-08-30)

### Features

* **YouTube - Keyword filter:** Add syntax to match whole keywords and not substrings ([#681](https://github.com/ReVanced/revanced-integrations/issues/681)) ([5314dd9](https://github.com/ReVanced/revanced-integrations/commit/5314dd90d16dc8565331c4cddce114956d85a173))
---
 CHANGELOG.md      | 7 +++++++
 gradle.properties | 2 +-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd99c70f..25a25829 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [1.14.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.1...v1.14.0-dev.2) (2024-08-30)
+
+
+### Features
+
+* **YouTube - Keyword filter:** Add syntax to match whole keywords and not substrings ([#681](https://github.com/ReVanced/revanced-integrations/issues/681)) ([5314dd9](https://github.com/ReVanced/revanced-integrations/commit/5314dd90d16dc8565331c4cddce114956d85a173))
+
 # [1.14.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.13.1-dev.2...v1.14.0-dev.1) (2024-08-22)
 
 
diff --git a/gradle.properties b/gradle.properties
index 96e93f7c..3aa90221 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
 org.gradle.parallel = true
 org.gradle.caching = true
 android.useAndroidX = true
-version = 1.14.0-dev.1
+version = 1.14.0-dev.2

From 27d2b60444ff5bcc84a1889e2cacf1750532b6ad Mon Sep 17 00:00:00 2001
From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
Date: Sun, 1 Sep 2024 17:49:15 -0400
Subject: [PATCH 09/29] fix(YouTube - ReturnYouTubeDislike): Show estimated
 like count for videos with hidden likes (#684)

---
 .../revanced/integrations/shared/Utils.java   |  17 ++
 .../patches/ReturnYouTubeDislikePatch.java    |  81 +++++----
 .../ReturnYouTubeDislikeFilterPatch.java      |  21 ++-
 .../youtube/requests/Requester.java           |   3 +
 .../ReturnYouTubeDislike.java                 | 162 ++++++++++--------
 .../requests/RYDVoteData.java                 |  90 ++++++++--
 .../requests/ReturnYouTubeDislikeApi.java     |  19 +-
 7 files changed, 259 insertions(+), 134 deletions(-)

diff --git a/app/src/main/java/app/revanced/integrations/shared/Utils.java b/app/src/main/java/app/revanced/integrations/shared/Utils.java
index 4b13a787..21a97a9a 100644
--- a/app/src/main/java/app/revanced/integrations/shared/Utils.java
+++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java
@@ -363,6 +363,23 @@ public class Utils {
         return isRightToLeftTextLayout;
     }
 
+    /**
+     * @return if the text contains at least 1 number character,
+     *         including any unicode numbers such as Arabic.
+     */
+    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+    public static boolean containsNumber(@NonNull CharSequence text) {
+        for (int index = 0, length = text.length(); index < length;) {
+            final int codePoint = Character.codePointAt(text, index);
+            if (Character.isDigit(codePoint)) {
+                return true;
+            }
+            index += Character.charCount(codePoint);
+        }
+
+        return false;
+    }
+
     /**
      * Safe to call from any thread
      */
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java
index 0fb84829..8aa2c5ca 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java
@@ -225,7 +225,6 @@ public class ReturnYouTubeDislikePatch {
                 return original;
             }
 
-            final CharSequence replacement;
             if (conversionContextString.contains("segmented_like_dislike_button.eml")) {
                 // Regular video.
                 ReturnYouTubeDislike videoData = currentVideoData;
@@ -235,46 +234,62 @@ public class ReturnYouTubeDislikePatch {
                 if (!(original instanceof Spanned)) {
                     original = new SpannableString(original);
                 }
-                replacement = videoData.getDislikesSpanForRegularVideo((Spanned) original,
+                return videoData.getDislikesSpanForRegularVideo((Spanned) original,
                         true, isRollingNumber);
-            } else if (!isRollingNumber && conversionContextString.contains("|shorts_dislike_button.eml|")) {
-                // Litho Shorts player.
-                if (!Settings.RYD_SHORTS.get() || Settings.HIDE_SHORTS_DISLIKE_BUTTON.get()) {
-                    // Must clear the current video here, otherwise if the user opens a regular video
-                    // then opens a litho short (while keeping the regular video on screen), then closes the short,
-                    // the original video may show the incorrect dislike value.
-                    clearData();
-                    return original;
-                }
-                ReturnYouTubeDislike videoData = lastLithoShortsVideoData;
-                if (videoData == null) {
-                    // The Shorts litho video id filter did not detect the video id.
-                    // This is normal in incognito mode, but otherwise is abnormal.
-                    Logger.printDebug(() -> "Cannot modify Shorts litho span, data is null");
-                    return original;
-                }
-                // Use the correct dislikes data after voting.
-                if (lithoShortsShouldUseCurrentData) {
-                    lithoShortsShouldUseCurrentData = false;
-                    videoData = currentVideoData;
-                    if (videoData == null) {
-                        Logger.printException(() -> "currentVideoData is null"); // Should never happen
-                        return original;
-                    }
-                    Logger.printDebug(() -> "Using current video data for litho span");
-                }
-                replacement = videoData.getDislikeSpanForShort((Spanned) original);
-            } else {
-                return original;
             }
 
-            return replacement;
+            if (isRollingNumber) {
+                return original; // No need to check for Shorts in the context.
+            }
+
+            if (conversionContextString.contains("|shorts_dislike_button.eml")) {
+                return getShortsSpan(original, true);
+            }
+
+            if (conversionContextString.contains("|shorts_like_button.eml")
+                    && !Utils.containsNumber(original)) {
+                Logger.printDebug(() -> "Replacing hidden likes count");
+                return getShortsSpan(original, false);
+            }
         } catch (Exception ex) {
             Logger.printException(() -> "onLithoTextLoaded failure", ex);
         }
         return original;
     }
 
+    private static CharSequence getShortsSpan(@NonNull CharSequence original, boolean isDislikesSpan) {
+        // Litho Shorts player.
+        if (!Settings.RYD_SHORTS.get() || (isDislikesSpan && Settings.HIDE_SHORTS_DISLIKE_BUTTON.get())
+                || (!isDislikesSpan && Settings.HIDE_SHORTS_LIKE_BUTTON.get())) {
+            return original;
+        }
+
+        ReturnYouTubeDislike videoData = lastLithoShortsVideoData;
+        if (videoData == null) {
+            // The Shorts litho video id filter did not detect the video id.
+            // This is normal in incognito mode, but otherwise is abnormal.
+            Logger.printDebug(() -> "Cannot modify Shorts litho span, data is null");
+            return original;
+        }
+
+        // Use the correct dislikes data after voting.
+        if (lithoShortsShouldUseCurrentData) {
+            if (isDislikesSpan) {
+                lithoShortsShouldUseCurrentData = false;
+            }
+            videoData = currentVideoData;
+            if (videoData == null) {
+                Logger.printException(() -> "currentVideoData is null"); // Should never happen
+                return original;
+            }
+            Logger.printDebug(() -> "Using current video data for litho span");
+        }
+
+        return isDislikesSpan
+                ? videoData.getDislikeSpanForShort((Spanned) original)
+                : videoData.getLikeSpanForShort((Spanned) original);
+    }
+
     //
     // Rolling Number
     //
@@ -597,6 +612,7 @@ public class ReturnYouTubeDislikePatch {
                 Logger.printDebug(() -> "Waiting for prefetch to complete: " + videoId);
                 fetch.getFetchData(20000); // Any arbitrarily large max wait time.
             }
+
             // Set the fields after the fetch completes, so any concurrent calls will also wait.
             lastPlayerResponseWasShort = videoIdIsShort;
             lastPrefetchedVideoId = videoId;
@@ -657,6 +673,7 @@ public class ReturnYouTubeDislikePatch {
             clearData();
             return;
         }
+
         Logger.printDebug(() -> "New litho Shorts video id: " + videoId);
         ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(videoId);
         videoData.setVideoIdIsShort(true);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java
index 927e4493..11bffcc5 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java
@@ -52,7 +52,7 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
     @SuppressWarnings("unused")
     public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) {
         try {
-            if (!isShortAndOpeningOrPlaying || !Settings.RYD_SHORTS.get()) {
+            if (!isShortAndOpeningOrPlaying || !Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
                 return;
             }
             synchronized (lastVideoIds) {
@@ -68,21 +68,28 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
     private final ByteArrayFilterGroupList videoIdFilterGroup = new ByteArrayFilterGroupList();
 
     public ReturnYouTubeDislikeFilterPatch() {
+        // Likes always seems to load before the dislikes, but if this
+        // ever changes then both likes and dislikes need callbacks.
         addPathCallbacks(
-                new StringFilterGroup(Settings.RYD_SHORTS, "|shorts_dislike_button.eml|")
+                new StringFilterGroup(null, "|shorts_like_button.eml")
         );
-        // After the dislikes icon name is some binary data and then the video id for that specific short.
+
+        // After the likes icon name is some binary data and then the video id for that specific short.
         videoIdFilterGroup.addAll(
-                // Video was previously disliked before video was opened.
-                new ByteArrayFilterGroup(null, "ic_right_dislike_on_shadowed"),
-                // Video was not already disliked.
-                new ByteArrayFilterGroup(null, "ic_right_dislike_off_shadowed")
+                // Video was previously liked before video was opened.
+                new ByteArrayFilterGroup(null, "ic_right_like_on_shadowed"),
+                // Video was not already liked.
+                new ByteArrayFilterGroup(null, "ic_right_like_off_shadowed")
         );
     }
 
     @Override
     boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
                        StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
+        if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
+            return false;
+        }
+
         FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray);
         if (result.isFiltered()) {
             String matchedVideoId = findVideoId(protobufBufferArray);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java b/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java
index ef409b52..c62e34f5 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java
@@ -23,6 +23,9 @@ public class Requester {
     public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
         String url = apiUrl + route.getCompiledRoute();
         HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+        // Request data is in the URL parameters and no body is sent.
+        // The calling code must set a length if using a request body.
+        connection.setFixedLengthStreamingMode(0);
         connection.setRequestMethod(route.getMethod().name());
         String agentString = System.getProperty("http.agent")
                 + "; ReVanced/" + Utils.getAppVersionName()
diff --git a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
index bfff1b15..b63d0484 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
@@ -10,6 +10,9 @@ import android.graphics.drawable.ShapeDrawable;
 import android.graphics.drawable.shapes.OvalShape;
 import android.graphics.drawable.shapes.RectShape;
 import android.icu.text.CompactDecimalFormat;
+import android.icu.text.DecimalFormat;
+import android.icu.text.DecimalFormatSymbols;
+import android.icu.text.NumberFormat;
 import android.os.Build;
 import android.text.Spannable;
 import android.text.SpannableString;
@@ -25,17 +28,11 @@ import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import java.text.NumberFormat;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
+import java.util.concurrent.*;
 
 import app.revanced.integrations.shared.Logger;
 import app.revanced.integrations.shared.Utils;
@@ -223,32 +220,29 @@ public class ReturnYouTubeDislike {
 
         // Note: Some locales use right to left layout (Arabic, Hebrew, etc).
         // If making changes to this code, change device settings to a RTL language and verify layout is correct.
-        String oldLikesString = oldSpannable.toString();
+        CharSequence oldLikes = oldSpannable;
 
         // YouTube creators can hide the like count on a video,
         // and the like count appears as a device language specific string that says 'Like'.
         // Check if the string contains any numbers.
-        if (!stringContainsNumber(oldLikesString)) {
-            // Likes are hidden.
-            // RYD does not provide usable data for these types of videos,
-            // and the API returns bogus data (zero likes and zero dislikes)
-            // discussion about this: https://github.com/Anarios/return-youtube-dislike/discussions/530
+        if (!Utils.containsNumber(oldLikes)) {
+            // Likes are hidden by video creator
+            //
+            // RYD does not directly provide like data, but can use an estimated likes
+            // using the same scale factor RYD applied to the raw dislikes.
             //
             // example video: https://www.youtube.com/watch?v=UnrU5vxCHxw
             // RYD data: https://returnyoutubedislikeapi.com/votes?videoId=UnrU5vxCHxw
             //
-            // Change the "Likes" string to show that likes and dislikes are hidden.
-            String hiddenMessageString = str("revanced_ryd_video_likes_hidden_by_video_owner");
-            return newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString);
+            Logger.printDebug(() -> "Using estimated likes");
+            oldLikes = formatDislikeCount(voteData.getLikeCount());
         }
 
         SpannableStringBuilder builder = new SpannableStringBuilder();
         final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get();
 
         if (!compactLayout) {
-            String leftSeparatorString = Utils.isRightToLeftTextLayout()
-                    ? "\u200F"  // u200F = right to left character
-                    : "\u200E"; // u200E = left to right character
+            String leftSeparatorString = getTextDirectionString();
             final Spannable leftSeparatorSpan;
             if (isRollingNumber) {
                  leftSeparatorSpan = new SpannableString(leftSeparatorString);
@@ -267,7 +261,7 @@ public class ReturnYouTubeDislike {
         }
 
         // likes
-        builder.append(newSpanUsingStylingOfAnotherSpan(oldSpannable, oldLikesString));
+        builder.append(newSpanUsingStylingOfAnotherSpan(oldSpannable, oldLikes));
 
         // middle separator
         String middleSeparatorString = compactLayout
@@ -292,6 +286,12 @@ public class ReturnYouTubeDislike {
         return new SpannableString(builder);
     }
 
+    private static @NonNull String getTextDirectionString() {
+        return Utils.isRightToLeftTextLayout()
+                ? "\u200F"  // u200F = right to left character
+                : "\u200E"; // u200E = left to right character
+    }
+
     /**
      * @return If the text is likely for a previously created likes/dislikes segmented span.
      */
@@ -299,20 +299,6 @@ public class ReturnYouTubeDislike {
         return text.indexOf(MIDDLE_SEPARATOR_CHARACTER) >= 0;
     }
 
-    /**
-     * Correctly handles any unicode numbers (such as Arabic numbers).
-     *
-     * @return if the string contains at least 1 number.
-     */
-    private static boolean stringContainsNumber(@NonNull String text) {
-        for (int index = 0, length = text.length(); index < length; index++) {
-            if (Character.isDigit(text.codePointAt(index))) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     private static boolean spansHaveEqualTextAndColor(@NonNull Spanned one, @NonNull Spanned two) {
         // Cannot use equals on the span, because many of the inner styling spans do not implement equals.
         // Instead, compare the underlying text and the text color to handle when dark mode is changed.
@@ -334,6 +320,10 @@ public class ReturnYouTubeDislike {
         return true;
     }
 
+    private static SpannableString newSpannableWithLikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) {
+        return newSpanUsingStylingOfAnotherSpan(sourceStyling, formatDislikeCount(voteData.getLikeCount()));
+    }
+
     private static SpannableString newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) {
         return newSpanUsingStylingOfAnotherSpan(sourceStyling,
                 Settings.RYD_DISLIKE_PERCENTAGE.get()
@@ -342,11 +332,16 @@ public class ReturnYouTubeDislike {
     }
 
     private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) {
+        if (sourceStyle == newSpanText && sourceStyle instanceof SpannableString) {
+            return (SpannableString) sourceStyle; // Nothing to do.
+        }
+
         SpannableString destination = new SpannableString(newSpanText);
         Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class);
         for (Object span : spans) {
             destination.setSpan(span, 0, destination.length(), sourceStyle.getSpanFlags(span));
         }
+
         return destination;
     }
 
@@ -354,13 +349,18 @@ public class ReturnYouTubeDislike {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
             synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
                 if (dislikeCountFormatter == null) {
-                    // Note: Java number formatters will use the locale specific number characters.
-                    // such as Arabic which formats "1.234" into "Û±,Û²Û³Ù¤"
-                    // But YouTube disregards locale specific number characters
-                    // and instead shows english number characters everywhere.
                     Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale;
-                    Logger.printDebug(() -> "Locale: " + locale);
                     dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
+
+                    // YouTube disregards locale specific number characters
+                    // and instead shows english number characters everywhere.
+                    // To use the same behavior, override the digit characters to use English
+                    // so languages such as Arabic will show "1.234" instead of the native "Û±,Û²Û³Ù¤"
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                        DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
+                        symbols.setDigitStrings(DecimalFormatSymbols.getInstance(Locale.ENGLISH).getDigitStrings());
+                        dislikeCountFormatter.setDecimalFormatSymbols(symbols);
+                    }
                 }
                 return dislikeCountFormatter.format(dislikeCount);
             }
@@ -371,19 +371,31 @@ public class ReturnYouTubeDislike {
     }
 
     private static String formatDislikePercentage(float dislikePercentage) {
-        synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
-            if (dislikePercentageFormatter == null) {
-                Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale;
-                Logger.printDebug(() -> "Locale: " + locale);
-                dislikePercentageFormatter = NumberFormat.getPercentInstance(locale);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
+                if (dislikePercentageFormatter == null) {
+                    Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale;
+                    dislikePercentageFormatter = NumberFormat.getPercentInstance(locale);
+
+                    // Want to set the digit strings, and the simplest way is to cast to the implementation NumberFormat returns.
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
+                            && dislikePercentageFormatter instanceof DecimalFormat) {
+                        DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
+                        symbols.setDigitStrings(DecimalFormatSymbols.getInstance(Locale.ENGLISH).getDigitStrings());
+                        ((DecimalFormat) dislikePercentageFormatter).setDecimalFormatSymbols(symbols);
+                    }
+                }
+                if (dislikePercentage >= 0.01) { // at least 1%
+                    dislikePercentageFormatter.setMaximumFractionDigits(0); // show only whole percentage points
+                } else {
+                    dislikePercentageFormatter.setMaximumFractionDigits(1); // show up to 1 digit precision
+                }
+                return dislikePercentageFormatter.format(dislikePercentage);
             }
-            if (dislikePercentage >= 0.01) { // at least 1%
-                dislikePercentageFormatter.setMaximumFractionDigits(0); // show only whole percentage points
-            } else {
-                dislikePercentageFormatter.setMaximumFractionDigits(1); // show up to 1 digit precision
-            }
-            return dislikePercentageFormatter.format(dislikePercentage);
         }
+
+        // Will never be reached, as the oldest supported YouTube app requires Android N or greater.
+        return String.valueOf((int) (dislikePercentage * 100));
     }
 
     @NonNull
@@ -484,7 +496,17 @@ public class ReturnYouTubeDislike {
     public synchronized Spanned getDislikesSpanForRegularVideo(@NonNull Spanned original,
                                                                boolean isSegmentedButton,
                                                                boolean isRollingNumber) {
-        return waitForFetchAndUpdateReplacementSpan(original, isSegmentedButton, isRollingNumber,false);
+        return waitForFetchAndUpdateReplacementSpan(original, isSegmentedButton,
+                isRollingNumber, false, false);
+    }
+
+    /**
+     * Called when a Shorts like Spannable is created.
+     */
+    @NonNull
+    public synchronized Spanned getLikeSpanForShort(@NonNull Spanned original) {
+        return waitForFetchAndUpdateReplacementSpan(original, false,
+                false, true, true);
     }
 
     /**
@@ -492,14 +514,16 @@ public class ReturnYouTubeDislike {
      */
     @NonNull
     public synchronized Spanned getDislikeSpanForShort(@NonNull Spanned original) {
-        return waitForFetchAndUpdateReplacementSpan(original, false, false, true);
+        return waitForFetchAndUpdateReplacementSpan(original, false,
+                false, true, false);
     }
 
     @NonNull
     private Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned original,
                                                          boolean isSegmentedButton,
                                                          boolean isRollingNumber,
-                                                         boolean spanIsForShort) {
+                                                         boolean spanIsForShort,
+                                                         boolean spanIsForLikes) {
         try {
             RYDVoteData votingData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH);
             if (votingData == null) {
@@ -526,24 +550,17 @@ public class ReturnYouTubeDislike {
                     return original;
                 }
 
-                if (originalDislikeSpan != null && replacementLikeDislikeSpan != null) {
-                    if (spansHaveEqualTextAndColor(original, replacementLikeDislikeSpan)) {
-                        Logger.printDebug(() -> "Ignoring previously created dislikes span of data: " + videoId);
-                        return original;
-                    }
-                    if (spansHaveEqualTextAndColor(original, originalDislikeSpan)) {
-                        Logger.printDebug(() -> "Replacing span with previously created dislike span of data: " + videoId);
-                        return replacementLikeDislikeSpan;
-                    }
+                if (spanIsForLikes) {
+                    // Scrolling Shorts does not cause the Spans to be reloaded,
+                    // so there is no need to cache the likes for this situations.
+                    Logger.printDebug(() -> "Creating likes span for: " + votingData.videoId);
+                    return newSpannableWithLikes(original, votingData);
                 }
-                if (isSegmentedButton && isPreviouslyCreatedSegmentedSpan(original.toString())) {
-                    // need to recreate using original, as original has prior outdated dislike values
-                    if (originalDislikeSpan == null) {
-                        // Should never happen.
-                        Logger.printDebug(() -> "Cannot add dislikes - original span is null. videoId: " + videoId);
-                        return original;
-                    }
-                    original = originalDislikeSpan;
+
+                if (originalDislikeSpan != null && replacementLikeDislikeSpan != null
+                        && spansHaveEqualTextAndColor(original, originalDislikeSpan)) {
+                    Logger.printDebug(() -> "Replacing span with previously created dislike span of data: " + videoId);
+                    return replacementLikeDislikeSpan;
                 }
 
                 // No replacement span exist, create it now.
@@ -558,9 +575,10 @@ public class ReturnYouTubeDislike {
 
                 return replacementLikeDislikeSpan;
             }
-        } catch (Exception e) {
-            Logger.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", e); // should never happen
+        } catch (Exception ex) {
+            Logger.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", ex);
         }
+
         return original;
     }
 
diff --git a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/RYDVoteData.java b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/RYDVoteData.java
index 820c0492..239ad2b0 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/RYDVoteData.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/RYDVoteData.java
@@ -3,10 +3,13 @@ package app.revanced.integrations.youtube.returnyoutubedislike.requests;
 import static app.revanced.integrations.youtube.returnyoutubedislike.ReturnYouTubeDislike.Vote;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import app.revanced.integrations.shared.Logger;
+
 /**
  * ReturnYouTubeDislike API estimated like/dislike/view counts.
  *
@@ -23,38 +26,65 @@ public final class RYDVoteData {
     public final long viewCount;
 
     private final long fetchedLikeCount;
-    private volatile long likeCount; // read/write from different threads
+    private volatile long likeCount; // Read/write from different threads.
+    /**
+     * Like count can be hidden by video creator, but RYD still tracks the number
+     * of like/dislikes it received thru it's browser extension and and API.
+     * The raw like/dislikes can be used to calculate a percentage.
+     *
+     * Raw values can be null, especially for older videos with little to no views.
+     */
+    @Nullable
+    private final Long fetchedRawLikeCount;
     private volatile float likePercentage;
 
     private final long fetchedDislikeCount;
-    private volatile long dislikeCount; // read/write from different threads
+    private volatile long dislikeCount; // Read/write from different threads.
+    @Nullable
+    private final Long fetchedRawDislikeCount;
     private volatile float dislikePercentage;
 
+    @Nullable
+    private static Long getLongIfExist(JSONObject json, String key) throws JSONException {
+        return json.isNull(key)
+                ? null
+                : json.getLong(key);
+    }
+
     /**
      * @throws JSONException if JSON parse error occurs, or if the values make no sense (ie: negative values)
      */
     public RYDVoteData(@NonNull JSONObject json) throws JSONException {
         videoId = json.getString("id");
         viewCount = json.getLong("viewCount");
+
         fetchedLikeCount = json.getLong("likes");
+        fetchedRawLikeCount = getLongIfExist(json, "rawLikes");
+
         fetchedDislikeCount = json.getLong("dislikes");
+        fetchedRawDislikeCount = getLongIfExist(json, "rawDislikes");
+
         if (viewCount < 0 || fetchedLikeCount < 0 || fetchedDislikeCount < 0) {
             throw new JSONException("Unexpected JSON values: " + json);
         }
         likeCount = fetchedLikeCount;
         dislikeCount = fetchedDislikeCount;
-        updatePercentages();
+
+        updateUsingVote(Vote.LIKE_REMOVE); // Calculate percentages.
     }
 
     /**
-     * Estimated like count
+     * Public like count of the video, as reported by YT when RYD last updated it's data.
+     *
+     * If the likes were hidden by the video creator, then this returns an
+     * estimated likes using the same extrapolation as the dislikes.
      */
     public long getLikeCount() {
         return likeCount;
     }
 
     /**
-     * Estimated dislike count
+     * Estimated total dislike count, extrapolated from the public like count using RYD data.
      */
     public long getDislikeCount() {
         return dislikeCount;
@@ -79,28 +109,56 @@ public final class RYDVoteData {
     }
 
     public void updateUsingVote(Vote vote) {
+        final int likesToAdd, dislikesToAdd;
+
         switch (vote) {
             case LIKE:
-                likeCount = fetchedLikeCount + 1;
-                dislikeCount = fetchedDislikeCount;
+                likesToAdd = 1;
+                dislikesToAdd = 0;
                 break;
             case DISLIKE:
-                likeCount = fetchedLikeCount;
-                dislikeCount = fetchedDislikeCount + 1;
+                likesToAdd = 0;
+                dislikesToAdd = 1;
                 break;
             case LIKE_REMOVE:
-                likeCount = fetchedLikeCount;
-                dislikeCount = fetchedDislikeCount;
+                likesToAdd = 0;
+                dislikesToAdd = 0;
                 break;
             default:
                 throw new IllegalStateException();
         }
-        updatePercentages();
-    }
 
-    private void updatePercentages() {
-        likePercentage = (likeCount == 0 ? 0 : (float) likeCount / (likeCount + dislikeCount));
-        dislikePercentage = (dislikeCount == 0 ? 0 : (float) dislikeCount / (likeCount + dislikeCount));
+        // If a video has no public likes but RYD has raw like data,
+        // then use the raw data instead.
+        final boolean videoHasNoPublicLikes = fetchedLikeCount == 0;
+        final boolean hasRawData = fetchedRawLikeCount != null && fetchedRawDislikeCount != null;
+
+        if (videoHasNoPublicLikes && hasRawData && fetchedRawDislikeCount > 0) {
+            // YT creator has hidden the likes count, and this is an older video that
+            // RYD does not provide estimated like counts.
+            //
+            // But we can calculate the public likes the same way RYD does for newer videos with hidden likes,
+            // by using the same raw to estimated scale factor applied to dislikes.
+            // This calculation exactly matches the public likes RYD provides for newer hidden videos.
+            final float estimatedRawScaleFactor = (float) fetchedDislikeCount / fetchedRawDislikeCount;
+            likeCount = (long) (estimatedRawScaleFactor * fetchedRawLikeCount) + likesToAdd;
+            Logger.printDebug(() -> "Using locally calculated estimated likes since RYD did not return an estimate");
+        } else {
+            likeCount = fetchedLikeCount + likesToAdd;
+        }
+        // RYD now always returns an estimated dislike count, even if the likes are hidden.
+        dislikeCount = fetchedDislikeCount + dislikesToAdd;
+
+        // Update percentages.
+
+        final float totalCount = likeCount + dislikeCount;
+        if (totalCount == 0) {
+            likePercentage = 0;
+            dislikePercentage = 0;
+        } else {
+            likePercentage = likeCount / totalCount;
+            dislikePercentage = dislikeCount / totalCount;
+        }
     }
 
     @NonNull
diff --git a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java
index bc729e47..cb211ea5 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java
@@ -197,7 +197,7 @@ public class ReturnYouTubeDislikeApi {
         return httpResponseCode == HTTP_STATUS_CODE_RATE_LIMIT;
     }
 
-    @SuppressWarnings("NonAtomicOperationOnVolatileField") // Don't care, fields are estimates.
+    @SuppressWarnings("NonAtomicOperationOnVolatileField") // Don't care, fields are only estimates.
     private static void updateRateLimitAndStats(long timeNetworkCallStarted, boolean connectionError, boolean rateLimitHit) {
         if (connectionError && rateLimitHit) {
             throw new IllegalArgumentException();
@@ -368,10 +368,12 @@ public class ReturnYouTubeDislikeApi {
             applyCommonPostRequestSettings(connection);
 
             String jsonInputString = "{\"solution\": \"" + solution + "\"}";
+            byte[] body = jsonInputString.getBytes(StandardCharsets.UTF_8);
+            connection.setFixedLengthStreamingMode(body.length);
             try (OutputStream os = connection.getOutputStream()) {
-                byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
-                os.write(input, 0, input.length);
+                os.write(body);
             }
+
             final int responseCode = connection.getResponseCode();
             if (checkIfRateLimitWasHit(responseCode)) {
                 connection.disconnect(); // disconnect, as no more connections will be made for a little while
@@ -440,9 +442,10 @@ public class ReturnYouTubeDislikeApi {
             applyCommonPostRequestSettings(connection);
 
             String voteJsonString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote.value + "\"}";
+            byte[] body = voteJsonString.getBytes(StandardCharsets.UTF_8);
+            connection.setFixedLengthStreamingMode(body.length);
             try (OutputStream os = connection.getOutputStream()) {
-                byte[] input = voteJsonString.getBytes(StandardCharsets.UTF_8);
-                os.write(input, 0, input.length);
+                os.write(body);
             }
 
             final int responseCode = connection.getResponseCode();
@@ -490,10 +493,12 @@ public class ReturnYouTubeDislikeApi {
             applyCommonPostRequestSettings(connection);
 
             String jsonInputString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}";
+            byte[] body = jsonInputString.getBytes(StandardCharsets.UTF_8);
+            connection.setFixedLengthStreamingMode(body.length);
             try (OutputStream os = connection.getOutputStream()) {
-                byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
-                os.write(input, 0, input.length);
+                os.write(body);
             }
+
             final int responseCode = connection.getResponseCode();
             if (checkIfRateLimitWasHit(responseCode)) {
                 connection.disconnect(); // disconnect, as no more connections will be made for a little while

From a35dfe8ea3d69bb7e9a6b39b7c591c10ba4016e8 Mon Sep 17 00:00:00 2001
From: semantic-release-bot 
Date: Sun, 1 Sep 2024 21:52:14 +0000
Subject: [PATCH 10/29] chore(release): 1.14.0-dev.3 [skip ci]

# [1.14.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.2...v1.14.0-dev.3) (2024-09-01)

### Bug Fixes

* **YouTube - ReturnYouTubeDislike:** Show estimated like count for videos with hidden likes ([#684](https://github.com/ReVanced/revanced-integrations/issues/684)) ([27d2b60](https://github.com/ReVanced/revanced-integrations/commit/27d2b60444ff5bcc84a1889e2cacf1750532b6ad))
---
 CHANGELOG.md      | 7 +++++++
 gradle.properties | 2 +-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 25a25829..9785fed9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [1.14.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.2...v1.14.0-dev.3) (2024-09-01)
+
+
+### Bug Fixes
+
+* **YouTube - ReturnYouTubeDislike:** Show estimated like count for videos with hidden likes ([#684](https://github.com/ReVanced/revanced-integrations/issues/684)) ([27d2b60](https://github.com/ReVanced/revanced-integrations/commit/27d2b60444ff5bcc84a1889e2cacf1750532b6ad))
+
 # [1.14.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.1...v1.14.0-dev.2) (2024-08-30)
 
 
diff --git a/gradle.properties b/gradle.properties
index 3aa90221..912885f5 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
 org.gradle.parallel = true
 org.gradle.caching = true
 android.useAndroidX = true
-version = 1.14.0-dev.2
+version = 1.14.0-dev.3

From a4848be653fae3e03972254fe48a7b76e561e5a6 Mon Sep 17 00:00:00 2001
From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
Date: Sun, 1 Sep 2024 18:03:43 -0400
Subject: [PATCH 11/29] fix(YouTube - GmsCore support): Show an error toast if
 GmsCore is included with root mounted installation (#686)

Co-authored-by: oSumAtrIX 
---
 .../integrations/shared/GmsCoreSupport.java      | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java b/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java
index 16f0ed0c..a0275fb3 100644
--- a/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java
+++ b/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java
@@ -24,6 +24,7 @@ import java.net.URL;
  * @noinspection unused
  */
 public class GmsCoreSupport {
+    public static final String ORIGINAL_UNPATCHED_PACKAGE_NAME = "com.google.android.youtube";
     private static final String GMS_CORE_PACKAGE_NAME
             = getGmsCoreVendorGroupId() + ".android.gms";
     private static final Uri GMS_CORE_PROVIDER
@@ -73,6 +74,21 @@ public class GmsCoreSupport {
     @RequiresApi(api = Build.VERSION_CODES.N)
     public static void checkGmsCore(Activity context) {
         try {
+            // Verify the user has not included GmsCore for a root installation.
+            // GmsCore Support changes the package name, but with a mounted installation
+            // all manifest changes are ignored and the original package name is used.
+            if (context.getPackageName().equals(ORIGINAL_UNPATCHED_PACKAGE_NAME)) {
+                Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
+                // Cannot use localize text here, since the app will load
+                // resources from the unpatched app and all patch strings are missing.
+                Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
+
+                // Do not exit. If the app exits before launch completes (and without
+                // opening another activity), then on some devices such as Pixel phone Android 10
+                // no toast will be shown and the app will continually be relaunched
+                // with the appearance of a hung app.
+            }
+
             // Verify GmsCore is installed.
             try {
                 PackageManager manager = context.getPackageManager();

From a324b16096096854b0fbefb402bf851b09a3b118 Mon Sep 17 00:00:00 2001
From: semantic-release-bot 
Date: Sun, 1 Sep 2024 22:06:37 +0000
Subject: [PATCH 12/29] chore(release): 1.14.0-dev.4 [skip ci]

# [1.14.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.3...v1.14.0-dev.4) (2024-09-01)

### Bug Fixes

* **YouTube - GmsCore support:** Show an error toast if GmsCore is included with root mounted installation ([#686](https://github.com/ReVanced/revanced-integrations/issues/686)) ([a4848be](https://github.com/ReVanced/revanced-integrations/commit/a4848be653fae3e03972254fe48a7b76e561e5a6))
---
 CHANGELOG.md      | 7 +++++++
 gradle.properties | 2 +-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9785fed9..0ee68cb6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [1.14.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.3...v1.14.0-dev.4) (2024-09-01)
+
+
+### Bug Fixes
+
+* **YouTube - GmsCore support:** Show an error toast if GmsCore is included with root mounted installation ([#686](https://github.com/ReVanced/revanced-integrations/issues/686)) ([a4848be](https://github.com/ReVanced/revanced-integrations/commit/a4848be653fae3e03972254fe48a7b76e561e5a6))
+
 # [1.14.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.2...v1.14.0-dev.3) (2024-09-01)
 
 
diff --git a/gradle.properties b/gradle.properties
index 912885f5..2210a22b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
 org.gradle.parallel = true
 org.gradle.caching = true
 android.useAndroidX = true
-version = 1.14.0-dev.3
+version = 1.14.0-dev.4

From e85645528336162e16acf89f7b9f029762972c72 Mon Sep 17 00:00:00 2001
From: oSumAtrIX 
Date: Fri, 6 Sep 2024 12:27:02 +0400
Subject: [PATCH 13/29] feat: Add `Check environment` patch (#683)

Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
---
 .../integrations/shared/GmsCoreSupport.java   |  21 +-
 .../revanced/integrations/shared/Logger.java  |  31 +-
 .../revanced/integrations/shared/Utils.java   |  81 ++++
 .../integrations/shared/checks/Check.java     | 164 ++++++++
 .../shared/checks/CheckEnvironmentPatch.java  | 369 ++++++++++++++++++
 .../integrations/shared/checks/PatchInfo.java |  33 ++
 ...WatchHistoryDomainNameResolutionPatch.java |   8 +-
 .../announcements/AnnouncementsPatch.java     |  15 +-
 .../youtube/settings/Settings.java            |  27 +-
 9 files changed, 703 insertions(+), 46 deletions(-)
 create mode 100644 app/src/main/java/app/revanced/integrations/shared/checks/Check.java
 create mode 100644 app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java
 create mode 100644 app/src/main/java/app/revanced/integrations/shared/checks/PatchInfo.java

diff --git a/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java b/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java
index a0275fb3..cdd474a9 100644
--- a/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java
+++ b/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java
@@ -54,18 +54,15 @@ public class GmsCoreSupport {
                                                       String dialogMessageRef,
                                                       String positiveButtonStringRef,
                                                       DialogInterface.OnClickListener onPositiveClickListener) {
-        // Use a delay to allow the activity to finish initializing.
-        // Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
-        Utils.runOnMainThreadDelayed(() -> {
-            new AlertDialog.Builder(context)
-                    .setIconAttribute(android.R.attr.alertDialogIcon)
-                    .setTitle(str("gms_core_dialog_title"))
-                    .setMessage(str(dialogMessageRef))
-                    .setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener)
-                    // Allow using back button to skip the action, just in case the check can never be satisfied.
-                    .setCancelable(true)
-                    .show();
-        }, 100);
+        // Do not set cancelable to false, to allow using back button to skip the action,
+        // just in case the check can never be satisfied.
+        var dialog = new AlertDialog.Builder(context)
+                .setIconAttribute(android.R.attr.alertDialogIcon)
+                .setTitle(str("gms_core_dialog_title"))
+                .setMessage(str(dialogMessageRef))
+                .setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener)
+                .create();
+        Utils.showDialog(context, dialog);
     }
 
     /**
diff --git a/app/src/main/java/app/revanced/integrations/shared/Logger.java b/app/src/main/java/app/revanced/integrations/shared/Logger.java
index 25885050..b3729ef7 100644
--- a/app/src/main/java/app/revanced/integrations/shared/Logger.java
+++ b/app/src/main/java/app/revanced/integrations/shared/Logger.java
@@ -1,24 +1,21 @@
 package app.revanced.integrations.shared;
 
-import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG;
-import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG_STACKTRACE;
-import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
-
 import android.util.Log;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import app.revanced.integrations.shared.settings.BaseSettings;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
 
-import app.revanced.integrations.shared.settings.BaseSettings;
+import static app.revanced.integrations.shared.settings.BaseSettings.*;
 
 public class Logger {
 
     /**
      * Log messages using lambdas.
      */
+    @FunctionalInterface
     public interface LogMessage {
         @NonNull
         String buildMessageString();
@@ -59,19 +56,33 @@ public class Logger {
      * so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
      */
     public static void printDebug(@NonNull LogMessage message) {
+        printDebug(message, null);
+    }
+
+    /**
+     * Logs debug messages under the outer class name of the code calling this method.
+     * Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
+     * so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
+     */
+    public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) {
         if (DEBUG.get()) {
-            var messageString = message.buildMessageString();
+            String logMessage = message.buildMessageString();
+            String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
 
             if (DEBUG_STACKTRACE.get()) {
-                var builder = new StringBuilder(messageString);
+                var builder = new StringBuilder(logMessage);
                 var sw = new StringWriter();
                 new Throwable().printStackTrace(new PrintWriter(sw));
 
                 builder.append('\n').append(sw);
-                messageString = builder.toString();
+                logMessage = builder.toString();
             }
 
-            Log.d(REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(), messageString);
+            if (ex == null) {
+                Log.d(logTag, logMessage);
+            } else {
+                Log.d(logTag, logMessage, ex);
+            }
         }
     }
 
diff --git a/app/src/main/java/app/revanced/integrations/shared/Utils.java b/app/src/main/java/app/revanced/integrations/shared/Utils.java
index 21a97a9a..22ed1e06 100644
--- a/app/src/main/java/app/revanced/integrations/shared/Utils.java
+++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java
@@ -1,6 +1,10 @@
 package app.revanced.integrations.shared;
 
 import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageInfo;
@@ -8,6 +12,7 @@ import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.preference.Preference;
@@ -380,6 +385,82 @@ public class Utils {
         return false;
     }
 
+    /**
+     * Ignore this class. It must be public to satisfy Android requirement.
+     */
+    @SuppressWarnings("deprecation")
+    public static class DialogFragmentWrapper extends DialogFragment {
+
+        private Dialog dialog;
+        @Nullable
+        private DialogFragmentOnStartAction onStartAction;
+
+        @Override
+        public void onSaveInstanceState(Bundle outState) {
+            // Do not call super method to prevent state saving.
+        }
+
+        @NonNull
+        @Override
+        public Dialog onCreateDialog(Bundle savedInstanceState) {
+            return dialog;
+        }
+
+        @Override
+        public void onStart() {
+            try {
+                super.onStart();
+
+                if (onStartAction != null) {
+                    onStartAction.onStart((AlertDialog) getDialog());
+                }
+            } catch (Exception ex) {
+                Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex);
+            }
+        }
+    }
+
+    /**
+     * Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}.
+     */
+    @FunctionalInterface
+    public interface DialogFragmentOnStartAction {
+        void onStart(AlertDialog dialog);
+    }
+
+    public static void showDialog(Activity activity, AlertDialog dialog) {
+        showDialog(activity, dialog, true, null);
+    }
+
+    /**
+     * Utility method to allow showing an AlertDialog on top of other alert dialogs.
+     * Calling this will always display the dialog on top of all other dialogs
+     * previously called using this method.
+     * 
+ * Be aware the on start action can be called multiple times for some situations, + * such as the user switching apps without dismissing the dialog then switching back to this app. + *
+ * This method is only useful during app startup and multiple patches may show their own dialog, + * and the most important dialog can be called last (using a delay) so it's always on top. + *
+ * For all other situations it's better to not use this method and + * call {@link AlertDialog#show()} on the dialog. + */ + @SuppressWarnings("deprecation") + public static void showDialog(Activity activity, + AlertDialog dialog, + boolean isCancelable, + @Nullable DialogFragmentOnStartAction onStartAction) { + verifyOnMainThread(); + + DialogFragmentWrapper fragment = new DialogFragmentWrapper(); + fragment.dialog = dialog; + fragment.onStartAction = onStartAction; + fragment.setCancelable(isCancelable); + + fragment.show(activity.getFragmentManager(), null); + } + /** * Safe to call from any thread */ diff --git a/app/src/main/java/app/revanced/integrations/shared/checks/Check.java b/app/src/main/java/app/revanced/integrations/shared/checks/Check.java new file mode 100644 index 00000000..a9497d5b --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/checks/Check.java @@ -0,0 +1,164 @@ +package app.revanced.integrations.shared.checks; + +import static android.text.Html.FROM_HTML_MODE_COMPACT; +import static app.revanced.integrations.shared.StringRef.str; +import static app.revanced.integrations.shared.Utils.DialogFragmentOnStartAction; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.text.Html; +import android.widget.Button; + +import androidx.annotation.Nullable; + +import java.util.Collection; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.youtube.settings.Settings; + +abstract class Check { + private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2; + + private static final int SECONDS_BEFORE_SHOWING_IGNORE_BUTTON = 15; + private static final int SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON = 10; + + private static final Uri GOOD_SOURCE = Uri.parse("https://revanced.app"); + + /** + * @return If the check conclusively passed or failed. A null value indicates it neither passed nor failed. + */ + @Nullable + protected abstract Boolean check(); + + protected abstract String failureReason(); + + /** + * Specifies a sorting order for displaying the checks that failed. + * A lower value indicates to show first before other checks. + */ + public abstract int uiSortingValue(); + + /** + * For debugging and development only. + * Forces all checks to be performed and the check failed dialog to be shown. + * Can be enabled by importing settings text with {@link Settings#CHECK_ENVIRONMENT_WARNINGS_ISSUED} + * set to -1. + */ + static boolean debugAlwaysShowWarning() { + final boolean alwaysShowWarning = Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0; + if (alwaysShowWarning) { + Logger.printInfo(() -> "Debug forcing environment check warning to show"); + } + + return alwaysShowWarning; + } + + static boolean shouldRun() { + return Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() + < NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING; + } + + static void disableForever() { + Logger.printInfo(() -> "Environment checks disabled forever"); + + Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE); + } + + @SuppressLint("NewApi") + static void issueWarning(Activity activity, Collection failedChecks) { + final var reasons = new StringBuilder(); + + reasons.append("
    "); + for (var check : failedChecks) { + // Add a non breaking space to fix bullet points spacing issue. + reasons.append("
  •  ").append(check.failureReason()); + } + reasons.append("
"); + + var message = Html.fromHtml( + str("revanced_check_environment_failed_message", reasons.toString()), + FROM_HTML_MODE_COMPACT + ); + + Utils.runOnMainThreadDelayed(() -> { + AlertDialog alert = new AlertDialog.Builder(activity) + .setCancelable(false) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setTitle(str("revanced_check_environment_failed_title")) + .setMessage(message) + .setPositiveButton( + " ", + (dialog, which) -> { + final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activity.startActivity(intent); + + // Shutdown to prevent the user from navigating back to this app, + // which is no longer showing a warning dialog. + activity.finishAffinity(); + System.exit(0); + } + ).setNegativeButton( + " ", + (dialog, which) -> { + // Cleanup data if the user incorrectly imported a huge negative number. + final int current = Math.max(0, Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()); + Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1); + + dialog.dismiss(); + } + ).create(); + + Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() { + boolean hasRun; + @Override + public void onStart(AlertDialog dialog) { + // Only run this once, otherwise if the user changes to a different app + // then changes back, this handler will run again and disable the buttons. + if (hasRun) { + return; + } + hasRun = true; + + var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + openWebsiteButton.setEnabled(false); + + var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); + dismissButton.setEnabled(false); + + getCountdownRunnable(dismissButton, openWebsiteButton).run(); + } + }); + }, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs. + } + + private static Runnable getCountdownRunnable(Button dismissButton, Button openWebsiteButton) { + return new Runnable() { + private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON; + + @Override + public void run() { + Utils.verifyOnMainThread(); + + if (secondsRemaining > 0) { + if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) { + openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button")); + openWebsiteButton.setEnabled(true); + } + + secondsRemaining--; + + Utils.runOnMainThreadDelayed(this, 1000); + } else { + dismissButton.setText(str("revanced_check_environment_dialog_ignore_button")); + dismissButton.setEnabled(true); + } + } + }; + } +} diff --git a/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java b/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java new file mode 100644 index 00000000..a782c7b2 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java @@ -0,0 +1,369 @@ +package app.revanced.integrations.shared.checks; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Base64; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; + +import static app.revanced.integrations.shared.StringRef.str; +import static app.revanced.integrations.shared.checks.Check.debugAlwaysShowWarning; +import static app.revanced.integrations.shared.checks.PatchInfo.Build.*; +import static app.revanced.integrations.shared.checks.PatchInfo.PATCH_TIME; + +/** + * This class is used to check if the app was patched by the user + * and not downloaded pre-patched, because pre-patched apps are difficult to trust. + *
+ * Various indicators help to detect if the app was patched by the user. + */ +@SuppressWarnings("unused") +public final class CheckEnvironmentPatch { + private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning(); + + private enum InstallationType { + /** + * CLI patching, manual installation of a previously patched using adb, + * or root installation if stock app is first installed using adb. + */ + ADB((String) null), + ROOT_MOUNT_ON_APP_STORE("com.android.vending"), + MANAGER("app.revanced.manager.flutter", + "app.revanced.manager", + "app.revanced.manager.debug"); + + @Nullable + static InstallationType installTypeFromPackageName(@Nullable String packageName) { + for (InstallationType type : values()) { + for (String installPackageName : type.packageNames) { + if (Objects.equals(installPackageName, packageName)) { + return type; + } + } + } + + return null; + } + + /** + * Array elements can be null. + */ + final String[] packageNames; + + InstallationType(String... packageNames) { + this.packageNames = packageNames; + } + } + + /** + * Check if the app is installed by the manager, the app store, or through adb/CLI. + *
+ * Does not conclusively + * If the app is installed by the manager or the app store, it is likely, the app was patched using the manager, + * or installed manually via ADB (in the case of ReVanced CLI for example). + *
+ * If the app is not installed by the manager or the app store, then the app was likely downloaded pre-patched + * and installed by the browser or another unknown app. + */ + private static class CheckExpectedInstaller extends Check { + @Nullable + InstallationType installerFound; + + @NonNull + @Override + protected Boolean check() { + final var context = Utils.getContext(); + + final var installerPackageName = + context.getPackageManager().getInstallerPackageName(context.getPackageName()); + + Logger.printInfo(() -> "Installed by: " + installerPackageName); + + installerFound = InstallationType.installTypeFromPackageName(installerPackageName); + final boolean passed = (installerFound != null); + + Logger.printInfo(() -> passed + ? "Apk was not installed from an unknown source" + : "Apk was installed from an unknown source"); + + return passed; + } + + @Override + protected String failureReason() { + return str("revanced_check_environment_manager_not_expected_installer"); + } + + @Override + public int uiSortingValue() { + return -100; // Show first. + } + } + + /** + * Check if the build properties are the same as during the patch. + *
+ * If the build properties are the same as during the patch, it is likely, the app was patched on the same device. + *
+ * If the build properties are different, the app was likely downloaded pre-patched or patched on another device. + */ + private static class CheckWasPatchedOnSameDevice extends Check { + @SuppressLint({"NewApi", "HardwareIds"}) + @Override + protected Boolean check() { + if (PATCH_BOARD.isEmpty()) { + // Did not patch with Manager, and cannot conclusively say where this was from. + Logger.printInfo(() -> "APK does not contain a hardware signature and cannot compare to current device"); + return null; + } + + //noinspection deprecation + final var passed = buildFieldEqualsHash("BOARD", Build.BOARD, PATCH_BOARD) & + buildFieldEqualsHash("BOOTLOADER", Build.BOOTLOADER, PATCH_BOOTLOADER) & + buildFieldEqualsHash("BRAND", Build.BRAND, PATCH_BRAND) & + buildFieldEqualsHash("CPU_ABI", Build.CPU_ABI, PATCH_CPU_ABI) & + buildFieldEqualsHash("CPU_ABI2", Build.CPU_ABI2, PATCH_CPU_ABI2) & + buildFieldEqualsHash("DEVICE", Build.DEVICE, PATCH_DEVICE) & + buildFieldEqualsHash("DISPLAY", Build.DISPLAY, PATCH_DISPLAY) & + buildFieldEqualsHash("FINGERPRINT", Build.FINGERPRINT, PATCH_FINGERPRINT) & + buildFieldEqualsHash("HARDWARE", Build.HARDWARE, PATCH_HARDWARE) & + buildFieldEqualsHash("HOST", Build.HOST, PATCH_HOST) & + buildFieldEqualsHash("ID", Build.ID, PATCH_ID) & + buildFieldEqualsHash("MANUFACTURER", Build.MANUFACTURER, PATCH_MANUFACTURER) & + buildFieldEqualsHash("MODEL", Build.MODEL, PATCH_MODEL) & + buildFieldEqualsHash("ODM_SKU", Build.ODM_SKU, PATCH_ODM_SKU) & + buildFieldEqualsHash("PRODUCT", Build.PRODUCT, PATCH_PRODUCT) & + buildFieldEqualsHash("RADIO", Build.RADIO, PATCH_RADIO) & + buildFieldEqualsHash("SKU", Build.SKU, PATCH_SKU) & + buildFieldEqualsHash("SOC_MANUFACTURER", Build.SOC_MANUFACTURER, PATCH_SOC_MANUFACTURER) & + buildFieldEqualsHash("SOC_MODEL", Build.SOC_MODEL, PATCH_SOC_MODEL) & + buildFieldEqualsHash("TAGS", Build.TAGS, PATCH_TAGS) & + buildFieldEqualsHash("TYPE", Build.TYPE, PATCH_TYPE) & + buildFieldEqualsHash("USER", Build.USER, PATCH_USER); + + Logger.printInfo(() -> passed + ? "Device hardware signature matches current device" + : "Device hardware signature does not match current device"); + + return passed; + } + + @Override + protected String failureReason() { + return str("revanced_check_environment_not_same_patching_device"); + } + + @Override + public int uiSortingValue() { + return 0; // Show in the middle. + } + } + + /** + * Check if the app was installed within the last 30 minutes after being patched. + *
+ * If the app was installed within the last 30 minutes, it is likely, the app was patched by the user. + *
+ * If the app was installed much later than the patch time, it is likely the app was + * downloaded pre-patched or the user waited too long to install the app. + */ + private static class CheckIsNearPatchTime extends Check { + /** + * How soon after patching the app must be first launched. + */ + static final int THRESHOLD_FOR_PATCHING_RECENTLY = 30 * 60 * 1000; // 30 minutes. + + /** + * How soon after installation or updating the app to check the patch time. + * If the install/update is older than this, this entire check is ignored + * to prevent showing any errors if the user clears the app data after installation. + */ + static final int THRESHOLD_FOR_RECENT_INSTALLATION = 12 * 60 * 60 * 1000; // 12 hours. + + static final long DURATION_SINCE_PATCHING = System.currentTimeMillis() - PATCH_TIME; + + @Override + protected Boolean check() { + Logger.printInfo(() -> "Installed: " + (DURATION_SINCE_PATCHING / 1000) + " seconds after patching"); + + // Also verify patched time is not in the future. + if (DURATION_SINCE_PATCHING < 0) { + // Patch time is in the future and clearly wrong. + return false; + } + + if (DURATION_SINCE_PATCHING < THRESHOLD_FOR_PATCHING_RECENTLY) { + // App is recently patched and this installation is new or recently updated. + return true; + } + + // Verify the app install/update is recent, + // to prevent showing errors if the user later clears the app data. + try { + Context context = Utils.getContext(); + PackageManager packageManager = context.getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); + + // Duration since initial install or last update, which ever is sooner. + final long durationSinceInstallUpdate = System.currentTimeMillis() - packageInfo.lastUpdateTime; + Logger.printInfo(() -> "App was installed/updated: " + + (durationSinceInstallUpdate / (60 * 60 * 1000)) + " hours ago"); + + if (durationSinceInstallUpdate > THRESHOLD_FOR_RECENT_INSTALLATION) { + Logger.printInfo(() -> "Ignoring install time check since install/update was over " + + THRESHOLD_FOR_RECENT_INSTALLATION + " hours ago"); + return null; + } + } catch (PackageManager.NameNotFoundException ex) { + Logger.printException(() -> "Package name not found exception", ex); // Will never happen. + } + + // Was patched between 30 minutes and 12 hours ago. + // This can only happen if someone installs the app then waits 30+ minutes to launch, + // or they clear the app data within 12 hours after installation. + return false; + } + + @Override + protected String failureReason() { + if (DURATION_SINCE_PATCHING < 0) { + // Could happen if the user has their device clock incorrectly set in the past, + // but assume that isn't the case and the apk was patched on a device with the wrong system time. + return str("revanced_check_environment_not_near_patch_time_invalid"); + } + + // If patched over 1 day ago, show how old this pre-patched apk is. + // Showing the age can help convey it's better to patch yourself and know it's the latest. + final long oneDay = 24 * 60 * 60 * 1000; + final long daysSincePatching = DURATION_SINCE_PATCHING / oneDay; + if (daysSincePatching > 1) { // Use over 1 day to avoid singular vs plural strings. + return str("revanced_check_environment_not_near_patch_time_days", daysSincePatching); + } + + return str("revanced_check_environment_not_near_patch_time"); + } + + @Override + public int uiSortingValue() { + return 100; // Show last. + } + } + + /** + * Injection point. + */ + public static void check(Activity context) { + // If the warning was already issued twice, or if the check was successful in the past, + // do not run the checks again. + if (!Check.shouldRun() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + Logger.printDebug(() -> "Environment checks are disabled"); + return; + } + + Utils.runOnBackgroundThread(() -> { + try { + Logger.printInfo(() -> "Running environment checks"); + List failedChecks = new ArrayList<>(); + + CheckWasPatchedOnSameDevice sameHardware = new CheckWasPatchedOnSameDevice(); + Boolean hardwareCheckPassed = sameHardware.check(); + if (hardwareCheckPassed != null) { + if (hardwareCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + // Patched on the same device using Manager, + // and no further checks are needed. + Check.disableForever(); + return; + } + + failedChecks.add(sameHardware); + } + + CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime(); + Boolean timeCheckPassed = nearPatchTime.check(); + if (timeCheckPassed != null) { + if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + if (failedChecks.isEmpty()) { + // Recently patched and installed. No further checks are needed. + // Stopping here also prevents showing warnings if patching and installing with Termux. + Check.disableForever(); + return; + } + } else { + failedChecks.add(nearPatchTime); + } + } + + CheckExpectedInstaller installerCheck = new CheckExpectedInstaller(); + // If the installer package is Manager but this code is reached, + // that means it must not be the right Manager otherwise the hardware hash + // signatures would be present and this check would not have run. + final boolean isManagerInstall = installerCheck.installerFound == InstallationType.MANAGER; + if (!installerCheck.check() || isManagerInstall) { + failedChecks.add(installerCheck); + + if (isManagerInstall) { + // If using Manager and reached here, then this must + // have been patched on a different device. + failedChecks.add(sameHardware); + } + } + + if (DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + // Show all failures for debugging layout. + failedChecks = Arrays.asList( + sameHardware, + nearPatchTime, + installerCheck + ); + } + + if (failedChecks.isEmpty()) { + Check.disableForever(); + return; + } + + //noinspection ComparatorCombinators + Collections.sort(failedChecks, (o1, o2) -> o1.uiSortingValue() - o2.uiSortingValue()); + + Check.issueWarning( + context, + failedChecks + ); + } catch (Exception ex) { + Logger.printException(() -> "check failure", ex); + } + }); + } + + private static boolean buildFieldEqualsHash(String buildFieldName, String buildFieldValue, @Nullable String hash) { + try { + final var sha1 = MessageDigest.getInstance("SHA-1") + .digest(buildFieldValue.getBytes(StandardCharsets.UTF_8)); + + // Must be careful to use same base64 encoding Kotlin uses. + String runtimeHash = new String(Base64.encode(sha1, Base64.NO_WRAP), StandardCharsets.ISO_8859_1); + final boolean equals = runtimeHash.equals(hash); + if (!equals) { + Logger.printInfo(() -> "Hashes do not match. " + buildFieldName + ": '" + buildFieldValue + + "' runtimeHash: '" + runtimeHash + "' patchTimeHash: '" + hash + "'"); + } + + return equals; + } catch (NoSuchAlgorithmException ex) { + Logger.printException(() -> "buildFieldEqualsHash failure", ex); // Will never happen. + + return false; + } + } +} diff --git a/app/src/main/java/app/revanced/integrations/shared/checks/PatchInfo.java b/app/src/main/java/app/revanced/integrations/shared/checks/PatchInfo.java new file mode 100644 index 00000000..6ebf4d8f --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/checks/PatchInfo.java @@ -0,0 +1,33 @@ +package app.revanced.integrations.shared.checks; + +// Fields are set by the patch. Do not modify. +// Fields are not final, because the compiler is inlining them. +final class PatchInfo { + static long PATCH_TIME = 0L; + + final static class Build { + static String PATCH_BOARD = ""; + static String PATCH_BOOTLOADER = ""; + static String PATCH_BRAND = ""; + static String PATCH_CPU_ABI = ""; + static String PATCH_CPU_ABI2 = ""; + static String PATCH_DEVICE = ""; + static String PATCH_DISPLAY = ""; + static String PATCH_FINGERPRINT = ""; + static String PATCH_HARDWARE = ""; + static String PATCH_HOST = ""; + static String PATCH_ID = ""; + static String PATCH_MANUFACTURER = ""; + static String PATCH_MODEL = ""; + static String PATCH_ODM_SKU = ""; + static String PATCH_PRODUCT = ""; + static String PATCH_RADIO = ""; + static String PATCH_SERIAL = ""; + static String PATCH_SKU = ""; + static String PATCH_SOC_MANUFACTURER = ""; + static String PATCH_SOC_MODEL = ""; + static String PATCH_TAGS = ""; + static String PATCH_TYPE = ""; + static String PATCH_USER = ""; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java index 48c8fd8c..da294d72 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java @@ -55,7 +55,7 @@ public class CheckWatchHistoryDomainNameResolutionPatch { } Utils.runOnMainThread(() -> { - var alertDialog = new android.app.AlertDialog.Builder(context) + var alert = new android.app.AlertDialog.Builder(context) .setTitle(str("revanced_check_watch_history_domain_name_dialog_title")) .setMessage(Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message"))) .setIconAttribute(android.R.attr.alertDialogIcon) @@ -64,9 +64,9 @@ public class CheckWatchHistoryDomainNameResolutionPatch { }).setNegativeButton(str("revanced_check_watch_history_domain_name_dialog_ignore"), (dialog, which) -> { Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false); dialog.dismiss(); - }) - .setCancelable(false) - .show(); + }).create(); + + Utils.showDialog(context, alert, false, null); }); } catch (Exception ex) { Logger.printException(() -> "checkDnsResolver failure", ex); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch.java index eec599ec..225dc206 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch.java @@ -1,6 +1,7 @@ package app.revanced.integrations.youtube.patches.announcements; import android.app.Activity; +import android.app.AlertDialog; import android.os.Build; import android.text.Html; import android.text.method.LinkMovementMethod; @@ -103,8 +104,6 @@ public final class AnnouncementsPatch { // Do not show the announcement, if the last announcement id is the same as the current one. if (Settings.ANNOUNCEMENT_LAST_ID.get() == id) return; - - int finalId = id; final var finalTitle = title; final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT); @@ -112,7 +111,7 @@ public final class AnnouncementsPatch { Utils.runOnMainThread(() -> { // Show the announcement. - var alertDialog = new android.app.AlertDialog.Builder(context) + var alert = new AlertDialog.Builder(context) .setTitle(finalTitle) .setMessage(finalMessage) .setIcon(finalLevel.icon) @@ -123,11 +122,13 @@ public final class AnnouncementsPatch { dialog.dismiss(); }) .setCancelable(false) - .show(); + .create(); - // Make links clickable. - ((TextView)alertDialog.findViewById(android.R.id.message)) - .setMovementMethod(LinkMovementMethod.getInstance()); + Utils.showDialog(context, alert, false, (AlertDialog dialog) -> { + // Make links clickable. + ((TextView) dialog.findViewById(android.R.id.message)) + .setMovementMethod(LinkMovementMethod.getInstance()); + }); }); } catch (Exception e) { final var message = "Failed to get announcement"; diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 8708d579..5a40ed0f 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -1,18 +1,5 @@ package app.revanced.integrations.youtube.settings; -import static java.lang.Boolean.FALSE; -import static java.lang.Boolean.TRUE; -import static app.revanced.integrations.shared.settings.Setting.*; -import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; -import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1; -import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3; -import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; -import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.settings.*; import app.revanced.integrations.shared.settings.preference.SharedPrefCategory; @@ -24,6 +11,19 @@ import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch; import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch; import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static app.revanced.integrations.shared.settings.Setting.*; +import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; +import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1; +import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3; +import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; +import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + @SuppressWarnings("deprecation") public class Settings extends BaseSettings { // Video @@ -264,6 +264,7 @@ public class Settings extends BaseSettings { public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1); public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false); public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE); + public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false); // Debugging /** From 5b9e0e8ad472fc6719a69b19a7915253bf98d4aa Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 6 Sep 2024 08:30:14 +0000 Subject: [PATCH 14/29] chore(release): 1.14.0-dev.5 [skip ci] # [1.14.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.4...v1.14.0-dev.5) (2024-09-06) ### Features * Add `Check environment` patch ([#683](https://github.com/ReVanced/revanced-integrations/issues/683)) ([e856455](https://github.com/ReVanced/revanced-integrations/commit/e85645528336162e16acf89f7b9f029762972c72)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee68cb6..6d2bc229 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.14.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.4...v1.14.0-dev.5) (2024-09-06) + + +### Features + +* Add `Check environment` patch ([#683](https://github.com/ReVanced/revanced-integrations/issues/683)) ([e856455](https://github.com/ReVanced/revanced-integrations/commit/e85645528336162e16acf89f7b9f029762972c72)) + # [1.14.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.3...v1.14.0-dev.4) (2024-09-01) diff --git a/gradle.properties b/gradle.properties index 2210a22b..8eb114cf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.14.0-dev.4 +version = 1.14.0-dev.5 From b0d82b016eeacca324b906037d1857b81f577b53 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 6 Sep 2024 04:44:14 -0400 Subject: [PATCH 15/29] fix(YouTube - Check environment patch): Use app install/update time instead of current time (#687) --- .../shared/checks/CheckEnvironmentPatch.java | 70 +++++++------------ 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java b/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java index a782c7b2..c75beb34 100644 --- a/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java +++ b/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java @@ -20,7 +20,6 @@ import java.util.*; import static app.revanced.integrations.shared.StringRef.str; import static app.revanced.integrations.shared.checks.Check.debugAlwaysShowWarning; import static app.revanced.integrations.shared.checks.PatchInfo.Build.*; -import static app.revanced.integrations.shared.checks.PatchInfo.PATCH_TIME; /** * This class is used to check if the app was patched by the user @@ -180,64 +179,47 @@ public final class CheckEnvironmentPatch { */ private static class CheckIsNearPatchTime extends Check { /** - * How soon after patching the app must be first launched. + * How soon after patching the app must be installed to pass. */ - static final int THRESHOLD_FOR_PATCHING_RECENTLY = 30 * 60 * 1000; // 30 minutes. + static final int INSTALL_AFTER_PATCHING_DURATION_THRESHOLD = 30 * 60 * 1000; // 30 minutes. /** - * How soon after installation or updating the app to check the patch time. - * If the install/update is older than this, this entire check is ignored - * to prevent showing any errors if the user clears the app data after installation. + * Milliseconds between the time the app was patched, and when it was installed/updated. */ - static final int THRESHOLD_FOR_RECENT_INSTALLATION = 12 * 60 * 60 * 1000; // 12 hours. - - static final long DURATION_SINCE_PATCHING = System.currentTimeMillis() - PATCH_TIME; + long durationBetweenPatchingAndInstallation; + @NonNull @Override protected Boolean check() { - Logger.printInfo(() -> "Installed: " + (DURATION_SINCE_PATCHING / 1000) + " seconds after patching"); - - // Also verify patched time is not in the future. - if (DURATION_SINCE_PATCHING < 0) { - // Patch time is in the future and clearly wrong. - return false; - } - - if (DURATION_SINCE_PATCHING < THRESHOLD_FOR_PATCHING_RECENTLY) { - // App is recently patched and this installation is new or recently updated. - return true; - } - - // Verify the app install/update is recent, - // to prevent showing errors if the user later clears the app data. try { Context context = Utils.getContext(); PackageManager packageManager = context.getPackageManager(); PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); // Duration since initial install or last update, which ever is sooner. - final long durationSinceInstallUpdate = System.currentTimeMillis() - packageInfo.lastUpdateTime; + durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME; Logger.printInfo(() -> "App was installed/updated: " - + (durationSinceInstallUpdate / (60 * 60 * 1000)) + " hours ago"); + + (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching")); - if (durationSinceInstallUpdate > THRESHOLD_FOR_RECENT_INSTALLATION) { - Logger.printInfo(() -> "Ignoring install time check since install/update was over " - + THRESHOLD_FOR_RECENT_INSTALLATION + " hours ago"); - return null; + if (durationBetweenPatchingAndInstallation < 0) { + // Patch time is in the future and clearly wrong. + return false; + } + + if (durationBetweenPatchingAndInstallation < INSTALL_AFTER_PATCHING_DURATION_THRESHOLD) { + return true; } } catch (PackageManager.NameNotFoundException ex) { Logger.printException(() -> "Package name not found exception", ex); // Will never happen. } - // Was patched between 30 minutes and 12 hours ago. - // This can only happen if someone installs the app then waits 30+ minutes to launch, - // or they clear the app data within 12 hours after installation. + // User installed more than 30 minutes after patching. return false; } @Override protected String failureReason() { - if (DURATION_SINCE_PATCHING < 0) { + if (durationBetweenPatchingAndInstallation < 0) { // Could happen if the user has their device clock incorrectly set in the past, // but assume that isn't the case and the apk was patched on a device with the wrong system time. return str("revanced_check_environment_not_near_patch_time_invalid"); @@ -246,7 +228,7 @@ public final class CheckEnvironmentPatch { // If patched over 1 day ago, show how old this pre-patched apk is. // Showing the age can help convey it's better to patch yourself and know it's the latest. final long oneDay = 24 * 60 * 60 * 1000; - final long daysSincePatching = DURATION_SINCE_PATCHING / oneDay; + final long daysSincePatching = durationBetweenPatchingAndInstallation / oneDay; if (daysSincePatching > 1) { // Use over 1 day to avoid singular vs plural strings. return str("revanced_check_environment_not_near_patch_time_days", daysSincePatching); } @@ -291,17 +273,15 @@ public final class CheckEnvironmentPatch { CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime(); Boolean timeCheckPassed = nearPatchTime.check(); - if (timeCheckPassed != null) { - if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { - if (failedChecks.isEmpty()) { - // Recently patched and installed. No further checks are needed. - // Stopping here also prevents showing warnings if patching and installing with Termux. - Check.disableForever(); - return; - } - } else { - failedChecks.add(nearPatchTime); + if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + if (failedChecks.isEmpty()) { + // Recently patched and installed. No further checks are needed. + // Stopping here also prevents showing warnings if patching and installing with Termux. + Check.disableForever(); + return; } + } else { + failedChecks.add(nearPatchTime); } CheckExpectedInstaller installerCheck = new CheckExpectedInstaller(); From 5f6239b6107ce63bf3498e5231a3e083d40b744a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 6 Sep 2024 08:47:32 +0000 Subject: [PATCH 16/29] chore(release): 1.14.0-dev.6 [skip ci] # [1.14.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.5...v1.14.0-dev.6) (2024-09-06) ### Bug Fixes * **YouTube - Check environment patch:** Use app install/update time instead of current time ([#687](https://github.com/ReVanced/revanced-integrations/issues/687)) ([b0d82b0](https://github.com/ReVanced/revanced-integrations/commit/b0d82b016eeacca324b906037d1857b81f577b53)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d2bc229..3d795917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.14.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.5...v1.14.0-dev.6) (2024-09-06) + + +### Bug Fixes + +* **YouTube - Check environment patch:** Use app install/update time instead of current time ([#687](https://github.com/ReVanced/revanced-integrations/issues/687)) ([b0d82b0](https://github.com/ReVanced/revanced-integrations/commit/b0d82b016eeacca324b906037d1857b81f577b53)) + # [1.14.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.4...v1.14.0-dev.5) (2024-09-06) diff --git a/gradle.properties b/gradle.properties index 8eb114cf..52159c7a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.14.0-dev.5 +version = 1.14.0-dev.6 From 5adf8bdd67c67502f5bc2912247e1eb1cec8a33d Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 6 Sep 2024 04:55:33 -0400 Subject: [PATCH 17/29] fix(YouTube - Check environment patch): Allow adb installs even if patched more than 30 minutes ago --- .../shared/checks/CheckEnvironmentPatch.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java b/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java index c75beb34..0aad321c 100644 --- a/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java +++ b/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java @@ -188,7 +188,6 @@ public final class CheckEnvironmentPatch { */ long durationBetweenPatchingAndInstallation; - @NonNull @Override protected Boolean check() { try { @@ -214,7 +213,8 @@ public final class CheckEnvironmentPatch { } // User installed more than 30 minutes after patching. - return false; + // Don't fail this, to allow adb install of older patched apps. + return null; } @Override @@ -273,15 +273,17 @@ public final class CheckEnvironmentPatch { CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime(); Boolean timeCheckPassed = nearPatchTime.check(); - if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { - if (failedChecks.isEmpty()) { - // Recently patched and installed. No further checks are needed. - // Stopping here also prevents showing warnings if patching and installing with Termux. - Check.disableForever(); - return; + if (timeCheckPassed != null) { + if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + if (failedChecks.isEmpty()) { + // Recently patched and installed. No further checks are needed. + // Stopping here also prevents showing warnings if patching and installing with Termux. + Check.disableForever(); + return; + } + } else { + failedChecks.add(nearPatchTime); } - } else { - failedChecks.add(nearPatchTime); } CheckExpectedInstaller installerCheck = new CheckExpectedInstaller(); From dffe7f6c34d4276d6141d5a09556cfff2fbe137d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 6 Sep 2024 08:58:50 +0000 Subject: [PATCH 18/29] chore(release): 1.14.0-dev.7 [skip ci] # [1.14.0-dev.7](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.6...v1.14.0-dev.7) (2024-09-06) ### Bug Fixes * **YouTube - Check environment patch:** Allow adb installs even if patched more than 30 minutes ago ([5adf8bd](https://github.com/ReVanced/revanced-integrations/commit/5adf8bdd67c67502f5bc2912247e1eb1cec8a33d)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d795917..e509a854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.14.0-dev.7](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.6...v1.14.0-dev.7) (2024-09-06) + + +### Bug Fixes + +* **YouTube - Check environment patch:** Allow adb installs even if patched more than 30 minutes ago ([5adf8bd](https://github.com/ReVanced/revanced-integrations/commit/5adf8bdd67c67502f5bc2912247e1eb1cec8a33d)) + # [1.14.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.5...v1.14.0-dev.6) (2024-09-06) diff --git a/gradle.properties b/gradle.properties index 52159c7a..b0509f37 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.14.0-dev.6 +version = 1.14.0-dev.7 From 18048f33243c4a877cf8b055d89fc04c4b963e0c Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 6 Sep 2024 05:39:33 -0400 Subject: [PATCH 19/29] fix(YouTube - Check environment patch): Show if patched apk is too old, if the install source is not Manager or ADB --- .../shared/checks/CheckEnvironmentPatch.java | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java b/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java index 0aad321c..c2106a1d 100644 --- a/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java +++ b/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java @@ -188,6 +188,7 @@ public final class CheckEnvironmentPatch { */ long durationBetweenPatchingAndInstallation; + @NonNull @Override protected Boolean check() { try { @@ -213,8 +214,7 @@ public final class CheckEnvironmentPatch { } // User installed more than 30 minutes after patching. - // Don't fail this, to allow adb install of older patched apps. - return null; + return false; } @Override @@ -271,34 +271,33 @@ public final class CheckEnvironmentPatch { failedChecks.add(sameHardware); } - CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime(); - Boolean timeCheckPassed = nearPatchTime.check(); - if (timeCheckPassed != null) { - if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { - if (failedChecks.isEmpty()) { - // Recently patched and installed. No further checks are needed. - // Stopping here also prevents showing warnings if patching and installing with Termux. - Check.disableForever(); - return; - } - } else { - failedChecks.add(nearPatchTime); + CheckExpectedInstaller installerCheck = new CheckExpectedInstaller(); + if (installerCheck.check() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + // If the installer package is Manager but this code is reached, + // that means it must not be the right Manager otherwise the hardware hash + // signatures would be present and this check would not have run. + if (installerCheck.installerFound == InstallationType.MANAGER) { + failedChecks.add(installerCheck); + // Also could not have been patched on this device. + failedChecks.add(sameHardware); + } else if (failedChecks.isEmpty()) { + // ADB install of CLI build. Allow even if patched a long time ago. + Check.disableForever(); + return; } + } else { + failedChecks.add(installerCheck); } - CheckExpectedInstaller installerCheck = new CheckExpectedInstaller(); - // If the installer package is Manager but this code is reached, - // that means it must not be the right Manager otherwise the hardware hash - // signatures would be present and this check would not have run. - final boolean isManagerInstall = installerCheck.installerFound == InstallationType.MANAGER; - if (!installerCheck.check() || isManagerInstall) { - failedChecks.add(installerCheck); - - if (isManagerInstall) { - // If using Manager and reached here, then this must - // have been patched on a different device. - failedChecks.add(sameHardware); - } + CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime(); + Boolean timeCheckPassed = nearPatchTime.check(); + if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + // Allow installing recently patched apks, + // even if the install source is not Manager or ADB. + Check.disableForever(); + return; + } else { + failedChecks.add(nearPatchTime); } if (DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { @@ -310,11 +309,6 @@ public final class CheckEnvironmentPatch { ); } - if (failedChecks.isEmpty()) { - Check.disableForever(); - return; - } - //noinspection ComparatorCombinators Collections.sort(failedChecks, (o1, o2) -> o1.uiSortingValue() - o2.uiSortingValue()); From 2c2641e1cfcfd79977d989176e217eee36b8efbb Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 6 Sep 2024 09:42:41 +0000 Subject: [PATCH 20/29] chore(release): 1.14.0-dev.8 [skip ci] # [1.14.0-dev.8](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.7...v1.14.0-dev.8) (2024-09-06) ### Bug Fixes * **YouTube - Check environment patch:** Show if patched apk is too old, if the install source is not Manager or ADB ([18048f3](https://github.com/ReVanced/revanced-integrations/commit/18048f33243c4a877cf8b055d89fc04c4b963e0c)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e509a854..204cc081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.14.0-dev.8](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.7...v1.14.0-dev.8) (2024-09-06) + + +### Bug Fixes + +* **YouTube - Check environment patch:** Show if patched apk is too old, if the install source is not Manager or ADB ([18048f3](https://github.com/ReVanced/revanced-integrations/commit/18048f33243c4a877cf8b055d89fc04c4b963e0c)) + # [1.14.0-dev.7](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.6...v1.14.0-dev.7) (2024-09-06) diff --git a/gradle.properties b/gradle.properties index b0509f37..7d69789c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.14.0-dev.7 +version = 1.14.0-dev.8 From 0f5dfb4e76337da7e086a08b59aed7881de56a31 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 9 Sep 2024 03:17:38 -0400 Subject: [PATCH 21/29] fix(YouTube - SponsorBlock): Add summary text to 'view my segments' button --- .../settings/preference/SponsorBlockPreferenceFragment.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java index 05e0dbaf..2de654a2 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java @@ -519,6 +519,7 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment { statsCategory.addPreference(preference); String formatted = SponsorBlockUtils.getNumberOfSkipsString(stats.segmentCount); preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted))); + preference.setSummary(str("revanced_sb_stats_submissions_sum")); if (stats.totalSegmentCountIncludingIgnored == 0) { preference.setSelectable(false); } else { From b816c45838769c6b3df7147d091696cb3ee9789e Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 9 Sep 2024 03:21:05 -0400 Subject: [PATCH 22/29] feat(YouTube): Add donation link to settings about screen (#688) --- .../preference/ReVancedAboutPreference.java | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ReVancedAboutPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ReVancedAboutPreference.java index f5911a01..a39b24db 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ReVancedAboutPreference.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ReVancedAboutPreference.java @@ -1,5 +1,6 @@ package app.revanced.integrations.shared.settings.preference; +import static app.revanced.integrations.shared.StringRef.sf; import static app.revanced.integrations.shared.StringRef.str; import static app.revanced.integrations.youtube.requests.Route.Method.GET; @@ -71,7 +72,7 @@ public class ReVancedAboutPreference extends Preference { return Color.BLACK; } - private String createDialogHtml(ReVancedSocialLink[] socialLinks) { + private String createDialogHtml(WebLink[] socialLinks) { final boolean isNetworkConnected = Utils.isNetworkConnected(); StringBuilder builder = new StringBuilder(); @@ -122,7 +123,7 @@ public class ReVancedAboutPreference extends Preference { .append(""); builder.append("
"); - for (ReVancedSocialLink social : socialLinks) { + for (WebLink social : socialLinks) { builder.append("
"); builder.append(String.format("%s", social.url, social.name)); builder.append("
"); @@ -151,7 +152,7 @@ public class ReVancedAboutPreference extends Preference { } private void fetchLinksAndShowDialog(@Nullable ProgressDialog progress) { - ReVancedSocialLink[] socialLinks = SocialLinksRoutes.fetchSocialLinks(); + WebLink[] socialLinks = SocialLinksRoutes.fetchSocialLinks(); String htmlDialog = createDialogHtml(socialLinks); Utils.runOnMainThreadNowOrLater(() -> { @@ -221,19 +222,19 @@ class WebViewDialog extends Dialog { } } -class ReVancedSocialLink { +class WebLink { final boolean preferred; final String name; final String url; - ReVancedSocialLink(JSONObject json) throws JSONException { + WebLink(JSONObject json) throws JSONException { this(json.getBoolean("preferred"), json.getString("name"), json.getString("url") ); } - ReVancedSocialLink(boolean preferred, String name, String url) { + WebLink(boolean preferred, String name, String url) { this.preferred = preferred; this.name = name; this.url = url; @@ -251,24 +252,33 @@ class ReVancedSocialLink { } class SocialLinksRoutes { + /** + * Simple link to the website donate page, + * rather than fetching and parsing the donation links using the API. + */ + public static final WebLink DONATE_LINK = new WebLink(true, + sf("revanced_settings_about_links_donate").toString(), + "https://revanced.app/donate"); + /** * Links to use if fetch links api call fails. */ - private static final ReVancedSocialLink[] NO_CONNECTION_STATIC_LINKS = { - new ReVancedSocialLink(true, "ReVanced.app", "https://revanced.app") + private static final WebLink[] NO_CONNECTION_STATIC_LINKS = { + new WebLink(true, "ReVanced.app", "https://revanced.app"), + DONATE_LINK, }; private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v2"; private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/socials").compile(); @Nullable - private static volatile ReVancedSocialLink[] fetchedLinks; + private static volatile WebLink[] fetchedLinks; static boolean hasFetchedLinks() { return fetchedLinks != null; } - static ReVancedSocialLink[] fetchSocialLinks() { + static WebLink[] fetchSocialLinks() { try { if (hasFetchedLinks()) return fetchedLinks; @@ -290,14 +300,17 @@ class SocialLinksRoutes { JSONObject json = Requester.parseJSONObjectAndDisconnect(connection); JSONArray socials = json.getJSONArray("socials"); - List links = new ArrayList<>(); + List links = new ArrayList<>(); + + links.add(DONATE_LINK); // Show donate link first. for (int i = 0, length = socials.length(); i < length; i++) { - ReVancedSocialLink link = new ReVancedSocialLink(socials.getJSONObject(i)); + WebLink link = new WebLink(socials.getJSONObject(i)); links.add(link); } + Logger.printDebug(() -> "links: " + links); - return fetchedLinks = links.toArray(new ReVancedSocialLink[0]); + return fetchedLinks = links.toArray(new WebLink[0]); } catch (SocketTimeoutException ex) { Logger.printInfo(() -> "Could not fetch social links", ex); // No toast. From 8332444c0d253d9506a889e6f8810c8e364d56b6 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 9 Sep 2024 07:24:25 +0000 Subject: [PATCH 23/29] chore(release): 1.14.0-dev.9 [skip ci] # [1.14.0-dev.9](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.8...v1.14.0-dev.9) (2024-09-09) ### Bug Fixes * **YouTube - SponsorBlock:** Add summary text to 'view my segments' button ([0f5dfb4](https://github.com/ReVanced/revanced-integrations/commit/0f5dfb4e76337da7e086a08b59aed7881de56a31)) ### Features * **YouTube:** Add donation link to settings about screen ([#688](https://github.com/ReVanced/revanced-integrations/issues/688)) ([b816c45](https://github.com/ReVanced/revanced-integrations/commit/b816c45838769c6b3df7147d091696cb3ee9789e)) --- CHANGELOG.md | 12 ++++++++++++ gradle.properties | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 204cc081..68c72c8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [1.14.0-dev.9](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.8...v1.14.0-dev.9) (2024-09-09) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Add summary text to 'view my segments' button ([0f5dfb4](https://github.com/ReVanced/revanced-integrations/commit/0f5dfb4e76337da7e086a08b59aed7881de56a31)) + + +### Features + +* **YouTube:** Add donation link to settings about screen ([#688](https://github.com/ReVanced/revanced-integrations/issues/688)) ([b816c45](https://github.com/ReVanced/revanced-integrations/commit/b816c45838769c6b3df7147d091696cb3ee9789e)) + # [1.14.0-dev.8](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.7...v1.14.0-dev.8) (2024-09-06) diff --git a/gradle.properties b/gradle.properties index 7d69789c..fe422c67 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.14.0-dev.8 +version = 1.14.0-dev.9 From 2eb5e3afebe374a86e9da521d6441402130f0fd0 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:37:40 -0400 Subject: [PATCH 24/29] fix(YouTube - Return YouTube Dislike): Show correct value when swiping back to prior Short and disliking --- .../patches/ReturnYouTubeDislikePatch.java | 1 + .../ReturnYouTubeDislikeFilterPatch.java | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java index 8aa2c5ca..ec52ef07 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java @@ -664,6 +664,7 @@ public class ReturnYouTubeDislikePatch { if (videoIdIsSame(lastLithoShortsVideoData, videoId)) { return; } + if (videoId == null) { // Litho filter did not detect the video id. App is in incognito mode, // or the proto buffer structure was changed and the video id is no longer present. diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java index 11bffcc5..8df0d190 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java @@ -68,18 +68,23 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter { private final ByteArrayFilterGroupList videoIdFilterGroup = new ByteArrayFilterGroupList(); public ReturnYouTubeDislikeFilterPatch() { - // Likes always seems to load before the dislikes, but if this - // ever changes then both likes and dislikes need callbacks. + // When a new Short is opened, the like buttons always seem to load before the dislike. + // But if swiping back to a previous video and liking/disliking, then only that single button reloads. + // So must check for both buttons. addPathCallbacks( - new StringFilterGroup(null, "|shorts_like_button.eml") + new StringFilterGroup(null, "|shorts_like_button.eml"), + new StringFilterGroup(null, "|shorts_dislike_button.eml") ); // After the likes icon name is some binary data and then the video id for that specific short. videoIdFilterGroup.addAll( - // Video was previously liked before video was opened. + // on_shadowed = Video was previously like/disliked before opening. + // off_shadowed = Video was not previously liked/disliked before opening. new ByteArrayFilterGroup(null, "ic_right_like_on_shadowed"), - // Video was not already liked. - new ByteArrayFilterGroup(null, "ic_right_like_off_shadowed") + new ByteArrayFilterGroup(null, "ic_right_like_off_shadowed"), + + new ByteArrayFilterGroup(null, "ic_right_dislike_on_shadowed"), + new ByteArrayFilterGroup(null, "ic_right_dislike_off_shadowed") ); } @@ -111,6 +116,7 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter { return videoId; } } + return null; } } @@ -132,6 +138,7 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter { return true; } } + return false; } } \ No newline at end of file From 5ffff1bd405dd5cb30716ebfe32b25014bddb56e Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 11 Sep 2024 03:41:25 +0000 Subject: [PATCH 25/29] chore(release): 1.14.0-dev.10 [skip ci] # [1.14.0-dev.10](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.9...v1.14.0-dev.10) (2024-09-11) ### Bug Fixes * **YouTube - Return YouTube Dislike:** Show correct value when swiping back to prior Short and disliking ([2eb5e3a](https://github.com/ReVanced/revanced-integrations/commit/2eb5e3afebe374a86e9da521d6441402130f0fd0)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c72c8e..6415270d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.14.0-dev.10](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.9...v1.14.0-dev.10) (2024-09-11) + + +### Bug Fixes + +* **YouTube - Return YouTube Dislike:** Show correct value when swiping back to prior Short and disliking ([2eb5e3a](https://github.com/ReVanced/revanced-integrations/commit/2eb5e3afebe374a86e9da521d6441402130f0fd0)) + # [1.14.0-dev.9](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.8...v1.14.0-dev.9) (2024-09-09) diff --git a/gradle.properties b/gradle.properties index fe422c67..2c9cd5aa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.14.0-dev.9 +version = 1.14.0-dev.10 From 6f3d2ffb0d65ec819038050dfabe1432f87ce360 Mon Sep 17 00:00:00 2001 From: MarcaD <152095496+MarcaDian@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:34:34 +0300 Subject: [PATCH 26/29] feat(YouTube - Hide Shorts components): Hide 'Use this sound' button (#691) Co-authored-by: oSumAtrIX --- .../integrations/youtube/patches/components/ShortsFilter.java | 4 ++++ .../app/revanced/integrations/youtube/settings/Settings.java | 1 + 2 files changed, 5 insertions(+) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java index 8ba6c46b..a7935f71 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java @@ -202,6 +202,10 @@ public final class ShortsFilter extends Filter { new ByteArrayFilterGroup( Settings.HIDE_SHORTS_SUPER_THANKS_BUTTON, "yt_outline_dollar_sign_heart_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_USE_THIS_SOUND_BUTTON, + "yt_outline_camera_" ) ); } diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 5a40ed0f..3cbd7b58 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -221,6 +221,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_SHORTS_LOCATION_LABEL = new BooleanSetting("revanced_hide_shorts_location_label", FALSE); // Save sound to playlist and Search suggestions may have been A/B tests that were abandoned by YT, and it's not clear if these are still used. public static final BooleanSetting HIDE_SHORTS_SAVE_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_save_sound_button", FALSE); + public static final BooleanSetting HIDE_SHORTS_USE_THIS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_use_this_sound_button", TRUE); public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS = new BooleanSetting("revanced_hide_shorts_search_suggestions", FALSE); public static final BooleanSetting HIDE_SHORTS_SUPER_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_super_thanks_button", TRUE); public static final BooleanSetting HIDE_SHORTS_LIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_like_button", FALSE); From ca50665ac8f02fcf3a64e959e8242d7909589897 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 17 Sep 2024 13:38:01 +0000 Subject: [PATCH 27/29] chore(release): 1.14.0-dev.11 [skip ci] # [1.14.0-dev.11](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.10...v1.14.0-dev.11) (2024-09-17) ### Features * **YouTube - Hide Shorts components:** Hide 'Use this sound' button ([#691](https://github.com/ReVanced/revanced-integrations/issues/691)) ([6f3d2ff](https://github.com/ReVanced/revanced-integrations/commit/6f3d2ffb0d65ec819038050dfabe1432f87ce360)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6415270d..59a66d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.14.0-dev.11](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.10...v1.14.0-dev.11) (2024-09-17) + + +### Features + +* **YouTube - Hide Shorts components:** Hide 'Use this sound' button ([#691](https://github.com/ReVanced/revanced-integrations/issues/691)) ([6f3d2ff](https://github.com/ReVanced/revanced-integrations/commit/6f3d2ffb0d65ec819038050dfabe1432f87ce360)) + # [1.14.0-dev.10](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.9...v1.14.0-dev.10) (2024-09-11) diff --git a/gradle.properties b/gradle.properties index 2c9cd5aa..f5df7333 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.14.0-dev.10 +version = 1.14.0-dev.11 From 04682353af9831d312a82264a8944268c7901db7 Mon Sep 17 00:00:00 2001 From: Zain Date: Wed, 18 Sep 2024 05:45:14 +0700 Subject: [PATCH 28/29] fix(YouTube): Fix issues related to playback by replace streaming data (#680) Co-authored-by: kitadai31 <90122968+kitadai31@users.noreply.github.com> Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Co-authored-by: oSumAtrIX --- .../youtube/patches/spoof/ClientType.java | 79 +++++ .../patches/spoof/DeviceHardwareSupport.java | 53 ++++ .../patches/spoof/SpoofClientPatch.java | 279 ------------------ .../patches/spoof/SpoofSignaturePatch.java | 242 --------------- .../patches/spoof/SpoofVideoStreamsPatch.java | 170 +++++++++++ .../patches/spoof/StoryboardRenderer.java | 36 --- .../patches/spoof/requests/PlayerRoutes.java | 80 ++--- .../requests/StoryboardRendererRequester.java | 153 ---------- .../spoof/requests/StreamingDataRequest.java | 215 ++++++++++++++ .../youtube/settings/Settings.java | 12 +- .../ForceAVCSpoofingPreference.java | 61 ++++ .../chromium/net/ExperimentalUrlRequest.java | 8 - .../java/org/chromium/net/UrlRequest.java | 4 + 13 files changed, 615 insertions(+), 777 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofVideoStreamsPatch.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/settings/preference/ForceAVCSpoofingPreference.java delete mode 100644 stub/src/main/java/org/chromium/net/ExperimentalUrlRequest.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java new file mode 100644 index 00000000..f5300cb8 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java @@ -0,0 +1,79 @@ +package app.revanced.integrations.youtube.patches.spoof; + +import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowAV1; +import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowVP9; + +import android.os.Build; + +import androidx.annotation.Nullable; + +public enum ClientType { + // https://dumps.tadiphone.dev/dumps/oculus/eureka + IOS(5, + // iPhone 15 supports AV1 hardware decoding. + // Only use if this Android device also has hardware decoding. + allowAV1() + ? "iPhone16,2" // 15 Pro Max + : "iPhone11,4", // XS Max + // iOS 14+ forces VP9. + allowVP9() + ? "17.5.1.21F90" + : "13.7.17H35", + allowVP9() + ? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)" + : "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)", + null, + // Version number should be a valid iOS release. + // https://www.ipa4fun.com/history/185230 + "19.10.7" + ), + ANDROID_VR(28, + "Quest 3", + "12", + "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", + "32", // Android 12.1 + "1.56.21" + ); + + /** + * YouTube + * client type + */ + public final int id; + + /** + * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) + */ + public final String model; + + /** + * Device OS version. + */ + public final String osVersion; + + /** + * Player user-agent. + */ + public final String userAgent; + + /** + * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk) + * Field is null if not applicable. + */ + @Nullable + public final String androidSdkVersion; + + /** + * App version. + */ + public final String appVersion; + + ClientType(int id, String model, String osVersion, String userAgent, @Nullable String androidSdkVersion, String appVersion) { + this.id = id; + this.model = model; + this.osVersion = osVersion; + this.userAgent = userAgent; + this.androidSdkVersion = androidSdkVersion; + this.appVersion = appVersion; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java new file mode 100644 index 00000000..6b147cd6 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java @@ -0,0 +1,53 @@ +package app.revanced.integrations.youtube.patches.spoof; + +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.os.Build; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.youtube.settings.Settings; + +public class DeviceHardwareSupport { + public static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9; + public static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1; + + static { + boolean vp9found = false; + boolean av1found = false; + MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + final boolean deviceIsAndroidTenOrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + + for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { + final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater + ? codecInfo.isHardwareAccelerated() + : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. + if (isHardwareAccelerated && !codecInfo.isEncoder()) { + for (String type : codecInfo.getSupportedTypes()) { + if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { + vp9found = true; + } else if (type.equalsIgnoreCase("video/av01")) { + av1found = true; + } + } + } + } + + DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found; + DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found; + + Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1 + ? "Device supports AV1 hardware decoding\n" + : "Device does not support AV1 hardware decoding\n" + + (DEVICE_HAS_HARDWARE_DECODING_VP9 + ? "Device supports VP9 hardware decoding" + : "Device does not support VP9 hardware decoding")); + } + + public static boolean allowVP9() { + return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get(); + } + + public static boolean allowAV1() { + return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java deleted file mode 100644 index 14e5e2f1..00000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ /dev/null @@ -1,279 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof; - -import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowAV1; -import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowVP9; - -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; -import android.net.Uri; -import android.os.Build; - -import org.chromium.net.ExperimentalUrlRequest; - -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.settings.Setting; -import app.revanced.integrations.youtube.patches.BackgroundPlaybackPatch; -import app.revanced.integrations.youtube.settings.Settings; - -@SuppressWarnings("unused") -public class SpoofClientPatch { - private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get(); - private static final ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get(); - private static final boolean SPOOF_IOS = SPOOF_CLIENT_ENABLED && SPOOF_CLIENT_TYPE == ClientType.IOS; - - /** - * Any unreachable ip address. Used to intentionally fail requests. - */ - private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0"; - private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING); - - /** - * Injection point. - * Blocks /get_watch requests by returning an unreachable URI. - * - * @param playerRequestUri The URI of the player request. - * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI. - */ - public static Uri blockGetWatchRequest(Uri playerRequestUri) { - if (SPOOF_CLIENT_ENABLED) { - try { - String path = playerRequestUri.getPath(); - - if (path != null && path.contains("get_watch")) { - Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri"); - - return UNREACHABLE_HOST_URI; - } - } catch (Exception ex) { - Logger.printException(() -> "blockGetWatchRequest failure", ex); - } - } - - return playerRequestUri; - } - - /** - * Injection point. - *

- * Blocks /initplayback requests. - */ - public static String blockInitPlaybackRequest(String originalUrlString) { - if (SPOOF_CLIENT_ENABLED) { - try { - var originalUri = Uri.parse(originalUrlString); - String path = originalUri.getPath(); - - if (path != null && path.contains("initplayback")) { - Logger.printDebug(() -> "Blocking 'initplayback' by returning unreachable url"); - - return UNREACHABLE_HOST_URI_STRING; - } - } catch (Exception ex) { - Logger.printException(() -> "blockInitPlaybackRequest failure", ex); - } - } - - return originalUrlString; - } - - /** - * Injection point. - */ - public static int getClientTypeId(int originalClientTypeId) { - return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.id : originalClientTypeId; - } - - /** - * Injection point. - */ - public static String getClientVersion(String originalClientVersion) { - return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.appVersion : originalClientVersion; - } - - /** - * Injection point. - */ - public static String getClientModel(String originalClientModel) { - return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.model : originalClientModel; - } - - /** - * Injection point. - * Fix video qualities missing, if spoofing to iOS by using the correct client OS version. - */ - public static String getOsVersion(String originalOsVersion) { - return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.osVersion : originalOsVersion; - } - - /** - * Injection point. - */ - public static boolean enablePlayerGesture(boolean original) { - return SPOOF_CLIENT_ENABLED || original; - } - - /** - * Injection point. - */ - public static boolean isClientSpoofingEnabled() { - return SPOOF_CLIENT_ENABLED; - } - - /** - * Injection point. - * When spoofing the client to iOS, the playback speed menu is missing from the player response. - * Return true to force create the playback speed menu. - */ - public static boolean forceCreatePlaybackSpeedMenu(boolean original) { - return SPOOF_IOS || original; - } - - /** - * Injection point. - * When spoofing the client to iOS, background audio only playback of livestreams fails. - * Return true to force enable audio background play. - */ - public static boolean overrideBackgroundAudioPlayback() { - return SPOOF_IOS && BackgroundPlaybackPatch.playbackIsNotShort(); - } - - /** - * Injection point. - * Fix video qualities missing, if spoofing to iOS by using the correct iOS user-agent. - */ - public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, String url) { - if (SPOOF_CLIENT_ENABLED) { - String path = Uri.parse(url).getPath(); - if (path != null && path.contains("player")) { - return builder.addHeader("User-Agent", SPOOF_CLIENT_TYPE.userAgent).build(); - } - } - - return builder.build(); - } - - // Must check for device features in a separate class and cannot place this code inside - // the Patch or ClientType enum due to cyclic Setting references. - static class DeviceHardwareSupport { - private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding(); - private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding(); - - private static boolean deviceHasVP9HardwareDecoding() { - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - - for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - ? codecInfo.isHardwareAccelerated() - : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. - if (isHardwareAccelerated && !codecInfo.isEncoder()) { - for (String type : codecInfo.getSupportedTypes()) { - if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { - Logger.printDebug(() -> "Device supports VP9 hardware decoding."); - return true; - } - } - } - } - - Logger.printDebug(() -> "Device does not support VP9 hardware decoding."); - return false; - } - - private static boolean deviceHasAV1HardwareDecoding() { - // It appears all devices with hardware AV1 are also Android 10 or newer. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); - - for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { - if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) { - for (String type : codecInfo.getSupportedTypes()) { - if (type.equalsIgnoreCase("video/av01")) { - Logger.printDebug(() -> "Device supports AV1 hardware decoding."); - return true; - } - } - } - } - } - - Logger.printDebug(() -> "Device does not support AV1 hardware decoding."); - return false; - } - - static boolean allowVP9() { - return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_CLIENT_IOS_FORCE_AVC.get(); - } - - static boolean allowAV1() { - return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; - } - } - - public enum ClientType { - // https://dumps.tadiphone.dev/dumps/oculus/eureka - IOS(5, - // iPhone 15 supports AV1 hardware decoding. - // Only use if this Android device also has hardware decoding. - allowAV1() - ? "iPhone16,2" // 15 Pro Max - : "iPhone11,4", // XS Max - // iOS 14+ forces VP9. - allowVP9() - ? "17.5.1.21F90" - : "13.7.17H35", - allowVP9() - ? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)" - : "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)", - // Version number should be a valid iOS release. - // https://www.ipa4fun.com/history/185230 - "19.10.7" - ), - ANDROID_VR(28, - "Quest 3", - "12", - "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", - "1.56.21" - ); - - /** - * YouTube - * client type - */ - final int id; - - /** - * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) - */ - final String model; - - /** - * Device OS version. - */ - final String osVersion; - - /** - * Player user-agent. - */ - final String userAgent; - - /** - * App version. - */ - final String appVersion; - - ClientType(int id, String model, String osVersion, String userAgent, String appVersion) { - this.id = id; - this.model = model; - this.osVersion = osVersion; - this.userAgent = userAgent; - this.appVersion = appVersion; - } - } - - public static final class ForceiOSAVCAvailability implements Setting.Availability { - @Override - public boolean isAvailable() { - return Settings.SPOOF_CLIENT.get() && Settings.SPOOF_CLIENT_TYPE.get() == ClientType.IOS; - } - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java deleted file mode 100644 index 41f03ed7..00000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofSignaturePatch.java +++ /dev/null @@ -1,242 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof; - -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import androidx.annotation.Nullable; -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.youtube.patches.VideoInformation; -import app.revanced.integrations.youtube.settings.Settings; -import app.revanced.integrations.youtube.shared.PlayerType; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import static app.revanced.integrations.shared.Utils.containsAny; -import static app.revanced.integrations.youtube.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer; - -/** @noinspection unused*/ -@Deprecated -public class SpoofSignaturePatch { - /** - * Parameter (also used by - * yt-dlp) - * to fix playback issues. - */ - private static final String INCOGNITO_PARAMETERS = "CgIQBg=="; - - /** - * Parameters used when playing clips. - */ - private static final String CLIPS_PARAMETERS = "kAIB"; - - /** - * 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"; - - /** - * Last video id loaded. Used to prevent reloading the same spec multiple times. - */ - @Nullable - private static volatile String lastPlayerResponseVideoId; - - @Nullable - private static volatile Future rendererFuture; - - private static volatile boolean useOriginalStoryboardRenderer; - - private static volatile boolean isPlayingShorts; - - @Nullable - private static StoryboardRenderer getRenderer(boolean waitForCompletion) { - Future future = rendererFuture; - if (future != null) { - try { - if (waitForCompletion || future.isDone()) { - return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout. - } // else, return null. - } catch (TimeoutException ex) { - Logger.printDebug(() -> "Could not get renderer (get timed out)"); - } catch (ExecutionException | InterruptedException ex) { - // Should never happen. - Logger.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, String videoId, boolean isShortAndOpeningOrPlaying) { - try { - Logger.printDebug(() -> "Original protobuf parameter value: " + parameters); - - if (parameters == null || !Settings.SPOOF_SIGNATURE.get()) { - 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. - //noinspection AssignmentUsedAsCondition - if (useOriginalStoryboardRenderer = parameters.length() > 150 || parameters.startsWith(CLIPS_PARAMETERS)) { - return parameters; - } - - // Shorts do not need to be spoofed. - //noinspection AssignmentUsedAsCondition - if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) { - isPlayingShorts = true; - return parameters; - } - isPlayingShorts = false; - - boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL - && containsAny(parameters, AUTOPLAY_PARAMETERS); - if (isPlayingFeed) { - //noinspection AssignmentUsedAsCondition - if (useOriginalStoryboardRenderer = !Settings.SPOOF_SIGNATURE_IN_FEED.get()) { - // Don't spoof the feed video playback. This will cause video playback issues, - // but only if user continues watching for more than 1 minute. - return parameters; - } - // Spoof the feed video. Video will show up in watch history and video subtitles are missing. - fetchStoryboardRenderer(); - return SCRIM_PARAMETER + INCOGNITO_PARAMETERS; - } - - fetchStoryboardRenderer(); - } catch (Exception ex) { - Logger.printException(() -> "spoofParameter failure", ex); - } - return INCOGNITO_PARAMETERS; - } - - private static void fetchStoryboardRenderer() { - if (!Settings.SPOOF_STORYBOARD_RENDERER.get()) { - lastPlayerResponseVideoId = null; - rendererFuture = null; - return; - } - String videoId = VideoInformation.getPlayerResponseVideoId(); - if (!videoId.equals(lastPlayerResponseVideoId)) { - rendererFuture = Utils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId)); - lastPlayerResponseVideoId = videoId; - } - // Block until the renderer fetch completes. - // This is desired because if this returns without finishing the fetch - // then video will start playback but the storyboard is not ready yet. - getRenderer(true); - } - - private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec, - boolean returnNullIfLiveStream) { - if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) { - StoryboardRenderer renderer = getRenderer(false); - if (renderer != null) { - if (returnNullIfLiveStream && renderer.isLiveStream) { - return null; - } - - if (renderer.spec != null) { - return renderer.spec; - } - } - } - - return originalStoryboardRendererSpec; - } - - /** - * Injection point. - * Called from background threads and from the main thread. - */ - @Nullable - public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) { - return getStoryboardRendererSpec(originalStoryboardRendererSpec, false); - } - - /** - * Injection point. - * Uses additional check to handle live streams. - * Called from background threads and from the main thread. - */ - @Nullable - public static String getStoryboardDecoderRendererSpec(String originalStoryboardRendererSpec) { - return getStoryboardRendererSpec(originalStoryboardRendererSpec, true); - } - - /** - * Injection point. - */ - public static int getRecommendedLevel(int originalLevel) { - if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) { - StoryboardRenderer renderer = getRenderer(false); - if (renderer != null) { - if (renderer.recommendedLevel != null) { - return renderer.recommendedLevel; - } - } - } - - return originalLevel; - } - - /** - * Injection point. Forces seekbar to be shown for paid videos or - * if {@link Settings#SPOOF_STORYBOARD_RENDERER} is not enabled. - */ - public static boolean getSeekbarThumbnailOverrideValue() { - if (!Settings.SPOOF_SIGNATURE.get()) { - return false; - } - StoryboardRenderer renderer = getRenderer(false); - if (renderer == null) { - // Spoof storyboard renderer is turned off, - // video is paid, or the storyboard fetch timed out. - // Show empty thumbnails so the seek time and chapters still show up. - return true; - } - return renderer.spec != null; - } - - /** - * Injection point. - * - * @param view seekbar thumbnail view. Includes both shorts and regular videos. - */ - public static void seekbarImageViewCreated(ImageView view) { - try { - if (!Settings.SPOOF_SIGNATURE.get() - || Settings.SPOOF_STORYBOARD_RENDERER.get()) { - return; - } - if (isPlayingShorts) return; - - 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); - } catch (Exception ex) { - Logger.printException(() -> "seekbarImageViewCreated failure", ex); - } - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofVideoStreamsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofVideoStreamsPatch.java new file mode 100644 index 00000000..d3c96407 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofVideoStreamsPatch.java @@ -0,0 +1,170 @@ +package app.revanced.integrations.youtube.patches.spoof; + +import android.net.Uri; + +import androidx.annotation.Nullable; + +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Objects; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.BaseSettings; +import app.revanced.integrations.shared.settings.Setting; +import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequest; +import app.revanced.integrations.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class SpoofVideoStreamsPatch { + public static final class ForceiOSAVCAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return Settings.SPOOF_VIDEO_STREAMS.get() && Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS; + } + } + + private static final boolean SPOOF_STREAMING_DATA = Settings.SPOOF_VIDEO_STREAMS.get(); + + /** + * Any unreachable ip address. Used to intentionally fail requests. + */ + private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0"; + private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING); + + /** + * Injection point. + * Blocks /get_watch requests by returning an unreachable URI. + * + * @param playerRequestUri The URI of the player request. + * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI. + */ + public static Uri blockGetWatchRequest(Uri playerRequestUri) { + if (SPOOF_STREAMING_DATA) { + try { + String path = playerRequestUri.getPath(); + + if (path != null && path.contains("get_watch")) { + Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri"); + + return UNREACHABLE_HOST_URI; + } + } catch (Exception ex) { + Logger.printException(() -> "blockGetWatchRequest failure", ex); + } + } + + return playerRequestUri; + } + + /** + * Injection point. + *

+ * Blocks /initplayback requests. + */ + public static String blockInitPlaybackRequest(String originalUrlString) { + if (SPOOF_STREAMING_DATA) { + try { + var originalUri = Uri.parse(originalUrlString); + String path = originalUri.getPath(); + + if (path != null && path.contains("initplayback")) { + Logger.printDebug(() -> "Blocking 'initplayback' by returning unreachable url"); + + return UNREACHABLE_HOST_URI_STRING; + } + } catch (Exception ex) { + Logger.printException(() -> "blockInitPlaybackRequest failure", ex); + } + } + + return originalUrlString; + } + + /** + * Injection point. + */ + public static boolean isSpoofingEnabled() { + return SPOOF_STREAMING_DATA; + } + + /** + * Injection point. + */ + public static void fetchStreams(String url, Map requestHeaders) { + if (SPOOF_STREAMING_DATA) { + try { + Uri uri = Uri.parse(url); + String path = uri.getPath(); + // 'heartbeat' has no video id and appears to be only after playback has started. + if (path != null && path.contains("player") && !path.contains("heartbeat")) { + String videoId = Objects.requireNonNull(uri.getQueryParameter("id")); + StreamingDataRequest.fetchRequest(videoId, requestHeaders); + } + } catch (Exception ex) { + Logger.printException(() -> "buildRequest failure", ex); + } + } + } + + /** + * Injection point. + * Fix playback by replace the streaming data. + * Called after {@link #fetchStreams(String, Map)}. + */ + @Nullable + public static ByteBuffer getStreamingData(String videoId) { + if (SPOOF_STREAMING_DATA) { + try { + StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId); + if (request != null) { + // This hook is always called off the main thread, + // but this can later be called for the same video id from the main thread. + // This is not a concern, since the fetch will always be finished + // and never block the main thread. + // But if debugging, then still verify this is the situation. + if (BaseSettings.DEBUG.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) { + Logger.printException(() -> "Error: Blocking main thread"); + } + + var stream = request.getStream(); + if (stream != null) { + Logger.printDebug(() -> "Overriding video stream: " + videoId); + return stream; + } + } + + Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId); + } catch (Exception ex) { + Logger.printException(() -> "getStreamingData failure", ex); + } + } + + return null; + } + + /** + * Injection point. + * Called after {@link #getStreamingData(String)}. + */ + @Nullable + public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) { + if (SPOOF_STREAMING_DATA) { + try { + final int methodPost = 2; + if (method == methodPost) { + String path = uri.getPath(); + String clientNameQueryKey = "c"; + final boolean iosClient = "IOS".equals(uri.getQueryParameter(clientNameQueryKey)); + if (iosClient && path != null && path.contains("videoplayback")) { + return null; + } + } + } catch (Exception ex) { + Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex); + } + } + + return postData; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java deleted file mode 100644 index 5014a5fc..00000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java +++ /dev/null @@ -1,36 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof; - -import androidx.annotation.Nullable; - -import org.jetbrains.annotations.NotNull; - -@Deprecated -public final class StoryboardRenderer { - public final String videoId; - @Nullable - public final String spec; - public final boolean isLiveStream; - /** - * Recommended image quality level, or NULL if no recommendation exists. - */ - @Nullable - public final Integer recommendedLevel; - - public StoryboardRenderer(String videoId, @Nullable String spec, boolean isLiveStream, @Nullable Integer recommendedLevel) { - this.videoId = videoId; - this.spec = spec; - this.isLiveStream = isLiveStream; - this.recommendedLevel = recommendedLevel; - } - - @NotNull - @Override - public String toString() { - return "StoryboardRenderer{" + - "videoId=" + videoId + - ", isLiveStream=" + isLiveStream + - ", spec='" + spec + '\'' + - ", recommendedLevel=" + recommendedLevel + - '}'; - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java index 1927b1d6..299110f4 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java @@ -1,94 +1,68 @@ package app.revanced.integrations.youtube.patches.spoof.requests; -import app.revanced.integrations.youtube.requests.Requester; -import app.revanced.integrations.youtube.requests.Route; -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.net.HttpURLConnection; +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.youtube.patches.spoof.ClientType; +import app.revanced.integrations.youtube.requests.Requester; +import app.revanced.integrations.youtube.requests.Route; + 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( + private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; + + static final Route.CompiledRoute GET_STREAMING_DATA = new Route( Route.Method.POST, "player" + - "?fields=storyboards.playerStoryboardSpecRenderer," + - "storyboards.playerLiveStoryboardSpecRenderer," + - "playabilityStatus.status" + "?fields=streamingData" + + "&alt=proto" ).compile(); - static final String ANDROID_INNER_TUBE_BODY; - static final String TV_EMBED_INNER_TUBE_BODY; - /** * TCP connection and HTTP read timeout */ private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds. - static { - JSONObject innerTubeBody = new JSONObject(); + private PlayerRoutes() { + } + + static String createInnertubeBody(ClientType clientType) { + JSONObject innerTubeBody = new JSONObject(); try { JSONObject context = new JSONObject(); JSONObject client = new JSONObject(); - client.put("clientName", "ANDROID"); - client.put("clientVersion", Utils.getAppVersionName()); - client.put("androidSdkVersion", 34); + client.put("clientName", clientType.name()); + client.put("clientVersion", clientType.appVersion); + client.put("deviceModel", clientType.model); + client.put("osVersion", clientType.osVersion); + if (clientType.androidSdkVersion != null) { + client.put("androidSdkVersion", clientType.androidSdkVersion); + } context.put("client", client); innerTubeBody.put("context", context); + innerTubeBody.put("contentCheckOk", true); + innerTubeBody.put("racyCheckOk", true); innerTubeBody.put("videoId", "%s"); } catch (JSONException e) { Logger.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) { - Logger.printException(() -> "Failed to create tvEmbedInnerTubeBody", e); - } - - TV_EMBED_INNER_TUBE_BODY = tvEmbedInnerTubeBody.toString(); - } - - private PlayerRoutes() { + return innerTubeBody.toString(); } /** @noinspection SameParameterValue*/ - static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route) throws IOException { + static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException { var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); - connection.setRequestProperty( - "User-Agent", "com.google.android.youtube/" + - Utils.getAppVersionName() + - " (Linux; U; Android 12; GB) gzip" - ); - connection.setRequestProperty("X-Goog-Api-Format-Version", "2"); connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("User-Agent", clientType.userAgent); connection.setUseCaches(false); connection.setDoOutput(true); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java deleted file mode 100644 index 0cbec194..00000000 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StoryboardRendererRequester.java +++ /dev/null @@ -1,153 +0,0 @@ -package app.revanced.integrations.youtube.patches.spoof.requests; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.youtube.patches.spoof.StoryboardRenderer; -import app.revanced.integrations.youtube.requests.Requester; -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.nio.charset.StandardCharsets; -import java.util.Objects; - -import static app.revanced.integrations.shared.StringRef.str; -import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.*; - -public class StoryboardRendererRequester { - - private StoryboardRendererRequester() { - } - - private static void randomlyWaitIfLocallyDebugging() { - final boolean randomlyWait = false; // Enable to simulate slow connection responses. - if (randomlyWait) { - final long maximumTimeToRandomlyWait = 10000; - Utils.doNothingForDuration(maximumTimeToRandomlyWait); - } - } - - private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex, - boolean showToastOnIOException) { - if (showToastOnIOException) Utils.showToastShort(toastMessage); - Logger.printInfo(() -> toastMessage, ex); - } - - @Nullable - private static JSONObject fetchPlayerResponse(@NonNull String requestBody, boolean showToastOnIOException) { - final long startTime = System.currentTimeMillis(); - try { - Utils.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(); - randomlyWaitIfLocallyDebugging(); - if (responseCode == 200) return Requester.parseJSONObject(connection); - - // Always show a toast for this, as a non 200 response means something is broken. - // Not a normal code path and should not be reached, so no translations are needed. - handleConnectionError("Spoof storyboard not available: " + responseCode, - null, showToastOnIOException || BaseSettings.DEBUG_TOAST_ON_ERROR.get()); - connection.disconnect(); - } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_spoof_client_storyboard_timeout"), ex, showToastOnIOException); - } catch (IOException ex) { - handleConnectionError(str("revanced_spoof_client_storyboard_io_exception", ex.getMessage()), - ex, showToastOnIOException); - } catch (Exception ex) { - Logger.printException(() -> "Spoof storyboard fetch failed", ex); // Should never happen. - } finally { - Logger.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms"); - } - - return null; - } - - private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) { - try { - return playerResponse.getJSONObject("playabilityStatus").getString("status").equals("OK"); - } catch (JSONException e) { - Logger.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(String videoId, - @NonNull String innerTubeBody, - boolean showToastOnIOException) { - final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, showToastOnIOException); - if (playerResponse != null && isPlayabilityStatusOk(playerResponse)) - return getStoryboardRendererUsingResponse(videoId, playerResponse); - - return null; - } - - @Nullable - private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull String videoId, @NonNull JSONObject playerResponse) { - try { - Logger.printDebug(() -> "Parsing response: " + playerResponse); - if (!playerResponse.has("storyboards")) { - Logger.printDebug(() -> "Using empty storyboard"); - return new StoryboardRenderer(videoId, null, false, null); - } - final JSONObject storyboards = playerResponse.getJSONObject("storyboards"); - final boolean isLiveStream = storyboards.has("playerLiveStoryboardSpecRenderer"); - final String storyboardsRendererTag = isLiveStream - ? "playerLiveStoryboardSpecRenderer" - : "playerStoryboardSpecRenderer"; - - final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag); - StoryboardRenderer renderer = new StoryboardRenderer( - videoId, - rendererElement.getString("spec"), - isLiveStream, - rendererElement.has("recommendedLevel") - ? rendererElement.getInt("recommendedLevel") - : null - ); - - Logger.printDebug(() -> "Fetched: " + renderer); - - return renderer; - } catch (JSONException e) { - Logger.printException(() -> "Failed to get storyboardRenderer", e); - } - - return null; - } - - @Nullable - public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) { - Objects.requireNonNull(videoId); - - var renderer = getStoryboardRendererUsingBody(videoId, - String.format(ANDROID_INNER_TUBE_BODY, videoId), false); - if (renderer == null) { - Logger.printDebug(() -> videoId + " not available using Android client"); - renderer = getStoryboardRendererUsingBody(videoId, - String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId), true); - if (renderer == null) { - Logger.printDebug(() -> videoId + " not available using TV embedded client"); - } - } - - return renderer; - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java new file mode 100644 index 00000000..c86b352f --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/StreamingDataRequest.java @@ -0,0 +1,215 @@ +package app.revanced.integrations.youtube.patches.spoof.requests; + +import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.BaseSettings; +import app.revanced.integrations.youtube.patches.spoof.ClientType; +import app.revanced.integrations.youtube.settings.Settings; + +/** + * Video streaming data. Fetching is tied to the behavior YT uses, + * where this class fetches the streams only when YT fetches. + * + * Effectively the cache expiration of these fetches is the same as the stock app, + * since the stock app would not use expired streams and therefor + * the integrations replace stream hook is called only if YT + * would have used it's own client streams. + */ +public class StreamingDataRequest { + + private static final ClientType[] CLIENT_ORDER_TO_USE; + + static { + ClientType[] allClientTypes = ClientType.values(); + ClientType preferredClient = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); + + CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length]; + CLIENT_ORDER_TO_USE[0] = preferredClient; + + int i = 1; + for (ClientType c : allClientTypes) { + if (c != preferredClient) { + CLIENT_ORDER_TO_USE[i++] = c; + } + } + } + + /** + * TCP connection and HTTP read timeout. + */ + private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000; + + /** + * Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS} + */ + private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; + + private static final Map cache = Collections.synchronizedMap( + new LinkedHashMap<>(100) { + /** + * Cache limit must be greater than the maximum number of videos open at once, + * which theoretically is more than 4 (3 Shorts + one regular minimized video). + * But instead use a much larger value, to handle if a video viewed a while ago + * is somehow still referenced. Each stream is a small array of Strings + * so memory usage is not a concern. + */ + private static final int CACHE_LIMIT = 50; + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. + } + }); + + public static void fetchRequest(String videoId, Map fetchHeaders) { + // Always fetch, even if there is a existing request for the same video. + cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); + } + + @Nullable + public static StreamingDataRequest getRequestForVideoId(String videoId) { + return cache.get(videoId); + } + + private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) { + if (showToast) Utils.showToastShort(toastMessage); + Logger.printInfo(() -> toastMessage, ex); + } + + @Nullable + private static HttpURLConnection send(ClientType clientType, String videoId, + Map playerHeaders, + boolean showErrorToasts) { + Objects.requireNonNull(clientType); + Objects.requireNonNull(videoId); + Objects.requireNonNull(playerHeaders); + + final long startTime = System.currentTimeMillis(); + String clientTypeName = clientType.name(); + Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name()); + + try { + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); + connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS); + + String authHeader = playerHeaders.get("Authorization"); + String visitorId = playerHeaders.get("X-Goog-Visitor-Id"); + connection.setRequestProperty("Authorization", authHeader); + connection.setRequestProperty("X-Goog-Visitor-Id", visitorId); + + String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); + byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) return connection; + + handleConnectionError(clientTypeName + " not available with response code: " + + responseCode + " message: " + connection.getResponseMessage(), + null, showErrorToasts); + } catch (SocketTimeoutException ex) { + handleConnectionError("Connection timeout", ex, showErrorToasts); + } catch (IOException ex) { + handleConnectionError("Network error", ex, showErrorToasts); + } catch (Exception ex) { + Logger.printException(() -> "send failed", ex); + } finally { + Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + + return null; + } + + private static ByteBuffer fetch(String videoId, Map playerHeaders) { + final boolean debugEnabled = BaseSettings.DEBUG.get(); + + // Retry with different client if empty response body is received. + int i = 0; + for (ClientType clientType : CLIENT_ORDER_TO_USE) { + // Show an error if the last client type fails, or if the debug is enabled then show for all attempts. + final boolean showErrorToast = (++i == CLIENT_ORDER_TO_USE.length) || debugEnabled; + + HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast); + if (connection != null) { + try { + // gzip encoding doesn't response with content length (-1), + // but empty response body does. + if (connection.getContentLength() != 0) { + try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buffer = new byte[2048]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) >= 0) { + baos.write(buffer, 0, bytesRead); + } + + return ByteBuffer.wrap(baos.toByteArray()); + } + } + } + } catch (IOException ex) { + Logger.printException(() -> "Fetch failed while processing response data", ex); + } + } + } + + handleConnectionError("Could not fetch any client streams", null, debugEnabled); + return null; + } + + private final String videoId; + private final Future future; + + private StreamingDataRequest(String videoId, Map playerHeaders) { + Objects.requireNonNull(playerHeaders); + this.videoId = videoId; + this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders)); + } + + public boolean fetchCompleted() { + return future.isDone(); + } + + @Nullable + public ByteBuffer getStream() { + try { + return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + Logger.printInfo(() -> "getStream timed out", ex); + } catch (InterruptedException ex) { + Logger.printException(() -> "getStream interrupted", ex); + Thread.currentThread().interrupt(); // Restore interrupt status flag. + } catch (ExecutionException ex) { + Logger.printException(() -> "getStream failure", ex); + } + + return null; + } + + @NonNull + @Override + public String toString() { + return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}'; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 3cbd7b58..85bc6c8e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -7,8 +7,9 @@ import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.DeAr import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.StillImagesAvailability; import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption; import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailStillTime; +import app.revanced.integrations.youtube.patches.spoof.ClientType; import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch; -import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch; +import app.revanced.integrations.youtube.patches.spoof.SpoofVideoStreamsPatch; import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings; import java.util.Arrays; @@ -19,7 +20,6 @@ import static app.revanced.integrations.shared.settings.Setting.*; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3; -import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType; import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; @@ -256,10 +256,10 @@ public class Settings extends BaseSettings { "revanced_spoof_device_dimensions_user_dialog_message"); public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE); public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE); - public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true,"revanced_spoof_client_user_dialog_message"); - public static final BooleanSetting SPOOF_CLIENT_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_client_ios_force_avc", FALSE, true, - "revanced_spoof_client_ios_force_avc_user_dialog_message", new SpoofClientPatch.ForceiOSAVCAvailability()); - public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", ClientType.IOS, true, parent(SPOOF_CLIENT)); + public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true,"revanced_spoof_video_streams_user_dialog_message"); + public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true, + "revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofVideoStreamsPatch.ForceiOSAVCAvailability()); + public static final EnumSetting SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.IOS, true, parent(SPOOF_VIDEO_STREAMS)); @Deprecated public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", ""); public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1); diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ForceAVCSpoofingPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ForceAVCSpoofingPreference.java new file mode 100644 index 00000000..8d37017e --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ForceAVCSpoofingPreference.java @@ -0,0 +1,61 @@ +package app.revanced.integrations.youtube.settings.preference; + +import static app.revanced.integrations.shared.StringRef.str; +import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.DEVICE_HAS_HARDWARE_DECODING_VP9; + +import android.content.Context; +import android.preference.SwitchPreference; +import android.util.AttributeSet; + +@SuppressWarnings({"unused", "deprecation"}) +public class ForceAVCSpoofingPreference extends SwitchPreference { + { + if (!DEVICE_HAS_HARDWARE_DECODING_VP9) { + setSummaryOn(str("revanced_spoof_video_streams_ios_force_avc_no_hardware_vp9_summary_on")); + } + } + + public ForceAVCSpoofingPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ForceAVCSpoofingPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ForceAVCSpoofingPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ForceAVCSpoofingPreference(Context context) { + super(context); + } + + private void updateUI() { + if (DEVICE_HAS_HARDWARE_DECODING_VP9) { + return; + } + + // Temporarily remove the preference key to allow changing this preference without + // causing the settings UI listeners from showing reboot dialogs by the changes made here. + String key = getKey(); + setKey(null); + + // This setting cannot be changed by the user. + super.setEnabled(false); + super.setChecked(true); + + setKey(key); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + + updateUI(); + } + + @Override + public void setChecked(boolean checked) { + super.setChecked(checked); + + updateUI(); + } +} diff --git a/stub/src/main/java/org/chromium/net/ExperimentalUrlRequest.java b/stub/src/main/java/org/chromium/net/ExperimentalUrlRequest.java deleted file mode 100644 index cdf2593e..00000000 --- a/stub/src/main/java/org/chromium/net/ExperimentalUrlRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.chromium.net; - -public abstract class ExperimentalUrlRequest { - public abstract class Builder { - public abstract ExperimentalUrlRequest.Builder addHeader(String name, String value); - public abstract ExperimentalUrlRequest build(); - } -} diff --git a/stub/src/main/java/org/chromium/net/UrlRequest.java b/stub/src/main/java/org/chromium/net/UrlRequest.java index 565fc222..4c02f1a4 100644 --- a/stub/src/main/java/org/chromium/net/UrlRequest.java +++ b/stub/src/main/java/org/chromium/net/UrlRequest.java @@ -1,4 +1,8 @@ package org.chromium.net; public abstract class UrlRequest { + public abstract class Builder { + public abstract Builder addHeader(String name, String value); + public abstract UrlRequest build(); + } } From ebfe083a24c022ca3d5b5341e4bee24d5985e4c6 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 17 Sep 2024 22:48:14 +0000 Subject: [PATCH 29/29] chore(release): 1.14.0-dev.12 [skip ci] # [1.14.0-dev.12](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.11...v1.14.0-dev.12) (2024-09-17) ### Bug Fixes * **YouTube:** Fix issues related to playback by replace streaming data ([#680](https://github.com/ReVanced/revanced-integrations/issues/680)) ([0468235](https://github.com/ReVanced/revanced-integrations/commit/04682353af9831d312a82264a8944268c7901db7)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a66d22..0419272e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.14.0-dev.12](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.11...v1.14.0-dev.12) (2024-09-17) + + +### Bug Fixes + +* **YouTube:** Fix issues related to playback by replace streaming data ([#680](https://github.com/ReVanced/revanced-integrations/issues/680)) ([0468235](https://github.com/ReVanced/revanced-integrations/commit/04682353af9831d312a82264a8944268c7901db7)) + # [1.14.0-dev.11](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.10...v1.14.0-dev.11) (2024-09-17) diff --git a/gradle.properties b/gradle.properties index f5df7333..d8b4047b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.14.0-dev.11 +version = 1.14.0-dev.12