chore: Merge branch dev to main (#530)

This commit is contained in:
oSumAtrIX 2023-12-12 01:11:30 +01:00 committed by GitHub
commit 69c5028661
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 866 additions and 245 deletions

View File

@ -1,3 +1,99 @@
# [1.0.0-dev.12](https://github.com/ReVanced/revanced-integrations/compare/v1.0.0-dev.11...v1.0.0-dev.12) (2023-12-12)
### Features
* **Tiktok:** Bump compatibility to `32.5.3` ([#536](https://github.com/ReVanced/revanced-integrations/issues/536)) ([10a1e16](https://github.com/ReVanced/revanced-integrations/commit/10a1e168d062adc3c979de17738d8cf1b8f25d63))
# [1.0.0-dev.11](https://github.com/ReVanced/revanced-integrations/compare/v1.0.0-dev.10...v1.0.0-dev.11) (2023-12-11)
### Features
* **YouTube:** Add `Change start page` patch ([792dc0c](https://github.com/ReVanced/revanced-integrations/commit/792dc0c52210dc9b1560290b0cc2afad572c584c))
# [1.0.0-dev.10](https://github.com/ReVanced/revanced-integrations/compare/v1.0.0-dev.9...v1.0.0-dev.10) (2023-12-11)
### Bug Fixes
* **YouTube - Return YouTube Dislike:** Wait until fetch is complete before allowing the first Short to start playback ([#538](https://github.com/ReVanced/revanced-integrations/issues/538)) ([1c9c51c](https://github.com/ReVanced/revanced-integrations/commit/1c9c51ca5f7970774d4e0b5aad5ebcd064cac716))
# [1.0.0-dev.9](https://github.com/ReVanced/revanced-integrations/compare/v1.0.0-dev.8...v1.0.0-dev.9) (2023-12-11)
### Features
* **YouTube - Alternative Thumbnails:** Add option to use DeArrow ([#534](https://github.com/ReVanced/revanced-integrations/issues/534)) ([c4ee6ca](https://github.com/ReVanced/revanced-integrations/commit/c4ee6ca4dde13ab8ce6f9cf94f1910455f9d9ecc))
# [1.0.0-dev.8](https://github.com/ReVanced/revanced-integrations/compare/v1.0.0-dev.7...v1.0.0-dev.8) (2023-12-10)
### Bug Fixes
* **YouTube - Announcements:** Don't show error toast if there is no internet connection ([#537](https://github.com/ReVanced/revanced-integrations/issues/537)) ([0ce92c2](https://github.com/ReVanced/revanced-integrations/commit/0ce92c284d08a1c6bffba976e9cf208e82288ddf))
# [1.0.0-dev.7](https://github.com/ReVanced/revanced-integrations/compare/v1.0.0-dev.6...v1.0.0-dev.7) (2023-12-09)
### Bug Fixes
* **YouTube - Spoof signature:** Wait until storyboard fetch is done ([#535](https://github.com/ReVanced/revanced-integrations/issues/535)) ([92e8619](https://github.com/ReVanced/revanced-integrations/commit/92e8619cd7bbcf82f27e9407e18c30d65214e31c))
# [1.0.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v1.0.0-dev.5...v1.0.0-dev.6) (2023-12-07)
### Bug Fixes
* **YouTube - Client spoof:** Do not break clips ([f9102fa](https://github.com/ReVanced/revanced-integrations/commit/f9102fa83bdb2b147543882cb8ebb80b5985ad3e))
# [1.0.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v1.0.0-dev.4...v1.0.0-dev.5) (2023-12-04)
### Bug Fixes
* **YouTube - Minimized playback:** Fix PIP incorrectly shown for some Shorts playback ([#533](https://github.com/ReVanced/revanced-integrations/issues/533)) ([fb433da](https://github.com/ReVanced/revanced-integrations/commit/fb433da6ad652aee48fc92794de82bb914ab80ca))
# [1.0.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.0.0-dev.3...v1.0.0-dev.4) (2023-12-04)
### Bug Fixes
* **YouTube - Return YouTube Dislike:** Prevent the first Short opened from freezing the UI ([#532](https://github.com/ReVanced/revanced-integrations/issues/532)) ([0bb8669](https://github.com/ReVanced/revanced-integrations/commit/0bb86694e24a6a41edee62f5ef1bb80fe7bc3f19))
# [1.0.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.0.0-dev.2...v1.0.0-dev.3) (2023-12-03)
### Bug Fixes
* **YouTube - SponsorBlock:** Prevent autoplay from stopping to work ([f4e2d56](https://github.com/ReVanced/revanced-integrations/commit/f4e2d56b181fee4d693dea1dfe81974237e4eff7))
# [1.0.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.0.0-dev.1...v1.0.0-dev.2) (2023-12-03)
### Bug Fixes
* **YouTube - Return YouTube Dislike:** Fix dislikes sometimes not showing for non English language ([5d4c8b0](https://github.com/ReVanced/revanced-integrations/commit/5d4c8b0a1b77e97c7c0c02288927e92f3c9765ce))
# [1.0.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.125.1-dev.1...v1.0.0-dev.1) (2023-12-02)
### Features
* Allow choosing the vendor of GmsCore via patch options ([#529](https://github.com/ReVanced/revanced-integrations/issues/529)) ([fba7181](https://github.com/ReVanced/revanced-integrations/commit/fba7181e70d695d7fb13c530754dc1db99b87216))
### BREAKING CHANGES
* The class `MicroGSupport` has been renamed to `GmsCoreSupport`
## [0.125.1-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.125.0...v0.125.1-dev.1) (2023-12-02)
### Bug Fixes
* **YouTube - SponsorBlock:** Allow autoplay when skipping to the end of the video ([3d660e1](https://github.com/ReVanced/revanced-integrations/commit/3d660e1b5eeab9771f96bd2d26a222b835e2485c))
# [0.125.0](https://github.com/ReVanced/revanced-integrations/compare/v0.124.1...v0.125.0) (2023-12-02) # [0.125.0](https://github.com/ReVanced/revanced-integrations/compare/v0.124.1...v0.125.0) (2023-12-02)

View File

@ -1,11 +1,17 @@
package app.revanced.integrations.patches; package app.revanced.integrations.patches;
import android.net.Uri;
import androidx.annotation.GuardedBy; import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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.UrlResponseInfo;
import org.chromium.net.impl.CronetUrlRequest;
import java.io.IOException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
@ -13,30 +19,289 @@ import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import app.revanced.integrations.settings.SettingsEnum; import static app.revanced.integrations.utils.StringRef.str;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
/** /**
* Alternative YouTube thumbnails, showing the beginning/middle/end of the video. * Alternative YouTube thumbnails.
* <p>
* Can show YouTube provided screen captures of beginning/middle/end of the video.
* (ie: sd1.jpg, sd2.jpg, sd3.jpg). * (ie: sd1.jpg, sd2.jpg, sd3.jpg).
* * <p>
* Has an additional option to use 'fast' thumbnails, * Or can show crowdsourced thumbnails provided by DeArrow (<a href="http://dearrow.ajay.app">...</a>).
* <p>
* Or can use DeArrow and fall back to screen captures if DeArrow is not available.
* <p>
* 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. * 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. * 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 * 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, * 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.
* * <p>
* Ideas for improvements: * Ideas for improvements:
* - Selectively allow using original thumbnails in some situations, * - Selectively allow using original thumbnails in some situations,
* such as videos subscription feed, watch history, or in search results. * 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. * - 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. * This would speed up loading the watch history and users saved playlists.
*/ */
@SuppressWarnings("unused")
public final class AlternativeThumbnailsPatch { 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.
* <p>
* 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.
* <p>
* 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).
* <p>
* 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 { private enum ThumbnailQuality {
// In order of lowest to highest resolution. // In order of lowest to highest resolution.
DEFAULT("default", ""), // effective alt name is 1.jpg, 2.jpg, 3.jpg 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); originalNameToEnum.put(quality.originalName, quality);
for (int i = 1; i <= 3; i++) { 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); altNameToEnum.put(quality.altImageName + i, quality);
} }
} }
@ -86,7 +356,7 @@ public final class AlternativeThumbnailsPatch {
return null; // Not a thumbnail for a regular video. 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) { switch (quality) {
case SDDEFAULT: case SDDEFAULT:
// SD alt images have somewhat worse quality with washed out color and poor contrast. // SD alt images have somewhat worse quality with washed out color and poor contrast.
@ -121,7 +391,7 @@ public final class AlternativeThumbnailsPatch {
} }
String getAltImageNameToUse() { 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 @Override
protected boolean removeEldestEntry(Map.Entry eldest) { 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, static boolean verifyAltThumbnailExist(@NonNull String videoId, @NonNull ThumbnailQuality quality,
@NonNull String imageUrl) { @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. if (verified == null) return true; // Fast alt thumbnails is enabled.
return verified.verifyYouTubeThumbnailExists(videoId, quality, imageUrl); return verified.verifyYouTubeThumbnailExists(videoId, quality, imageUrl);
} }
static void setAltThumbnailDoesNotExist(@NonNull String videoId, @NonNull ThumbnailQuality quality) { static void setAltThumbnailDoesNotExist(@NonNull String videoId, @NonNull ThumbnailQuality quality) {
VerifiedQualities verified = getVerifiedQualities(videoId, false); VerifiedQualities verified = getVerifiedQualities(videoId, false);
//noinspection ConstantConditions
verified.setQualityVerified(videoId, quality, false); verified.setQualityVerified(videoId, quality, false);
} }
@ -180,20 +451,20 @@ public final class AlternativeThumbnailsPatch {
* Highest quality verified as existing. * Highest quality verified as existing.
*/ */
@Nullable @Nullable
ThumbnailQuality highestQualityVerified; private ThumbnailQuality highestQualityVerified;
/** /**
* Lowest quality verified as not existing. * Lowest quality verified as not existing.
*/ */
@Nullable @Nullable
ThumbnailQuality lowestQualityNotAvailable; private ThumbnailQuality lowestQualityNotAvailable;
/** /**
* System time, of when to invalidate {@link #lowestQualityNotAvailable}. * System time, of when to invalidate {@link #lowestQualityNotAvailable}.
* Used only if fast mode is not enabled. * 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 (isVerified) {
if (highestQualityVerified == null || highestQualityVerified.ordinal() < quality.ordinal()) { if (highestQualityVerified == null || highestQualityVerified.ordinal() < quality.ordinal()) {
highestQualityVerified = quality; highestQualityVerified = quality;
@ -216,7 +487,7 @@ public final class AlternativeThumbnailsPatch {
return true; // Previously verified as existing. 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 (lowestQualityNotAvailable != null && lowestQualityNotAvailable.ordinal() <= quality.ordinal()) {
if (fastQuality || System.currentTimeMillis() < timeToReVerifyLowestQuality) { if (fastQuality || System.currentTimeMillis() < timeToReVerifyLowestQuality) {
return false; // Previously verified as not existing. return false; // Previously verified as not existing.
@ -279,131 +550,61 @@ public final class AlternativeThumbnailsPatch {
static DecodedThumbnailUrl decodeImageUrl(String url) { static DecodedThumbnailUrl decodeImageUrl(String url) {
final int videoIdStartIndex = url.indexOf('/', YOUTUBE_THUMBNAIL_PREFIX.length()) + 1; final int videoIdStartIndex = url.indexOf('/', YOUTUBE_THUMBNAIL_PREFIX.length()) + 1;
if (videoIdStartIndex <= 0) return null; if (videoIdStartIndex <= 0) return null;
final int videoIdEndIndex = url.indexOf('/', videoIdStartIndex); final int videoIdEndIndex = url.indexOf('/', videoIdStartIndex);
if (videoIdEndIndex < 0) return null; if (videoIdEndIndex < 0) return null;
final int imageSizeStartIndex = videoIdEndIndex + 1; final int imageSizeStartIndex = videoIdEndIndex + 1;
final int imageSizeEndIndex = url.indexOf('.', imageSizeStartIndex); final int imageSizeEndIndex = url.indexOf('.', imageSizeStartIndex);
if (imageSizeEndIndex < 0) return null; if (imageSizeEndIndex < 0) return null;
int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex); int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex);
if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length(); if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length();
return new DecodedThumbnailUrl(url, videoIdStartIndex, videoIdEndIndex, return new DecodedThumbnailUrl(url, videoIdStartIndex, videoIdEndIndex,
imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex); imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex);
} }
final String originalFullUrl;
/** Full usable url, but stripped of any tracking information. */ /** Full usable url, but stripped of any tracking information. */
final String sanitizedUrl; final String sanitizedUrl;
/** Url up to the video id. */ /** Url up to the video ID. */
final String urlPrefix; final String urlPrefix;
final String videoId; final String videoId;
/** Quality, such as hq720 or sddefault. */ /** Quality, such as hq720 or sddefault. */
final String imageQuality; final String imageQuality;
/** jpg or webp */ /** JPG or WEBP */
final String imageExtension; final String imageExtension;
/** User view tracking parameters, only present on some images. */ /** User view tracking parameters, only present on some images. */
final String urlTrackingParameters; final String viewTrackingParameters;
private DecodedThumbnailUrl(String fullUrl, int videoIdStartIndex, int videoIdEndIndex, DecodedThumbnailUrl(String fullUrl, int videoIdStartIndex, int videoIdEndIndex,
int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) { int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) {
originalFullUrl = fullUrl;
sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex); sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex);
urlPrefix = fullUrl.substring(0, videoIdStartIndex); urlPrefix = fullUrl.substring(0, videoIdStartIndex);
videoId = fullUrl.substring(videoIdStartIndex, videoIdEndIndex); videoId = fullUrl.substring(videoIdStartIndex, videoIdEndIndex);
imageQuality = fullUrl.substring(imageSizeStartIndex, imageSizeEndIndex); imageQuality = fullUrl.substring(imageSizeStartIndex, imageSizeEndIndex);
imageExtension = fullUrl.substring(imageSizeEndIndex + 1, imageExtensionEndIndex); imageExtension = fullUrl.substring(imageSizeEndIndex + 1, imageExtensionEndIndex);
urlTrackingParameters = (imageExtensionEndIndex == fullUrl.length()) viewTrackingParameters = (imageExtensionEndIndex == fullUrl.length())
? "" : fullUrl.substring(imageExtensionEndIndex); ? "" : 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, // 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. // 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. // 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). // (as much as 4x slower has been observed, despite the alt webp image being a smaller file).
StringBuilder builder = new StringBuilder(originalFullUrl.length() + 2);
StringBuilder builder = new StringBuilder(originalUrl.length() + 2); builder.append(urlPrefix);
builder.append(decodedUrl.urlPrefix); builder.append(videoId).append('/');
builder.append(decodedUrl.videoId).append('/');
builder.append(qualityToUse.getAltImageNameToUse()); builder.append(qualityToUse.getAltImageNameToUse());
builder.append('.').append(decodedUrl.imageExtension); builder.append('.').append(imageExtension);
if (includeViewTracking) {
String sanitizedReplacement = builder.toString(); builder.append(viewTrackingParameters);
if (!VerifiedQualities.verifyAltThumbnailExist(decodedUrl.videoId, qualityToUse, sanitizedReplacement)) {
return originalUrl;
} }
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(); 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);
}
}
} }

View File

@ -0,0 +1,16 @@
package app.revanced.integrations.patches;
import android.content.Intent;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
@SuppressWarnings("unused")
public final class ChangeStartPagePatch {
public static void changeIntent(Intent intent) {
final var startPage = SettingsEnum.START_PAGE.getString();
if (startPage.isEmpty()) return;
LogHelper.printDebug(() -> "Changing start page to " + startPage);
intent.setAction("com.google.android.youtube.action." + startPage);
}
}

View File

@ -0,0 +1,74 @@
package app.revanced.integrations.patches;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.RequiresApi;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import java.util.Objects;
import static app.revanced.integrations.utils.StringRef.str;
/**
* @noinspection unused
*/
public class GmsCoreSupport {
private static final String GMS_CORE_PACKAGE_NAME
= getGmsCoreVendor() + ".android.gms";
private static final String DONT_KILL_MY_APP_LINK
= "https://dontkillmyapp.com";
private static final Uri GMS_CORE_PROVIDER
= Uri.parse("content://" + getGmsCoreVendor() + ".android.gsf.gservices/prefix");
private static void search(Context context, String uriString, String message) {
ReVancedUtils.showToastLong(message);
var intent = new Intent(Intent.ACTION_WEB_SEARCH);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(SearchManager.QUERY, uriString);
context.startActivity(intent);
}
@RequiresApi(api = Build.VERSION_CODES.N)
public static void checkAvailability() {
var context = Objects.requireNonNull(ReVancedUtils.getContext());
try {
context.getPackageManager().getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException exception) {
LogHelper.printInfo(() -> "GmsCore was not found", exception);
search(context, getGmsCoreDownloadLink(), str("gms_core_not_installed_warning"));
// Gracefully exit the app, so it does not crash.
System.exit(0);
}
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
if (client != null) return;
LogHelper.printInfo(() -> "GmsCore is not running in the background");
search(context, DONT_KILL_MY_APP_LINK, str("gms_core_not_running_warning"));
}
}
private static String getGmsCoreDownloadLink() {
final var vendor = getGmsCoreVendor();
switch (vendor) {
case "com.mgoogle":
return "https://github.com/TeamVanced/VancedMicroG/releases/latest";
case "app.revanced":
return "https://github.com/revanced/gmscore/releases/latest";
default:
return vendor + ".android.gms";
}
}
// Modified by a patch. Do not touch.
private static String getGmsCoreVendor() {
return "app.revanced";
}
}

View File

@ -1,53 +0,0 @@
package app.revanced.integrations.patches;
import static app.revanced.integrations.utils.StringRef.str;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import java.util.Objects;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class MicroGSupport {
private static final String MICROG_VENDOR = "com.mgoogle";
private static final String MICROG_PACKAGE_NAME = MICROG_VENDOR + ".android.gms";
private static final String VANCED_MICROG_DOWNLOAD_LINK = "https://github.com/TeamVanced/VancedMicroG/releases/latest";
private static final String DONT_KILL_MY_APP_LINK = "https://dontkillmyapp.com";
private static final Uri VANCED_MICROG_PROVIDER = Uri.parse("content://" + MICROG_VENDOR + ".android.gsf.gservices/prefix");
private static void startIntent(Context context, String uriString, String message) {
ReVancedUtils.showToastLong(message);
var intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.parse(uriString));
context.startActivity(intent);
}
@TargetApi(26)
public static void checkAvailability() {
var context = Objects.requireNonNull(ReVancedUtils.getContext());
try {
context.getPackageManager().getPackageInfo(MICROG_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException exception) {
LogHelper.printInfo(() -> "Vanced MicroG was not found", exception);
startIntent(context, VANCED_MICROG_DOWNLOAD_LINK, str("microg_not_installed_warning"));
// Gracefully exit the app, so it does not crash.
System.exit(0);
}
try (var client = context.getContentResolver().acquireContentProviderClient(VANCED_MICROG_PROVIDER)) {
if (client != null) return;
LogHelper.printInfo(() -> "Vanced MicroG is not running in the background");
startIntent(context, DONT_KILL_MY_APP_LINK, str("microg_not_running_warning"));
}
}
}

View File

@ -2,12 +2,39 @@ package app.revanced.integrations.patches;
import app.revanced.integrations.shared.PlayerType; import app.revanced.integrations.shared.PlayerType;
@SuppressWarnings("unused")
public class MinimizedPlaybackPatch { public class MinimizedPlaybackPatch {
public static boolean isPlaybackNotShort() { /**
return !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized(); * Injection point.
*/
public static boolean playbackIsNotShort() {
// Steps to verify most edge cases:
// 1. Open a regular video
// 2. Minimize app (PIP should appear)
// 3. Reopen app
// 4. Open a Short (without closing the regular video)
// (try opening both Shorts in the video player suggestions AND Shorts from the home feed)
// 5. Minimize the app (PIP should not appear)
// 6. Reopen app
// 7. Close the Short
// 8. Resume playing the regular video
// 9. Minimize the app (PIP should appear)
if (!VideoInformation.lastVideoIdIsShort()) {
return true; // Definitely is not a Short.
}
// Might be a Short, or might be a prior regular video on screen again after a Short was closed.
// This incorrectly prevents PIP if player is in WATCH_WHILE_MINIMIZED after closing a Short,
// But there's no way around this unless an additional hook is added to definitively detect
// the Shorts player is on screen. This use case is unusual anyways so it's not a huge concern.
return !PlayerType.getCurrent().isNoneHiddenOrMinimized();
} }
/**
* Injection point.
*/
public static boolean overrideMinimizedPlaybackAvailable() { public static boolean overrideMinimizedPlaybackAvailable() {
// This could be done entirely in the patch, // This could be done entirely in the patch,
// but having a unique method to search for makes manually inspecting the patched apk much easier. // but having a unique method to search for makes manually inspecting the patched apk much easier.

View File

@ -9,6 +9,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import app.revanced.integrations.patches.components.ReturnYouTubeDislikeFilterPatch; import app.revanced.integrations.patches.components.ReturnYouTubeDislikeFilterPatch;
import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType; import app.revanced.integrations.shared.PlayerType;
@ -27,19 +28,25 @@ import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislik
* Handles all interaction of UI patch components. * Handles all interaction of UI patch components.
* *
* Known limitation: * Known limitation:
* Litho based Shorts player can experience temporarily frozen video playback if the RYD fetch takes too long. * The implementation of Shorts litho requires blocking the loading the first Short until RYD has completed.
* This is because it modifies the dislikes text synchronously, and if the RYD fetch has
* not completed yet then the UI will be temporarily frozen.
* *
* Temporary work around: * A (yet to be implemented) solution that fixes this problem. Any one of:
* Enable app spoofing to version 18.33.40 or older, as that uses a non litho Shorts player. * - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes text asynchronously.
* * - Find a way to force Litho to rebuild it's component tree,
* Permanent fix (yet to be implemented), either of: * and use that hook to force the shorts dislikes to update after the fetch is completed.
* - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes asynchronously. * - Hook into the dislikes button image view, and replace the dislikes thumb down image with a
* - Find a way to force Litho to rebuild it's component tree * generated image of the number of dislikes, then update the image asynchronously. This Could
* (and use that hook to force the shorts dislikes to update after the fetch is completed). * also be used for the regular video player to give a better UI layout and completely remove
* the need for the Rolling Number patches.
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class ReturnYouTubeDislikePatch { public class ReturnYouTubeDislikePatch {
public static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER =
SpoofAppVersionPatch.isSpoofingToEqualOrLessThan("18.33.40");
/** /**
* RYD data for the current video on screen. * RYD data for the current video on screen.
*/ */
@ -318,8 +325,12 @@ public class ReturnYouTubeDislikePatch {
try { try {
if (SettingsEnum.RYD_ENABLED.getBoolean() && !SettingsEnum.RYD_COMPACT_LAYOUT.getBoolean()) { if (SettingsEnum.RYD_ENABLED.getBoolean() && !SettingsEnum.RYD_COMPACT_LAYOUT.getBoolean()) {
if (ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(text)) { if (ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(text)) {
// +1 pixel is needed for some foreign languages that measure
// the text different from what is used for layout (Greek in particular).
// Probably a bug in Android, but who knows.
// Single line mode is also used as an additional fix for this issue.
return measuredTextWidth + ReturnYouTubeDislike.leftSeparatorBounds.right return measuredTextWidth + ReturnYouTubeDislike.leftSeparatorBounds.right
+ ReturnYouTubeDislike.leftSeparatorShapePaddingPixels; + ReturnYouTubeDislike.leftSeparatorShapePaddingPixels + 1;
} }
} }
} catch (Exception ex) { } catch (Exception ex) {
@ -342,6 +353,10 @@ public class ReturnYouTubeDislikePatch {
} else { } else {
view.setCompoundDrawables(separator, null, null, null); view.setCompoundDrawables(separator, null, null, null);
} }
// Single line mode does not clip words if the span is larger than the view bounds.
// The styled span applied to the view should always have the same bounds,
// but use this feature just in case the measurements are somehow off by a few pixels.
view.setSingleLine(true);
} }
} }
@ -354,6 +369,7 @@ public class ReturnYouTubeDislikePatch {
LogHelper.printDebug(() -> "Removing rolling number TextView changes"); LogHelper.printDebug(() -> "Removing rolling number TextView changes");
view.setCompoundDrawablePadding(0); view.setCompoundDrawablePadding(0);
view.setCompoundDrawables(null, null, null, null); view.setCompoundDrawables(null, null, null, null);
view.setSingleLine(false);
} }
} }
@ -540,26 +556,47 @@ public class ReturnYouTubeDislikePatch {
// Video Id and voting hooks (all players). // Video Id and voting hooks (all players).
// //
private static volatile boolean lastPlayerResponseWasShort;
/** /**
* Injection point. Uses 'playback response' video id hook to preload RYD. * Injection point. Uses 'playback response' video id hook to preload RYD.
*/ */
public static void preloadVideoId(@NonNull String videoId, boolean videoIsOpeningOrPlaying) { public static void preloadVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
try { try {
// Shorts shelf in home and subscription feed causes player response hook to be called, if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
// and the 'is opening/playing' parameter will be false.
// This hook will be called again when the Short is actually opened.
if (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_ENABLED.getBoolean()) {
return;
}
if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized()) {
return; return;
} }
if (videoId.equals(lastPrefetchedVideoId)) { if (videoId.equals(lastPrefetchedVideoId)) {
return; return;
} }
lastPrefetchedVideoId = videoId;
final boolean videoIdIsShort = VideoInformation.lastVideoIdIsShort();
// Shorts shelf in home and subscription feed causes player response hook to be called,
// and the 'is opening/playing' parameter will be false.
// This hook will be called again when the Short is actually opened.
if (videoIdIsShort && (!isShortAndOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean())) {
return;
}
final boolean waitForFetchToComplete = !IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER
&& videoIdIsShort && !lastPlayerResponseWasShort;
LogHelper.printDebug(() -> "Prefetching RYD for video: " + videoId); LogHelper.printDebug(() -> "Prefetching RYD for video: " + videoId);
ReturnYouTubeDislike.getFetchForVideoId(videoId); ReturnYouTubeDislike fetch = ReturnYouTubeDislike.getFetchForVideoId(videoId);
if (waitForFetchToComplete && !fetch.fetchCompleted()) {
// This call is off the main thread, so wait until the RYD fetch completely finishes,
// otherwise if this returns before the fetch completes then the UI can
// become frozen when the main thread tries to modify the litho Shorts dislikes and
// it must wait for the fetch.
// Only need to do this for the first Short opened, as the next Short to swipe to
// are preloaded in the background.
//
// If an asynchronous litho Shorts solution is found, then this blocking call should be removed.
LogHelper.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;
} catch (Exception ex) { } catch (Exception ex) {
LogHelper.printException(() -> "preloadVideoId failure", ex); LogHelper.printException(() -> "preloadVideoId failure", ex);
} }

View File

@ -17,6 +17,10 @@ import java.util.Objects;
public final class VideoInformation { public final class VideoInformation {
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
private static final String SEEK_METHOD_NAME = "seekTo"; private static final String SEEK_METHOD_NAME = "seekTo";
/**
* Prefix present in all Short player parameters signature.
*/
private static final String SHORTS_PLAYER_PARAMETERS = "8AEB";
private static WeakReference<Object> playerControllerRef; private static WeakReference<Object> playerControllerRef;
private static Method seekMethod; private static Method seekMethod;
@ -28,6 +32,7 @@ public final class VideoInformation {
@NonNull @NonNull
private static volatile String playerResponseVideoId = ""; private static volatile String playerResponseVideoId = "";
private static volatile boolean videoIdIsShort;
/** /**
* The current playback speed * The current playback speed
@ -65,12 +70,33 @@ public final class VideoInformation {
} }
} }
/**
* @return If the player parameters are for a Short.
*/
public static boolean playerParametersAreShort(@NonNull String parameters) {
return parameters.startsWith(SHORTS_PLAYER_PARAMETERS);
}
/**
* Injection point.
*/
public static String newPlayerResponseSignature(@NonNull String signature, boolean isShortAndOpeningOrPlaying) {
final boolean isShort = playerParametersAreShort(signature);
if (!isShort || isShortAndOpeningOrPlaying) {
if (videoIdIsShort != isShort) {
videoIdIsShort = isShort;
LogHelper.printDebug(() -> "videoIdIsShort: " + isShort);
}
}
return signature; // Return the original value since we are observing and not modifying.
}
/** /**
* Injection point. Called off the main thread. * Injection point. Called off the main thread.
* *
* @param videoId The id of the last video loaded. * @param videoId The id of the last video loaded.
*/ */
public static void setPlayerResponseVideoId(@NonNull String videoId, boolean videoIsOpeningOrPlaying) { public static void setPlayerResponseVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
if (!playerResponseVideoId.equals(videoId)) { if (!playerResponseVideoId.equals(videoId)) {
LogHelper.printDebug(() -> "New player response video id: " + videoId); LogHelper.printDebug(() -> "New player response video id: " + videoId);
playerResponseVideoId = videoId; playerResponseVideoId = videoId;
@ -135,10 +161,8 @@ public final class VideoInformation {
public static boolean seekTo(final long millisecond) { public static boolean seekTo(final long millisecond) {
final long videoLength = getVideoLength(); final long videoLength = getVideoLength();
// Don't seek more than the video length to prevent issues such as // Prevent issues such as play/ pause button or autoplay not working.
// Play pause button or autoplay not working. final long seekToMilliseconds = Math.min(millisecond, VideoInformation.getVideoLength() - 250);
// TODO: These are arbitrarily chosen values and should be subject to be adjusted.
final long seekToMilliseconds = millisecond <= videoLength - 500 ? millisecond : millisecond - 100;
ReVancedUtils.verifyOnMainThread(); ReVancedUtils.verifyOnMainThread();
try { try {
@ -157,9 +181,9 @@ public final class VideoInformation {
} }
/** /**
* Id of the current video playing. Includes Shorts. * Id of the last video opened. Includes Shorts.
* *
* @return The id of the video. Empty string if not set yet. * @return The id of the video, or an empty string if no videos have been opened yet.
*/ */
@NonNull @NonNull
public static String getVideoId() { public static String getVideoId() {
@ -168,20 +192,30 @@ public final class VideoInformation {
/** /**
* Differs from {@link #videoId} as this is the video id for the * Differs from {@link #videoId} as this is the video id for the
* last player response received, which may not be the current video playing. * last player response received, which may not be the last video opened.
* <p> * <p>
* If Shorts are loading the background, this commonly will be * If Shorts are loading the background, this commonly will be
* different from the Short that is currently on screen. * different from the Short that is currently on screen.
* <p> * <p>
* For most use cases, you should instead use {@link #getVideoId()}. * For most use cases, you should instead use {@link #getVideoId()}.
* *
* @return The id of the last video loaded. Empty string if not set yet. * @return The id of the last video loaded, or an empty string if no videos have been loaded yet.
*/ */
@NonNull @NonNull
public static String getPlayerResponseVideoId() { public static String getPlayerResponseVideoId() {
return playerResponseVideoId; return playerResponseVideoId;
} }
/**
* @return If the last player response video id _that was opened_ was a Short.
* <p>
* Note: This value returned may not match the status of {@link #getPlayerResponseVideoId()}
* since that includes player responses for videos not opened.
*/
public static boolean lastVideoIdIsShort() {
return videoIdIsShort;
}
/** /**
* @return The current playback speed. * @return The current playback speed.
*/ */

View File

@ -32,6 +32,9 @@ public final class AnnouncementsPatch {
public static void showAnnouncement(final Activity context) { public static void showAnnouncement(final Activity context) {
if (!SettingsEnum.ANNOUNCEMENTS.getBoolean()) return; if (!SettingsEnum.ANNOUNCEMENTS.getBoolean()) return;
// Check if there is internet connection
if (!ReVancedUtils.isNetworkConnected()) return;
ReVancedUtils.runOnBackgroundThread(() -> { ReVancedUtils.runOnBackgroundThread(() -> {
try { try {
HttpURLConnection connection = AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT, CONSUMER); HttpURLConnection connection = AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT, CONSUMER);
@ -43,7 +46,7 @@ public final class AnnouncementsPatch {
if (connection.getResponseCode() != 200) { if (connection.getResponseCode() != 200) {
if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return; if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return;
SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue(""); SettingsEnum.ANNOUNCEMENT_LAST_HASH.resetToDefault();
ReVancedUtils.showToastLong("Failed to get announcement"); ReVancedUtils.showToastLong("Failed to get announcement");
return; return;
@ -118,7 +121,7 @@ public final class AnnouncementsPatch {
*/ */
private static boolean emptyLastAnnouncementHash() { private static boolean emptyLastAnnouncementHash() {
if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return true; if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return true;
SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue(""); SettingsEnum.ANNOUNCEMENT_LAST_HASH.resetToDefault();
return false; return false;
} }

View File

@ -133,7 +133,7 @@ final class CustomFilterGroup extends StringFilterGroup {
for (String pattern : patterns) { for (String pattern : patterns) {
if (!StringTrieSearch.isValidPattern(pattern)) { if (!StringTrieSearch.isValidPattern(pattern)) {
ReVancedUtils.showToastLong("Invalid custom filter, resetting to default"); ReVancedUtils.showToastLong("Invalid custom filter, resetting to default");
setting.saveValue(setting.defaultValue); setting.resetToDefault();
return getFilterPatterns(setting); return getFilterPatterns(setting);
} }
} }

View File

@ -53,14 +53,14 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
/** /**
* Injection point. * Injection point.
*/ */
public static void newPlayerResponseVideoId(String videoId, boolean videoIsOpeningOrPlaying) { public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) {
try { try {
if (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean()) { if (!isShortAndOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean()) {
return; return;
} }
synchronized (lastVideoIds) { synchronized (lastVideoIds) {
if (lastVideoIds.put(videoId, Boolean.TRUE) == null) { if (lastVideoIds.put(videoId, Boolean.TRUE) == null) {
LogHelper.printDebug(() -> "New video id: " + videoId); LogHelper.printDebug(() -> "New Short video id: " + videoId);
} }
} }
} catch (Exception ex) { } catch (Exception ex) {

View File

@ -43,7 +43,7 @@ public class CustomPlaybackSpeedPatch {
private static void resetCustomSpeeds(@NonNull String toastMessage) { private static void resetCustomSpeeds(@NonNull String toastMessage) {
ReVancedUtils.showToastLong(toastMessage); ReVancedUtils.showToastLong(toastMessage);
SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.saveValue(SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.defaultValue); SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
} }
private static void loadCustomSpeeds() { private static void loadCustomSpeeds() {

View File

@ -1,5 +1,6 @@
package app.revanced.integrations.patches.spoof; package app.revanced.integrations.patches.spoof;
import static app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
import static app.revanced.integrations.utils.ReVancedUtils.containsAny; import static app.revanced.integrations.utils.ReVancedUtils.containsAny;
import android.view.View; import android.view.View;
@ -8,11 +9,16 @@ import android.widget.ImageView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import app.revanced.integrations.patches.VideoInformation; import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester;
import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType; import app.revanced.integrations.shared.PlayerType;
import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
/** @noinspection unused*/ /** @noinspection unused*/
public class SpoofSignaturePatch { public class SpoofSignaturePatch {
@ -23,6 +29,11 @@ public class SpoofSignaturePatch {
*/ */
private static final String INCOGNITO_PARAMETERS = "CgIQBg=="; private static final String INCOGNITO_PARAMETERS = "CgIQBg==";
/**
* Parameters used when playing clips.
*/
private static final String CLIPS_PARAMETERS = "kAIB";
/** /**
* Parameters causing playback issues. * Parameters causing playback issues.
*/ */
@ -37,11 +48,6 @@ public class SpoofSignaturePatch {
*/ */
private static final String SCRIM_PARAMETER = "SAFgAXgB"; private static final String SCRIM_PARAMETER = "SAFgAXgB";
/**
* Parameters used in YouTube Shorts.
*/
private static final String SHORTS_PLAYER_PARAMETERS = "8AEB";
/** /**
* Last video id loaded. Used to prevent reloading the same spec multiple times. * Last video id loaded. Used to prevent reloading the same spec multiple times.
*/ */
@ -49,12 +55,30 @@ public class SpoofSignaturePatch {
private static volatile String lastPlayerResponseVideoId; private static volatile String lastPlayerResponseVideoId;
@Nullable @Nullable
private static volatile StoryboardRenderer videoRenderer; private static volatile Future<StoryboardRenderer> rendererFuture;
private static volatile boolean useOriginalStoryboardRenderer; private static volatile boolean useOriginalStoryboardRenderer;
private static volatile boolean isPlayingShorts; private static volatile boolean isPlayingShorts;
@Nullable
private static StoryboardRenderer getRenderer(boolean waitForCompletion) {
Future<StoryboardRenderer> 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) {
LogHelper.printDebug(() -> "Could not get renderer (get timed out)");
} catch (ExecutionException | InterruptedException ex) {
// Should never happen.
LogHelper.printException(() -> "Could not get renderer", ex);
}
}
return null;
}
/** /**
* Injection point. * Injection point.
* *
@ -62,19 +86,25 @@ public class SpoofSignaturePatch {
* *
* @param parameters Original protobuf parameter value. * @param parameters Original protobuf parameter value.
*/ */
public static String spoofParameter(String parameters) { public static String spoofParameter(String parameters, boolean isShortAndOpeningOrPlaying) {
try { try {
LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters); LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters);
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) return parameters; if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) {
return parameters;
}
// Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops) // 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). // 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. // Clips are 60 seconds or less in length, so no spoofing.
if (useOriginalStoryboardRenderer = parameters.length() > 150) return parameters; //noinspection AssignmentUsedAsCondition
if (useOriginalStoryboardRenderer = parameters.length() > 150 || containsAny(parameters, CLIPS_PARAMETERS)) {
return parameters;
}
// Shorts do not need to be spoofed. // Shorts do not need to be spoofed.
if (useOriginalStoryboardRenderer = parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) { //noinspection AssignmentUsedAsCondition
if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) {
isPlayingShorts = true; isPlayingShorts = true;
return parameters; return parameters;
} }
@ -83,6 +113,7 @@ public class SpoofSignaturePatch {
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
&& containsAny(parameters, AUTOPLAY_PARAMETERS); && containsAny(parameters, AUTOPLAY_PARAMETERS);
if (isPlayingFeed) { if (isPlayingFeed) {
//noinspection AssignmentUsedAsCondition
if (useOriginalStoryboardRenderer = !SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean()) { if (useOriginalStoryboardRenderer = !SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean()) {
// Don't spoof the feed video playback. This will cause video playback issues, // Don't spoof the feed video playback. This will cause video playback issues,
// but only if user continues watching for more than 1 minute. // but only if user continues watching for more than 1 minute.
@ -103,27 +134,32 @@ public class SpoofSignaturePatch {
private static void fetchStoryboardRenderer() { private static void fetchStoryboardRenderer() {
if (!SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) { if (!SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
lastPlayerResponseVideoId = null; lastPlayerResponseVideoId = null;
videoRenderer = null; rendererFuture = null;
return; return;
} }
String videoId = VideoInformation.getPlayerResponseVideoId(); String videoId = VideoInformation.getPlayerResponseVideoId();
if (!videoId.equals(lastPlayerResponseVideoId)) { if (!videoId.equals(lastPlayerResponseVideoId)) {
rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId));
lastPlayerResponseVideoId = videoId; lastPlayerResponseVideoId = videoId;
// This will block starting video playback until the fetch completes.
// This is desired because if this returns without finishing the fetch,
// then video will start playback but the image will be frozen
// while the main thread call for the renderer waits for the fetch to complete.
videoRenderer = StoryboardRendererRequester.getStoryboardRenderer(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, private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec,
boolean returnNullIfLiveStream) { boolean returnNullIfLiveStream) {
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) { if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
StoryboardRenderer renderer = videoRenderer; StoryboardRenderer renderer = getRenderer(false);
if (renderer != null) { if (renderer != null) {
if (returnNullIfLiveStream && renderer.isLiveStream()) return null; if (returnNullIfLiveStream && renderer.isLiveStream()) {
return renderer.getSpec(); return null;
}
String spec = renderer.getSpec();
if (spec != null) {
return spec;
}
} }
} }
@ -154,7 +190,7 @@ public class SpoofSignaturePatch {
*/ */
public static int getRecommendedLevel(int originalLevel) { public static int getRecommendedLevel(int originalLevel) {
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) { if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
StoryboardRenderer renderer = videoRenderer; StoryboardRenderer renderer = getRenderer(false);
if (renderer != null) { if (renderer != null) {
Integer recommendedLevel = renderer.getRecommendedLevel(); Integer recommendedLevel = renderer.getRecommendedLevel();
if (recommendedLevel != null) return recommendedLevel; if (recommendedLevel != null) return recommendedLevel;
@ -172,7 +208,7 @@ public class SpoofSignaturePatch {
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) { if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) {
return false; return false;
} }
StoryboardRenderer renderer = videoRenderer; StoryboardRenderer renderer = getRenderer(false);
if (renderer == null) { if (renderer == null) {
// Spoof storyboard renderer is turned off, // Spoof storyboard renderer is turned off,
// video is paid, or the storyboard fetch timed out. // video is paid, or the storyboard fetch timed out.

View File

@ -48,7 +48,7 @@ public final class SeekbarColorPatch {
Color.colorToHSV(seekbarColor, customSeekbarColorHSV); Color.colorToHSV(seekbarColor, customSeekbarColorHSV);
} catch (Exception ex) { } catch (Exception ex) {
ReVancedUtils.showToastShort("Invalid seekbar color value. Using default value."); 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(); loadCustomSeekbarColor();
} }
} }

View File

@ -62,12 +62,12 @@ public class ReturnYouTubeDislikeApi {
* How long to wait until API calls are resumed, if the API requested a back off. * How long to wait until API calls are resumed, if the API requested a back off.
* No clear guideline of how long to wait until resuming. * No clear guideline of how long to wait until resuming.
*/ */
private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 4 * 60 * 1000; // 4 Minutes. private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes.
/** /**
* How long to wait until API calls are resumed, if any connection error occurs. * How long to wait until API calls are resumed, if any connection error occurs.
*/ */
private static final int BACKOFF_CONNECTION_ERROR_MILLISECONDS = 60 * 1000; // 60 Seconds. private static final int BACKOFF_CONNECTION_ERROR_MILLISECONDS = 2 * 60 * 1000; // 2 Minutes.
/** /**
* If non zero, then the system time of when API calls can resume. * If non zero, then the system time of when API calls can resume.

View File

@ -56,9 +56,13 @@ public enum SettingsEnum {
HIDE_WEB_SEARCH_RESULTS("revanced_hide_web_search_results", BOOLEAN, TRUE), HIDE_WEB_SEARCH_RESULTS("revanced_hide_web_search_results", BOOLEAN, TRUE),
// Layout // Layout
ALT_THUMBNAIL("revanced_alt_thumbnail", BOOLEAN, FALSE), ALT_THUMBNAIL_STILLS("revanced_alt_thumbnail_stills", BOOLEAN, FALSE),
ALT_THUMBNAIL_TYPE("revanced_alt_thumbnail_type", INTEGER, 2, parents(ALT_THUMBNAIL)), ALT_THUMBNAIL_STILLS_TIME("revanced_alt_thumbnail_stills_time", INTEGER, 2, parents(ALT_THUMBNAIL_STILLS)),
ALT_THUMBNAIL_FAST_QUALITY("revanced_alt_thumbnail_fast_quality", BOOLEAN, FALSE, parents(ALT_THUMBNAIL)), 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("revanced_custom_filter", BOOLEAN, FALSE),
CUSTOM_FILTER_STRINGS("revanced_custom_filter_strings", STRING, "", true, parents(CUSTOM_FILTER)), CUSTOM_FILTER_STRINGS("revanced_custom_filter_strings", STRING, "", true, parents(CUSTOM_FILTER)),
DISABLE_FULLSCREEN_AMBIENT_MODE("revanced_disable_fullscreen_ambient_mode", BOOLEAN, TRUE, true), DISABLE_FULLSCREEN_AMBIENT_MODE("revanced_disable_fullscreen_ambient_mode", BOOLEAN, TRUE, true),
@ -125,6 +129,8 @@ public enum SettingsEnum {
TABLET_LAYOUT("revanced_tablet_layout", BOOLEAN, FALSE, true, "revanced_tablet_layout_user_dialog_message"), TABLET_LAYOUT("revanced_tablet_layout", BOOLEAN, FALSE, true, "revanced_tablet_layout_user_dialog_message"),
USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true), USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true),
WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true), WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true),
START_PAGE("revanced_start_page", STRING, ""),
// Description // Description
HIDE_CHAPTERS("revanced_hide_chapters", BOOLEAN, TRUE), HIDE_CHAPTERS("revanced_hide_chapters", BOOLEAN, TRUE),
HIDE_INFO_CARDS_SECTION("revanced_hide_info_cards_section", BOOLEAN, TRUE), HIDE_INFO_CARDS_SECTION("revanced_hide_info_cards_section", BOOLEAN, TRUE),
@ -430,7 +436,7 @@ public enum SettingsEnum {
LogHelper.printInfo(() -> "Migrating old setting of '" + oldSetting.value LogHelper.printInfo(() -> "Migrating old setting of '" + oldSetting.value
+ "' from: " + oldSetting + " into replacement setting: " + newSetting); + "' from: " + oldSetting + " into replacement setting: " + newSetting);
newSetting.saveValue(oldSetting.value); newSetting.saveValue(oldSetting.value);
oldSetting.saveValue(oldSetting.defaultValue); // reset old value oldSetting.resetToDefault();
} }
} }
@ -522,6 +528,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. * @return if this setting can be configured and used.
* <p> * <p>
@ -694,7 +707,7 @@ public enum SettingsEnum {
} else if (setting.includeWithImportExport() && !setting.isSetToDefault()) { } else if (setting.includeWithImportExport() && !setting.isSetToDefault()) {
LogHelper.printDebug(() -> "Resetting to default: " + setting); LogHelper.printDebug(() -> "Resetting to default: " + setting);
rebootSettingChanged |= setting.rebootApp; rebootSettingChanged |= setting.rebootApp;
setting.saveValue(setting.defaultValue); setting.resetToDefault();
} }
} }
numberOfSettingsImported += SponsorBlockSettings.importCategoriesFromFlatJson(json); numberOfSettingsImported += SponsorBlockSettings.importCategoriesFromFlatJson(json);

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -13,7 +13,6 @@ import android.preference.PreferenceScreen;
import android.preference.SwitchPreference; import android.preference.SwitchPreference;
import app.revanced.integrations.patches.ReturnYouTubeDislikePatch; import app.revanced.integrations.patches.ReturnYouTubeDislikePatch;
import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.settings.SettingsEnum;
@ -21,9 +20,6 @@ import app.revanced.integrations.settings.SharedPrefCategory;
public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
private static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER =
SpoofAppVersionPatch.isSpoofingToEqualOrLessThan("18.33.40");
/** /**
* If dislikes are shown on Shorts. * If dislikes are shown on Shorts.
*/ */
@ -79,7 +75,7 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
shortsPreference.setChecked(SettingsEnum.RYD_SHORTS.getBoolean()); shortsPreference.setChecked(SettingsEnum.RYD_SHORTS.getBoolean());
shortsPreference.setTitle(str("revanced_ryd_shorts_title")); shortsPreference.setTitle(str("revanced_ryd_shorts_title"));
String shortsSummary = str("revanced_ryd_shorts_summary_on", String shortsSummary = str("revanced_ryd_shorts_summary_on",
IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER ReturnYouTubeDislikePatch.IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER
? "" ? ""
: "\n\n" + str("revanced_ryd_shorts_summary_disclaimer")); : "\n\n" + str("revanced_ryd_shorts_summary_disclaimer"));
shortsPreference.setSummaryOn(shortsSummary); shortsPreference.setSummaryOn(shortsSummary);

View File

@ -351,7 +351,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> { DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> {
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) { 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")); ReVancedUtils.showToastLong(str("sb_api_url_reset"));
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) { } else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) {
String serverAddress = editText.getText().toString(); String serverAddress = editText.getText().toString();
@ -583,8 +583,8 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
new AlertDialog.Builder(preference1.getContext()) new AlertDialog.Builder(preference1.getContext())
.setTitle(str("sb_stats_self_saved_reset_title")) .setTitle(str("sb_stats_self_saved_reset_title"))
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { .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_NUMBER_SEGMENTS.resetToDefault();
SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.saveValue(SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.defaultValue); SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault();
updateStatsSelfSaved.run(); updateStatsSelfSaved.run();
}) })
.setNegativeButton(android.R.string.no, null).show(); .setNegativeButton(android.R.string.no, null).show();

View File

@ -1,10 +1,11 @@
package app.revanced.integrations.shared package app.revanced.integrations.shared
import app.revanced.integrations.patches.VideoInformation
import app.revanced.integrations.utils.Event import app.revanced.integrations.utils.Event
import app.revanced.integrations.utils.LogHelper import app.revanced.integrations.utils.LogHelper
/** /**
* WatchWhile player type * WatchWhile player type.
*/ */
enum class PlayerType { enum class PlayerType {
/** /**
@ -83,6 +84,8 @@ enum class PlayerType {
* Does not include the first moment after a short is opened when a regular video is minimized on screen, * Does not include the first moment after a short is opened when a regular video is minimized on screen,
* or while watching a short with a regular video present on a spoofed 16.x version of YouTube. * or while watching a short with a regular video present on a spoofed 16.x version of YouTube.
* To include those situations instead use [isNoneHiddenOrMinimized]. * To include those situations instead use [isNoneHiddenOrMinimized].
*
* @see VideoInformation
*/ */
fun isNoneOrHidden(): Boolean { fun isNoneOrHidden(): Boolean {
return this == NONE || this == HIDDEN return this == NONE || this == HIDDEN
@ -99,6 +102,7 @@ enum class PlayerType {
* though a Short is being opened or is on screen (see [isNoneHiddenOrMinimized]). * though a Short is being opened or is on screen (see [isNoneHiddenOrMinimized]).
* *
* @return If nothing, a Short, or a regular video is sliding off screen to a dismissed or hidden state. * @return If nothing, a Short, or a regular video is sliding off screen to a dismissed or hidden state.
* @see VideoInformation
*/ */
fun isNoneHiddenOrSlidingMinimized(): Boolean { fun isNoneHiddenOrSlidingMinimized(): Boolean {
return isNoneOrHidden() || this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED return isNoneOrHidden() || this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED
@ -117,6 +121,7 @@ enum class PlayerType {
* *
* @return If nothing, a Short, a regular video is sliding off screen to a dismissed or hidden state, * @return If nothing, a Short, a regular video is sliding off screen to a dismissed or hidden state,
* a regular video is minimized (and a new video is not being opened). * a regular video is minimized (and a new video is not being opened).
* @see VideoInformation
*/ */
fun isNoneHiddenOrMinimized(): Boolean { fun isNoneHiddenOrMinimized(): Boolean {
return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED

View File

@ -7,13 +7,15 @@ import android.preference.PreferenceFragment;
import android.view.View; import android.view.View;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import app.revanced.tiktok.utils.LogHelper;
import app.revanced.tiktok.utils.ReVancedUtils;
import com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization.AdPersonalizationActivity; import com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization.AdPersonalizationActivity;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import app.revanced.tiktok.utils.LogHelper;
import app.revanced.tiktok.utils.ReVancedUtils;
public class SettingsMenu { public class SettingsMenu {
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) { public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
@ -22,10 +24,9 @@ public class SettingsMenu {
Class<?> entryInfoClazz = Class.forName(entryInfoClazzName); Class<?> entryInfoClazz = Class.forName(entryInfoClazzName);
Constructor<?> entryConstructor = entryClazz.getConstructor(entryInfoClazz); Constructor<?> entryConstructor = entryClazz.getConstructor(entryInfoClazz);
Constructor<?> entryInfoConstructor = entryInfoClazz.getDeclaredConstructors()[0]; Constructor<?> entryInfoConstructor = entryInfoClazz.getDeclaredConstructors()[0];
Object buttonInfo = entryInfoConstructor.newInstance("Revanced settings", null, (View.OnClickListener) view -> startSettingsActivity()); Object buttonInfo = entryInfoConstructor.newInstance("ReVanced settings", null, (View.OnClickListener) view -> startSettingsActivity(), "revanced");
return entryConstructor.newInstance(buttonInfo); return entryConstructor.newInstance(buttonInfo);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) {
InstantiationException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }

View File

@ -0,0 +1,4 @@
package org.chromium.net;
public abstract class UrlRequest {
}

View File

@ -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();
}

View File

@ -1,4 +1,4 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
android.useAndroidX = true android.useAndroidX = true
version = 0.125.0 version = 1.0.0-dev.12

View File

@ -2,7 +2,7 @@ rootProject.name = "revanced-integrations"
buildCache { buildCache {
local { local {
isEnabled = !System.getenv().containsKey("CI") isEnabled = "CI" !in System.getenv()
} }
} }