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)

View File

@ -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.
* <p>
* 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,
* <p>
* 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.
* 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.
* <p>
* 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.
* <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 {
// 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);
}
}
}

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;
@SuppressWarnings("unused")
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() {
// This could be done entirely in the patch,
// 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.Nullable;
import app.revanced.integrations.patches.components.ReturnYouTubeDislikeFilterPatch;
import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType;
@ -27,19 +28,25 @@ import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislik
* Handles all interaction of UI patch components.
*
* 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:
* Enable app spoofing to version 18.33.40 or older, as that uses a non litho Shorts player.
*
* Permanent fix (yet to be implemented), either of:
* - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes asynchronously.
* - Find a way to force Litho to rebuild it's component tree
* (and use that hook to force the shorts dislikes to update after the fetch is completed).
* A (yet to be implemented) solution that fixes this problem. Any one of:
* - 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,
* and use that hook to force the shorts dislikes to update after the fetch is completed.
* - Hook into the dislikes button image view, and replace the dislikes thumb down image with a
* generated image of the number of dislikes, then update the image asynchronously. This Could
* 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")
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.
*/
@ -318,8 +325,12 @@ public class ReturnYouTubeDislikePatch {
try {
if (SettingsEnum.RYD_ENABLED.getBoolean() && !SettingsEnum.RYD_COMPACT_LAYOUT.getBoolean()) {
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
+ ReturnYouTubeDislike.leftSeparatorShapePaddingPixels;
+ ReturnYouTubeDislike.leftSeparatorShapePaddingPixels + 1;
}
}
} catch (Exception ex) {
@ -342,6 +353,10 @@ public class ReturnYouTubeDislikePatch {
} else {
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");
view.setCompoundDrawablePadding(0);
view.setCompoundDrawables(null, null, null, null);
view.setSingleLine(false);
}
}
@ -540,26 +556,47 @@ public class ReturnYouTubeDislikePatch {
// Video Id and voting hooks (all players).
//
private static volatile boolean lastPlayerResponseWasShort;
/**
* 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 {
// 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 (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_ENABLED.getBoolean()) {
return;
}
if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized()) {
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
return;
}
if (videoId.equals(lastPrefetchedVideoId)) {
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);
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) {
LogHelper.printException(() -> "preloadVideoId failure", ex);
}

View File

@ -17,6 +17,10 @@ import java.util.Objects;
public final class VideoInformation {
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
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 Method seekMethod;
@ -28,6 +32,7 @@ public final class VideoInformation {
@NonNull
private static volatile String playerResponseVideoId = "";
private static volatile boolean videoIdIsShort;
/**
* 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.
*
* @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)) {
LogHelper.printDebug(() -> "New player response video id: " + videoId);
playerResponseVideoId = videoId;
@ -135,10 +161,8 @@ public final class VideoInformation {
public static boolean seekTo(final long millisecond) {
final long videoLength = getVideoLength();
// Don't seek more than the video length to prevent issues such as
// Play pause button or autoplay not working.
// TODO: These are arbitrarily chosen values and should be subject to be adjusted.
final long seekToMilliseconds = millisecond <= videoLength - 500 ? millisecond : millisecond - 100;
// Prevent issues such as play/ pause button or autoplay not working.
final long seekToMilliseconds = Math.min(millisecond, VideoInformation.getVideoLength() - 250);
ReVancedUtils.verifyOnMainThread();
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
public static String getVideoId() {
@ -168,20 +192,30 @@ public final class VideoInformation {
/**
* 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>
* If Shorts are loading the background, this commonly will be
* different from the Short that is currently on screen.
* <p>
* 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
public static String getPlayerResponseVideoId() {
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.
*/

View File

@ -32,6 +32,9 @@ public final class AnnouncementsPatch {
public static void showAnnouncement(final Activity context) {
if (!SettingsEnum.ANNOUNCEMENTS.getBoolean()) return;
// Check if there is internet connection
if (!ReVancedUtils.isNetworkConnected()) return;
ReVancedUtils.runOnBackgroundThread(() -> {
try {
HttpURLConnection connection = AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT, CONSUMER);
@ -43,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;
@ -118,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;
}

View File

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

View File

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

View File

@ -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() {

View File

@ -1,5 +1,6 @@
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 android.view.View;
@ -8,11 +9,16 @@ import android.widget.ImageView;
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.spoof.requests.StoryboardRendererRequester;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
/** @noinspection unused*/
public class SpoofSignaturePatch {
@ -23,6 +29,11 @@ public class SpoofSignaturePatch {
*/
private static final String INCOGNITO_PARAMETERS = "CgIQBg==";
/**
* Parameters used when playing clips.
*/
private static final String CLIPS_PARAMETERS = "kAIB";
/**
* Parameters causing playback issues.
*/
@ -37,11 +48,6 @@ public class SpoofSignaturePatch {
*/
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.
*/
@ -49,12 +55,30 @@ public class SpoofSignaturePatch {
private static volatile String lastPlayerResponseVideoId;
@Nullable
private static volatile StoryboardRenderer videoRenderer;
private static volatile Future<StoryboardRenderer> rendererFuture;
private static volatile boolean useOriginalStoryboardRenderer;
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.
*
@ -62,19 +86,25 @@ public class SpoofSignaturePatch {
*
* @param parameters Original protobuf parameter value.
*/
public static String spoofParameter(String parameters) {
public static String spoofParameter(String parameters, boolean isShortAndOpeningOrPlaying) {
try {
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)
// 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.
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.
if (useOriginalStoryboardRenderer = parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) {
//noinspection AssignmentUsedAsCondition
if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) {
isPlayingShorts = true;
return parameters;
}
@ -83,6 +113,7 @@ public class SpoofSignaturePatch {
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
&& containsAny(parameters, AUTOPLAY_PARAMETERS);
if (isPlayingFeed) {
//noinspection AssignmentUsedAsCondition
if (useOriginalStoryboardRenderer = !SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean()) {
// Don't spoof the feed video playback. This will cause video playback issues,
// but only if user continues watching for more than 1 minute.
@ -103,27 +134,32 @@ public class SpoofSignaturePatch {
private static void fetchStoryboardRenderer() {
if (!SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
lastPlayerResponseVideoId = null;
videoRenderer = null;
rendererFuture = null;
return;
}
String videoId = VideoInformation.getPlayerResponseVideoId();
if (!videoId.equals(lastPlayerResponseVideoId)) {
rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(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,
boolean returnNullIfLiveStream) {
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
StoryboardRenderer renderer = videoRenderer;
StoryboardRenderer renderer = getRenderer(false);
if (renderer != null) {
if (returnNullIfLiveStream && renderer.isLiveStream()) return null;
return renderer.getSpec();
if (returnNullIfLiveStream && renderer.isLiveStream()) {
return null;
}
String spec = renderer.getSpec();
if (spec != null) {
return spec;
}
}
}
@ -154,7 +190,7 @@ public class SpoofSignaturePatch {
*/
public static int getRecommendedLevel(int originalLevel) {
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
StoryboardRenderer renderer = videoRenderer;
StoryboardRenderer renderer = getRenderer(false);
if (renderer != null) {
Integer recommendedLevel = renderer.getRecommendedLevel();
if (recommendedLevel != null) return recommendedLevel;
@ -172,7 +208,7 @@ public class SpoofSignaturePatch {
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) {
return false;
}
StoryboardRenderer renderer = videoRenderer;
StoryboardRenderer renderer = getRenderer(false);
if (renderer == null) {
// Spoof storyboard renderer is turned off,
// video is paid, or the storyboard fetch timed out.

View File

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

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.
* 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.
*/
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.

View File

@ -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),
@ -125,6 +129,8 @@ public enum SettingsEnum {
TABLET_LAYOUT("revanced_tablet_layout", BOOLEAN, FALSE, true, "revanced_tablet_layout_user_dialog_message"),
USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true),
WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true),
START_PAGE("revanced_start_page", STRING, ""),
// Description
HIDE_CHAPTERS("revanced_hide_chapters", 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
+ "' from: " + oldSetting + " into replacement setting: " + newSetting);
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.
* <p>
@ -694,7 +707,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);

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 app.revanced.integrations.patches.ReturnYouTubeDislikePatch;
import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.integrations.settings.SettingsEnum;
@ -21,9 +20,6 @@ import app.revanced.integrations.settings.SharedPrefCategory;
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.
*/
@ -79,7 +75,7 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
shortsPreference.setChecked(SettingsEnum.RYD_SHORTS.getBoolean());
shortsPreference.setTitle(str("revanced_ryd_shorts_title"));
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"));
shortsPreference.setSummaryOn(shortsSummary);

View File

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

View File

@ -1,10 +1,11 @@
package app.revanced.integrations.shared
import app.revanced.integrations.patches.VideoInformation
import app.revanced.integrations.utils.Event
import app.revanced.integrations.utils.LogHelper
/**
* WatchWhile player type
* WatchWhile player type.
*/
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,
* 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].
*
* @see VideoInformation
*/
fun isNoneOrHidden(): Boolean {
return this == NONE || this == HIDDEN
@ -99,6 +102,7 @@ enum class PlayerType {
* 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.
* @see VideoInformation
*/
fun isNoneHiddenOrSlidingMinimized(): Boolean {
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,
* a regular video is minimized (and a new video is not being opened).
* @see VideoInformation
*/
fun isNoneHiddenOrMinimized(): Boolean {
return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED

View File

@ -7,13 +7,15 @@ import android.preference.PreferenceFragment;
import android.view.View;
import android.widget.FrameLayout;
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 java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import app.revanced.tiktok.utils.LogHelper;
import app.revanced.tiktok.utils.ReVancedUtils;
public class SettingsMenu {
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
@ -22,10 +24,9 @@ public class SettingsMenu {
Class<?> entryInfoClazz = Class.forName(entryInfoClazzName);
Constructor<?> entryConstructor = entryClazz.getConstructor(entryInfoClazz);
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);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException |
InstantiationException e) {
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException 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.caching = 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 {
local {
isEnabled = !System.getenv().containsKey("CI")
isEnabled = "CI" !in System.getenv()
}
}