mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-05-17 05:57:05 +02:00
chore: Merge branch dev
to main
(#530)
This commit is contained in:
commit
69c5028661
96
CHANGELOG.md
96
CHANGELOG.md
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
@ -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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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() {
|
||||||
|
@ -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.
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
dummy/src/main/java/org/chromium/net/UrlRequest.java
Normal file
4
dummy/src/main/java/org/chromium/net/UrlRequest.java
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package org.chromium.net;
|
||||||
|
|
||||||
|
public abstract class UrlRequest {
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -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
|
||||||
|
@ -2,7 +2,7 @@ rootProject.name = "revanced-integrations"
|
|||||||
|
|
||||||
buildCache {
|
buildCache {
|
||||||
local {
|
local {
|
||||||
isEnabled = !System.getenv().containsKey("CI")
|
isEnabled = "CI" !in System.getenv()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user