diff --git a/CHANGELOG.md b/CHANGELOG.md index a225f11b..bbab41ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,59 @@ +# [0.120.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v0.120.0-dev.5...v0.120.0-dev.6) (2023-10-20) + + +### Features + +* **YouTube:** Add `Announcements` patch ([#503](https://github.com/ReVanced/revanced-integrations/issues/503)) ([59687f1](https://github.com/ReVanced/revanced-integrations/commit/59687f1a39768ab71b2680a5c49df5aaae0d3b4c)) + +# [0.120.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v0.120.0-dev.4...v0.120.0-dev.5) (2023-10-19) + + +### Features + +* **YouTube:** Add `Spoof device dimensions` patch ([16f1163](https://github.com/ReVanced/revanced-integrations/commit/16f1163a346fef0a87ca9384c9bf6aea977dc8fb)) + +# [0.120.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v0.120.0-dev.3...v0.120.0-dev.4) (2023-10-17) + + +### Performance Improvements + +* **YouTube:** Reduce memory requirement for prefix tree searching ([#501](https://github.com/ReVanced/revanced-integrations/issues/501)) ([f5add51](https://github.com/ReVanced/revanced-integrations/commit/f5add51fa7eb620a6edd1b27f02d38618f144480)) + +# [0.120.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v0.120.0-dev.2...v0.120.0-dev.3) (2023-10-15) + + +### Bug Fixes + +* **YouTube - Old video quality menu:** Fix toast error on tablet devices ([#500](https://github.com/ReVanced/revanced-integrations/issues/500)) ([d3eba27](https://github.com/ReVanced/revanced-integrations/commit/d3eba27c909e519eaeda072c1f995a2284007749)) + +# [0.120.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v0.120.0-dev.1...v0.120.0-dev.2) (2023-10-14) + + +### Bug Fixes + +* **YouTube - Minimized playback:** Fix pip incorrectly showing if app is minimized immediately after opening a Short ([7d02774](https://github.com/ReVanced/revanced-integrations/commit/7d02774ea192510e692e90ae55a86e25ee321926)) + +# [0.120.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.119.3-dev.2...v0.120.0-dev.1) (2023-10-13) + + +### Features + +* **YouTube - Theme:** Disable gradient loading screen ([fd09e46](https://github.com/ReVanced/revanced-integrations/commit/fd09e46d01c820632cfe440dac34f5cd957e793d)) + +## [0.119.3-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v0.119.3-dev.1...v0.119.3-dev.2) (2023-10-13) + + +### Bug Fixes + +* **YouTube - Hide Layout components:** Exempt expandable chips from exceptions ([#498](https://github.com/ReVanced/revanced-integrations/issues/498)) ([6f79746](https://github.com/ReVanced/revanced-integrations/commit/6f79746d788f196f3aa63b8e7c24b7f15ecd3f50)) + +## [0.119.3-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.119.2...v0.119.3-dev.1) (2023-10-13) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Hide new channel watermark component ([9670bd3](https://github.com/ReVanced/revanced-integrations/commit/9670bd305b3b9bbbc900af3b64152aaac125ec14)) + ## [0.119.2](https://github.com/ReVanced/revanced-integrations/compare/v0.119.1...v0.119.2) (2023-10-12) diff --git a/app/src/main/java/app/revanced/integrations/patches/BrandingWaterMarkPatch.java b/app/src/main/java/app/revanced/integrations/patches/BrandingWaterMarkPatch.java deleted file mode 100644 index e729e153..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/BrandingWaterMarkPatch.java +++ /dev/null @@ -1,11 +0,0 @@ -package app.revanced.integrations.patches; - -import app.revanced.integrations.settings.SettingsEnum; - -public class BrandingWaterMarkPatch { - - // Used by: app.revanced.patches.youtube.layout.watermark.patch.HideWatermarkPatch - public static boolean isBrandingWatermarkShown() { - return SettingsEnum.HIDE_VIDEO_WATERMARK.getBoolean() == false; - } -} diff --git a/app/src/main/java/app/revanced/integrations/patches/MinimizedPlaybackPatch.java b/app/src/main/java/app/revanced/integrations/patches/MinimizedPlaybackPatch.java index e7f8af36..6fcae6bf 100644 --- a/app/src/main/java/app/revanced/integrations/patches/MinimizedPlaybackPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/MinimizedPlaybackPatch.java @@ -5,7 +5,7 @@ import app.revanced.integrations.shared.PlayerType; public class MinimizedPlaybackPatch { public static boolean isPlaybackNotShort() { - return !PlayerType.getCurrent().isNoneOrHidden(); + return !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized(); } public static boolean overrideMinimizedPlaybackAvailable() { diff --git a/app/src/main/java/app/revanced/integrations/patches/announcements/AnnouncementsPatch.java b/app/src/main/java/app/revanced/integrations/patches/announcements/AnnouncementsPatch.java new file mode 100644 index 00000000..e68d10c1 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/announcements/AnnouncementsPatch.java @@ -0,0 +1,151 @@ +package app.revanced.integrations.patches.announcements; + +import android.app.Activity; +import android.os.Build; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.widget.TextView; +import androidx.annotation.RequiresApi; +import app.revanced.integrations.patches.announcements.requests.AnnouncementsRoutes; +import app.revanced.integrations.requests.Requester; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.UUID; + +import static android.text.Html.FROM_HTML_MODE_COMPACT; +import static app.revanced.integrations.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT; + +public final class AnnouncementsPatch { + private final static String CONSUMER = getOrSetConsumer(); + + private AnnouncementsPatch() { + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static void showAnnouncement(final Activity context) { + if (!SettingsEnum.ANNOUNCEMENTS.getBoolean()) return; + + ReVancedUtils.runOnBackgroundThread(() -> { + try { + HttpURLConnection connection = AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT, CONSUMER); + + LogHelper.printDebug(() -> "Get latest announcement route connection url: " + connection.getURL().toString()); + + try { + // Do not show the announcement if the request failed. + if (connection.getResponseCode() != 200) { + if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return; + + SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue(""); + ReVancedUtils.showToastLong("Failed to get announcement"); + + return; + } + } catch (IOException ex) { + final var message = "Failed connecting to announcements provider"; + + LogHelper.printException(() -> message, ex); + return; + } + + var jsonString = Requester.parseInputStreamAndClose(connection.getInputStream(), false); + + // Do not show the announcement if it is older or the same as the last one. + final byte[] hashBytes = MessageDigest.getInstance("SHA-256").digest(jsonString.getBytes(StandardCharsets.UTF_8)); + final var hash = java.util.Base64.getEncoder().encodeToString(hashBytes); + if (hash.equals(SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString())) return; + + // Parse the announcement. Fall-back to raw string if it fails. + String title; + String message; + Level level = Level.INFO; + try { + final var announcement = new JSONObject(jsonString); + + title = announcement.getString("title"); + message = announcement.getJSONObject("content").getString("message"); + + if (!announcement.isNull("level")) level = Level.fromInt(announcement.getInt("level")); + } catch (Throwable ex) { + LogHelper.printException(() -> "Failed to parse announcement. Fall-backing to raw string", ex); + + title = "Announcement"; + message = jsonString; + } + + final var finalTitle = title; + final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT); + final Level finalLevel = level; + + ReVancedUtils.runOnMainThread(() -> { + // Show the announcement. + var alertDialog = new android.app.AlertDialog.Builder(context) + .setTitle(finalTitle) + .setMessage(finalMessage) + .setIcon(finalLevel.icon) + .setPositiveButton("Ok", (dialog, which) -> { + SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue(hash); + dialog.dismiss(); + }).setNegativeButton("Dismiss", (dialog, which) -> { + dialog.dismiss(); + }) + .setCancelable(false) + .show(); + + // Make links clickable. + ((TextView)alertDialog.findViewById(android.R.id.message)) + .setMovementMethod(LinkMovementMethod.getInstance()); + }); + } catch (Exception e) { + final var message = "Failed to get announcement"; + + LogHelper.printException(() -> message, e); + } + }); + } + + /** + * Clears the last announcement hash if it is not empty. + * + * @return true if the last announcement hash was empty. + */ + private static boolean emptyLastAnnouncementHash() { + if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return true; + SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue(""); + + return false; + } + + private static String getOrSetConsumer() { + final var consumer = SettingsEnum.ANNOUNCEMENT_CONSUMER.getString(); + if (!consumer.isEmpty()) return consumer; + + final var uuid = UUID.randomUUID().toString(); + SettingsEnum.ANNOUNCEMENT_CONSUMER.saveValue(uuid); + return uuid; + } + + // TODO: Use better icons. + private enum Level { + INFO(android.R.drawable.ic_dialog_info), + WARNING(android.R.drawable.ic_dialog_alert), + SEVERE(android.R.drawable.ic_dialog_alert); + + public final int icon; + + Level(int icon) { + this.icon = icon; + } + + public static Level fromInt(int value) { + return values()[Math.min(value, values().length - 1)]; + } + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/announcements/requests/AnnouncementsRoutes.java b/app/src/main/java/app/revanced/integrations/patches/announcements/requests/AnnouncementsRoutes.java new file mode 100644 index 00000000..fc39090b --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/announcements/requests/AnnouncementsRoutes.java @@ -0,0 +1,23 @@ +package app.revanced.integrations.patches.announcements.requests; + +import app.revanced.integrations.requests.Requester; +import app.revanced.integrations.requests.Route; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import static app.revanced.integrations.requests.Route.Method.GET; + +public class AnnouncementsRoutes { + private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v2"; + + + public static final Route GET_LATEST_ANNOUNCEMENT = new Route(GET, "/announcements/youtube/latest?consumer={consumer}"); + + private AnnouncementsRoutes() { + } + + public static HttpURLConnection getAnnouncementsConnectionFromRoute(Route route, String... params) throws IOException { + return Requester.getConnectionFromRoute(ANNOUNCEMENTS_PROVIDER, route, params); + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/patches/components/LayoutComponentsFilter.java b/app/src/main/java/app/revanced/integrations/patches/components/LayoutComponentsFilter.java index 4ee419d3..a5882d22 100644 --- a/app/src/main/java/app/revanced/integrations/patches/components/LayoutComponentsFilter.java +++ b/app/src/main/java/app/revanced/integrations/patches/components/LayoutComponentsFilter.java @@ -22,6 +22,7 @@ public final class LayoutComponentsFilter extends Filter { private final StringFilterGroup searchResultShelfHeader; private final StringFilterGroup inFeedSurvey; private final StringFilterGroup notifyMe; + private final StringFilterGroup expandableMetadata; @RequiresApi(api = Build.VERSION_CODES.N) public LayoutComponentsFilter() { @@ -114,7 +115,7 @@ public final class LayoutComponentsFilter extends Filter { "official_card" ); - final var expandableMetadata = new StringFilterGroup( + expandableMetadata = new StringFilterGroup( SettingsEnum.HIDE_EXPANDABLE_CHIP, "inline_expander" ); @@ -175,11 +176,17 @@ public final class LayoutComponentsFilter extends Filter { "chips_shelf" ); + final var channelWatermark = new StringFilterGroup( + SettingsEnum.HIDE_VIDEO_CHANNEL_WATERMARK, + "featured_channel_watermark_overlay" + ); + this.pathFilterGroupList.addAll( channelBar, communityPosts, paidContent, latestPosts, + channelWatermark, communityGuidelines, quickActions, expandableMetadata, @@ -211,7 +218,10 @@ public final class LayoutComponentsFilter extends Filter { public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) { - if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey) return true; + // The groups are excluded from the filter due to the exceptions list below. + // Filter them separately here. + if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata) + return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex); if (matchedGroup != custom && exceptions.matches(path)) return false; // Exceptions are not filtered. @@ -225,7 +235,6 @@ public final class LayoutComponentsFilter extends Filter { /** * Injection point. - * * Called from a different place then the other filters. */ public static boolean filterMixPlaylists(final byte[] bytes) { @@ -236,4 +245,8 @@ public final class LayoutComponentsFilter extends Filter { return isMixPlaylistFiltered; } + + public static boolean showWatermark() { + return !SettingsEnum.HIDE_VIDEO_CHANNEL_WATERMARK.getBoolean(); + } } diff --git a/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java index 5099a4df..a4051c55 100644 --- a/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/components/LithoFilterPatch.java @@ -425,15 +425,15 @@ public final class LithoFilterPatch { static { for (Filter filter : filters) { - filterGroupLists(pathSearchTree, filter, filter.pathFilterGroupList); filterGroupLists(identifierSearchTree, filter, filter.identifierFilterGroupList); + filterGroupLists(pathSearchTree, filter, filter.pathFilterGroupList); } LogHelper.printDebug(() -> "Using: " - + pathSearchTree.numberOfPatterns() + " path filters" - + " (" + pathSearchTree.getEstimatedMemorySize() + " KB), " + identifierSearchTree.numberOfPatterns() + " identifier filters" - + " (" + identifierSearchTree.getEstimatedMemorySize() + " KB)"); + + " (" + identifierSearchTree.getEstimatedMemorySize() + " KB), " + + pathSearchTree.numberOfPatterns() + " path filters" + + " (" + pathSearchTree.getEstimatedMemorySize() + " KB)"); } private static void filterGroupLists(TrieSearch pathSearchTree, diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldVideoQualityMenuPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldVideoQualityMenuPatch.java index 0d914ab3..47053674 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldVideoQualityMenuPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/quality/OldVideoQualityMenuPatch.java @@ -27,10 +27,13 @@ public final class OldVideoQualityMenuPatch { // Check if the current view is the quality menu. if (VideoQualityMenuFilterPatch.isVideoQualityMenuVisible) { VideoQualityMenuFilterPatch.isVideoQualityMenuVisible = false; - ((ViewGroup) recyclerView.getParent().getParent().getParent()).setVisibility(View.GONE); - // Click the "Advanced" quality menu to show the "old" quality menu. - ((ViewGroup) recyclerView.getChildAt(0)).getChildAt(3).performClick(); + ((ViewGroup) recyclerView.getParent().getParent().getParent()).setVisibility(View.GONE); + View advancedQualityView = ((ViewGroup) recyclerView.getChildAt(0)).getChildAt(3); + if (advancedQualityView != null) { + // Click the "Advanced" quality menu to show the "old" quality menu. + advancedQualityView.performClick(); + } } } catch (Exception ex) { LogHelper.printException(() -> "onFlyoutMenuCreate failure", ex); diff --git a/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofDeviceDimensionsPatch.java b/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofDeviceDimensionsPatch.java new file mode 100644 index 00000000..5ac5355c --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/SpoofDeviceDimensionsPatch.java @@ -0,0 +1,14 @@ +package app.revanced.integrations.patches.spoof; + +import app.revanced.integrations.settings.SettingsEnum; + +public class SpoofDeviceDimensionsPatch { + private static final boolean SPOOF = SettingsEnum.SPOOF_DEVICE_DIMENSIONS.getBoolean(); + public static int getMinHeightOrWidth(int minHeightOrWidth) { + return SPOOF ? 64 : minHeightOrWidth; + } + + public static int getMaxHeightOrWidth(int maxHeightOrWidth) { + return SPOOF ? 4096 : maxHeightOrWidth; + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/patches/spoof/requests/PlayerRoutes.java index db3971ba..a37dd6f3 100644 --- a/app/src/main/java/app/revanced/integrations/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/requests/PlayerRoutes.java @@ -3,6 +3,7 @@ package app.revanced.integrations.patches.spoof.requests; import app.revanced.integrations.requests.Requester; import app.revanced.integrations.requests.Route; import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; import org.json.JSONException; import org.json.JSONObject; @@ -75,7 +76,12 @@ final class PlayerRoutes { /** @noinspection SameParameterValue*/ static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route) throws IOException { var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); - connection.setRequestProperty("User-Agent", "com.google.android.youtube/18.37.36 (Linux; U; Android 12; GB) gzip"); + + connection.setRequestProperty( + "User-Agent", "com.google.android.youtube/" + + ReVancedUtils.getVersionName() + + " (Linux; U; Android 12; GB) gzip" + ); connection.setRequestProperty("X-Goog-Api-Format-Version", "2"); connection.setRequestProperty("Content-Type", "application/json"); diff --git a/app/src/main/java/app/revanced/integrations/patches/theme/ThemeLithoComponentsPatch.java b/app/src/main/java/app/revanced/integrations/patches/theme/ThemePatch.java similarity index 90% rename from app/src/main/java/app/revanced/integrations/patches/theme/ThemeLithoComponentsPatch.java rename to app/src/main/java/app/revanced/integrations/patches/theme/ThemePatch.java index da9f3963..e9796db2 100644 --- a/app/src/main/java/app/revanced/integrations/patches/theme/ThemeLithoComponentsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/theme/ThemePatch.java @@ -1,9 +1,10 @@ package app.revanced.integrations.patches.theme; +import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.utils.ReVancedUtils; import app.revanced.integrations.utils.ThemeHelper; -public class ThemeLithoComponentsPatch { +public class ThemePatch { // color constants used in relation with litho components private static final int[] WHITE_VALUES = { -1, // comments chip background @@ -40,6 +41,10 @@ public class ThemeLithoComponentsPatch { return originalValue; } + public static boolean gradientLoadingScreenEnabled() { + return SettingsEnum.GRADIENT_LOADING_SCREEN.getBoolean(); + } + private static int getBlackColor() { if (blackColor == 0) blackColor = ReVancedUtils.getResourceColor("yt_black1"); return blackColor; diff --git a/app/src/main/java/app/revanced/integrations/requests/Requester.java b/app/src/main/java/app/revanced/integrations/requests/Requester.java index c756dfe8..0a49ecac 100644 --- a/app/src/main/java/app/revanced/integrations/requests/Requester.java +++ b/app/src/main/java/app/revanced/integrations/requests/Requester.java @@ -1,5 +1,6 @@ package app.revanced.integrations.requests; +import app.revanced.integrations.utils.ReVancedUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -23,7 +24,7 @@ public class Requester { String url = apiUrl + route.getCompiledRoute(); HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestMethod(route.getMethod().name()); - connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";revanced"); + connection.setRequestProperty("User-Agent", System.getProperty("http.agent") + "; ReVanced/" + ReVancedUtils.getVersionName()); return connection; } diff --git a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java index 3affe384..a6f47755 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -10,10 +10,7 @@ import app.revanced.integrations.utils.StringRef; import org.json.JSONException; import org.json.JSONObject; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; +import java.util.*; import static app.revanced.integrations.settings.SettingsEnum.ReturnType.*; import static app.revanced.integrations.settings.SharedPrefCategory.RETURN_YOUTUBE_DISLIKE; @@ -123,6 +120,7 @@ public enum SettingsEnum { HIDE_SUBSCRIPTIONS_BUTTON("revanced_hide_subscriptions_button", BOOLEAN, FALSE, true), HIDE_TIMESTAMP("revanced_hide_timestamp", BOOLEAN, FALSE), HIDE_VIDEO_WATERMARK("revanced_hide_video_watermark", BOOLEAN, TRUE), + HIDE_VIDEO_CHANNEL_WATERMARK("revanced_hide_channel_watermark", BOOLEAN, TRUE), PLAYER_POPUP_PANELS("revanced_hide_player_popup_panels", BOOLEAN, FALSE), SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON("revanced_switch_create_with_notifications_button", BOOLEAN, TRUE, true), SPOOF_APP_VERSION("revanced_spoof_app_version", BOOLEAN, FALSE, true, "revanced_spoof_app_version_user_dialog_message"), @@ -130,6 +128,7 @@ public enum SettingsEnum { USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true), TABLET_LAYOUT("revanced_tablet_layout", BOOLEAN, FALSE, true, "revanced_tablet_layout_user_dialog_message"), WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true), + GRADIENT_LOADING_SCREEN("revanced_gradient_loading_screen", BOOLEAN, FALSE), SEEKBAR_CUSTOM_COLOR("revanced_seekbar_custom_color", BOOLEAN, TRUE, true), SEEKBAR_CUSTOM_COLOR_VALUE("revanced_seekbar_custom_color_value", STRING, "#FF0000", true, parents(SEEKBAR_CUSTOM_COLOR)), HIDE_FILTER_BAR_FEED_IN_FEED("revanced_hide_filter_bar_feed_in_feed", BOOLEAN, FALSE, true), @@ -175,7 +174,11 @@ public enum SettingsEnum { "revanced_spoof_signature_verification_enabled_user_dialog_message"), SPOOF_SIGNATURE_IN_FEED("revanced_spoof_signature_in_feed_enabled", BOOLEAN, FALSE, false, parents(SPOOF_SIGNATURE)), + SPOOF_DEVICE_DIMENSIONS("revanced_spoof_device_dimensions", BOOLEAN, FALSE, true), BYPASS_URL_REDIRECTS("revanced_bypass_url_redirects", BOOLEAN, TRUE), + ANNOUNCEMENTS("revanced_announcements", BOOLEAN, TRUE), + ANNOUNCEMENT_CONSUMER("revanced_announcement_consumer", STRING, ""), + ANNOUNCEMENT_LAST_HASH("revanced_announcement_last_hash", STRING, ""), // Swipe controls SWIPE_BRIGHTNESS("revanced_swipe_brightness", BOOLEAN, TRUE), @@ -374,6 +377,8 @@ public enum SettingsEnum { // region Migration + migrateOldSettingToNew(HIDE_VIDEO_WATERMARK, HIDE_VIDEO_CHANNEL_WATERMARK); + // Do _not_ delete this SB private user id migration property until sometime in 2024. // This is the only setting that cannot be reconfigured if lost, // and more time should be given for users who rarely upgrade. @@ -550,6 +555,7 @@ public enum SettingsEnum { private boolean includeWithImportExport() { switch (this) { case RYD_USER_ID: // Not useful to export, no reason to include it. + case ANNOUNCEMENT_CONSUMER: // Not useful to export, no reason to include it. case SB_LAST_VIP_CHECK: case SB_HIDE_EXPORT_WARNING: case SB_SEEN_GUIDELINES: diff --git a/app/src/main/java/app/revanced/integrations/utils/ByteTrieSearch.java b/app/src/main/java/app/revanced/integrations/utils/ByteTrieSearch.java index 807f3f8a..02a1ff70 100644 --- a/app/src/main/java/app/revanced/integrations/utils/ByteTrieSearch.java +++ b/app/src/main/java/app/revanced/integrations/utils/ByteTrieSearch.java @@ -8,9 +8,17 @@ import java.util.Objects; public final class ByteTrieSearch extends TrieSearch { private static final class ByteTrieNode extends TrieNode { - TrieNode createNode() { - return new ByteTrieNode(); + ByteTrieNode() { + super(); } + ByteTrieNode(char nodeCharacterValue) { + super(nodeCharacterValue); + } + @Override + TrieNode createNode(char nodeCharacterValue) { + return new ByteTrieNode(nodeCharacterValue); + } + @Override char getCharValue(byte[] text, int index) { return (char) text[index]; } diff --git a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java index 286a03bf..918d55b7 100644 --- a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java +++ b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java @@ -2,8 +2,11 @@ package app.revanced.integrations.utils; import android.annotation.SuppressLint; import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.net.ConnectivityManager; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.View; @@ -25,9 +28,36 @@ public class ReVancedUtils { @SuppressLint("StaticFieldLeak") public static Context context; + private static String versionName; + private ReVancedUtils() { } // utility class + public static String getVersionName() { + if (versionName != null) return versionName; + + PackageInfo packageInfo; + try { + final var packageName = Objects.requireNonNull(getContext()).getPackageName(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + packageInfo = context.getPackageManager().getPackageInfo( + packageName, + PackageManager.PackageInfoFlags.of(0) + ); + else + packageInfo = context.getPackageManager().getPackageInfo( + packageName, + 0 + ); + } catch (PackageManager.NameNotFoundException e) { + LogHelper.printException(() -> "Failed to get package info", e); + return null; + } + + return versionName = packageInfo.versionName; + } + /** * Hide a view by setting its layout height and width to 1dp. * diff --git a/app/src/main/java/app/revanced/integrations/utils/StringTrieSearch.java b/app/src/main/java/app/revanced/integrations/utils/StringTrieSearch.java index 1a1a0a9e..28d960cf 100644 --- a/app/src/main/java/app/revanced/integrations/utils/StringTrieSearch.java +++ b/app/src/main/java/app/revanced/integrations/utils/StringTrieSearch.java @@ -11,9 +11,17 @@ import java.util.Objects; public final class StringTrieSearch extends TrieSearch { private static final class StringTrieNode extends TrieNode { - TrieNode createNode() { - return new StringTrieNode(); + StringTrieNode() { + super(); } + StringTrieNode(char nodeCharacterValue) { + super(nodeCharacterValue); + } + @Override + TrieNode createNode(char nodeValue) { + return new StringTrieNode(nodeValue); + } + @Override char getCharValue(String text, int index) { return text.charAt(index); } diff --git a/app/src/main/java/app/revanced/integrations/utils/TrieSearch.java b/app/src/main/java/app/revanced/integrations/utils/TrieSearch.java index 8b9fecb8..d42c305c 100644 --- a/app/src/main/java/app/revanced/integrations/utils/TrieSearch.java +++ b/app/src/main/java/app/revanced/integrations/utils/TrieSearch.java @@ -71,15 +71,31 @@ public abstract class TrieSearch { } static abstract class TrieNode { + /** + * Dummy value used for root node. Value can be anything as it's never referenced. + */ + private static final char ROOT_NODE_CHARACTER_VALUE = 0; // ASCII null character. + // Support only ASCII letters/numbers/symbols and filter out all control characters. private static final char MIN_VALID_CHAR = 32; // Space character. private static final char MAX_VALID_CHAR = 126; // 127 = delete character. - private static final int NUMBER_OF_CHILDREN = MAX_VALID_CHAR - MIN_VALID_CHAR + 1; + + /** + * How much to expand the children array when resizing. + */ + private static final int CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT = 2; + private static final int CHILDREN_ARRAY_MAX_SIZE = MAX_VALID_CHAR - MIN_VALID_CHAR + 1; private static boolean isInvalidRange(char character) { return character < MIN_VALID_CHAR || character > MAX_VALID_CHAR; } + /** + * Character this node represents. + * This field is ignored for the root node (which does not represent any character). + */ + private final char nodeValue; + /** * A compressed graph path that represents the remaining pattern characters of a single child node. * @@ -91,6 +107,24 @@ public abstract class TrieSearch { /** * All child nodes. Only present if no compressed leaf exist. + * + * Array is dynamically increased in size as needed, + * and uses perfect hashing for the elements it contains. + * + * So if the array contains a given character, + * the character will always map to the node with index: (character % arraySize). + * + * Elements not contained can collide with elements the array does contain, + * so must compare the nodes character value. + * + * Alternatively this array could be a sorted and densely packed array, + * and lookup is done using binary search. + * That would save a small amount of memory because there's no null children entries, + * but would give a worst case search of O(nlog(m)) where n is the number of + * characters in the searched text and m is the maximum size of the sorted character arrays. + * Using a hash table array always gives O(n) search time. + * The memory usage here is very small (all Litho filters use ~10KB of memory), + * so the more performant hash implementation is chosen. */ @Nullable private TrieNode[] children; @@ -101,6 +135,13 @@ public abstract class TrieSearch { @Nullable private List> endOfPatternCallback; + TrieNode() { + this.nodeValue = ROOT_NODE_CHARACTER_VALUE; + } + TrieNode(char nodeCharacterValue) { + this.nodeValue = nodeCharacterValue; + } + /** * @param pattern Pattern to add. * @param patternLength Length of the pattern. @@ -121,7 +162,7 @@ public abstract class TrieSearch { // Recursively call back into this method and push the existing leaf down 1 level. if (children != null) throw new IllegalStateException(); //noinspection unchecked - children = new TrieNode[NUMBER_OF_CHILDREN]; + children = new TrieNode[1]; TrieCompressedPath temp = leaf; leaf = null; addPattern(temp.pattern, temp.patternLength, temp.patternStartIndex, temp.callback); @@ -130,19 +171,65 @@ public abstract class TrieSearch { leaf = new TrieCompressedPath<>(pattern, patternLength, patternIndex, callback); return; } - char character = getCharValue(pattern, patternIndex); + final char character = getCharValue(pattern, patternIndex); if (isInvalidRange(character)) { throw new IllegalArgumentException("invalid character at index " + patternIndex + ": " + pattern); } - character -= MIN_VALID_CHAR; // Adjust to the array range. - TrieNode child = children[character]; + final int arrayIndex = hashIndexForTableSize(children.length, character); + TrieNode child = children[arrayIndex]; if (child == null) { - child = createNode(); - children[character] = child; + child = createNode(character); + children[arrayIndex] = child; + } else if (child.nodeValue != character) { + // Hash collision. Resize the table until perfect hashing is found. + child = createNode(character); + expandChildArray(child); } child.addPattern(pattern, patternLength, patternIndex + 1, callback); } + /** + * Resizes the children table until all nodes hash to exactly one array index. + * Worse case, this will resize the array to {@link #CHILDREN_ARRAY_MAX_SIZE} elements. + */ + private void expandChildArray(TrieNode child) { + int replacementArraySize = Objects.requireNonNull(children).length; + while (true) { + replacementArraySize += CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT; + //noinspection unchecked + TrieNode[] replacement = new TrieNode[replacementArraySize]; + addNodeToArray(replacement, child); + boolean collision = false; + for (TrieNode existingChild : children) { + if (existingChild != null) { + if (!addNodeToArray(replacement, existingChild)) { + collision = true; + break; + } + } + } + if (collision) { + if (replacementArraySize > CHILDREN_ARRAY_MAX_SIZE) throw new IllegalStateException(); + continue; + } + children = replacement; + return; + } + } + + private static boolean addNodeToArray(TrieNode[] array, TrieNode childToAdd) { + final int insertIndex = hashIndexForTableSize(array.length, childToAdd.nodeValue); + if (array[insertIndex] != null ) { + return false; // Collision. + } + array[insertIndex] = childToAdd; + return true; + } + + private static int hashIndexForTableSize(int arraySize, char nodeValue) { + return (nodeValue - MIN_VALID_CHAR) % arraySize; + } + /** * @param searchText Text to search for patterns in. * @param searchTextLength Length of the search text. @@ -170,18 +257,17 @@ public abstract class TrieSearch { if (children == null) { return false; // Reached a graph end point and there's no further patterns to search. } - if (searchTextIndex == searchTextLength) { return false; // Reached end of the search text and found no matches. } - char character = getCharValue(searchText, searchTextIndex); + final char character = getCharValue(searchText, searchTextIndex); if (isInvalidRange(character)) { return false; // Not an ASCII letter/number/symbol. } - character -= MIN_VALID_CHAR; // Adjust to the array range. - TrieNode child = children[character]; - if (child == null) { + final int arrayIndex = hashIndexForTableSize(children.length, character); + TrieNode child = children[arrayIndex]; + if (child == null || child.nodeValue != character) { return false; } return child.matches(searchText, searchTextLength, searchTextIndex + 1, @@ -194,7 +280,7 @@ public abstract class TrieSearch { * @return Estimated number of memory pointers used, starting from this node and including all children. */ private int estimatedNumberOfPointersUsed() { - int numberOfPointers = 3; // Number of fields in this class. + int numberOfPointers = 4; // Number of fields in this class. if (leaf != null) { numberOfPointers += 4; // Number of fields in leaf node. } @@ -202,7 +288,7 @@ public abstract class TrieSearch { numberOfPointers += endOfPatternCallback.size(); } if (children != null) { - numberOfPointers += NUMBER_OF_CHILDREN; + numberOfPointers += children.length; for (TrieNode child : children) { if (child != null) { numberOfPointers += child.estimatedNumberOfPointersUsed(); @@ -212,7 +298,7 @@ public abstract class TrieSearch { return numberOfPointers; } - abstract TrieNode createNode(); + abstract TrieNode createNode(char nodeValue); abstract char getCharValue(T text, int index); } diff --git a/gradle.properties b/gradle.properties index e75081b5..143e2e8b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 0.119.2 +version = 0.120.0-dev.6