diff --git a/app/src/main/java/app/revanced/integrations/patches/AlternativeThumbnailsPatch.java b/app/src/main/java/app/revanced/integrations/patches/AlternativeThumbnailsPatch.java index 1e4d3f05..49d84103 100644 --- a/app/src/main/java/app/revanced/integrations/patches/AlternativeThumbnailsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/AlternativeThumbnailsPatch.java @@ -1,11 +1,17 @@ package app.revanced.integrations.patches; +import android.net.Uri; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; +import org.chromium.net.UrlRequest; import org.chromium.net.UrlResponseInfo; +import org.chromium.net.impl.CronetUrlRequest; +import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.HashMap; @@ -13,30 +19,289 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ExecutionException; -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; +import static app.revanced.integrations.utils.StringRef.str; /** - * Alternative YouTube thumbnails, showing the beginning/middle/end of the video. + * Alternative YouTube thumbnails. + *

+ * Can show YouTube provided screen captures of beginning/middle/end of the video. * (ie: sd1.jpg, sd2.jpg, sd3.jpg). - * - * Has an additional option to use 'fast' thumbnails, + *

+ * Or can show crowdsourced thumbnails provided by DeArrow (...). + *

+ * Or can use DeArrow and fall back to screen captures if DeArrow is not available. + *

+ * Has an additional option to use 'fast' video still thumbnails, * where it forces sd thumbnail quality and skips verifying if the alt thumbnail image exists. - * The UI loading time will be the same or better than using the the original thumbnails, + * The UI loading time will be the same or better than using original thumbnails, * but thumbnails will initially fail to load for all live streams, unreleased, and occasionally very old videos. * If a failed thumbnail load is reloaded (ie: scroll off, then on screen), then the original thumbnail * is reloaded instead. Fast thumbnails requires using SD or lower thumbnail resolution, - * because a noticeable number of videos do not have hq720 and too many fail to load. - * + * because a noticeable number of videos do not have hq720 and too much fail to load. + *

* Ideas for improvements: * - Selectively allow using original thumbnails in some situations, * such as videos subscription feed, watch history, or in search results. * - Save to a temporary file the video id's verified to have alt thumbnails. * This would speed up loading the watch history and users saved playlists. */ +@SuppressWarnings("unused") public final class AlternativeThumbnailsPatch { + private static final Uri dearrowApiUri; + + /** + * The scheme and host of {@link #dearrowApiUri}. + */ + private static final String deArrowApiUrlPrefix; + + /** + * How long to temporarily turn off DeArrow if it fails for any reason. + */ + private static final long DEARROW_FAILURE_API_BACKOFF_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes. + + /** + * If non zero, then the system time of when DeArrow API calls can resume. + */ + private static volatile long timeToResumeDeArrowAPICalls; + + static { + dearrowApiUri = validateSettings(); + final int port = dearrowApiUri.getPort(); + String portString = port == -1 ? "" : (":" + port); + deArrowApiUrlPrefix = dearrowApiUri.getScheme() + "://" + dearrowApiUri.getHost() + portString + "/"; + LogHelper.printDebug(() -> "Using DeArrow API address: " + deArrowApiUrlPrefix); + } + + /** + * Fix any bad imported data. + */ + private static Uri validateSettings() { + final int altThumbnailType = SettingsEnum.ALT_THUMBNAIL_STILLS_TIME.getInt(); + if (altThumbnailType < 1 || altThumbnailType > 3) { + ReVancedUtils.showToastLong("Invalid Alternative still thumbnail type: " + + altThumbnailType + ". Using default"); + SettingsEnum.ALT_THUMBNAIL_STILLS_TIME.resetToDefault(); + } + + Uri apiUri = Uri.parse(SettingsEnum.ALT_THUMBNAIL_DEARROW_API_URL.getString()); + // Cannot use unsecured 'http', otherwise the connections fail to start and no callbacks hooks are made. + String scheme = apiUri.getScheme(); + if (scheme == null || scheme.equals("http") || apiUri.getHost() == null) { + ReVancedUtils.showToastLong("Invalid DeArrow API URL. Using default"); + SettingsEnum.ALT_THUMBNAIL_DEARROW_API_URL.resetToDefault(); + return validateSettings(); + } + return apiUri; + } + + private static boolean usingDeArrow() { + return SettingsEnum.ALT_THUMBNAIL_DEARROW.getBoolean(); + } + + private static boolean usingVideoStills() { + return SettingsEnum.ALT_THUMBNAIL_STILLS.getBoolean(); + } + + /** + * Build the alternative thumbnail url using YouTube provided still video captures. + * + * @param decodedUrl Decoded original thumbnail request url. + * @return The alternative thumbnail url, or the original url. Both without tracking parameters. + */ + @NonNull + private static String buildYoutubeVideoStillURL(@NonNull DecodedThumbnailUrl decodedUrl, + @NonNull ThumbnailQuality qualityToUse) { + String sanitizedReplacement = decodedUrl.createStillsUrl(qualityToUse, false); + if (VerifiedQualities.verifyAltThumbnailExist(decodedUrl.videoId, qualityToUse, sanitizedReplacement)) { + return sanitizedReplacement; + } + return decodedUrl.sanitizedUrl; + } + + /** + * Build the alternative thumbnail url using DeArrow thumbnail cache. + * + * @param videoId ID of the video to get a thumbnail of. Can be any video (regular or Short). + * @param fallbackUrl URL to fall back to in case. + * @return The alternative thumbnail url, without tracking parameters. + */ + @NonNull + private static String buildDeArrowThumbnailURL(String videoId, String fallbackUrl) { + // Build thumbnail request url. + // See https://github.com/ajayyy/DeArrowThumbnailCache/blob/29eb4359ebdf823626c79d944a901492d760bbbc/app.py#L29. + return dearrowApiUri + .buildUpon() + .appendQueryParameter("videoID", videoId) + .appendQueryParameter("redirectUrl", fallbackUrl) + .build() + .toString(); + } + + private static boolean urlIsDeArrow(@NonNull String imageUrl) { + return imageUrl.startsWith(deArrowApiUrlPrefix); + } + + /** + * @return If this client has not recently experienced any DeArrow API errors. + */ + private static boolean canUseDeArrowAPI() { + if (timeToResumeDeArrowAPICalls == 0) { + return true; + } + if (timeToResumeDeArrowAPICalls < System.currentTimeMillis()) { + LogHelper.printDebug(() -> "Resuming DeArrow API calls"); + timeToResumeDeArrowAPICalls = 0; + return true; + } + return false; + } + + private static void handleDeArrowError(@NonNull String url, int statusCode) { + LogHelper.printDebug(() -> "Encountered DeArrow error. Url: " + url); + final long now = System.currentTimeMillis(); + if (timeToResumeDeArrowAPICalls < now) { + timeToResumeDeArrowAPICalls = now + DEARROW_FAILURE_API_BACKOFF_MILLISECONDS; + if (SettingsEnum.ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST.getBoolean()) { + String toastMessage = (statusCode != 0) + ? str("revanced_alt_thumbnail_dearrow_error", statusCode) + : str("revanced_alt_thumbnail_dearrow_error_generic"); + ReVancedUtils.showToastLong(toastMessage); + } + } + } + + /** + * Injection point. Called off the main thread and by multiple threads at the same time. + * + * @param originalUrl Image url for all url images loaded, including video thumbnails. + */ + public static String overrideImageURL(String originalUrl) { + try { + final boolean usingDeArrow = usingDeArrow(); + final boolean usingVideoStills = usingVideoStills(); + if (!usingDeArrow && !usingVideoStills) { + return originalUrl; + } + + final var decodedUrl = DecodedThumbnailUrl.decodeImageUrl(originalUrl); + if (decodedUrl == null) { + return originalUrl; // Not a thumbnail. + } + + LogHelper.printDebug(() -> "Original url: " + decodedUrl.sanitizedUrl); + + ThumbnailQuality qualityToUse = ThumbnailQuality.getQualityToUse(decodedUrl.imageQuality); + if (qualityToUse == null) { + // Thumbnail is a Short or a Storyboard image used for seekbar thumbnails (must not replace these). + return originalUrl; + } + + String sanitizedReplacementUrl; + final boolean includeTracking; + if (usingDeArrow && canUseDeArrowAPI()) { + includeTracking = false; // Do not include view tracking parameters with API call. + final String fallbackUrl = usingVideoStills + ? buildYoutubeVideoStillURL(decodedUrl, qualityToUse) + : decodedUrl.sanitizedUrl; + + sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl); + } else if (usingVideoStills) { + includeTracking = true; // Include view tracking parameters if present. + sanitizedReplacementUrl = buildYoutubeVideoStillURL(decodedUrl, qualityToUse); + } else { + return originalUrl; // Recently experienced DeArrow failure and video stills are not enabled. + } + + // Do not log any tracking parameters. + LogHelper.printDebug(() -> "Replacement url: " + sanitizedReplacementUrl); + + return includeTracking + ? sanitizedReplacementUrl + decodedUrl.viewTrackingParameters + : sanitizedReplacementUrl; + } catch (Exception ex) { + LogHelper.printException(() -> "overrideImageURL failure", ex); + return originalUrl; + } + } + + /** + * Injection point. + *

+ * Cronet considers all completed connections as a success, even if the response is 404 or 5xx. + */ + public static void handleCronetSuccess(UrlRequest request, @NonNull UrlResponseInfo responseInfo) { + try { + final int statusCode = responseInfo.getHttpStatusCode(); + if (statusCode != 200) { + String url = responseInfo.getUrl(); + + if (usingDeArrow() && urlIsDeArrow(url)) { + LogHelper.printDebug(() -> "handleCronetSuccess, statusCode: " + statusCode); + handleDeArrowError(url, statusCode); + return; + } + + if (usingVideoStills() && statusCode == 404) { + // Fast alt thumbnails is enabled and the thumbnail is not available. + // The video is: + // - live stream + // - upcoming unreleased video + // - very old + // - very low view count + // Take note of this, so if the image reloads the original thumbnail will be used. + DecodedThumbnailUrl decodedUrl = DecodedThumbnailUrl.decodeImageUrl(url); + if (decodedUrl == null) { + return; // Not a thumbnail. + } + + LogHelper.printDebug(() -> "handleCronetSuccess, image not available: " + url); + + ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedUrl.imageQuality); + if (quality == null) { + // Video is a short or a seekbar thumbnail, but somehow did not load. Should not happen. + LogHelper.printDebug(() -> "Failed to recognize image quality of url: " + decodedUrl.sanitizedUrl); + return; + } + + VerifiedQualities.setAltThumbnailDoesNotExist(decodedUrl.videoId, quality); + } + } + } catch (Exception ex) { + LogHelper.printException(() -> "Callback success error", ex); + } + } + + /** + * Injection point. + *

+ * To test failure cases, try changing the API URL to each of: + * - A non-existent domain. + * - A url path of something incorrect (ie: /v1/nonExistentEndPoint). + *

+ * Known limitation: YT uses an infinite timeout, so this hook is never called if a host never responds. + * But this does not appear to be a problem, as the DeArrow API has not been observed to 'go silent' + * Instead if there's a problem it returns an error code status response, which is handled in this patch. + */ + public static void handleCronetFailure(UrlRequest request, + @Nullable UrlResponseInfo responseInfo, + IOException exception) { + try { + if (usingDeArrow()) { + String url = ((CronetUrlRequest) request).getHookedUrl(); + if (urlIsDeArrow(url)) { + LogHelper.printDebug(() -> "handleCronetFailure, exception: " + exception); + final int statusCode = (responseInfo != null) + ? responseInfo.getHttpStatusCode() + : 0; + handleDeArrowError(url, statusCode); + } + } + } catch (Exception ex) { + LogHelper.printException(() -> "Callback failure error", ex); + } + } + private enum ThumbnailQuality { // In order of lowest to highest resolution. DEFAULT("default", ""), // effective alt name is 1.jpg, 2.jpg, 3.jpg @@ -61,6 +326,11 @@ public final class AlternativeThumbnailsPatch { originalNameToEnum.put(quality.originalName, quality); for (int i = 1; i <= 3; i++) { + // 'custom' thumbnails set by the content creator. + // These show up in place of regular thumbnails + // and seem to be limited to [1, 3] range. + originalNameToEnum.put(quality.originalName + "_custom_" + i, quality); + altNameToEnum.put(quality.altImageName + i, quality); } } @@ -86,7 +356,7 @@ public final class AlternativeThumbnailsPatch { return null; // Not a thumbnail for a regular video. } - final boolean useFastQuality = SettingsEnum.ALT_THUMBNAIL_FAST_QUALITY.getBoolean(); + final boolean useFastQuality = SettingsEnum.ALT_THUMBNAIL_STILLS_FAST.getBoolean(); switch (quality) { case SDDEFAULT: // SD alt images have somewhat worse quality with washed out color and poor contrast. @@ -121,7 +391,7 @@ public final class AlternativeThumbnailsPatch { } String getAltImageNameToUse() { - return altImageName + SettingsEnum.ALT_THUMBNAIL_TYPE.getInt(); + return altImageName + SettingsEnum.ALT_THUMBNAIL_STILLS_TIME.getInt(); } } @@ -146,7 +416,7 @@ public final class AlternativeThumbnailsPatch { @Override protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > CACHE_LIMIT; // Evict oldest entry if over the cache limit. + return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. } }; @@ -166,13 +436,14 @@ public final class AlternativeThumbnailsPatch { static boolean verifyAltThumbnailExist(@NonNull String videoId, @NonNull ThumbnailQuality quality, @NonNull String imageUrl) { - VerifiedQualities verified = getVerifiedQualities(videoId, SettingsEnum.ALT_THUMBNAIL_FAST_QUALITY.getBoolean()); + VerifiedQualities verified = getVerifiedQualities(videoId, SettingsEnum.ALT_THUMBNAIL_STILLS_FAST.getBoolean()); if (verified == null) return true; // Fast alt thumbnails is enabled. return verified.verifyYouTubeThumbnailExists(videoId, quality, imageUrl); } static void setAltThumbnailDoesNotExist(@NonNull String videoId, @NonNull ThumbnailQuality quality) { VerifiedQualities verified = getVerifiedQualities(videoId, false); + //noinspection ConstantConditions verified.setQualityVerified(videoId, quality, false); } @@ -180,20 +451,20 @@ public final class AlternativeThumbnailsPatch { * Highest quality verified as existing. */ @Nullable - ThumbnailQuality highestQualityVerified; + private ThumbnailQuality highestQualityVerified; /** * Lowest quality verified as not existing. */ @Nullable - ThumbnailQuality lowestQualityNotAvailable; + private ThumbnailQuality lowestQualityNotAvailable; /** * System time, of when to invalidate {@link #lowestQualityNotAvailable}. * Used only if fast mode is not enabled. */ - long timeToReVerifyLowestQuality; + private long timeToReVerifyLowestQuality; - synchronized void setQualityVerified(String videoId, ThumbnailQuality quality, boolean isVerified) { + private synchronized void setQualityVerified(String videoId, ThumbnailQuality quality, boolean isVerified) { if (isVerified) { if (highestQualityVerified == null || highestQualityVerified.ordinal() < quality.ordinal()) { highestQualityVerified = quality; @@ -216,7 +487,7 @@ public final class AlternativeThumbnailsPatch { return true; // Previously verified as existing. } - final boolean fastQuality = SettingsEnum.ALT_THUMBNAIL_FAST_QUALITY.getBoolean(); + final boolean fastQuality = SettingsEnum.ALT_THUMBNAIL_STILLS_FAST.getBoolean(); if (lowestQualityNotAvailable != null && lowestQualityNotAvailable.ordinal() <= quality.ordinal()) { if (fastQuality || System.currentTimeMillis() < timeToReVerifyLowestQuality) { return false; // Previously verified as not existing. @@ -279,131 +550,61 @@ public final class AlternativeThumbnailsPatch { static DecodedThumbnailUrl decodeImageUrl(String url) { final int videoIdStartIndex = url.indexOf('/', YOUTUBE_THUMBNAIL_PREFIX.length()) + 1; if (videoIdStartIndex <= 0) return null; + final int videoIdEndIndex = url.indexOf('/', videoIdStartIndex); if (videoIdEndIndex < 0) return null; + final int imageSizeStartIndex = videoIdEndIndex + 1; final int imageSizeEndIndex = url.indexOf('.', imageSizeStartIndex); if (imageSizeEndIndex < 0) return null; + int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex); if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length(); + return new DecodedThumbnailUrl(url, videoIdStartIndex, videoIdEndIndex, imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex); } + final String originalFullUrl; /** Full usable url, but stripped of any tracking information. */ final String sanitizedUrl; - /** Url up to the video id. */ + /** Url up to the video ID. */ final String urlPrefix; final String videoId; /** Quality, such as hq720 or sddefault. */ final String imageQuality; - /** jpg or webp */ + /** JPG or WEBP */ final String imageExtension; /** User view tracking parameters, only present on some images. */ - final String urlTrackingParameters; + final String viewTrackingParameters; - private DecodedThumbnailUrl(String fullUrl, int videoIdStartIndex, int videoIdEndIndex, - int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) { + DecodedThumbnailUrl(String fullUrl, int videoIdStartIndex, int videoIdEndIndex, + int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) { + originalFullUrl = fullUrl; sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex); urlPrefix = fullUrl.substring(0, videoIdStartIndex); videoId = fullUrl.substring(videoIdStartIndex, videoIdEndIndex); imageQuality = fullUrl.substring(imageSizeStartIndex, imageSizeEndIndex); imageExtension = fullUrl.substring(imageSizeEndIndex + 1, imageExtensionEndIndex); - urlTrackingParameters = (imageExtensionEndIndex == fullUrl.length()) + viewTrackingParameters = (imageExtensionEndIndex == fullUrl.length()) ? "" : fullUrl.substring(imageExtensionEndIndex); } - } - - static { - // Fix any bad imported data. - final int altThumbnailType = SettingsEnum.ALT_THUMBNAIL_TYPE.getInt(); - if (altThumbnailType < 1 || altThumbnailType > 3) { - LogHelper.printException(() -> "Invalid alt thumbnail type: " + altThumbnailType); - SettingsEnum.ALT_THUMBNAIL_TYPE.saveValue(SettingsEnum.ALT_THUMBNAIL_TYPE.defaultValue); - } - } - - /** - * Injection point. Called off the main thread and by multiple threads at the same time. - * - * @param originalUrl Image url for all url images loaded, including video thumbnails. - */ - public static String overrideImageURL(String originalUrl) { - try { - if (!SettingsEnum.ALT_THUMBNAIL.getBoolean()) { - return originalUrl; - } - DecodedThumbnailUrl decodedUrl = DecodedThumbnailUrl.decodeImageUrl(originalUrl); - if (decodedUrl == null) { - return originalUrl; // Not a thumbnail. - } - - // Keep any tracking parameters out of the logs, and log only the base url. - LogHelper.printDebug(() -> "Original url: " + decodedUrl.sanitizedUrl); - - ThumbnailQuality qualityToUse = ThumbnailQuality.getQualityToUse(decodedUrl.imageQuality); - if (qualityToUse == null) return originalUrl; // Video is a short. + /** @noinspection SameParameterValue*/ + String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) { // Images could be upgraded to webp if they are not already, but this fails quite often, // especially for new videos uploaded in the last hour. // And even if alt webp images do exist, sometimes they can load much slower than the original jpg alt images. // (as much as 4x slower has been observed, despite the alt webp image being a smaller file). - - StringBuilder builder = new StringBuilder(originalUrl.length() + 2); - builder.append(decodedUrl.urlPrefix); - builder.append(decodedUrl.videoId).append('/'); + StringBuilder builder = new StringBuilder(originalFullUrl.length() + 2); + builder.append(urlPrefix); + builder.append(videoId).append('/'); builder.append(qualityToUse.getAltImageNameToUse()); - builder.append('.').append(decodedUrl.imageExtension); - - String sanitizedReplacement = builder.toString(); - if (!VerifiedQualities.verifyAltThumbnailExist(decodedUrl.videoId, qualityToUse, sanitizedReplacement)) { - return originalUrl; + builder.append('.').append(imageExtension); + if (includeViewTracking) { + builder.append(viewTrackingParameters); } - - LogHelper.printDebug(() -> "Replaced url: " + sanitizedReplacement); - - // URL tracking parameters. Presumably they are to determine if a user has viewed a thumbnail. - // This likely is used for recommendations, so they are retained if present. - builder.append(decodedUrl.urlTrackingParameters); return builder.toString(); - } catch (Exception ex) { - LogHelper.printException(() -> "Alt thumbnails failure", ex); - return originalUrl; } } - - /** - * Injection point. - * - * Cronet considers all completed connections as a success, even if the response is 404 or 5xx. - */ - public static void handleCronetSuccess(@NonNull UrlResponseInfo responseInfo) { - try { - if (responseInfo.getHttpStatusCode() == 404 && SettingsEnum.ALT_THUMBNAIL.getBoolean()) { - // Fast alt thumbnails is enabled and the thumbnail is not available. - // The video is: - // - live stream - // - upcoming unreleased video - // - very old - // - very low view count - // Take note of this, so if the image reloads the original thumbnail will be used. - DecodedThumbnailUrl decodedUrl = DecodedThumbnailUrl.decodeImageUrl(responseInfo.getUrl()); - if (decodedUrl == null) { - return; // Not a thumbnail. - } - - ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedUrl.imageQuality); - if (quality == null) { - // Video is a short or unknown quality, but the url returned 404. Should never happen. - LogHelper.printDebug(() -> "Failed to load unknown url: " + decodedUrl.sanitizedUrl); - return; - } - - VerifiedQualities.setAltThumbnailDoesNotExist(decodedUrl.videoId, quality); - } - } catch (Exception ex) { - LogHelper.printException(() -> "Alt thumbnails callback failure", ex); - } - } - } 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 index 9961c4be..81148c1f 100644 --- a/app/src/main/java/app/revanced/integrations/patches/announcements/AnnouncementsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/announcements/AnnouncementsPatch.java @@ -46,7 +46,7 @@ public final class AnnouncementsPatch { if (connection.getResponseCode() != 200) { if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return; - SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue(""); + SettingsEnum.ANNOUNCEMENT_LAST_HASH.resetToDefault(); ReVancedUtils.showToastLong("Failed to get announcement"); return; @@ -121,7 +121,7 @@ public final class AnnouncementsPatch { */ private static boolean emptyLastAnnouncementHash() { if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return true; - SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue(""); + SettingsEnum.ANNOUNCEMENT_LAST_HASH.resetToDefault(); return false; } 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 8712a551..96b0976e 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 @@ -133,7 +133,7 @@ final class CustomFilterGroup extends StringFilterGroup { for (String pattern : patterns) { if (!StringTrieSearch.isValidPattern(pattern)) { ReVancedUtils.showToastLong("Invalid custom filter, resetting to default"); - setting.saveValue(setting.defaultValue); + setting.resetToDefault(); return getFilterPatterns(setting); } } diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomPlaybackSpeedPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomPlaybackSpeedPatch.java index 3c281722..12e962d0 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomPlaybackSpeedPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomPlaybackSpeedPatch.java @@ -43,7 +43,7 @@ public class CustomPlaybackSpeedPatch { private static void resetCustomSpeeds(@NonNull String toastMessage) { ReVancedUtils.showToastLong(toastMessage); - SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.saveValue(SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.defaultValue); + SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.resetToDefault(); } private static void loadCustomSpeeds() { diff --git a/app/src/main/java/app/revanced/integrations/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/patches/theme/SeekbarColorPatch.java index 942ff3c6..41fa877d 100644 --- a/app/src/main/java/app/revanced/integrations/patches/theme/SeekbarColorPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/theme/SeekbarColorPatch.java @@ -48,7 +48,7 @@ public final class SeekbarColorPatch { Color.colorToHSV(seekbarColor, customSeekbarColorHSV); } catch (Exception ex) { ReVancedUtils.showToastShort("Invalid seekbar color value. Using default value."); - SettingsEnum.SEEKBAR_CUSTOM_COLOR_VALUE.saveValue(SettingsEnum.SEEKBAR_CUSTOM_COLOR_VALUE.defaultValue); + SettingsEnum.SEEKBAR_CUSTOM_COLOR_VALUE.resetToDefault(); loadCustomSeekbarColor(); } } 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 e4b9550a..2b18ab78 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -56,9 +56,13 @@ public enum SettingsEnum { HIDE_WEB_SEARCH_RESULTS("revanced_hide_web_search_results", BOOLEAN, TRUE), // Layout - ALT_THUMBNAIL("revanced_alt_thumbnail", BOOLEAN, FALSE), - ALT_THUMBNAIL_TYPE("revanced_alt_thumbnail_type", INTEGER, 2, parents(ALT_THUMBNAIL)), - ALT_THUMBNAIL_FAST_QUALITY("revanced_alt_thumbnail_fast_quality", BOOLEAN, FALSE, parents(ALT_THUMBNAIL)), + ALT_THUMBNAIL_STILLS("revanced_alt_thumbnail_stills", BOOLEAN, FALSE), + ALT_THUMBNAIL_STILLS_TIME("revanced_alt_thumbnail_stills_time", INTEGER, 2, parents(ALT_THUMBNAIL_STILLS)), + ALT_THUMBNAIL_STILLS_FAST("revanced_alt_thumbnail_stills_fast", BOOLEAN, FALSE, parents(ALT_THUMBNAIL_STILLS)), + ALT_THUMBNAIL_DEARROW("revanced_alt_thumbnail_dearrow", BOOLEAN, false), + ALT_THUMBNAIL_DEARROW_API_URL("revanced_alt_thumbnail_dearrow_api_url", STRING, + "https://dearrow-thumb.ajay.app/api/v1/getThumbnail", true, parents(ALT_THUMBNAIL_DEARROW)), + ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST("revanced_alt_thumbnail_dearrow_connection_toast", BOOLEAN, TRUE, parents(ALT_THUMBNAIL_DEARROW)), CUSTOM_FILTER("revanced_custom_filter", BOOLEAN, FALSE), CUSTOM_FILTER_STRINGS("revanced_custom_filter_strings", STRING, "", true, parents(CUSTOM_FILTER)), DISABLE_FULLSCREEN_AMBIENT_MODE("revanced_disable_fullscreen_ambient_mode", BOOLEAN, TRUE, true), @@ -430,7 +434,7 @@ public enum SettingsEnum { LogHelper.printInfo(() -> "Migrating old setting of '" + oldSetting.value + "' from: " + oldSetting + " into replacement setting: " + newSetting); newSetting.saveValue(oldSetting.value); - oldSetting.saveValue(oldSetting.defaultValue); // reset old value + oldSetting.resetToDefault(); } } @@ -522,6 +526,13 @@ public enum SettingsEnum { } } + /** + * Identical to calling {@link #saveValue(Object)} using {@link #defaultValue}. + */ + public void resetToDefault() { + saveValue(defaultValue); + } + /** * @return if this setting can be configured and used. *

@@ -694,7 +705,7 @@ public enum SettingsEnum { } else if (setting.includeWithImportExport() && !setting.isSetToDefault()) { LogHelper.printDebug(() -> "Resetting to default: " + setting); rebootSettingChanged |= setting.rebootApp; - setting.saveValue(setting.defaultValue); + setting.resetToDefault(); } } numberOfSettingsImported += SponsorBlockSettings.importCategoriesFromFlatJson(json); diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/AlternativeThumbnailsAboutDeArrowPreference.java b/app/src/main/java/app/revanced/integrations/settingsmenu/AlternativeThumbnailsAboutDeArrowPreference.java new file mode 100644 index 00000000..353f40cc --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/AlternativeThumbnailsAboutDeArrowPreference.java @@ -0,0 +1,35 @@ +package app.revanced.integrations.settingsmenu; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.preference.Preference; +import android.util.AttributeSet; + +/** + * Allows tapping the DeArrow about preference to open the DeArrow website. + */ +@SuppressWarnings("unused") +public class AlternativeThumbnailsAboutDeArrowPreference extends Preference { + { + setOnPreferenceClickListener(pref -> { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("https://dearrow.ajay.app")); + pref.getContext().startActivity(i); + return false; + }); + } + + public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public AlternativeThumbnailsAboutDeArrowPreference(Context context) { + super(context); + } +} diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/AlternativeThumbnailsStatusPreference.java b/app/src/main/java/app/revanced/integrations/settingsmenu/AlternativeThumbnailsStatusPreference.java new file mode 100644 index 00000000..97dc6d53 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/AlternativeThumbnailsStatusPreference.java @@ -0,0 +1,85 @@ +package app.revanced.integrations.settingsmenu; + +import static app.revanced.integrations.utils.StringRef.str; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.Preference; +import android.preference.PreferenceManager; +import android.util.AttributeSet; + +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.settings.SharedPrefCategory; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + +/** + * Shows what thumbnails will be used based on the current settings. + */ +@SuppressWarnings("unused") +public class AlternativeThumbnailsStatusPreference extends Preference { + + private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { + // Because this listener may run before the ReVanced settings fragment updates SettingsEnum, + // this could show the prior config and not the current. + // + // Push this call to the end of the main run queue, + // so all other listeners are done and SettingsEnum is up to date. + ReVancedUtils.runOnMainThread(this::updateUI); + }; + + public AlternativeThumbnailsStatusPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public AlternativeThumbnailsStatusPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public AlternativeThumbnailsStatusPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public AlternativeThumbnailsStatusPreference(Context context) { + super(context); + } + + private void addChangeListener() { + LogHelper.printDebug(() -> "addChangeListener"); + SharedPrefCategory.YOUTUBE.preferences.registerOnSharedPreferenceChangeListener(listener); + } + + private void removeChangeListener() { + LogHelper.printDebug(() -> "removeChangeListener"); + SharedPrefCategory.YOUTUBE.preferences.unregisterOnSharedPreferenceChangeListener(listener); + } + + @Override + protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { + super.onAttachedToHierarchy(preferenceManager); + updateUI(); + addChangeListener(); + } + + @Override + protected void onPrepareForRemoval() { + super.onPrepareForRemoval(); + removeChangeListener(); + } + + private void updateUI() { + LogHelper.printDebug(() -> "updateUI"); + final boolean usingDeArrow = SettingsEnum.ALT_THUMBNAIL_DEARROW.getBoolean(); + final boolean usingVideoStills = SettingsEnum.ALT_THUMBNAIL_STILLS.getBoolean(); + + final String summaryTextKey; + if (usingDeArrow && usingVideoStills) { + summaryTextKey = "revanced_alt_thumbnail_about_status_dearrow_stills"; + } else if (usingDeArrow) { + summaryTextKey = "revanced_alt_thumbnail_about_status_dearrow"; + } else if (usingVideoStills) { + summaryTextKey = "revanced_alt_thumbnail_about_status_stills"; + } else { + summaryTextKey = "revanced_alt_thumbnail_about_status_disabled"; + } + + setSummary(str(summaryTextKey)); + } +} diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java index 456bcdc8..e25ae7a5 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java @@ -351,7 +351,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> { if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) { - SettingsEnum.SB_API_URL.saveValue(SettingsEnum.SB_API_URL.defaultValue); + SettingsEnum.SB_API_URL.resetToDefault(); ReVancedUtils.showToastLong(str("sb_api_url_reset")); } else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) { String serverAddress = editText.getText().toString(); @@ -583,8 +583,8 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { new AlertDialog.Builder(preference1.getContext()) .setTitle(str("sb_stats_self_saved_reset_title")) .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { - SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.saveValue(SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.defaultValue); - SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.saveValue(SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.defaultValue); + SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault(); + SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault(); updateStatsSelfSaved.run(); }) .setNegativeButton(android.R.string.no, null).show(); diff --git a/dummy/src/main/java/org/chromium/net/UrlRequest.java b/dummy/src/main/java/org/chromium/net/UrlRequest.java new file mode 100644 index 00000000..565fc222 --- /dev/null +++ b/dummy/src/main/java/org/chromium/net/UrlRequest.java @@ -0,0 +1,4 @@ +package org.chromium.net; + +public abstract class UrlRequest { +} diff --git a/dummy/src/main/java/org/chromium/net/impl/CronetUrlRequest.java b/dummy/src/main/java/org/chromium/net/impl/CronetUrlRequest.java new file mode 100644 index 00000000..fa0dcacd --- /dev/null +++ b/dummy/src/main/java/org/chromium/net/impl/CronetUrlRequest.java @@ -0,0 +1,11 @@ +package org.chromium.net.impl; + +import org.chromium.net.UrlRequest; + +public abstract class CronetUrlRequest extends UrlRequest { + + /** + * Method is added by patch. + */ + public abstract String getHookedUrl(); +}