mirror of
https://github.com/revanced/revanced-integrations.git
synced 2024-11-19 02:19:23 +01: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)
|
||||
|
||||
|
||||
|
@ -1,11 +1,17 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import org.chromium.net.UrlRequest;
|
||||
import org.chromium.net.UrlResponseInfo;
|
||||
import org.chromium.net.impl.CronetUrlRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
@ -13,30 +19,289 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
/**
|
||||
* Alternative YouTube thumbnails, showing the beginning/middle/end of the video.
|
||||
* Alternative YouTube thumbnails.
|
||||
* <p>
|
||||
* Can show YouTube provided screen captures of beginning/middle/end of the video.
|
||||
* (ie: sd1.jpg, sd2.jpg, sd3.jpg).
|
||||
*
|
||||
* Has an additional option to use 'fast' thumbnails,
|
||||
* <p>
|
||||
* Or can show crowdsourced thumbnails provided by DeArrow (<a href="http://dearrow.ajay.app">...</a>).
|
||||
* <p>
|
||||
* Or can use DeArrow and fall back to screen captures if DeArrow is not available.
|
||||
* <p>
|
||||
* Has an additional option to use 'fast' video still thumbnails,
|
||||
* where it forces sd thumbnail quality and skips verifying if the alt thumbnail image exists.
|
||||
* The UI loading time will be the same or better than using the the original thumbnails,
|
||||
* The UI loading time will be the same or better than using original thumbnails,
|
||||
* but thumbnails will initially fail to load for all live streams, unreleased, and occasionally very old videos.
|
||||
* If a failed thumbnail load is reloaded (ie: scroll off, then on screen), then the original thumbnail
|
||||
* is reloaded instead. Fast thumbnails requires using SD or lower thumbnail resolution,
|
||||
* because a noticeable number of videos do not have hq720 and too many fail to load.
|
||||
*
|
||||
* because a noticeable number of videos do not have hq720 and too much fail to load.
|
||||
* <p>
|
||||
* Ideas for improvements:
|
||||
* - Selectively allow using original thumbnails in some situations,
|
||||
* such as videos subscription feed, watch history, or in search results.
|
||||
* - Save to a temporary file the video id's verified to have alt thumbnails.
|
||||
* This would speed up loading the watch history and users saved playlists.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class AlternativeThumbnailsPatch {
|
||||
|
||||
private static final Uri dearrowApiUri;
|
||||
|
||||
/**
|
||||
* The scheme and host of {@link #dearrowApiUri}.
|
||||
*/
|
||||
private static final String deArrowApiUrlPrefix;
|
||||
|
||||
/**
|
||||
* How long to temporarily turn off DeArrow if it fails for any reason.
|
||||
*/
|
||||
private static final long DEARROW_FAILURE_API_BACKOFF_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes.
|
||||
|
||||
/**
|
||||
* If non zero, then the system time of when DeArrow API calls can resume.
|
||||
*/
|
||||
private static volatile long timeToResumeDeArrowAPICalls;
|
||||
|
||||
static {
|
||||
dearrowApiUri = validateSettings();
|
||||
final int port = dearrowApiUri.getPort();
|
||||
String portString = port == -1 ? "" : (":" + port);
|
||||
deArrowApiUrlPrefix = dearrowApiUri.getScheme() + "://" + dearrowApiUri.getHost() + portString + "/";
|
||||
LogHelper.printDebug(() -> "Using DeArrow API address: " + deArrowApiUrlPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix any bad imported data.
|
||||
*/
|
||||
private static Uri validateSettings() {
|
||||
final int altThumbnailType = SettingsEnum.ALT_THUMBNAIL_STILLS_TIME.getInt();
|
||||
if (altThumbnailType < 1 || altThumbnailType > 3) {
|
||||
ReVancedUtils.showToastLong("Invalid Alternative still thumbnail type: "
|
||||
+ altThumbnailType + ". Using default");
|
||||
SettingsEnum.ALT_THUMBNAIL_STILLS_TIME.resetToDefault();
|
||||
}
|
||||
|
||||
Uri apiUri = Uri.parse(SettingsEnum.ALT_THUMBNAIL_DEARROW_API_URL.getString());
|
||||
// Cannot use unsecured 'http', otherwise the connections fail to start and no callbacks hooks are made.
|
||||
String scheme = apiUri.getScheme();
|
||||
if (scheme == null || scheme.equals("http") || apiUri.getHost() == null) {
|
||||
ReVancedUtils.showToastLong("Invalid DeArrow API URL. Using default");
|
||||
SettingsEnum.ALT_THUMBNAIL_DEARROW_API_URL.resetToDefault();
|
||||
return validateSettings();
|
||||
}
|
||||
return apiUri;
|
||||
}
|
||||
|
||||
private static boolean usingDeArrow() {
|
||||
return SettingsEnum.ALT_THUMBNAIL_DEARROW.getBoolean();
|
||||
}
|
||||
|
||||
private static boolean usingVideoStills() {
|
||||
return SettingsEnum.ALT_THUMBNAIL_STILLS.getBoolean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the alternative thumbnail url using YouTube provided still video captures.
|
||||
*
|
||||
* @param decodedUrl Decoded original thumbnail request url.
|
||||
* @return The alternative thumbnail url, or the original url. Both without tracking parameters.
|
||||
*/
|
||||
@NonNull
|
||||
private static String buildYoutubeVideoStillURL(@NonNull DecodedThumbnailUrl decodedUrl,
|
||||
@NonNull ThumbnailQuality qualityToUse) {
|
||||
String sanitizedReplacement = decodedUrl.createStillsUrl(qualityToUse, false);
|
||||
if (VerifiedQualities.verifyAltThumbnailExist(decodedUrl.videoId, qualityToUse, sanitizedReplacement)) {
|
||||
return sanitizedReplacement;
|
||||
}
|
||||
return decodedUrl.sanitizedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the alternative thumbnail url using DeArrow thumbnail cache.
|
||||
*
|
||||
* @param videoId ID of the video to get a thumbnail of. Can be any video (regular or Short).
|
||||
* @param fallbackUrl URL to fall back to in case.
|
||||
* @return The alternative thumbnail url, without tracking parameters.
|
||||
*/
|
||||
@NonNull
|
||||
private static String buildDeArrowThumbnailURL(String videoId, String fallbackUrl) {
|
||||
// Build thumbnail request url.
|
||||
// See https://github.com/ajayyy/DeArrowThumbnailCache/blob/29eb4359ebdf823626c79d944a901492d760bbbc/app.py#L29.
|
||||
return dearrowApiUri
|
||||
.buildUpon()
|
||||
.appendQueryParameter("videoID", videoId)
|
||||
.appendQueryParameter("redirectUrl", fallbackUrl)
|
||||
.build()
|
||||
.toString();
|
||||
}
|
||||
|
||||
private static boolean urlIsDeArrow(@NonNull String imageUrl) {
|
||||
return imageUrl.startsWith(deArrowApiUrlPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If this client has not recently experienced any DeArrow API errors.
|
||||
*/
|
||||
private static boolean canUseDeArrowAPI() {
|
||||
if (timeToResumeDeArrowAPICalls == 0) {
|
||||
return true;
|
||||
}
|
||||
if (timeToResumeDeArrowAPICalls < System.currentTimeMillis()) {
|
||||
LogHelper.printDebug(() -> "Resuming DeArrow API calls");
|
||||
timeToResumeDeArrowAPICalls = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void handleDeArrowError(@NonNull String url, int statusCode) {
|
||||
LogHelper.printDebug(() -> "Encountered DeArrow error. Url: " + url);
|
||||
final long now = System.currentTimeMillis();
|
||||
if (timeToResumeDeArrowAPICalls < now) {
|
||||
timeToResumeDeArrowAPICalls = now + DEARROW_FAILURE_API_BACKOFF_MILLISECONDS;
|
||||
if (SettingsEnum.ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST.getBoolean()) {
|
||||
String toastMessage = (statusCode != 0)
|
||||
? str("revanced_alt_thumbnail_dearrow_error", statusCode)
|
||||
: str("revanced_alt_thumbnail_dearrow_error_generic");
|
||||
ReVancedUtils.showToastLong(toastMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread and by multiple threads at the same time.
|
||||
*
|
||||
* @param originalUrl Image url for all url images loaded, including video thumbnails.
|
||||
*/
|
||||
public static String overrideImageURL(String originalUrl) {
|
||||
try {
|
||||
final boolean usingDeArrow = usingDeArrow();
|
||||
final boolean usingVideoStills = usingVideoStills();
|
||||
if (!usingDeArrow && !usingVideoStills) {
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
final var decodedUrl = DecodedThumbnailUrl.decodeImageUrl(originalUrl);
|
||||
if (decodedUrl == null) {
|
||||
return originalUrl; // Not a thumbnail.
|
||||
}
|
||||
|
||||
LogHelper.printDebug(() -> "Original url: " + decodedUrl.sanitizedUrl);
|
||||
|
||||
ThumbnailQuality qualityToUse = ThumbnailQuality.getQualityToUse(decodedUrl.imageQuality);
|
||||
if (qualityToUse == null) {
|
||||
// Thumbnail is a Short or a Storyboard image used for seekbar thumbnails (must not replace these).
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
String sanitizedReplacementUrl;
|
||||
final boolean includeTracking;
|
||||
if (usingDeArrow && canUseDeArrowAPI()) {
|
||||
includeTracking = false; // Do not include view tracking parameters with API call.
|
||||
final String fallbackUrl = usingVideoStills
|
||||
? buildYoutubeVideoStillURL(decodedUrl, qualityToUse)
|
||||
: decodedUrl.sanitizedUrl;
|
||||
|
||||
sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl);
|
||||
} else if (usingVideoStills) {
|
||||
includeTracking = true; // Include view tracking parameters if present.
|
||||
sanitizedReplacementUrl = buildYoutubeVideoStillURL(decodedUrl, qualityToUse);
|
||||
} else {
|
||||
return originalUrl; // Recently experienced DeArrow failure and video stills are not enabled.
|
||||
}
|
||||
|
||||
// Do not log any tracking parameters.
|
||||
LogHelper.printDebug(() -> "Replacement url: " + sanitizedReplacementUrl);
|
||||
|
||||
return includeTracking
|
||||
? sanitizedReplacementUrl + decodedUrl.viewTrackingParameters
|
||||
: sanitizedReplacementUrl;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "overrideImageURL failure", ex);
|
||||
return originalUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Cronet considers all completed connections as a success, even if the response is 404 or 5xx.
|
||||
*/
|
||||
public static void handleCronetSuccess(UrlRequest request, @NonNull UrlResponseInfo responseInfo) {
|
||||
try {
|
||||
final int statusCode = responseInfo.getHttpStatusCode();
|
||||
if (statusCode != 200) {
|
||||
String url = responseInfo.getUrl();
|
||||
|
||||
if (usingDeArrow() && urlIsDeArrow(url)) {
|
||||
LogHelper.printDebug(() -> "handleCronetSuccess, statusCode: " + statusCode);
|
||||
handleDeArrowError(url, statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (usingVideoStills() && statusCode == 404) {
|
||||
// Fast alt thumbnails is enabled and the thumbnail is not available.
|
||||
// The video is:
|
||||
// - live stream
|
||||
// - upcoming unreleased video
|
||||
// - very old
|
||||
// - very low view count
|
||||
// Take note of this, so if the image reloads the original thumbnail will be used.
|
||||
DecodedThumbnailUrl decodedUrl = DecodedThumbnailUrl.decodeImageUrl(url);
|
||||
if (decodedUrl == null) {
|
||||
return; // Not a thumbnail.
|
||||
}
|
||||
|
||||
LogHelper.printDebug(() -> "handleCronetSuccess, image not available: " + url);
|
||||
|
||||
ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedUrl.imageQuality);
|
||||
if (quality == null) {
|
||||
// Video is a short or a seekbar thumbnail, but somehow did not load. Should not happen.
|
||||
LogHelper.printDebug(() -> "Failed to recognize image quality of url: " + decodedUrl.sanitizedUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
VerifiedQualities.setAltThumbnailDoesNotExist(decodedUrl.videoId, quality);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Callback success error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* To test failure cases, try changing the API URL to each of:
|
||||
* - A non-existent domain.
|
||||
* - A url path of something incorrect (ie: /v1/nonExistentEndPoint).
|
||||
* <p>
|
||||
* Known limitation: YT uses an infinite timeout, so this hook is never called if a host never responds.
|
||||
* But this does not appear to be a problem, as the DeArrow API has not been observed to 'go silent'
|
||||
* Instead if there's a problem it returns an error code status response, which is handled in this patch.
|
||||
*/
|
||||
public static void handleCronetFailure(UrlRequest request,
|
||||
@Nullable UrlResponseInfo responseInfo,
|
||||
IOException exception) {
|
||||
try {
|
||||
if (usingDeArrow()) {
|
||||
String url = ((CronetUrlRequest) request).getHookedUrl();
|
||||
if (urlIsDeArrow(url)) {
|
||||
LogHelper.printDebug(() -> "handleCronetFailure, exception: " + exception);
|
||||
final int statusCode = (responseInfo != null)
|
||||
? responseInfo.getHttpStatusCode()
|
||||
: 0;
|
||||
handleDeArrowError(url, statusCode);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Callback failure error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private enum ThumbnailQuality {
|
||||
// In order of lowest to highest resolution.
|
||||
DEFAULT("default", ""), // effective alt name is 1.jpg, 2.jpg, 3.jpg
|
||||
@ -61,6 +326,11 @@ public final class AlternativeThumbnailsPatch {
|
||||
originalNameToEnum.put(quality.originalName, quality);
|
||||
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
// 'custom' thumbnails set by the content creator.
|
||||
// These show up in place of regular thumbnails
|
||||
// and seem to be limited to [1, 3] range.
|
||||
originalNameToEnum.put(quality.originalName + "_custom_" + i, quality);
|
||||
|
||||
altNameToEnum.put(quality.altImageName + i, quality);
|
||||
}
|
||||
}
|
||||
@ -86,7 +356,7 @@ public final class AlternativeThumbnailsPatch {
|
||||
return null; // Not a thumbnail for a regular video.
|
||||
}
|
||||
|
||||
final boolean useFastQuality = SettingsEnum.ALT_THUMBNAIL_FAST_QUALITY.getBoolean();
|
||||
final boolean useFastQuality = SettingsEnum.ALT_THUMBNAIL_STILLS_FAST.getBoolean();
|
||||
switch (quality) {
|
||||
case SDDEFAULT:
|
||||
// SD alt images have somewhat worse quality with washed out color and poor contrast.
|
||||
@ -121,7 +391,7 @@ public final class AlternativeThumbnailsPatch {
|
||||
}
|
||||
|
||||
String getAltImageNameToUse() {
|
||||
return altImageName + SettingsEnum.ALT_THUMBNAIL_TYPE.getInt();
|
||||
return altImageName + SettingsEnum.ALT_THUMBNAIL_STILLS_TIME.getInt();
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,7 +416,7 @@ public final class AlternativeThumbnailsPatch {
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry eldest) {
|
||||
return size() > CACHE_LIMIT; // Evict oldest entry if over the cache limit.
|
||||
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
|
||||
}
|
||||
};
|
||||
|
||||
@ -166,13 +436,14 @@ public final class AlternativeThumbnailsPatch {
|
||||
|
||||
static boolean verifyAltThumbnailExist(@NonNull String videoId, @NonNull ThumbnailQuality quality,
|
||||
@NonNull String imageUrl) {
|
||||
VerifiedQualities verified = getVerifiedQualities(videoId, SettingsEnum.ALT_THUMBNAIL_FAST_QUALITY.getBoolean());
|
||||
VerifiedQualities verified = getVerifiedQualities(videoId, SettingsEnum.ALT_THUMBNAIL_STILLS_FAST.getBoolean());
|
||||
if (verified == null) return true; // Fast alt thumbnails is enabled.
|
||||
return verified.verifyYouTubeThumbnailExists(videoId, quality, imageUrl);
|
||||
}
|
||||
|
||||
static void setAltThumbnailDoesNotExist(@NonNull String videoId, @NonNull ThumbnailQuality quality) {
|
||||
VerifiedQualities verified = getVerifiedQualities(videoId, false);
|
||||
//noinspection ConstantConditions
|
||||
verified.setQualityVerified(videoId, quality, false);
|
||||
}
|
||||
|
||||
@ -180,20 +451,20 @@ public final class AlternativeThumbnailsPatch {
|
||||
* Highest quality verified as existing.
|
||||
*/
|
||||
@Nullable
|
||||
ThumbnailQuality highestQualityVerified;
|
||||
private ThumbnailQuality highestQualityVerified;
|
||||
/**
|
||||
* Lowest quality verified as not existing.
|
||||
*/
|
||||
@Nullable
|
||||
ThumbnailQuality lowestQualityNotAvailable;
|
||||
private ThumbnailQuality lowestQualityNotAvailable;
|
||||
|
||||
/**
|
||||
* System time, of when to invalidate {@link #lowestQualityNotAvailable}.
|
||||
* Used only if fast mode is not enabled.
|
||||
*/
|
||||
long timeToReVerifyLowestQuality;
|
||||
private long timeToReVerifyLowestQuality;
|
||||
|
||||
synchronized void setQualityVerified(String videoId, ThumbnailQuality quality, boolean isVerified) {
|
||||
private synchronized void setQualityVerified(String videoId, ThumbnailQuality quality, boolean isVerified) {
|
||||
if (isVerified) {
|
||||
if (highestQualityVerified == null || highestQualityVerified.ordinal() < quality.ordinal()) {
|
||||
highestQualityVerified = quality;
|
||||
@ -216,7 +487,7 @@ public final class AlternativeThumbnailsPatch {
|
||||
return true; // Previously verified as existing.
|
||||
}
|
||||
|
||||
final boolean fastQuality = SettingsEnum.ALT_THUMBNAIL_FAST_QUALITY.getBoolean();
|
||||
final boolean fastQuality = SettingsEnum.ALT_THUMBNAIL_STILLS_FAST.getBoolean();
|
||||
if (lowestQualityNotAvailable != null && lowestQualityNotAvailable.ordinal() <= quality.ordinal()) {
|
||||
if (fastQuality || System.currentTimeMillis() < timeToReVerifyLowestQuality) {
|
||||
return false; // Previously verified as not existing.
|
||||
@ -279,131 +550,61 @@ public final class AlternativeThumbnailsPatch {
|
||||
static DecodedThumbnailUrl decodeImageUrl(String url) {
|
||||
final int videoIdStartIndex = url.indexOf('/', YOUTUBE_THUMBNAIL_PREFIX.length()) + 1;
|
||||
if (videoIdStartIndex <= 0) return null;
|
||||
|
||||
final int videoIdEndIndex = url.indexOf('/', videoIdStartIndex);
|
||||
if (videoIdEndIndex < 0) return null;
|
||||
|
||||
final int imageSizeStartIndex = videoIdEndIndex + 1;
|
||||
final int imageSizeEndIndex = url.indexOf('.', imageSizeStartIndex);
|
||||
if (imageSizeEndIndex < 0) return null;
|
||||
|
||||
int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex);
|
||||
if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length();
|
||||
|
||||
return new DecodedThumbnailUrl(url, videoIdStartIndex, videoIdEndIndex,
|
||||
imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex);
|
||||
}
|
||||
|
||||
final String originalFullUrl;
|
||||
/** Full usable url, but stripped of any tracking information. */
|
||||
final String sanitizedUrl;
|
||||
/** Url up to the video id. */
|
||||
/** Url up to the video ID. */
|
||||
final String urlPrefix;
|
||||
final String videoId;
|
||||
/** Quality, such as hq720 or sddefault. */
|
||||
final String imageQuality;
|
||||
/** jpg or webp */
|
||||
/** JPG or WEBP */
|
||||
final String imageExtension;
|
||||
/** User view tracking parameters, only present on some images. */
|
||||
final String urlTrackingParameters;
|
||||
final String viewTrackingParameters;
|
||||
|
||||
private DecodedThumbnailUrl(String fullUrl, int videoIdStartIndex, int videoIdEndIndex,
|
||||
DecodedThumbnailUrl(String fullUrl, int videoIdStartIndex, int videoIdEndIndex,
|
||||
int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) {
|
||||
originalFullUrl = fullUrl;
|
||||
sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex);
|
||||
urlPrefix = fullUrl.substring(0, videoIdStartIndex);
|
||||
videoId = fullUrl.substring(videoIdStartIndex, videoIdEndIndex);
|
||||
imageQuality = fullUrl.substring(imageSizeStartIndex, imageSizeEndIndex);
|
||||
imageExtension = fullUrl.substring(imageSizeEndIndex + 1, imageExtensionEndIndex);
|
||||
urlTrackingParameters = (imageExtensionEndIndex == fullUrl.length())
|
||||
viewTrackingParameters = (imageExtensionEndIndex == fullUrl.length())
|
||||
? "" : fullUrl.substring(imageExtensionEndIndex);
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
// Fix any bad imported data.
|
||||
final int altThumbnailType = SettingsEnum.ALT_THUMBNAIL_TYPE.getInt();
|
||||
if (altThumbnailType < 1 || altThumbnailType > 3) {
|
||||
LogHelper.printException(() -> "Invalid alt thumbnail type: " + altThumbnailType);
|
||||
SettingsEnum.ALT_THUMBNAIL_TYPE.saveValue(SettingsEnum.ALT_THUMBNAIL_TYPE.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread and by multiple threads at the same time.
|
||||
*
|
||||
* @param originalUrl Image url for all url images loaded, including video thumbnails.
|
||||
*/
|
||||
public static String overrideImageURL(String originalUrl) {
|
||||
try {
|
||||
if (!SettingsEnum.ALT_THUMBNAIL.getBoolean()) {
|
||||
return originalUrl;
|
||||
}
|
||||
DecodedThumbnailUrl decodedUrl = DecodedThumbnailUrl.decodeImageUrl(originalUrl);
|
||||
if (decodedUrl == null) {
|
||||
return originalUrl; // Not a thumbnail.
|
||||
}
|
||||
|
||||
// Keep any tracking parameters out of the logs, and log only the base url.
|
||||
LogHelper.printDebug(() -> "Original url: " + decodedUrl.sanitizedUrl);
|
||||
|
||||
ThumbnailQuality qualityToUse = ThumbnailQuality.getQualityToUse(decodedUrl.imageQuality);
|
||||
if (qualityToUse == null) return originalUrl; // Video is a short.
|
||||
|
||||
/** @noinspection SameParameterValue*/
|
||||
String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) {
|
||||
// Images could be upgraded to webp if they are not already, but this fails quite often,
|
||||
// especially for new videos uploaded in the last hour.
|
||||
// And even if alt webp images do exist, sometimes they can load much slower than the original jpg alt images.
|
||||
// (as much as 4x slower has been observed, despite the alt webp image being a smaller file).
|
||||
|
||||
StringBuilder builder = new StringBuilder(originalUrl.length() + 2);
|
||||
builder.append(decodedUrl.urlPrefix);
|
||||
builder.append(decodedUrl.videoId).append('/');
|
||||
StringBuilder builder = new StringBuilder(originalFullUrl.length() + 2);
|
||||
builder.append(urlPrefix);
|
||||
builder.append(videoId).append('/');
|
||||
builder.append(qualityToUse.getAltImageNameToUse());
|
||||
builder.append('.').append(decodedUrl.imageExtension);
|
||||
|
||||
String sanitizedReplacement = builder.toString();
|
||||
if (!VerifiedQualities.verifyAltThumbnailExist(decodedUrl.videoId, qualityToUse, sanitizedReplacement)) {
|
||||
return originalUrl;
|
||||
builder.append('.').append(imageExtension);
|
||||
if (includeViewTracking) {
|
||||
builder.append(viewTrackingParameters);
|
||||
}
|
||||
|
||||
LogHelper.printDebug(() -> "Replaced url: " + sanitizedReplacement);
|
||||
|
||||
// URL tracking parameters. Presumably they are to determine if a user has viewed a thumbnail.
|
||||
// This likely is used for recommendations, so they are retained if present.
|
||||
builder.append(decodedUrl.urlTrackingParameters);
|
||||
return builder.toString();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Alt thumbnails failure", ex);
|
||||
return originalUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Cronet considers all completed connections as a success, even if the response is 404 or 5xx.
|
||||
*/
|
||||
public static void handleCronetSuccess(@NonNull UrlResponseInfo responseInfo) {
|
||||
try {
|
||||
if (responseInfo.getHttpStatusCode() == 404 && SettingsEnum.ALT_THUMBNAIL.getBoolean()) {
|
||||
// Fast alt thumbnails is enabled and the thumbnail is not available.
|
||||
// The video is:
|
||||
// - live stream
|
||||
// - upcoming unreleased video
|
||||
// - very old
|
||||
// - very low view count
|
||||
// Take note of this, so if the image reloads the original thumbnail will be used.
|
||||
DecodedThumbnailUrl decodedUrl = DecodedThumbnailUrl.decodeImageUrl(responseInfo.getUrl());
|
||||
if (decodedUrl == null) {
|
||||
return; // Not a thumbnail.
|
||||
}
|
||||
|
||||
ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedUrl.imageQuality);
|
||||
if (quality == null) {
|
||||
// Video is a short or unknown quality, but the url returned 404. Should never happen.
|
||||
LogHelper.printDebug(() -> "Failed to load unknown url: " + decodedUrl.sanitizedUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
VerifiedQualities.setAltThumbnailDoesNotExist(decodedUrl.videoId, quality);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Alt thumbnails callback failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class MinimizedPlaybackPatch {
|
||||
|
||||
public static boolean isPlaybackNotShort() {
|
||||
return !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized();
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean playbackIsNotShort() {
|
||||
// Steps to verify most edge cases:
|
||||
// 1. Open a regular video
|
||||
// 2. Minimize app (PIP should appear)
|
||||
// 3. Reopen app
|
||||
// 4. Open a Short (without closing the regular video)
|
||||
// (try opening both Shorts in the video player suggestions AND Shorts from the home feed)
|
||||
// 5. Minimize the app (PIP should not appear)
|
||||
// 6. Reopen app
|
||||
// 7. Close the Short
|
||||
// 8. Resume playing the regular video
|
||||
// 9. Minimize the app (PIP should appear)
|
||||
|
||||
if (!VideoInformation.lastVideoIdIsShort()) {
|
||||
return true; // Definitely is not a Short.
|
||||
}
|
||||
|
||||
// Might be a Short, or might be a prior regular video on screen again after a Short was closed.
|
||||
// This incorrectly prevents PIP if player is in WATCH_WHILE_MINIMIZED after closing a Short,
|
||||
// But there's no way around this unless an additional hook is added to definitively detect
|
||||
// the Shorts player is on screen. This use case is unusual anyways so it's not a huge concern.
|
||||
return !PlayerType.getCurrent().isNoneHiddenOrMinimized();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean overrideMinimizedPlaybackAvailable() {
|
||||
// This could be done entirely in the patch,
|
||||
// but having a unique method to search for makes manually inspecting the patched apk much easier.
|
||||
|
@ -9,6 +9,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.integrations.patches.components.ReturnYouTubeDislikeFilterPatch;
|
||||
import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch;
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
@ -27,19 +28,25 @@ import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislik
|
||||
* Handles all interaction of UI patch components.
|
||||
*
|
||||
* Known limitation:
|
||||
* Litho based Shorts player can experience temporarily frozen video playback if the RYD fetch takes too long.
|
||||
* The implementation of Shorts litho requires blocking the loading the first Short until RYD has completed.
|
||||
* This is because it modifies the dislikes text synchronously, and if the RYD fetch has
|
||||
* not completed yet then the UI will be temporarily frozen.
|
||||
*
|
||||
* Temporary work around:
|
||||
* Enable app spoofing to version 18.33.40 or older, as that uses a non litho Shorts player.
|
||||
*
|
||||
* Permanent fix (yet to be implemented), either of:
|
||||
* - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes asynchronously.
|
||||
* - Find a way to force Litho to rebuild it's component tree
|
||||
* (and use that hook to force the shorts dislikes to update after the fetch is completed).
|
||||
* A (yet to be implemented) solution that fixes this problem. Any one of:
|
||||
* - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes text asynchronously.
|
||||
* - Find a way to force Litho to rebuild it's component tree,
|
||||
* and use that hook to force the shorts dislikes to update after the fetch is completed.
|
||||
* - Hook into the dislikes button image view, and replace the dislikes thumb down image with a
|
||||
* generated image of the number of dislikes, then update the image asynchronously. This Could
|
||||
* also be used for the regular video player to give a better UI layout and completely remove
|
||||
* the need for the Rolling Number patches.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ReturnYouTubeDislikePatch {
|
||||
|
||||
public static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER =
|
||||
SpoofAppVersionPatch.isSpoofingToEqualOrLessThan("18.33.40");
|
||||
|
||||
/**
|
||||
* RYD data for the current video on screen.
|
||||
*/
|
||||
@ -318,8 +325,12 @@ public class ReturnYouTubeDislikePatch {
|
||||
try {
|
||||
if (SettingsEnum.RYD_ENABLED.getBoolean() && !SettingsEnum.RYD_COMPACT_LAYOUT.getBoolean()) {
|
||||
if (ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(text)) {
|
||||
// +1 pixel is needed for some foreign languages that measure
|
||||
// the text different from what is used for layout (Greek in particular).
|
||||
// Probably a bug in Android, but who knows.
|
||||
// Single line mode is also used as an additional fix for this issue.
|
||||
return measuredTextWidth + ReturnYouTubeDislike.leftSeparatorBounds.right
|
||||
+ ReturnYouTubeDislike.leftSeparatorShapePaddingPixels;
|
||||
+ ReturnYouTubeDislike.leftSeparatorShapePaddingPixels + 1;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
@ -342,6 +353,10 @@ public class ReturnYouTubeDislikePatch {
|
||||
} else {
|
||||
view.setCompoundDrawables(separator, null, null, null);
|
||||
}
|
||||
// Single line mode does not clip words if the span is larger than the view bounds.
|
||||
// The styled span applied to the view should always have the same bounds,
|
||||
// but use this feature just in case the measurements are somehow off by a few pixels.
|
||||
view.setSingleLine(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -354,6 +369,7 @@ public class ReturnYouTubeDislikePatch {
|
||||
LogHelper.printDebug(() -> "Removing rolling number TextView changes");
|
||||
view.setCompoundDrawablePadding(0);
|
||||
view.setCompoundDrawables(null, null, null, null);
|
||||
view.setSingleLine(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -540,26 +556,47 @@ public class ReturnYouTubeDislikePatch {
|
||||
// Video Id and voting hooks (all players).
|
||||
//
|
||||
|
||||
private static volatile boolean lastPlayerResponseWasShort;
|
||||
|
||||
/**
|
||||
* Injection point. Uses 'playback response' video id hook to preload RYD.
|
||||
*/
|
||||
public static void preloadVideoId(@NonNull String videoId, boolean videoIsOpeningOrPlaying) {
|
||||
public static void preloadVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||
try {
|
||||
// Shorts shelf in home and subscription feed causes player response hook to be called,
|
||||
// and the 'is opening/playing' parameter will be false.
|
||||
// This hook will be called again when the Short is actually opened.
|
||||
if (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized()) {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
if (videoId.equals(lastPrefetchedVideoId)) {
|
||||
return;
|
||||
}
|
||||
lastPrefetchedVideoId = videoId;
|
||||
|
||||
final boolean videoIdIsShort = VideoInformation.lastVideoIdIsShort();
|
||||
// Shorts shelf in home and subscription feed causes player response hook to be called,
|
||||
// and the 'is opening/playing' parameter will be false.
|
||||
// This hook will be called again when the Short is actually opened.
|
||||
if (videoIdIsShort && (!isShortAndOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean())) {
|
||||
return;
|
||||
}
|
||||
final boolean waitForFetchToComplete = !IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER
|
||||
&& videoIdIsShort && !lastPlayerResponseWasShort;
|
||||
|
||||
LogHelper.printDebug(() -> "Prefetching RYD for video: " + videoId);
|
||||
ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
ReturnYouTubeDislike fetch = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
if (waitForFetchToComplete && !fetch.fetchCompleted()) {
|
||||
// This call is off the main thread, so wait until the RYD fetch completely finishes,
|
||||
// otherwise if this returns before the fetch completes then the UI can
|
||||
// become frozen when the main thread tries to modify the litho Shorts dislikes and
|
||||
// it must wait for the fetch.
|
||||
// Only need to do this for the first Short opened, as the next Short to swipe to
|
||||
// are preloaded in the background.
|
||||
//
|
||||
// If an asynchronous litho Shorts solution is found, then this blocking call should be removed.
|
||||
LogHelper.printDebug(() -> "Waiting for prefetch to complete: " + videoId);
|
||||
fetch.getFetchData(20000); // Any arbitrarily large max wait time.
|
||||
}
|
||||
// Set the fields after the fetch completes, so any concurrent calls will also wait.
|
||||
lastPlayerResponseWasShort = videoIdIsShort;
|
||||
lastPrefetchedVideoId = videoId;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "preloadVideoId failure", ex);
|
||||
}
|
||||
|
@ -17,6 +17,10 @@ import java.util.Objects;
|
||||
public final class VideoInformation {
|
||||
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
|
||||
private static final String SEEK_METHOD_NAME = "seekTo";
|
||||
/**
|
||||
* Prefix present in all Short player parameters signature.
|
||||
*/
|
||||
private static final String SHORTS_PLAYER_PARAMETERS = "8AEB";
|
||||
|
||||
private static WeakReference<Object> playerControllerRef;
|
||||
private static Method seekMethod;
|
||||
@ -28,6 +32,7 @@ public final class VideoInformation {
|
||||
|
||||
@NonNull
|
||||
private static volatile String playerResponseVideoId = "";
|
||||
private static volatile boolean videoIdIsShort;
|
||||
|
||||
/**
|
||||
* The current playback speed
|
||||
@ -65,12 +70,33 @@ public final class VideoInformation {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the player parameters are for a Short.
|
||||
*/
|
||||
public static boolean playerParametersAreShort(@NonNull String parameters) {
|
||||
return parameters.startsWith(SHORTS_PLAYER_PARAMETERS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String newPlayerResponseSignature(@NonNull String signature, boolean isShortAndOpeningOrPlaying) {
|
||||
final boolean isShort = playerParametersAreShort(signature);
|
||||
if (!isShort || isShortAndOpeningOrPlaying) {
|
||||
if (videoIdIsShort != isShort) {
|
||||
videoIdIsShort = isShort;
|
||||
LogHelper.printDebug(() -> "videoIdIsShort: " + isShort);
|
||||
}
|
||||
}
|
||||
return signature; // Return the original value since we are observing and not modifying.
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
*
|
||||
* @param videoId The id of the last video loaded.
|
||||
*/
|
||||
public static void setPlayerResponseVideoId(@NonNull String videoId, boolean videoIsOpeningOrPlaying) {
|
||||
public static void setPlayerResponseVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||
if (!playerResponseVideoId.equals(videoId)) {
|
||||
LogHelper.printDebug(() -> "New player response video id: " + videoId);
|
||||
playerResponseVideoId = videoId;
|
||||
@ -135,10 +161,8 @@ public final class VideoInformation {
|
||||
public static boolean seekTo(final long millisecond) {
|
||||
final long videoLength = getVideoLength();
|
||||
|
||||
// Don't seek more than the video length to prevent issues such as
|
||||
// Play pause button or autoplay not working.
|
||||
// TODO: These are arbitrarily chosen values and should be subject to be adjusted.
|
||||
final long seekToMilliseconds = millisecond <= videoLength - 500 ? millisecond : millisecond - 100;
|
||||
// Prevent issues such as play/ pause button or autoplay not working.
|
||||
final long seekToMilliseconds = Math.min(millisecond, VideoInformation.getVideoLength() - 250);
|
||||
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
try {
|
||||
@ -157,9 +181,9 @@ public final class VideoInformation {
|
||||
}
|
||||
|
||||
/**
|
||||
* Id of the current video playing. Includes Shorts.
|
||||
* Id of the last video opened. Includes Shorts.
|
||||
*
|
||||
* @return The id of the video. Empty string if not set yet.
|
||||
* @return The id of the video, or an empty string if no videos have been opened yet.
|
||||
*/
|
||||
@NonNull
|
||||
public static String getVideoId() {
|
||||
@ -168,20 +192,30 @@ public final class VideoInformation {
|
||||
|
||||
/**
|
||||
* Differs from {@link #videoId} as this is the video id for the
|
||||
* last player response received, which may not be the current video playing.
|
||||
* last player response received, which may not be the last video opened.
|
||||
* <p>
|
||||
* If Shorts are loading the background, this commonly will be
|
||||
* different from the Short that is currently on screen.
|
||||
* <p>
|
||||
* For most use cases, you should instead use {@link #getVideoId()}.
|
||||
*
|
||||
* @return The id of the last video loaded. Empty string if not set yet.
|
||||
* @return The id of the last video loaded, or an empty string if no videos have been loaded yet.
|
||||
*/
|
||||
@NonNull
|
||||
public static String getPlayerResponseVideoId() {
|
||||
return playerResponseVideoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the last player response video id _that was opened_ was a Short.
|
||||
* <p>
|
||||
* Note: This value returned may not match the status of {@link #getPlayerResponseVideoId()}
|
||||
* since that includes player responses for videos not opened.
|
||||
*/
|
||||
public static boolean lastVideoIdIsShort() {
|
||||
return videoIdIsShort;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current playback speed.
|
||||
*/
|
||||
|
@ -32,6 +32,9 @@ public final class AnnouncementsPatch {
|
||||
public static void showAnnouncement(final Activity context) {
|
||||
if (!SettingsEnum.ANNOUNCEMENTS.getBoolean()) return;
|
||||
|
||||
// Check if there is internet connection
|
||||
if (!ReVancedUtils.isNetworkConnected()) return;
|
||||
|
||||
ReVancedUtils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
HttpURLConnection connection = AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT, CONSUMER);
|
||||
@ -43,7 +46,7 @@ public final class AnnouncementsPatch {
|
||||
if (connection.getResponseCode() != 200) {
|
||||
if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return;
|
||||
|
||||
SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue("");
|
||||
SettingsEnum.ANNOUNCEMENT_LAST_HASH.resetToDefault();
|
||||
ReVancedUtils.showToastLong("Failed to get announcement");
|
||||
|
||||
return;
|
||||
@ -118,7 +121,7 @@ public final class AnnouncementsPatch {
|
||||
*/
|
||||
private static boolean emptyLastAnnouncementHash() {
|
||||
if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return true;
|
||||
SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue("");
|
||||
SettingsEnum.ANNOUNCEMENT_LAST_HASH.resetToDefault();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ final class CustomFilterGroup extends StringFilterGroup {
|
||||
for (String pattern : patterns) {
|
||||
if (!StringTrieSearch.isValidPattern(pattern)) {
|
||||
ReVancedUtils.showToastLong("Invalid custom filter, resetting to default");
|
||||
setting.saveValue(setting.defaultValue);
|
||||
setting.resetToDefault();
|
||||
return getFilterPatterns(setting);
|
||||
}
|
||||
}
|
||||
|
@ -53,14 +53,14 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newPlayerResponseVideoId(String videoId, boolean videoIsOpeningOrPlaying) {
|
||||
public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||
try {
|
||||
if (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean()) {
|
||||
if (!isShortAndOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
synchronized (lastVideoIds) {
|
||||
if (lastVideoIds.put(videoId, Boolean.TRUE) == null) {
|
||||
LogHelper.printDebug(() -> "New video id: " + videoId);
|
||||
LogHelper.printDebug(() -> "New Short video id: " + videoId);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
|
@ -43,7 +43,7 @@ public class CustomPlaybackSpeedPatch {
|
||||
|
||||
private static void resetCustomSpeeds(@NonNull String toastMessage) {
|
||||
ReVancedUtils.showToastLong(toastMessage);
|
||||
SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.saveValue(SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.defaultValue);
|
||||
SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
|
||||
}
|
||||
|
||||
private static void loadCustomSpeeds() {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package app.revanced.integrations.patches.spoof;
|
||||
|
||||
import static app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.containsAny;
|
||||
|
||||
import android.view.View;
|
||||
@ -8,11 +9,16 @@ import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import app.revanced.integrations.patches.VideoInformation;
|
||||
import app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
/** @noinspection unused*/
|
||||
public class SpoofSignaturePatch {
|
||||
@ -23,6 +29,11 @@ public class SpoofSignaturePatch {
|
||||
*/
|
||||
private static final String INCOGNITO_PARAMETERS = "CgIQBg==";
|
||||
|
||||
/**
|
||||
* Parameters used when playing clips.
|
||||
*/
|
||||
private static final String CLIPS_PARAMETERS = "kAIB";
|
||||
|
||||
/**
|
||||
* Parameters causing playback issues.
|
||||
*/
|
||||
@ -37,11 +48,6 @@ public class SpoofSignaturePatch {
|
||||
*/
|
||||
private static final String SCRIM_PARAMETER = "SAFgAXgB";
|
||||
|
||||
/**
|
||||
* Parameters used in YouTube Shorts.
|
||||
*/
|
||||
private static final String SHORTS_PLAYER_PARAMETERS = "8AEB";
|
||||
|
||||
/**
|
||||
* Last video id loaded. Used to prevent reloading the same spec multiple times.
|
||||
*/
|
||||
@ -49,12 +55,30 @@ public class SpoofSignaturePatch {
|
||||
private static volatile String lastPlayerResponseVideoId;
|
||||
|
||||
@Nullable
|
||||
private static volatile StoryboardRenderer videoRenderer;
|
||||
private static volatile Future<StoryboardRenderer> rendererFuture;
|
||||
|
||||
private static volatile boolean useOriginalStoryboardRenderer;
|
||||
|
||||
private static volatile boolean isPlayingShorts;
|
||||
|
||||
@Nullable
|
||||
private static StoryboardRenderer getRenderer(boolean waitForCompletion) {
|
||||
Future<StoryboardRenderer> future = rendererFuture;
|
||||
if (future != null) {
|
||||
try {
|
||||
if (waitForCompletion || future.isDone()) {
|
||||
return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout.
|
||||
} // else, return null.
|
||||
} catch (TimeoutException ex) {
|
||||
LogHelper.printDebug(() -> "Could not get renderer (get timed out)");
|
||||
} catch (ExecutionException | InterruptedException ex) {
|
||||
// Should never happen.
|
||||
LogHelper.printException(() -> "Could not get renderer", ex);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
@ -62,19 +86,25 @@ public class SpoofSignaturePatch {
|
||||
*
|
||||
* @param parameters Original protobuf parameter value.
|
||||
*/
|
||||
public static String spoofParameter(String parameters) {
|
||||
public static String spoofParameter(String parameters, boolean isShortAndOpeningOrPlaying) {
|
||||
try {
|
||||
LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters);
|
||||
|
||||
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) return parameters;
|
||||
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
// Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops)
|
||||
// For this reason, the player parameters of a clip are usually very long (150~300 characters).
|
||||
// Clips are 60 seconds or less in length, so no spoofing.
|
||||
if (useOriginalStoryboardRenderer = parameters.length() > 150) return parameters;
|
||||
//noinspection AssignmentUsedAsCondition
|
||||
if (useOriginalStoryboardRenderer = parameters.length() > 150 || containsAny(parameters, CLIPS_PARAMETERS)) {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
// Shorts do not need to be spoofed.
|
||||
if (useOriginalStoryboardRenderer = parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) {
|
||||
//noinspection AssignmentUsedAsCondition
|
||||
if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) {
|
||||
isPlayingShorts = true;
|
||||
return parameters;
|
||||
}
|
||||
@ -83,6 +113,7 @@ public class SpoofSignaturePatch {
|
||||
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
|
||||
&& containsAny(parameters, AUTOPLAY_PARAMETERS);
|
||||
if (isPlayingFeed) {
|
||||
//noinspection AssignmentUsedAsCondition
|
||||
if (useOriginalStoryboardRenderer = !SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean()) {
|
||||
// Don't spoof the feed video playback. This will cause video playback issues,
|
||||
// but only if user continues watching for more than 1 minute.
|
||||
@ -103,27 +134,32 @@ public class SpoofSignaturePatch {
|
||||
private static void fetchStoryboardRenderer() {
|
||||
if (!SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
|
||||
lastPlayerResponseVideoId = null;
|
||||
videoRenderer = null;
|
||||
rendererFuture = null;
|
||||
return;
|
||||
}
|
||||
String videoId = VideoInformation.getPlayerResponseVideoId();
|
||||
if (!videoId.equals(lastPlayerResponseVideoId)) {
|
||||
rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId));
|
||||
lastPlayerResponseVideoId = videoId;
|
||||
// This will block starting video playback until the fetch completes.
|
||||
// This is desired because if this returns without finishing the fetch,
|
||||
// then video will start playback but the image will be frozen
|
||||
// while the main thread call for the renderer waits for the fetch to complete.
|
||||
videoRenderer = StoryboardRendererRequester.getStoryboardRenderer(videoId);
|
||||
}
|
||||
// Block until the renderer fetch completes.
|
||||
// This is desired because if this returns without finishing the fetch
|
||||
// then video will start playback but the storyboard is not ready yet.
|
||||
getRenderer(true);
|
||||
}
|
||||
|
||||
private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec,
|
||||
boolean returnNullIfLiveStream) {
|
||||
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
|
||||
StoryboardRenderer renderer = videoRenderer;
|
||||
StoryboardRenderer renderer = getRenderer(false);
|
||||
if (renderer != null) {
|
||||
if (returnNullIfLiveStream && renderer.isLiveStream()) return null;
|
||||
return renderer.getSpec();
|
||||
if (returnNullIfLiveStream && renderer.isLiveStream()) {
|
||||
return null;
|
||||
}
|
||||
String spec = renderer.getSpec();
|
||||
if (spec != null) {
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,7 +190,7 @@ public class SpoofSignaturePatch {
|
||||
*/
|
||||
public static int getRecommendedLevel(int originalLevel) {
|
||||
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
|
||||
StoryboardRenderer renderer = videoRenderer;
|
||||
StoryboardRenderer renderer = getRenderer(false);
|
||||
if (renderer != null) {
|
||||
Integer recommendedLevel = renderer.getRecommendedLevel();
|
||||
if (recommendedLevel != null) return recommendedLevel;
|
||||
@ -172,7 +208,7 @@ public class SpoofSignaturePatch {
|
||||
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) {
|
||||
return false;
|
||||
}
|
||||
StoryboardRenderer renderer = videoRenderer;
|
||||
StoryboardRenderer renderer = getRenderer(false);
|
||||
if (renderer == null) {
|
||||
// Spoof storyboard renderer is turned off,
|
||||
// video is paid, or the storyboard fetch timed out.
|
||||
|
@ -48,7 +48,7 @@ public final class SeekbarColorPatch {
|
||||
Color.colorToHSV(seekbarColor, customSeekbarColorHSV);
|
||||
} catch (Exception ex) {
|
||||
ReVancedUtils.showToastShort("Invalid seekbar color value. Using default value.");
|
||||
SettingsEnum.SEEKBAR_CUSTOM_COLOR_VALUE.saveValue(SettingsEnum.SEEKBAR_CUSTOM_COLOR_VALUE.defaultValue);
|
||||
SettingsEnum.SEEKBAR_CUSTOM_COLOR_VALUE.resetToDefault();
|
||||
loadCustomSeekbarColor();
|
||||
}
|
||||
}
|
||||
|
@ -62,12 +62,12 @@ public class ReturnYouTubeDislikeApi {
|
||||
* How long to wait until API calls are resumed, if the API requested a back off.
|
||||
* No clear guideline of how long to wait until resuming.
|
||||
*/
|
||||
private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 4 * 60 * 1000; // 4 Minutes.
|
||||
private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes.
|
||||
|
||||
/**
|
||||
* How long to wait until API calls are resumed, if any connection error occurs.
|
||||
*/
|
||||
private static final int BACKOFF_CONNECTION_ERROR_MILLISECONDS = 60 * 1000; // 60 Seconds.
|
||||
private static final int BACKOFF_CONNECTION_ERROR_MILLISECONDS = 2 * 60 * 1000; // 2 Minutes.
|
||||
|
||||
/**
|
||||
* If non zero, then the system time of when API calls can resume.
|
||||
|
@ -56,9 +56,13 @@ public enum SettingsEnum {
|
||||
HIDE_WEB_SEARCH_RESULTS("revanced_hide_web_search_results", BOOLEAN, TRUE),
|
||||
|
||||
// Layout
|
||||
ALT_THUMBNAIL("revanced_alt_thumbnail", BOOLEAN, FALSE),
|
||||
ALT_THUMBNAIL_TYPE("revanced_alt_thumbnail_type", INTEGER, 2, parents(ALT_THUMBNAIL)),
|
||||
ALT_THUMBNAIL_FAST_QUALITY("revanced_alt_thumbnail_fast_quality", BOOLEAN, FALSE, parents(ALT_THUMBNAIL)),
|
||||
ALT_THUMBNAIL_STILLS("revanced_alt_thumbnail_stills", BOOLEAN, FALSE),
|
||||
ALT_THUMBNAIL_STILLS_TIME("revanced_alt_thumbnail_stills_time", INTEGER, 2, parents(ALT_THUMBNAIL_STILLS)),
|
||||
ALT_THUMBNAIL_STILLS_FAST("revanced_alt_thumbnail_stills_fast", BOOLEAN, FALSE, parents(ALT_THUMBNAIL_STILLS)),
|
||||
ALT_THUMBNAIL_DEARROW("revanced_alt_thumbnail_dearrow", BOOLEAN, false),
|
||||
ALT_THUMBNAIL_DEARROW_API_URL("revanced_alt_thumbnail_dearrow_api_url", STRING,
|
||||
"https://dearrow-thumb.ajay.app/api/v1/getThumbnail", true, parents(ALT_THUMBNAIL_DEARROW)),
|
||||
ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST("revanced_alt_thumbnail_dearrow_connection_toast", BOOLEAN, TRUE, parents(ALT_THUMBNAIL_DEARROW)),
|
||||
CUSTOM_FILTER("revanced_custom_filter", BOOLEAN, FALSE),
|
||||
CUSTOM_FILTER_STRINGS("revanced_custom_filter_strings", STRING, "", true, parents(CUSTOM_FILTER)),
|
||||
DISABLE_FULLSCREEN_AMBIENT_MODE("revanced_disable_fullscreen_ambient_mode", BOOLEAN, TRUE, true),
|
||||
@ -125,6 +129,8 @@ public enum SettingsEnum {
|
||||
TABLET_LAYOUT("revanced_tablet_layout", BOOLEAN, FALSE, true, "revanced_tablet_layout_user_dialog_message"),
|
||||
USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true),
|
||||
WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true),
|
||||
START_PAGE("revanced_start_page", STRING, ""),
|
||||
|
||||
// Description
|
||||
HIDE_CHAPTERS("revanced_hide_chapters", BOOLEAN, TRUE),
|
||||
HIDE_INFO_CARDS_SECTION("revanced_hide_info_cards_section", BOOLEAN, TRUE),
|
||||
@ -430,7 +436,7 @@ public enum SettingsEnum {
|
||||
LogHelper.printInfo(() -> "Migrating old setting of '" + oldSetting.value
|
||||
+ "' from: " + oldSetting + " into replacement setting: " + newSetting);
|
||||
newSetting.saveValue(oldSetting.value);
|
||||
oldSetting.saveValue(oldSetting.defaultValue); // reset old value
|
||||
oldSetting.resetToDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@ -522,6 +528,13 @@ public enum SettingsEnum {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identical to calling {@link #saveValue(Object)} using {@link #defaultValue}.
|
||||
*/
|
||||
public void resetToDefault() {
|
||||
saveValue(defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if this setting can be configured and used.
|
||||
* <p>
|
||||
@ -694,7 +707,7 @@ public enum SettingsEnum {
|
||||
} else if (setting.includeWithImportExport() && !setting.isSetToDefault()) {
|
||||
LogHelper.printDebug(() -> "Resetting to default: " + setting);
|
||||
rebootSettingChanged |= setting.rebootApp;
|
||||
setting.saveValue(setting.defaultValue);
|
||||
setting.resetToDefault();
|
||||
}
|
||||
}
|
||||
numberOfSettingsImported += SponsorBlockSettings.importCategoriesFromFlatJson(json);
|
||||
|
@ -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 app.revanced.integrations.patches.ReturnYouTubeDislikePatch;
|
||||
import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch;
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
@ -21,9 +20,6 @@ import app.revanced.integrations.settings.SharedPrefCategory;
|
||||
|
||||
public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
|
||||
|
||||
private static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER =
|
||||
SpoofAppVersionPatch.isSpoofingToEqualOrLessThan("18.33.40");
|
||||
|
||||
/**
|
||||
* If dislikes are shown on Shorts.
|
||||
*/
|
||||
@ -79,7 +75,7 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
|
||||
shortsPreference.setChecked(SettingsEnum.RYD_SHORTS.getBoolean());
|
||||
shortsPreference.setTitle(str("revanced_ryd_shorts_title"));
|
||||
String shortsSummary = str("revanced_ryd_shorts_summary_on",
|
||||
IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER
|
||||
ReturnYouTubeDislikePatch.IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER
|
||||
? ""
|
||||
: "\n\n" + str("revanced_ryd_shorts_summary_disclaimer"));
|
||||
shortsPreference.setSummaryOn(shortsSummary);
|
||||
|
@ -351,7 +351,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
|
||||
|
||||
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> {
|
||||
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) {
|
||||
SettingsEnum.SB_API_URL.saveValue(SettingsEnum.SB_API_URL.defaultValue);
|
||||
SettingsEnum.SB_API_URL.resetToDefault();
|
||||
ReVancedUtils.showToastLong(str("sb_api_url_reset"));
|
||||
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) {
|
||||
String serverAddress = editText.getText().toString();
|
||||
@ -583,8 +583,8 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
|
||||
new AlertDialog.Builder(preference1.getContext())
|
||||
.setTitle(str("sb_stats_self_saved_reset_title"))
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.saveValue(SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.defaultValue);
|
||||
SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.saveValue(SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.defaultValue);
|
||||
SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault();
|
||||
SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault();
|
||||
updateStatsSelfSaved.run();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null).show();
|
||||
|
@ -1,10 +1,11 @@
|
||||
package app.revanced.integrations.shared
|
||||
|
||||
import app.revanced.integrations.patches.VideoInformation
|
||||
import app.revanced.integrations.utils.Event
|
||||
import app.revanced.integrations.utils.LogHelper
|
||||
|
||||
/**
|
||||
* WatchWhile player type
|
||||
* WatchWhile player type.
|
||||
*/
|
||||
enum class PlayerType {
|
||||
/**
|
||||
@ -83,6 +84,8 @@ enum class PlayerType {
|
||||
* Does not include the first moment after a short is opened when a regular video is minimized on screen,
|
||||
* or while watching a short with a regular video present on a spoofed 16.x version of YouTube.
|
||||
* To include those situations instead use [isNoneHiddenOrMinimized].
|
||||
*
|
||||
* @see VideoInformation
|
||||
*/
|
||||
fun isNoneOrHidden(): Boolean {
|
||||
return this == NONE || this == HIDDEN
|
||||
@ -99,6 +102,7 @@ enum class PlayerType {
|
||||
* though a Short is being opened or is on screen (see [isNoneHiddenOrMinimized]).
|
||||
*
|
||||
* @return If nothing, a Short, or a regular video is sliding off screen to a dismissed or hidden state.
|
||||
* @see VideoInformation
|
||||
*/
|
||||
fun isNoneHiddenOrSlidingMinimized(): Boolean {
|
||||
return isNoneOrHidden() || this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED
|
||||
@ -117,6 +121,7 @@ enum class PlayerType {
|
||||
*
|
||||
* @return If nothing, a Short, a regular video is sliding off screen to a dismissed or hidden state,
|
||||
* a regular video is minimized (and a new video is not being opened).
|
||||
* @see VideoInformation
|
||||
*/
|
||||
fun isNoneHiddenOrMinimized(): Boolean {
|
||||
return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED
|
||||
|
@ -7,13 +7,15 @@ import android.preference.PreferenceFragment;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import app.revanced.tiktok.utils.LogHelper;
|
||||
import app.revanced.tiktok.utils.ReVancedUtils;
|
||||
|
||||
import com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization.AdPersonalizationActivity;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
import app.revanced.tiktok.utils.LogHelper;
|
||||
import app.revanced.tiktok.utils.ReVancedUtils;
|
||||
|
||||
|
||||
public class SettingsMenu {
|
||||
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
|
||||
@ -22,10 +24,9 @@ public class SettingsMenu {
|
||||
Class<?> entryInfoClazz = Class.forName(entryInfoClazzName);
|
||||
Constructor<?> entryConstructor = entryClazz.getConstructor(entryInfoClazz);
|
||||
Constructor<?> entryInfoConstructor = entryInfoClazz.getDeclaredConstructors()[0];
|
||||
Object buttonInfo = entryInfoConstructor.newInstance("Revanced settings", null, (View.OnClickListener) view -> startSettingsActivity());
|
||||
Object buttonInfo = entryInfoConstructor.newInstance("ReVanced settings", null, (View.OnClickListener) view -> startSettingsActivity(), "revanced");
|
||||
return entryConstructor.newInstance(buttonInfo);
|
||||
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException |
|
||||
InstantiationException e) {
|
||||
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
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.caching = true
|
||||
android.useAndroidX = true
|
||||
version = 0.125.0
|
||||
version = 1.0.0-dev.12
|
||||
|
@ -2,7 +2,7 @@ rootProject.name = "revanced-integrations"
|
||||
|
||||
buildCache {
|
||||
local {
|
||||
isEnabled = !System.getenv().containsKey("CI")
|
||||
isEnabled = "CI" !in System.getenv()
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user