refactor: Bump ReVanced Patcher & merge integrations by using ReVanced Patches Gradle plugin

BREAKING CHANGE: ReVanced Patcher >= 21 required
This commit is contained in:
inotia00
2024-12-07 22:13:39 +09:00
parent f074c3ecc5
commit b31865afbe
2706 changed files with 64970 additions and 30705 deletions

View File

@ -0,0 +1,46 @@
package app.revanced.extension.youtube.patches.ads;
import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition;
import android.view.View;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class AdsPatch {
private static final boolean hideGeneralAdsEnabled = Settings.HIDE_GENERAL_ADS.get();
private static final boolean hideGetPremiumAdsEnabled = Settings.HIDE_GET_PREMIUM.get();
private static final boolean hideVideoAdsEnabled = Settings.HIDE_VIDEO_ADS.get();
/**
* Injection point.
* Hide the view, which shows ads in the homepage.
*
* @param view The view, which shows ads.
*/
public static void hideAdAttributionView(View view) {
hideViewBy0dpUnderCondition(hideGeneralAdsEnabled, view);
}
public static boolean hideGetPremium() {
return hideGetPremiumAdsEnabled;
}
/**
* Injection point.
*/
public static boolean hideVideoAds() {
return !hideVideoAdsEnabled;
}
/**
* Injection point.
* <p>
* Only used by old clients.
* It is presumed to have been deprecated, and if it is confirmed that it is no longer used, remove it.
*/
public static boolean hideVideoAds(boolean original) {
return !hideVideoAdsEnabled && original;
}
}

View File

@ -0,0 +1,721 @@
package app.revanced.extension.youtube.patches.alternativethumbnails;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_HOME;
import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_LIBRARY;
import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_PLAYER;
import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_SEARCH;
import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_SUBSCRIPTIONS;
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
import android.net.Uri;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.regex.Pattern;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.RootView;
/**
* @noinspection ALL
* Alternative YouTube thumbnails.
* <p>
* Can show YouTube provided screen captures of beginning/middle/end of the video.
* (ie: sd1.jpg, sd2.jpg, sd3.jpg).
* <p>
* Or can show crowd-sourced 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 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 much fail to load.
*/
public final class AlternativeThumbnailsPatch {
// These must be class declarations if declared here,
// otherwise the app will not load due to cyclic initialization errors.
public static final class DeArrowAvailability implements Setting.Availability {
public static boolean usingDeArrowAnywhere() {
return ALT_THUMBNAIL_HOME.get().useDeArrow
|| ALT_THUMBNAIL_SUBSCRIPTIONS.get().useDeArrow
|| ALT_THUMBNAIL_LIBRARY.get().useDeArrow
|| ALT_THUMBNAIL_PLAYER.get().useDeArrow
|| ALT_THUMBNAIL_SEARCH.get().useDeArrow;
}
@Override
public boolean isAvailable() {
return usingDeArrowAnywhere();
}
}
public static final class StillImagesAvailability implements Setting.Availability {
public static boolean usingStillImagesAnywhere() {
return ALT_THUMBNAIL_HOME.get().useStillImages
|| ALT_THUMBNAIL_SUBSCRIPTIONS.get().useStillImages
|| ALT_THUMBNAIL_LIBRARY.get().useStillImages
|| ALT_THUMBNAIL_PLAYER.get().useStillImages
|| ALT_THUMBNAIL_SEARCH.get().useStillImages;
}
@Override
public boolean isAvailable() {
return usingStillImagesAnywhere();
}
}
public enum ThumbnailOption {
ORIGINAL(false, false),
DEARROW(true, false),
DEARROW_STILL_IMAGES(true, true),
STILL_IMAGES(false, true);
final boolean useDeArrow;
final boolean useStillImages;
ThumbnailOption(boolean useDeArrow, boolean useStillImages) {
this.useDeArrow = useDeArrow;
this.useStillImages = useStillImages;
}
}
public enum ThumbnailStillTime {
BEGINNING(1),
MIDDLE(2),
END(3);
/**
* The url alt image number. Such as the 2 in 'hq720_2.jpg'
*/
final int altImageNumber;
ThumbnailStillTime(int altImageNumber) {
this.altImageNumber = altImageNumber;
}
}
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.
/**
* Regex to match youtube static thumbnails domain.
* Used to find and replace blocked domain with a working ones
*/
private static final String YOUTUBE_STATIC_THUMBNAILS_DOMAIN_REGEX = "(yt[3-4]|lh[3-6]|play-lh)\\.(ggpht|googleusercontent)\\.com";
private static final Pattern YOUTUBE_STATIC_THUMBNAILS_DOMAIN_PATTERN = Pattern.compile(YOUTUBE_STATIC_THUMBNAILS_DOMAIN_REGEX);
/**
* 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 + "/";
Logger.printDebug(() -> "Using DeArrow API address: " + deArrowApiUrlPrefix);
}
/**
* Fix any bad imported data.
*/
private static Uri validateSettings() {
Uri apiUri = Uri.parse(Settings.ALT_THUMBNAIL_DEARROW_API_URL.get());
// 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) {
Utils.showToastLong(str("revanced_alt_thumbnail_dearrow_api_url_invalid_toast"));
Utils.showToastShort(str("revanced_extended_reset_to_default_toast"));
Settings.ALT_THUMBNAIL_DEARROW_API_URL.resetToDefault();
return validateSettings();
}
return apiUri;
}
private static ThumbnailOption optionSettingForCurrentNavigation() {
// Must check player type first, as search bar can be active behind the player.
if (RootView.isPlayerActive()) {
return ALT_THUMBNAIL_PLAYER.get();
}
// Must check second, as search can be from any tab.
if (RootView.isSearchBarActive()) {
return ALT_THUMBNAIL_SEARCH.get();
}
// Avoid checking which navigation button is selected, if all other settings are the same.
ThumbnailOption homeOption = ALT_THUMBNAIL_HOME.get();
ThumbnailOption subscriptionsOption = ALT_THUMBNAIL_SUBSCRIPTIONS.get();
ThumbnailOption libraryOption = ALT_THUMBNAIL_LIBRARY.get();
if ((homeOption == subscriptionsOption) && (homeOption == libraryOption)) {
return homeOption; // All are the same option.
}
NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton();
if (selectedNavButton == null) {
// Unknown tab, treat as the home tab;
return homeOption;
}
if (selectedNavButton == NavigationButton.HOME) {
return homeOption;
}
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS || selectedNavButton == NavigationButton.NOTIFICATIONS) {
return subscriptionsOption;
}
// A library tab variant is active.
return libraryOption;
}
/**
* 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()) {
Logger.printDebug(() -> "Resuming DeArrow API calls");
timeToResumeDeArrowAPICalls = 0;
return true;
}
return false;
}
private static void handleDeArrowError(@NonNull String url, int statusCode) {
Logger.printDebug(() -> "Encountered DeArrow error. Url: " + url);
final long now = System.currentTimeMillis();
if (timeToResumeDeArrowAPICalls < now) {
timeToResumeDeArrowAPICalls = now + DEARROW_FAILURE_API_BACKOFF_MILLISECONDS;
if (Settings.ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST.get()) {
String toastMessage = (statusCode != 0)
? str("revanced_alt_thumbnail_dearrow_error", statusCode)
: str("revanced_alt_thumbnail_dearrow_error_generic");
Utils.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 {
ThumbnailOption option = optionSettingForCurrentNavigation();
if (option == ThumbnailOption.ORIGINAL) {
return originalUrl;
}
final var decodedUrl = DecodedThumbnailUrl.decodeImageUrl(originalUrl);
if (decodedUrl == null) {
return originalUrl; // Not a thumbnail.
}
Logger.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 (option.useDeArrow && canUseDeArrowAPI()) {
includeTracking = false; // Do not include view tracking parameters with API call.
final String fallbackUrl = option.useStillImages
? buildYoutubeVideoStillURL(decodedUrl, qualityToUse)
: decodedUrl.sanitizedUrl;
sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl);
} else if (option.useStillImages) {
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.
Logger.printDebug(() -> "Replacement url: " + sanitizedReplacementUrl);
return includeTracking
? sanitizedReplacementUrl + decodedUrl.viewTrackingParameters
: sanitizedReplacementUrl;
} catch (Exception ex) {
Logger.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) {
return;
}
String url = responseInfo.getUrl();
if (urlIsDeArrow(url)) {
Logger.printDebug(() -> "handleCronetSuccess, statusCode: " + statusCode);
if (statusCode == 304) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304
return; // Normal response.
}
handleDeArrowError(url, statusCode);
return;
}
if (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.
}
Logger.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.
Logger.printDebug(() -> "Failed to recognize image quality of url: " + decodedUrl.sanitizedUrl);
return;
}
VerifiedQualities.setAltThumbnailDoesNotExist(decodedUrl.videoId, quality);
}
} catch (Exception ex) {
Logger.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>
* Cronet uses a very timeout (several minutes), so if the API never responds this hook can take a while to be called.
* 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 {
String url = ((CronetUrlRequest) request).getHookedUrl();
if (urlIsDeArrow(url)) {
Logger.printDebug(() -> "handleCronetFailure, exception: " + exception);
final int statusCode = (responseInfo != null)
? responseInfo.getHttpStatusCode()
: 0;
handleDeArrowError(url, statusCode);
}
} catch (Exception ex) {
Logger.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
MQDEFAULT("mqdefault", "mq"),
HQDEFAULT("hqdefault", "hq"),
SDDEFAULT("sddefault", "sd"),
HQ720("hq720", "hq720_"),
MAXRESDEFAULT("maxresdefault", "maxres");
/**
* Lookup map of original name to enum.
*/
private static final Map<String, ThumbnailQuality> originalNameToEnum = new HashMap<>();
/**
* Lookup map of alt name to enum. ie: "hq720_1" to {@link #HQ720}.
*/
private static final Map<String, ThumbnailQuality> altNameToEnum = new HashMap<>();
static {
for (ThumbnailQuality quality : values()) {
originalNameToEnum.put(quality.originalName, quality);
for (ThumbnailStillTime time : ThumbnailStillTime.values()) {
// 'custom' thumbnails set by the content creator.
// These show up in place of regular thumbnails
// and seem to be limited to the same [1, 3] range as the still captures.
originalNameToEnum.put(quality.originalName + "_custom_" + time.altImageNumber, quality);
altNameToEnum.put(quality.altImageName + time.altImageNumber, quality);
}
}
}
/**
* Convert an alt image name to enum.
* ie: "hq720_2" returns {@link #HQ720}.
*/
@Nullable
static ThumbnailQuality altImageNameToQuality(@NonNull String altImageName) {
return altNameToEnum.get(altImageName);
}
/**
* Original quality to effective alt quality to use.
* ie: If fast alt image is enabled, then "hq720" returns {@link #SDDEFAULT}.
*/
@Nullable
static ThumbnailQuality getQualityToUse(@NonNull String originalSize) {
ThumbnailQuality quality = originalNameToEnum.get(originalSize);
if (quality == null) {
return null; // Not a thumbnail for a regular video.
}
final boolean useFastQuality = Settings.ALT_THUMBNAIL_STILLS_FAST.get();
switch (quality) {
case SDDEFAULT:
// SD alt images have somewhat worse quality with washed out color and poor contrast.
// But the 720 images look much better and don't suffer from these issues.
// For unknown reasons, the 720 thumbnails are used only for the home feed,
// while SD is used for the search and subscription feed
// (even though search and subscriptions use the exact same layout as the home feed).
// Of note, this image quality issue only appears with the alt thumbnail images,
// and the regular thumbnails have identical color/contrast quality for all sizes.
// Fix this by falling thru and upgrading SD to 720.
case HQ720:
if (useFastQuality) {
return SDDEFAULT; // SD is max resolution for fast alt images.
}
return HQ720;
case MAXRESDEFAULT:
if (useFastQuality) {
return SDDEFAULT;
}
return MAXRESDEFAULT;
default:
return quality;
}
}
final String originalName;
final String altImageName;
ThumbnailQuality(String originalName, String altImageName) {
this.originalName = originalName;
this.altImageName = altImageName;
}
String getAltImageNameToUse() {
return altImageName + Settings.ALT_THUMBNAIL_STILLS_TIME.get().altImageNumber;
}
}
/**
* Uses HTTP HEAD requests to verify and keep track of which thumbnail sizes
* are available and not available.
*/
private static class VerifiedQualities {
/**
* After a quality is verified as not available, how long until the quality is re-verified again.
* Used only if fast mode is not enabled. Intended for live streams and unreleased videos
* that are now finished and available (and thus, the alt thumbnails are also now available).
*/
private static final long NOT_AVAILABLE_TIMEOUT_MILLISECONDS = 10 * 60 * 1000; // 10 minutes.
/**
* Cache used to verify if an alternative thumbnails exists for a given video id.
*/
@GuardedBy("itself")
private static final Map<String, VerifiedQualities> altVideoIdLookup = new LinkedHashMap<>(100) {
private static final int CACHE_LIMIT = 1000;
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
}
};
private static VerifiedQualities getVerifiedQualities(@NonNull String videoId, boolean returnNullIfDoesNotExist) {
synchronized (altVideoIdLookup) {
VerifiedQualities verified = altVideoIdLookup.get(videoId);
if (verified == null) {
if (returnNullIfDoesNotExist) {
return null;
}
verified = new VerifiedQualities();
altVideoIdLookup.put(videoId, verified);
}
return verified;
}
}
static boolean verifyAltThumbnailExist(@NonNull String videoId, @NonNull ThumbnailQuality quality,
@NonNull String imageUrl) {
VerifiedQualities verified = getVerifiedQualities(videoId, Settings.ALT_THUMBNAIL_STILLS_FAST.get());
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);
}
/**
* Highest quality verified as existing.
*/
@Nullable
private ThumbnailQuality highestQualityVerified;
/**
* Lowest quality verified as not existing.
*/
@Nullable
private ThumbnailQuality lowestQualityNotAvailable;
/**
* System time, of when to invalidate {@link #lowestQualityNotAvailable}.
* Used only if fast mode is not enabled.
*/
private long timeToReVerifyLowestQuality;
private synchronized void setQualityVerified(String videoId, ThumbnailQuality quality, boolean isVerified) {
if (isVerified) {
if (highestQualityVerified == null || highestQualityVerified.ordinal() < quality.ordinal()) {
highestQualityVerified = quality;
}
} else {
if (lowestQualityNotAvailable == null || lowestQualityNotAvailable.ordinal() > quality.ordinal()) {
lowestQualityNotAvailable = quality;
timeToReVerifyLowestQuality = System.currentTimeMillis() + NOT_AVAILABLE_TIMEOUT_MILLISECONDS;
}
Logger.printDebug(() -> quality + " not available for video: " + videoId);
}
}
/**
* Verify if a video alt thumbnail exists. Does so by making a minimal HEAD http request.
*/
synchronized boolean verifyYouTubeThumbnailExists(@NonNull String videoId, @NonNull ThumbnailQuality quality,
@NonNull String imageUrl) {
if (highestQualityVerified != null && highestQualityVerified.ordinal() >= quality.ordinal()) {
return true; // Previously verified as existing.
}
final boolean fastQuality = Settings.ALT_THUMBNAIL_STILLS_FAST.get();
if (lowestQualityNotAvailable != null && lowestQualityNotAvailable.ordinal() <= quality.ordinal()) {
if (fastQuality || System.currentTimeMillis() < timeToReVerifyLowestQuality) {
return false; // Previously verified as not existing.
}
// Enough time has passed, and should re-verify again.
Logger.printDebug(() -> "Resetting lowest verified quality for: " + videoId);
lowestQualityNotAvailable = null;
}
if (fastQuality) {
return true; // Unknown if it exists or not. Use the URL anyways and update afterwards if loading fails.
}
boolean imageFileFound;
try {
// This hooked code is running on a low priority thread, and it's slightly faster
// to run the url connection thru the integrations thread pool which runs at the highest priority.
final long start = System.currentTimeMillis();
imageFileFound = Utils.submitOnBackgroundThread(() -> {
final int connectionTimeoutMillis = 10000; // 10 seconds.
HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection();
connection.setConnectTimeout(connectionTimeoutMillis);
connection.setReadTimeout(connectionTimeoutMillis);
connection.setRequestMethod("HEAD");
// Even with a HEAD request, the response is the same size as a full GET request.
// Using an empty range fixes this.
connection.setRequestProperty("Range", "bytes=0-0");
final int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_PARTIAL) {
String contentType = connection.getContentType();
return (contentType != null && contentType.startsWith("image"));
}
if (responseCode != HttpURLConnection.HTTP_NOT_FOUND) {
Logger.printDebug(() -> "Unexpected response code: " + responseCode + " for url: " + imageUrl);
}
return false;
}).get();
Logger.printDebug(() -> "Verification took: " + (System.currentTimeMillis() - start) + "ms for image: " + imageUrl);
} catch (ExecutionException | InterruptedException ex) {
Logger.printInfo(() -> "Could not verify alt url: " + imageUrl, ex);
imageFileFound = false;
}
setQualityVerified(videoId, quality, imageFileFound);
return imageFileFound;
}
}
/**
* YouTube video thumbnail url, decoded into it's relevant parts.
*/
private static class DecodedThumbnailUrl {
/**
* YouTube thumbnail URL prefix. Can be '/vi/' or '/vi_webp/'
*/
private static final String YOUTUBE_THUMBNAIL_PREFIX = "https://i.ytimg.com/vi";
@Nullable
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.
*/
final String urlPrefix;
final String videoId;
/**
* Quality, such as hq720 or sddefault.
*/
final String imageQuality;
/**
* JPG or WEBP
*/
final String imageExtension;
/**
* User view tracking parameters, only present on some images.
*/
final String viewTrackingParameters;
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);
viewTrackingParameters = (imageExtensionEndIndex == fullUrl.length())
? "" : fullUrl.substring(imageExtensionEndIndex);
}
/**
* @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(originalFullUrl.length() + 2);
builder.append(urlPrefix);
builder.append(videoId).append('/');
builder.append(qualityToUse.getAltImageNameToUse());
builder.append('.').append(imageExtension);
if (includeViewTracking) {
builder.append(viewTrackingParameters);
}
return builder.toString();
}
}
}

View File

@ -0,0 +1,118 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class ActionButtonsFilter extends Filter {
private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.eml";
private static final String ANIMATED_VECTOR_TYPE_PATH = "AnimatedVectorType";
private final StringFilterGroup actionBarRule;
private final StringFilterGroup bufferFilterPathRule;
private final StringFilterGroup likeSubscribeGlow;
private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList();
public ActionButtonsFilter() {
actionBarRule = new StringFilterGroup(
null,
VIDEO_ACTION_BAR_PATH_PREFIX
);
addIdentifierCallbacks(actionBarRule);
bufferFilterPathRule = new StringFilterGroup(
null,
"|ContainerType|button.eml|"
);
likeSubscribeGlow = new StringFilterGroup(
Settings.DISABLE_LIKE_DISLIKE_GLOW,
"animated_button_border.eml"
);
addPathCallbacks(
new StringFilterGroup(
Settings.HIDE_LIKE_DISLIKE_BUTTON,
"|segmented_like_dislike_button"
),
new StringFilterGroup(
Settings.HIDE_DOWNLOAD_BUTTON,
"|download_button.eml|"
),
new StringFilterGroup(
Settings.HIDE_CLIP_BUTTON,
"|clip_button.eml|"
),
new StringFilterGroup(
Settings.HIDE_PLAYLIST_BUTTON,
"|save_to_playlist_button"
),
new StringFilterGroup(
Settings.HIDE_REWARDS_BUTTON,
"account_link_button"
),
bufferFilterPathRule,
likeSubscribeGlow
);
bufferButtonsGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_REPORT_BUTTON,
"yt_outline_flag"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHARE_BUTTON,
"yt_outline_share"
),
new ByteArrayFilterGroup(
Settings.HIDE_REMIX_BUTTON,
"yt_outline_youtube_shorts_plus"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHOP_BUTTON,
"yt_outline_bag"
),
new ByteArrayFilterGroup(
Settings.HIDE_THANKS_BUTTON,
"yt_outline_dollar_sign_heart"
)
);
}
private boolean isEveryFilterGroupEnabled() {
for (StringFilterGroup group : pathCallbacks)
if (!group.isEnabled()) return false;
for (ByteArrayFilterGroup group : bufferButtonsGroupList)
if (!group.isEnabled()) return false;
return true;
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (!path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX)) {
return false;
}
if (matchedGroup == actionBarRule && !isEveryFilterGroupEnabled()) {
return false;
}
if (matchedGroup == likeSubscribeGlow) {
if (!path.contains(ANIMATED_VECTOR_TYPE_PATH)) {
return false;
}
}
if (matchedGroup == bufferFilterPathRule) {
// In case the group list has no match, return false.
if (!bufferButtonsGroupList.check(protobufBufferArray).isFiltered()) {
return false;
}
}
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}

View File

@ -0,0 +1,160 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.youtube.settings.Settings;
/**
* If A/B testing is applied, ad components can only be filtered by identifier
* <p>
* Before A/B testing:
* Identifier: video_display_button_group_layout.eml
* Path: video_display_button_group_layout.eml|ContainerType|....
* (Path always starts with an Identifier)
* <p>
* After A/B testing:
* Identifier: video_display_button_group_layout.eml
* Path: video_lockup_with_attachment.eml|ContainerType|....
* (Path does not contain an Identifier)
*/
@SuppressWarnings("unused")
public final class AdsFilter extends Filter {
private final StringFilterGroup playerShoppingShelf;
private final ByteArrayFilterGroup playerShoppingShelfBuffer;
public AdsFilter() {
// Identifiers.
final StringFilterGroup alertBannerPromo = new StringFilterGroup(
Settings.HIDE_PROMOTION_ALERT_BANNER,
"alert_banner_promo.eml"
);
// Keywords checked in 2024:
final StringFilterGroup generalAdsIdentifier = new StringFilterGroup(
Settings.HIDE_GENERAL_ADS,
// "brand_video_shelf.eml"
"brand_video",
// "carousel_footered_layout.eml"
"carousel_footered_layout",
// "composite_concurrent_carousel_layout"
"composite_concurrent_carousel_layout",
// "landscape_image_wide_button_layout.eml"
"landscape_image_wide_button_layout",
// "square_image_layout.eml"
"square_image_layout",
// "statement_banner.eml"
"statement_banner",
// "video_display_full_layout.eml"
"video_display_full_layout",
// "text_image_button_group_layout.eml"
// "video_display_button_group_layout.eml"
"_button_group_layout",
// "banner_text_icon_buttoned_layout.eml"
// "video_display_compact_buttoned_layout.eml"
// "video_display_full_buttoned_layout.eml"
"_buttoned_layout",
// "compact_landscape_image_layout.eml"
// "full_width_portrait_image_layout.eml"
// "full_width_square_image_layout.eml"
"_image_layout"
);
final StringFilterGroup merchandise = new StringFilterGroup(
Settings.HIDE_MERCHANDISE_SHELF,
"product_carousel",
"shopping_carousel"
);
final StringFilterGroup paidContent = new StringFilterGroup(
Settings.HIDE_PAID_PROMOTION_LABEL,
"paid_content_overlay"
);
final StringFilterGroup selfSponsor = new StringFilterGroup(
Settings.HIDE_SELF_SPONSOR_CARDS,
"cta_shelf_card"
);
final StringFilterGroup viewProducts = new StringFilterGroup(
Settings.HIDE_VIEW_PRODUCTS,
"product_item",
"products_in_video",
"shopping_overlay"
);
final StringFilterGroup webSearchPanel = new StringFilterGroup(
Settings.HIDE_WEB_SEARCH_RESULTS,
"web_link_panel",
"web_result_panel"
);
addIdentifierCallbacks(
alertBannerPromo,
generalAdsIdentifier,
merchandise,
paidContent,
selfSponsor,
viewProducts,
webSearchPanel
);
// Path.
final StringFilterGroup generalAdsPath = new StringFilterGroup(
Settings.HIDE_GENERAL_ADS,
"carousel_ad",
"carousel_headered_layout",
"hero_promo_image",
"legal_disclosure",
"lumiere_promo_carousel",
"primetime_promo",
"product_details",
"text_image_button_layout",
"video_display_carousel_button",
"watch_metadata_app_promo"
);
playerShoppingShelf = new StringFilterGroup(
null,
"horizontal_shelf.eml"
);
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_STORE_SHELF,
"shopping_item_card_list.eml"
);
addPathCallbacks(
generalAdsPath,
playerShoppingShelf,
viewProducts
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == playerShoppingShelf) {
if (contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}

View File

@ -0,0 +1,95 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import java.util.function.Supplier;
import java.util.stream.Stream;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
import app.revanced.extension.youtube.shared.RootView;
@SuppressWarnings("unused")
public final class CarouselShelfFilter extends Filter {
private static final String BROWSE_ID_HOME = "FEwhat_to_watch";
private static final String BROWSE_ID_LIBRARY = "FElibrary";
private static final String BROWSE_ID_NOTIFICATION = "FEactivity";
private static final String BROWSE_ID_NOTIFICATION_INBOX = "FEnotifications_inbox";
private static final String BROWSE_ID_PLAYLIST = "VLPL";
private static final String BROWSE_ID_SUBSCRIPTION = "FEsubscriptions";
private static final Supplier<Stream<String>> knownBrowseId = () -> Stream.of(
BROWSE_ID_HOME,
BROWSE_ID_NOTIFICATION,
BROWSE_ID_PLAYLIST,
BROWSE_ID_SUBSCRIPTION
);
private static final Supplier<Stream<String>> whitelistBrowseId = () -> Stream.of(
BROWSE_ID_LIBRARY,
BROWSE_ID_NOTIFICATION_INBOX
);
private final StringTrieSearch exceptions = new StringTrieSearch();
public final StringFilterGroup horizontalShelf;
public CarouselShelfFilter() {
exceptions.addPattern("library_recent_shelf.eml");
final StringFilterGroup carouselShelf = new StringFilterGroup(
Settings.HIDE_CAROUSEL_SHELF,
"horizontal_shelf_inline.eml",
"horizontal_tile_shelf.eml",
"horizontal_video_shelf.eml"
);
horizontalShelf = new StringFilterGroup(
Settings.HIDE_CAROUSEL_SHELF,
"horizontal_shelf.eml"
);
addPathCallbacks(carouselShelf, horizontalShelf);
}
private static boolean hideShelves(boolean playerActive, boolean searchBarActive, NavigationButton selectedNavButton, String browseId) {
// Must check player type first, as search bar can be active behind the player.
if (playerActive) {
return false;
}
// Must check second, as search can be from any tab.
if (searchBarActive) {
return true;
}
// Unknown tab, treat the same as home.
if (selectedNavButton == null) {
return true;
}
return knownBrowseId.get().anyMatch(browseId::equals) || whitelistBrowseId.get().noneMatch(browseId::equals);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (exceptions.matches(path)) {
return false;
}
final boolean playerActive = RootView.isPlayerActive();
final boolean searchBarActive = RootView.isSearchBarActive();
final NavigationButton navigationButton = NavigationButton.getSelectedNavigationButton();
final String navigation = navigationButton == null ? "null" : navigationButton.name();
final String browseId = RootView.getBrowseId();
final boolean hideShelves = matchedGroup != horizontalShelf || hideShelves(playerActive, searchBarActive, navigationButton, browseId);
if (contentIndex != 0) {
return false;
}
Logger.printDebug(() -> "hideShelves: " + hideShelves + "\nplayerActive: " + playerActive + "\nsearchBarActive: " + searchBarActive + "\nbrowseId: " + browseId + "\nnavigation: " + navigation);
if (!hideShelves) {
return false;
}
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}

View File

@ -0,0 +1,133 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import java.util.regex.Pattern;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.utils.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class CommentsFilter extends Filter {
private static final String COMMENT_COMPOSER_PATH = "comment_composer";
private static final String COMMENT_ENTRY_POINT_TEASER_PATH = "comments_entry_point_teaser";
private static final Pattern COMMENT_PREVIEW_TEXT_PATTERN = Pattern.compile("comments_entry_point_teaser.+ContainerType");
private static final String FEED_VIDEO_PATH = "video_lockup_with_attachment";
private static final String VIDEO_METADATA_CAROUSEL_PATH = "video_metadata_carousel.eml";
private final StringFilterGroup comments;
private final StringFilterGroup commentsPreviewDots;
private final StringFilterGroup createShorts;
private final StringFilterGroup previewCommentText;
private final StringFilterGroup thanks;
private final StringFilterGroup timeStampAndEmojiPicker;
private final StringTrieSearch exceptions = new StringTrieSearch();
public CommentsFilter() {
exceptions.addPatterns("macro_markers_list_item");
final StringFilterGroup channelGuidelines = new StringFilterGroup(
Settings.HIDE_CHANNEL_GUIDELINES,
"channel_guidelines_entry_banner",
"community_guidelines",
"sponsorships_comments_upsell"
);
comments = new StringFilterGroup(
null,
VIDEO_METADATA_CAROUSEL_PATH,
"comments_"
);
commentsPreviewDots = new StringFilterGroup(
Settings.HIDE_PREVIEW_COMMENT_OLD_METHOD,
"|ContainerType|ContainerType|ContainerType|"
);
createShorts = new StringFilterGroup(
Settings.HIDE_COMMENT_CREATE_SHORTS_BUTTON,
"composer_short_creation_button"
);
final StringFilterGroup membersBanner = new StringFilterGroup(
Settings.HIDE_COMMENTS_BY_MEMBERS,
"sponsorships_comments_header.eml",
"sponsorships_comments_footer.eml"
);
final StringFilterGroup previewComment = new StringFilterGroup(
Settings.HIDE_PREVIEW_COMMENT_OLD_METHOD,
"|carousel_item.",
"|carousel_listener",
COMMENT_ENTRY_POINT_TEASER_PATH,
"comments_entry_point_simplebox"
);
previewCommentText = new StringFilterGroup(
Settings.HIDE_PREVIEW_COMMENT_NEW_METHOD,
COMMENT_ENTRY_POINT_TEASER_PATH
);
thanks = new StringFilterGroup(
Settings.HIDE_COMMENT_THANKS_BUTTON,
"|super_thanks_button.eml"
);
timeStampAndEmojiPicker = new StringFilterGroup(
Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS,
"|CellType|ContainerType|ContainerType|ContainerType|ContainerType|ContainerType|"
);
addIdentifierCallbacks(channelGuidelines);
addPathCallbacks(
comments,
commentsPreviewDots,
createShorts,
membersBanner,
previewComment,
previewCommentText,
thanks,
timeStampAndEmojiPicker
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (exceptions.matches(path))
return false;
if (matchedGroup == createShorts || matchedGroup == thanks || matchedGroup == timeStampAndEmojiPicker) {
if (path.startsWith(COMMENT_COMPOSER_PATH)) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
} else if (matchedGroup == comments) {
if (path.startsWith(FEED_VIDEO_PATH)) {
if (Settings.HIDE_COMMENTS_SECTION_IN_HOME_FEED.get()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
} else if (Settings.HIDE_COMMENTS_SECTION.get()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
} else if (matchedGroup == commentsPreviewDots) {
if (path.startsWith(VIDEO_METADATA_CAROUSEL_PATH)) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
} else if (matchedGroup == previewCommentText) {
if (COMMENT_PREVIEW_TEXT_PATTERN.matcher(path).find()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}

View File

@ -0,0 +1,164 @@
package app.revanced.extension.youtube.patches.components;
import static app.revanced.extension.shared.utils.StringRef.str;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.utils.ByteTrieSearch;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
/**
* Allows custom filtering using a path and optionally a proto buffer string.
*/
@SuppressWarnings("unused")
public final class CustomFilter extends Filter {
private static void showInvalidSyntaxToast(@NonNull String expression) {
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
}
private static class CustomFilterGroup extends StringFilterGroup {
/**
* Optional character for the path that indicates the custom filter path must match the start.
* Must be the first character of the expression.
*/
public static final String SYNTAX_STARTS_WITH = "^";
/**
* Optional character that separates the path from a proto buffer string pattern.
*/
public static final String SYNTAX_BUFFER_SYMBOL = "$";
/**
* @return the parsed objects
*/
@NonNull
@SuppressWarnings("ConstantConditions")
static Collection<CustomFilterGroup> parseCustomFilterGroups() {
String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get();
if (rawCustomFilterText.isBlank()) {
return Collections.emptyList();
}
// Map key is the path including optional special characters (^ and/or $)
Map<String, CustomFilterGroup> result = new HashMap<>();
Pattern pattern = Pattern.compile(
"(" // map key group
+ "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" // optional starts with
+ "([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*)" // path
+ "(\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E?)" // optional buffer symbol
+ ")" // end map key group
+ "(.*)"); // optional buffer string
for (String expression : rawCustomFilterText.split("\n")) {
if (expression.isBlank()) continue;
Matcher matcher = pattern.matcher(expression);
if (!matcher.find()) {
showInvalidSyntaxToast(expression);
continue;
}
final String mapKey = matcher.group(1);
final boolean pathStartsWith = !matcher.group(2).isEmpty();
final String path = matcher.group(3);
final boolean hasBufferSymbol = !matcher.group(4).isEmpty();
final String bufferString = matcher.group(5);
if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) {
showInvalidSyntaxToast(expression);
continue;
}
// Use one group object for all expressions with the same path.
// This ensures the buffer is searched exactly once
// when multiple paths are used with different buffer strings.
CustomFilterGroup group = result.get(mapKey);
if (group == null) {
group = new CustomFilterGroup(pathStartsWith, path);
result.put(mapKey, group);
}
if (hasBufferSymbol) {
group.addBufferString(bufferString);
}
}
return result.values();
}
final boolean startsWith;
ByteTrieSearch bufferSearch;
CustomFilterGroup(boolean startsWith, @NonNull String path) {
super(Settings.CUSTOM_FILTER, path);
this.startsWith = startsWith;
}
void addBufferString(@NonNull String bufferString) {
if (bufferSearch == null) {
bufferSearch = new ByteTrieSearch();
}
bufferSearch.addPattern(bufferString.getBytes());
}
@NonNull
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("CustomFilterGroup{");
builder.append("path=");
if (startsWith) builder.append(SYNTAX_STARTS_WITH);
builder.append(filters[0]);
if (bufferSearch != null) {
String delimitingCharacter = "";
builder.append(", bufferStrings=");
builder.append(delimitingCharacter);
for (byte[] bufferString : bufferSearch.getPatterns()) {
builder.append(new String(bufferString));
builder.append(delimitingCharacter);
}
}
builder.append("}");
return builder.toString();
}
}
public CustomFilter() {
Collection<CustomFilterGroup> groups = CustomFilterGroup.parseCustomFilterGroups();
if (!groups.isEmpty()) {
CustomFilterGroup[] groupsArray = groups.toArray(new CustomFilterGroup[0]);
Logger.printDebug(() -> "Using Custom filters: " + Arrays.toString(groupsArray));
addPathCallbacks(groupsArray);
}
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
// All callbacks are custom filter groups.
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
if (custom.startsWith && contentIndex != 0) {
return false;
}
if (custom.bufferSearch != null && !custom.bufferSearch.matches(protobufBufferArray)) {
return false;
}
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}

View File

@ -0,0 +1,111 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class DescriptionsFilter extends Filter {
private final ByteArrayFilterGroupList macroMarkerShelfGroupList = new ByteArrayFilterGroupList();
private final StringFilterGroup howThisWasMadeSection;
private final StringFilterGroup infoCardsSection;
private final StringFilterGroup macroMarkerShelf;
private final StringFilterGroup shoppingLinks;
public DescriptionsFilter() {
// game section, music section and places section now use the same identifier in the latest version.
final StringFilterGroup attributesSection = new StringFilterGroup(
Settings.HIDE_ATTRIBUTES_SECTION,
"gaming_section.eml",
"music_section.eml",
"place_section.eml",
"video_attributes_section.eml"
);
final StringFilterGroup podcastSection = new StringFilterGroup(
Settings.HIDE_PODCAST_SECTION,
"playlist_section.eml"
);
final StringFilterGroup transcriptSection = new StringFilterGroup(
Settings.HIDE_TRANSCRIPT_SECTION,
"transcript_section.eml"
);
final StringFilterGroup videoSummarySection = new StringFilterGroup(
Settings.HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION,
"cell_expandable_metadata.eml-js"
);
addIdentifierCallbacks(
attributesSection,
podcastSection,
transcriptSection,
videoSummarySection
);
howThisWasMadeSection = new StringFilterGroup(
Settings.HIDE_CONTENTS_SECTION,
"how_this_was_made_section.eml"
);
infoCardsSection = new StringFilterGroup(
Settings.HIDE_INFO_CARDS_SECTION,
"infocards_section.eml"
);
macroMarkerShelf = new StringFilterGroup(
null,
"macro_markers_carousel.eml"
);
shoppingLinks = new StringFilterGroup(
Settings.HIDE_SHOPPING_LINKS,
"expandable_list.",
"shopping_description_shelf"
);
addPathCallbacks(
howThisWasMadeSection,
infoCardsSection,
macroMarkerShelf,
shoppingLinks
);
macroMarkerShelfGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_CHAPTERS_SECTION,
"chapters_horizontal_shelf"
),
new ByteArrayFilterGroup(
Settings.HIDE_KEY_CONCEPTS_SECTION,
"learning_concept_macro_markers_carousel_shelf"
)
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
// Check for the index because of likelihood of false positives.
if (matchedGroup == howThisWasMadeSection || matchedGroup == infoCardsSection || matchedGroup == shoppingLinks) {
if (contentIndex != 0) {
return false;
}
} else if (matchedGroup == macroMarkerShelf) {
if (contentIndex != 0) {
return false;
}
if (!macroMarkerShelfGroupList.check(protobufBufferArray).isFiltered()) {
return false;
}
}
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}

View File

@ -0,0 +1,268 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.patches.components.StringFilterGroupList;
import app.revanced.extension.shared.utils.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class FeedComponentsFilter extends Filter {
private static final String CONVERSATION_CONTEXT_FEED_IDENTIFIER =
"horizontalCollectionSwipeProtector=null";
private static final String CONVERSATION_CONTEXT_SUBSCRIPTIONS_IDENTIFIER =
"heightConstraint=null";
private static final String INLINE_EXPANSION_PATH = "inline_expansion";
private static final String FEED_VIDEO_PATH = "video_lockup_with_attachment";
private static final ByteArrayFilterGroup inlineExpansion =
new ByteArrayFilterGroup(
Settings.HIDE_EXPANDABLE_CHIP,
"inline_expansion"
);
private static final ByteArrayFilterGroup mixPlaylists =
new ByteArrayFilterGroup(
Settings.HIDE_MIX_PLAYLISTS,
"&list="
);
private static final ByteArrayFilterGroup mixPlaylistsBufferExceptions =
new ByteArrayFilterGroup(
null,
"cell_description_body",
"channel_profile"
);
private static final StringTrieSearch mixPlaylistsContextExceptions = new StringTrieSearch();
private final StringFilterGroup channelProfile;
private final StringFilterGroup communityPosts;
private final StringFilterGroup expandableChip;
private final ByteArrayFilterGroup visitStoreButton;
private final StringFilterGroup videoLockup;
private static final StringTrieSearch communityPostsFeedGroupSearch = new StringTrieSearch();
private final StringFilterGroupList communityPostsFeedGroup = new StringFilterGroupList();
public FeedComponentsFilter() {
communityPostsFeedGroupSearch.addPatterns(
CONVERSATION_CONTEXT_FEED_IDENTIFIER,
CONVERSATION_CONTEXT_SUBSCRIPTIONS_IDENTIFIER
);
mixPlaylistsContextExceptions.addPatterns(
"V.ED", // playlist browse id
"java.lang.ref.WeakReference"
);
// Identifiers.
final StringFilterGroup chipsShelf = new StringFilterGroup(
Settings.HIDE_CHIPS_SHELF,
"chips_shelf"
);
communityPosts = new StringFilterGroup(
null,
"post_base_wrapper",
"images_post_root",
"images_post_slim",
"text_post_root"
);
final StringFilterGroup expandableShelf = new StringFilterGroup(
Settings.HIDE_EXPANDABLE_SHELF,
"expandable_section"
);
final StringFilterGroup feedSearchBar = new StringFilterGroup(
Settings.HIDE_FEED_SEARCH_BAR,
"search_bar_entry_point"
);
final StringFilterGroup tasteBuilder = new StringFilterGroup(
Settings.HIDE_FEED_SURVEY,
"selectable_item.eml",
"cell_button.eml"
);
videoLockup = new StringFilterGroup(
null,
FEED_VIDEO_PATH
);
addIdentifierCallbacks(
chipsShelf,
communityPosts,
expandableShelf,
feedSearchBar,
tasteBuilder,
videoLockup
);
// Paths.
final StringFilterGroup albumCard = new StringFilterGroup(
Settings.HIDE_ALBUM_CARDS,
"browsy_bar",
"official_card"
);
channelProfile = new StringFilterGroup(
Settings.HIDE_BROWSE_STORE_BUTTON,
"channel_profile.eml",
"page_header.eml" // new layout
);
visitStoreButton = new ByteArrayFilterGroup(
null,
"header_store_button"
);
final StringFilterGroup channelMemberShelf = new StringFilterGroup(
Settings.HIDE_CHANNEL_MEMBER_SHELF,
"member_recognition_shelf"
);
final StringFilterGroup channelProfileLinks = new StringFilterGroup(
Settings.HIDE_CHANNEL_PROFILE_LINKS,
"channel_header_links",
"attribution.eml" // new layout
);
expandableChip = new StringFilterGroup(
Settings.HIDE_EXPANDABLE_CHIP,
INLINE_EXPANSION_PATH,
"inline_expander",
"expandable_metadata.eml"
);
final StringFilterGroup feedSurvey = new StringFilterGroup(
Settings.HIDE_FEED_SURVEY,
"feed_nudge",
"_survey"
);
final StringFilterGroup forYouShelf = new StringFilterGroup(
Settings.HIDE_FOR_YOU_SHELF,
"mixed_content_shelf"
);
final StringFilterGroup imageShelf = new StringFilterGroup(
Settings.HIDE_IMAGE_SHELF,
"image_shelf"
);
final StringFilterGroup latestPosts = new StringFilterGroup(
Settings.HIDE_LATEST_POSTS,
"post_shelf"
);
final StringFilterGroup movieShelf = new StringFilterGroup(
Settings.HIDE_MOVIE_SHELF,
"compact_movie",
"horizontal_movie_shelf",
"movie_and_show_upsell_card",
"compact_tvfilm_item",
"offer_module"
);
final StringFilterGroup notifyMe = new StringFilterGroup(
Settings.HIDE_NOTIFY_ME_BUTTON,
"set_reminder_button"
);
final StringFilterGroup playables = new StringFilterGroup(
Settings.HIDE_PLAYABLES,
"horizontal_gaming_shelf.eml",
"mini_game_card.eml"
);
final StringFilterGroup subscriptionsChannelBar = new StringFilterGroup(
Settings.HIDE_SUBSCRIPTIONS_CAROUSEL,
"subscriptions_channel_bar"
);
final StringFilterGroup ticketShelf = new StringFilterGroup(
Settings.HIDE_TICKET_SHELF,
"ticket_horizontal_shelf",
"ticket_shelf"
);
addPathCallbacks(
albumCard,
channelProfile,
channelMemberShelf,
channelProfileLinks,
expandableChip,
feedSurvey,
forYouShelf,
imageShelf,
latestPosts,
movieShelf,
notifyMe,
playables,
subscriptionsChannelBar,
ticketShelf,
videoLockup
);
final StringFilterGroup communityPostsHomeAndRelatedVideos =
new StringFilterGroup(
Settings.HIDE_COMMUNITY_POSTS_HOME_RELATED_VIDEOS,
CONVERSATION_CONTEXT_FEED_IDENTIFIER
);
final StringFilterGroup communityPostsSubscriptions =
new StringFilterGroup(
Settings.HIDE_COMMUNITY_POSTS_SUBSCRIPTIONS,
CONVERSATION_CONTEXT_SUBSCRIPTIONS_IDENTIFIER
);
communityPostsFeedGroup.addAll(communityPostsHomeAndRelatedVideos, communityPostsSubscriptions);
}
/**
* Injection point.
* <p>
* Called from a different place then the other filters.
*/
public static boolean filterMixPlaylists(final Object conversionContext, final byte[] bytes) {
return bytes != null
&& mixPlaylists.check(bytes).isFiltered()
&& !mixPlaylistsBufferExceptions.check(bytes).isFiltered()
&& !mixPlaylistsContextExceptions.matches(conversionContext.toString());
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == channelProfile) {
if (contentIndex == 0 && visitStoreButton.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
} else if (matchedGroup == communityPosts) {
if (!communityPostsFeedGroupSearch.matches(allValue) && Settings.HIDE_COMMUNITY_POSTS_CHANNEL.get()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
if (!communityPostsFeedGroup.check(allValue).isFiltered()) {
return false;
}
} else if (matchedGroup == expandableChip) {
if (path.startsWith(FEED_VIDEO_PATH)) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
} else if (matchedGroup == videoLockup) {
if (contentIndex == 0 && path.startsWith("CellType|") && inlineExpansion.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}

View File

@ -0,0 +1,99 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.patches.components.StringFilterGroupList;
import app.revanced.extension.shared.utils.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.RootView;
@SuppressWarnings("unused")
public final class FeedVideoFilter extends Filter {
private static final String CONVERSATION_CONTEXT_FEED_IDENTIFIER =
"horizontalCollectionSwipeProtector=null";
private static final String ENDORSEMENT_FOOTER_PATH = "endorsement_header_footer";
private static final StringTrieSearch feedOnlyVideoPattern = new StringTrieSearch();
// In search results, vertical video with shorts labels mostly include videos with gray descriptions.
// Filters without check process.
private final StringFilterGroup inlineShorts;
// Used for home, related videos, subscriptions, and search results.
private final StringFilterGroup videoLockup = new StringFilterGroup(
null,
"video_lockup_with_attachment.eml"
);
private final ByteArrayFilterGroupList feedAndDrawerGroupList = new ByteArrayFilterGroupList();
private final ByteArrayFilterGroupList feedOnlyGroupList = new ByteArrayFilterGroupList();
private final StringFilterGroupList videoLockupFilterGroup = new StringFilterGroupList();
private static final ByteArrayFilterGroup relatedVideo =
new ByteArrayFilterGroup(
Settings.HIDE_RELATED_VIDEOS,
"relatedH"
);
public FeedVideoFilter() {
feedOnlyVideoPattern.addPattern(CONVERSATION_CONTEXT_FEED_IDENTIFIER);
inlineShorts = new StringFilterGroup(
Settings.HIDE_RECOMMENDED_VIDEO,
"inline_shorts.eml" // vertical video with shorts label
);
addIdentifierCallbacks(inlineShorts);
addPathCallbacks(videoLockup);
feedAndDrawerGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_RECOMMENDED_VIDEO,
ENDORSEMENT_FOOTER_PATH, // videos with gray descriptions
"high-ptsZ" // videos for membership only
)
);
feedOnlyGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_LOW_VIEWS_VIDEO,
"g-highZ" // videos with less than 1000 views
)
);
videoLockupFilterGroup.addAll(
new StringFilterGroup(
Settings.HIDE_RECOMMENDED_VIDEO,
ENDORSEMENT_FOOTER_PATH
)
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == inlineShorts) {
if (RootView.isSearchBarActive()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
} else if (matchedGroup == videoLockup) {
if (relatedVideo.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
if (feedOnlyVideoPattern.matches(allValue)) {
if (feedOnlyGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
} else if (videoLockupFilterGroup.check(allValue).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
} else {
if (feedAndDrawerGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}
}
return false;
}
}

View File

@ -0,0 +1,180 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.NavigationBar;
import app.revanced.extension.youtube.shared.RootView;
@SuppressWarnings("all")
public final class FeedVideoViewsFilter extends Filter {
private final StringFilterGroup feedVideoFilter = new StringFilterGroup(
null,
"video_lockup_with_attachment.eml"
);
public FeedVideoViewsFilter() {
addPathCallbacks(feedVideoFilter);
}
private boolean hideFeedVideoViewsSettingIsActive() {
final boolean hideHome = Settings.HIDE_VIDEO_BY_VIEW_COUNTS_HOME.get();
final boolean hideSearch = Settings.HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH.get();
final boolean hideSubscriptions = Settings.HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS.get();
if (!hideHome && !hideSearch && !hideSubscriptions) {
return false;
} else if (hideHome && hideSearch && hideSubscriptions) {
return true;
}
// Must check player type first, as search bar can be active behind the player.
if (RootView.isPlayerActive()) {
// For now, consider the under video results the same as the home feed.
return hideHome;
}
// Must check second, as search can be from any tab.
if (RootView.isSearchBarActive()) {
return hideSearch;
}
NavigationBar.NavigationButton selectedNavButton = NavigationBar.NavigationButton.getSelectedNavigationButton();
if (selectedNavButton == null) {
return hideHome; // Unknown tab, treat the same as home.
} else if (selectedNavButton == NavigationBar.NavigationButton.HOME) {
return hideHome;
} else if (selectedNavButton == NavigationBar.NavigationButton.SUBSCRIPTIONS) {
return hideSubscriptions;
}
// User is in the Library or Notifications tab.
return false;
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (hideFeedVideoViewsSettingIsActive() &&
filterByViews(protobufBufferArray)) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
private final String ARROW = " -> ";
private final String VIEWS = "views";
private final String[] parts = Settings.HIDE_VIDEO_VIEW_COUNTS_MULTIPLIER.get().split("\\n");
private Pattern[] viewCountPatterns = null;
/**
* Hide videos based on views count
*/
private synchronized boolean filterByViews(byte[] protobufBufferArray) {
final String protobufString = new String(protobufBufferArray);
final long lessThan = Settings.HIDE_VIDEO_VIEW_COUNTS_LESS_THAN.get();
final long greaterThan = Settings.HIDE_VIDEO_VIEW_COUNTS_GREATER_THAN.get();
if (viewCountPatterns == null) {
viewCountPatterns = getViewCountPatterns(parts);
}
for (Pattern pattern : viewCountPatterns) {
final Matcher matcher = pattern.matcher(protobufString);
if (matcher.find()) {
String numString = Objects.requireNonNull(matcher.group(1));
double num = parseNumber(numString);
String multiplierKey = matcher.group(2);
long multiplierValue = getMultiplierValue(parts, multiplierKey);
return num * multiplierValue < lessThan || num * multiplierValue > greaterThan;
}
}
return false;
}
private synchronized double parseNumber(String numString) {
/**
* Some languages have comma (,) as a decimal separator.
* In order to detect those numbers as doubles in Java
* we convert commas (,) to dots (.).
* Unless we find a language that has commas used in
* a different manner, it should work.
*/
numString = numString.replace(",", ".");
/**
* Some languages have dot (.) as a kilo separator.
* So we check with regex if there is a number with 3+
* digits after dot (.), we replace it with nothing
* to make Java understand the number as a whole.
*/
if (numString.matches("\\d+\\.\\d{3,}")) {
numString = numString.replace(".", "");
}
return Double.parseDouble(numString);
}
private synchronized Pattern[] getViewCountPatterns(String[] parts) {
StringBuilder prefixPatternBuilder = new StringBuilder("(\\d+(?:[.,]\\d+)?)\\s?("); // LTR layout
StringBuilder secondPatternBuilder = new StringBuilder(); // RTL layout
StringBuilder suffixBuilder = getSuffixBuilder(parts, prefixPatternBuilder, secondPatternBuilder);
prefixPatternBuilder.deleteCharAt(prefixPatternBuilder.length() - 1); // Remove the trailing |
prefixPatternBuilder.append(")?\\s*");
prefixPatternBuilder.append(suffixBuilder.length() > 0 ? suffixBuilder.toString() : VIEWS);
secondPatternBuilder.deleteCharAt(secondPatternBuilder.length() - 1); // Remove the trailing |
secondPatternBuilder.append(")?");
final Pattern[] patterns = new Pattern[2];
patterns[0] = Pattern.compile(prefixPatternBuilder.toString());
patterns[1] = Pattern.compile(secondPatternBuilder.toString());
return patterns;
}
@NonNull
private synchronized StringBuilder getSuffixBuilder(String[] parts, StringBuilder prefixPatternBuilder, StringBuilder secondPatternBuilder) {
StringBuilder suffixBuilder = new StringBuilder();
for (String part : parts) {
final String[] pair = part.split(ARROW);
final String pair0 = pair[0].trim();
final String pair1 = pair[1].trim();
if (pair.length == 2 && !pair1.equals(VIEWS)) {
prefixPatternBuilder.append(pair0).append("|");
}
if (pair.length == 2 && pair1.equals(VIEWS)) {
suffixBuilder.append(pair0);
secondPatternBuilder.append(pair0).append("\\s*").append(prefixPatternBuilder);
}
}
return suffixBuilder;
}
private synchronized long getMultiplierValue(String[] parts, String multiplier) {
for (String part : parts) {
final String[] pair = part.split(ARROW);
final String pair0 = pair[0].trim();
final String pair1 = pair[1].trim();
if (pair.length == 2 && pair0.equals(multiplier) && !pair1.equals(VIEWS)) {
return Long.parseLong(pair[1].replaceAll("[^\\d]", ""));
}
}
return 1L; // Default value if not found
}
}

View File

@ -0,0 +1,632 @@
package app.revanced.extension.youtube.patches.components;
import static java.lang.Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS;
import static java.lang.Character.UnicodeBlock.HIRAGANA;
import static java.lang.Character.UnicodeBlock.KATAKANA;
import static java.lang.Character.UnicodeBlock.KHMER;
import static java.lang.Character.UnicodeBlock.LAO;
import static java.lang.Character.UnicodeBlock.MYANMAR;
import static java.lang.Character.UnicodeBlock.THAI;
import static java.lang.Character.UnicodeBlock.TIBETAN;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.utils.ByteTrieSearch;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.StringTrieSearch;
import app.revanced.extension.shared.utils.TrieSearch;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.RootView;
/**
* <pre>
* Allows hiding home feed and search results based on video title keywords and/or channel names.
*
* Limitations:
* - Searching for a keyword phrase will give no search results.
* This is because the buffer for each video contains the text the user searched for, and everything
* will be filtered away (even if that video title/channel does not contain any keywords).
* - Filtering a channel name can still show Shorts from that channel in the search results.
* The most common Shorts layouts do not include the channel name, so they will not be filtered.
* - Some layout component residue will remain, such as the video chapter previews for some search results.
* These components do not include the video title or channel name, and they
* appear outside the filtered components so they are not caught.
* - Keywords are case sensitive, but some casing variation is manually added.
* (ie: "mr beast" automatically filters "Mr Beast" and "MR BEAST").
* - Keywords present in the layout or video data cannot be used as filters, otherwise all videos
* will always be hidden. This patch checks for some words of these words.
* - When using whole word syntax, some keywords may need additional pluralized variations.
*/
@SuppressWarnings("unused")
public final class KeywordContentFilter extends Filter {
/**
* Strings found in the buffer for every videos. Full strings should be specified.
* <p>
* This list does not include every common buffer string, and this can be added/changed as needed.
* Words must be entered with the exact casing as found in the buffer.
*/
private static final String[] STRINGS_IN_EVERY_BUFFER = {
// Video playback data.
"googlevideo.com/initplayback?source=youtube", // Video url.
"ANDROID", // Video url parameter.
"https://i.ytimg.com/vi/", // Thumbnail url.
"mqdefault.jpg",
"hqdefault.jpg",
"sddefault.jpg",
"hq720.jpg",
"webp",
"_custom_", // Custom thumbnail set by video creator.
// Video decoders.
"OMX.ffmpeg.vp9.decoder",
"OMX.Intel.sw_vd.vp9",
"OMX.MTK.VIDEO.DECODER.SW.VP9",
"OMX.google.vp9.decoder",
"OMX.google.av1.decoder",
"OMX.sprd.av1.decoder",
"c2.android.av1.decoder",
"c2.android.av1-dav1d.decoder",
"c2.android.vp9.decoder",
"c2.mtk.sw.vp9.decoder",
// Analytics.
"searchR",
"browse-feed",
"FEwhat_to_watch",
"FEsubscriptions",
"search_vwc_description_transition_key",
"g-high-recZ",
// Text and litho components found in the buffer that belong to path filters.
"expandable_metadata.eml",
"thumbnail.eml",
"avatar.eml",
"overflow_button.eml",
"shorts-lockup-image",
"shorts-lockup.overlay-metadata.secondary-text",
"YouTubeSans-SemiBold",
"sans-serif"
};
/**
* Substrings that are always first in the identifier.
*/
private final StringFilterGroup startsWithFilter = new StringFilterGroup(
null, // Multiple settings are used and must be individually checked if active.
"video_lockup_with_attachment.eml",
"compact_video.eml",
"inline_shorts",
"shorts_video_cell",
"shorts_pivot_item.eml"
);
/**
* Substrings that are never at the start of the path.
*/
@SuppressWarnings("FieldCanBeLocal")
private final StringFilterGroup containsFilter = new StringFilterGroup(
null,
"modern_type_shelf_header_content.eml",
"shorts_lockup_cell.eml", // Part of 'shorts_shelf_carousel.eml'
"video_card.eml" // Shorts that appear in a horizontal shelf.
);
/**
* Path components to not filter. Cannot filter the buffer when these are present,
* otherwise text in UI controls can be filtered as a keyword (such as using "Playlist" as a keyword).
* <p>
* This is also a small performance improvement since
* the buffer of the parent component was already searched and passed.
*/
private final StringTrieSearch exceptions = new StringTrieSearch(
"metadata.eml",
"thumbnail.eml",
"avatar.eml",
"overflow_button.eml"
);
/**
* Minimum keyword/phrase length to prevent excessively broad content filtering.
* Only applies when not using whole word syntax.
*/
private static final int MINIMUM_KEYWORD_LENGTH = 3;
/**
* Threshold for {@link #filteredVideosPercentage}
* that indicates all or nearly all videos have been filtered.
* This should be close to 100% to reduce false positives.
*/
private static final float ALL_VIDEOS_FILTERED_THRESHOLD = 0.95f;
private static final float ALL_VIDEOS_FILTERED_SAMPLE_SIZE = 50;
private static final long ALL_VIDEOS_FILTERED_BACKOFF_MILLISECONDS = 60 * 1000; // 60 seconds
private static final int UTF8_MAX_BYTE_COUNT = 4;
/**
* Rolling average of how many videos were filtered by a keyword.
* Used to detect if a keyword passes the initial check against {@link #STRINGS_IN_EVERY_BUFFER}
* but a keyword is still hiding all videos.
* <p>
* This check can still fail if some extra UI elements pass the keywords,
* such as the video chapter preview or any other elements.
* <p>
* To test this, add a filter that appears in all videos (such as 'ovd='),
* and open the subscription feed. In practice this does not always identify problems
* in the home feed and search, because the home feed has a finite amount of content and
* search results have a lot of extra video junk that is not hidden and interferes with the detection.
*/
private volatile float filteredVideosPercentage;
/**
* If filtering is temporarily turned off, the time to resume filtering.
* Field is zero if no timeout is in effect.
*/
private volatile long timeToResumeFiltering;
private final StringFilterGroup commentsFilter;
private final StringTrieSearch commentsFilterExceptions = new StringTrieSearch();
/**
* The last value of {@link Settings#HIDE_KEYWORD_CONTENT_PHRASES}
* parsed and loaded into {@link #bufferSearch}.
* Allows changing the keywords without restarting the app.
*/
private volatile String lastKeywordPhrasesParsed;
private volatile ByteTrieSearch bufferSearch;
private static void logNavigationState(String state) {
// Enable locally to debug filtering. Default off to reduce log spam.
final boolean LOG_NAVIGATION_STATE = false;
// noinspection ConstantValue
if (LOG_NAVIGATION_STATE) {
Logger.printDebug(() -> "Navigation state: " + state);
}
}
/**
* Change first letter of the first word to use title case.
*/
private static String titleCaseFirstWordOnly(String sentence) {
if (sentence.isEmpty()) {
return sentence;
}
final int firstCodePoint = sentence.codePointAt(0);
// In some non English languages title case is different than uppercase.
return new StringBuilder()
.appendCodePoint(Character.toTitleCase(firstCodePoint))
.append(sentence, Character.charCount(firstCodePoint), sentence.length())
.toString();
}
/**
* Uppercase the first letter of each word.
*/
private static String capitalizeAllFirstLetters(String sentence) {
if (sentence.isEmpty()) {
return sentence;
}
final int delimiter = ' ';
// Use code points and not characters to handle unicode surrogates.
int[] codePoints = sentence.codePoints().toArray();
boolean capitalizeNext = true;
for (int i = 0, length = codePoints.length; i < length; i++) {
final int codePoint = codePoints[i];
if (codePoint == delimiter) {
capitalizeNext = true;
} else if (capitalizeNext) {
codePoints[i] = Character.toUpperCase(codePoint);
capitalizeNext = false;
}
}
return new String(codePoints, 0, codePoints.length);
}
/**
* @return If the string contains any characters from languages that do not use spaces between words.
*/
private static boolean isLanguageWithNoSpaces(String text) {
for (int i = 0, length = text.length(); i < length; ) {
final int codePoint = text.codePointAt(i);
Character.UnicodeBlock block = Character.UnicodeBlock.of(codePoint);
if (block == CJK_UNIFIED_IDEOGRAPHS // Chinese and Kanji
|| block == HIRAGANA // Japanese Hiragana
|| block == KATAKANA // Japanese Katakana
|| block == THAI
|| block == LAO
|| block == MYANMAR
|| block == KHMER
|| block == TIBETAN) {
return true;
}
i += Character.charCount(codePoint);
}
return false;
}
/**
* @return If the phrase will hide all videos. Not an exhaustive check.
*/
private static boolean phrasesWillHideAllVideos(@NonNull String[] phrases, boolean matchWholeWords) {
for (String phrase : phrases) {
for (String commonString : STRINGS_IN_EVERY_BUFFER) {
if (matchWholeWords) {
byte[] commonStringBytes = commonString.getBytes(StandardCharsets.UTF_8);
int matchIndex = 0;
while (true) {
matchIndex = commonString.indexOf(phrase, matchIndex);
if (matchIndex < 0) break;
if (keywordMatchIsWholeWord(commonStringBytes, matchIndex, phrase.length())) {
return true;
}
matchIndex++;
}
} else if (Utils.containsAny(commonString, phrases)) {
return true;
}
}
}
return false;
}
/**
* @return If the start and end indexes are not surrounded by other letters.
* If the indexes are surrounded by numbers/symbols/punctuation it is considered a whole word.
*/
private static boolean keywordMatchIsWholeWord(byte[] text, int keywordStartIndex, int keywordLength) {
final Integer codePointBefore = getUtf8CodePointBefore(text, keywordStartIndex);
if (codePointBefore != null && Character.isLetter(codePointBefore)) {
return false;
}
final Integer codePointAfter = getUtf8CodePointAt(text, keywordStartIndex + keywordLength);
//noinspection RedundantIfStatement
if (codePointAfter != null && Character.isLetter(codePointAfter)) {
return false;
}
return true;
}
/**
* @return The UTF8 character point immediately before the index,
* or null if the bytes before the index is not a valid UTF8 character.
*/
@Nullable
private static Integer getUtf8CodePointBefore(byte[] data, int index) {
int characterByteCount = 0;
while (--index >= 0 && ++characterByteCount <= UTF8_MAX_BYTE_COUNT) {
if (isValidUtf8(data, index, characterByteCount)) {
return decodeUtf8ToCodePoint(data, index, characterByteCount);
}
}
return null;
}
/**
* @return The UTF8 character point at the index,
* or null if the index holds no valid UTF8 character.
*/
@Nullable
private static Integer getUtf8CodePointAt(byte[] data, int index) {
int characterByteCount = 0;
final int dataLength = data.length;
while (index + characterByteCount < dataLength && ++characterByteCount <= UTF8_MAX_BYTE_COUNT) {
if (isValidUtf8(data, index, characterByteCount)) {
return decodeUtf8ToCodePoint(data, index, characterByteCount);
}
}
return null;
}
public static boolean isValidUtf8(byte[] data, int startIndex, int numberOfBytes) {
switch (numberOfBytes) {
case 1 -> { // 0xxxxxxx (ASCII)
return (data[startIndex] & 0x80) == 0;
}
case 2 -> { // 110xxxxx, 10xxxxxx
return (data[startIndex] & 0xE0) == 0xC0
&& (data[startIndex + 1] & 0xC0) == 0x80;
}
case 3 -> { // 1110xxxx, 10xxxxxx, 10xxxxxx
return (data[startIndex] & 0xF0) == 0xE0
&& (data[startIndex + 1] & 0xC0) == 0x80
&& (data[startIndex + 2] & 0xC0) == 0x80;
}
case 4 -> { // 11110xxx, 10xxxxxx, 10xxxxxx, 10xxxxxx
return (data[startIndex] & 0xF8) == 0xF0
&& (data[startIndex + 1] & 0xC0) == 0x80
&& (data[startIndex + 2] & 0xC0) == 0x80
&& (data[startIndex + 3] & 0xC0) == 0x80;
}
}
throw new IllegalArgumentException("numberOfBytes: " + numberOfBytes);
}
public static int decodeUtf8ToCodePoint(byte[] data, int startIndex, int numberOfBytes) {
switch (numberOfBytes) {
case 1 -> {
return data[startIndex];
}
case 2 -> {
return ((data[startIndex] & 0x1F) << 6) |
(data[startIndex + 1] & 0x3F);
}
case 3 -> {
return ((data[startIndex] & 0x0F) << 12) |
((data[startIndex + 1] & 0x3F) << 6) |
(data[startIndex + 2] & 0x3F);
}
case 4 -> {
return ((data[startIndex] & 0x07) << 18) |
((data[startIndex + 1] & 0x3F) << 12) |
((data[startIndex + 2] & 0x3F) << 6) |
(data[startIndex + 3] & 0x3F);
}
}
throw new IllegalArgumentException("numberOfBytes: " + numberOfBytes);
}
private static boolean phraseUsesWholeWordSyntax(String phrase) {
return phrase.startsWith("\"") && phrase.endsWith("\"");
}
private static String stripWholeWordSyntax(String phrase) {
return phrase.substring(1, phrase.length() - 1);
}
private synchronized void parseKeywords() { // Must be synchronized since Litho is multi-threaded.
String rawKeywords = Settings.HIDE_KEYWORD_CONTENT_PHRASES.get();
//noinspection StringEquality
if (rawKeywords == lastKeywordPhrasesParsed) {
Logger.printDebug(() -> "Using previously initialized search");
return; // Another thread won the race, and search is already initialized.
}
ByteTrieSearch search = new ByteTrieSearch();
String[] split = rawKeywords.split("\n");
if (split.length != 0) {
// Linked Set so log statement are more organized and easier to read.
// Map is: Phrase -> isWholeWord
Map<String, Boolean> keywords = new LinkedHashMap<>(10 * split.length);
for (String phrase : split) {
// Remove any trailing spaces the user may have accidentally included.
phrase = phrase.stripTrailing();
if (phrase.isBlank()) continue;
final boolean wholeWordMatching;
if (phraseUsesWholeWordSyntax(phrase)) {
if (phrase.length() == 2) {
continue; // Empty "" phrase
}
phrase = stripWholeWordSyntax(phrase);
wholeWordMatching = true;
} else if (phrase.length() < MINIMUM_KEYWORD_LENGTH && !isLanguageWithNoSpaces(phrase)) {
// Allow phrases of 1 and 2 characters if using a
// language that does not use spaces between words.
// Do not reset the setting. Keep the invalid keywords so the user can fix the mistake.
Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_length", phrase, MINIMUM_KEYWORD_LENGTH));
continue;
} else {
wholeWordMatching = false;
}
// Common casing that might appear.
//
// This could be simplified by adding case insensitive search to the prefix search,
// which is very simple to add to StringTreSearch for Unicode and ByteTrieSearch for ASCII.
//
// But to support Unicode with ByteTrieSearch would require major changes because
// UTF-8 characters can be different byte lengths, which does
// not allow comparing two different byte arrays using simple plain array indexes.
//
// Instead use all common case variations of the words.
String[] phraseVariations = {
phrase,
phrase.toLowerCase(),
titleCaseFirstWordOnly(phrase),
capitalizeAllFirstLetters(phrase),
phrase.toUpperCase()
};
if (phrasesWillHideAllVideos(phraseVariations, wholeWordMatching)) {
String toastMessage;
// If whole word matching is off, but would pass with on, then show a different toast.
if (!wholeWordMatching && !phrasesWillHideAllVideos(phraseVariations, true)) {
toastMessage = "revanced_hide_keyword_toast_invalid_common_whole_word_required";
} else {
toastMessage = "revanced_hide_keyword_toast_invalid_common";
}
Utils.showToastLong(str(toastMessage, phrase));
continue;
}
for (String variation : phraseVariations) {
// Check if the same phrase is declared both with and without quotes.
Boolean existing = keywords.get(variation);
if (existing == null) {
keywords.put(variation, wholeWordMatching);
} else if (existing != wholeWordMatching) {
Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_conflicting", phrase));
break;
}
}
}
for (Map.Entry<String, Boolean> entry : keywords.entrySet()) {
String keyword = entry.getKey();
//noinspection ExtractMethodRecommender
final boolean isWholeWord = entry.getValue();
TrieSearch.TriePatternMatchedCallback<byte[]> callback =
(textSearched, startIndex, matchLength, callbackParameter) -> {
if (isWholeWord && !keywordMatchIsWholeWord(textSearched, startIndex, matchLength)) {
return false;
}
Logger.printDebug(() -> (isWholeWord ? "Matched whole keyword: '"
: "Matched keyword: '") + keyword + "'");
// noinspection unchecked
((MutableReference<String>) callbackParameter).value = keyword;
return true;
};
byte[] stringBytes = keyword.getBytes(StandardCharsets.UTF_8);
search.addPattern(stringBytes, callback);
}
Logger.printDebug(() -> "Search using: (" + search.getEstimatedMemorySize() + " KB) keywords: " + keywords.keySet());
}
bufferSearch = search;
timeToResumeFiltering = 0;
filteredVideosPercentage = 0;
lastKeywordPhrasesParsed = rawKeywords; // Must set last.
}
public KeywordContentFilter() {
commentsFilterExceptions.addPatterns("engagement_toolbar");
commentsFilter = new StringFilterGroup(
Settings.HIDE_KEYWORD_CONTENT_COMMENTS,
"comment_thread.eml"
);
// Keywords are parsed on first call to isFiltered()
addPathCallbacks(startsWithFilter, containsFilter, commentsFilter);
}
private boolean hideKeywordSettingIsActive() {
if (timeToResumeFiltering != 0) {
if (System.currentTimeMillis() < timeToResumeFiltering) {
return false;
}
timeToResumeFiltering = 0;
filteredVideosPercentage = 0;
Logger.printDebug(() -> "Resuming keyword filtering");
}
final boolean hideHome = Settings.HIDE_KEYWORD_CONTENT_HOME.get();
final boolean hideSearch = Settings.HIDE_KEYWORD_CONTENT_SEARCH.get();
final boolean hideSubscriptions = Settings.HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS.get();
if (!hideHome && !hideSearch && !hideSubscriptions) {
return false;
} else if (hideHome && hideSearch && hideSubscriptions) {
return true;
}
// Must check player type first, as search bar can be active behind the player.
if (RootView.isPlayerActive()) {
// For now, consider the under video results the same as the home feed.
return hideHome;
}
// Must check second, as search can be from any tab.
if (RootView.isSearchBarActive()) {
return hideSearch;
}
NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton();
if (selectedNavButton == null) {
return hideHome; // Unknown tab, treat the same as home.
}
if (selectedNavButton == NavigationButton.HOME) {
return hideHome;
}
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) {
return hideSubscriptions;
}
// User is in the Library or Notifications tab.
return false;
}
private void updateStats(boolean videoWasHidden, @Nullable String keyword) {
float updatedAverage = filteredVideosPercentage
* ((ALL_VIDEOS_FILTERED_SAMPLE_SIZE - 1) / ALL_VIDEOS_FILTERED_SAMPLE_SIZE);
if (videoWasHidden) {
updatedAverage += 1 / ALL_VIDEOS_FILTERED_SAMPLE_SIZE;
}
if (updatedAverage <= ALL_VIDEOS_FILTERED_THRESHOLD) {
filteredVideosPercentage = updatedAverage;
return;
}
// A keyword is hiding everything.
// Inform the user, and temporarily turn off filtering.
timeToResumeFiltering = System.currentTimeMillis() + ALL_VIDEOS_FILTERED_BACKOFF_MILLISECONDS;
Logger.printDebug(() -> "Temporarily turning off filtering due to excessively broad filter: " + keyword);
Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_broad", keyword));
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
return false;
}
// Do not filter if comments path includes an engagement toolbar (like, dislike...)
if (matchedGroup == commentsFilter && commentsFilterExceptions.matches(path)) {
return false;
}
// Field is intentionally compared using reference equality.
//noinspection StringEquality
if (Settings.HIDE_KEYWORD_CONTENT_PHRASES.get() != lastKeywordPhrasesParsed) {
// User changed the keywords or whole word setting.
parseKeywords();
}
if (matchedGroup != commentsFilter && !hideKeywordSettingIsActive()) {
return false;
}
if (exceptions.matches(path)) {
return false; // Do not update statistics.
}
MutableReference<String> matchRef = new MutableReference<>();
if (bufferSearch.matches(protobufBufferArray, matchRef)) {
updateStats(true, matchRef.value);
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
updateStats(false, null);
return false;
}
}
/**
* Simple non-atomic wrapper since {@link AtomicReference#setPlain(Object)} is not available with Android 8.0.
*/
final class MutableReference<T> {
T value;
}

View File

@ -0,0 +1,38 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class LayoutComponentsFilter extends Filter {
private static final String ACCOUNT_HEADER_PATH = "account_header.eml";
public LayoutComponentsFilter() {
addIdentifierCallbacks(
new StringFilterGroup(
Settings.HIDE_GRAY_SEPARATOR,
"cell_divider"
)
);
addPathCallbacks(
new StringFilterGroup(
Settings.HIDE_HANDLE,
"|CellType|ContainerType|ContainerType|ContainerType|TextType|"
)
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (contentType == FilterContentType.PATH && !path.startsWith(ACCOUNT_HEADER_PATH)) {
return false;
}
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}

View File

@ -0,0 +1,52 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch;
import app.revanced.extension.youtube.settings.Settings;
/**
* Abuse LithoFilter for {@link CustomPlaybackSpeedPatch}.
*/
public final class PlaybackSpeedMenuFilter extends Filter {
/**
* Old litho based speed selection menu.
*/
public static volatile boolean isOldPlaybackSpeedMenuVisible;
/**
* 0.05x speed selection menu.
*/
public static volatile boolean isPlaybackRateSelectorMenuVisible;
private final StringFilterGroup oldPlaybackMenuGroup;
public PlaybackSpeedMenuFilter() {
// 0.05x litho speed menu.
final StringFilterGroup playbackRateSelectorGroup = new StringFilterGroup(
Settings.ENABLE_CUSTOM_PLAYBACK_SPEED,
"playback_rate_selector_menu_sheet.eml-js"
);
// Old litho based speed menu.
oldPlaybackMenuGroup = new StringFilterGroup(
Settings.ENABLE_CUSTOM_PLAYBACK_SPEED,
"playback_speed_sheet_content.eml-js");
addPathCallbacks(playbackRateSelectorGroup, oldPlaybackMenuGroup);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == oldPlaybackMenuGroup) {
isOldPlaybackSpeedMenuVisible = true;
} else {
isPlaybackRateSelectorMenuVisible = true;
}
return false;
}
}

View File

@ -0,0 +1,129 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.patches.components.StringFilterGroupList;
import app.revanced.extension.shared.utils.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
@SuppressWarnings("unused")
public final class PlayerComponentsFilter extends Filter {
private final StringFilterGroupList channelBarGroupList = new StringFilterGroupList();
private final StringFilterGroup channelBar;
private final StringTrieSearch suggestedActionsException = new StringTrieSearch();
private final StringFilterGroup suggestedActions;
public PlayerComponentsFilter() {
suggestedActionsException.addPatterns(
"channel_bar",
"shorts"
);
// The player audio track button does the exact same function as the audio track flyout menu option.
// But if the copy url button is shown, these button clashes and the the audio button does not work.
// Previously this was a setting to show/hide the player button.
// But it was decided it's simpler to always hide this button because:
// - it doesn't work with copy video url feature
// - the button is rare
// - always hiding makes the ReVanced settings simpler and easier to understand
// - nobody is going to notice the redundant button is always hidden
final StringFilterGroup audioTrackButton = new StringFilterGroup(
null,
"multi_feed_icon_button"
);
channelBar = new StringFilterGroup(
null,
"channel_bar_inner"
);
final StringFilterGroup channelWaterMark = new StringFilterGroup(
Settings.HIDE_CHANNEL_WATERMARK,
"featured_channel_watermark_overlay.eml"
);
final StringFilterGroup infoCards = new StringFilterGroup(
Settings.HIDE_INFO_CARDS,
"info_card_teaser_overlay.eml"
);
final StringFilterGroup infoPanel = new StringFilterGroup(
Settings.HIDE_INFO_PANEL,
"compact_banner",
"publisher_transparency_panel",
"single_item_information_panel"
);
final StringFilterGroup liveChat = new StringFilterGroup(
Settings.HIDE_LIVE_CHAT_MESSAGES,
"live_chat_text_message",
"viewer_engagement_message" // message about poll, not poll itself
);
final StringFilterGroup medicalPanel = new StringFilterGroup(
Settings.HIDE_MEDICAL_PANEL,
"emergency_onebox",
"medical_panel"
);
suggestedActions = new StringFilterGroup(
Settings.HIDE_SUGGESTED_ACTION,
"|suggested_action.eml|"
);
final StringFilterGroup timedReactions = new StringFilterGroup(
Settings.HIDE_TIMED_REACTIONS,
"emoji_control_panel",
"timed_reaction"
);
addPathCallbacks(
audioTrackButton,
channelBar,
channelWaterMark,
infoCards,
infoPanel,
liveChat,
medicalPanel,
suggestedActions,
timedReactions
);
final StringFilterGroup joinMembership = new StringFilterGroup(
Settings.HIDE_JOIN_BUTTON,
"compact_sponsor_button",
"|ContainerType|button.eml|"
);
final StringFilterGroup startTrial = new StringFilterGroup(
Settings.HIDE_START_TRIAL_BUTTON,
"channel_purchase_button"
);
channelBarGroupList.addAll(
joinMembership,
startTrial
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == suggestedActions) {
// suggested actions button on shorts and the suggested actions button on video players use the same path builder.
// Check PlayerType to make each setting work independently.
if (suggestedActionsException.matches(path) || PlayerType.getCurrent().isNoneOrHidden()) {
return false;
}
} else if (matchedGroup == channelBar) {
if (!channelBarGroupList.check(path).isFiltered()) {
return false;
}
}
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}

View File

@ -0,0 +1,170 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.utils.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
@SuppressWarnings("unused")
public final class PlayerFlyoutMenuFilter extends Filter {
private final ByteArrayFilterGroupList flyoutFilterGroupList = new ByteArrayFilterGroupList();
private final ByteArrayFilterGroup byteArrayException;
private final StringTrieSearch pathBuilderException = new StringTrieSearch();
private final StringTrieSearch playerFlyoutMenuFooter = new StringTrieSearch();
private final StringFilterGroup playerFlyoutMenu;
private final StringFilterGroup qualityHeader;
public PlayerFlyoutMenuFilter() {
byteArrayException = new ByteArrayFilterGroup(
null,
"quality_sheet"
);
pathBuilderException.addPattern(
"bottom_sheet_list_option"
);
playerFlyoutMenuFooter.addPatterns(
"captions_sheet_content.eml",
"quality_sheet_content.eml"
);
final StringFilterGroup captionsFooter = new StringFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_CAPTIONS_FOOTER,
"|ContainerType|ContainerType|ContainerType|TextType|",
"|divider.eml|"
);
final StringFilterGroup qualityFooter = new StringFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_QUALITY_FOOTER,
"quality_sheet_footer.eml",
"|divider.eml|"
);
qualityHeader = new StringFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_QUALITY_HEADER,
"quality_sheet_header.eml"
);
playerFlyoutMenu = new StringFilterGroup(null, "overflow_menu_item.eml|");
// Using pathFilterGroupList due to new flyout panel(A/B)
addPathCallbacks(
captionsFooter,
qualityFooter,
qualityHeader,
playerFlyoutMenu
);
flyoutFilterGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_AMBIENT,
"yt_outline_screen_light"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_AUDIO_TRACK,
"yt_outline_person_radar"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_CAPTIONS,
"closed_caption"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_HELP,
"yt_outline_question_circle"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_LOCK_SCREEN,
"yt_outline_lock"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_LOOP,
"yt_outline_arrow_repeat_1_"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_MORE,
"yt_outline_info_circle"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_PIP,
"yt_fill_picture_in_picture",
"yt_outline_picture_in_picture"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_PLAYBACK_SPEED,
"yt_outline_play_arrow_half_circle"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_PREMIUM_CONTROLS,
"yt_outline_adjust"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_ADDITIONAL_SETTINGS,
"yt_outline_gear"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_REPORT,
"yt_outline_flag"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_STABLE_VOLUME,
"volume_stable"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_SLEEP_TIMER,
"yt_outline_moon_z_"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_STATS_FOR_NERDS,
"yt_outline_statistics_graph"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_WATCH_IN_VR,
"yt_outline_vr"
),
new ByteArrayFilterGroup(
Settings.HIDE_PLAYER_FLYOUT_MENU_YT_MUSIC,
"yt_outline_open_new"
)
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == playerFlyoutMenu) {
// Overflow menu is always the start of the path.
if (contentIndex != 0) {
return false;
}
// Shorts also use this player flyout panel
if (PlayerType.getCurrent().isNoneOrHidden() || byteArrayException.check(protobufBufferArray).isFiltered()) {
return false;
}
if (flyoutFilterGroupList.check(protobufBufferArray).isFiltered()) {
// Super class handles logging.
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
} else if (matchedGroup == qualityHeader) {
// Quality header is always the start of the path.
if (contentIndex != 0) {
return false;
}
// Super class handles logging.
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
} else {
// Components other than the footer separator are not filtered.
if (pathBuilderException.matches(path) || !playerFlyoutMenuFooter.matches(path)) {
return false;
}
// Super class handles logging.
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
}

View File

@ -0,0 +1,117 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class QuickActionFilter extends Filter {
private static final String QUICK_ACTION_PATH = "quick_actions.eml";
private final StringFilterGroup quickActionRule;
private final StringFilterGroup bufferFilterPathRule;
private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList();
private final StringFilterGroup liveChatReplay;
public QuickActionFilter() {
quickActionRule = new StringFilterGroup(null, QUICK_ACTION_PATH);
addIdentifierCallbacks(quickActionRule);
bufferFilterPathRule = new StringFilterGroup(
null,
"|ContainerType|button.eml|",
"|fullscreen_video_action_button.eml|"
);
liveChatReplay = new StringFilterGroup(
Settings.HIDE_LIVE_CHAT_REPLAY_BUTTON,
"live_chat_ep_entrypoint.eml"
);
addIdentifierCallbacks(liveChatReplay);
addPathCallbacks(
new StringFilterGroup(
Settings.HIDE_QUICK_ACTIONS_LIKE_BUTTON,
"|like_button"
),
new StringFilterGroup(
Settings.HIDE_QUICK_ACTIONS_DISLIKE_BUTTON,
"dislike_button"
),
new StringFilterGroup(
Settings.HIDE_QUICK_ACTIONS_COMMENT_BUTTON,
"comments_entry_point_button"
),
new StringFilterGroup(
Settings.HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON,
"|save_to_playlist_button"
),
new StringFilterGroup(
Settings.HIDE_QUICK_ACTIONS_MORE_BUTTON,
"|overflow_menu_button"
),
new StringFilterGroup(
Settings.HIDE_RELATED_VIDEO_OVERLAY,
"fullscreen_related_videos"
),
bufferFilterPathRule
);
bufferButtonsGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_QUICK_ACTIONS_COMMENT_BUTTON,
"yt_outline_message_bubble_right"
),
new ByteArrayFilterGroup(
Settings.HIDE_QUICK_ACTIONS_LIVE_CHAT_BUTTON,
"yt_outline_message_bubble_overlap"
),
new ByteArrayFilterGroup(
Settings.HIDE_QUICK_ACTIONS_OPEN_MIX_PLAYLIST_BUTTON,
"yt_outline_youtube_mix"
),
new ByteArrayFilterGroup(
Settings.HIDE_QUICK_ACTIONS_OPEN_PLAYLIST_BUTTON,
"yt_outline_list_play_arrow"
),
new ByteArrayFilterGroup(
Settings.HIDE_QUICK_ACTIONS_SHARE_BUTTON,
"yt_outline_share"
)
);
}
private boolean isEveryFilterGroupEnabled() {
for (StringFilterGroup group : pathCallbacks)
if (!group.isEnabled()) return false;
for (ByteArrayFilterGroup group : bufferButtonsGroupList)
if (!group.isEnabled()) return false;
return true;
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == liveChatReplay) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
if (!path.startsWith(QUICK_ACTION_PATH)) {
return false;
}
if (matchedGroup == quickActionRule && !isEveryFilterGroupEnabled()) {
return false;
}
if (matchedGroup == bufferFilterPathRule) {
return bufferButtonsGroupList.check(protobufBufferArray).isFiltered();
}
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}

View File

@ -0,0 +1,55 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import java.util.concurrent.atomic.AtomicBoolean;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.shared.PlayerType;
/**
* Here is an unintended behavior:
* <p>
* 1. The user does not hide Shorts in the Subscriptions tab, but hides them otherwise.
* 2. Goes to the Subscriptions tab and scrolls to where Shorts is.
* 3. Opens a regular video.
* 4. Minimizes the video and turns off the screen.
* 5. Turns the screen on and maximizes the video.
* 6. Shorts belonging to related videos are not hidden.
* <p>
* Here is an explanation of this special issue:
* <p>
* When the user minimizes the video, turns off the screen, and then turns it back on,
* the components below the player are reloaded, and at this moment the PlayerType is [WATCH_WHILE_MINIMIZED].
* (Shorts belonging to related videos are also reloaded)
* Since the PlayerType is [WATCH_WHILE_MINIMIZED] at this moment, the navigation tab is checked.
* (Even though PlayerType is [WATCH_WHILE_MINIMIZED], this is a Shorts belonging to a related video)
* <p>
* As a workaround for this special issue, if a video actionbar is detected, which is one of the components below the player,
* it is treated as being in the same state as [WATCH_WHILE_MAXIMIZED].
*/
public final class RelatedVideoFilter extends Filter {
// Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread.
public static final AtomicBoolean isActionBarVisible = new AtomicBoolean(false);
public RelatedVideoFilter() {
addIdentifierCallbacks(
new StringFilterGroup(
null,
"video_action_bar.eml"
)
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED &&
isActionBarVisible.compareAndSet(false, true))
Utils.runOnMainThreadDelayed(() -> isActionBarVisible.compareAndSet(true, false), 750);
return false;
}
}

View File

@ -0,0 +1,105 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import java.net.URLDecoder;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.patches.utils.ReturnYouTubeChannelNamePatch;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"unused", "CharsetObjectCanBeUsed"})
public final class ReturnYouTubeChannelNameFilterPatch extends Filter {
private static final String DELIMITING_CHARACTER = "";
private static final String CHANNEL_ID_IDENTIFIER_CHARACTER = "UC";
private static final String CHANNEL_ID_IDENTIFIER_WITH_DELIMITING_CHARACTER =
DELIMITING_CHARACTER + CHANNEL_ID_IDENTIFIER_CHARACTER;
private static final String HANDLE_IDENTIFIER_CHARACTER = "@";
private static final String HANDLE_IDENTIFIER_WITH_DELIMITING_CHARACTER =
HANDLE_IDENTIFIER_CHARACTER + CHANNEL_ID_IDENTIFIER_CHARACTER;
private final ByteArrayFilterGroupList shortsChannelBarAvatarFilterGroup = new ByteArrayFilterGroupList();
public ReturnYouTubeChannelNameFilterPatch() {
addPathCallbacks(
new StringFilterGroup(Settings.REPLACE_CHANNEL_HANDLE, "|reel_channel_bar_inner.eml|")
);
shortsChannelBarAvatarFilterGroup.addAll(
new ByteArrayFilterGroup(Settings.REPLACE_CHANNEL_HANDLE, "/@")
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (shortsChannelBarAvatarFilterGroup.check(protobufBufferArray).isFiltered()) {
setLastShortsChannelId(protobufBufferArray);
}
return false;
}
private void setLastShortsChannelId(byte[] protobufBufferArray) {
try {
String[] splitArr;
final String bufferString = findAsciiStrings(protobufBufferArray);
splitArr = bufferString.split(CHANNEL_ID_IDENTIFIER_WITH_DELIMITING_CHARACTER);
if (splitArr.length < 2) {
return;
}
final String splitedBufferString = CHANNEL_ID_IDENTIFIER_CHARACTER + splitArr[1];
splitArr = splitedBufferString.split(HANDLE_IDENTIFIER_WITH_DELIMITING_CHARACTER);
if (splitArr.length < 2) {
return;
}
splitArr = splitArr[1].split(DELIMITING_CHARACTER);
if (splitArr.length < 1) {
return;
}
final String cachedHandle = HANDLE_IDENTIFIER_CHARACTER + splitArr[0];
splitArr = splitedBufferString.split(DELIMITING_CHARACTER);
if (splitArr.length < 1) {
return;
}
final String channelId = splitArr[0].replaceAll("\"", "").trim();
final String handle = URLDecoder.decode(cachedHandle, "UTF-8").trim();
ReturnYouTubeChannelNamePatch.setLastShortsChannelId(handle, channelId);
} catch (Exception ex) {
Logger.printException(() -> "setLastShortsChannelId failed", ex);
}
}
private String findAsciiStrings(byte[] buffer) {
StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
builder.append("");
// Valid ASCII values (ignore control characters).
final int minimumAscii = 32; // 32 = space character
final int maximumAscii = 126; // 127 = delete character
final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include.
String delimitingCharacter = ""; // Non ascii character, to allow easier log filtering.
final int length = buffer.length;
int start = 0;
int end = 0;
while (end < length) {
int value = buffer[end];
if (value < minimumAscii || value > maximumAscii || end == length - 1) {
if (end - start >= minimumAsciiStringLength) {
for (int i = start; i < end; i++) {
builder.append((char) buffer[i]);
}
builder.append(delimitingCharacter);
}
start = end + 1;
}
end++;
}
return builder.toString();
}
}

View File

@ -0,0 +1,171 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.FilterGroup;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.TrieSearch;
import app.revanced.extension.youtube.patches.utils.ReturnYouTubeDislikePatch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.VideoInformation;
/**
* @noinspection ALL
* <p>
* Searches for video id's in the proto buffer of Shorts dislike.
* <p>
* Because multiple litho dislike spans are created in the background
* (and also anytime litho refreshes the components, which is somewhat arbitrary),
* that makes the value of {@link VideoInformation#getVideoId()} and {@link VideoInformation#getPlayerResponseVideoId()}
* unreliable to determine which video id a Shorts litho span belongs to.
* <p>
* But the correct video id does appear in the protobuffer just before a Shorts litho span is created.
* <p>
* Once a way to asynchronously update litho text is found, this strategy will no longer be needed.
*/
public final class ReturnYouTubeDislikeFilterPatch extends Filter {
/**
* Last unique video id's loaded. Value is ignored and Map is treated as a Set.
* Cannot use {@link LinkedHashSet} because it's missing #removeEldestEntry().
*/
@GuardedBy("itself")
private static final Map<String, Boolean> lastVideoIds = new LinkedHashMap<>() {
/**
* Number of video id's to keep track of for searching thru the buffer.
* A minimum value of 3 should be sufficient, but check a few more just in case.
*/
private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 5;
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK;
}
};
private final ByteArrayFilterGroupList videoIdFilterGroup = new ByteArrayFilterGroupList();
public ReturnYouTubeDislikeFilterPatch() {
// When a new Short is opened, the like buttons always seem to load before the dislike.
// But if swiping back to a previous video and liking/disliking, then only that single button reloads.
// So must check for both buttons.
addPathCallbacks(
new StringFilterGroup(null, "|shorts_like_button.eml"),
new StringFilterGroup(null, "|shorts_dislike_button.eml")
);
// After the likes icon name is some binary data and then the video id for that specific short.
videoIdFilterGroup.addAll(
// on_shadowed = Video was previously like/disliked before opening.
// off_shadowed = Video was not previously liked/disliked before opening.
new ByteArrayFilterGroup(null, "ic_right_like_on_shadowed"),
new ByteArrayFilterGroup(null, "ic_right_like_off_shadowed"),
new ByteArrayFilterGroup(null, "ic_right_dislike_on_shadowed"),
new ByteArrayFilterGroup(null, "ic_right_dislike_off_shadowed")
);
}
private volatile static String shortsVideoId = "";
public static String getShortsVideoId() {
return shortsVideoId;
}
/**
* Injection point.
*/
public static void newShortsVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
if (!Settings.RYD_SHORTS.get()) {
return;
}
if (shortsVideoId.equals(newlyLoadedVideoId)) {
return;
}
Logger.printDebug(() -> "newShortsVideoStarted: " + newlyLoadedVideoId);
shortsVideoId = newlyLoadedVideoId;
}
/**
* Injection point.
*/
public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) {
try {
if (!isShortAndOpeningOrPlaying || !Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
return;
}
synchronized (lastVideoIds) {
if (lastVideoIds.put(videoId, Boolean.TRUE) == null) {
Logger.printDebug(() -> "New Short video id: " + videoId);
}
}
} catch (Exception ex) {
Logger.printException(() -> "newPlayerResponseVideoId failure", ex);
}
}
/**
* This could use {@link TrieSearch}, but since the patterns are constantly changing
* the overhead of updating the Trie might negate the search performance gain.
*/
private static boolean byteArrayContainsString(@NonNull byte[] array, @NonNull String text) {
for (int i = 0, lastArrayStartIndex = array.length - text.length(); i <= lastArrayStartIndex; i++) {
boolean found = true;
for (int j = 0, textLength = text.length(); j < textLength; j++) {
if (array[i + j] != (byte) text.charAt(j)) {
found = false;
break;
}
}
if (found) {
return true;
}
}
return false;
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
return false;
}
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray);
if (result.isFiltered()) {
String matchedVideoId = findVideoId(protobufBufferArray);
// Matched video will be null if in incognito mode.
// Must pass a null id to correctly clear out the current video data.
// Otherwise if a Short is opened in non-incognito, then incognito is enabled and another Short is opened,
// the new incognito Short will show the old prior data.
ReturnYouTubeDislikePatch.setLastLithoShortsVideoId(matchedVideoId);
}
return false;
}
@Nullable
private String findVideoId(byte[] protobufBufferArray) {
synchronized (lastVideoIds) {
for (String videoId : lastVideoIds.keySet()) {
if (byteArrayContainsString(protobufBufferArray, videoId)) {
return videoId;
}
}
return null;
}
}
}

View File

@ -0,0 +1,33 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.youtube.patches.misc.ShareSheetPatch;
import app.revanced.extension.youtube.settings.Settings;
/**
* Abuse LithoFilter for {@link ShareSheetPatch}.
*/
public final class ShareSheetMenuFilter extends Filter {
// Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread.
public static volatile boolean isShareSheetMenuVisible;
public ShareSheetMenuFilter() {
addIdentifierCallbacks(
new StringFilterGroup(
Settings.CHANGE_SHARE_SHEET,
"share_sheet_container.eml"
)
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
isShareSheetMenuVisible = true;
return false;
}
}

View File

@ -0,0 +1,274 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class ShortsButtonFilter extends Filter {
private final static String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml";
private final static String REEL_LIVE_HEADER_PATH = "immersive_live_header.eml";
/**
* For paid promotion label and subscribe button that appears in the channel bar.
*/
private final static String REEL_METAPANEL_PATH = "reel_metapanel.eml";
private final static String SHORTS_PAUSED_STATE_BUTTON_PATH = "|ScrollableContainerType|ContainerType|button.eml|";
private final StringFilterGroup subscribeButton;
private final StringFilterGroup joinButton;
private final StringFilterGroup pausedOverlayButtons;
private final StringFilterGroup metaPanelButton;
private final ByteArrayFilterGroupList pausedOverlayButtonsGroupList = new ByteArrayFilterGroupList();
private final StringFilterGroup suggestedAction;
private final ByteArrayFilterGroupList suggestedActionsGroupList = new ByteArrayFilterGroupList();
private final StringFilterGroup actionBar;
private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList();
private final ByteArrayFilterGroup useThisSoundButton = new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_USE_THIS_SOUND_BUTTON,
"yt_outline_camera"
);
public ShortsButtonFilter() {
StringFilterGroup floatingButton = new StringFilterGroup(
Settings.HIDE_SHORTS_FLOATING_BUTTON,
"floating_action_button"
);
addIdentifierCallbacks(floatingButton);
pausedOverlayButtons = new StringFilterGroup(
null,
"shorts_paused_state"
);
StringFilterGroup channelBar = new StringFilterGroup(
Settings.HIDE_SHORTS_CHANNEL_BAR,
REEL_CHANNEL_BAR_PATH
);
StringFilterGroup fullVideoLinkLabel = new StringFilterGroup(
Settings.HIDE_SHORTS_FULL_VIDEO_LINK_LABEL,
"reel_multi_format_link"
);
StringFilterGroup videoTitle = new StringFilterGroup(
Settings.HIDE_SHORTS_VIDEO_TITLE,
"shorts_video_title_item"
);
StringFilterGroup reelSoundMetadata = new StringFilterGroup(
Settings.HIDE_SHORTS_SOUND_METADATA_LABEL,
"reel_sound_metadata"
);
StringFilterGroup infoPanel = new StringFilterGroup(
Settings.HIDE_SHORTS_INFO_PANEL,
"shorts_info_panel_overview"
);
StringFilterGroup stickers = new StringFilterGroup(
Settings.HIDE_SHORTS_STICKERS,
"stickers_layer.eml"
);
StringFilterGroup liveHeader = new StringFilterGroup(
Settings.HIDE_SHORTS_LIVE_HEADER,
"immersive_live_header"
);
StringFilterGroup paidPromotionButton = new StringFilterGroup(
Settings.HIDE_SHORTS_PAID_PROMOTION_LABEL,
"reel_player_disclosure.eml"
);
metaPanelButton = new StringFilterGroup(
null,
"|ContainerType|button.eml|"
);
joinButton = new StringFilterGroup(
Settings.HIDE_SHORTS_JOIN_BUTTON,
"sponsor_button"
);
subscribeButton = new StringFilterGroup(
Settings.HIDE_SHORTS_SUBSCRIBE_BUTTON,
"subscribe_button"
);
actionBar = new StringFilterGroup(
null,
"shorts_action_bar"
);
suggestedAction = new StringFilterGroup(
null,
"|suggested_action_inner.eml|"
);
addPathCallbacks(
suggestedAction, actionBar, joinButton, subscribeButton, metaPanelButton,
paidPromotionButton, pausedOverlayButtons, channelBar, fullVideoLinkLabel,
videoTitle, reelSoundMetadata, infoPanel, liveHeader, stickers
);
//
// Action buttons
//
videoActionButtonGroupList.addAll(
// This also appears as the path item 'shorts_like_button.eml'
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_LIKE_BUTTON,
"reel_like_button",
"reel_like_toggled_button"
),
// This also appears as the path item 'shorts_dislike_button.eml'
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_DISLIKE_BUTTON,
"reel_dislike_button",
"reel_dislike_toggled_button"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
"reel_comment_button"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SHARE_BUTTON,
"reel_share_button"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_REMIX_BUTTON,
"reel_remix_button"
),
new ByteArrayFilterGroup(
Settings.DISABLE_SHORTS_LIKE_BUTTON_FOUNTAIN_ANIMATION,
"shorts_like_fountain"
)
);
//
// Paused overlay buttons.
//
pausedOverlayButtonsGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_TRENDS_BUTTON,
"yt_outline_fire_"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SHOPPING_BUTTON,
"yt_outline_bag_"
)
);
//
// Suggested actions.
//
suggestedActionsGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_TAGGED_PRODUCTS,
// Product buttons show pictures of the products, and does not have any unique icons to identify.
// Instead use a unique identifier found in the buffer.
"PAproduct_listZ"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SHOP_BUTTON,
"yt_outline_bag_"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_LOCATION_BUTTON,
"yt_outline_location_point_"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SAVE_MUSIC_BUTTON,
"yt_outline_list_add_",
"yt_outline_bookmark_"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SEARCH_SUGGESTIONS_BUTTON,
"yt_outline_search_"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SUPER_THANKS_BUTTON,
"yt_outline_dollar_sign_heart_"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_USE_TEMPLATE_BUTTON,
"yt_outline_template_add"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_GREEN_SCREEN_BUTTON,
"shorts_green_screen"
),
useThisSoundButton
);
}
private boolean isEverySuggestedActionFilterEnabled() {
for (ByteArrayFilterGroup group : suggestedActionsGroupList)
if (!group.isEnabled()) return false;
return true;
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == subscribeButton || matchedGroup == joinButton) {
// Selectively filter to avoid false positive filtering of other subscribe/join buttons.
if (StringUtils.startsWithAny(path, REEL_CHANNEL_BAR_PATH, REEL_LIVE_HEADER_PATH, REEL_METAPANEL_PATH)) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
if (matchedGroup == metaPanelButton) {
if (path.startsWith(REEL_METAPANEL_PATH) && useThisSoundButton.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
// Video action buttons (like, dislike, comment, share, remix) have the same path.
if (matchedGroup == actionBar) {
if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
if (matchedGroup == suggestedAction) {
if (isEverySuggestedActionFilterEnabled()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
// Suggested actions can be at the start or in the middle of a path.
if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
if (matchedGroup == pausedOverlayButtons) {
if (Settings.HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS.get()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
} else if (StringUtils.contains(path, SHORTS_PAUSED_STATE_BUTTON_PATH)) {
if (pausedOverlayButtonsGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}
return false;
}
// Super class handles logging.
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}

View File

@ -0,0 +1,188 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
import app.revanced.extension.youtube.shared.RootView;
@SuppressWarnings("unused")
public final class ShortsShelfFilter extends Filter {
private static final String BROWSE_ID_HISTORY = "FEhistory";
private static final String BROWSE_ID_LIBRARY = "FElibrary";
private static final String BROWSE_ID_NOTIFICATION_INBOX = "FEnotifications_inbox";
private static final String BROWSE_ID_SUBSCRIPTIONS = "FEsubscriptions";
private static final String CONVERSATION_CONTEXT_FEED_IDENTIFIER =
"horizontalCollectionSwipeProtector=null";
private static final String SHELF_HEADER_PATH = "shelf_header.eml";
private final StringFilterGroup channelProfile;
private final StringFilterGroup compactFeedVideoPath;
private final ByteArrayFilterGroup compactFeedVideoBuffer;
private final StringFilterGroup shelfHeaderIdentifier;
private final StringFilterGroup shelfHeaderPath;
private static final StringTrieSearch feedGroup = new StringTrieSearch();
private static final BooleanSetting hideShortsShelf = Settings.HIDE_SHORTS_SHELF;
private static final BooleanSetting hideChannel = Settings.HIDE_SHORTS_SHELF_CHANNEL;
private static final ByteArrayFilterGroup channelProfileShelfHeader =
new ByteArrayFilterGroup(
hideChannel,
"Shorts"
);
public ShortsShelfFilter() {
feedGroup.addPattern(CONVERSATION_CONTEXT_FEED_IDENTIFIER);
channelProfile = new StringFilterGroup(
hideChannel,
"shorts_pivot_item"
);
final StringFilterGroup shortsIdentifiers = new StringFilterGroup(
hideShortsShelf,
"shorts_shelf",
"inline_shorts",
"shorts_grid",
"shorts_video_cell"
);
shelfHeaderIdentifier = new StringFilterGroup(
hideShortsShelf,
SHELF_HEADER_PATH
);
addIdentifierCallbacks(channelProfile, shortsIdentifiers, shelfHeaderIdentifier);
compactFeedVideoPath = new StringFilterGroup(
hideShortsShelf,
// Shorts that appear in the feed/search when the device is using tablet layout.
"compact_video.eml",
// 'video_lockup_with_attachment.eml' is used instead of 'compact_video.eml' for some users. (A/B tests)
"video_lockup_with_attachment.eml",
// Search results that appear in a horizontal shelf.
"video_card.eml"
);
// Filter out items that use the 'frame0' thumbnail.
// This is a valid thumbnail for both regular videos and Shorts,
// but it appears these thumbnails are used only for Shorts.
compactFeedVideoBuffer = new ByteArrayFilterGroup(
hideShortsShelf,
"/frame0.jpg"
);
// Feed Shorts shelf header.
// Use a different filter group for this pattern, as it requires an additional check after matching.
shelfHeaderPath = new StringFilterGroup(
hideShortsShelf,
SHELF_HEADER_PATH
);
addPathCallbacks(compactFeedVideoPath, shelfHeaderPath);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
final boolean playerActive = RootView.isPlayerActive();
final boolean searchBarActive = RootView.isSearchBarActive();
final NavigationButton navigationButton = NavigationButton.getSelectedNavigationButton();
final String navigation = navigationButton == null ? "null" : navigationButton.name();
final String browseId = RootView.getBrowseId();
final boolean hideShelves = shouldHideShortsFeedItems(playerActive, searchBarActive, navigationButton, browseId);
Logger.printDebug(() -> "hideShelves: " + hideShelves + "\nplayerActive: " + playerActive + "\nsearchBarActive: " + searchBarActive + "\nbrowseId: " + browseId + "\nnavigation: " + navigation);
if (contentType == FilterContentType.PATH) {
if (matchedGroup == compactFeedVideoPath) {
if (hideShelves && compactFeedVideoBuffer.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
} else if (matchedGroup == shelfHeaderPath) {
// Because the header is used in watch history and possibly other places, check for the index,
// which is 0 when the shelf header is used for Shorts.
if (contentIndex != 0) {
return false;
}
if (!channelProfileShelfHeader.check(protobufBufferArray).isFiltered()) {
return false;
}
if (feedGroup.matches(allValue)) {
return false;
}
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
} else if (contentType == FilterContentType.IDENTIFIER) {
// Feed/search identifier components.
if (matchedGroup == shelfHeaderIdentifier) {
// Check ConversationContext to not hide shelf header in channel profile
// This value does not exist in the shelf header in the channel profile
if (!feedGroup.matches(allValue)) {
return false;
}
} else if (matchedGroup == channelProfile) {
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
if (!hideShelves) {
return false;
}
}
// Super class handles logging.
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
private static boolean shouldHideShortsFeedItems(boolean playerActive, boolean searchBarActive, NavigationButton selectedNavButton, String browseId) {
final boolean hideHomeAndRelatedVideos = Settings.HIDE_SHORTS_SHELF_HOME_RELATED_VIDEOS.get();
final boolean hideSubscriptions = Settings.HIDE_SHORTS_SHELF_SUBSCRIPTIONS.get();
final boolean hideSearch = Settings.HIDE_SHORTS_SHELF_SEARCH.get();
if (hideHomeAndRelatedVideos && hideSubscriptions && hideSearch) {
// Shorts suggestions can load in the background if a video is opened and
// then immediately minimized before any suggestions are loaded.
// In this state the player type will show minimized, which makes it not possible to
// distinguish between Shorts suggestions loading in the player and between
// scrolling thru search/home/subscription tabs while a player is minimized.
//
// To avoid this situation for users that never want to show Shorts (all hide Shorts options are enabled)
// then hide all Shorts everywhere including the Library history and Library playlists.
return true;
}
// Must check player type first, as search bar can be active behind the player.
if (playerActive) {
// For now, consider the under video results the same as the home feed.
return hideHomeAndRelatedVideos;
}
// Must check second, as search can be from any tab.
if (searchBarActive) {
return hideSearch;
}
// Avoid checking navigation button status if all other Shorts should show.
if (!hideHomeAndRelatedVideos && !hideSubscriptions) {
return false;
}
if (selectedNavButton == null) {
return hideHomeAndRelatedVideos; // Unknown tab, treat the same as home.
}
switch (browseId) {
case BROWSE_ID_HISTORY, BROWSE_ID_LIBRARY, BROWSE_ID_NOTIFICATION_INBOX -> {
return false;
}
case BROWSE_ID_SUBSCRIPTIONS -> {
return hideSubscriptions;
}
default -> {
return hideHomeAndRelatedVideos;
}
}
}
}

View File

@ -0,0 +1,33 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.youtube.patches.video.RestoreOldVideoQualityMenuPatch;
import app.revanced.extension.youtube.settings.Settings;
/**
* Abuse LithoFilter for {@link RestoreOldVideoQualityMenuPatch}.
*/
public final class VideoQualityMenuFilter extends Filter {
// Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread.
public static volatile boolean isVideoQualityMenuVisible;
public VideoQualityMenuFilter() {
addPathCallbacks(
new StringFilterGroup(
Settings.RESTORE_OLD_VIDEO_QUALITY_MENU,
"quick_quality_sheet_content.eml-js"
)
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
isVideoQualityMenuVisible = true;
return false;
}
}

View File

@ -0,0 +1,221 @@
package app.revanced.extension.youtube.patches.feed;
import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition;
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class FeedPatch {
// region [Hide feed components] patch
public static int hideCategoryBarInFeed(final int height) {
return Settings.HIDE_CATEGORY_BAR_IN_FEED.get() ? 0 : height;
}
public static void hideCategoryBarInRelatedVideos(final View chipView) {
Utils.hideViewBy0dpUnderCondition(
Settings.HIDE_CATEGORY_BAR_IN_RELATED_VIDEOS.get() || Settings.HIDE_RELATED_VIDEOS.get(),
chipView
);
}
public static int hideCategoryBarInSearch(final int height) {
return Settings.HIDE_CATEGORY_BAR_IN_SEARCH.get() ? 0 : height;
}
/**
* Rather than simply hiding the channel tab view, completely removes channel tab from list.
* If a channel tab is removed from the list, users will not be able to open it by swiping.
*
* @param channelTabText Text to be assigned to channel tab, such as 'Shorts', 'Playlists', 'Community', 'Store'.
* This text is hardcoded, so it follows the user's language.
* @return Whether to remove the channel tab from the list.
*/
public static boolean hideChannelTab(String channelTabText) {
if (!Settings.HIDE_CHANNEL_TAB.get()) {
return false;
}
if (channelTabText == null || channelTabText.isEmpty()) {
return false;
}
String[] blockList = Settings.HIDE_CHANNEL_TAB_FILTER_STRINGS.get().split("\\n");
for (String filter : blockList) {
if (!filter.isEmpty() && channelTabText.equals(filter)) {
return true;
}
}
return false;
}
public static void hideBreakingNewsShelf(View view) {
hideViewBy0dpUnderCondition(
Settings.HIDE_CAROUSEL_SHELF.get(),
view
);
}
public static View hideCaptionsButton(View view) {
return Settings.HIDE_FEED_CAPTIONS_BUTTON.get() ? null : view;
}
public static void hideCaptionsButtonContainer(View view) {
hideViewUnderCondition(
Settings.HIDE_FEED_CAPTIONS_BUTTON,
view
);
}
public static boolean hideFloatingButton() {
return Settings.HIDE_FLOATING_BUTTON.get();
}
public static void hideLatestVideosButton(View view) {
hideViewUnderCondition(Settings.HIDE_LATEST_VIDEOS_BUTTON.get(), view);
}
public static boolean hideSubscriptionsChannelSection() {
return Settings.HIDE_SUBSCRIPTIONS_CAROUSEL.get();
}
public static void hideSubscriptionsChannelSection(View view) {
hideViewUnderCondition(Settings.HIDE_SUBSCRIPTIONS_CAROUSEL, view);
}
private static FrameLayout.LayoutParams layoutParams;
private static int minimumHeight = -1;
private static int paddingLeft = 12;
private static int paddingTop = 0;
private static int paddingRight = 12;
private static int paddingBottom = 0;
/**
* expandButtonContainer is used in channel profiles as well as search results.
* We need to hide expandButtonContainer only in search results, not in channel profile.
* <p>
* If we hide expandButtonContainer with setVisibility, the empty space occupied by expandButtonContainer will still be left.
* Therefore, we need to dynamically resize the View with LayoutParams.
* <p>
* Unlike other Views, expandButtonContainer cannot make a View invisible using the normal {@link Utils#hideViewByLayoutParams} method.
* We should set the parent view's padding and MinimumHeight to 0 to completely hide the expandButtonContainer.
*
* @param parentView Parent view of expandButtonContainer.
*/
public static void hideShowMoreButton(View parentView) {
if (!Settings.HIDE_SHOW_MORE_BUTTON.get())
return;
if (!(parentView instanceof ViewGroup viewGroup))
return;
if (!(viewGroup.getChildAt(0) instanceof ViewGroup expandButtonContainer))
return;
if (layoutParams == null) {
// We need to get the original LayoutParams and paddings applied to expandButtonContainer.
// Theses are used to make the expandButtonContainer visible again.
if (expandButtonContainer.getLayoutParams() instanceof FrameLayout.LayoutParams lp) {
layoutParams = lp;
paddingLeft = parentView.getPaddingLeft();
paddingTop = parentView.getPaddingTop();
paddingRight = parentView.getPaddingRight();
paddingBottom = parentView.getPaddingBottom();
}
}
// I'm not sure if 'Utils.runOnMainThreadDelayed' is absolutely necessary.
Utils.runOnMainThreadDelayed(() -> {
// MinimumHeight is also needed to make expandButtonContainer visible again.
// Get original MinimumHeight.
if (minimumHeight == -1) {
minimumHeight = parentView.getMinimumHeight();
}
// In the search results, the child view structure of expandButtonContainer is as follows:
// expandButtonContainer
// L TextView (first child view is SHOWN, 'Show more' text)
// L ImageView (second child view is shown, dropdown arrow icon)
// In the channel profiles, the child view structure of expandButtonContainer is as follows:
// expandButtonContainer
// L TextView (first child view is HIDDEN, 'Show more' text)
// L ImageView (second child view is shown, dropdown arrow icon)
if (expandButtonContainer.getChildAt(0).getVisibility() != View.VISIBLE && layoutParams != null) {
// If the first child view (TextView) is HIDDEN, the channel profile is open.
// Restore parent view's padding and MinimumHeight to make them visible.
parentView.setMinimumHeight(minimumHeight);
parentView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
expandButtonContainer.setLayoutParams(layoutParams);
} else {
// If the first child view (TextView) is SHOWN, the search results is open.
// Set the parent view's padding and MinimumHeight to 0 to completely hide the expandButtonContainer.
parentView.setMinimumHeight(0);
parentView.setPadding(0, 0, 0, 0);
expandButtonContainer.setLayoutParams(new FrameLayout.LayoutParams(0, 0));
}
}, 0
);
}
// endregion
// region [Hide feed flyout menu] patch
/**
* hide feed flyout menu for phone
*
* @param menuTitleCharSequence menu title
*/
@Nullable
public static CharSequence hideFlyoutMenu(@Nullable CharSequence menuTitleCharSequence) {
if (menuTitleCharSequence != null && Settings.HIDE_FEED_FLYOUT_MENU.get()) {
String[] blockList = Settings.HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS.get().split("\\n");
String menuTitleString = menuTitleCharSequence.toString();
for (String filter : blockList) {
if (menuTitleString.equals(filter) && !filter.isEmpty())
return null;
}
}
return menuTitleCharSequence;
}
/**
* hide feed flyout panel for tablet
*
* @param menuTextView flyout text view
* @param menuTitleCharSequence raw text
*/
public static void hideFlyoutMenu(TextView menuTextView, CharSequence menuTitleCharSequence) {
if (menuTitleCharSequence == null || !Settings.HIDE_FEED_FLYOUT_MENU.get())
return;
if (!(menuTextView.getParent() instanceof View parentView))
return;
String[] blockList = Settings.HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS.get().split("\\n");
String menuTitleString = menuTitleCharSequence.toString();
for (String filter : blockList) {
if (menuTitleString.equals(filter) && !filter.isEmpty())
Utils.hideViewByLayoutParams(parentView);
}
}
// endregion
}

View File

@ -0,0 +1,49 @@
package app.revanced.extension.youtube.patches.feed;
import androidx.annotation.Nullable;
import java.util.concurrent.atomic.AtomicBoolean;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.BottomSheetState;
import app.revanced.extension.youtube.shared.RootView;
@SuppressWarnings("unused")
public final class RelatedVideoPatch {
private static final boolean HIDE_RELATED_VIDEOS = Settings.HIDE_RELATED_VIDEOS.get();
private static final int OFFSET = Settings.RELATED_VIDEOS_OFFSET.get();
// video title,channel bar, video action bar, comment
private static final int MAX_ITEM_COUNT = 4 + OFFSET;
private static final AtomicBoolean engagementPanelOpen = new AtomicBoolean(false);
public static void showEngagementPanel(@Nullable Object object) {
engagementPanelOpen.set(object != null);
}
public static void hideEngagementPanel() {
engagementPanelOpen.compareAndSet(true, false);
}
public static int overrideItemCounts(int itemCounts) {
if (!HIDE_RELATED_VIDEOS) {
return itemCounts;
}
if (itemCounts < MAX_ITEM_COUNT) {
return itemCounts;
}
if (!RootView.isPlayerActive()) {
return itemCounts;
}
if (BottomSheetState.getCurrent().isOpen()) {
return itemCounts;
}
if (engagementPanelOpen.get()) {
return itemCounts;
}
return MAX_ITEM_COUNT;
}
}

View File

@ -0,0 +1,134 @@
package app.revanced.extension.youtube.patches.general;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class ChangeStartPagePatch {
public enum StartPage {
/**
* Unmodified type, and same as un-patched.
*/
ORIGINAL("", null),
/**
* Browse id.
*/
BROWSE("FEguide_builder", TRUE),
EXPLORE("FEexplore", TRUE),
HISTORY("FEhistory", TRUE),
LIBRARY("FElibrary", TRUE),
MOVIE("FEstorefront", TRUE),
SUBSCRIPTIONS("FEsubscriptions", TRUE),
TRENDING("FEtrending", TRUE),
/**
* Channel id, this can be used as a browseId.
*/
GAMING("UCOpNcN46UbXVtpKMrmU4Abg", TRUE),
LIVE("UC4R8DWoMoI7CAwX8_LjQHig", TRUE),
MUSIC("UC-9-kyTW8ZkZNDHQJ6FgpwQ", TRUE),
SPORTS("UCEgdi0XIXXZ-qJOFPf4JSKw", TRUE),
/**
* Playlist id, this can be used as a browseId.
*/
LIKED_VIDEO("VLLL", TRUE),
WATCH_LATER("VLWL", TRUE),
/**
* Intent action.
*/
SEARCH("com.google.android.youtube.action.open.search", FALSE),
SHORTS("com.google.android.youtube.action.open.shorts", FALSE);
@Nullable
final Boolean isBrowseId;
@NonNull
final String id;
StartPage(@NonNull String id, @Nullable Boolean isBrowseId) {
this.id = id;
this.isBrowseId = isBrowseId;
}
private boolean isBrowseId() {
return BooleanUtils.isTrue(isBrowseId);
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean isIntentAction() {
return BooleanUtils.isFalse(isBrowseId);
}
}
/**
* Intent action when YouTube is cold started from the launcher.
* <p>
* If you don't check this, the hooking will also apply in the following cases:
* Case 1. The user clicked Shorts button on the YouTube shortcut.
* Case 2. The user clicked Shorts button on the YouTube widget.
* In this case, instead of opening Shorts, the start page specified by the user is opened.
*/
private static final String ACTION_MAIN = "android.intent.action.MAIN";
private static final StartPage START_PAGE = Settings.CHANGE_START_PAGE.get();
private static final boolean ALWAYS_CHANGE_START_PAGE = Settings.CHANGE_START_PAGE_TYPE.get();
/**
* There is an issue where the back button on the toolbar doesn't work properly.
* As a workaround for this issue, instead of overriding the browserId multiple times, just override it once.
*/
private static boolean appLaunched = false;
public static String overrideBrowseId(@NonNull String original) {
if (!START_PAGE.isBrowseId()) {
return original;
}
if (!ALWAYS_CHANGE_START_PAGE && appLaunched) {
Logger.printDebug(() -> "Ignore override browseId as the app already launched");
return original;
}
appLaunched = true;
final String browseId = START_PAGE.id;
Logger.printDebug(() -> "Changing browseId to " + browseId);
return browseId;
}
public static void overrideIntentAction(@NonNull Intent intent) {
if (!START_PAGE.isIntentAction()) {
return;
}
if (!StringUtils.equals(intent.getAction(), ACTION_MAIN)) {
Logger.printDebug(() -> "Ignore override intent action" +
" as the current activity is not the entry point of the application");
return;
}
final String intentAction = START_PAGE.id;
Logger.printDebug(() -> "Changing intent action to " + intentAction);
intent.setAction(intentAction);
}
public static final class ChangeStartPageTypeAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return Settings.CHANGE_START_PAGE.get() != StartPage.ORIGINAL;
}
}
}

View File

@ -0,0 +1,98 @@
package app.revanced.extension.youtube.patches.general;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
public final class DownloadActionsPatch extends VideoUtils {
private static final BooleanSetting overrideVideoDownloadButton =
Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON;
private static final BooleanSetting overridePlaylistDownloadButton =
Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON;
/**
* Injection point.
* <p>
* Called from the in app download hook,
* for both the player action button (below the video)
* and the 'Download video' flyout option for feed videos.
* <p>
* Appears to always be called from the main thread.
*/
public static boolean inAppVideoDownloadButtonOnClick(String videoId) {
try {
if (!overrideVideoDownloadButton.get()) {
return false;
}
if (videoId == null || videoId.isEmpty()) {
return false;
}
launchVideoExternalDownloader(videoId);
return true;
} catch (Exception ex) {
Logger.printException(() -> "inAppVideoDownloadButtonOnClick failure", ex);
}
return false;
}
/**
* Injection point.
* <p>
* Called from the in app playlist download hook.
* <p>
* Appears to always be called from the main thread.
*/
public static String inAppPlaylistDownloadButtonOnClick(String playlistId) {
try {
if (!overridePlaylistDownloadButton.get()) {
return playlistId;
}
if (playlistId == null || playlistId.isEmpty()) {
return playlistId;
}
launchPlaylistExternalDownloader(playlistId);
return "";
} catch (Exception ex) {
Logger.printException(() -> "inAppPlaylistDownloadButtonOnClick failure", ex);
}
return playlistId;
}
/**
* Injection point.
* <p>
* Called from the 'Download playlist' flyout option.
* <p>
* Appears to always be called from the main thread.
*/
public static boolean inAppPlaylistDownloadMenuOnClick(String playlistId) {
try {
if (!overridePlaylistDownloadButton.get()) {
return false;
}
if (playlistId == null || playlistId.isEmpty()) {
return false;
}
launchPlaylistExternalDownloader(playlistId);
return true;
} catch (Exception ex) {
Logger.printException(() -> "inAppPlaylistDownloadMenuOnClick failure", ex);
}
return false;
}
/**
* Injection point.
*/
public static boolean overridePlaylistDownloadButtonVisibility() {
return overridePlaylistDownloadButton.get();
}
}

View File

@ -0,0 +1,589 @@
package app.revanced.extension.youtube.patches.general;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.getChildView;
import static app.revanced.extension.shared.utils.Utils.hideViewByLayoutParams;
import static app.revanced.extension.shared.utils.Utils.hideViewGroupByMarginLayoutParams;
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
import static app.revanced.extension.youtube.patches.utils.PatchStatus.ImageSearchButton;
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.TypedValue;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.google.android.apps.youtube.app.application.Shell_SettingsActivity;
import com.google.android.apps.youtube.app.settings.SettingsActivity;
import com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.Map;
import java.util.Objects;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.ThemeUtils;
@SuppressWarnings("unused")
public class GeneralPatch {
// region [Disable auto audio tracks] patch
private static final String DEFAULT_AUDIO_TRACKS_IDENTIFIER = "original";
private static ArrayList<Object> formatStreamModelArray;
/**
* Find the stream format containing the parameter {@link GeneralPatch#DEFAULT_AUDIO_TRACKS_IDENTIFIER}, and save to the array.
*
* @param formatStreamModel stream format model including audio tracks.
*/
public static void setFormatStreamModelArray(final Object formatStreamModel) {
if (!Settings.DISABLE_AUTO_AUDIO_TRACKS.get()) {
return;
}
// Ignoring, as the stream format model array has already been added.
if (formatStreamModelArray != null) {
return;
}
// Ignoring, as it is not an original audio track.
if (!formatStreamModel.toString().contains(DEFAULT_AUDIO_TRACKS_IDENTIFIER)) {
return;
}
// For some reason, when YouTube handles formatStreamModelArray,
// it uses an array with duplicate values at the first and second indices.
formatStreamModelArray = new ArrayList<>();
formatStreamModelArray.add(formatStreamModel);
formatStreamModelArray.add(formatStreamModel);
}
/**
* Returns an array of stream format models containing the default audio tracks.
*
* @param localizedFormatStreamModelArray stream format model array consisting of audio tracks in the system's language.
* @return stream format model array consisting of original audio tracks.
*/
public static ArrayList<Object> getFormatStreamModelArray(final ArrayList<Object> localizedFormatStreamModelArray) {
if (!Settings.DISABLE_AUTO_AUDIO_TRACKS.get()) {
return localizedFormatStreamModelArray;
}
// Ignoring, as the stream format model array is empty.
if (formatStreamModelArray == null || formatStreamModelArray.isEmpty()) {
return localizedFormatStreamModelArray;
}
// Initialize the array before returning it.
ArrayList<Object> defaultFormatStreamModelArray = formatStreamModelArray;
formatStreamModelArray = null;
return defaultFormatStreamModelArray;
}
// endregion
// region [Disable splash animation] patch
public static boolean disableSplashAnimation(boolean original) {
try {
return !Settings.DISABLE_SPLASH_ANIMATION.get() && original;
} catch (Exception ex) {
Logger.printException(() -> "Failed to load disableSplashAnimation", ex);
}
return original;
}
// endregion
// region [Enable gradient loading screen] patch
public static boolean enableGradientLoadingScreen() {
return Settings.ENABLE_GRADIENT_LOADING_SCREEN.get();
}
// endregion
// region [Hide layout components] patch
private static String[] accountMenuBlockList;
static {
accountMenuBlockList = Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS.get().split("\\n");
// Some settings should not be hidden.
accountMenuBlockList = Arrays.stream(accountMenuBlockList)
.filter(item -> !Objects.equals(item, str("settings")))
.toArray(String[]::new);
}
/**
* hide account menu in you tab
*
* @param menuTitleCharSequence menu title
*/
public static void hideAccountList(View view, CharSequence menuTitleCharSequence) {
if (!Settings.HIDE_ACCOUNT_MENU.get())
return;
if (menuTitleCharSequence == null)
return;
if (!(view.getParent().getParent().getParent() instanceof ViewGroup viewGroup))
return;
hideAccountMenu(viewGroup, menuTitleCharSequence.toString());
}
/**
* hide account menu for tablet and old clients
*
* @param menuTitleCharSequence menu title
*/
public static void hideAccountMenu(View view, CharSequence menuTitleCharSequence) {
if (!Settings.HIDE_ACCOUNT_MENU.get())
return;
if (menuTitleCharSequence == null)
return;
if (!(view.getParent().getParent() instanceof ViewGroup viewGroup))
return;
hideAccountMenu(viewGroup, menuTitleCharSequence.toString());
}
private static void hideAccountMenu(ViewGroup viewGroup, String menuTitleString) {
for (String filter : accountMenuBlockList) {
if (!filter.isEmpty() && menuTitleString.equals(filter)) {
if (viewGroup.getLayoutParams() instanceof MarginLayoutParams)
hideViewGroupByMarginLayoutParams(viewGroup);
else
viewGroup.setLayoutParams(new LayoutParams(0, 0));
}
}
}
public static int hideHandle(int originalValue) {
return Settings.HIDE_HANDLE.get() ? 8 : originalValue;
}
public static boolean hideFloatingMicrophone(boolean original) {
return Settings.HIDE_FLOATING_MICROPHONE.get() || original;
}
public static boolean hideSnackBar() {
return Settings.HIDE_SNACK_BAR.get();
}
// endregion
// region [Hide navigation bar components] patch
private static final Map<NavigationButton, Boolean> shouldHideMap = new EnumMap<>(NavigationButton.class) {
{
put(NavigationButton.HOME, Settings.HIDE_NAVIGATION_HOME_BUTTON.get());
put(NavigationButton.SHORTS, Settings.HIDE_NAVIGATION_SHORTS_BUTTON.get());
put(NavigationButton.SUBSCRIPTIONS, Settings.HIDE_NAVIGATION_SUBSCRIPTIONS_BUTTON.get());
put(NavigationButton.CREATE, Settings.HIDE_NAVIGATION_CREATE_BUTTON.get());
put(NavigationButton.NOTIFICATIONS, Settings.HIDE_NAVIGATION_NOTIFICATIONS_BUTTON.get());
put(NavigationButton.LIBRARY, Settings.HIDE_NAVIGATION_LIBRARY_BUTTON.get());
}
};
public static boolean enableNarrowNavigationButton(boolean original) {
return Settings.ENABLE_NARROW_NAVIGATION_BUTTONS.get() || original;
}
public static boolean enableTranslucentNavigationBar() {
return Settings.ENABLE_TRANSLUCENT_NAVIGATION_BAR.get();
}
public static boolean switchCreateWithNotificationButton(boolean original) {
return Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get() || original;
}
public static void navigationTabCreated(NavigationButton button, View tabView) {
if (BooleanUtils.isTrue(shouldHideMap.get(button))) {
tabView.setVisibility(View.GONE);
}
}
public static void hideNavigationLabel(TextView view) {
hideViewUnderCondition(Settings.HIDE_NAVIGATION_LABEL.get(), view);
}
public static void hideNavigationBar(View view) {
hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR.get(), view);
}
// endregion
// region [Remove viewer discretion dialog] patch
/**
* Injection point.
* <p>
* The {@link AlertDialog#getButton(int)} method must be used after {@link AlertDialog#show()} is called.
* Otherwise {@link AlertDialog#getButton(int)} method will always return null.
* <a href="https://stackoverflow.com/a/4604145"/>
* <p>
* That's why {@link AlertDialog#show()} is absolutely necessary.
* Instead, use two tricks to hide Alertdialog.
* <p>
* 1. Change the size of AlertDialog to 0.
* 2. Disable AlertDialog's background dim.
* <p>
* This way, AlertDialog will be completely hidden,
* and {@link AlertDialog#getButton(int)} method can be used without issue.
*/
public static void confirmDialog(final AlertDialog dialog) {
if (!Settings.REMOVE_VIEWER_DISCRETION_DIALOG.get()) {
return;
}
// This method is called after AlertDialog#show(),
// So we need to hide the AlertDialog before pressing the possitive button.
final Window window = dialog.getWindow();
final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
if (window != null && button != null) {
WindowManager.LayoutParams params = window.getAttributes();
params.height = 0;
params.width = 0;
// Change the size of AlertDialog to 0.
window.setAttributes(params);
// Disable AlertDialog's background dim.
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
Utils.clickView(button);
}
}
public static void confirmDialogAgeVerified(final AlertDialog dialog) {
final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
if (!button.getText().toString().equals(str("og_continue")))
return;
confirmDialog(dialog);
}
// endregion
// region [Spoof app version] patch
public static String getVersionOverride(String appVersion) {
return Settings.SPOOF_APP_VERSION.get()
? Settings.SPOOF_APP_VERSION_TARGET.get()
: appVersion;
}
// endregion
// region [Toolbar components] patch
private static final int generalHeaderAttributeId = ResourceUtils.getAttrIdentifier("ytWordmarkHeader");
private static final int premiumHeaderAttributeId = ResourceUtils.getAttrIdentifier("ytPremiumWordmarkHeader");
public static void setDrawerNavigationHeader(View lithoView) {
final int headerAttributeId = getHeaderAttributeId();
lithoView.getViewTreeObserver().addOnDrawListener(() -> {
if (!(lithoView instanceof ViewGroup viewGroup))
return;
if (!(viewGroup.getChildAt(0) instanceof ImageView imageView))
return;
final Activity mActivity = Utils.getActivity();
if (mActivity == null)
return;
imageView.setImageDrawable(getHeaderDrawable(mActivity, headerAttributeId));
});
}
public static int getHeaderAttributeId() {
return Settings.CHANGE_YOUTUBE_HEADER.get()
? premiumHeaderAttributeId
: generalHeaderAttributeId;
}
public static boolean overridePremiumHeader() {
return Settings.CHANGE_YOUTUBE_HEADER.get();
}
private static Drawable getHeaderDrawable(Activity mActivity, int resourceId) {
// Rest of the implementation added by patch.
return ResourceUtils.getDrawable("");
}
private static final int searchBarId = ResourceUtils.getIdIdentifier("search_bar");
private static final int youtubeTextId = ResourceUtils.getIdIdentifier("youtube_text");
private static final int searchBoxId = ResourceUtils.getIdIdentifier("search_box");
private static final int searchIconId = ResourceUtils.getIdIdentifier("search_icon");
private static final boolean wideSearchbarEnabled = Settings.ENABLE_WIDE_SEARCH_BAR.get();
// Loads the search bar deprecated by Google.
private static final boolean wideSearchbarWithHeaderEnabled = Settings.ENABLE_WIDE_SEARCH_BAR_WITH_HEADER.get();
private static final boolean wideSearchbarYouTabEnabled = Settings.ENABLE_WIDE_SEARCH_BAR_IN_YOU_TAB.get();
public static boolean enableWideSearchBar(boolean original) {
return wideSearchbarEnabled || original;
}
/**
* Limitation: Premium header will not be applied for YouTube Premium users if the user uses the 'Wide search bar with header' option.
* This is because it forces the deprecated search bar to be loaded.
* As a solution to this limitation, 'Change YouTube header' patch is required.
*/
public static boolean enableWideSearchBarWithHeader(boolean original) {
if (!wideSearchbarEnabled)
return original;
else
return wideSearchbarWithHeaderEnabled || original;
}
public static boolean enableWideSearchBarWithHeaderInverse(boolean original) {
if (!wideSearchbarEnabled)
return original;
else
return !wideSearchbarWithHeaderEnabled && original;
}
public static boolean enableWideSearchBarInYouTab(boolean original) {
if (!wideSearchbarEnabled)
return original;
else
return !wideSearchbarYouTabEnabled && original;
}
public static void setWideSearchBarLayout(View view) {
if (!wideSearchbarEnabled)
return;
if (!(view.findViewById(searchBarId) instanceof RelativeLayout searchBarView))
return;
// When the deprecated search bar is loaded, two search bars overlap.
// Manually hides another search bar.
if (wideSearchbarWithHeaderEnabled) {
final View searchIconView = searchBarView.findViewById(searchIconId);
final View searchBoxView = searchBarView.findViewById(searchBoxId);
final View textView = searchBarView.findViewById(youtubeTextId);
if (textView != null) {
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(0, 0);
layoutParams.setMargins(0, 0, 0, 0);
textView.setLayoutParams(layoutParams);
}
// The search icon in the deprecated search bar is clickable, but onClickListener is not assigned.
// Assign onClickListener and disable the effect when clicked.
if (searchIconView != null && searchBoxView != null) {
searchIconView.setOnClickListener(view1 -> searchBoxView.callOnClick());
searchIconView.getBackground().setAlpha(0);
}
} else {
// This is the legacy method - Wide search bar without YouTube header.
// Since the padding start is 0, it does not look good.
// Add a padding start of 8.0 dip.
final int paddingLeft = searchBarView.getPaddingLeft();
final int paddingRight = searchBarView.getPaddingRight();
final int paddingTop = searchBarView.getPaddingTop();
final int paddingBottom = searchBarView.getPaddingBottom();
final int paddingStart = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, Utils.getResources().getDisplayMetrics());
// In RelativeLayout, paddingStart cannot be assigned programmatically.
// Check RTL layout and set left padding or right padding.
if (Utils.isRightToLeftTextLayout()) {
searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom);
} else {
searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom);
}
}
}
public static boolean hideCastButton(boolean original) {
return !Settings.HIDE_TOOLBAR_CAST_BUTTON.get() && original;
}
public static void hideCastButton(MenuItem menuItem) {
if (!Settings.HIDE_TOOLBAR_CAST_BUTTON.get())
return;
menuItem.setVisible(false);
menuItem.setEnabled(false);
}
public static void hideCreateButton(String enumString, View view) {
if (!Settings.HIDE_TOOLBAR_CREATE_BUTTON.get())
return;
hideViewUnderCondition(isCreateButton(enumString), view);
}
public static void hideNotificationButton(String enumString, View view) {
if (!Settings.HIDE_TOOLBAR_NOTIFICATION_BUTTON.get())
return;
hideViewUnderCondition(isNotificationButton(enumString), view);
}
public static boolean hideSearchTermThumbnail() {
return Settings.HIDE_SEARCH_TERM_THUMBNAIL.get();
}
private static final boolean hideImageSearchButton = Settings.HIDE_IMAGE_SEARCH_BUTTON.get();
private static final boolean hideVoiceSearchButton = Settings.HIDE_VOICE_SEARCH_BUTTON.get();
/**
* If the user does not hide the Image search button but only the Voice search button,
* {@link View#setVisibility(int)} cannot be used on the Voice search button.
* (This breaks the search bar layout.)
* <p>
* In this case, {@link Utils#hideViewByLayoutParams(View)} should be used.
*/
private static final boolean showImageSearchButtonAndHideVoiceSearchButton = !hideImageSearchButton && hideVoiceSearchButton && ImageSearchButton();
public static boolean hideImageSearchButton(boolean original) {
return !hideImageSearchButton && original;
}
public static void hideVoiceSearchButton(View view) {
if (showImageSearchButtonAndHideVoiceSearchButton) {
hideViewByLayoutParams(view);
} else {
hideViewUnderCondition(hideVoiceSearchButton, view);
}
}
public static void hideVoiceSearchButton(View view, int visibility) {
if (showImageSearchButtonAndHideVoiceSearchButton) {
view.setVisibility(visibility);
hideViewByLayoutParams(view);
} else {
view.setVisibility(
hideVoiceSearchButton
? View.GONE : visibility
);
}
}
/**
* In ReVanced, image files are replaced to change the header,
* Whereas in RVX, the header is changed programmatically.
* There is an issue where the header is not changed in RVX when YouTube Doodles are hidden.
* As a workaround, manually set the header when YouTube Doodles are hidden.
*/
public static void hideYouTubeDoodles(ImageView imageView, Drawable drawable) {
final Activity mActivity = Utils.getActivity();
if (Settings.HIDE_YOUTUBE_DOODLES.get() && mActivity != null) {
drawable = getHeaderDrawable(mActivity, getHeaderAttributeId());
}
imageView.setImageDrawable(drawable);
}
private static final int settingsDrawableId =
ResourceUtils.getDrawableIdentifier("yt_outline_gear_black_24");
public static int getCreateButtonDrawableId(int original) {
return Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get() &&
settingsDrawableId != 0
? settingsDrawableId
: original;
}
public static void replaceCreateButton(String enumString, View toolbarView) {
if (!Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get())
return;
// Check if the button is a create button.
if (!isCreateButton(enumString))
return;
ImageView imageView = getChildView((ViewGroup) toolbarView, view -> view instanceof ImageView);
if (imageView == null)
return;
// Overriding is possible only after OnClickListener is assigned to the create button.
Utils.runOnMainThreadDelayed(() -> {
if (Settings.REPLACE_TOOLBAR_CREATE_BUTTON_TYPE.get()) {
imageView.setOnClickListener(GeneralPatch::openRVXSettings);
imageView.setOnLongClickListener(button -> {
openYouTubeSettings(button);
return true;
});
} else {
imageView.setOnClickListener(GeneralPatch::openYouTubeSettings);
imageView.setOnLongClickListener(button -> {
openRVXSettings(button);
return true;
});
}
}, 0);
}
private static void openYouTubeSettings(View view) {
Context context = view.getContext();
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setPackage(context.getPackageName());
intent.setClass(context, Shell_SettingsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
context.startActivity(intent);
}
private static void openRVXSettings(View view) {
Context context = view.getContext();
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setPackage(context.getPackageName());
intent.setData(Uri.parse("revanced_extended_settings_intent"));
intent.setClass(context, VideoQualitySettingsActivity.class);
context.startActivity(intent);
}
/**
* The theme of {@link Shell_SettingsActivity} is dark theme.
* Since this theme is hardcoded, we should manually specify the theme for the activity.
* <p>
* Since {@link Shell_SettingsActivity} only invokes {@link SettingsActivity}, finish activity after specifying a theme.
*
* @param base {@link Shell_SettingsActivity}
*/
public static void setShellActivityTheme(Activity base) {
if (!Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get())
return;
base.setTheme(ThemeUtils.getThemeId());
Utils.runOnMainThreadDelayed(base::finish, 0);
}
private static boolean isCreateButton(String enumString) {
return StringUtils.equalsAny(
enumString,
"CREATION_ENTRY", // Create button for Phone layout
"FAB_CAMERA" // Create button for Tablet layout
);
}
private static boolean isNotificationButton(String enumString) {
return StringUtils.equalsAny(
enumString,
"TAB_ACTIVITY", // Notification button
"TAB_ACTIVITY_CAIRO" // Notification button (new layout)
);
}
// endregion
}

View File

@ -0,0 +1,79 @@
package app.revanced.extension.youtube.patches.general;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils;
import java.util.Objects;
import app.revanced.extension.shared.utils.PackageUtils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class LayoutSwitchPatch {
public enum FormFactor {
/**
* Unmodified type, and same as un-patched.
*/
ORIGINAL(null, null, null),
SMALL_FORM_FACTOR(1, null, TRUE),
SMALL_FORM_FACTOR_WIDTH_DP(1, 480, TRUE),
LARGE_FORM_FACTOR(2, null, FALSE),
LARGE_FORM_FACTOR_WIDTH_DP(2, 600, FALSE);
@Nullable
final Integer formFactorType;
@Nullable
final Integer widthDp;
@Nullable
final Boolean setMinimumDp;
FormFactor(@Nullable Integer formFactorType, @Nullable Integer widthDp, @Nullable Boolean setMinimumDp) {
this.formFactorType = formFactorType;
this.widthDp = widthDp;
this.setMinimumDp = setMinimumDp;
}
private boolean setMinimumDp() {
return BooleanUtils.isTrue(setMinimumDp);
}
}
private static final FormFactor FORM_FACTOR = Settings.CHANGE_LAYOUT.get();
public static int getFormFactor(int original) {
Integer formFactorType = FORM_FACTOR.formFactorType;
return formFactorType == null
? original
: formFactorType;
}
public static int getWidthDp(int original) {
Integer widthDp = FORM_FACTOR.widthDp;
if (widthDp == null) {
return original;
}
final int smallestScreenWidthDp = PackageUtils.getSmallestScreenWidthDp();
if (smallestScreenWidthDp == 0) {
return original;
}
return FORM_FACTOR.setMinimumDp()
? Math.min(smallestScreenWidthDp, widthDp)
: Math.max(smallestScreenWidthDp, widthDp);
}
public static boolean phoneLayoutEnabled() {
return Objects.equals(FORM_FACTOR.formFactorType, 1);
}
public static boolean tabletLayoutEnabled() {
return Objects.equals(FORM_FACTOR.formFactorType, 2);
}
}

View File

@ -0,0 +1,197 @@
package app.revanced.extension.youtube.patches.general;
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_1;
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_2;
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_3;
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.ORIGINAL;
import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class MiniplayerPatch {
/**
* Mini player type. Null fields indicates to use the original un-patched value.
*/
public enum MiniplayerType {
/**
* Unmodified type, and same as un-patched.
*/
ORIGINAL(null, null),
PHONE(false, null),
TABLET(true, null),
MODERN_1(null, 1),
MODERN_2(null, 2),
MODERN_3(null, 3);
/**
* Legacy tablet hook value.
*/
@Nullable
final Boolean legacyTabletOverride;
/**
* Modern player type used by YT.
*/
@Nullable
final Integer modernPlayerType;
MiniplayerType(@Nullable Boolean legacyTabletOverride, @Nullable Integer modernPlayerType) {
this.legacyTabletOverride = legacyTabletOverride;
this.modernPlayerType = modernPlayerType;
}
public boolean isModern() {
return modernPlayerType != null;
}
}
/**
* Modern subtitle overlay for {@link MiniplayerType#MODERN_2}.
* Resource is not present in older targets, and this field will be zero.
*/
private static final int MODERN_OVERLAY_SUBTITLE_TEXT
= ResourceUtils.getIdIdentifier("modern_miniplayer_subtitle_text");
private static final MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get();
private static final boolean DOUBLE_TAP_ACTION_ENABLED =
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_2 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get();
private static final boolean DRAG_AND_DROP_ENABLED =
CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_DRAG_AND_DROP.get();
private static final boolean HIDE_EXPAND_CLOSE_AVAILABLE =
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) &&
!DOUBLE_TAP_ACTION_ENABLED &&
!DRAG_AND_DROP_ENABLED;
private static final boolean HIDE_EXPAND_CLOSE_ENABLED =
HIDE_EXPAND_CLOSE_AVAILABLE && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get();
private static final boolean HIDE_SUBTEXT_ENABLED =
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_SUBTEXT.get();
private static final boolean HIDE_REWIND_FORWARD_ENABLED =
CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get();
private static final int OPACITY_LEVEL;
static {
final int opacity = validateValue(
Settings.MINIPLAYER_OPACITY,
0,
100,
"revanced_miniplayer_opacity_invalid_toast"
);
OPACITY_LEVEL = (opacity * 255) / 100;
}
/**
* Injection point.
*/
public static boolean getLegacyTabletMiniplayerOverride(boolean original) {
Boolean isTablet = CURRENT_TYPE.legacyTabletOverride;
return isTablet == null
? original
: isTablet;
}
/**
* Injection point.
*/
public static boolean getModernMiniplayerOverride(boolean original) {
return CURRENT_TYPE == ORIGINAL
? original
: CURRENT_TYPE.isModern();
}
/**
* Injection point.
*/
public static int getModernMiniplayerOverrideType(int original) {
Integer modernValue = CURRENT_TYPE.modernPlayerType;
return modernValue == null
? original
: modernValue;
}
/**
* Injection point.
*/
public static void adjustMiniplayerOpacity(ImageView view) {
if (CURRENT_TYPE == MODERN_1) {
view.setImageAlpha(OPACITY_LEVEL);
}
}
/**
* Injection point.
*/
public static boolean enableMiniplayerDoubleTapAction() {
return DOUBLE_TAP_ACTION_ENABLED;
}
/**
* Injection point.
*/
public static boolean enableMiniplayerDragAndDrop() {
return DRAG_AND_DROP_ENABLED;
}
/**
* Injection point.
*/
public static void hideMiniplayerExpandClose(ImageView view) {
Utils.hideViewByRemovingFromParentUnderCondition(HIDE_EXPAND_CLOSE_ENABLED, view);
}
/**
* Injection point.
*/
public static void hideMiniplayerRewindForward(ImageView view) {
Utils.hideViewByRemovingFromParentUnderCondition(HIDE_REWIND_FORWARD_ENABLED, view);
}
/**
* Injection point.
*/
public static boolean hideMiniplayerSubTexts(View view) {
// Different subviews are passed in, but only TextView and layouts are of interest here.
final boolean hideView = HIDE_SUBTEXT_ENABLED && (view instanceof TextView || view instanceof LinearLayout);
Utils.hideViewByRemovingFromParentUnderCondition(hideView, view);
return hideView || view == null;
}
/**
* Injection point.
*/
public static void playerOverlayGroupCreated(View group) {
// Modern 2 has an half broken subtitle that is always present.
// Always hide it to make the miniplayer mostly usable.
if (CURRENT_TYPE == MODERN_2 && MODERN_OVERLAY_SUBTITLE_TEXT != 0) {
if (group instanceof ViewGroup viewGroup) {
View subtitleText = Utils.getChildView(viewGroup, true,
view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT);
if (subtitleText != null) {
subtitleText.setVisibility(View.GONE);
Logger.printDebug(() -> "Modern overlay subtitle view set to hidden");
}
}
}
}
}

View File

@ -0,0 +1,52 @@
package app.revanced.extension.youtube.patches.general;
import androidx.preference.PreferenceScreen;
import app.revanced.extension.shared.patches.BaseSettingsMenuPatch;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class SettingsMenuPatch extends BaseSettingsMenuPatch {
public static void hideSettingsMenu(PreferenceScreen mPreferenceScreen) {
if (mPreferenceScreen == null) return;
for (SettingsMenuComponent component : SettingsMenuComponent.values())
if (component.enabled)
removePreference(mPreferenceScreen, component.key);
}
private enum SettingsMenuComponent {
YOUTUBE_TV("yt_unplugged_pref_key", Settings.HIDE_SETTINGS_MENU_YOUTUBE_TV.get()),
PARENT_TOOLS("parent_tools_key", Settings.HIDE_SETTINGS_MENU_PARENT_TOOLS.get()),
PRE_PURCHASE("yt_unlimited_pre_purchase_key", Settings.HIDE_SETTINGS_MENU_PRE_PURCHASE.get()),
GENERAL("general_key", Settings.HIDE_SETTINGS_MENU_GENERAL.get()),
ACCOUNT("account_switcher_key", Settings.HIDE_SETTINGS_MENU_ACCOUNT.get()),
DATA_SAVING("data_saving_settings_key", Settings.HIDE_SETTINGS_MENU_DATA_SAVING.get()),
AUTOPLAY("auto_play_key", Settings.HIDE_SETTINGS_MENU_AUTOPLAY.get()),
VIDEO_QUALITY_PREFERENCES("video_quality_settings_key", Settings.HIDE_SETTINGS_MENU_VIDEO_QUALITY_PREFERENCES.get()),
POST_PURCHASE("yt_unlimited_post_purchase_key", Settings.HIDE_SETTINGS_MENU_POST_PURCHASE.get()),
OFFLINE("offline_key", Settings.HIDE_SETTINGS_MENU_OFFLINE.get()),
WATCH_ON_TV("pair_with_tv_key", Settings.HIDE_SETTINGS_MENU_WATCH_ON_TV.get()),
MANAGE_ALL_HISTORY("history_key", Settings.HIDE_SETTINGS_MENU_MANAGE_ALL_HISTORY.get()),
YOUR_DATA_IN_YOUTUBE("your_data_key", Settings.HIDE_SETTINGS_MENU_YOUR_DATA_IN_YOUTUBE.get()),
PRIVACY("privacy_key", Settings.HIDE_SETTINGS_MENU_PRIVACY.get()),
TRY_EXPERIMENTAL_NEW_FEATURES("premium_early_access_browse_page_key", Settings.HIDE_SETTINGS_MENU_TRY_EXPERIMENTAL_NEW_FEATURES.get()),
PURCHASES_AND_MEMBERSHIPS("subscription_product_setting_key", Settings.HIDE_SETTINGS_MENU_PURCHASES_AND_MEMBERSHIPS.get()),
BILLING_AND_PAYMENTS("billing_and_payment_key", Settings.HIDE_SETTINGS_MENU_BILLING_AND_PAYMENTS.get()),
NOTIFICATIONS("notification_key", Settings.HIDE_SETTINGS_MENU_NOTIFICATIONS.get()),
THIRD_PARTY("third_party_key", Settings.HIDE_SETTINGS_MENU_THIRD_PARTY.get()),
CONNECTED_APPS("connected_accounts_browse_page_key", Settings.HIDE_SETTINGS_MENU_CONNECTED_APPS.get()),
LIVE_CHAT("live_chat_key", Settings.HIDE_SETTINGS_MENU_LIVE_CHAT.get()),
CAPTIONS("captions_key", Settings.HIDE_SETTINGS_MENU_CAPTIONS.get()),
ACCESSIBILITY("accessibility_settings_key", Settings.HIDE_SETTINGS_MENU_ACCESSIBILITY.get()),
ABOUT("about_key", Settings.HIDE_SETTINGS_MENU_ABOUT.get());
private final String key;
private final boolean enabled;
SettingsMenuComponent(String key, boolean enabled) {
this.key = key;
this.enabled = enabled;
}
}
}

View File

@ -0,0 +1,55 @@
package app.revanced.extension.youtube.patches.general;
import androidx.annotation.NonNull;
import org.apache.commons.lang3.StringUtils;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.ExtendedUtils;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
public final class YouTubeMusicActionsPatch extends VideoUtils {
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
private static final boolean isOverrideYouTubeMusicEnabled =
Settings.OVERRIDE_YOUTUBE_MUSIC_BUTTON.get();
private static final boolean overrideYouTubeMusicEnabled =
isOverrideYouTubeMusicEnabled && isYouTubeMusicEnabled();
public static String overridePackageName(@NonNull String packageName) {
if (!overrideYouTubeMusicEnabled) {
return packageName;
}
if (!StringUtils.equals(PACKAGE_NAME_YOUTUBE_MUSIC, packageName)) {
return packageName;
}
final String thirdPartyPackageName = Settings.THIRD_PARTY_YOUTUBE_MUSIC_PACKAGE_NAME.get();
if (!ExtendedUtils.isPackageEnabled(thirdPartyPackageName)) {
return packageName;
}
return thirdPartyPackageName;
}
private static boolean isYouTubeMusicEnabled() {
return ExtendedUtils.isPackageEnabled(PACKAGE_NAME_YOUTUBE_MUSIC);
}
public static final class HookYouTubeMusicAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return isYouTubeMusicEnabled();
}
}
public static final class HookYouTubeMusicPackageNameAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return isOverrideYouTubeMusicEnabled && isYouTubeMusicEnabled();
}
}
}

View File

@ -0,0 +1,12 @@
package app.revanced.extension.youtube.patches.misc;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
@SuppressWarnings("unused")
public class BackgroundPlaybackPatch {
public static boolean allowBackgroundPlayback(boolean original) {
return original || ShortsPlayerState.getCurrent().isClosed();
}
}

View File

@ -0,0 +1,14 @@
package app.revanced.extension.youtube.patches.misc;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class ExternalBrowserPatch {
public static String enableExternalBrowser(final String original) {
if (!Settings.ENABLE_EXTERNAL_BROWSER.get())
return original;
return "";
}
}

View File

@ -0,0 +1,24 @@
package app.revanced.extension.youtube.patches.misc;
import android.net.Uri;
import java.util.Objects;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class OpenLinksDirectlyPatch {
private static final String YOUTUBE_REDIRECT_PATH = "/redirect";
public static Uri enableBypassRedirect(String uri) {
final Uri parsed = Uri.parse(uri);
if (!Settings.ENABLE_OPEN_LINKS_DIRECTLY.get())
return parsed;
if (Objects.equals(parsed.getPath(), YOUTUBE_REDIRECT_PATH)) {
return Uri.parse(Uri.decode(parsed.getQueryParameter("q")));
}
return parsed;
}
}

View File

@ -0,0 +1,11 @@
package app.revanced.extension.youtube.patches.misc;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class OpusCodecPatch {
public static boolean enableOpusCodec() {
return Settings.ENABLE_OPUS_CODEC.get();
}
}

View File

@ -0,0 +1,17 @@
package app.revanced.extension.youtube.patches.misc;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class QUICProtocolPatch {
public static boolean disableQUICProtocol(boolean original) {
try {
return !Settings.DISABLE_QUIC_PROTOCOL.get() && original;
} catch (Exception ex) {
Logger.printException(() -> "Failed to load disableQUICProtocol", ex);
}
return original;
}
}

View File

@ -0,0 +1,62 @@
package app.revanced.extension.youtube.patches.misc;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.components.ShareSheetMenuFilter;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class ShareSheetPatch {
private static final boolean changeShareSheetEnabled = Settings.CHANGE_SHARE_SHEET.get();
private static void clickSystemShareButton(final RecyclerView bottomSheetRecyclerView,
final RecyclerView appsContainerRecyclerView) {
if (appsContainerRecyclerView.getChildAt(appsContainerRecyclerView.getChildCount() - 1) instanceof ViewGroup parentView &&
parentView.getChildAt(0) instanceof ViewGroup shareWithOtherAppsView) {
ShareSheetMenuFilter.isShareSheetMenuVisible = false;
bottomSheetRecyclerView.setVisibility(View.GONE);
Utils.clickView(shareWithOtherAppsView);
}
}
/**
* Injection point.
*/
public static void onShareSheetMenuCreate(final RecyclerView recyclerView) {
if (!changeShareSheetEnabled)
return;
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
try {
if (!ShareSheetMenuFilter.isShareSheetMenuVisible) {
return;
}
if (!(recyclerView.getChildAt(0) instanceof ViewGroup parentView4th)) {
return;
}
if (parentView4th.getChildAt(0) instanceof ViewGroup parentView3rd &&
parentView3rd.getChildAt(0) instanceof RecyclerView appsContainerRecyclerView) {
clickSystemShareButton(recyclerView, appsContainerRecyclerView);
} else if (parentView4th.getChildAt(1) instanceof ViewGroup parentView3rd &&
parentView3rd.getChildAt(0) instanceof RecyclerView appsContainerRecyclerView) {
clickSystemShareButton(recyclerView, appsContainerRecyclerView);
}
} catch (Exception ex) {
Logger.printException(() -> "onShareSheetMenuCreate failure", ex);
}
});
}
/**
* Injection point.
*/
public static String overridePackageName(String original) {
return changeShareSheetEnabled ? "" : original;
}
}

View File

@ -0,0 +1,185 @@
package app.revanced.extension.youtube.patches.misc;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Objects;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
import app.revanced.extension.youtube.patches.misc.requests.StreamingDataRequest;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class SpoofStreamingDataPatch {
private static final boolean SPOOF_STREAMING_DATA = Settings.SPOOF_STREAMING_DATA.get();
/**
* Any unreachable ip address. Used to intentionally fail requests.
*/
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
/**
* Injection point.
* Blocks /get_watch requests by returning an unreachable URI.
*
* @param playerRequestUri The URI of the player request.
* @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
*/
public static Uri blockGetWatchRequest(Uri playerRequestUri) {
if (SPOOF_STREAMING_DATA) {
try {
String path = playerRequestUri.getPath();
if (path != null && path.contains("get_watch")) {
Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
return UNREACHABLE_HOST_URI;
}
} catch (Exception ex) {
Logger.printException(() -> "blockGetWatchRequest failure", ex);
}
}
return playerRequestUri;
}
/**
* Injection point.
* <p>
* Blocks /initplayback requests.
*/
public static String blockInitPlaybackRequest(String originalUrlString) {
if (SPOOF_STREAMING_DATA) {
try {
var originalUri = Uri.parse(originalUrlString);
String path = originalUri.getPath();
if (path != null && path.contains("initplayback")) {
Logger.printDebug(() -> "Blocking 'initplayback' by returning unreachable url");
return UNREACHABLE_HOST_URI_STRING;
}
} catch (Exception ex) {
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
}
}
return originalUrlString;
}
/**
* Injection point.
*/
public static boolean isSpoofingEnabled() {
return SPOOF_STREAMING_DATA;
}
/**
* Injection point.
*/
public static void fetchStreams(String url, Map<String, String> requestHeaders) {
if (SPOOF_STREAMING_DATA) {
try {
Uri uri = Uri.parse(url);
String path = uri.getPath();
// 'heartbeat' has no video id and appears to be only after playback has started.
if (path != null && path.contains("player") && !path.contains("heartbeat")) {
String videoId = Objects.requireNonNull(uri.getQueryParameter("id"));
StreamingDataRequest.fetchRequest(videoId, requestHeaders);
}
} catch (Exception ex) {
Logger.printException(() -> "buildRequest failure", ex);
}
}
}
/**
* Injection point.
* Fix playback by replace the streaming data.
* Called after {@link #fetchStreams(String, Map)} .
*/
@Nullable
public static ByteBuffer getStreamingData(String videoId) {
if (SPOOF_STREAMING_DATA) {
try {
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);
if (request != null) {
// This hook is always called off the main thread,
// but this can later be called for the same video id from the main thread.
// This is not a concern, since the fetch will always be finished
// and never block the main thread.
// But if debugging, then still verify this is the situation.
if (Settings.ENABLE_DEBUG_LOGGING.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
Logger.printException(() -> "Error: Blocking main thread");
}
var stream = request.getStream();
if (stream != null) {
Logger.printDebug(() -> "Overriding video stream: " + videoId);
return stream;
}
}
Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId);
} catch (Exception ex) {
Logger.printException(() -> "getStreamingData failure", ex);
}
}
return null;
}
/**
* Injection point.
* Called after {@link #getStreamingData(String)}.
*/
@Nullable
public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) {
if (SPOOF_STREAMING_DATA) {
try {
final int methodPost = 2;
if (method == methodPost) {
String path = uri.getPath();
if (path != null && path.contains("videoplayback")) {
return null;
}
}
} catch (Exception ex) {
Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex);
}
}
return postData;
}
/**
* Injection point.
*/
public static String appendSpoofedClient(String videoFormat) {
try {
if (SPOOF_STREAMING_DATA && Settings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get()
&& !TextUtils.isEmpty(videoFormat)) {
// Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages
return "\u202D" + videoFormat + String.format("\u2009(%s)", StreamingDataRequest.getLastSpoofedClientName()); // u202D = left to right override
}
} catch (Exception ex) {
Logger.printException(() -> "appendSpoofedClient failure", ex);
}
return videoFormat;
}
public static final class iOSAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return Settings.SPOOF_STREAMING_DATA.get() && Settings.SPOOF_STREAMING_DATA_TYPE.get() == ClientType.IOS;
}
}
}

View File

@ -0,0 +1,37 @@
package app.revanced.extension.youtube.patches.misc;
import android.net.Uri;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class WatchHistoryPatch {
public enum WatchHistoryType {
ORIGINAL,
REPLACE,
BLOCK
}
private static final Uri UNREACHABLE_HOST_URI = Uri.parse("https://127.0.0.0");
private static final String WWW_TRACKING_URL_AUTHORITY = "www.youtube.com";
public static Uri replaceTrackingUrl(Uri trackingUrl) {
final WatchHistoryType watchHistoryType = Settings.WATCH_HISTORY_TYPE.get();
if (watchHistoryType != WatchHistoryType.ORIGINAL) {
try {
if (watchHistoryType == WatchHistoryType.REPLACE) {
return trackingUrl.buildUpon().authority(WWW_TRACKING_URL_AUTHORITY).build();
} else if (watchHistoryType == WatchHistoryType.BLOCK) {
return UNREACHABLE_HOST_URI;
}
} catch (Exception ex) {
Logger.printException(() -> "replaceTrackingUrl failure", ex);
}
}
return trackingUrl;
}
}

View File

@ -0,0 +1,221 @@
package app.revanced.extension.youtube.patches.misc.client;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.os.Build;
import androidx.annotation.Nullable;
public class AppClient {
// ANDROID
private static final String OS_NAME_ANDROID = "Android";
// IOS
/**
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
*
* <p>
* It can be extracted by getting the latest release version of the app on
* <a href="https://apps.apple.com/us/app/youtube-watch-listen-stream/id544007664/">the App
* Store page of the YouTube app</a>, in the {@code Whats New} section.
* </p>
*/
private static final String CLIENT_VERSION_IOS = "19.47.7";
private static final String DEVICE_MAKE_IOS = "Apple";
/**
* The device machine id for the iPhone XS Max (iPhone11,4), used to get 60fps.
* The device machine id for the iPhone 16 Pro Max (iPhone17,2), used to get HDR with AV1 hardware decoding.
*
* <p>
* See <a href="https://gist.github.com/adamawolf/3048717">this GitHub Gist</a> for more
* information.
* </p>
*/
private static final String DEVICE_MODEL_IOS = DeviceHardwareSupport.allowAV1()
? "iPhone17,2"
: "iPhone11,4";
private static final String OS_NAME_IOS = "iOS";
/**
* The minimum supported OS version for the iOS YouTube client is iOS 14.0.
* Using an invalid OS version will use the AVC codec.
*/
private static final String OS_VERSION_IOS = DeviceHardwareSupport.allowVP9()
? "18.1.1.22B91"
: "13.7.17H35";
private static final String USER_AGENT_VERSION_IOS = DeviceHardwareSupport.allowVP9()
? "18_1_1"
: "13_7";
private static final String USER_AGENT_IOS = "com.google.ios.youtube/" +
CLIENT_VERSION_IOS +
"(" +
DEVICE_MODEL_IOS +
"; U; CPU iOS " +
USER_AGENT_VERSION_IOS +
" like Mac OS X)";
// ANDROID VR
/**
* The hardcoded client version of the Android VR app used for InnerTube requests with this client.
*
* <p>
* It can be extracted by getting the latest release version of the app on
* <a href="https://www.meta.com/en-us/experiences/2002317119880945/">the App
* Store page of the YouTube app</a>, in the {@code Additional details} section.
* </p>
*/
private static final String CLIENT_VERSION_ANDROID_VR = "1.60.19";
/**
* The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client.
*
* <p>
* See <a href="https://dumps.tadiphone.dev/dumps/oculus/eureka">this GitLab</a> for more
* information.
* </p>
*/
private static final String DEVICE_MODEL_ANDROID_VR = "Quest 3";
private static final String OS_VERSION_ANDROID_VR = "12";
/**
* The SDK version for Android 12 is 31,
* but for some reason the build.props for the {@code Quest 3} state that the SDK version is 32.
*/
private static final int ANDROID_SDK_VERSION_ANDROID_VR = 32;
/**
* Package name for YouTube VR (Google DayDream): com.google.android.apps.youtube.vr (Deprecated)
* Package name for YouTube VR (Meta Quests): com.google.android.apps.youtube.vr.oculus
* Package name for YouTube VR (ByteDance Pico 4): com.google.android.apps.youtube.vr.pico
*/
private static final String USER_AGENT_ANDROID_VR = "com.google.android.apps.youtube.vr.oculus/" +
CLIENT_VERSION_ANDROID_VR +
" (Linux; U; Android " +
OS_VERSION_ANDROID_VR +
"; GB) gzip";
// ANDROID UNPLUGGED
private static final String CLIENT_VERSION_ANDROID_UNPLUGGED = "8.47.0";
/**
* The device machine id for the Chromecast with Google TV 4K.
*
* <p>
* See <a href="https://dumps.tadiphone.dev/dumps/google/kirkwood">this GitLab</a> for more
* information.
* </p>
*/
private static final String DEVICE_MODEL_ANDROID_UNPLUGGED = "Google TV Streamer";
private static final String OS_VERSION_ANDROID_UNPLUGGED = "14";
private static final int ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = 34;
private static final String USER_AGENT_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged/" +
CLIENT_VERSION_ANDROID_UNPLUGGED +
" (Linux; U; Android " +
OS_VERSION_ANDROID_UNPLUGGED +
"; GB) gzip";
private AppClient() {
}
public enum ClientType {
IOS(5,
DEVICE_MAKE_IOS,
DEVICE_MODEL_IOS,
CLIENT_VERSION_IOS,
OS_NAME_IOS,
OS_VERSION_IOS,
null,
USER_AGENT_IOS,
false
),
ANDROID_VR(28,
null,
DEVICE_MODEL_ANDROID_VR,
CLIENT_VERSION_ANDROID_VR,
OS_NAME_ANDROID,
OS_VERSION_ANDROID_VR,
ANDROID_SDK_VERSION_ANDROID_VR,
USER_AGENT_ANDROID_VR,
true
),
ANDROID_UNPLUGGED(29,
null,
DEVICE_MODEL_ANDROID_UNPLUGGED,
CLIENT_VERSION_ANDROID_UNPLUGGED,
OS_NAME_ANDROID,
OS_VERSION_ANDROID_UNPLUGGED,
ANDROID_SDK_VERSION_ANDROID_UNPLUGGED,
USER_AGENT_ANDROID_UNPLUGGED,
true
);
public final String friendlyName;
/**
* YouTube
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
*/
public final int id;
/**
* Device manufacturer.
*/
@Nullable
public final String make;
/**
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
*/
public final String deviceModel;
/**
* Device OS name.
*/
@Nullable
public final String osName;
/**
* Device OS version.
*/
public final String osVersion;
/**
* Player user-agent.
*/
public final String userAgent;
/**
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
* Field is null if not applicable.
*/
public final Integer androidSdkVersion;
/**
* App version.
*/
public final String clientVersion;
/**
* If the client can access the API logged in.
*/
public final boolean canLogin;
ClientType(int id,
@Nullable String make,
String deviceModel,
String clientVersion,
@Nullable String osName,
String osVersion,
Integer androidSdkVersion,
String userAgent,
boolean canLogin
) {
this.friendlyName = str("revanced_spoof_streaming_data_type_entry_" + name().toLowerCase());
this.id = id;
this.make = make;
this.deviceModel = deviceModel;
this.clientVersion = clientVersion;
this.osName = osName;
this.osVersion = osVersion;
this.androidSdkVersion = androidSdkVersion;
this.userAgent = userAgent;
this.canLogin = canLogin;
}
}
}

View File

@ -0,0 +1,54 @@
package app.revanced.extension.youtube.patches.misc.client;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
public class DeviceHardwareSupport {
private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9;
private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1;
static {
boolean vp9found = false;
boolean av1found = false;
MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
final boolean deviceIsAndroidTenOrLater = isSDKAbove(29);
for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater
? codecInfo.isHardwareAccelerated()
: !codecInfo.getName().startsWith("OMX.google"); // Software decoder.
if (isHardwareAccelerated && !codecInfo.isEncoder()) {
for (String type : codecInfo.getSupportedTypes()) {
if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) {
vp9found = true;
} else if (type.equalsIgnoreCase("video/av01")) {
av1found = true;
}
}
}
}
DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found;
DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found;
Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1
? "Device supports AV1 hardware decoding\n"
: "Device does not support AV1 hardware decoding\n"
+ (DEVICE_HAS_HARDWARE_DECODING_VP9
? "Device supports VP9 hardware decoding"
: "Device does not support VP9 hardware decoding"));
}
public static boolean allowVP9() {
return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get();
}
public static boolean allowAV1() {
return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1;
}
}

View File

@ -0,0 +1,103 @@
package app.revanced.extension.youtube.patches.misc.requests;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.Objects;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
@SuppressWarnings("deprecation")
public final class PlayerRoutes {
/**
* The base URL of requests of non-web clients to the InnerTube internal API.
*/
private static final String YOUTUBEI_V1_GAPIS_URL = "https://youtubei.googleapis.com/youtubei/v1/";
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
Route.Method.POST,
"player" +
"?fields=streamingData" +
"&alt=proto"
).compile();
static final Route.CompiledRoute GET_PLAYLIST_PAGE = new Route(
Route.Method.POST,
"next" +
"?fields=contents.singleColumnWatchNextResults.playlist.playlist"
).compile();
/**
* TCP connection and HTTP read timeout
*/
private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds.
private PlayerRoutes() {
}
static String createInnertubeBody(ClientType clientType, String videoId) {
return createInnertubeBody(clientType, videoId, null);
}
static String createInnertubeBody(ClientType clientType, String videoId, String playlistId) {
JSONObject innerTubeBody = new JSONObject();
try {
JSONObject context = new JSONObject();
JSONObject client = new JSONObject();
client.put("clientName", clientType.name());
client.put("clientVersion", clientType.clientVersion);
client.put("deviceModel", clientType.deviceModel);
client.put("osVersion", clientType.osVersion);
if (clientType.make != null) {
client.put("deviceMake", clientType.make);
}
if (clientType.osName != null) {
client.put("osName", clientType.osName);
}
if (clientType.androidSdkVersion != null) {
client.put("androidSdkVersion", clientType.androidSdkVersion.toString());
}
String languageCode = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale.getLanguage();
client.put("hl", languageCode);
context.put("client", client);
innerTubeBody.put("context", context);
innerTubeBody.put("contentCheckOk", true);
innerTubeBody.put("racyCheckOk", true);
innerTubeBody.put("videoId", videoId);
if (playlistId != null) {
innerTubeBody.put("playlistId", playlistId);
}
} catch (JSONException e) {
Logger.printException(() -> "Failed to create innerTubeBody", e);
}
return innerTubeBody.toString();
}
/**
* @noinspection SameParameterValue
*/
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
var connection = Requester.getConnectionFromCompiledRoute(YOUTUBEI_V1_GAPIS_URL, route);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", clientType.userAgent);
connection.setUseCaches(false);
connection.setDoOutput(true);
connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
return connection;
}
}

View File

@ -0,0 +1,199 @@
package app.revanced.extension.youtube.patches.misc.requests;
import static app.revanced.extension.youtube.patches.misc.requests.PlayerRoutes.GET_PLAYLIST_PAGE;
import android.annotation.SuppressLint;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
import app.revanced.extension.youtube.shared.VideoInformation;
public class PlaylistRequest {
/**
* How long to keep fetches until they are expired.
*/
private static final long CACHE_RETENTION_TIME_MILLISECONDS = 60 * 1000; // 1 Minute
private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds
@GuardedBy("itself")
private static final Map<String, PlaylistRequest> cache = new HashMap<>();
@SuppressLint("ObsoleteSdkInt")
public static void fetchRequestIfNeeded(@Nullable String videoId) {
Objects.requireNonNull(videoId);
synchronized (cache) {
final long now = System.currentTimeMillis();
cache.values().removeIf(request -> {
final boolean expired = request.isExpired(now);
if (expired) Logger.printDebug(() -> "Removing expired stream: " + request.videoId);
return expired;
});
if (!cache.containsKey(videoId)) {
cache.put(videoId, new PlaylistRequest(videoId));
}
}
}
@Nullable
public static PlaylistRequest getRequestForVideoId(@Nullable String videoId) {
synchronized (cache) {
return cache.get(videoId);
}
}
private static void handleConnectionError(String toastMessage, @Nullable Exception ex) {
Logger.printInfo(() -> toastMessage, ex);
}
@Nullable
private static JSONObject send(ClientType clientType, String videoId) {
Objects.requireNonNull(clientType);
Objects.requireNonNull(videoId);
final long startTime = System.currentTimeMillis();
String clientTypeName = clientType.name();
Logger.printDebug(() -> "Fetching playlist request for: " + videoId + " using client: " + clientTypeName);
try {
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_PLAYLIST_PAGE, clientType);
String innerTubeBody = PlayerRoutes.createInnertubeBody(
clientType,
videoId,
"RD" + videoId
);
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
connection.setFixedLengthStreamingMode(requestBody.length);
connection.getOutputStream().write(requestBody);
final int responseCode = connection.getResponseCode();
if (responseCode == 200) return Requester.parseJSONObject(connection);
handleConnectionError(clientTypeName + " not available with response code: "
+ responseCode + " message: " + connection.getResponseMessage(),
null);
} catch (SocketTimeoutException ex) {
handleConnectionError("Connection timeout", ex);
} catch (IOException ex) {
handleConnectionError("Network error", ex);
} catch (Exception ex) {
Logger.printException(() -> "send failed", ex);
} finally {
Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
}
return null;
}
private static Boolean fetch(@NonNull String videoId) {
final ClientType clientType = ClientType.ANDROID_VR;
final JSONObject playlistJson = send(clientType, videoId);
if (playlistJson != null) {
try {
final JSONObject singleColumnWatchNextResultsJsonObject = playlistJson
.getJSONObject("contents")
.getJSONObject("singleColumnWatchNextResults");
if (!singleColumnWatchNextResultsJsonObject.has("playlist")) {
return false;
}
final JSONObject playlistJsonObject = singleColumnWatchNextResultsJsonObject
.getJSONObject("playlist")
.getJSONObject("playlist");
final Object currentStreamObject = playlistJsonObject
.getJSONArray("contents")
.get(0);
if (!(currentStreamObject instanceof JSONObject currentStreamJsonObject)) {
return false;
}
final JSONObject watchEndpointJsonObject = currentStreamJsonObject
.getJSONObject("playlistPanelVideoRenderer")
.getJSONObject("navigationEndpoint")
.getJSONObject("watchEndpoint");
Logger.printDebug(() -> "watchEndpoint: " + watchEndpointJsonObject);
return watchEndpointJsonObject.has("playerParams") &&
VideoInformation.isMixPlaylistsOpenedByUser(watchEndpointJsonObject.getString("playerParams"));
} catch (JSONException e) {
Logger.printDebug(() -> "Fetch failed while processing response data for response: " + playlistJson);
}
}
return false;
}
/**
* Time this instance and the fetch future was created.
*/
private final long timeFetched;
private final String videoId;
private final Future<Boolean> future;
private PlaylistRequest(String videoId) {
this.timeFetched = System.currentTimeMillis();
this.videoId = videoId;
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId));
}
public boolean isExpired(long now) {
final long timeSinceCreation = now - timeFetched;
if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) {
return true;
}
// Only expired if the fetch failed (API null response).
return (fetchCompleted() && getStream() == null);
}
/**
* @return if the fetch call has completed.
*/
public boolean fetchCompleted() {
return future.isDone();
}
public Boolean getStream() {
try {
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {
Logger.printInfo(() -> "getStream timed out", ex);
} catch (InterruptedException ex) {
Logger.printException(() -> "getStream interrupted", ex);
Thread.currentThread().interrupt(); // Restore interrupt status flag.
} catch (ExecutionException ex) {
Logger.printException(() -> "getStream failure", ex);
}
return null;
}
}

View File

@ -0,0 +1,242 @@
package app.revanced.extension.youtube.patches.misc.requests;
import static app.revanced.extension.youtube.patches.misc.requests.PlayerRoutes.GET_STREAMING_DATA;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
import app.revanced.extension.youtube.settings.Settings;
public class StreamingDataRequest {
private static final ClientType[] ALL_CLIENT_TYPES = ClientType.values();
private static final ClientType[] CLIENT_ORDER_TO_USE;
static {
ClientType preferredClient = Settings.SPOOF_STREAMING_DATA_TYPE.get();
CLIENT_ORDER_TO_USE = new ClientType[ALL_CLIENT_TYPES.length];
CLIENT_ORDER_TO_USE[0] = preferredClient;
int i = 1;
for (ClientType c : ALL_CLIENT_TYPES) {
if (c != preferredClient) {
CLIENT_ORDER_TO_USE[i++] = c;
}
}
}
private static ClientType lastSpoofedClientType;
public static String getLastSpoofedClientName() {
return lastSpoofedClientType == null
? "Unknown"
: lastSpoofedClientType.friendlyName;
}
/**
* TCP connection and HTTP read timeout.
*/
private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000;
/**
* Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS}
*/
private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000;
@GuardedBy("itself")
private static final Map<String, StreamingDataRequest> cache = Collections.synchronizedMap(
new LinkedHashMap<>(100) {
/**
* Cache limit must be greater than the maximum number of videos open at once,
* which theoretically is more than 4 (3 Shorts + one regular minimized video).
* But instead use a much larger value, to handle if a video viewed a while ago
* is somehow still referenced. Each stream is a small array of Strings
* so memory usage is not a concern.
*/
private static final int CACHE_LIMIT = 50;
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
}
});
public static void fetchRequest(@NonNull String videoId, Map<String, String> fetchHeaders) {
cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
}
@Nullable
public static StreamingDataRequest getRequestForVideoId(@Nullable String videoId) {
return cache.get(videoId);
}
private static void handleConnectionError(String toastMessage, @Nullable Exception ex) {
Logger.printInfo(() -> toastMessage, ex);
}
// Available only to logged in users.
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String[] REQUEST_HEADER_KEYS = {
AUTHORIZATION_HEADER,
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
};
private static void writeInnerTubeBody(HttpURLConnection connection, ClientType clientType,
String videoId, Map<String, String> playerHeaders) {
try {
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
if (playerHeaders != null) {
for (String key : REQUEST_HEADER_KEYS) {
if (!clientType.canLogin && key.equals(AUTHORIZATION_HEADER)) {
continue;
}
String value = playerHeaders.get(key);
if (value != null) {
connection.setRequestProperty(key, value);
}
}
}
String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId);
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
connection.setFixedLengthStreamingMode(requestBody.length);
connection.getOutputStream().write(requestBody);
} catch (IOException ex) {
handleConnectionError("Network error", ex);
}
}
@Nullable
private static HttpURLConnection send(ClientType clientType, String videoId,
Map<String, String> playerHeaders) {
Objects.requireNonNull(clientType);
Objects.requireNonNull(videoId);
Objects.requireNonNull(playerHeaders);
final long startTime = System.currentTimeMillis();
String clientTypeName = clientType.name();
Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name());
try {
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
writeInnerTubeBody(connection, clientType, videoId, playerHeaders);
final int responseCode = connection.getResponseCode();
if (responseCode == 200) return connection;
handleConnectionError(clientTypeName + " not available with response code: "
+ responseCode + " message: " + connection.getResponseMessage(),
null);
} catch (Exception ex) {
Logger.printException(() -> "send failed", ex);
} finally {
Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
}
return null;
}
private static final ByteArrayFilterGroup liveStreams =
new ByteArrayFilterGroup(
Settings.SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK,
"yt_live_broadcast",
"yt_premiere_broadcast"
);
private static ByteBuffer fetch(@NonNull String videoId, Map<String, String> playerHeaders) {
try {
lastSpoofedClientType = null;
// Retry with different client if empty response body is received.
for (ClientType clientType : CLIENT_ORDER_TO_USE) {
HttpURLConnection connection = send(clientType, videoId, playerHeaders);
// gzip encoding doesn't response with content length (-1),
// but empty response body does.
if (connection == null || connection.getContentLength() == 0) {
continue;
}
InputStream inputStream = new BufferedInputStream(connection.getInputStream());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) >= 0) {
baos.write(buffer, 0, bytesRead);
}
inputStream.close();
if (clientType == ClientType.IOS && liveStreams.check(buffer).isFiltered()) {
Logger.printDebug(() -> "Ignore IOS spoofing as it is a livestream (video: " + videoId + ")");
continue;
}
lastSpoofedClientType = clientType;
return ByteBuffer.wrap(baos.toByteArray());
}
} catch (IOException ex) {
Logger.printException(() -> "Fetch failed while processing response data", ex);
}
handleConnectionError("Could not fetch any client streams", null);
return null;
}
private final String videoId;
private final Future<ByteBuffer> future;
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
Objects.requireNonNull(playerHeaders);
this.videoId = videoId;
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
}
public boolean fetchCompleted() {
return future.isDone();
}
@Nullable
public ByteBuffer getStream() {
try {
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {
Logger.printInfo(() -> "getStream timed out", ex);
} catch (InterruptedException ex) {
Logger.printException(() -> "getStream interrupted", ex);
Thread.currentThread().interrupt(); // Restore interrupt status flag.
} catch (ExecutionException ex) {
Logger.printException(() -> "getStream failure", ex);
}
return null;
}
@NonNull
@Override
public String toString() {
return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}';
}
}

View File

@ -0,0 +1,59 @@
package app.revanced.extension.youtube.patches.overlaybutton;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class AlwaysRepeat extends BottomControlButton {
@Nullable
private static AlwaysRepeat instance;
public AlwaysRepeat(ViewGroup bottomControlsViewGroup) {
super(
bottomControlsViewGroup,
"always_repeat_button",
Settings.OVERLAY_BUTTON_ALWAYS_REPEAT,
Settings.ALWAYS_REPEAT,
Settings.ALWAYS_REPEAT_PAUSE,
view -> {
if (instance != null)
instance.changeSelected(!view.isSelected());
},
view -> {
if (instance != null)
instance.changeColorFilter();
return true;
}
);
}
/**
* Injection point.
*/
public static void initialize(View bottomControlsViewGroup) {
try {
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
instance = new AlwaysRepeat(viewGroup);
}
} catch (Exception ex) {
Logger.printException(() -> "initialize failure", ex);
}
}
/**
* Injection point.
*/
public static void changeVisibility(boolean showing, boolean animation) {
if (instance != null) instance.setVisibility(showing, animation);
}
public static void changeVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
}

View File

@ -0,0 +1,174 @@
package app.revanced.extension.youtube.patches.overlaybutton;
import static app.revanced.extension.shared.utils.ResourceUtils.getAnimation;
import static app.revanced.extension.shared.utils.ResourceUtils.getInteger;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.getChildView;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.util.Objects;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
public abstract class BottomControlButton {
private static final Animation fadeIn;
private static final Animation fadeOut;
private static final Animation fadeOutImmediate;
private final ColorFilter cf =
new PorterDuffColorFilter(Color.parseColor("#fffffc79"), PorterDuff.Mode.SRC_ATOP);
private final WeakReference<ImageView> buttonRef;
private final BooleanSetting setting;
private final BooleanSetting primaryInteractionSetting;
private final BooleanSetting secondaryInteractionSetting;
protected boolean isVisible;
static {
fadeIn = getAnimation("fade_in");
// android.R.integer.config_shortAnimTime, 200
fadeIn.setDuration(getInteger("fade_duration_fast"));
fadeOut = getAnimation("fade_out");
// android.R.integer.config_mediumAnimTime, 400
fadeOut.setDuration(getInteger("fade_overlay_fade_duration"));
fadeOutImmediate = getAnimation("abc_fade_out");
// android.R.integer.config_shortAnimTime, 200
fadeOutImmediate.setDuration(getInteger("fade_duration_fast"));
}
@NonNull
public static Animation getButtonFadeIn() {
return fadeIn;
}
@NonNull
public static Animation getButtonFadeOut() {
return fadeOut;
}
@NonNull
public static Animation getButtonFadeOutImmediate() {
return fadeOutImmediate;
}
public BottomControlButton(@NonNull ViewGroup bottomControlsViewGroup, @NonNull String imageViewButtonId, @NonNull BooleanSetting booleanSetting,
@NonNull View.OnClickListener onClickListener, @Nullable View.OnLongClickListener longClickListener) {
this(bottomControlsViewGroup, imageViewButtonId, booleanSetting, null, null, onClickListener, longClickListener);
}
@SuppressWarnings("unused")
public BottomControlButton(@NonNull ViewGroup bottomControlsViewGroup, @NonNull String imageViewButtonId, @NonNull BooleanSetting booleanSetting, @Nullable BooleanSetting primaryInteractionSetting,
@NonNull View.OnClickListener onClickListener, @Nullable View.OnLongClickListener longClickListener) {
this(bottomControlsViewGroup, imageViewButtonId, booleanSetting, primaryInteractionSetting, null, onClickListener, longClickListener);
}
public BottomControlButton(@NonNull ViewGroup bottomControlsViewGroup, @NonNull String imageViewButtonId, @NonNull BooleanSetting booleanSetting,
@Nullable BooleanSetting primaryInteractionSetting, @Nullable BooleanSetting secondaryInteractionSetting,
@NonNull View.OnClickListener onClickListener, @Nullable View.OnLongClickListener longClickListener) {
Logger.printDebug(() -> "Initializing button: " + imageViewButtonId);
setting = booleanSetting;
// Create the button.
ImageView imageView = Objects.requireNonNull(getChildView(bottomControlsViewGroup, imageViewButtonId));
imageView.setOnClickListener(onClickListener);
this.primaryInteractionSetting = primaryInteractionSetting;
this.secondaryInteractionSetting = secondaryInteractionSetting;
if (primaryInteractionSetting != null) {
imageView.setSelected(primaryInteractionSetting.get());
}
if (secondaryInteractionSetting != null) {
setColorFilter(imageView, secondaryInteractionSetting.get());
}
if (longClickListener != null) {
imageView.setOnLongClickListener(longClickListener);
}
imageView.setVisibility(View.GONE);
buttonRef = new WeakReference<>(imageView);
}
public void changeActivated(boolean activated) {
ImageView imageView = buttonRef.get();
if (imageView == null)
return;
imageView.setActivated(activated);
}
public void changeSelected(boolean selected) {
ImageView imageView = buttonRef.get();
if (imageView == null || primaryInteractionSetting == null)
return;
if (imageView.getColorFilter() == cf) {
Utils.showToastShort(str("revanced_overlay_button_not_allowed_warning"));
return;
}
imageView.setSelected(selected);
primaryInteractionSetting.save(selected);
}
public void changeColorFilter() {
ImageView imageView = buttonRef.get();
if (imageView == null) return;
if (primaryInteractionSetting == null || secondaryInteractionSetting == null)
return;
imageView.setSelected(true);
primaryInteractionSetting.save(true);
final boolean newValue = !secondaryInteractionSetting.get();
secondaryInteractionSetting.save(newValue);
setColorFilter(imageView, newValue);
}
public void setColorFilter(ImageView imageView, boolean selected) {
if (selected)
imageView.setColorFilter(cf);
else
imageView.clearColorFilter();
}
public void setVisibility(boolean visible, boolean animation) {
ImageView imageView = buttonRef.get();
if (imageView == null || isVisible == visible) return;
isVisible = visible;
imageView.clearAnimation();
if (visible && setting.get()) {
imageView.setVisibility(View.VISIBLE);
if (animation) imageView.startAnimation(fadeIn);
return;
}
if (imageView.getVisibility() == View.VISIBLE) {
if (animation) imageView.startAnimation(fadeOut);
imageView.setVisibility(View.GONE);
}
}
public void setVisibilityNegatedImmediate() {
ImageView imageView = buttonRef.get();
if (imageView == null) return;
if (!setting.get()) return;
imageView.clearAnimation();
imageView.startAnimation(fadeOutImmediate);
imageView.setVisibility(View.GONE);
}
}

View File

@ -0,0 +1,55 @@
package app.revanced.extension.youtube.patches.overlaybutton;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
public class CopyVideoUrl extends BottomControlButton {
@Nullable
private static CopyVideoUrl instance;
public CopyVideoUrl(ViewGroup bottomControlsViewGroup) {
super(
bottomControlsViewGroup,
"copy_video_url_button",
Settings.OVERLAY_BUTTON_COPY_VIDEO_URL,
view -> VideoUtils.copyUrl(false),
view -> {
VideoUtils.copyUrl(true);
return true;
}
);
}
/**
* Injection point.
*/
public static void initialize(View bottomControlsViewGroup) {
try {
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
instance = new CopyVideoUrl(viewGroup);
}
} catch (Exception ex) {
Logger.printException(() -> "initialize failure", ex);
}
}
/**
* Injection point.
*/
public static void changeVisibility(boolean showing, boolean animation) {
if (instance != null) instance.setVisibility(showing, animation);
}
public static void changeVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
}

View File

@ -0,0 +1,55 @@
package app.revanced.extension.youtube.patches.overlaybutton;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
public class CopyVideoUrlTimestamp extends BottomControlButton {
@Nullable
private static CopyVideoUrlTimestamp instance;
public CopyVideoUrlTimestamp(ViewGroup bottomControlsViewGroup) {
super(
bottomControlsViewGroup,
"copy_video_url_timestamp_button",
Settings.OVERLAY_BUTTON_COPY_VIDEO_URL_TIMESTAMP,
view -> VideoUtils.copyUrl(true),
view -> {
VideoUtils.copyTimeStamp();
return true;
}
);
}
/**
* Injection point.
*/
public static void initialize(View bottomControlsViewGroup) {
try {
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
instance = new CopyVideoUrlTimestamp(viewGroup);
}
} catch (Exception ex) {
Logger.printException(() -> "initialize failure", ex);
}
}
/**
* Injection point.
*/
public static void changeVisibility(boolean showing, boolean animation) {
if (instance != null) instance.setVisibility(showing, animation);
}
public static void changeVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
}

View File

@ -0,0 +1,52 @@
package app.revanced.extension.youtube.patches.overlaybutton;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
public class ExternalDownload extends BottomControlButton {
@Nullable
private static ExternalDownload instance;
public ExternalDownload(ViewGroup bottomControlsViewGroup) {
super(
bottomControlsViewGroup,
"external_download_button",
Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER,
view -> VideoUtils.launchVideoExternalDownloader(),
null
);
}
/**
* Injection point.
*/
public static void initialize(View bottomControlsViewGroup) {
try {
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
instance = new ExternalDownload(viewGroup);
}
} catch (Exception ex) {
Logger.printException(() -> "initialize failure", ex);
}
}
/**
* Injection point.
*/
public static void changeVisibility(boolean showing, boolean animation) {
if (instance != null) instance.setVisibility(showing, animation);
}
public static void changeVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
}

View File

@ -0,0 +1,76 @@
package app.revanced.extension.youtube.patches.overlaybutton;
import android.content.Context;
import android.media.AudioManager;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"deprecation", "unused"})
public class MuteVolume extends BottomControlButton {
@Nullable
private static MuteVolume instance;
private static AudioManager audioManager;
private static final int stream = AudioManager.STREAM_MUSIC;
public MuteVolume(ViewGroup bottomControlsViewGroup) {
super(
bottomControlsViewGroup,
"mute_volume_button",
Settings.OVERLAY_BUTTON_MUTE_VOLUME,
view -> {
if (instance != null && audioManager != null) {
boolean unMuted = !audioManager.isStreamMute(stream);
audioManager.setStreamMute(stream, unMuted);
instance.changeActivated(unMuted);
}
},
null
);
}
/**
* Injection point.
*/
public static void initialize(View bottomControlsViewGroup) {
try {
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
instance = new MuteVolume(viewGroup);
}
if (bottomControlsViewGroup.getContext().getSystemService(Context.AUDIO_SERVICE) instanceof AudioManager am) {
audioManager = am;
}
} catch (Exception ex) {
Logger.printException(() -> "initialize failure", ex);
}
}
/**
* Injection point.
*/
public static void changeVisibility(boolean showing, boolean animation) {
if (instance != null) {
instance.setVisibility(showing, animation);
changeActivated(instance);
}
}
public static void changeVisibilityNegatedImmediate() {
if (instance != null) {
instance.setVisibilityNegatedImmediate();
changeActivated(instance);
}
}
private static void changeActivated(MuteVolume instance) {
if (audioManager != null) {
boolean muted = audioManager.isStreamMute(stream);
instance.changeActivated(muted);
}
}
}

View File

@ -0,0 +1,55 @@
package app.revanced.extension.youtube.patches.overlaybutton;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
public class PlayAll extends BottomControlButton {
@Nullable
private static PlayAll instance;
public PlayAll(ViewGroup bottomControlsViewGroup) {
super(
bottomControlsViewGroup,
"play_all_button",
Settings.OVERLAY_BUTTON_PLAY_ALL,
view -> VideoUtils.openVideo(Settings.OVERLAY_BUTTON_PLAY_ALL_TYPE.get()),
view -> {
VideoUtils.openVideo();
return true;
}
);
}
/**
* Injection point.
*/
public static void initialize(View bottomControlsViewGroup) {
try {
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
instance = new PlayAll(viewGroup);
}
} catch (Exception ex) {
Logger.printException(() -> "initialize failure", ex);
}
}
/**
* Injection point.
*/
public static void changeVisibility(boolean showing, boolean animation) {
if (instance != null) instance.setVisibility(showing, animation);
}
public static void changeVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
}

View File

@ -0,0 +1,68 @@
package app.revanced.extension.youtube.patches.overlaybutton;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.showToastShort;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.VideoInformation;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
public class SpeedDialog extends BottomControlButton {
@Nullable
private static SpeedDialog instance;
public SpeedDialog(ViewGroup bottomControlsViewGroup) {
super(
bottomControlsViewGroup,
"speed_dialog_button",
Settings.OVERLAY_BUTTON_SPEED_DIALOG,
view -> VideoUtils.showPlaybackSpeedDialog(view.getContext()),
view -> {
if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get() ||
VideoInformation.getPlaybackSpeed() == Settings.DEFAULT_PLAYBACK_SPEED.get()) {
VideoInformation.overridePlaybackSpeed(1.0f);
showToastShort(str("revanced_overlay_button_speed_dialog_reset", "1.0"));
} else {
float defaultSpeed = Settings.DEFAULT_PLAYBACK_SPEED.get();
VideoInformation.overridePlaybackSpeed(defaultSpeed);
showToastShort(str("revanced_overlay_button_speed_dialog_reset", defaultSpeed));
}
return true;
}
);
}
/**
* Injection point.
*/
public static void initialize(View bottomControlsViewGroup) {
try {
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
instance = new SpeedDialog(viewGroup);
}
} catch (Exception ex) {
Logger.printException(() -> "initialize failure", ex);
}
}
/**
* Injection point.
*/
public static void changeVisibility(boolean showing, boolean animation) {
if (instance != null) instance.setVisibility(showing, animation);
}
public static void changeVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
}

View File

@ -0,0 +1,55 @@
package app.revanced.extension.youtube.patches.overlaybutton;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.settings.preference.WhitelistedChannelsPreference;
import app.revanced.extension.youtube.whitelist.Whitelist;
@SuppressWarnings("unused")
public class Whitelists extends BottomControlButton {
@Nullable
private static Whitelists instance;
public Whitelists(ViewGroup bottomControlsViewGroup) {
super(
bottomControlsViewGroup,
"whitelist_button",
Settings.OVERLAY_BUTTON_WHITELIST,
view -> Whitelist.showWhitelistDialog(view.getContext()),
view -> {
WhitelistedChannelsPreference.showWhitelistedChannelDialog(view.getContext());
return true;
}
);
}
/**
* Injection point.
*/
public static void initialize(View bottomControlsViewGroup) {
try {
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
instance = new Whitelists(viewGroup);
}
} catch (Exception ex) {
Logger.printException(() -> "initialize failure", ex);
}
}
/**
* Injection point.
*/
public static void changeVisibility(boolean showing, boolean animation) {
if (instance != null) instance.setVisibility(showing, animation);
}
public static void changeVisibilityNegatedImmediate() {
if (instance != null) instance.setVisibilityNegatedImmediate();
}
}

View File

@ -0,0 +1,730 @@
package app.revanced.extension.youtube.patches.player;
import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition;
import static app.revanced.extension.shared.utils.Utils.hideViewByRemovingFromParentUnderCondition;
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue;
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import java.lang.ref.WeakReference;
import java.util.Objects;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.IntegerSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.utils.InitializationPatch;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.RootView;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
import app.revanced.extension.youtube.shared.VideoInformation;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
public class PlayerPatch {
private static final IntegerSetting quickActionsMarginTopSetting = Settings.QUICK_ACTIONS_TOP_MARGIN;
private static final int PLAYER_OVERLAY_OPACITY_LEVEL;
private static final int QUICK_ACTIONS_MARGIN_TOP;
private static final float SPEED_OVERLAY_VALUE;
static {
final int opacity = validateValue(
Settings.CUSTOM_PLAYER_OVERLAY_OPACITY,
0,
100,
"revanced_custom_player_overlay_opacity_invalid_toast"
);
PLAYER_OVERLAY_OPACITY_LEVEL = (opacity * 255) / 100;
SPEED_OVERLAY_VALUE = validateValue(
Settings.SPEED_OVERLAY_VALUE,
0.0f,
8.0f,
"revanced_speed_overlay_value_invalid_toast"
);
final int topMargin = validateValue(
Settings.QUICK_ACTIONS_TOP_MARGIN,
0,
32,
"revanced_quick_actions_top_margin_invalid_toast"
);
QUICK_ACTIONS_MARGIN_TOP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (float) topMargin, Utils.getResources().getDisplayMetrics());
}
// region [Ambient mode control] patch
public static boolean bypassAmbientModeRestrictions(boolean original) {
return (!Settings.BYPASS_AMBIENT_MODE_RESTRICTIONS.get() && original) || Settings.DISABLE_AMBIENT_MODE.get();
}
public static boolean disableAmbientModeInFullscreen() {
return !Settings.DISABLE_AMBIENT_MODE_IN_FULLSCREEN.get();
}
// endregion
// region [Change player flyout menu toggles] patch
public static boolean changeSwitchToggle(boolean original) {
return !Settings.CHANGE_PLAYER_FLYOUT_MENU_TOGGLE.get() && original;
}
public static String getToggleString(String str) {
return ResourceUtils.getString(str);
}
// endregion
// region [Description components] patch
public static boolean disableRollingNumberAnimations() {
return Settings.DISABLE_ROLLING_NUMBER_ANIMATIONS.get();
}
/**
* view id R.id.content
*/
private static final int contentId = ResourceUtils.getIdIdentifier("content");
private static final boolean expandDescriptionEnabled = Settings.EXPAND_VIDEO_DESCRIPTION.get();
private static final String descriptionString = Settings.EXPAND_VIDEO_DESCRIPTION_STRINGS.get();
private static boolean isDescriptionPanel = false;
public static void setContentDescription(String contentDescription) {
if (!expandDescriptionEnabled) {
return;
}
if (contentDescription == null || contentDescription.isEmpty()) {
isDescriptionPanel = false;
return;
}
if (descriptionString.isEmpty()) {
isDescriptionPanel = false;
return;
}
isDescriptionPanel = descriptionString.equals(contentDescription);
}
/**
* The last time the clickDescriptionView method was called.
*/
private static long lastTimeDescriptionViewInvoked;
public static void onVideoDescriptionCreate(RecyclerView recyclerView) {
if (!expandDescriptionEnabled)
return;
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
try {
// Video description panel is only open when the player is active.
if (!RootView.isPlayerActive()) {
return;
}
// Video description's recyclerView is a child view of [contentId].
if (!(recyclerView.getParent().getParent() instanceof View contentView)) {
return;
}
if (contentView.getId() != contentId) {
return;
}
// This method is invoked whenever the Engagement panel is opened. (Description, Chapters, Comments, etc.)
// Check the title of the Engagement panel to prevent unnecessary clicking.
if (!isDescriptionPanel) {
return;
}
// The first view group contains information such as the video's title, like count, and number of views.
if (!(recyclerView.getChildAt(0) instanceof ViewGroup primaryViewGroup)) {
return;
}
if (primaryViewGroup.getChildCount() < 2) {
return;
}
// Typically, descriptionView is placed as the second child of recyclerView.
if (recyclerView.getChildAt(1) instanceof ViewGroup viewGroup) {
clickDescriptionView(viewGroup);
}
// In some videos, descriptionView is placed as the third child of recyclerView.
if (recyclerView.getChildAt(2) instanceof ViewGroup viewGroup) {
clickDescriptionView(viewGroup);
}
// Even if both methods are performed, there is no major issue with the operation of the patch.
} catch (Exception ex) {
Logger.printException(() -> "onVideoDescriptionCreate failed.", ex);
}
});
}
private static void clickDescriptionView(@NonNull ViewGroup descriptionViewGroup) {
final View descriptionView = descriptionViewGroup.getChildAt(0);
if (descriptionView == null) {
return;
}
// This method is sometimes used multiple times.
// To prevent this, ignore method reuse within 1 second.
final long now = System.currentTimeMillis();
if (now - lastTimeDescriptionViewInvoked < 1000) {
return;
}
lastTimeDescriptionViewInvoked = now;
// The type of descriptionView can be either ViewGroup or TextView. (A/B tests)
// If the type of descriptionView is TextView, longer delay is required.
final long delayMillis = descriptionView instanceof TextView
? 500
: 100;
Utils.runOnMainThreadDelayed(() -> Utils.clickView(descriptionView), delayMillis);
}
/**
* This method is invoked only when the view type of descriptionView is {@link TextView}. (A/B tests)
*
* @param textView descriptionView.
* @param original Whether to apply {@link TextView#setTextIsSelectable}.
* Patch replaces the {@link TextView#setTextIsSelectable} method invoke.
*/
public static void disableVideoDescriptionInteraction(TextView textView, boolean original) {
if (textView != null) {
textView.setTextIsSelectable(
!Settings.DISABLE_VIDEO_DESCRIPTION_INTERACTION.get() && original
);
}
}
// endregion
// region [Disable haptic feedback] patch
public static boolean disableChapterVibrate() {
return Settings.DISABLE_HAPTIC_FEEDBACK_CHAPTERS.get();
}
public static boolean disableSeekVibrate() {
return Settings.DISABLE_HAPTIC_FEEDBACK_SEEK.get();
}
public static boolean disableSeekUndoVibrate() {
return Settings.DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO.get();
}
public static boolean disableScrubbingVibrate() {
return Settings.DISABLE_HAPTIC_FEEDBACK_SCRUBBING.get();
}
public static boolean disableZoomVibrate() {
return Settings.DISABLE_HAPTIC_FEEDBACK_ZOOM.get();
}
// endregion
// region [Fullscreen components] patch
public static void disableEngagementPanels(CoordinatorLayout coordinatorLayout) {
if (!Settings.DISABLE_ENGAGEMENT_PANEL.get()) return;
coordinatorLayout.setVisibility(View.GONE);
}
public static void showVideoTitleSection(FrameLayout frameLayout, View view) {
final boolean isEnabled = Settings.SHOW_VIDEO_TITLE_SECTION.get() || !Settings.DISABLE_ENGAGEMENT_PANEL.get();
if (isEnabled) {
frameLayout.addView(view);
}
}
public static boolean hideAutoPlayPreview() {
return Settings.HIDE_AUTOPLAY_PREVIEW.get();
}
public static boolean hideRelatedVideoOverlay() {
return Settings.HIDE_RELATED_VIDEO_OVERLAY.get();
}
public static void hideQuickActions(View view) {
final boolean isEnabled = Settings.DISABLE_ENGAGEMENT_PANEL.get() || Settings.HIDE_QUICK_ACTIONS.get();
Utils.hideViewBy0dpUnderCondition(
isEnabled,
view
);
}
public static void setQuickActionMargin(View view) {
int topMarginPx = getQuickActionsTopMargin();
if (topMarginPx == 0) {
return;
}
if (!(view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams mlp))
return;
mlp.setMargins(
mlp.leftMargin,
topMarginPx,
mlp.rightMargin,
mlp.bottomMargin
);
view.requestLayout();
}
public static boolean enableCompactControlsOverlay(boolean original) {
return Settings.ENABLE_COMPACT_CONTROLS_OVERLAY.get() || original;
}
public static boolean disableLandScapeMode(boolean original) {
return Settings.DISABLE_LANDSCAPE_MODE.get() || original;
}
private static volatile boolean isScreenOn;
public static boolean keepFullscreen(boolean original) {
if (!Settings.KEEP_LANDSCAPE_MODE.get())
return original;
return isScreenOn;
}
public static void setScreenOn() {
if (!Settings.KEEP_LANDSCAPE_MODE.get())
return;
isScreenOn = true;
Utils.runOnMainThreadDelayed(() -> isScreenOn = false, Settings.KEEP_LANDSCAPE_MODE_TIMEOUT.get());
}
private static WeakReference<Activity> watchDescriptorActivityRef = new WeakReference<>(null);
private static volatile boolean isLandScapeVideo = true;
public static void setWatchDescriptorActivity(Activity activity) {
watchDescriptorActivityRef = new WeakReference<>(activity);
}
public static boolean forceFullscreen(boolean original) {
if (!Settings.FORCE_FULLSCREEN.get())
return original;
Utils.runOnMainThreadDelayed(PlayerPatch::setOrientation, 1000);
return true;
}
private static void setOrientation() {
final Activity watchDescriptorActivity = watchDescriptorActivityRef.get();
final int requestedOrientation = isLandScapeVideo
? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
: watchDescriptorActivity.getRequestedOrientation();
watchDescriptorActivity.setRequestedOrientation(requestedOrientation);
}
public static void setVideoPortrait(int width, int height) {
if (!Settings.FORCE_FULLSCREEN.get())
return;
isLandScapeVideo = width > height;
}
// endregion
// region [Hide comments component] patch
public static void changeEmojiPickerOpacity(ImageView imageView) {
if (!Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS.get())
return;
imageView.setImageAlpha(0);
}
@Nullable
public static Object disableEmojiPickerOnClickListener(@Nullable Object object) {
return Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS.get() ? null : object;
}
// endregion
// region [Hide player buttons] patch
public static boolean hideAutoPlayButton() {
return Settings.HIDE_PLAYER_AUTOPLAY_BUTTON.get();
}
public static boolean hideCaptionsButton(boolean original) {
return !Settings.HIDE_PLAYER_CAPTIONS_BUTTON.get() && original;
}
public static int hideCastButton(int original) {
return Settings.HIDE_PLAYER_CAST_BUTTON.get()
? View.GONE
: original;
}
public static void hideCaptionsButton(View view) {
Utils.hideViewUnderCondition(Settings.HIDE_PLAYER_CAPTIONS_BUTTON, view);
}
public static void hideCollapseButton(ImageView imageView) {
if (!Settings.HIDE_PLAYER_COLLAPSE_BUTTON.get())
return;
imageView.setImageResource(android.R.color.transparent);
imageView.setImageAlpha(0);
imageView.setEnabled(false);
var layoutParams = imageView.getLayoutParams();
if (layoutParams instanceof RelativeLayout.LayoutParams) {
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(0, 0);
imageView.setLayoutParams(lp);
} else {
Logger.printDebug(() -> "Unknown collapse button layout params: " + layoutParams);
}
}
public static void setTitleAnchorStartMargin(View titleAnchorView) {
if (!Settings.HIDE_PLAYER_COLLAPSE_BUTTON.get())
return;
var layoutParams = titleAnchorView.getLayoutParams();
if (titleAnchorView.getLayoutParams() instanceof RelativeLayout.LayoutParams lp) {
lp.setMarginStart(0);
} else {
Logger.printDebug(() -> "Unknown title anchor layout params: " + layoutParams);
}
}
public static ImageView hideFullscreenButton(ImageView imageView) {
final boolean hideView = Settings.HIDE_PLAYER_FULLSCREEN_BUTTON.get();
Utils.hideViewUnderCondition(hideView, imageView);
return hideView ? null : imageView;
}
public static boolean hidePreviousNextButton(boolean previousOrNextButtonVisible) {
return !Settings.HIDE_PLAYER_PREVIOUS_NEXT_BUTTON.get() && previousOrNextButtonVisible;
}
public static boolean hideMusicButton() {
return Settings.HIDE_PLAYER_YOUTUBE_MUSIC_BUTTON.get();
}
// endregion
// region [Player components] patch
public static void changeOpacity(ImageView imageView) {
imageView.setImageAlpha(PLAYER_OVERLAY_OPACITY_LEVEL);
}
private static boolean isAutoPopupPanel;
public static boolean disableAutoPlayerPopupPanels(boolean isLiveChatOrPlaylistPanel) {
if (!Settings.DISABLE_AUTO_PLAYER_POPUP_PANELS.get()) {
return false;
}
if (isLiveChatOrPlaylistPanel) {
return true;
}
return isAutoPopupPanel && ShortsPlayerState.getCurrent().isClosed();
}
public static void setInitVideoPanel(boolean initVideoPanel) {
isAutoPopupPanel = initVideoPanel;
}
@NonNull
public static String videoId = "";
public static void disableAutoSwitchMixPlaylists(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
if (!Settings.DISABLE_AUTO_SWITCH_MIX_PLAYLISTS.get()) {
return;
}
if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL) {
return;
}
if (Objects.equals(newlyLoadedVideoId, videoId)) {
return;
}
videoId = newlyLoadedVideoId;
if (!VideoInformation.lastPlayerResponseIsAutoGeneratedMixPlaylist()) {
return;
}
VideoUtils.pauseMedia();
VideoUtils.openVideo(videoId);
}
public static boolean disableSpeedOverlay() {
return disableSpeedOverlay(true);
}
public static boolean disableSpeedOverlay(boolean original) {
return !Settings.DISABLE_SPEED_OVERLAY.get() && original;
}
public static double speedOverlayValue() {
return speedOverlayValue(2.0f);
}
public static float speedOverlayValue(float original) {
return SPEED_OVERLAY_VALUE;
}
public static boolean hideChannelWatermark(boolean original) {
return !Settings.HIDE_CHANNEL_WATERMARK.get() && original;
}
public static void hideCrowdfundingBox(View view) {
hideViewBy0dpUnderCondition(Settings.HIDE_CROWDFUNDING_BOX.get(), view);
}
public static void hideDoubleTapOverlayFilter(View view) {
hideViewByRemovingFromParentUnderCondition(Settings.HIDE_DOUBLE_TAP_OVERLAY_FILTER, view);
}
public static void hideEndScreenCards(View view) {
if (Settings.HIDE_END_SCREEN_CARDS.get()) {
view.setVisibility(View.GONE);
}
}
public static boolean hideFilmstripOverlay() {
return Settings.HIDE_FILMSTRIP_OVERLAY.get();
}
public static boolean hideInfoCard(boolean original) {
return !Settings.HIDE_INFO_CARDS.get() && original;
}
public static boolean hideSeekMessage() {
return Settings.HIDE_SEEK_MESSAGE.get();
}
public static boolean hideSeekUndoMessage() {
return Settings.HIDE_SEEK_UNDO_MESSAGE.get();
}
public static void hideSuggestedActions(View view) {
hideViewUnderCondition(Settings.HIDE_SUGGESTED_ACTION.get(), view);
}
public static boolean hideSuggestedVideoEndScreen() {
return Settings.HIDE_SUGGESTED_VIDEO_END_SCREEN.get();
}
public static void skipAutoPlayCountdown(View view) {
if (!hideSuggestedVideoEndScreen())
return;
if (!Settings.SKIP_AUTOPLAY_COUNTDOWN.get())
return;
Utils.clickView(view);
}
public static boolean hideZoomOverlay() {
return Settings.HIDE_ZOOM_OVERLAY.get();
}
// endregion
// region [Hide player flyout menu] patch
private static final String QUALITY_LABEL_PREMIUM = "1080p Premium";
public static String hidePlayerFlyoutMenuEnhancedBitrate(String qualityLabel) {
return Settings.HIDE_PLAYER_FLYOUT_MENU_ENHANCED_BITRATE.get() &&
Objects.equals(QUALITY_LABEL_PREMIUM, qualityLabel)
? null
: qualityLabel;
}
public static void hidePlayerFlyoutMenuCaptionsFooter(View view) {
Utils.hideViewUnderCondition(
Settings.HIDE_PLAYER_FLYOUT_MENU_CAPTIONS_FOOTER.get(),
view
);
}
public static void hidePlayerFlyoutMenuQualityFooter(View view) {
Utils.hideViewUnderCondition(
Settings.HIDE_PLAYER_FLYOUT_MENU_QUALITY_FOOTER.get(),
view
);
}
public static View hidePlayerFlyoutMenuQualityHeader(View view) {
return Settings.HIDE_PLAYER_FLYOUT_MENU_QUALITY_HEADER.get()
? new View(view.getContext()) // empty view
: view;
}
/**
* Overriding this values is possible only after the litho component has been loaded.
* Otherwise, crash will occur.
* See {@link InitializationPatch#onCreate}.
*
* @param original original value.
* @return whether to enable PiP Mode in the player flyout menu.
*/
public static boolean hidePiPModeMenu(boolean original) {
if (!BaseSettings.SETTINGS_INITIALIZED.get()) {
return original;
}
return !Settings.HIDE_PLAYER_FLYOUT_MENU_PIP.get();
}
// endregion
// region [Seekbar components] patch
public static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000;
public static String appendTimeStampInformation(String original) {
if (!Settings.APPEND_TIME_STAMP_INFORMATION.get()) return original;
String appendString = Settings.APPEND_TIME_STAMP_INFORMATION_TYPE.get()
? VideoUtils.getFormattedQualityString(null)
: VideoUtils.getFormattedSpeedString(null);
// Encapsulate the entire appendString with bidi control characters
appendString = "\u2066" + appendString + "\u2069";
// Format the original string with the appended timestamp information
return String.format(
"%s\u2009•\u2009%s", // Add the separator and the appended information
original, appendString
);
}
public static void setContainerClickListener(View view) {
if (!Settings.APPEND_TIME_STAMP_INFORMATION.get())
return;
if (!(view.getParent() instanceof View containerView))
return;
final BooleanSetting appendTypeSetting = Settings.APPEND_TIME_STAMP_INFORMATION_TYPE;
final boolean previousBoolean = appendTypeSetting.get();
containerView.setOnLongClickListener(timeStampContainerView -> {
appendTypeSetting.save(!previousBoolean);
return true;
}
);
if (Settings.REPLACE_TIME_STAMP_ACTION.get()) {
containerView.setOnClickListener(timeStampContainerView -> VideoUtils.showFlyoutMenu());
}
}
public static int getSeekbarClickedColorValue(final int colorValue) {
return colorValue == ORIGINAL_SEEKBAR_COLOR
? overrideSeekbarColor(colorValue)
: colorValue;
}
public static int resumedProgressBarColor(final int colorValue) {
return Settings.ENABLE_CUSTOM_SEEKBAR_COLOR.get()
? getSeekbarClickedColorValue(colorValue)
: colorValue;
}
/**
* Overrides all drawable color that use the YouTube seekbar color.
* Used only for the video thumbnails seekbar.
* <p>
* If {@link Settings#HIDE_SEEKBAR_THUMBNAIL} is enabled, this returns a fully transparent color.
*/
public static int getColor(int colorValue) {
if (colorValue == ORIGINAL_SEEKBAR_COLOR) {
if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) {
return 0x00000000;
}
return overrideSeekbarColor(ORIGINAL_SEEKBAR_COLOR);
}
return colorValue;
}
/**
* Points where errors occur when playing videos on the PlayStore (ROOT Build)
*/
public static int overrideSeekbarColor(final int colorValue) {
try {
return Settings.ENABLE_CUSTOM_SEEKBAR_COLOR.get()
? Color.parseColor(Settings.ENABLE_CUSTOM_SEEKBAR_COLOR_VALUE.get())
: colorValue;
} catch (Exception ignored) {
Settings.ENABLE_CUSTOM_SEEKBAR_COLOR_VALUE.resetToDefault();
}
return colorValue;
}
public static boolean enableSeekbarTapping() {
return Settings.ENABLE_SEEKBAR_TAPPING.get();
}
public static boolean enableHighQualityFullscreenThumbnails() {
return Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get();
}
private static final int timeBarChapterViewId =
ResourceUtils.getIdIdentifier("time_bar_chapter_title");
public static boolean hideSeekbar() {
return Settings.HIDE_SEEKBAR.get();
}
public static boolean disableSeekbarChapters() {
return Settings.DISABLE_SEEKBAR_CHAPTERS.get();
}
public static boolean hideSeekbarChapterLabel(View view) {
return Settings.HIDE_SEEKBAR_CHAPTER_LABEL.get() && view.getId() == timeBarChapterViewId;
}
public static boolean hideTimeStamp() {
return Settings.HIDE_TIME_STAMP.get();
}
public static boolean restoreOldSeekbarThumbnails() {
return !Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get();
}
public static boolean enableCairoSeekbar() {
return Settings.ENABLE_CAIRO_SEEKBAR.get();
}
// endregion
public static int getQuickActionsTopMargin() {
if (!PatchStatus.QuickActions()) {
return 0;
}
return QUICK_ACTIONS_MARGIN_TOP;
}
}

View File

@ -0,0 +1,84 @@
package app.revanced.extension.youtube.patches.shorts;
import static app.revanced.extension.shared.utils.ResourceUtils.getRawIdentifier;
import static app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType.ORIGINAL;
import androidx.annotation.Nullable;
import com.airbnb.lottie.LottieAnimationView;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.youtube.patches.utils.LottieAnimationViewPatch;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class AnimationFeedbackPatch {
public enum AnimationType {
/**
* Unmodified type, and same as un-patched.
*/
ORIGINAL(null),
THUMBS_UP("like_tap_feedback"),
THUMBS_UP_CAIRO("like_tap_feedback_cairo"),
HEART("like_tap_feedback_heart"),
HEART_TINT("like_tap_feedback_heart_tint"),
HIDDEN("like_tap_feedback_hidden");
/**
* Animation id.
*/
final int rawRes;
AnimationType(@Nullable String jsonName) {
this.rawRes = jsonName != null
? getRawIdentifier(jsonName)
: 0;
}
}
private static final AnimationType CURRENT_TYPE = Settings.ANIMATION_TYPE.get();
private static final boolean HIDE_PLAY_PAUSE_FEEDBACK = Settings.HIDE_SHORTS_PLAY_PAUSE_BUTTON_BACKGROUND.get();
private static final int PAUSE_TAP_FEEDBACK_HIDDEN
= ResourceUtils.getRawIdentifier("pause_tap_feedback_hidden");
private static final int PLAY_TAP_FEEDBACK_HIDDEN
= ResourceUtils.getRawIdentifier("play_tap_feedback_hidden");
/**
* Injection point.
*/
public static void setShortsLikeFeedback(LottieAnimationView lottieAnimationView) {
if (CURRENT_TYPE == ORIGINAL) {
return;
}
LottieAnimationViewPatch.setLottieAnimationRawResources(lottieAnimationView, CURRENT_TYPE.rawRes);
}
/**
* Injection point.
*/
public static void setShortsPauseFeedback(LottieAnimationView lottieAnimationView) {
if (!HIDE_PLAY_PAUSE_FEEDBACK) {
return;
}
LottieAnimationViewPatch.setLottieAnimationRawResources(lottieAnimationView, PAUSE_TAP_FEEDBACK_HIDDEN);
}
/**
* Injection point.
*/
public static void setShortsPlayFeedback(LottieAnimationView lottieAnimationView) {
if (!HIDE_PLAY_PAUSE_FEEDBACK) {
return;
}
LottieAnimationViewPatch.setLottieAnimationRawResources(lottieAnimationView, PLAY_TAP_FEEDBACK_HIDDEN);
}
}

View File

@ -0,0 +1,224 @@
package app.revanced.extension.youtube.patches.shorts;
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
import java.lang.ref.WeakReference;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
public class ShortsPatch {
private static final boolean ENABLE_TIME_STAMP = Settings.ENABLE_TIME_STAMP.get();
public static final boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get();
private static final int META_PANEL_BOTTOM_MARGIN;
private static final double NAVIGATION_BAR_HEIGHT_PERCENTAGE;
static {
if (HIDE_SHORTS_NAVIGATION_BAR) {
ShortsPlayerState.getOnChange().addObserver((ShortsPlayerState state) -> {
setNavigationBarLayoutParams(state);
return null;
});
}
final int bottomMargin = validateValue(
Settings.META_PANEL_BOTTOM_MARGIN,
0,
64,
"revanced_shorts_meta_panel_bottom_margin_invalid_toast"
);
META_PANEL_BOTTOM_MARGIN = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (float) bottomMargin, Utils.getResources().getDisplayMetrics());
final int heightPercentage = validateValue(
Settings.SHORTS_NAVIGATION_BAR_HEIGHT_PERCENTAGE,
0,
100,
"revanced_shorts_navigation_bar_height_percentage_invalid_toast"
);
NAVIGATION_BAR_HEIGHT_PERCENTAGE = heightPercentage / 100d;
}
public static Enum<?> repeat;
public static Enum<?> singlePlay;
public static Enum<?> endScreen;
public static Enum<?> changeShortsRepeatState(Enum<?> currentState) {
switch (Settings.CHANGE_SHORTS_REPEAT_STATE.get()) {
case 1 -> currentState = repeat;
case 2 -> currentState = singlePlay;
case 3 -> currentState = endScreen;
}
return currentState;
}
public static boolean disableResumingStartupShortsPlayer() {
return Settings.DISABLE_RESUMING_SHORTS_PLAYER.get();
}
public static boolean enableShortsTimeStamp(boolean original) {
return ENABLE_TIME_STAMP || original;
}
public static int enableShortsTimeStamp(int original) {
return ENABLE_TIME_STAMP ? 10010 : original;
}
public static void setShortsMetaPanelBottomMargin(View view) {
if (!ENABLE_TIME_STAMP)
return;
if (!(view.getLayoutParams() instanceof RelativeLayout.LayoutParams lp))
return;
lp.setMargins(0, 0, 0, META_PANEL_BOTTOM_MARGIN);
lp.setMarginEnd(ResourceUtils.getDimension("reel_player_right_dyn_bar_width"));
}
public static void setShortsTimeStampChangeRepeatState(View view) {
if (!ENABLE_TIME_STAMP)
return;
if (!Settings.TIME_STAMP_CHANGE_REPEAT_STATE.get())
return;
if (view == null)
return;
view.setLongClickable(true);
view.setOnLongClickListener(view1 -> {
VideoUtils.showShortsRepeatDialog(view1.getContext());
return true;
});
}
public static void hideShortsCommentsButton(View view) {
hideViewUnderCondition(Settings.HIDE_SHORTS_COMMENTS_BUTTON.get(), view);
}
public static boolean hideShortsDislikeButton() {
return Settings.HIDE_SHORTS_DISLIKE_BUTTON.get();
}
public static ViewGroup hideShortsInfoPanel(ViewGroup viewGroup) {
return Settings.HIDE_SHORTS_INFO_PANEL.get() ? null : viewGroup;
}
public static boolean hideShortsLikeButton() {
return Settings.HIDE_SHORTS_LIKE_BUTTON.get();
}
public static boolean hideShortsPaidPromotionLabel() {
return Settings.HIDE_SHORTS_PAID_PROMOTION_LABEL.get();
}
public static void hideShortsPaidPromotionLabel(TextView textView) {
hideViewUnderCondition(Settings.HIDE_SHORTS_PAID_PROMOTION_LABEL.get(), textView);
}
public static void hideShortsRemixButton(View view) {
hideViewUnderCondition(Settings.HIDE_SHORTS_REMIX_BUTTON.get(), view);
}
public static void hideShortsShareButton(View view) {
hideViewUnderCondition(Settings.HIDE_SHORTS_SHARE_BUTTON.get(), view);
}
public static boolean hideShortsSoundButton() {
return Settings.HIDE_SHORTS_SOUND_BUTTON.get();
}
private static final int zeroPaddingDimenId =
ResourceUtils.getDimenIdentifier("revanced_zero_padding");
public static int getShortsSoundButtonDimenId(int dimenId) {
return Settings.HIDE_SHORTS_SOUND_BUTTON.get()
? zeroPaddingDimenId
: dimenId;
}
public static int hideShortsSubscribeButton(int original) {
return Settings.HIDE_SHORTS_SUBSCRIBE_BUTTON.get() ? 0 : original;
}
// YouTube 18.29.38 ~ YouTube 19.28.42
public static boolean hideShortsPausedHeader() {
return Settings.HIDE_SHORTS_PAUSED_HEADER.get();
}
// YouTube 19.29.42 ~
public static boolean hideShortsPausedHeader(boolean original) {
return Settings.HIDE_SHORTS_PAUSED_HEADER.get() || original;
}
public static boolean hideShortsToolBar(boolean original) {
return !Settings.HIDE_SHORTS_TOOLBAR.get() && original;
}
/**
* BottomBarContainer is the parent view of {@link PivotBar},
* And can be hidden using {@link View#setVisibility} only when it is initialized.
* <p>
* If it was not hidden with {@link View#setVisibility} when it was initialized,
* it should be hidden with {@link FrameLayout.LayoutParams}.
* <p>
* When Shorts is opened, {@link FrameLayout.LayoutParams} should be changed to 0dp,
* When Shorts is closed, {@link FrameLayout.LayoutParams} should be changed to the original.
*/
private static WeakReference<View> bottomBarContainerRef = new WeakReference<>(null);
private static FrameLayout.LayoutParams originalLayoutParams;
private static final FrameLayout.LayoutParams zeroLayoutParams =
new FrameLayout.LayoutParams(0, 0);
public static void setNavigationBar(View view) {
if (!HIDE_SHORTS_NAVIGATION_BAR) {
return;
}
bottomBarContainerRef = new WeakReference<>(view);
if (!(view.getLayoutParams() instanceof FrameLayout.LayoutParams lp)) {
return;
}
if (originalLayoutParams == null) {
originalLayoutParams = lp;
}
}
public static int setNavigationBarHeight(int original) {
return HIDE_SHORTS_NAVIGATION_BAR
? (int) Math.round(original * NAVIGATION_BAR_HEIGHT_PERCENTAGE)
: original;
}
private static void setNavigationBarLayoutParams(@NonNull ShortsPlayerState shortsPlayerState) {
final View navigationBar = bottomBarContainerRef.get();
if (navigationBar == null) {
return;
}
if (!(navigationBar.getLayoutParams() instanceof FrameLayout.LayoutParams lp)) {
return;
}
navigationBar.setLayoutParams(
shortsPlayerState.isClosed()
? originalLayoutParams
: zeroLayoutParams
);
}
}

View File

@ -0,0 +1,36 @@
package app.revanced.extension.youtube.patches.spans;
import android.text.SpannableString;
import app.revanced.extension.shared.patches.spans.Filter;
import app.revanced.extension.shared.patches.spans.SpanType;
import app.revanced.extension.shared.patches.spans.StringFilterGroup;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"unused", "ConstantValue", "FieldCanBeLocal"})
public final class SanitizeVideoSubtitleFilter extends Filter {
public SanitizeVideoSubtitleFilter() {
addCallbacks(
new StringFilterGroup(
Settings.SANITIZE_VIDEO_SUBTITLE,
"|video_subtitle.eml|"
)
);
}
@Override
public boolean skip(String conversionContext, SpannableString spannableString, Object span,
int start, int end, int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) {
if (isWord) {
if (spanType == SpanType.IMAGE) {
hideImageSpan(spannableString, start, end, flags);
return super.skip(conversionContext, spannableString, span, start, end, flags, isWord, spanType, matchedGroup);
} else if (spanType == SpanType.CUSTOM_CHARACTER_STYLE) {
hideSpan(spannableString, start, end, flags);
return super.skip(conversionContext, spannableString, span, start, end, flags, isWord, spanType, matchedGroup);
}
}
return false;
}
}

View File

@ -0,0 +1,52 @@
package app.revanced.extension.youtube.patches.spans;
import android.text.SpannableString;
import app.revanced.extension.shared.patches.spans.Filter;
import app.revanced.extension.shared.patches.spans.SpanType;
import app.revanced.extension.shared.patches.spans.StringFilterGroup;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"unused", "ConstantValue", "FieldCanBeLocal"})
public final class SearchLinksFilter extends Filter {
/**
* Located in front of the search icon.
*/
private final String WORD_JOINER_CHARACTER = "\u2060";
public SearchLinksFilter() {
addCallbacks(
new StringFilterGroup(
Settings.HIDE_COMMENT_HIGHLIGHTED_SEARCH_LINKS,
"|comment."
)
);
}
/**
* @return Whether the word contains a search icon or not.
*/
private boolean isSearchLinks(SpannableString original, int end) {
String originalString = original.toString();
int wordJoinerIndex = originalString.indexOf(WORD_JOINER_CHARACTER);
// There may be more than one highlight keyword in the comment.
// Check the index of all highlight keywords.
while (wordJoinerIndex != -1) {
if (end - wordJoinerIndex == 2) return true;
wordJoinerIndex = originalString.indexOf(WORD_JOINER_CHARACTER, wordJoinerIndex + 1);
}
return false;
}
@Override
public boolean skip(String conversionContext, SpannableString spannableString, Object span,
int start, int end, int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) {
if (isWord && isSearchLinks(spannableString, end)) {
if (spanType == SpanType.IMAGE) {
hideSpan(spannableString, start, end, flags);
}
return super.skip(conversionContext, spannableString, span, start, end, flags, isWord, spanType, matchedGroup);
}
return false;
}
}

View File

@ -0,0 +1,48 @@
package app.revanced.extension.youtube.patches.swipe;
import android.view.View;
import java.lang.ref.WeakReference;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"unused", "deprecation"})
public class SwipeControlsPatch {
private static WeakReference<View> fullscreenEngagementOverlayViewRef = new WeakReference<>(null);
/**
* Injection point.
*/
public static boolean disableHDRAutoBrightness() {
return Settings.DISABLE_HDR_AUTO_BRIGHTNESS.get();
}
/**
* Injection point.
*/
public static boolean enableSwipeToSwitchVideo() {
return Settings.ENABLE_SWIPE_TO_SWITCH_VIDEO.get();
}
/**
* Injection point.
*/
public static boolean enableWatchPanelGestures() {
return Settings.ENABLE_WATCH_PANEL_GESTURES.get();
}
/**
* Injection point.
*
* @param fullscreenEngagementOverlayView R.layout.fullscreen_engagement_overlay
*/
public static void setFullscreenEngagementOverlayView(View fullscreenEngagementOverlayView) {
fullscreenEngagementOverlayViewRef = new WeakReference<>(fullscreenEngagementOverlayView);
}
public static boolean isEngagementOverlayVisible() {
final View engagementOverlayView = fullscreenEngagementOverlayViewRef.get();
return engagementOverlayView != null && engagementOverlayView.getVisibility() == View.VISIBLE;
}
}

View File

@ -0,0 +1,29 @@
package app.revanced.extension.youtube.patches.utils;
import static app.revanced.extension.youtube.utils.VideoUtils.pauseMedia;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.VideoInformation;
@SuppressWarnings("unused")
public class AlwaysRepeatPatch extends Utils {
/**
* Injection point.
*
* @return video is repeated.
*/
public static boolean alwaysRepeat() {
return alwaysRepeatEnabled() && VideoInformation.overrideVideoTime(0);
}
public static boolean alwaysRepeatEnabled() {
final boolean alwaysRepeat = Settings.ALWAYS_REPEAT.get();
final boolean alwaysRepeatPause = Settings.ALWAYS_REPEAT_PAUSE.get();
if (alwaysRepeat && alwaysRepeatPause) pauseMedia();
return alwaysRepeat;
}
}

View File

@ -0,0 +1,21 @@
package app.revanced.extension.youtube.patches.utils;
import app.revanced.extension.youtube.shared.BottomSheetState;
@SuppressWarnings("unused")
public class BottomSheetHookPatch {
/**
* Injection point.
*/
public static void onAttachedToWindow() {
BottomSheetState.set(BottomSheetState.OPEN);
}
/**
* Injection point.
*/
public static void onDetachedFromWindow() {
BottomSheetState.set(BottomSheetState.CLOSED);
}
}

View File

@ -0,0 +1,25 @@
package app.revanced.extension.youtube.patches.utils;
import android.view.View;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class CastButtonPatch {
/**
* The [Hide cast button] setting is separated into the [Hide cast button in player] setting and the [Hide cast button in toolbar] setting.
* Always hide the cast button when both settings are true.
* <p>
* These two settings belong to different patches, and since the default value for this setting is true,
* it is essential to ensure that each patch is included to ensure independent operation.
*/
public static int hideCastButton(int original) {
return Settings.HIDE_TOOLBAR_CAST_BUTTON.get()
&& PatchStatus.ToolBarComponents()
&& Settings.HIDE_PLAYER_CAST_BUTTON.get()
&& PatchStatus.PlayerButtons()
? View.GONE
: original;
}
}

View File

@ -0,0 +1,66 @@
package app.revanced.extension.youtube.patches.utils;
import android.app.Activity;
import androidx.annotation.NonNull;
import app.revanced.extension.youtube.settings.Settings;
/**
* @noinspection ALL
*/
public class DoubleBackToClosePatch {
/**
* Time between two back button presses
*/
private static final long PRESSED_TIMEOUT_MILLISECONDS = Settings.DOUBLE_BACK_TO_CLOSE_TIMEOUT.get();
/**
* Last time back button was pressed
*/
private static long lastTimeBackPressed = 0;
/**
* State whether scroll position reaches the top
*/
private static boolean isScrollTop = false;
/**
* Detect event when back button is pressed
*
* @param activity is used when closing the app
*/
public static void closeActivityOnBackPressed(@NonNull Activity activity) {
// Check scroll position reaches the top in home feed
if (!isScrollTop)
return;
final long currentTime = System.currentTimeMillis();
// If the time between two back button presses does not reach PRESSED_TIMEOUT_MILLISECONDS,
// set lastTimeBackPressed to the current time.
if (currentTime - lastTimeBackPressed < PRESSED_TIMEOUT_MILLISECONDS ||
PRESSED_TIMEOUT_MILLISECONDS == 0)
activity.finish();
else
lastTimeBackPressed = currentTime;
}
/**
* Detect event when ScrollView is created by RecyclerView
* <p>
* start of ScrollView
*/
public static void onStartScrollView() {
isScrollTop = false;
}
/**
* Detect event when the scroll position reaches the top by the back button
* <p>
* stop of ScrollView
*/
public static void onStopScrollView() {
isScrollTop = true;
}
}

View File

@ -0,0 +1,50 @@
package app.revanced.extension.youtube.patches.utils;
import app.revanced.extension.shared.utils.ResourceUtils;
@SuppressWarnings("unused")
public class DrawableColorPatch {
private static final int[] WHITE_VALUES = {
-1, // comments chip background
-394759, // music related results panel background
-83886081 // video chapters list background
};
private static final int[] DARK_VALUES = {
-14145496, // drawer content view background
-14606047, // comments chip background
-15198184, // music related results panel background
-15790321, // comments chip background (new layout)
-98492127 // video chapters list background
};
// background colors
private static int whiteColor = 0;
private static int blackColor = 0;
public static int getColor(int originalValue) {
if (anyEquals(originalValue, DARK_VALUES)) {
return getBlackColor();
} else if (anyEquals(originalValue, WHITE_VALUES)) {
return getWhiteColor();
}
return originalValue;
}
private static int getBlackColor() {
if (blackColor == 0) blackColor = ResourceUtils.getColor("yt_black1");
return blackColor;
}
private static int getWhiteColor() {
if (whiteColor == 0) whiteColor = ResourceUtils.getColor("yt_white1");
return whiteColor;
}
private static boolean anyEquals(int value, int... of) {
for (int v : of) if (value == v) return true;
return false;
}
}

View File

@ -0,0 +1,39 @@
package app.revanced.extension.youtube.patches.utils;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.runOnMainThreadDelayed;
import android.app.Activity;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.youtube.utils.ExtendedUtils;
@SuppressWarnings("unused")
public class InitializationPatch {
private static final BooleanSetting SETTINGS_INITIALIZED = BaseSettings.SETTINGS_INITIALIZED;
/**
* Some layouts that depend on litho do not load when the app is first installed.
* (Also reproduced on unPatched YouTube)
* <p>
* To fix this, show the restart dialog when the app is installed for the first time.
*/
public static void onCreate(@NonNull Activity mActivity) {
if (SETTINGS_INITIALIZED.get()) {
return;
}
runOnMainThreadDelayed(() -> showRestartDialog(mActivity, str("revanced_extended_restart_first_run"), 3500), 500);
runOnMainThreadDelayed(() -> SETTINGS_INITIALIZED.save(true), 1000);
}
public static void setExtendedUtils(@NonNull Activity mActivity) {
ExtendedUtils.setApplicationLabel();
ExtendedUtils.setSmallestScreenWidthDp();
ExtendedUtils.setVersionName();
ExtendedUtils.setPlayerFlyoutMenuAdditionalSettings();
}
}

View File

@ -0,0 +1,18 @@
package app.revanced.extension.youtube.patches.utils;
import androidx.annotation.Nullable;
import app.revanced.extension.youtube.shared.LockModeState;
@SuppressWarnings("unused")
public class LockModeStateHookPatch {
/**
* Injection point.
*/
public static void setLockModeState(@Nullable Enum<?> lockModeState) {
if (lockModeState == null) return;
LockModeState.setFromString(lockModeState.name());
}
}

View File

@ -0,0 +1,25 @@
package app.revanced.extension.youtube.patches.utils;
import com.airbnb.lottie.LottieAnimationView;
import app.revanced.extension.shared.utils.Logger;
public class LottieAnimationViewPatch {
public static void setLottieAnimationRawResources(LottieAnimationView lottieAnimationView, int rawRes) {
if (lottieAnimationView == null) {
Logger.printDebug(() -> "View is null");
return;
}
if (rawRes == 0) {
Logger.printDebug(() -> "Resource is not found");
return;
}
setAnimation(lottieAnimationView, rawRes);
}
@SuppressWarnings("unused")
private static void setAnimation(LottieAnimationView lottieAnimationView, int rawRes) {
// Rest of the implementation added by patch.
}
}

View File

@ -0,0 +1,50 @@
package app.revanced.extension.youtube.patches.utils;
public class PatchStatus {
public static boolean ImageSearchButton() {
// Replace this with true if the Hide image search buttons patch succeeds
return false;
}
public static boolean MinimalHeader() {
// Replace this with true If the Custom header patch succeeds and the patch option was `youtube_minimal_header`
return false;
}
public static boolean PlayerButtons() {
// Replace this with true if the Hide player buttons patch succeeds
return false;
}
public static boolean QuickActions() {
// Replace this with true if the Fullscreen components patch succeeds
return false;
}
public static boolean RememberPlaybackSpeed() {
// Replace this with true if the Video playback patch succeeds
return false;
}
public static boolean SponsorBlock() {
// Replace this with true if the SponsorBlock patch succeeds
return false;
}
public static boolean ToolBarComponents() {
// Replace this with true if the Toolbar components patch succeeds
return false;
}
// Modified by a patch. Do not touch.
public static String RVXMusicPackageName() {
return "com.google.android.apps.youtube.music";
}
// Modified by a patch. Do not touch.
public static boolean OldSeekbarThumbnailsDefaultBoolean() {
return false;
}
}

View File

@ -0,0 +1,122 @@
package app.revanced.extension.youtube.patches.utils;
import static app.revanced.extension.shared.utils.ResourceUtils.getIdIdentifier;
import android.view.View;
import androidx.annotation.NonNull;
import java.lang.ref.WeakReference;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.shared.PlayerControlsVisibility;
/**
* @noinspection ALL
*/
public class PlayerControlsPatch {
private static WeakReference<View> playerOverflowButtonViewRef = new WeakReference<>(null);
private static final int playerOverflowButtonId =
getIdIdentifier("player_overflow_button");
/**
* Injection point.
*/
public static void initializeBottomControlButton(View bottomControlsViewGroup) {
// AlwaysRepeat.initialize(bottomControlsViewGroup);
// CopyVideoUrl.initialize(bottomControlsViewGroup);
// CopyVideoUrlTimestamp.initialize(bottomControlsViewGroup);
// MuteVolume.initialize(bottomControlsViewGroup);
// ExternalDownload.initialize(bottomControlsViewGroup);
// SpeedDialog.initialize(bottomControlsViewGroup);
// TimeOrderedPlaylist.initialize(bottomControlsViewGroup);
// Whitelists.initialize(bottomControlsViewGroup);
}
/**
* Injection point.
*/
public static void initializeTopControlButton(View youtubeControlsLayout) {
// CreateSegmentButtonController.initialize(youtubeControlsLayout);
// VotingButtonController.initialize(youtubeControlsLayout);
}
/**
* Injection point.
* Legacy method.
* <p>
* Player overflow button view does not attach to windows immediately after cold start.
* Player overflow button view is not attached to the windows until the user touches the player at least once, and the overlay buttons are hidden until then.
* To prevent this, uses the legacy method to show the overlay button until the player overflow button view is attached to the windows.
*/
public static void changeVisibility(boolean showing) {
if (playerOverflowButtonViewRef.get() != null) {
return;
}
changeVisibility(showing, false);
}
private static void changeVisibility(boolean showing, boolean animation) {
// AlwaysRepeat.changeVisibility(showing, animation);
// CopyVideoUrl.changeVisibility(showing, animation);
// CopyVideoUrlTimestamp.changeVisibility(showing, animation);
// MuteVolume.changeVisibility(showing, animation);
// ExternalDownload.changeVisibility(showing, animation);
// SpeedDialog.changeVisibility(showing, animation);
// TimeOrderedPlaylist.changeVisibility(showing, animation);
// Whitelists.changeVisibility(showing, animation);
// CreateSegmentButtonController.changeVisibility(showing, animation);
// VotingButtonController.changeVisibility(showing, animation);
}
/**
* Injection point.
* New method.
* <p>
* Show or hide the overlay button when the player overflow button view is visible and hidden, respectively.
* <p>
* Inject the current view into {@link PlayerControlsPatch#playerOverflowButtonView} to check that the player overflow button view is attached to the window.
* From this point on, the legacy method is deprecated.
*/
public static void changeVisibility(boolean showing, boolean animation, @NonNull View view) {
if (view.getId() != playerOverflowButtonId) {
return;
}
if (playerOverflowButtonViewRef.get() == null) {
Utils.runOnMainThreadDelayed(() -> playerOverflowButtonViewRef = new WeakReference<>(view), 1400);
}
changeVisibility(showing, animation);
}
/**
* Injection point.
* <p>
* Called whenever a motion event occurs on the player controller.
* <p>
* When the user touches the player overlay (motion event occurs), the player overlay disappears immediately.
* In this case, the overlay buttons should also disappear immediately.
* <p>
* In other words, this method detects when the player overlay disappears immediately upon the user's touch,
* and quickly fades out all overlay buttons.
*/
public static void changeVisibilityNegatedImmediate() {
if (PlayerControlsVisibility.getCurrent() == PlayerControlsVisibility.PLAYER_CONTROLS_VISIBILITY_HIDDEN) {
changeVisibilityNegatedImmediately();
}
}
private static void changeVisibilityNegatedImmediately() {
// AlwaysRepeat.changeVisibilityNegatedImmediate();
// CopyVideoUrl.changeVisibilityNegatedImmediate();
// CopyVideoUrlTimestamp.changeVisibilityNegatedImmediate();
// MuteVolume.changeVisibilityNegatedImmediate();
// ExternalDownload.changeVisibilityNegatedImmediate();
// SpeedDialog.changeVisibilityNegatedImmediate();
// TimeOrderedPlaylist.changeVisibilityNegatedImmediate();
// Whitelists.changeVisibilityNegatedImmediate();
// CreateSegmentButtonController.changeVisibilityNegatedImmediate();
// VotingButtonController.changeVisibilityNegatedImmediate();
}
}

View File

@ -0,0 +1,18 @@
package app.revanced.extension.youtube.patches.utils;
import androidx.annotation.Nullable;
import app.revanced.extension.youtube.shared.PlayerControlsVisibility;
@SuppressWarnings("unused")
public class PlayerControlsVisibilityHookPatch {
/**
* Injection point.
*/
public static void setPlayerControlsVisibility(@Nullable Enum<?> youTubePlayerControlsVisibility) {
if (youTubePlayerControlsVisibility == null) return;
PlayerControlsVisibility.setFromString(youTubePlayerControlsVisibility.name());
}
}

View File

@ -0,0 +1,52 @@
package app.revanced.extension.youtube.patches.utils;
import android.view.View;
import androidx.annotation.Nullable;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
import app.revanced.extension.youtube.shared.VideoState;
@SuppressWarnings("unused")
public class PlayerTypeHookPatch {
/**
* Injection point.
*/
public static void setPlayerType(@Nullable Enum<?> youTubePlayerType) {
if (youTubePlayerType == null) return;
PlayerType.setFromString(youTubePlayerType.name());
}
/**
* Injection point.
*/
public static void setVideoState(@Nullable Enum<?> youTubeVideoState) {
if (youTubeVideoState == null) return;
VideoState.setFromString(youTubeVideoState.name());
}
/**
* Injection point.
* <p>
* Add a listener to the shorts player overlay View.
* Triggered when a shorts player is attached or detached to Windows.
*
* @param view shorts player overlay (R.id.reel_watch_player).
*/
public static void onShortsCreate(View view) {
view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(@Nullable View v) {
ShortsPlayerState.set(ShortsPlayerState.OPEN);
}
@Override
public void onViewDetachedFromWindow(@Nullable View v) {
ShortsPlayerState.set(ShortsPlayerState.CLOSED);
}
});
}
}

View File

@ -0,0 +1,46 @@
package app.revanced.extension.youtube.patches.utils;
import static app.revanced.extension.youtube.patches.player.PlayerPatch.ORIGINAL_SEEKBAR_COLOR;
import static app.revanced.extension.youtube.patches.player.PlayerPatch.resumedProgressBarColor;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class ProgressBarDrawable extends Drawable {
private final Paint paint = new Paint();
@Override
public void draw(@NonNull Canvas canvas) {
if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) {
return;
}
paint.setColor(resumedProgressBarColor(ORIGINAL_SEEKBAR_COLOR));
canvas.drawRect(getBounds(), paint);
}
@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}

View File

@ -0,0 +1,130 @@
package app.revanced.extension.youtube.patches.utils;
import androidx.annotation.NonNull;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class ReturnYouTubeChannelNamePatch {
private static final boolean REPLACE_CHANNEL_HANDLE = Settings.REPLACE_CHANNEL_HANDLE.get();
/**
* The last character of some handles is an official channel certification mark.
* This was in the form of nonBreakSpaceCharacter before SpannableString was made.
*/
private static final String NON_BREAK_SPACE_CHARACTER = "\u00A0";
private volatile static String channelName = "";
/**
* Key: channelId, Value: channelName.
*/
private static final Map<String, String> channelIdMap = Collections.synchronizedMap(
new LinkedHashMap<>(20) {
private static final int CACHE_LIMIT = 10;
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
}
});
/**
* Key: handle, Value: channelName.
*/
private static final Map<String, String> channelHandleMap = Collections.synchronizedMap(
new LinkedHashMap<>(20) {
private static final int CACHE_LIMIT = 10;
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
}
});
/**
* This method is only invoked on Shorts and is updated whenever the user swipes up or down on the Shorts.
*/
public static void newShortsVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
if (!REPLACE_CHANNEL_HANDLE) {
return;
}
if (channelIdMap.get(newlyLoadedChannelId) != null) {
return;
}
if (channelIdMap.put(newlyLoadedChannelId, newlyLoadedChannelName) == null) {
channelName = newlyLoadedChannelName;
Logger.printDebug(() -> "New video started, ChannelId " + newlyLoadedChannelId + ", Channel Name: " + newlyLoadedChannelName);
}
}
/**
* Injection point.
*/
public static CharSequence onCharSequenceLoaded(@NonNull Object conversionContext,
@NonNull CharSequence charSequence) {
try {
if (!REPLACE_CHANNEL_HANDLE) {
return charSequence;
}
final String conversionContextString = conversionContext.toString();
if (!conversionContextString.contains("|reel_channel_bar_inner.eml|")) {
return charSequence;
}
final String originalString = charSequence.toString();
if (!originalString.startsWith("@")) {
return charSequence;
}
return getChannelName(originalString);
} catch (Exception ex) {
Logger.printException(() -> "onCharSequenceLoaded failed", ex);
}
return charSequence;
}
private static CharSequence getChannelName(@NonNull String handle) {
final String trimmedHandle = handle.replaceAll(NON_BREAK_SPACE_CHARACTER, "");
String cachedChannelName = channelHandleMap.get(trimmedHandle);
if (cachedChannelName == null) {
if (!channelName.isEmpty() && channelHandleMap.put(handle, channelName) == null) {
Logger.printDebug(() -> "Set Handle from last fetched Channel Name, Handle: " + handle + ", Channel Name: " + channelName);
cachedChannelName = channelName;
} else {
Logger.printDebug(() -> "Channel handle is not found: " + trimmedHandle);
return handle;
}
}
if (handle.contains(NON_BREAK_SPACE_CHARACTER)) {
cachedChannelName += NON_BREAK_SPACE_CHARACTER;
}
String replacedChannelName = cachedChannelName;
Logger.printDebug(() -> "Replace Handle " + handle + " to " + replacedChannelName);
return replacedChannelName;
}
public synchronized static void setLastShortsChannelId(@NonNull String handle, @NonNull String channelId) {
try {
if (channelHandleMap.get(handle) != null) {
return;
}
final String channelName = channelIdMap.get(channelId);
if (channelName == null) {
Logger.printDebug(() -> "Channel name is not found!");
return;
}
if (channelHandleMap.put(handle, channelName) == null) {
Logger.printDebug(() -> "Set Handle from Shorts, Handle: " + handle + ", Channel Name: " + channelName);
}
} catch (Exception ex) {
Logger.printException(() -> "setLastShortsChannelId failure ", ex);
}
}
}

View File

@ -0,0 +1,690 @@
package app.revanced.extension.youtube.patches.utils;
import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote;
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import app.revanced.extension.shared.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch;
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.VideoInformation;
/**
* Handles all interaction of UI patch components.
* <p>
* Known limitation:
* 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.
* <p>
* 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 =
isSpoofingToLessThan("18.34.00");
/**
* RYD data for the current video on screen.
*/
@Nullable
private static volatile ReturnYouTubeDislike currentVideoData;
/**
* The last litho based Shorts loaded.
* May be the same value as {@link #currentVideoData}, but usually is the next short to swipe to.
*/
@Nullable
private static volatile ReturnYouTubeDislike lastLithoShortsVideoData;
/**
* Because the litho Shorts spans are created after {@link ReturnYouTubeDislikeFilterPatch}
* detects the video ids, after the user votes the litho will update
* but {@link #lastLithoShortsVideoData} is not the correct data to use.
* If this is true, then instead use {@link #currentVideoData}.
*/
private static volatile boolean lithoShortsShouldUseCurrentData;
/**
* Last video id prefetched. Field is to prevent prefetching the same video id multiple times in a row.
*/
@Nullable
private static volatile String lastPrefetchedVideoId;
public static void onRYDStatusChange() {
ReturnYouTubeDislikeApi.resetRateLimits();
// Must remove all values to protect against using stale data
// if the user enables RYD while a video is on screen.
clearData();
}
private static void clearData() {
currentVideoData = null;
lastLithoShortsVideoData = null;
lithoShortsShouldUseCurrentData = false;
// Rolling number text should not be cleared,
// as it's used if incognito Short is opened/closed
// while a regular video is on screen.
}
//
// Litho player for both regular videos and Shorts.
//
/**
* Injection point.
* <p>
* For Litho segmented buttons and Litho Shorts player.
*/
@NonNull
public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
@NonNull CharSequence original) {
return onLithoTextLoaded(conversionContext, original, false);
}
/**
* Injection point.
* <p>
* Called when a litho text component is initially created,
* and also when a Span is later reused again (such as scrolling off/on screen).
* <p>
* This method is sometimes called on the main thread, but it usually is called _off_ the main thread.
* This method can be called multiple times for the same UI element (including after dislikes was added).
*
* @param original Original char sequence was created or reused by Litho.
* @param isRollingNumber If the span is for a Rolling Number.
* @return The original char sequence (if nothing should change), or a replacement char sequence that contains dislikes.
*/
@NonNull
private static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
@NonNull CharSequence original,
boolean isRollingNumber) {
try {
if (!Settings.RYD_ENABLED.get()) {
return original;
}
String conversionContextString = conversionContext.toString();
if (isRollingNumber && !conversionContextString.contains("video_action_bar.eml")) {
return original;
}
if (conversionContextString.contains("segmented_like_dislike_button.eml")) {
// Regular video.
ReturnYouTubeDislike videoData = currentVideoData;
if (videoData == null) {
return original; // User enabled RYD while a video was on screen.
}
if (!(original instanceof Spanned)) {
original = new SpannableString(original);
}
return videoData.getDislikesSpanForRegularVideo((Spanned) original,
true, isRollingNumber);
}
if (isRollingNumber) {
return original; // No need to check for Shorts in the context.
}
if (conversionContextString.contains("|shorts_dislike_button.eml")) {
return getShortsSpan(original, true);
}
if (conversionContextString.contains("|shorts_like_button.eml")
&& !Utils.containsNumber(original)) {
Logger.printDebug(() -> "Replacing hidden likes count");
return getShortsSpan(original, false);
}
} catch (Exception ex) {
Logger.printException(() -> "onLithoTextLoaded failure", ex);
}
return original;
}
//
// Litho Shorts player in the incognito mode / live stream.
//
/**
* Injection point.
* <p>
* This method is used in the following situations.
* <p>
* 1. When the dislike counts are fetched in the Incognito mode.
* 2. When the dislike counts are fetched in the live stream.
*
* @param original Original span that was created or reused by Litho.
* @return The original span (if nothing should change), or a replacement span that contains dislikes.
*/
public static CharSequence onCharSequenceLoaded(@NonNull Object conversionContext,
@NonNull CharSequence original) {
try {
String conversionContextString = conversionContext.toString();
if (!Settings.RYD_ENABLED.get()) {
return original;
}
if (!Settings.RYD_SHORTS.get()) {
return original;
}
final boolean fetchDislikeLiveStream =
conversionContextString.contains("immersive_live_video_action_bar.eml")
&& conversionContextString.contains("|dislike_button.eml|");
if (!fetchDislikeLiveStream) {
return original;
}
ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(ReturnYouTubeDislikeFilterPatch.getShortsVideoId());
videoData.setVideoIdIsShort(true);
lastLithoShortsVideoData = videoData;
lithoShortsShouldUseCurrentData = false;
return videoData.getDislikeSpanForShort(SHORTS_LOADING_SPAN);
} catch (Exception ex) {
Logger.printException(() -> "onCharSequenceLoaded failure", ex);
}
return original;
}
private static CharSequence getShortsSpan(@NonNull CharSequence original, boolean isDislikesSpan) {
// Litho Shorts player.
if (!Settings.RYD_SHORTS.get() || (isDislikesSpan && Settings.HIDE_SHORTS_DISLIKE_BUTTON.get())
|| (!isDislikesSpan && Settings.HIDE_SHORTS_LIKE_BUTTON.get())) {
return original;
}
ReturnYouTubeDislike videoData = lastLithoShortsVideoData;
if (videoData == null) {
// The Shorts litho video id filter did not detect the video id.
// This is normal in incognito mode, but otherwise is abnormal.
Logger.printDebug(() -> "Cannot modify Shorts litho span, data is null");
return original;
}
// Use the correct dislikes data after voting.
if (lithoShortsShouldUseCurrentData) {
if (isDislikesSpan) {
lithoShortsShouldUseCurrentData = false;
}
videoData = currentVideoData;
if (videoData == null) {
Logger.printException(() -> "currentVideoData is null"); // Should never happen
return original;
}
Logger.printDebug(() -> "Using current video data for litho span");
}
return isDislikesSpan
? videoData.getDislikeSpanForShort((Spanned) original)
: videoData.getLikeSpanForShort((Spanned) original);
}
//
// Rolling Number
//
/**
* Current regular video rolling number text, if rolling number is in use.
* This is saved to a field as it's used in every draw() call.
*/
@Nullable
private static volatile CharSequence rollingNumberSpan;
/**
* Injection point.
*/
public static String onRollingNumberLoaded(@NonNull Object conversionContext,
@NonNull String original) {
try {
CharSequence replacement = onLithoTextLoaded(conversionContext, original, true);
String replacementString = replacement.toString();
if (!replacementString.equals(original)) {
rollingNumberSpan = replacement;
return replacementString;
} // Else, the text was not a likes count but instead the view count or something else.
} catch (Exception ex) {
Logger.printException(() -> "onRollingNumberLoaded failure", ex);
}
return original;
}
/**
* Injection point.
* <p>
* Called for all usage of Rolling Number.
* Modifies the measured String text width to include the left separator and padding, if needed.
*/
public static float onRollingNumberMeasured(String text, float measuredTextWidth) {
try {
if (Settings.RYD_ENABLED.get()) {
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.
if (Settings.RYD_COMPACT_LAYOUT.get()) {
return measuredTextWidth + 1;
}
return measuredTextWidth + 1
+ ReturnYouTubeDislike.leftSeparatorBounds.right
+ ReturnYouTubeDislike.leftSeparatorShapePaddingPixels;
}
}
} catch (Exception ex) {
Logger.printException(() -> "onRollingNumberMeasured failure", ex);
}
return measuredTextWidth;
}
/**
* Add Rolling Number text view modifications.
*/
private static void addRollingNumberPatchChanges(TextView view) {
// YouTube Rolling Numbers do not use compound drawables or drawable padding.
if (view.getCompoundDrawablePadding() == 0) {
Logger.printDebug(() -> "Adding rolling number TextView changes");
view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels);
ShapeDrawable separator = ReturnYouTubeDislike.getLeftSeparatorDrawable();
if (Utils.isRightToLeftTextLayout()) {
view.setCompoundDrawables(null, null, separator, null);
} else {
view.setCompoundDrawables(separator, null, null, null);
}
// Disliking can cause the span to grow in size, which is ok and is laid out correctly,
// but if the user then removes their dislike the layout will not adjust to the new shorter width.
// Use a center alignment to take up any extra space.
view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
// 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);
}
}
/**
* Remove Rolling Number text view modifications made by this patch.
* Required as it appears text views can be reused for other rolling numbers (view count, upload time, etc).
*/
private static void removeRollingNumberPatchChanges(TextView view) {
if (view.getCompoundDrawablePadding() != 0) {
Logger.printDebug(() -> "Removing rolling number TextView changes");
view.setCompoundDrawablePadding(0);
view.setCompoundDrawables(null, null, null, null);
view.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY); // Default alignment
view.setSingleLine(false);
}
}
/**
* Injection point.
*/
public static CharSequence updateRollingNumber(TextView view, CharSequence original) {
try {
if (!Settings.RYD_ENABLED.get()) {
removeRollingNumberPatchChanges(view);
return original;
}
final boolean isDescriptionPanel = view.getParent() instanceof ViewGroup viewGroupParent
&& viewGroupParent.getChildCount() < 2;
// Called for all instances of RollingNumber, so must check if text is for a dislikes.
// Text will already have the correct content but it's missing the drawable separators.
if (!ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(original.toString()) || isDescriptionPanel) {
// The text is the video view count, upload time, or some other text.
removeRollingNumberPatchChanges(view);
return original;
}
CharSequence replacement = rollingNumberSpan;
if (replacement == null) {
// User enabled RYD while a video was open,
// or user opened/closed a Short while a regular video was opened.
Logger.printDebug(() -> "Cannot update rolling number (field is null)");
removeRollingNumberPatchChanges(view);
return original;
}
if (Settings.RYD_COMPACT_LAYOUT.get()) {
removeRollingNumberPatchChanges(view);
} else {
addRollingNumberPatchChanges(view);
}
// Remove any padding set by Rolling Number.
view.setPadding(0, 0, 0, 0);
// When displaying dislikes, the rolling animation is not visually correct
// and the dislikes always animate (even though the dislike count has not changed).
// The animation is caused by an image span attached to the span,
// and using only the modified segmented span prevents the animation from showing.
return replacement;
} catch (Exception ex) {
Logger.printException(() -> "updateRollingNumber failure", ex);
return original;
}
}
//
// Non litho Shorts player.
//
/**
* Replacement text to use for "Dislikes" while RYD is fetching.
*/
private static final Spannable SHORTS_LOADING_SPAN = new SpannableString("-");
/**
* Dislikes TextViews used by Shorts.
* <p>
* Multiple TextViews are loaded at once (for the prior and next videos to swipe to).
* Keep track of all of them, and later pick out the correct one based on their on screen position.
*/
private static final List<WeakReference<TextView>> shortsTextViewRefs = new ArrayList<>();
private static void clearRemovedShortsTextViews() {
shortsTextViewRefs.removeIf(ref -> ref.get() == null);
}
/**
* Injection point. Called when a Shorts dislike is updated. Always on main thread.
* Handles update asynchronously, otherwise Shorts video will be frozen while the UI thread is blocked.
*
* @return if RYD is enabled and the TextView was updated.
*/
public static boolean setShortsDislikes(@NonNull View likeDislikeView) {
try {
if (!Settings.RYD_ENABLED.get()) {
return false;
}
if (!Settings.RYD_SHORTS.get() || Settings.HIDE_SHORTS_DISLIKE_BUTTON.get()) {
// Must clear the data here, in case a new video was loaded while PlayerType
// suggested the video was not a short (can happen when spoofing to an old app version).
clearData();
return false;
}
Logger.printDebug(() -> "setShortsDislikes");
TextView textView = (TextView) likeDislikeView;
textView.setText(SHORTS_LOADING_SPAN); // Change 'Dislike' text to the loading text.
shortsTextViewRefs.add(new WeakReference<>(textView));
if (likeDislikeView.isSelected() && isShortTextViewOnScreen(textView)) {
Logger.printDebug(() -> "Shorts dislike is already selected");
ReturnYouTubeDislike videoData = currentVideoData;
if (videoData != null) videoData.setUserVote(Vote.DISLIKE);
}
// For the first short played, the Shorts dislike hook is called after the video id hook.
// But for most other times this hook is called before the video id (which is not ideal).
// Must update the TextViews here, and also after the videoId changes.
updateOnScreenShortsTextViews(false);
return true;
} catch (Exception ex) {
Logger.printException(() -> "setShortsDislikes failure", ex);
return false;
}
}
/**
* @param forceUpdate if false, then only update the 'loading text views.
* If true, update all on screen text views.
*/
private static void updateOnScreenShortsTextViews(boolean forceUpdate) {
try {
clearRemovedShortsTextViews();
if (shortsTextViewRefs.isEmpty()) {
return;
}
ReturnYouTubeDislike videoData = currentVideoData;
if (videoData == null) {
return;
}
Logger.printDebug(() -> "updateShortsTextViews");
Runnable update = () -> {
Spanned shortsDislikesSpan = videoData.getDislikeSpanForShort(SHORTS_LOADING_SPAN);
Utils.runOnMainThreadNowOrLater(() -> {
String videoId = videoData.getVideoId();
if (!videoId.equals(VideoInformation.getVideoId())) {
// User swiped to new video before fetch completed
Logger.printDebug(() -> "Ignoring stale dislikes data for short: " + videoId);
return;
}
// Update text views that appear to be visible on screen.
// Only 1 will be the actual textview for the current Short,
// but discarded and not yet garbage collected views can remain.
// So must set the dislike span on all views that match.
for (WeakReference<TextView> textViewRef : shortsTextViewRefs) {
TextView textView = textViewRef.get();
if (textView == null) {
continue;
}
if (isShortTextViewOnScreen(textView)
&& (forceUpdate || textView.getText().toString().equals(SHORTS_LOADING_SPAN.toString()))) {
Logger.printDebug(() -> "Setting Shorts TextView to: " + shortsDislikesSpan);
textView.setText(shortsDislikesSpan);
}
}
});
};
if (videoData.fetchCompleted()) {
update.run(); // Network call is completed, no need to wait on background thread.
} else {
Utils.runOnBackgroundThread(update);
}
} catch (Exception ex) {
Logger.printException(() -> "updateOnScreenShortsTextViews failure", ex);
}
}
/**
* Check if a view is within the screen bounds.
*/
private static boolean isShortTextViewOnScreen(@NonNull View view) {
final int[] location = new int[2];
view.getLocationInWindow(location);
if (location[0] <= 0 && location[1] <= 0) { // Lower bound
return false;
}
Rect windowRect = new Rect();
view.getWindowVisibleDisplayFrame(windowRect); // Upper bound
return location[0] < windowRect.width() && location[1] < windowRect.height();
}
//
// 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 isShortAndOpeningOrPlaying) {
try {
if (!Settings.RYD_ENABLED.get()) {
return;
}
if (videoId.equals(lastPrefetchedVideoId)) {
return;
}
final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort();
// 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 || !Settings.RYD_SHORTS.get())) {
return;
}
final boolean waitForFetchToComplete = !IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER
&& videoIdIsShort && !lastPlayerResponseWasShort;
Logger.printDebug(() -> "Prefetching RYD for video: " + 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.
Logger.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) {
Logger.printException(() -> "preloadVideoId failure", ex);
}
}
/**
* Injection point. Uses 'current playing' video id hook. Always called on main thread.
*/
public static void newVideoLoaded(@NonNull String videoId) {
try {
if (!Settings.RYD_ENABLED.get()) {
return;
}
Objects.requireNonNull(videoId);
final PlayerType currentPlayerType = PlayerType.getCurrent();
final boolean isNoneHiddenOrSlidingMinimized = currentPlayerType.isNoneHiddenOrSlidingMinimized();
if (isNoneHiddenOrSlidingMinimized && !Settings.RYD_SHORTS.get()) {
// Must clear here, otherwise the wrong data can be used for a minimized regular video.
clearData();
return;
}
if (videoIdIsSame(currentVideoData, videoId)) {
return;
}
Logger.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType);
ReturnYouTubeDislike data = ReturnYouTubeDislike.getFetchForVideoId(videoId);
// Pre-emptively set the data to short status.
// Required to prevent Shorts data from being used on a minimized video in incognito mode.
if (isNoneHiddenOrSlidingMinimized) {
data.setVideoIdIsShort(true);
}
currentVideoData = data;
// Current video id hook can be called out of order with the non litho Shorts text view hook.
// Must manually update again here.
if (isNoneHiddenOrSlidingMinimized) {
updateOnScreenShortsTextViews(true);
}
} catch (Exception ex) {
Logger.printException(() -> "newVideoLoaded failure", ex);
}
}
public static void setLastLithoShortsVideoId(@Nullable String videoId) {
if (videoIdIsSame(lastLithoShortsVideoData, videoId)) {
return;
}
if (videoId == null) {
// Litho filter did not detect the video id. App is in incognito mode,
// or the proto buffer structure was changed and the video id is no longer present.
// Must clear both currently playing and last litho data otherwise the
// next regular video may use the wrong data.
Logger.printDebug(() -> "Litho filter did not find any video ids");
clearData();
return;
}
Logger.printDebug(() -> "New litho Shorts video id: " + videoId);
ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(videoId);
videoData.setVideoIdIsShort(true);
lastLithoShortsVideoData = videoData;
lithoShortsShouldUseCurrentData = false;
}
private static boolean videoIdIsSame(@Nullable ReturnYouTubeDislike fetch, @Nullable String videoId) {
return (fetch == null && videoId == null)
|| (fetch != null && fetch.getVideoId().equals(videoId));
}
/**
* Injection point.
* <p>
* Called when the user likes or dislikes.
*
* @param vote int that matches {@link Vote#value}
*/
public static void sendVote(int vote) {
try {
if (!Settings.RYD_ENABLED.get()) {
return;
}
final boolean isNoneHiddenOrMinimized = PlayerType.getCurrent().isNoneHiddenOrMinimized();
if (isNoneHiddenOrMinimized && !Settings.RYD_SHORTS.get()) {
return;
}
ReturnYouTubeDislike videoData = currentVideoData;
if (videoData == null) {
Logger.printDebug(() -> "Cannot send vote, as current video data is null");
return; // User enabled RYD while a regular video was minimized.
}
for (Vote v : Vote.values()) {
if (v.value == vote) {
videoData.sendVote(v);
if (isNoneHiddenOrMinimized && lastLithoShortsVideoData != null) {
lithoShortsShouldUseCurrentData = true;
}
return;
}
}
Logger.printException(() -> "Unknown vote type: " + vote);
} catch (Exception ex) {
Logger.printException(() -> "sendVote failure", ex);
}
}
}

View File

@ -0,0 +1,28 @@
package app.revanced.extension.youtube.patches.utils;
import android.view.View;
import android.widget.ImageView;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public class ToolBarPatch {
public static void hookToolBar(Enum<?> buttonEnum, ImageView imageView) {
final String enumString = buttonEnum.name();
if (enumString.isEmpty() ||
imageView == null ||
!(imageView.getParent() instanceof View view)) {
return;
}
Logger.printDebug(() -> "enumString: " + enumString);
hookToolBar(enumString, view);
}
private static void hookToolBar(String enumString, View parentView) {
}
}

View File

@ -0,0 +1,53 @@
package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class AV1CodecPatch {
private static final int LITERAL_VALUE_AV01 = 1635135811;
private static final int LITERAL_VALUE_DOLBY_VISION = 1685485123;
private static final String VP9_CODEC = "video/x-vnd.on2.vp9";
private static long lastTimeResponse = 0;
/**
* Replace the SW AV01 codec to VP9 codec.
* May not be valid on some clients.
*
* @param original hardcoded value - "video/av01"
*/
public static String replaceCodec(String original) {
return Settings.REPLACE_AV1_CODEC.get() ? VP9_CODEC : original;
}
/**
* Replace the SW AV01 codec request with a Dolby Vision codec request.
* This request is invalid, so it falls back to codecs other than AV01.
* <p>
* Limitation: Fallback process causes about 15-20 seconds of buffering.
*
* @param literalValue literal value of the codec
*/
public static int rejectResponse(int literalValue) {
if (!Settings.REJECT_AV1_CODEC.get())
return literalValue;
Logger.printDebug(() -> "Response: " + literalValue);
if (literalValue != LITERAL_VALUE_AV01)
return literalValue;
final long currentTime = System.currentTimeMillis();
// Ignore the invoke within 20 seconds.
if (currentTime - lastTimeResponse > 20000) {
lastTimeResponse = currentTime;
Utils.showToastShort(str("revanced_reject_av1_codec_toast"));
}
return LITERAL_VALUE_DOLBY_VISION;
}
}

View File

@ -0,0 +1,266 @@
package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.ResourceUtils.getString;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import java.util.Arrays;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilter;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
public class CustomPlaybackSpeedPatch {
/**
* Maximum playback speed, exclusive value. Custom speeds must be less than this value.
* <p>
* Going over 8x does not increase the actual playback speed any higher,
* and the UI selector starts flickering and acting weird.
* Over 10x and the speeds show up out of order in the UI selector.
*/
public static final float MAXIMUM_PLAYBACK_SPEED = 8;
private static final String[] defaultSpeedEntries;
private static final String[] defaultSpeedEntryValues;
/**
* Custom playback speeds.
*/
private static float[] playbackSpeeds;
private static String[] customSpeedEntries;
private static String[] customSpeedEntryValues;
private static String[] playbackSpeedEntries;
private static String[] playbackSpeedEntryValues;
/**
* The last time the old playback menu was forcefully called.
*/
private static long lastTimeOldPlaybackMenuInvoked;
static {
defaultSpeedEntries = new String[]{getString("quality_auto"), "0.25x", "0.5x", "0.75x", getString("revanced_playback_speed_normal"), "1.25x", "1.5x", "1.75x", "2.0x"};
defaultSpeedEntryValues = new String[]{"-2.0", "0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0"};
loadCustomSpeeds();
}
/**
* Injection point.
*/
public static float[] getArray(float[] original) {
return isCustomPlaybackSpeedEnabled() ? playbackSpeeds : original;
}
/**
* Injection point.
*/
public static int getLength(int original) {
return isCustomPlaybackSpeedEnabled() ? playbackSpeeds.length : original;
}
/**
* Injection point.
*/
public static int getSize(int original) {
return isCustomPlaybackSpeedEnabled() ? 0 : original;
}
public static String[] getListEntries() {
return isCustomPlaybackSpeedEnabled()
? customSpeedEntries
: defaultSpeedEntries;
}
public static String[] getListEntryValues() {
return isCustomPlaybackSpeedEnabled()
? customSpeedEntryValues
: defaultSpeedEntryValues;
}
public static String[] getTrimmedListEntries() {
if (playbackSpeedEntries == null) {
final String[] playbackSpeedWithAutoEntries = getListEntries();
playbackSpeedEntries = Arrays.copyOfRange(playbackSpeedWithAutoEntries, 1, playbackSpeedWithAutoEntries.length);
}
return playbackSpeedEntries;
}
public static String[] getTrimmedListEntryValues() {
if (playbackSpeedEntryValues == null) {
final String[] playbackSpeedWithAutoEntryValues = getListEntryValues();
playbackSpeedEntryValues = Arrays.copyOfRange(playbackSpeedWithAutoEntryValues, 1, playbackSpeedWithAutoEntryValues.length);
}
return playbackSpeedEntryValues;
}
private static void resetCustomSpeeds(@NonNull String toastMessage) {
Utils.showToastLong(toastMessage);
Utils.showToastShort(str("revanced_extended_reset_to_default_toast"));
Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
}
private static void loadCustomSpeeds() {
try {
if (!Settings.ENABLE_CUSTOM_PLAYBACK_SPEED.get()) {
return;
}
String[] speedStrings = Settings.CUSTOM_PLAYBACK_SPEEDS.get().split("\\s+");
Arrays.sort(speedStrings);
if (speedStrings.length == 0) {
throw new IllegalArgumentException();
}
playbackSpeeds = new float[speedStrings.length];
int i = 0;
for (String speedString : speedStrings) {
final float speedFloat = Float.parseFloat(speedString);
if (speedFloat <= 0 || arrayContains(playbackSpeeds, speedFloat)) {
throw new IllegalArgumentException();
}
if (speedFloat > MAXIMUM_PLAYBACK_SPEED) {
resetCustomSpeeds(str("revanced_custom_playback_speeds_invalid", MAXIMUM_PLAYBACK_SPEED));
loadCustomSpeeds();
return;
}
playbackSpeeds[i] = speedFloat;
i++;
}
if (customSpeedEntries != null) return;
customSpeedEntries = new String[playbackSpeeds.length + 1];
customSpeedEntryValues = new String[playbackSpeeds.length + 1];
customSpeedEntries[0] = getString("quality_auto");
customSpeedEntryValues[0] = "-2.0";
i = 1;
for (float speed : playbackSpeeds) {
String speedString = String.valueOf(speed);
customSpeedEntries[i] = speed != 1.0f
? speedString + "x"
: getString("revanced_playback_speed_normal");
customSpeedEntryValues[i] = speedString;
i++;
}
} catch (Exception ex) {
Logger.printInfo(() -> "parse error", ex);
resetCustomSpeeds(str("revanced_custom_playback_speeds_parse_exception"));
loadCustomSpeeds();
}
}
private static boolean arrayContains(float[] array, float value) {
for (float arrayValue : array) {
if (arrayValue == value) return true;
}
return false;
}
private static boolean isCustomPlaybackSpeedEnabled() {
return Settings.ENABLE_CUSTOM_PLAYBACK_SPEED.get() && playbackSpeeds != null;
}
/**
* Injection point.
*/
public static void onFlyoutMenuCreate(RecyclerView recyclerView) {
if (!Settings.ENABLE_CUSTOM_PLAYBACK_SPEED.get()) {
return;
}
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
try {
if (PlaybackSpeedMenuFilter.isOldPlaybackSpeedMenuVisible) {
if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 8)) {
PlaybackSpeedMenuFilter.isOldPlaybackSpeedMenuVisible = false;
}
return;
}
} catch (Exception ex) {
Logger.printException(() -> "isOldPlaybackSpeedMenuVisible failure", ex);
}
try {
if (PlaybackSpeedMenuFilter.isPlaybackRateSelectorMenuVisible) {
if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 5)) {
PlaybackSpeedMenuFilter.isPlaybackRateSelectorMenuVisible = false;
}
}
} catch (Exception ex) {
Logger.printException(() -> "isPlaybackRateSelectorMenuVisible failure", ex);
}
});
}
private static boolean hideLithoMenuAndShowOldSpeedMenu(RecyclerView recyclerView, int expectedChildCount) {
if (recyclerView.getChildCount() == 0) {
return false;
}
if (!(recyclerView.getChildAt(0) instanceof ViewGroup PlaybackSpeedParentView)) {
return false;
}
if (PlaybackSpeedParentView.getChildCount() != expectedChildCount) {
return false;
}
if (!(Utils.getParentView(recyclerView, 3) instanceof ViewGroup parentView3rd)) {
return false;
}
if (!(parentView3rd.getParent() instanceof ViewGroup parentView4th)) {
return false;
}
// Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView.
// This only shows in phone layout.
Utils.clickView(parentView4th.getChildAt(0));
// In tablet layout there is no Dismiss View, instead we just hide all two parent views.
parentView3rd.setVisibility(View.GONE);
parentView4th.setVisibility(View.GONE);
// Show old playback speed menu.
showCustomPlaybackSpeedMenu(recyclerView.getContext());
return true;
}
/**
* This method is sometimes used multiple times
* To prevent this, ignore method reuse within 1 second.
*
* @param context Context for [playbackSpeedDialogListener]
*/
private static void showCustomPlaybackSpeedMenu(@NonNull Context context) {
// This method is sometimes used multiple times.
// To prevent this, ignore method reuse within 1 second.
final long now = System.currentTimeMillis();
if (now - lastTimeOldPlaybackMenuInvoked < 1000) {
return;
}
lastTimeOldPlaybackMenuInvoked = now;
if (Settings.CUSTOM_PLAYBACK_SPEED_MENU_TYPE.get()) {
// Open playback speed dialog
VideoUtils.showPlaybackSpeedDialog(context);
} else {
// Open old style flyout menu
VideoUtils.showPlaybackSpeedFlyoutMenu();
}
}
}

View File

@ -0,0 +1,11 @@
package app.revanced.extension.youtube.patches.video;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class HDRVideoPatch {
public static boolean disableHDRVideo() {
return !Settings.DISABLE_HDR_VIDEO.get();
}
}

View File

@ -0,0 +1,139 @@
package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.misc.requests.PlaylistRequest;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.VideoInformation;
import app.revanced.extension.youtube.whitelist.Whitelist;
@SuppressWarnings("unused")
public class PlaybackSpeedPatch {
private static final long TOAST_DELAY_MILLISECONDS = 750;
private static long lastTimeSpeedChanged;
private static boolean isLiveStream;
/**
* Injection point.
*/
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
isLiveStream = newlyLoadedLiveStreamValue;
Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId);
final float defaultPlaybackSpeed = getDefaultPlaybackSpeed(newlyLoadedChannelId, newlyLoadedVideoId);
Logger.printDebug(() -> "overridePlaybackSpeed: " + defaultPlaybackSpeed);
VideoInformation.overridePlaybackSpeed(defaultPlaybackSpeed);
}
/**
* Injection point.
*/
public static void fetchPlaylistData(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
if (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get()) {
try {
final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort();
// 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) {
return;
}
PlaylistRequest.fetchRequestIfNeeded(videoId);
} catch (Exception ex) {
Logger.printException(() -> "fetchPlaylistData failure", ex);
}
}
}
/**
* Injection point.
*/
public static float getPlaybackSpeedInShorts(final float playbackSpeed) {
if (!VideoInformation.lastPlayerResponseIsShort())
return playbackSpeed;
if (!Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get())
return playbackSpeed;
float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null);
Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed);
return defaultPlaybackSpeed;
}
/**
* Injection point.
* Called when user selects a playback speed.
*
* @param playbackSpeed The playback speed the user selected
*/
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
if (PatchStatus.RememberPlaybackSpeed() &&
Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) {
// With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
// then the menu will allow increasing without bounds but the max speed is
// still capped to under 8.0x.
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.MAXIMUM_PLAYBACK_SPEED - 0.05f);
// Prevent toast spamming if using the 0.05x adjustments.
// Show exactly one toast after the user stops interacting with the speed menu.
final long now = System.currentTimeMillis();
lastTimeSpeedChanged = now;
final float finalPlaybackSpeed = playbackSpeed;
Utils.runOnMainThreadDelayed(() -> {
if (lastTimeSpeedChanged != now) {
// The user made additional speed adjustments and this call is outdated.
return;
}
if (Settings.DEFAULT_PLAYBACK_SPEED.get() == finalPlaybackSpeed) {
// User changed to a different speed and immediately changed back.
// Or the user is going past 8.0x in the glitched out 0.05x menu.
return;
}
Settings.DEFAULT_PLAYBACK_SPEED.save(finalPlaybackSpeed);
if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get()) {
return;
}
Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
}, TOAST_DELAY_MILLISECONDS);
}
}
private static float getDefaultPlaybackSpeed(@NonNull String channelId, @Nullable String videoId) {
return (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_LIVE.get() && isLiveStream) ||
Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) ||
getPlaylistData(videoId)
? 1.0f
: Settings.DEFAULT_PLAYBACK_SPEED.get();
}
private static boolean getPlaylistData(@Nullable String videoId) {
if (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get() && videoId != null) {
try {
PlaylistRequest request = PlaylistRequest.getRequestForVideoId(videoId);
final boolean isPlaylist = request != null && BooleanUtils.toBoolean(request.getStream());
Logger.printDebug(() -> "isPlaylist: " + isPlaylist);
return isPlaylist;
} catch (Exception ex) {
Logger.printException(() -> "getPlaylistData failure", ex);
}
}
return false;
}
}

View File

@ -0,0 +1,53 @@
package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.VideoInformation;
@SuppressWarnings("unused")
public class ReloadVideoPatch {
private static final long RELOAD_VIDEO_TIME_MILLISECONDS = 15000L;
@NonNull
public static String videoId = "";
/**
* Injection point.
*/
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
if (!Settings.SKIP_PRELOADED_BUFFER.get())
return;
if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL)
return;
if (videoId.equals(newlyLoadedVideoId))
return;
videoId = newlyLoadedVideoId;
if (newlyLoadedVideoLength < RELOAD_VIDEO_TIME_MILLISECONDS || newlyLoadedLiveStreamValue)
return;
final long seekTime = Math.max(RELOAD_VIDEO_TIME_MILLISECONDS, (long) (newlyLoadedVideoLength * 0.5));
Utils.runOnMainThreadDelayed(() -> reloadVideo(seekTime), 250);
}
private static void reloadVideo(final long videoLength) {
final long lastVideoTime = VideoInformation.getVideoTime();
final float playbackSpeed = VideoInformation.getPlaybackSpeed();
final long speedAdjustedTimeThreshold = (long) (playbackSpeed * 300);
VideoInformation.overrideVideoTime(videoLength);
VideoInformation.overrideVideoTime(lastVideoTime + speedAdjustedTimeThreshold);
if (!Settings.SKIP_PRELOADED_BUFFER_TOAST.get())
return;
Utils.showToastShort(str("revanced_skipped_preloaded_buffer"));
}
}

View File

@ -0,0 +1,73 @@
package app.revanced.extension.youtube.patches.video;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.components.VideoQualityMenuFilter;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class RestoreOldVideoQualityMenuPatch {
public static boolean restoreOldVideoQualityMenu() {
return Settings.RESTORE_OLD_VIDEO_QUALITY_MENU.get();
}
public static void restoreOldVideoQualityMenu(ListView listView) {
if (!Settings.RESTORE_OLD_VIDEO_QUALITY_MENU.get())
return;
listView.setVisibility(View.GONE);
Utils.runOnMainThreadDelayed(() -> {
listView.setSoundEffectsEnabled(false);
listView.performItemClick(null, 2, 0);
},
1
);
}
public static void onFlyoutMenuCreate(final RecyclerView recyclerView) {
if (!Settings.RESTORE_OLD_VIDEO_QUALITY_MENU.get())
return;
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
try {
// Check if the current view is the quality menu.
if (!VideoQualityMenuFilter.isVideoQualityMenuVisible || recyclerView.getChildCount() == 0) {
return;
}
if (!(Utils.getParentView(recyclerView, 3) instanceof ViewGroup quickQualityViewParent)) {
return;
}
if (!(recyclerView.getChildAt(0) instanceof ViewGroup advancedQualityParentView)) {
return;
}
if (advancedQualityParentView.getChildCount() < 4) {
return;
}
View advancedQualityView = advancedQualityParentView.getChildAt(3);
if (advancedQualityView == null) {
return;
}
quickQualityViewParent.setVisibility(View.GONE);
// Click the "Advanced" quality menu to show the "old" quality menu.
advancedQualityView.callOnClick();
VideoQualityMenuFilter.isVideoQualityMenuVisible = false;
} catch (Exception ex) {
Logger.printException(() -> "onFlyoutMenuCreate failure", ex);
}
});
}
}

View File

@ -0,0 +1,16 @@
package app.revanced.extension.youtube.patches.video;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class SpoofDeviceDimensionsPatch {
private static final boolean SPOOF = Settings.SPOOF_DEVICE_DIMENSIONS.get();
public static int getMinHeightOrWidth(int minHeightOrWidth) {
return SPOOF ? 64 : minHeightOrWidth;
}
public static int getMaxHeightOrWidth(int maxHeightOrWidth) {
return SPOOF ? 4096 : maxHeightOrWidth;
}
}

View File

@ -0,0 +1,11 @@
package app.revanced.extension.youtube.patches.video;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class VP9CodecPatch {
public static boolean disableVP9Codec() {
return !Settings.DISABLE_VP9_CODEC.get();
}
}

View File

@ -0,0 +1,91 @@
package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.settings.IntegerSetting;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.VideoInformation;
@SuppressWarnings("unused")
public class VideoQualityPatch {
private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2;
private static final IntegerSetting mobileQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_MOBILE;
private static final IntegerSetting wifiQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_WIFI;
@NonNull
public static String videoId = "";
/**
* Injection point.
*/
public static void newVideoStarted() {
setVideoQuality(0);
}
/**
* Injection point.
*/
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL)
return;
if (videoId.equals(newlyLoadedVideoId))
return;
videoId = newlyLoadedVideoId;
setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 500);
}
/**
* Injection point.
*/
public static void userSelectedVideoQuality() {
Utils.runOnMainThreadDelayed(() ->
userSelectedVideoQuality(VideoInformation.getVideoQuality()),
300
);
}
private static void setVideoQuality(final long delayMillis) {
final int defaultQuality = Utils.getNetworkType() == Utils.NetworkType.MOBILE
? mobileQualitySetting.get()
: wifiQualitySetting.get();
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY)
return;
Utils.runOnMainThreadDelayed(() ->
VideoInformation.overrideVideoQuality(
VideoInformation.getAvailableVideoQuality(defaultQuality)
),
delayMillis
);
}
private static void userSelectedVideoQuality(final int defaultQuality) {
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get())
return;
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY)
return;
final Utils.NetworkType networkType = Utils.getNetworkType();
switch (networkType) {
case NONE -> {
Utils.showToastShort(str("revanced_remember_video_quality_none"));
return;
}
case MOBILE -> mobileQualitySetting.save(defaultQuality);
default -> wifiQualitySetting.save(defaultQuality);
}
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
return;
Utils.showToastShort(str("revanced_remember_video_quality_" + networkType.getName(), defaultQuality + "p"));
}
}

View File

@ -0,0 +1,776 @@
package app.revanced.extension.youtube.returnyoutubedislike;
import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.graphics.drawable.shapes.RectShape;
import android.icu.text.CompactDecimalFormat;
import android.icu.text.DecimalFormat;
import android.icu.text.DecimalFormatSymbols;
import android.icu.text.NumberFormat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.text.style.ReplacementSpan;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import app.revanced.extension.shared.returnyoutubedislike.requests.RYDVoteData;
import app.revanced.extension.shared.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.utils.ThemeUtils;
/**
* Handles fetching and creation/replacing of RYD dislike text spans.
* <p>
* Because Litho creates spans using multiple threads, this entire class supports multithreading as well.
*/
public class ReturnYouTubeDislike {
/**
* Maximum amount of time to block the UI from updates while waiting for network call to complete.
* <p>
* Must be less than 5 seconds, as per:
* <a href="https://developer.android.com/topic/performance/vitals/anr">...</a>
*/
private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH = 4000;
/**
* How long to retain successful RYD fetches.
*/
private static final long CACHE_TIMEOUT_SUCCESS_MILLISECONDS = 7 * 60 * 1000; // 7 Minutes
/**
* How long to retain unsuccessful RYD fetches,
* and also the minimum time before retrying again.
*/
private static final long CACHE_TIMEOUT_FAILURE_MILLISECONDS = 3 * 60 * 1000; // 3 Minutes
/**
* Unique placeholder character, used to detect if a segmented span already has dislikes added to it.
* Must be something YouTube is unlikely to use, as it's searched for in all usage of Rolling Number.
*/
private static final char MIDDLE_SEPARATOR_CHARACTER = '◎'; // 'bullseye'
public static final boolean IS_SPOOFING_TO_OLD_SEPARATOR_COLOR =
isSpoofingToLessThan("18.10.00");
/**
* Cached lookup of all video ids.
*/
@GuardedBy("itself")
private static final Map<String, ReturnYouTubeDislike> fetchCache = new HashMap<>();
/**
* Used to send votes, one by one, in the same order the user created them.
*/
private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor();
/**
* For formatting dislikes as number.
*/
@GuardedBy("ReturnYouTubeDislike.class") // not thread safe
private static CompactDecimalFormat dislikeCountFormatter;
/**
* For formatting dislikes as percentage.
*/
@GuardedBy("ReturnYouTubeDislike.class")
private static NumberFormat dislikePercentageFormatter;
// Used for segmented dislike spans in Litho regular player.
public static final Rect leftSeparatorBounds;
private static final Rect middleSeparatorBounds;
/**
* Left separator horizontal padding for Rolling Number layout.
*/
public static final int leftSeparatorShapePaddingPixels;
private static final ShapeDrawable leftSeparatorShape;
public static final Locale locale;
static {
final Resources resources = Utils.getResources();
DisplayMetrics dp = resources.getDisplayMetrics();
leftSeparatorBounds = new Rect(0, 0,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp),
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, dp));
final int middleSeparatorSize =
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0f, dp);
leftSeparatorShape = new ShapeDrawable(new RectShape());
leftSeparatorShape.setBounds(leftSeparatorBounds);
locale = resources.getConfiguration().getLocales().get(0);
ReturnYouTubeDislikeApi.toastOnConnectionError = Settings.RYD_TOAST_ON_CONNECTION_ERROR.get();
}
private final String videoId;
/**
* Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes.
* Absolutely cannot be holding any lock during calls to {@link Future#get()}.
*/
private final Future<RYDVoteData> future;
/**
* Time this instance and the fetch future was created.
*/
private final long timeFetched;
/**
* If this instance was previously used for a Short.
*/
@GuardedBy("this")
private boolean isShort;
/**
* Optional current vote status of the UI. Used to apply a user vote that was done on a previous video viewing.
*/
@Nullable
@GuardedBy("this")
private Vote userVote;
/**
* Original dislike span, before modifications.
*/
@Nullable
@GuardedBy("this")
private Spanned originalDislikeSpan;
/**
* Replacement like/dislike span that includes formatted dislikes.
* Used to prevent recreating the same span multiple times.
*/
@Nullable
@GuardedBy("this")
private SpannableString replacementLikeDislikeSpan;
/**
* Color of the left and middle separator, based on the color of the right separator.
* It's unknown where YT gets the color from, and the values here are approximated by hand.
* Ideally, this would be the actual color YT uses at runtime.
* <p>
* Older versions before the 'Me' library tab use a slightly different color.
* If spoofing was previously used and is now turned off,
* or an old version was recently upgraded then the old colors are sometimes still used.
*/
private static int getSeparatorColor() {
if (IS_SPOOFING_TO_OLD_SEPARATOR_COLOR) {
return ThemeUtils.isDarkTheme()
? 0x29AAAAAA // transparent dark gray
: 0xFFD9D9D9; // light gray
}
return ThemeUtils.isDarkTheme()
? 0x33FFFFFF
: 0xFFD9D9D9;
}
public static ShapeDrawable getLeftSeparatorDrawable() {
leftSeparatorShape.getPaint().setColor(getSeparatorColor());
return leftSeparatorShape;
}
/**
* @param isSegmentedButton If UI is using the segmented single UI component for both like and dislike.
*/
@NonNull
private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable,
boolean isSegmentedButton,
boolean isRollingNumber,
@NonNull RYDVoteData voteData) {
if (!isSegmentedButton) {
// Simple replacement of 'dislike' with a number/percentage.
return newSpannableWithDislikes(oldSpannable, voteData);
}
// Note: Some locales use right to left layout (Arabic, Hebrew, etc).
// If making changes to this code, change device settings to a RTL language and verify layout is correct.
CharSequence oldLikes = oldSpannable;
// YouTube creators can hide the like count on a video,
// and the like count appears as a device language specific string that says 'Like'.
// Check if the string contains any numbers.
if (!Utils.containsNumber(oldLikes)) {
if (Settings.RYD_ESTIMATED_LIKE.get()) {
// Likes are hidden by video creator
//
// RYD does not directly provide like data, but can use an estimated likes
// using the same scale factor RYD applied to the raw dislikes.
//
// example video: https://www.youtube.com/watch?v=UnrU5vxCHxw
// RYD data: https://returnyoutubedislikeapi.com/votes?videoId=UnrU5vxCHxw
Logger.printDebug(() -> "Using estimated likes");
oldLikes = formatDislikeCount(voteData.getLikeCount());
} else {
// Change the "Likes" string to show that likes and dislikes are hidden.
String hiddenMessageString = str("revanced_ryd_video_likes_hidden_by_video_owner");
return newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString);
}
}
SpannableStringBuilder builder = new SpannableStringBuilder();
final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get();
if (!compactLayout) {
String leftSeparatorString = getTextDirectionString();
final Spannable leftSeparatorSpan;
if (isRollingNumber) {
leftSeparatorSpan = new SpannableString(leftSeparatorString);
} else {
leftSeparatorString += " ";
leftSeparatorSpan = new SpannableString(leftSeparatorString);
// Styling spans cannot overwrite RTL or LTR character.
leftSeparatorSpan.setSpan(
new VerticallyCenteredImageSpan(getLeftSeparatorDrawable(), false),
1, 2, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
leftSeparatorSpan.setSpan(
new FixedWidthEmptySpan(leftSeparatorShapePaddingPixels),
2, 3, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
builder.append(leftSeparatorSpan);
}
// likes
builder.append(newSpanUsingStylingOfAnotherSpan(oldSpannable, oldLikes));
// middle separator
String middleSeparatorString = compactLayout
? " " + MIDDLE_SEPARATOR_CHARACTER + " "
: " \u2009" + MIDDLE_SEPARATOR_CHARACTER + "\u2009 "; // u2009 = 'narrow space' character
final int shapeInsertionIndex = middleSeparatorString.length() / 2;
Spannable middleSeparatorSpan = new SpannableString(middleSeparatorString);
ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
shapeDrawable.getPaint().setColor(getSeparatorColor());
shapeDrawable.setBounds(middleSeparatorBounds);
// Use original text width if using Rolling Number,
// to ensure the replacement styled span has the same width as the measured String,
// otherwise layout can be broken (especially on devices with small system font sizes).
middleSeparatorSpan.setSpan(
new VerticallyCenteredImageSpan(shapeDrawable, isRollingNumber),
shapeInsertionIndex, shapeInsertionIndex + 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
builder.append(middleSeparatorSpan);
// dislikes
builder.append(newSpannableWithDislikes(oldSpannable, voteData));
return new SpannableString(builder);
}
private static @NonNull String getTextDirectionString() {
return Utils.isRightToLeftTextLayout()
? "\u200F" // u200F = right to left character
: "\u200E"; // u200E = left to right character
}
/**
* @return If the text is likely for a previously created likes/dislikes segmented span.
*/
public static boolean isPreviouslyCreatedSegmentedSpan(@NonNull String text) {
return text.indexOf(MIDDLE_SEPARATOR_CHARACTER) >= 0;
}
private static boolean spansHaveEqualTextAndColor(@NonNull Spanned one, @NonNull Spanned two) {
// Cannot use equals on the span, because many of the inner styling spans do not implement equals.
// Instead, compare the underlying text and the text color to handle when dark mode is changed.
// Cannot compare the status of device dark mode, as Litho components are updated just before dark mode status changes.
if (!one.toString().equals(two.toString())) {
return false;
}
ForegroundColorSpan[] oneColors = one.getSpans(0, one.length(), ForegroundColorSpan.class);
ForegroundColorSpan[] twoColors = two.getSpans(0, two.length(), ForegroundColorSpan.class);
final int oneLength = oneColors.length;
if (oneLength != twoColors.length) {
return false;
}
for (int i = 0; i < oneLength; i++) {
if (oneColors[i].getForegroundColor() != twoColors[i].getForegroundColor()) {
return false;
}
}
return true;
}
private static SpannableString newSpannableWithLikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) {
return newSpanUsingStylingOfAnotherSpan(sourceStyling, formatDislikeCount(voteData.getLikeCount()));
}
private static SpannableString newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) {
return newSpanUsingStylingOfAnotherSpan(sourceStyling,
Settings.RYD_DISLIKE_PERCENTAGE.get()
? formatDislikePercentage(voteData.getDislikePercentage())
: formatDislikeCount(voteData.getDislikeCount()));
}
private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) {
if (sourceStyle == newSpanText && sourceStyle instanceof SpannableString spannableString) {
return spannableString; // Nothing to do.
}
SpannableString destination = new SpannableString(newSpanText);
Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class);
for (Object span : spans) {
destination.setSpan(span, 0, destination.length(), sourceStyle.getSpanFlags(span));
}
return destination;
}
private static String formatDislikeCount(long dislikeCount) {
if (isSDKAbove(24)) {
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
if (dislikeCountFormatter == null) {
Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().getLocales().get(0);
dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
// YouTube disregards locale specific number characters
// and instead shows english number characters everywhere.
// To use the same behavior, override the digit characters to use English
// so languages such as Arabic will show "1.234" instead of the native "۱,۲۳٤"
if (isSDKAbove(28)) {
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
symbols.setDigitStrings(DecimalFormatSymbols.getInstance(Locale.ENGLISH).getDigitStrings());
dislikeCountFormatter.setDecimalFormatSymbols(symbols);
}
}
return dislikeCountFormatter.format(dislikeCount);
}
}
// Will never be reached, as the oldest supported YouTube app requires Android N or greater.
return String.valueOf(dislikeCount);
}
private static String formatDislikePercentage(float dislikePercentage) {
if (isSDKAbove(24)) {
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
if (dislikePercentageFormatter == null) {
Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().getLocales().get(0);
dislikePercentageFormatter = NumberFormat.getPercentInstance(locale);
// Want to set the digit strings, and the simplest way is to cast to the implementation NumberFormat returns.
if (isSDKAbove(28) && dislikePercentageFormatter instanceof DecimalFormat decimalFormat) {
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
symbols.setDigitStrings(DecimalFormatSymbols.getInstance(Locale.ENGLISH).getDigitStrings());
decimalFormat.setDecimalFormatSymbols(symbols);
}
}
if (dislikePercentage >= 0.01) { // at least 1%
dislikePercentageFormatter.setMaximumFractionDigits(0); // show only whole percentage points
} else {
dislikePercentageFormatter.setMaximumFractionDigits(1); // show up to 1 digit precision
}
return dislikePercentageFormatter.format(dislikePercentage);
}
}
// Will never be reached, as the oldest supported YouTube app requires Android N or greater.
return String.valueOf((int) (dislikePercentage * 100));
}
@NonNull
public static ReturnYouTubeDislike getFetchForVideoId(@Nullable String videoId) {
Objects.requireNonNull(videoId);
synchronized (fetchCache) {
// Remove any expired entries.
final long now = System.currentTimeMillis();
if (isSDKAbove(24)) {
fetchCache.values().removeIf(value -> {
final boolean expired = value.isExpired(now);
if (expired)
Logger.printDebug(() -> "Removing expired fetch: " + value.videoId);
return expired;
});
} else {
final Iterator<Map.Entry<String, ReturnYouTubeDislike>> itr = fetchCache.entrySet().iterator();
while (itr.hasNext()) {
final Map.Entry<String, ReturnYouTubeDislike> entry = itr.next();
if (entry.getValue().isExpired(now)) {
Logger.printDebug(() -> "Removing expired fetch: " + entry.getValue().videoId);
itr.remove();
}
}
}
ReturnYouTubeDislike fetch = fetchCache.get(videoId);
if (fetch == null) {
fetch = new ReturnYouTubeDislike(videoId);
fetchCache.put(videoId, fetch);
}
return fetch;
}
}
/**
* Should be called if the user changes dislikes appearance settings.
*/
public static void clearAllUICaches() {
synchronized (fetchCache) {
for (ReturnYouTubeDislike fetch : fetchCache.values()) {
fetch.clearUICache();
}
}
}
private ReturnYouTubeDislike(@NonNull String videoId) {
this.videoId = Objects.requireNonNull(videoId);
this.timeFetched = System.currentTimeMillis();
this.future = Utils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
}
private boolean isExpired(long now) {
final long timeSinceCreation = now - timeFetched;
if (timeSinceCreation < CACHE_TIMEOUT_FAILURE_MILLISECONDS) {
return false; // Not expired, even if the API call failed.
}
if (timeSinceCreation > CACHE_TIMEOUT_SUCCESS_MILLISECONDS) {
return true; // Always expired.
}
// Only expired if the fetch failed (API null response).
return (!fetchCompleted() || getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH) == null);
}
@Nullable
public RYDVoteData getFetchData(long maxTimeToWait) {
try {
return future.get(maxTimeToWait, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {
Logger.printDebug(() -> "Waited but future was not complete after: " + maxTimeToWait + "ms");
} catch (ExecutionException | InterruptedException ex) {
Logger.printException(() -> "Future failure ", ex); // will never happen
}
return null;
}
/**
* @return if the RYD fetch call has completed.
*/
public boolean fetchCompleted() {
return future.isDone();
}
private synchronized void clearUICache() {
if (replacementLikeDislikeSpan != null) {
Logger.printDebug(() -> "Clearing replacement span for: " + videoId);
}
replacementLikeDislikeSpan = null;
}
/**
* Must call off main thread, as this will make a network call if user is not yet registered.
*
* @return ReturnYouTubeDislike user ID. If user registration has never happened
* and the network call fails, this returns NULL.
*/
@Nullable
private static String getUserId() {
Utils.verifyOffMainThread();
String userId = Settings.RYD_USER_ID.get();
if (!userId.isEmpty()) {
return userId;
}
userId = ReturnYouTubeDislikeApi.registerAsNewUser();
if (userId != null) {
Settings.RYD_USER_ID.save(userId);
}
return userId;
}
@NonNull
public String getVideoId() {
return videoId;
}
/**
* Pre-emptively set this as a Short.
*/
public synchronized void setVideoIdIsShort(boolean isShort) {
this.isShort = isShort;
}
/**
* @return the replacement span containing dislikes, or the original span if RYD is not available.
*/
@NonNull
public synchronized Spanned getDislikesSpanForRegularVideo(@NonNull Spanned original,
boolean isSegmentedButton,
boolean isRollingNumber) {
return waitForFetchAndUpdateReplacementSpan(original, isSegmentedButton,
isRollingNumber, false, false);
}
/**
* Called when a Shorts like Spannable is created.
*/
@NonNull
public synchronized Spanned getLikeSpanForShort(@NonNull Spanned original) {
return waitForFetchAndUpdateReplacementSpan(original, false,
false, true, true);
}
/**
* Called when a Shorts dislike Spannable is created.
*/
@NonNull
public synchronized Spanned getDislikeSpanForShort(@NonNull Spanned original) {
return waitForFetchAndUpdateReplacementSpan(original, false,
false, true, false);
}
@NonNull
private Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned original,
boolean isSegmentedButton,
boolean isRollingNumber,
boolean spanIsForShort,
boolean spanIsForLikes) {
try {
RYDVoteData votingData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH);
if (votingData == null) {
Logger.printDebug(() -> "Cannot add dislike to UI (RYD data not available)");
return original;
}
synchronized (this) {
if (spanIsForShort) {
// Cannot set this to false if span is not for a Short.
// When spoofing to an old version and a Short is opened while a regular video
// is on screen, this instance can be loaded for the minimized regular video.
// But this Shorts data won't be displayed for that call
// and when it is un-minimized it will reload again and the load will be ignored.
isShort = true;
} else if (isShort) {
// user:
// 1, opened a video
// 2. opened a short (without closing the regular video)
// 3. closed the short
// 4. regular video is now present, but the videoId and RYD data is still for the short
Logger.printDebug(() -> "Ignoring regular video dislike span,"
+ " as data loaded was previously used for a Short: " + videoId);
return original;
}
// prevents reproducible bugs with the following steps:
// (user is using YouTube with RollingNumber applied)
// 1. opened a video
// 2. switched to fullscreen
// 3. click video's title to open the video description
// 4. dislike count may be replaced in the like count area or view count area of the video description
if (PlayerType.getCurrent().isFullScreenOrSlidingFullScreen()) {
Logger.printDebug(() -> "Ignoring fullscreen video description panel: " + videoId);
return original;
}
if (spanIsForLikes) {
// Scrolling Shorts does not cause the Spans to be reloaded,
// so there is no need to cache the likes for this situations.
Logger.printDebug(() -> "Creating likes span for: " + votingData.videoId);
return newSpannableWithLikes(original, votingData);
}
if (originalDislikeSpan != null && replacementLikeDislikeSpan != null
&& spansHaveEqualTextAndColor(original, originalDislikeSpan)) {
Logger.printDebug(() -> "Replacing span with previously created dislike span of data: " + videoId);
return replacementLikeDislikeSpan;
}
// No replacement span exist, create it now.
if (userVote != null) {
votingData.updateUsingVote(userVote);
}
originalDislikeSpan = original;
replacementLikeDislikeSpan = createDislikeSpan(original, isSegmentedButton, isRollingNumber, votingData);
Logger.printDebug(() -> "Replaced: '" + originalDislikeSpan + "' with: '"
+ replacementLikeDislikeSpan + "'" + " using video: " + videoId);
return replacementLikeDislikeSpan;
}
} catch (Exception ex) {
Logger.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", ex);
}
return original;
}
public void sendVote(@NonNull Vote vote) {
Utils.verifyOnMainThread();
Objects.requireNonNull(vote);
try {
if (isShort != PlayerType.getCurrent().isNoneOrHidden()) {
// Shorts was loaded with regular video present, then Shorts was closed.
// and then user voted on the now visible original video.
// Cannot send a vote, because this instance is for the wrong video.
Utils.showToastLong(str("revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted"));
return;
}
setUserVote(vote);
voteSerialExecutor.execute(() -> {
try { // Must wrap in try/catch to properly log exceptions.
ReturnYouTubeDislikeApi.sendVote(getUserId(), videoId, vote);
} catch (Exception ex) {
Logger.printException(() -> "Failed to send vote", ex);
}
});
} catch (Exception ex) {
Logger.printException(() -> "Error trying to send vote", ex);
}
}
/**
* Sets the current user vote value, and does not send the vote to the RYD API.
* <p>
* Only used to set value if thumbs up/down is already selected on video load.
*/
public void setUserVote(@NonNull Vote vote) {
Objects.requireNonNull(vote);
try {
Logger.printDebug(() -> "setUserVote: " + vote);
synchronized (this) {
userVote = vote;
clearUICache();
}
if (future.isDone()) {
// Update the fetched vote data.
RYDVoteData voteData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH);
if (voteData == null) {
// RYD fetch failed.
Logger.printDebug(() -> "Cannot update UI (vote data not available)");
return;
}
voteData.updateUsingVote(vote);
} // Else, vote will be applied after fetch completes.
} catch (Exception ex) {
Logger.printException(() -> "setUserVote failure", ex);
}
}
}
/**
* Styles a Spannable with an empty fixed width.
*/
class FixedWidthEmptySpan extends ReplacementSpan {
final int fixedWidth;
/**
* @param fixedWith Fixed width in screen pixels.
*/
FixedWidthEmptySpan(int fixedWith) {
this.fixedWidth = fixedWith;
if (fixedWith < 0) throw new IllegalArgumentException();
}
@Override
public int getSize(@NonNull Paint paint, @NonNull CharSequence text,
int start, int end, @Nullable Paint.FontMetricsInt fontMetrics) {
return fixedWidth;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, @NonNull Paint paint) {
// Nothing to draw.
}
}
/**
* Vertically centers a Spanned Drawable.
*/
class VerticallyCenteredImageSpan extends ImageSpan {
final boolean useOriginalWidth;
/**
* @param useOriginalWidth Use the original layout width of the text this span is applied to,
* and not the bounds of the Drawable. Drawable is always displayed using it's own bounds,
* and this setting only affects the layout width of the entire span.
*/
public VerticallyCenteredImageSpan(Drawable drawable, boolean useOriginalWidth) {
super(drawable);
this.useOriginalWidth = useOriginalWidth;
}
@Override
public int getSize(@NonNull Paint paint, @NonNull CharSequence text,
int start, int end, @Nullable Paint.FontMetricsInt fontMetrics) {
Drawable drawable = getDrawable();
Rect bounds = drawable.getBounds();
if (fontMetrics != null) {
Paint.FontMetricsInt paintMetrics = paint.getFontMetricsInt();
final int fontHeight = paintMetrics.descent - paintMetrics.ascent;
final int drawHeight = bounds.bottom - bounds.top;
final int halfDrawHeight = drawHeight / 2;
final int yCenter = paintMetrics.ascent + fontHeight / 2;
fontMetrics.ascent = yCenter - halfDrawHeight;
fontMetrics.top = fontMetrics.ascent;
fontMetrics.bottom = yCenter + halfDrawHeight;
fontMetrics.descent = fontMetrics.bottom;
}
if (useOriginalWidth) {
return (int) paint.measureText(text, start, end);
}
return bounds.right;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, @NonNull Paint paint) {
Drawable drawable = getDrawable();
canvas.save();
Paint.FontMetricsInt paintMetrics = paint.getFontMetricsInt();
final int fontHeight = paintMetrics.descent - paintMetrics.ascent;
final int yCenter = y + paintMetrics.descent - fontHeight / 2;
final Rect drawBounds = drawable.getBounds();
float translateX = x;
if (useOriginalWidth) {
// Horizontally center the drawable in the same space as the original text.
translateX += (paint.measureText(text, start, end) - (drawBounds.right - drawBounds.left)) / 2;
}
final int translateY = yCenter - (drawBounds.bottom - drawBounds.top) / 2;
canvas.translate(translateX, translateY);
drawable.draw(canvas);
canvas.restore();
}
}

View File

@ -0,0 +1,654 @@
package app.revanced.extension.youtube.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.settings.Setting.migrateFromOldPreferences;
import static app.revanced.extension.shared.settings.Setting.parent;
import static app.revanced.extension.shared.settings.Setting.parentsAny;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType;
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_1;
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_2;
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_3;
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.settings.IntegerSetting;
import app.revanced.extension.shared.settings.LongSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.DeArrowAvailability;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailStillTime;
import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch;
import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch.StartPage;
import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch.FormFactor;
import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch;
import app.revanced.extension.youtube.patches.misc.SpoofStreamingDataPatch;
import app.revanced.extension.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType;
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
import app.revanced.extension.youtube.shared.PlaylistIdPrefix;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
@SuppressWarnings("unused")
public class Settings extends BaseSettings {
// PreferenceScreen: Ads
public static final BooleanSetting HIDE_GENERAL_ADS = new BooleanSetting("revanced_hide_general_ads", TRUE);
public static final BooleanSetting HIDE_GET_PREMIUM = new BooleanSetting("revanced_hide_get_premium", TRUE, true);
public static final BooleanSetting HIDE_MERCHANDISE_SHELF = new BooleanSetting("revanced_hide_merchandise_shelf", TRUE);
public static final BooleanSetting HIDE_PLAYER_STORE_SHELF = new BooleanSetting("revanced_hide_player_store_shelf", TRUE);
public static final BooleanSetting HIDE_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_paid_promotion_label", TRUE);
public static final BooleanSetting HIDE_SELF_SPONSOR_CARDS = new BooleanSetting("revanced_hide_self_sponsor_cards", TRUE);
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_hide_video_ads", TRUE, true);
public static final BooleanSetting HIDE_VIEW_PRODUCTS = new BooleanSetting("revanced_hide_view_products", TRUE);
public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE);
// PreferenceScreen: Alternative Thumbnails
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL);
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscriptions", ThumbnailOption.ORIGINAL);
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_LIBRARY = new EnumSetting<>("revanced_alt_thumbnail_library", ThumbnailOption.ORIGINAL);
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_PLAYER = new EnumSetting<>("revanced_alt_thumbnail_player", ThumbnailOption.ORIGINAL);
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SEARCH = new EnumSetting<>("revanced_alt_thumbnail_search", ThumbnailOption.ORIGINAL);
public static final StringSetting ALT_THUMBNAIL_DEARROW_API_URL = new StringSetting("revanced_alt_thumbnail_dearrow_api_url",
"https://dearrow-thumb.ajay.app/api/v1/getThumbnail", true, new DeArrowAvailability());
public static final BooleanSetting ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST = new BooleanSetting("revanced_alt_thumbnail_dearrow_connection_toast", FALSE, new DeArrowAvailability());
public static final EnumSetting<ThumbnailStillTime> ALT_THUMBNAIL_STILLS_TIME = new EnumSetting<>("revanced_alt_thumbnail_stills_time", ThumbnailStillTime.MIDDLE, new StillImagesAvailability());
public static final BooleanSetting ALT_THUMBNAIL_STILLS_FAST = new BooleanSetting("revanced_alt_thumbnail_stills_fast", FALSE, new StillImagesAvailability());
// PreferenceScreen: Feed
public static final BooleanSetting HIDE_ALBUM_CARDS = new BooleanSetting("revanced_hide_album_card", TRUE);
public static final BooleanSetting HIDE_CAROUSEL_SHELF = new BooleanSetting("revanced_hide_carousel_shelf", FALSE, true);
public static final BooleanSetting HIDE_CHIPS_SHELF = new BooleanSetting("revanced_hide_chips_shelf", TRUE);
public static final BooleanSetting HIDE_EXPANDABLE_CHIP = new BooleanSetting("revanced_hide_expandable_chip", TRUE);
public static final BooleanSetting HIDE_EXPANDABLE_SHELF = new BooleanSetting("revanced_hide_expandable_shelf", TRUE);
public static final BooleanSetting HIDE_FEED_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_feed_captions_button", FALSE, true);
public static final BooleanSetting HIDE_FEED_SEARCH_BAR = new BooleanSetting("revanced_hide_feed_search_bar", FALSE);
public static final BooleanSetting HIDE_FEED_SURVEY = new BooleanSetting("revanced_hide_feed_survey", TRUE);
public static final BooleanSetting HIDE_FLOATING_BUTTON = new BooleanSetting("revanced_hide_floating_button", FALSE, true);
public static final BooleanSetting HIDE_IMAGE_SHELF = new BooleanSetting("revanced_hide_image_shelf", TRUE);
public static final BooleanSetting HIDE_LATEST_POSTS = new BooleanSetting("revanced_hide_latest_posts", TRUE);
public static final BooleanSetting HIDE_LATEST_VIDEOS_BUTTON = new BooleanSetting("revanced_hide_latest_videos_button", TRUE);
public static final BooleanSetting HIDE_MIX_PLAYLISTS = new BooleanSetting("revanced_hide_mix_playlists", FALSE);
public static final BooleanSetting HIDE_MOVIE_SHELF = new BooleanSetting("revanced_hide_movie_shelf", FALSE);
public static final BooleanSetting HIDE_NOTIFY_ME_BUTTON = new BooleanSetting("revanced_hide_notify_me_button", FALSE);
public static final BooleanSetting HIDE_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE);
public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true);
public static final BooleanSetting HIDE_SUBSCRIPTIONS_CAROUSEL = new BooleanSetting("revanced_hide_subscriptions_carousel", FALSE, true);
public static final BooleanSetting HIDE_TICKET_SHELF = new BooleanSetting("revanced_hide_ticket_shelf", TRUE);
// PreferenceScreen: Feed - Category bar
public static final BooleanSetting HIDE_CATEGORY_BAR_IN_FEED = new BooleanSetting("revanced_hide_category_bar_in_feed", FALSE, true);
public static final BooleanSetting HIDE_CATEGORY_BAR_IN_SEARCH = new BooleanSetting("revanced_hide_category_bar_in_search", FALSE, true);
public static final BooleanSetting HIDE_CATEGORY_BAR_IN_RELATED_VIDEOS = new BooleanSetting("revanced_hide_category_bar_in_related_videos", FALSE, true);
// PreferenceScreen: Feed - Channel profile
public static final BooleanSetting HIDE_CHANNEL_TAB = new BooleanSetting("revanced_hide_channel_tab", FALSE);
public static final StringSetting HIDE_CHANNEL_TAB_FILTER_STRINGS = new StringSetting("revanced_hide_channel_tab_filter_strings", "", true, parent(HIDE_CHANNEL_TAB));
public static final BooleanSetting HIDE_BROWSE_STORE_BUTTON = new BooleanSetting("revanced_hide_browse_store_button", TRUE);
public static final BooleanSetting HIDE_CHANNEL_MEMBER_SHELF = new BooleanSetting("revanced_hide_channel_member_shelf", TRUE);
public static final BooleanSetting HIDE_CHANNEL_PROFILE_LINKS = new BooleanSetting("revanced_hide_channel_profile_links", TRUE);
public static final BooleanSetting HIDE_FOR_YOU_SHELF = new BooleanSetting("revanced_hide_for_you_shelf", TRUE);
// PreferenceScreen: Feed - Community posts
public static final BooleanSetting HIDE_COMMUNITY_POSTS_CHANNEL = new BooleanSetting("revanced_hide_community_posts_channel", FALSE);
public static final BooleanSetting HIDE_COMMUNITY_POSTS_HOME_RELATED_VIDEOS = new BooleanSetting("revanced_hide_community_posts_home_related_videos", TRUE);
public static final BooleanSetting HIDE_COMMUNITY_POSTS_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_community_posts_subscriptions", FALSE);
// PreferenceScreen: Feed - Flyout menu
public static final BooleanSetting HIDE_FEED_FLYOUT_MENU = new BooleanSetting("revanced_hide_feed_flyout_menu", FALSE);
public static final StringSetting HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS = new StringSetting("revanced_hide_feed_flyout_menu_filter_strings", "", true, parent(HIDE_FEED_FLYOUT_MENU));
// PreferenceScreen: Feed - Video filter
public static final BooleanSetting HIDE_KEYWORD_CONTENT_HOME = new BooleanSetting("revanced_hide_keyword_content_home", FALSE);
public static final BooleanSetting HIDE_KEYWORD_CONTENT_SEARCH = new BooleanSetting("revanced_hide_keyword_content_search", FALSE);
public static final BooleanSetting HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_keyword_content_subscriptions", FALSE);
public static final BooleanSetting HIDE_KEYWORD_CONTENT_COMMENTS = new BooleanSetting("revanced_hide_keyword_content_comments", FALSE);
public static final StringSetting HIDE_KEYWORD_CONTENT_PHRASES = new StringSetting("revanced_hide_keyword_content_phrases", "",
parentsAny(HIDE_KEYWORD_CONTENT_HOME, HIDE_KEYWORD_CONTENT_SEARCH, HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS, HIDE_KEYWORD_CONTENT_COMMENTS));
public static final BooleanSetting HIDE_RECOMMENDED_VIDEO = new BooleanSetting("revanced_hide_recommended_video", FALSE);
public static final BooleanSetting HIDE_LOW_VIEWS_VIDEO = new BooleanSetting("revanced_hide_low_views_video", TRUE);
public static final BooleanSetting HIDE_VIDEO_BY_VIEW_COUNTS_HOME = new BooleanSetting("revanced_hide_video_by_view_counts_home", FALSE);
public static final BooleanSetting HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH = new BooleanSetting("revanced_hide_video_by_view_counts_search", FALSE);
public static final BooleanSetting HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_video_by_view_counts_subscriptions", FALSE);
public static final LongSetting HIDE_VIDEO_VIEW_COUNTS_LESS_THAN = new LongSetting("revanced_hide_video_view_counts_less_than", 1000L,
parentsAny(HIDE_VIDEO_BY_VIEW_COUNTS_HOME, HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH, HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS));
public static final LongSetting HIDE_VIDEO_VIEW_COUNTS_GREATER_THAN = new LongSetting("revanced_hide_video_view_counts_greater_than", 1_000_000_000_000L,
parentsAny(HIDE_VIDEO_BY_VIEW_COUNTS_HOME, HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH, HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS));
public static final StringSetting HIDE_VIDEO_VIEW_COUNTS_MULTIPLIER = new StringSetting("revanced_hide_video_view_counts_multiplier", str("revanced_hide_video_view_counts_multiplier_default_value"), true,
parentsAny(HIDE_VIDEO_BY_VIEW_COUNTS_HOME, HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH, HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS));
// Experimental Flags
public static final BooleanSetting HIDE_RELATED_VIDEOS = new BooleanSetting("revanced_hide_related_videos", FALSE, true, "revanced_hide_related_videos_user_dialog_message");
public static final IntegerSetting RELATED_VIDEOS_OFFSET = new IntegerSetting("revanced_related_videos_offset", 2, true, parent(HIDE_RELATED_VIDEOS));
// PreferenceScreen: General
public static final EnumSetting<StartPage> CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.ORIGINAL, true);
public static final BooleanSetting CHANGE_START_PAGE_TYPE = new BooleanSetting("revanced_change_start_page_type", FALSE, true,
new ChangeStartPagePatch.ChangeStartPageTypeAvailability());
public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE);
public static final BooleanSetting DISABLE_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_splash_animation", FALSE, true);
public static final BooleanSetting ENABLE_GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_enable_gradient_loading_screen", FALSE, true);
public static final BooleanSetting HIDE_FLOATING_MICROPHONE = new BooleanSetting("revanced_hide_floating_microphone", TRUE, true);
public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE);
public static final BooleanSetting HIDE_SNACK_BAR = new BooleanSetting("revanced_hide_snack_bar", FALSE);
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE);
public static final EnumSetting<FormFactor> CHANGE_LAYOUT = new EnumSetting<>("revanced_change_layout", FormFactor.ORIGINAL, true);
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", false, true, "revanced_spoof_app_version_user_dialog_message");
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "18.17.43", true, parent(SPOOF_APP_VERSION));
// PreferenceScreen: General - Account menu
public static final BooleanSetting HIDE_ACCOUNT_MENU = new BooleanSetting("revanced_hide_account_menu", FALSE);
public static final StringSetting HIDE_ACCOUNT_MENU_FILTER_STRINGS = new StringSetting("revanced_hide_account_menu_filter_strings", "", true, parent(HIDE_ACCOUNT_MENU));
public static final BooleanSetting HIDE_HANDLE = new BooleanSetting("revanced_hide_handle", TRUE, true);
// PreferenceScreen: General - Custom filter
public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE);
public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER));
// PreferenceScreen: General - Miniplayer
public static final EnumSetting<MiniplayerType> MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.ORIGINAL, true);
public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_enable_double_tap_action", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3));
public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_enable_drag_and_drop", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1));
public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true);
public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3));
public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1));
public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1));
// PreferenceScreen: General - Navigation bar
public static final BooleanSetting ENABLE_NARROW_NAVIGATION_BUTTONS = new BooleanSetting("revanced_enable_narrow_navigation_buttons", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_CREATE_BUTTON = new BooleanSetting("revanced_hide_navigation_create_button", TRUE, true);
public static final BooleanSetting HIDE_NAVIGATION_HOME_BUTTON = new BooleanSetting("revanced_hide_navigation_home_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_LIBRARY_BUTTON = new BooleanSetting("revanced_hide_navigation_library_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_hide_navigation_notifications_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_SHORTS_BUTTON = new BooleanSetting("revanced_hide_navigation_shorts_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_SUBSCRIPTIONS_BUTTON = new BooleanSetting("revanced_hide_navigation_subscriptions_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_LABEL = new BooleanSetting("revanced_hide_navigation_label", FALSE, true);
public static final BooleanSetting SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_switch_create_with_notifications_button", TRUE, true, "revanced_switch_create_with_notifications_button_user_dialog_message");
public static final BooleanSetting ENABLE_TRANSLUCENT_NAVIGATION_BAR = new BooleanSetting("revanced_enable_translucent_navigation_bar", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true);
// PreferenceScreen: General - Override buttons
public static final BooleanSetting OVERRIDE_VIDEO_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_video_download_button", FALSE);
public static final BooleanSetting OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_playlist_download_button", FALSE);
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_VIDEO = new StringSetting("revanced_external_downloader_package_name_video", "com.deniscerri.ytdl");
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_PLAYLIST = new StringSetting("revanced_external_downloader_package_name_playlist", "com.deniscerri.ytdl");
public static final BooleanSetting OVERRIDE_YOUTUBE_MUSIC_BUTTON = new BooleanSetting("revanced_override_youtube_music_button", FALSE, true
, new YouTubeMusicActionsPatch.HookYouTubeMusicAvailability());
public static final StringSetting THIRD_PARTY_YOUTUBE_MUSIC_PACKAGE_NAME = new StringSetting("revanced_third_party_youtube_music_package_name", PatchStatus.RVXMusicPackageName(), true
, new YouTubeMusicActionsPatch.HookYouTubeMusicPackageNameAvailability());
// PreferenceScreen: General - Settings menu
public static final BooleanSetting HIDE_SETTINGS_MENU_PARENT_TOOLS = new BooleanSetting("revanced_hide_settings_menu_parent_tools", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_GENERAL = new BooleanSetting("revanced_hide_settings_menu_general", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_ACCOUNT = new BooleanSetting("revanced_hide_settings_menu_account", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_DATA_SAVING = new BooleanSetting("revanced_hide_settings_menu_data_saving", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_AUTOPLAY = new BooleanSetting("revanced_hide_settings_menu_auto_play", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_VIDEO_QUALITY_PREFERENCES = new BooleanSetting("revanced_hide_settings_menu_video_quality", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_OFFLINE = new BooleanSetting("revanced_hide_settings_menu_offline", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_WATCH_ON_TV = new BooleanSetting("revanced_hide_settings_menu_pair_with_tv", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_MANAGE_ALL_HISTORY = new BooleanSetting("revanced_hide_settings_menu_history", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_YOUR_DATA_IN_YOUTUBE = new BooleanSetting("revanced_hide_settings_menu_your_data", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_PRIVACY = new BooleanSetting("revanced_hide_settings_menu_privacy", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_TRY_EXPERIMENTAL_NEW_FEATURES = new BooleanSetting("revanced_hide_settings_menu_premium_early_access", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_PURCHASES_AND_MEMBERSHIPS = new BooleanSetting("revanced_hide_settings_menu_subscription_product", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_BILLING_AND_PAYMENTS = new BooleanSetting("revanced_hide_settings_menu_billing_and_payment", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_NOTIFICATIONS = new BooleanSetting("revanced_hide_settings_menu_notification", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_CONNECTED_APPS = new BooleanSetting("revanced_hide_settings_menu_connected_accounts", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_LIVE_CHAT = new BooleanSetting("revanced_hide_settings_menu_live_chat", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_CAPTIONS = new BooleanSetting("revanced_hide_settings_menu_captions", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_ACCESSIBILITY = new BooleanSetting("revanced_hide_settings_menu_accessibility", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_ABOUT = new BooleanSetting("revanced_hide_settings_menu_about", FALSE, true);
// dummy data
public static final BooleanSetting HIDE_SETTINGS_MENU_YOUTUBE_TV = new BooleanSetting("revanced_hide_settings_menu_youtube_tv", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_PRE_PURCHASE = new BooleanSetting("revanced_hide_settings_menu_pre_purchase", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_POST_PURCHASE = new BooleanSetting("revanced_hide_settings_menu_post_purchase", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_THIRD_PARTY = new BooleanSetting("revanced_hide_settings_menu_third_party", FALSE, true);
// PreferenceScreen: General - Toolbar
public static final BooleanSetting CHANGE_YOUTUBE_HEADER = new BooleanSetting("revanced_change_youtube_header", TRUE, true);
public static final BooleanSetting ENABLE_WIDE_SEARCH_BAR = new BooleanSetting("revanced_enable_wide_search_bar", FALSE, true);
public static final BooleanSetting ENABLE_WIDE_SEARCH_BAR_WITH_HEADER = new BooleanSetting("revanced_enable_wide_search_bar_with_header", TRUE, true);
public static final BooleanSetting ENABLE_WIDE_SEARCH_BAR_IN_YOU_TAB = new BooleanSetting("revanced_enable_wide_search_bar_in_you_tab", FALSE, true);
public static final BooleanSetting HIDE_TOOLBAR_CAST_BUTTON = new BooleanSetting("revanced_hide_toolbar_cast_button", TRUE, true);
public static final BooleanSetting HIDE_TOOLBAR_CREATE_BUTTON = new BooleanSetting("revanced_hide_toolbar_create_button", FALSE, true);
public static final BooleanSetting HIDE_TOOLBAR_NOTIFICATION_BUTTON = new BooleanSetting("revanced_hide_toolbar_notification_button", FALSE, true);
public static final BooleanSetting HIDE_SEARCH_TERM_THUMBNAIL = new BooleanSetting("revanced_hide_search_term_thumbnail", FALSE);
public static final BooleanSetting HIDE_IMAGE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_image_search_button", FALSE, true);
public static final BooleanSetting HIDE_VOICE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_voice_search_button", FALSE, true);
public static final BooleanSetting HIDE_YOUTUBE_DOODLES = new BooleanSetting("revanced_hide_youtube_doodles", FALSE, true, "revanced_hide_youtube_doodles_user_dialog_message");
public static final BooleanSetting REPLACE_TOOLBAR_CREATE_BUTTON = new BooleanSetting("revanced_replace_toolbar_create_button", FALSE, true);
public static final BooleanSetting REPLACE_TOOLBAR_CREATE_BUTTON_TYPE = new BooleanSetting("revanced_replace_toolbar_create_button_type", FALSE, true);
// PreferenceScreen: Player
public static final IntegerSetting CUSTOM_PLAYER_OVERLAY_OPACITY = new IntegerSetting("revanced_custom_player_overlay_opacity", 100, true);
public static final BooleanSetting DISABLE_AUTO_PLAYER_POPUP_PANELS = new BooleanSetting("revanced_disable_auto_player_popup_panels", TRUE, true);
public static final BooleanSetting DISABLE_AUTO_SWITCH_MIX_PLAYLISTS = new BooleanSetting("revanced_disable_auto_switch_mix_playlists", FALSE, true, "revanced_disable_auto_switch_mix_playlists_user_dialog_message");
public static final BooleanSetting DISABLE_SPEED_OVERLAY = new BooleanSetting("revanced_disable_speed_overlay", FALSE, true);
public static final FloatSetting SPEED_OVERLAY_VALUE = new FloatSetting("revanced_speed_overlay_value", 2.0f, true);
public static final BooleanSetting HIDE_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE);
public static final BooleanSetting HIDE_CROWDFUNDING_BOX = new BooleanSetting("revanced_hide_crowdfunding_box", TRUE, true);
public static final BooleanSetting HIDE_DOUBLE_TAP_OVERLAY_FILTER = new BooleanSetting("revanced_hide_double_tap_overlay_filter", FALSE, true);
public static final BooleanSetting HIDE_END_SCREEN_CARDS = new BooleanSetting("revanced_hide_end_screen_cards", FALSE, true);
public static final BooleanSetting HIDE_FILMSTRIP_OVERLAY = new BooleanSetting("revanced_hide_filmstrip_overlay", FALSE, true);
public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", FALSE, true);
public static final BooleanSetting HIDE_INFO_PANEL = new BooleanSetting("revanced_hide_info_panel", TRUE);
public static final BooleanSetting HIDE_LIVE_CHAT_MESSAGES = new BooleanSetting("revanced_hide_live_chat_messages", FALSE);
public static final BooleanSetting HIDE_MEDICAL_PANEL = new BooleanSetting("revanced_hide_medical_panel", TRUE);
public static final BooleanSetting HIDE_SEEK_MESSAGE = new BooleanSetting("revanced_hide_seek_message", FALSE, true);
public static final BooleanSetting HIDE_SEEK_UNDO_MESSAGE = new BooleanSetting("revanced_hide_seek_undo_message", FALSE, true);
public static final BooleanSetting HIDE_SUGGESTED_ACTION = new BooleanSetting("revanced_hide_suggested_actions", TRUE, true);
public static final BooleanSetting HIDE_TIMED_REACTIONS = new BooleanSetting("revanced_hide_timed_reactions", TRUE);
public static final BooleanSetting HIDE_SUGGESTED_VIDEO_END_SCREEN = new BooleanSetting("revanced_hide_suggested_video_end_screen", TRUE, true);
public static final BooleanSetting SKIP_AUTOPLAY_COUNTDOWN = new BooleanSetting("revanced_skip_autoplay_countdown", FALSE, true, parent(HIDE_SUGGESTED_VIDEO_END_SCREEN));
public static final BooleanSetting HIDE_ZOOM_OVERLAY = new BooleanSetting("revanced_hide_zoom_overlay", FALSE, true);
public static final BooleanSetting SANITIZE_VIDEO_SUBTITLE = new BooleanSetting("revanced_sanitize_video_subtitle", FALSE);
// PreferenceScreen: Player - Action buttons
public static final BooleanSetting DISABLE_LIKE_DISLIKE_GLOW = new BooleanSetting("revanced_disable_like_dislike_glow", FALSE);
public static final BooleanSetting HIDE_CLIP_BUTTON = new BooleanSetting("revanced_hide_clip_button", FALSE);
public static final BooleanSetting HIDE_DOWNLOAD_BUTTON = new BooleanSetting("revanced_hide_download_button", FALSE);
public static final BooleanSetting HIDE_LIKE_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_like_dislike_button", FALSE);
public static final BooleanSetting HIDE_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_playlist_button", FALSE);
public static final BooleanSetting HIDE_REMIX_BUTTON = new BooleanSetting("revanced_hide_remix_button", FALSE);
public static final BooleanSetting HIDE_REWARDS_BUTTON = new BooleanSetting("revanced_hide_rewards_button", FALSE);
public static final BooleanSetting HIDE_REPORT_BUTTON = new BooleanSetting("revanced_hide_report_button", FALSE);
public static final BooleanSetting HIDE_SHARE_BUTTON = new BooleanSetting("revanced_hide_share_button", FALSE);
public static final BooleanSetting HIDE_SHOP_BUTTON = new BooleanSetting("revanced_hide_shop_button", FALSE);
public static final BooleanSetting HIDE_THANKS_BUTTON = new BooleanSetting("revanced_hide_thanks_button", FALSE);
// PreferenceScreen: Player - Ambient mode
public static final BooleanSetting BYPASS_AMBIENT_MODE_RESTRICTIONS = new BooleanSetting("revanced_bypass_ambient_mode_restrictions", FALSE);
public static final BooleanSetting DISABLE_AMBIENT_MODE = new BooleanSetting("revanced_disable_ambient_mode", FALSE, true);
public static final BooleanSetting DISABLE_AMBIENT_MODE_IN_FULLSCREEN = new BooleanSetting("revanced_disable_ambient_mode_in_fullscreen", FALSE, true);
// PreferenceScreen: Player - Channel bar
public static final BooleanSetting HIDE_JOIN_BUTTON = new BooleanSetting("revanced_hide_join_button", TRUE);
public static final BooleanSetting HIDE_START_TRIAL_BUTTON = new BooleanSetting("revanced_hide_start_trial_button", TRUE);
// PreferenceScreen: Player - Comments
public static final BooleanSetting HIDE_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_channel_guidelines", TRUE);
public static final BooleanSetting HIDE_COMMENTS_BY_MEMBERS = new BooleanSetting("revanced_hide_comments_by_members", FALSE);
public static final BooleanSetting HIDE_COMMENT_HIGHLIGHTED_SEARCH_LINKS = new BooleanSetting("revanced_hide_comment_highlighted_search_links", FALSE, true);
public static final BooleanSetting HIDE_COMMENTS_SECTION = new BooleanSetting("revanced_hide_comments_section", FALSE);
public static final BooleanSetting HIDE_COMMENTS_SECTION_IN_HOME_FEED = new BooleanSetting("revanced_hide_comments_section_in_home_feed", FALSE);
public static final BooleanSetting HIDE_PREVIEW_COMMENT = new BooleanSetting("revanced_hide_preview_comment", FALSE);
public static final BooleanSetting HIDE_PREVIEW_COMMENT_TYPE = new BooleanSetting("revanced_hide_preview_comment_type", FALSE);
public static final BooleanSetting HIDE_PREVIEW_COMMENT_OLD_METHOD = new BooleanSetting("revanced_hide_preview_comment_old_method", FALSE);
public static final BooleanSetting HIDE_PREVIEW_COMMENT_NEW_METHOD = new BooleanSetting("revanced_hide_preview_comment_new_method", FALSE);
public static final BooleanSetting HIDE_COMMENT_CREATE_SHORTS_BUTTON = new BooleanSetting("revanced_hide_comment_create_shorts_button", FALSE);
public static final BooleanSetting HIDE_COMMENT_THANKS_BUTTON = new BooleanSetting("revanced_hide_comment_thanks_button", FALSE, true);
public static final BooleanSetting HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS = new BooleanSetting("revanced_hide_comment_timestamp_and_emoji_buttons", FALSE);
// PreferenceScreen: Player - Flyout menu
public static final BooleanSetting CHANGE_PLAYER_FLYOUT_MENU_TOGGLE = new BooleanSetting("revanced_change_player_flyout_menu_toggle", FALSE, true);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_ENHANCED_BITRATE = new BooleanSetting("revanced_hide_player_flyout_menu_enhanced_bitrate", TRUE, true);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_AUDIO_TRACK = new BooleanSetting("revanced_hide_player_flyout_menu_audio_track", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_CAPTIONS = new BooleanSetting("revanced_hide_player_flyout_menu_captions", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_CAPTIONS_FOOTER = new BooleanSetting("revanced_hide_player_flyout_menu_captions_footer", TRUE, true);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_LOCK_SCREEN = new BooleanSetting("revanced_hide_player_flyout_menu_lock_screen", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_MORE = new BooleanSetting("revanced_hide_player_flyout_menu_more_info", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_PLAYBACK_SPEED = new BooleanSetting("revanced_hide_player_flyout_menu_playback_speed", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_QUALITY_HEADER = new BooleanSetting("revanced_hide_player_flyout_menu_quality_header", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_QUALITY_FOOTER = new BooleanSetting("revanced_hide_player_flyout_menu_quality_footer", TRUE, true);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_REPORT = new BooleanSetting("revanced_hide_player_flyout_menu_report", TRUE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_ADDITIONAL_SETTINGS = new BooleanSetting("revanced_hide_player_flyout_menu_additional_settings", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_AMBIENT = new BooleanSetting("revanced_hide_player_flyout_menu_ambient_mode", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_HELP = new BooleanSetting("revanced_hide_player_flyout_menu_help", TRUE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_LOOP = new BooleanSetting("revanced_hide_player_flyout_menu_loop_video", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_PIP = new BooleanSetting("revanced_hide_player_flyout_menu_pip", TRUE, true);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_PREMIUM_CONTROLS = new BooleanSetting("revanced_hide_player_flyout_menu_premium_controls", TRUE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_SLEEP_TIMER = new BooleanSetting("revanced_hide_player_flyout_menu_sleep_timer", TRUE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_STABLE_VOLUME = new BooleanSetting("revanced_hide_player_flyout_menu_stable_volume", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_STATS_FOR_NERDS = new BooleanSetting("revanced_hide_player_flyout_menu_stats_for_nerds", FALSE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_WATCH_IN_VR = new BooleanSetting("revanced_hide_player_flyout_menu_watch_in_vr", TRUE);
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_YT_MUSIC = new BooleanSetting("revanced_hide_player_flyout_menu_listen_with_youtube_music", TRUE);
// PreferenceScreen: Player - Fullscreen
public static final BooleanSetting DISABLE_ENGAGEMENT_PANEL = new BooleanSetting("revanced_disable_engagement_panel", FALSE, true);
public static final BooleanSetting SHOW_VIDEO_TITLE_SECTION = new BooleanSetting("revanced_show_video_title_section", TRUE, true, parent(DISABLE_ENGAGEMENT_PANEL));
public static final BooleanSetting HIDE_AUTOPLAY_PREVIEW = new BooleanSetting("revanced_hide_autoplay_preview", FALSE, true);
public static final BooleanSetting HIDE_LIVE_CHAT_REPLAY_BUTTON = new BooleanSetting("revanced_hide_live_chat_replay_button", FALSE);
public static final BooleanSetting HIDE_RELATED_VIDEO_OVERLAY = new BooleanSetting("revanced_hide_related_video_overlay", FALSE, true);
public static final BooleanSetting HIDE_QUICK_ACTIONS = new BooleanSetting("revanced_hide_quick_actions", FALSE, true);
public static final BooleanSetting HIDE_QUICK_ACTIONS_COMMENT_BUTTON = new BooleanSetting("revanced_hide_quick_actions_comment_button", FALSE);
public static final BooleanSetting HIDE_QUICK_ACTIONS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_quick_actions_dislike_button", FALSE);
public static final BooleanSetting HIDE_QUICK_ACTIONS_LIKE_BUTTON = new BooleanSetting("revanced_hide_quick_actions_like_button", FALSE);
public static final BooleanSetting HIDE_QUICK_ACTIONS_LIVE_CHAT_BUTTON = new BooleanSetting("revanced_hide_quick_actions_live_chat_button", FALSE);
public static final BooleanSetting HIDE_QUICK_ACTIONS_MORE_BUTTON = new BooleanSetting("revanced_hide_quick_actions_more_button", FALSE);
public static final BooleanSetting HIDE_QUICK_ACTIONS_OPEN_MIX_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_quick_actions_open_mix_playlist_button", FALSE);
public static final BooleanSetting HIDE_QUICK_ACTIONS_OPEN_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_quick_actions_open_playlist_button", FALSE);
public static final BooleanSetting HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_quick_actions_save_to_playlist_button", FALSE);
public static final BooleanSetting HIDE_QUICK_ACTIONS_SHARE_BUTTON = new BooleanSetting("revanced_hide_quick_actions_share_button", FALSE);
public static final IntegerSetting QUICK_ACTIONS_TOP_MARGIN = new IntegerSetting("revanced_quick_actions_top_margin", 0, true);
public static final BooleanSetting DISABLE_LANDSCAPE_MODE = new BooleanSetting("revanced_disable_landscape_mode", FALSE, true);
public static final BooleanSetting ENABLE_COMPACT_CONTROLS_OVERLAY = new BooleanSetting("revanced_enable_compact_controls_overlay", FALSE, true);
public static final BooleanSetting FORCE_FULLSCREEN = new BooleanSetting("revanced_force_fullscreen", FALSE, true);
public static final BooleanSetting KEEP_LANDSCAPE_MODE = new BooleanSetting("revanced_keep_landscape_mode", FALSE, true);
public static final LongSetting KEEP_LANDSCAPE_MODE_TIMEOUT = new LongSetting("revanced_keep_landscape_mode_timeout", 3000L, true);
// PreferenceScreen: Player - Haptic feedback
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SCRUBBING = new BooleanSetting("revanced_disable_haptic_feedback_scrubbing", FALSE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SEEK = new BooleanSetting("revanced_disable_haptic_feedback_seek", FALSE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO = new BooleanSetting("revanced_disable_haptic_feedback_seek_undo", FALSE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_ZOOM = new BooleanSetting("revanced_disable_haptic_feedback_zoom", FALSE);
// PreferenceScreen: Player - Player buttons
public static final BooleanSetting HIDE_PLAYER_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_player_autoplay_button", TRUE, true);
public static final BooleanSetting HIDE_PLAYER_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_player_captions_button", FALSE, true);
public static final BooleanSetting HIDE_PLAYER_CAST_BUTTON = new BooleanSetting("revanced_hide_player_cast_button", TRUE, true);
public static final BooleanSetting HIDE_PLAYER_COLLAPSE_BUTTON = new BooleanSetting("revanced_hide_player_collapse_button", FALSE, true);
public static final BooleanSetting HIDE_PLAYER_FULLSCREEN_BUTTON = new BooleanSetting("revanced_hide_player_fullscreen_button", FALSE, true);
public static final BooleanSetting HIDE_PLAYER_PREVIOUS_NEXT_BUTTON = new BooleanSetting("revanced_hide_player_previous_next_button", FALSE, true);
public static final BooleanSetting HIDE_PLAYER_YOUTUBE_MUSIC_BUTTON = new BooleanSetting("revanced_hide_player_youtube_music_button", FALSE);
public static final BooleanSetting ALWAYS_REPEAT = new BooleanSetting("revanced_always_repeat", FALSE);
public static final BooleanSetting ALWAYS_REPEAT_PAUSE = new BooleanSetting("revanced_always_repeat_pause", FALSE);
public static final BooleanSetting OVERLAY_BUTTON_ALWAYS_REPEAT = new BooleanSetting("revanced_overlay_button_always_repeat", FALSE);
public static final BooleanSetting OVERLAY_BUTTON_COPY_VIDEO_URL = new BooleanSetting("revanced_overlay_button_copy_video_url", FALSE);
public static final BooleanSetting OVERLAY_BUTTON_COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_overlay_button_copy_video_url_timestamp", FALSE);
public static final BooleanSetting OVERLAY_BUTTON_MUTE_VOLUME = new BooleanSetting("revanced_overlay_button_mute_volume", FALSE);
public static final BooleanSetting OVERLAY_BUTTON_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_overlay_button_external_downloader", FALSE);
public static final BooleanSetting OVERLAY_BUTTON_SPEED_DIALOG = new BooleanSetting("revanced_overlay_button_speed_dialog", FALSE);
public static final BooleanSetting OVERLAY_BUTTON_PLAY_ALL = new BooleanSetting("revanced_overlay_button_play_all", FALSE);
public static final EnumSetting<PlaylistIdPrefix> OVERLAY_BUTTON_PLAY_ALL_TYPE = new EnumSetting<>("revanced_overlay_button_play_all_type", PlaylistIdPrefix.ALL_CONTENTS_WITH_TIME_DESCENDING);
public static final BooleanSetting OVERLAY_BUTTON_WHITELIST = new BooleanSetting("revanced_overlay_button_whitelist", FALSE);
// PreferenceScreen: Player - Seekbar
public static final BooleanSetting APPEND_TIME_STAMP_INFORMATION = new BooleanSetting("revanced_append_time_stamp_information", TRUE, true);
public static final BooleanSetting APPEND_TIME_STAMP_INFORMATION_TYPE = new BooleanSetting("revanced_append_time_stamp_information_type", TRUE, parent(APPEND_TIME_STAMP_INFORMATION));
public static final BooleanSetting REPLACE_TIME_STAMP_ACTION = new BooleanSetting("revanced_replace_time_stamp_action", TRUE, true, parent(APPEND_TIME_STAMP_INFORMATION));
public static final BooleanSetting ENABLE_CUSTOM_SEEKBAR_COLOR = new BooleanSetting("revanced_enable_custom_seekbar_color", FALSE, true);
public static final StringSetting ENABLE_CUSTOM_SEEKBAR_COLOR_VALUE = new StringSetting("revanced_custom_seekbar_color_value", "#FF0000", true, parent(ENABLE_CUSTOM_SEEKBAR_COLOR));
public static final BooleanSetting ENABLE_SEEKBAR_TAPPING = new BooleanSetting("revanced_enable_seekbar_tapping", TRUE);
public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE, true);
public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE);
public static final BooleanSetting DISABLE_SEEKBAR_CHAPTERS = new BooleanSetting("revanced_disable_seekbar_chapters", FALSE, true);
public static final BooleanSetting HIDE_SEEKBAR_CHAPTER_LABEL = new BooleanSetting("revanced_hide_seekbar_chapter_label", FALSE, true);
public static final BooleanSetting HIDE_TIME_STAMP = new BooleanSetting("revanced_hide_time_stamp", FALSE, true);
public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails",
PatchStatus.OldSeekbarThumbnailsDefaultBoolean(), true);
public static final BooleanSetting ENABLE_SEEKBAR_THUMBNAILS_HIGH_QUALITY = new BooleanSetting("revanced_enable_seekbar_thumbnails_high_quality", FALSE, true, "revanced_enable_seekbar_thumbnails_high_quality_dialog_message");
public static final BooleanSetting ENABLE_CAIRO_SEEKBAR = new BooleanSetting("revanced_enable_cairo_seekbar", FALSE, true);
// PreferenceScreen: Player - Video description
public static final BooleanSetting DISABLE_ROLLING_NUMBER_ANIMATIONS = new BooleanSetting("revanced_disable_rolling_number_animations", FALSE);
public static final BooleanSetting HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION = new BooleanSetting("revanced_hide_ai_generated_video_summary_section", FALSE);
public static final BooleanSetting HIDE_ATTRIBUTES_SECTION = new BooleanSetting("revanced_hide_attributes_section", FALSE);
public static final BooleanSetting HIDE_CHAPTERS_SECTION = new BooleanSetting("revanced_hide_chapters_section", FALSE);
public static final BooleanSetting HIDE_CONTENTS_SECTION = new BooleanSetting("revanced_hide_contents_section", FALSE);
public static final BooleanSetting HIDE_INFO_CARDS_SECTION = new BooleanSetting("revanced_hide_info_cards_section", FALSE);
public static final BooleanSetting HIDE_KEY_CONCEPTS_SECTION = new BooleanSetting("revanced_hide_key_concepts_section", FALSE);
public static final BooleanSetting HIDE_PODCAST_SECTION = new BooleanSetting("revanced_hide_podcast_section", FALSE);
public static final BooleanSetting HIDE_SHOPPING_LINKS = new BooleanSetting("revanced_hide_shopping_links", TRUE);
public static final BooleanSetting HIDE_TRANSCRIPT_SECTION = new BooleanSetting("revanced_hide_transcript_section", FALSE);
public static final BooleanSetting DISABLE_VIDEO_DESCRIPTION_INTERACTION = new BooleanSetting("revanced_disable_video_description_interaction", FALSE, true);
public static final BooleanSetting EXPAND_VIDEO_DESCRIPTION = new BooleanSetting("revanced_expand_video_description", FALSE, true);
public static final StringSetting EXPAND_VIDEO_DESCRIPTION_STRINGS = new StringSetting("revanced_expand_video_description_strings", str("revanced_expand_video_description_strings_default_value"), true, parent(EXPAND_VIDEO_DESCRIPTION));
// PreferenceScreen: Shorts
public static final BooleanSetting DISABLE_RESUMING_SHORTS_PLAYER = new BooleanSetting("revanced_disable_resuming_shorts_player", TRUE);
public static final BooleanSetting HIDE_SHORTS_FLOATING_BUTTON = new BooleanSetting("revanced_hide_shorts_floating_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SHELF = new BooleanSetting("revanced_hide_shorts_shelf", TRUE, true);
public static final BooleanSetting HIDE_SHORTS_SHELF_CHANNEL = new BooleanSetting("revanced_hide_shorts_shelf_channel", FALSE);
public static final BooleanSetting HIDE_SHORTS_SHELF_HOME_RELATED_VIDEOS = new BooleanSetting("revanced_hide_shorts_shelf_home_related_videos", TRUE);
public static final BooleanSetting HIDE_SHORTS_SHELF_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_shorts_shelf_subscriptions", TRUE);
public static final BooleanSetting HIDE_SHORTS_SHELF_SEARCH = new BooleanSetting("revanced_hide_shorts_shelf_search", TRUE);
public static final BooleanSetting HIDE_SHORTS_SHELF_HISTORY = new BooleanSetting("revanced_hide_shorts_shelf_history", FALSE);
public static final IntegerSetting CHANGE_SHORTS_REPEAT_STATE = new IntegerSetting("revanced_change_shorts_repeat_state", 0);
// PreferenceScreen: Shorts - Shorts player components
public static final BooleanSetting HIDE_SHORTS_JOIN_BUTTON = new BooleanSetting("revanced_hide_shorts_join_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SUBSCRIBE_BUTTON = new BooleanSetting("revanced_hide_shorts_subscribe_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_PAUSED_HEADER = new BooleanSetting("revanced_hide_shorts_paused_header", FALSE, true);
public static final BooleanSetting HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS = new BooleanSetting("revanced_hide_shorts_paused_overlay_buttons", FALSE);
public static final BooleanSetting HIDE_SHORTS_TRENDS_BUTTON = new BooleanSetting("revanced_hide_shorts_trends_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SHOPPING_BUTTON = new BooleanSetting("revanced_hide_shorts_shopping_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_STICKERS = new BooleanSetting("revanced_hide_shorts_stickers", TRUE);
public static final BooleanSetting HIDE_SHORTS_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_shorts_paid_promotion_label", TRUE, true);
public static final BooleanSetting HIDE_SHORTS_INFO_PANEL = new BooleanSetting("revanced_hide_shorts_info_panel", TRUE);
public static final BooleanSetting HIDE_SHORTS_LIVE_HEADER = new BooleanSetting("revanced_hide_shorts_live_header", FALSE);
public static final BooleanSetting HIDE_SHORTS_CHANNEL_BAR = new BooleanSetting("revanced_hide_shorts_channel_bar", FALSE);
public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE);
public static final BooleanSetting HIDE_SHORTS_SOUND_METADATA_LABEL = new BooleanSetting("revanced_hide_shorts_sound_metadata_label", TRUE);
public static final BooleanSetting HIDE_SHORTS_FULL_VIDEO_LINK_LABEL = new BooleanSetting("revanced_hide_shorts_full_video_link_label", TRUE);
// PreferenceScreen: Shorts - Shorts player components - Suggested actions
public static final BooleanSetting HIDE_SHORTS_GREEN_SCREEN_BUTTON = new BooleanSetting("revanced_hide_shorts_green_screen_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SAVE_MUSIC_BUTTON = new BooleanSetting("revanced_hide_shorts_save_music_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SHOP_BUTTON = new BooleanSetting("revanced_hide_shorts_shop_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SUPER_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_super_thanks_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_USE_THIS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_use_this_sound_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_USE_TEMPLATE_BUTTON = new BooleanSetting("revanced_hide_shorts_use_template_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_LOCATION_BUTTON = new BooleanSetting("revanced_hide_shorts_location_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS_BUTTON = new BooleanSetting("revanced_hide_shorts_search_suggestions_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_TAGGED_PRODUCTS = new BooleanSetting("revanced_hide_shorts_tagged_products", TRUE);
// PreferenceScreen: Shorts - Shorts player components - Action buttons
public static final BooleanSetting HIDE_SHORTS_LIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_like_button", FALSE);
public static final BooleanSetting HIDE_SHORTS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_dislike_button", FALSE);
public static final BooleanSetting HIDE_SHORTS_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_shorts_comments_button", FALSE);
public static final BooleanSetting HIDE_SHORTS_REMIX_BUTTON = new BooleanSetting("revanced_hide_shorts_remix_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_SHARE_BUTTON = new BooleanSetting("revanced_hide_shorts_share_button", FALSE);
public static final BooleanSetting HIDE_SHORTS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_sound_button", TRUE);
public static final BooleanSetting DISABLE_SHORTS_LIKE_BUTTON_FOUNTAIN_ANIMATION = new BooleanSetting("revanced_disable_shorts_like_button_fountain_animation", FALSE);
public static final BooleanSetting HIDE_SHORTS_PLAY_PAUSE_BUTTON_BACKGROUND = new BooleanSetting("revanced_hide_shorts_play_pause_button_background", FALSE, true);
public static final EnumSetting<AnimationType> ANIMATION_TYPE = new EnumSetting<>("revanced_shorts_double_tap_to_like_animation", AnimationType.ORIGINAL, true);
// Experimental Flags
public static final BooleanSetting ENABLE_TIME_STAMP = new BooleanSetting("revanced_enable_shorts_time_stamp", FALSE, true);
public static final BooleanSetting TIME_STAMP_CHANGE_REPEAT_STATE = new BooleanSetting("revanced_shorts_time_stamp_change_repeat_state", TRUE, true, parent(ENABLE_TIME_STAMP));
public static final IntegerSetting META_PANEL_BOTTOM_MARGIN = new IntegerSetting("revanced_shorts_meta_panel_bottom_margin", 32, true, parent(ENABLE_TIME_STAMP));
public static final BooleanSetting HIDE_SHORTS_TOOLBAR = new BooleanSetting("revanced_hide_shorts_toolbar", FALSE, true);
public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", FALSE, true);
public static final IntegerSetting SHORTS_NAVIGATION_BAR_HEIGHT_PERCENTAGE = new IntegerSetting("revanced_shorts_navigation_bar_height_percentage", 45, true, parent(HIDE_SHORTS_NAVIGATION_BAR));
public static final BooleanSetting REPLACE_CHANNEL_HANDLE = new BooleanSetting("revanced_replace_channel_handle", FALSE, true);
// PreferenceScreen: Swipe controls
public static final BooleanSetting ENABLE_SWIPE_BRIGHTNESS = new BooleanSetting("revanced_enable_swipe_brightness", TRUE, true);
public static final BooleanSetting ENABLE_SWIPE_VOLUME = new BooleanSetting("revanced_enable_swipe_volume", TRUE, true);
public static final BooleanSetting ENABLE_SWIPE_LOWEST_VALUE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_enable_swipe_lowest_value_auto_brightness", TRUE, parent(ENABLE_SWIPE_BRIGHTNESS));
public static final BooleanSetting ENABLE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_enable_save_and_restore_brightness", TRUE, true, parent(ENABLE_SWIPE_BRIGHTNESS));
public static final BooleanSetting ENABLE_SWIPE_PRESS_TO_ENGAGE = new BooleanSetting("revanced_enable_swipe_press_to_engage", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final BooleanSetting ENABLE_SWIPE_HAPTIC_FEEDBACK = new BooleanSetting("revanced_enable_swipe_haptic_feedback", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final BooleanSetting SWIPE_LOCK_MODE = new BooleanSetting("revanced_swipe_gestures_lock_mode", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_magnitude_threshold", 0, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_BACKGROUND_ALPHA = new IntegerSetting("revanced_swipe_overlay_background_alpha", 127, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_overlay_text_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_RECT_SIZE = new IntegerSetting("revanced_swipe_overlay_rect_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_BRIGHTNESS_SENSITIVITY = new IntegerSetting("revanced_swipe_brightness_sensitivity", 100, true, parent(ENABLE_SWIPE_BRIGHTNESS));
public static final IntegerSetting SWIPE_VOLUME_SENSITIVITY = new IntegerSetting("revanced_swipe_volume_sensitivity", 100, true, parent(ENABLE_SWIPE_VOLUME));
/**
* @noinspection DeprecatedIsStillUsed
*/
@Deprecated // Patch is obsolete and no longer works with 19.09+
public static final BooleanSetting DISABLE_HDR_AUTO_BRIGHTNESS = new BooleanSetting("revanced_disable_hdr_auto_brightness", TRUE, true, parent(ENABLE_SWIPE_BRIGHTNESS));
public static final BooleanSetting ENABLE_SWIPE_TO_SWITCH_VIDEO = new BooleanSetting("revanced_enable_swipe_to_switch_video", FALSE, true);
public static final BooleanSetting ENABLE_WATCH_PANEL_GESTURES = new BooleanSetting("revanced_enable_watch_panel_gestures", FALSE, true);
public static final BooleanSetting SWIPE_BRIGHTNESS_AUTO = new BooleanSetting("revanced_swipe_brightness_auto", TRUE, false, false);
public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1.0f, false, false);
// PreferenceScreen: Video
public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", -2.0f);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2);
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE, true);
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_LIVE = new BooleanSetting("revanced_disable_default_playback_speed_live", TRUE);
public static final BooleanSetting ENABLE_CUSTOM_PLAYBACK_SPEED = new BooleanSetting("revanced_enable_custom_playback_speed", FALSE, true);
public static final BooleanSetting CUSTOM_PLAYBACK_SPEED_MENU_TYPE = new BooleanSetting("revanced_custom_playback_speed_menu_type", FALSE, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", "0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.25\n2.5", true, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED));
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", TRUE);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_LAST_SELECTED));
public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE, true);
// Experimental Flags
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = new BooleanSetting("revanced_disable_default_playback_speed_music", FALSE, true);
public static final BooleanSetting ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS = new BooleanSetting("revanced_enable_default_playback_speed_shorts", FALSE);
public static final BooleanSetting SKIP_PRELOADED_BUFFER = new BooleanSetting("revanced_skip_preloaded_buffer", FALSE, true, "revanced_skip_preloaded_buffer_user_dialog_message");
public static final BooleanSetting SKIP_PRELOADED_BUFFER_TOAST = new BooleanSetting("revanced_skip_preloaded_buffer_toast", TRUE);
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true);
public static final BooleanSetting DISABLE_VP9_CODEC = new BooleanSetting("revanced_disable_vp9_codec", FALSE, true);
public static final BooleanSetting REPLACE_AV1_CODEC = new BooleanSetting("revanced_replace_av1_codec", FALSE, true);
public static final BooleanSetting REJECT_AV1_CODEC = new BooleanSetting("revanced_reject_av1_codec", FALSE, true);
// PreferenceScreen: Miscellaneous
public static final BooleanSetting ENABLE_EXTERNAL_BROWSER = new BooleanSetting("revanced_enable_external_browser", TRUE, true);
public static final BooleanSetting ENABLE_OPEN_LINKS_DIRECTLY = new BooleanSetting("revanced_enable_open_links_directly", TRUE);
public static final BooleanSetting DISABLE_QUIC_PROTOCOL = new BooleanSetting("revanced_disable_quic_protocol", FALSE, true);
// Experimental Flags
public static final BooleanSetting CHANGE_SHARE_SHEET = new BooleanSetting("revanced_change_share_sheet", FALSE, true);
public static final BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true);
/**
* @noinspection DeprecatedIsStillUsed
*/
@Deprecated
public static final LongSetting DOUBLE_BACK_TO_CLOSE_TIMEOUT = new LongSetting("revanced_double_back_to_close_timeout", 2000L);
// PreferenceScreen: Miscellaneous - Watch history
public static final EnumSetting<WatchHistoryType> WATCH_HISTORY_TYPE = new EnumSetting<>("revanced_watch_history_type", WatchHistoryType.REPLACE);
// PreferenceScreen: Miscellaneous - Spoof streaming data
// The order of the settings should not be changed otherwise the app may crash
public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true, "revanced_spoof_streaming_data_user_dialog_message");
public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_streaming_data_ios_force_avc", FALSE, true,
"revanced_spoof_streaming_data_ios_force_avc_user_dialog_message", new SpoofStreamingDataPatch.iOSAvailability());
public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK = new BooleanSetting("revanced_spoof_streaming_data_ios_skip_livestream_playback", TRUE, true, new SpoofStreamingDataPatch.iOSAvailability());
public static final EnumSetting<ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.IOS, true, parent(SPOOF_STREAMING_DATA));
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_STREAMING_DATA));
// PreferenceScreen: Return YouTube Dislike
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("ryd_enabled", TRUE);
public static final StringSetting RYD_USER_ID = new StringSetting("ryd_user_id", "");
public static final BooleanSetting RYD_SHORTS = new BooleanSetting("ryd_shorts", TRUE, parent(RYD_ENABLED));
public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("ryd_dislike_percentage", FALSE, parent(RYD_ENABLED));
public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("ryd_compact_layout", FALSE, parent(RYD_ENABLED));
public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("ryd_estimated_like", FALSE, true, parent(RYD_ENABLED));
public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("ryd_toast_on_connection_error", FALSE, parent(RYD_ENABLED));
// PreferenceScreen: SponsorBlock
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
/**
* Do not use directly, instead use {@link SponsorBlockSettings}
*/
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "", parent(SB_ENABLED));
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
public static final BooleanSetting SB_CREATE_NEW_SEGMENT = new BooleanSetting("sb_create_new_segment", FALSE, parent(SB_ENABLED));
public static final BooleanSetting SB_COMPACT_SKIP_BUTTON = new BooleanSetting("sb_compact_skip_button", FALSE, parent(SB_ENABLED));
public static final BooleanSetting SB_AUTO_HIDE_SKIP_BUTTON = new BooleanSetting("sb_auto_hide_skip_button", TRUE, parent(SB_ENABLED));
public static final BooleanSetting SB_TOAST_ON_SKIP = new BooleanSetting("sb_toast_on_skip", TRUE, parent(SB_ENABLED));
public static final BooleanSetting SB_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("sb_toast_on_connection_error", FALSE, parent(SB_ENABLED));
public static final BooleanSetting SB_TRACK_SKIP_COUNT = new BooleanSetting("sb_track_skip_count", TRUE, parent(SB_ENABLED));
public static final FloatSetting SB_SEGMENT_MIN_DURATION = new FloatSetting("sb_min_segment_duration", 0F, parent(SB_ENABLED));
public static final BooleanSetting SB_VIDEO_LENGTH_WITHOUT_SEGMENTS = new BooleanSetting("sb_video_length_without_segments", FALSE, parent(SB_ENABLED));
public static final StringSetting SB_API_URL = new StringSetting("sb_api_url", "https://sponsor.ajay.app", parent(SB_ENABLED));
public static final BooleanSetting SB_USER_IS_VIP = new BooleanSetting("sb_user_is_vip", FALSE);
public static final IntegerSetting SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS = new IntegerSetting("sb_local_time_saved_number_segments", 0);
public static final LongSetting SB_LOCAL_TIME_SAVED_MILLISECONDS = new LongSetting("sb_local_time_saved_milliseconds", 0L);
public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400");
public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00");
public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF");
public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#FF1684");
public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF");
public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED");
public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6");
public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF");
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900");
public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFF");
// SB Setting not exported
public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false);
public static final BooleanSetting SB_HIDE_EXPORT_WARNING = new BooleanSetting("sb_hide_export_warning", FALSE, false, false);
public static final BooleanSetting SB_SEEN_GUIDELINES = new BooleanSetting("sb_seen_guidelines", FALSE, false, false);
static {
// region Migration initialized
// Categories were previously saved without a 'sb_' key prefix, so they need an additional adjustment.
Set<Setting<?>> sbCategories = new HashSet<>(Arrays.asList(
SB_CATEGORY_SPONSOR,
SB_CATEGORY_SPONSOR_COLOR,
SB_CATEGORY_SELF_PROMO,
SB_CATEGORY_SELF_PROMO_COLOR,
SB_CATEGORY_INTERACTION,
SB_CATEGORY_INTERACTION_COLOR,
SB_CATEGORY_HIGHLIGHT,
SB_CATEGORY_HIGHLIGHT_COLOR,
SB_CATEGORY_INTRO,
SB_CATEGORY_INTRO_COLOR,
SB_CATEGORY_OUTRO,
SB_CATEGORY_OUTRO_COLOR,
SB_CATEGORY_PREVIEW,
SB_CATEGORY_PREVIEW_COLOR,
SB_CATEGORY_FILLER,
SB_CATEGORY_FILLER_COLOR,
SB_CATEGORY_MUSIC_OFFTOPIC,
SB_CATEGORY_MUSIC_OFFTOPIC_COLOR,
SB_CATEGORY_UNSUBMITTED,
SB_CATEGORY_UNSUBMITTED_COLOR));
SharedPrefCategory ytPrefs = new SharedPrefCategory("youtube");
SharedPrefCategory rydPrefs = new SharedPrefCategory("ryd");
SharedPrefCategory sbPrefs = new SharedPrefCategory("sponsor-block");
for (Setting<?> setting : Setting.allLoadedSettings()) {
String key = setting.key;
if (setting.key.startsWith("sb_")) {
if (sbCategories.contains(setting)) {
key = key.substring(3); // Remove the "sb_" prefix, as old categories are saved without it.
}
migrateFromOldPreferences(sbPrefs, setting, key);
} else if (setting.key.startsWith("ryd_")) {
migrateFromOldPreferences(rydPrefs, setting, key);
} else {
migrateFromOldPreferences(ytPrefs, setting, key);
}
}
// endregion
}
}

View File

@ -0,0 +1,46 @@
package app.revanced.extension.youtube.settings.preference;
import android.app.Activity;
import android.content.Context;
import android.preference.Preference;
import android.util.AttributeSet;
import app.revanced.extension.shared.settings.preference.YouTubeDataAPIDialogBuilder;
@SuppressWarnings({"unused", "deprecation"})
public class AboutYouTubeDataAPIPreference extends Preference implements Preference.OnPreferenceClickListener {
private void init() {
setSelectable(true);
setOnPreferenceClickListener(this);
}
public AboutYouTubeDataAPIPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public AboutYouTubeDataAPIPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public AboutYouTubeDataAPIPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public AboutYouTubeDataAPIPreference(Context context) {
super(context);
init();
}
@Override
public boolean onPreferenceClick(Preference preference) {
if (getContext() instanceof Activity mActivity) {
YouTubeDataAPIDialogBuilder.showDialog(mActivity);
}
return true;
}
}

View File

@ -0,0 +1,38 @@
package app.revanced.extension.youtube.settings.preference;
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", "deprecation"})
public class AlternativeThumbnailsAboutDeArrowPreference extends Preference {
{
setOnPreferenceClickListener(pref -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://dearrow.ajay.app"));
pref.getContext().startActivity(i);
return false;
});
}
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AlternativeThumbnailsAboutDeArrowPreference(Context context) {
super(context);
}
}

View File

@ -0,0 +1,175 @@
package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.preference.Preference;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import java.util.Arrays;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.ExtendedUtils;
@SuppressWarnings({"unused", "deprecation"})
public class ExternalDownloaderPlaylistPreference extends Preference implements Preference.OnPreferenceClickListener {
private static final StringSetting settings = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME_PLAYLIST;
private static final String[] mEntries = ResourceUtils.getStringArray("revanced_external_downloader_playlist_label");
private static final String[] mEntryValues = ResourceUtils.getStringArray("revanced_external_downloader_playlist_package_name");
private static final String[] mWebsiteEntries = ResourceUtils.getStringArray("revanced_external_downloader_playlist_website");
@SuppressLint("StaticFieldLeak")
private static EditText mEditText;
private static String packageName;
private static int mClickedDialogEntryIndex;
private final TextWatcher textWatcher = new TextWatcher() {
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
public void afterTextChanged(Editable s) {
packageName = s.toString();
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
}
};
private void init() {
setSelectable(true);
setOnPreferenceClickListener(this);
}
public ExternalDownloaderPlaylistPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public ExternalDownloaderPlaylistPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public ExternalDownloaderPlaylistPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ExternalDownloaderPlaylistPreference(Context context) {
super(context);
init();
}
@Override
public boolean onPreferenceClick(Preference preference) {
packageName = settings.get();
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
final Context context = getContext();
AlertDialog.Builder builder = Utils.getEditTextDialogBuilder(context);
TableLayout table = new TableLayout(context);
table.setOrientation(LinearLayout.HORIZONTAL);
table.setPadding(15, 0, 15, 0);
TableRow row = new TableRow(context);
mEditText = new EditText(context);
mEditText.setHint(settings.defaultValue);
mEditText.setText(packageName);
mEditText.addTextChangedListener(textWatcher);
mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 9);
mEditText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f));
row.addView(mEditText);
table.addView(row);
builder.setView(table);
builder.setTitle(str("revanced_external_downloader_dialog_title"));
builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, (dialog, which) -> {
mClickedDialogEntryIndex = which;
mEditText.setText(mEntryValues[which]);
});
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
final String packageName = mEditText.getText().toString().trim();
settings.save(packageName);
checkPackageIsValid(context, packageName);
dialog.dismiss();
});
builder.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> settings.resetToDefault());
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
return true;
}
private static boolean checkPackageIsValid(Context context, String packageName) {
String appName = "";
String website = "";
if (mClickedDialogEntryIndex >= 0) {
appName = mEntries[mClickedDialogEntryIndex];
website = mWebsiteEntries[mClickedDialogEntryIndex];
}
return showToastOrOpenWebsites(context, appName, packageName, website);
}
private static boolean showToastOrOpenWebsites(Context context, String appName, String packageName, String website) {
if (ExtendedUtils.isPackageEnabled(packageName))
return true;
if (website.isEmpty()) {
Utils.showToastShort(str("revanced_external_downloader_not_installed_warning", packageName));
return false;
}
new AlertDialog.Builder(context)
.setTitle(str("revanced_external_downloader_not_installed_dialog_title"))
.setMessage(str("revanced_external_downloader_not_installed_dialog_message", appName, appName))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(website));
context.startActivity(i);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
return false;
}
public static boolean checkPackageIsDisabled() {
final Context context = Utils.getActivity();
packageName = settings.get();
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
return !checkPackageIsValid(context, packageName);
}
public static String getExternalDownloaderPackageName() {
String downloaderPackageName = settings.get().trim();
if (downloaderPackageName.isEmpty()) {
settings.resetToDefault();
downloaderPackageName = settings.defaultValue;
}
return downloaderPackageName;
}
}

View File

@ -0,0 +1,175 @@
package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.preference.Preference;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import java.util.Arrays;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.ExtendedUtils;
@SuppressWarnings({"unused", "deprecation"})
public class ExternalDownloaderVideoPreference extends Preference implements Preference.OnPreferenceClickListener {
private static final StringSetting settings = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME_VIDEO;
private static final String[] mEntries = ResourceUtils.getStringArray("revanced_external_downloader_video_label");
private static final String[] mEntryValues = ResourceUtils.getStringArray("revanced_external_downloader_video_package_name");
private static final String[] mWebsiteEntries = ResourceUtils.getStringArray("revanced_external_downloader_video_website");
@SuppressLint("StaticFieldLeak")
private static EditText mEditText;
private static String packageName;
private static int mClickedDialogEntryIndex;
private final TextWatcher textWatcher = new TextWatcher() {
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
public void afterTextChanged(Editable s) {
packageName = s.toString();
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
}
};
private void init() {
setSelectable(true);
setOnPreferenceClickListener(this);
}
public ExternalDownloaderVideoPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public ExternalDownloaderVideoPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public ExternalDownloaderVideoPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ExternalDownloaderVideoPreference(Context context) {
super(context);
init();
}
@Override
public boolean onPreferenceClick(Preference preference) {
packageName = settings.get();
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
final Context context = getContext();
AlertDialog.Builder builder = Utils.getEditTextDialogBuilder(context);
TableLayout table = new TableLayout(context);
table.setOrientation(LinearLayout.HORIZONTAL);
table.setPadding(15, 0, 15, 0);
TableRow row = new TableRow(context);
mEditText = new EditText(context);
mEditText.setHint(settings.defaultValue);
mEditText.setText(packageName);
mEditText.addTextChangedListener(textWatcher);
mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 9);
mEditText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f));
row.addView(mEditText);
table.addView(row);
builder.setView(table);
builder.setTitle(str("revanced_external_downloader_dialog_title"));
builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, (dialog, which) -> {
mClickedDialogEntryIndex = which;
mEditText.setText(mEntryValues[which]);
});
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
final String packageName = mEditText.getText().toString().trim();
settings.save(packageName);
checkPackageIsValid(context, packageName);
dialog.dismiss();
});
builder.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> settings.resetToDefault());
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
return true;
}
private static boolean checkPackageIsValid(Context context, String packageName) {
String appName = "";
String website = "";
if (mClickedDialogEntryIndex >= 0) {
appName = mEntries[mClickedDialogEntryIndex];
website = mWebsiteEntries[mClickedDialogEntryIndex];
}
return showToastOrOpenWebsites(context, appName, packageName, website);
}
private static boolean showToastOrOpenWebsites(Context context, String appName, String packageName, String website) {
if (ExtendedUtils.isPackageEnabled(packageName))
return true;
if (website.isEmpty()) {
Utils.showToastShort(str("revanced_external_downloader_not_installed_warning", packageName));
return false;
}
new AlertDialog.Builder(context)
.setTitle(str("revanced_external_downloader_not_installed_dialog_title"))
.setMessage(str("revanced_external_downloader_not_installed_dialog_message", appName, appName))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(website));
context.startActivity(i);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
return false;
}
public static boolean checkPackageIsDisabled() {
final Context context = Utils.getActivity();
packageName = settings.get();
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
return !checkPackageIsValid(context, packageName);
}
public static String getExternalDownloaderPackageName() {
String downloaderPackageName = settings.get().trim();
if (downloaderPackageName.isEmpty()) {
settings.resetToDefault();
downloaderPackageName = settings.defaultValue;
}
return downloaderPackageName;
}
}

View File

@ -0,0 +1,102 @@
package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.EditText;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings({"unused", "deprecation"})
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
private String existingSettings;
@TargetApi(26)
private void init() {
setSelectable(true);
EditText editText = getEditText();
editText.setTextIsSelectable(true);
editText.setAutofillHints((String) null);
editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); // Use a smaller font to reduce text wrap.
setOnPreferenceClickListener(this);
}
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public ImportExportPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ImportExportPreference(Context context) {
super(context);
init();
}
@Override
public boolean onPreferenceClick(Preference preference) {
try {
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened.
existingSettings = Setting.exportToJson(null);
getEditText().setText(existingSettings);
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
return true;
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try {
Utils.setEditTextDialogTheme(builder, true);
super.onPrepareDialogBuilder(builder);
// Show the user the settings in JSON format.
builder.setNeutralButton(str("revanced_extended_settings_import_copy"), (dialog, which) ->
Utils.setClipboard(getEditText().getText().toString(), str("revanced_share_copy_settings_success")))
.setPositiveButton(str("revanced_extended_settings_import"), (dialog, which) ->
importSettings(getEditText().getText().toString()));
} catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
}
}
private void importSettings(String replacementSettings) {
try {
if (replacementSettings.equals(existingSettings)) {
return;
}
ReVancedPreferenceFragment.settingImportInProgress = true;
final boolean rebootNeeded = Setting.importFromJSON(replacementSettings, true);
if (rebootNeeded) {
AbstractPreferenceFragment.showRestartDialog(getContext());
}
} catch (Exception ex) {
Logger.printException(() -> "importSettings failure", ex);
} finally {
ReVancedPreferenceFragment.settingImportInProgress = false;
}
}
}

View File

@ -0,0 +1,47 @@
package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.preference.Preference;
import android.util.AttributeSet;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings({"unused", "deprecation"})
public class OpenDefaultAppSettingsPreference extends Preference {
{
setOnPreferenceClickListener(pref -> {
try {
Context context = Utils.getActivity();
final Uri uri = Uri.parse("package:" + context.getPackageName());
final Intent intent = isSDKAbove(31)
? new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri)
: new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri);
context.startActivity(intent);
} catch (Exception exception) {
Logger.printException(() -> "OpenDefaultAppSettings Failed");
}
return false;
});
}
public OpenDefaultAppSettingsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public OpenDefaultAppSettingsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public OpenDefaultAppSettingsPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public OpenDefaultAppSettingsPreference(Context context) {
super(context);
}
}

View File

@ -0,0 +1,689 @@
package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary;
import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.getChildView;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import static app.revanced.extension.shared.utils.Utils.showToastShort;
import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED;
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT;
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT_TYPE;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import android.util.TypedValue;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toolbar;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch;
import app.revanced.extension.youtube.utils.ExtendedUtils;
import app.revanced.extension.youtube.utils.ThemeUtils;
@SuppressWarnings("deprecation")
public class ReVancedPreferenceFragment extends PreferenceFragment {
private static final int READ_REQUEST_CODE = 42;
private static final int WRITE_REQUEST_CODE = 43;
static boolean settingImportInProgress = false;
static boolean showingUserDialogMessage;
@SuppressLint("SuspiciousIndentation")
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try {
if (str == null) return;
Setting<?> setting = Setting.getSettingFromPath(str);
if (setting == null) return;
Preference mPreference = findPreference(str);
if (mPreference == null) return;
if (mPreference instanceof SwitchPreference switchPreference) {
BooleanSetting boolSetting = (BooleanSetting) setting;
if (settingImportInProgress) {
switchPreference.setChecked(boolSetting.get());
} else {
BooleanSetting.privateSetValue(boolSetting, switchPreference.isChecked());
}
if (ExtendedUtils.anyMatchSetting(setting)) {
ExtendedUtils.setPlayerFlyoutMenuAdditionalSettings();
} else if (setting.equals(HIDE_PREVIEW_COMMENT) || setting.equals(HIDE_PREVIEW_COMMENT_TYPE)) {
ExtendedUtils.setCommentPreviewSettings();
}
} else if (mPreference instanceof EditTextPreference editTextPreference) {
if (settingImportInProgress) {
editTextPreference.setText(setting.get().toString());
} else {
Setting.privateSetValueFromString(setting, editTextPreference.getText());
}
} else if (mPreference instanceof ListPreference listPreference) {
if (settingImportInProgress) {
listPreference.setValue(setting.get().toString());
} else {
Setting.privateSetValueFromString(setting, listPreference.getValue());
}
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) {
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries());
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues());
}
if (!(mPreference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
updateListPreferenceSummary(listPreference, setting);
}
} else {
Logger.printException(() -> "Setting cannot be handled: " + mPreference.getClass() + " " + mPreference);
return;
}
ReVancedSettingsPreference.initializeReVancedSettings();
if (settingImportInProgress) {
return;
}
if (!showingUserDialogMessage) {
final Context context = getActivity();
if (setting.userDialogMessage != null
&& mPreference instanceof SwitchPreference switchPreference
&& setting.defaultValue instanceof Boolean defaultValue
&& switchPreference.isChecked() != defaultValue) {
showSettingUserDialogConfirmation(context, switchPreference, (BooleanSetting) setting);
} else if (setting.rebootApp) {
showRestartDialog(context);
}
}
} catch (Exception ex) {
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
}
};
private void showSettingUserDialogConfirmation(Context context, SwitchPreference switchPreference, BooleanSetting setting) {
Utils.verifyOnMainThread();
showingUserDialogMessage = true;
assert setting.userDialogMessage != null;
new AlertDialog.Builder(context)
.setTitle(str("revanced_extended_confirm_user_dialog_title"))
.setMessage(setting.userDialogMessage.toString())
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
if (setting.rebootApp) {
showRestartDialog(context);
}
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
switchPreference.setChecked(setting.defaultValue); // Recursive call that resets the Setting value.
})
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
.setCancelable(false)
.show();
}
static PreferenceManager mPreferenceManager;
private SharedPreferences mSharedPreferences;
private PreferenceScreen originalPreferenceScreen;
public ReVancedPreferenceFragment() {
// Required empty public constructor
}
private void putPreferenceScreenMap(SortedMap<String, PreferenceScreen> preferenceScreenMap, PreferenceGroup preferenceGroup) {
if (preferenceGroup instanceof PreferenceScreen mPreferenceScreen) {
preferenceScreenMap.put(mPreferenceScreen.getKey(), mPreferenceScreen);
}
}
private void setPreferenceScreenToolbar() {
SortedMap<String, PreferenceScreen> preferenceScreenMap = new TreeMap<>();
PreferenceScreen rootPreferenceScreen = getPreferenceScreen();
for (Preference preference : getAllPreferencesBy(rootPreferenceScreen)) {
if (!(preference instanceof PreferenceGroup preferenceGroup)) continue;
putPreferenceScreenMap(preferenceScreenMap, preferenceGroup);
for (Preference childPreference : getAllPreferencesBy(preferenceGroup)) {
if (!(childPreference instanceof PreferenceGroup nestedPreferenceGroup)) continue;
putPreferenceScreenMap(preferenceScreenMap, nestedPreferenceGroup);
for (Preference nestedPreference : getAllPreferencesBy(nestedPreferenceGroup)) {
if (!(nestedPreference instanceof PreferenceGroup childPreferenceGroup))
continue;
putPreferenceScreenMap(preferenceScreenMap, childPreferenceGroup);
}
}
}
for (PreferenceScreen mPreferenceScreen : preferenceScreenMap.values()) {
mPreferenceScreen.setOnPreferenceClickListener(
preferenceScreen -> {
Dialog preferenceScreenDialog = mPreferenceScreen.getDialog();
ViewGroup rootView = (ViewGroup) preferenceScreenDialog
.findViewById(android.R.id.content)
.getParent();
Toolbar toolbar = new Toolbar(preferenceScreen.getContext());
toolbar.setTitle(preferenceScreen.getTitle());
toolbar.setNavigationIcon(ThemeUtils.getBackButtonDrawable());
toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
int margin = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()
);
toolbar.setTitleMargin(margin, 0, margin, 0);
TextView toolbarTextView = getChildView(toolbar, TextView.class::isInstance);
if (toolbarTextView != null) {
toolbarTextView.setTextColor(ThemeUtils.getForegroundColor());
}
rootView.addView(toolbar, 0);
return false;
}
);
}
}
// Map to store dependencies: key is the preference key, value is a list of dependent preferences
private final Map<String, List<Preference>> dependencyMap = new HashMap<>();
// Set to track already added preferences to avoid duplicates
private final Set<String> addedPreferences = new HashSet<>();
// Map to store preferences grouped by their parent PreferenceGroup
private final Map<PreferenceGroup, List<Preference>> groupedPreferences = new LinkedHashMap<>();
@SuppressLint("ResourceType")
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
try {
mPreferenceManager = getPreferenceManager();
mPreferenceManager.setSharedPreferencesName(Setting.preferences.name);
mSharedPreferences = mPreferenceManager.getSharedPreferences();
addPreferencesFromResource(getXmlIdentifier("revanced_prefs"));
// Initialize toolbars and other UI elements
setPreferenceScreenToolbar();
// Initialize ReVanced settings
ReVancedSettingsPreference.initializeReVancedSettings();
SponsorBlockSettingsPreference.init(getActivity());
// Import/export
setBackupRestorePreference();
// Store all preferences and their dependencies
storeAllPreferences(getPreferenceScreen());
// Load and set initial preferences states
for (Setting<?> setting : Setting.allLoadedSettings()) {
final Preference preference = mPreferenceManager.findPreference(setting.key);
if (preference != null && isSDKAbove(26)) {
preference.setSingleLineTitle(false);
}
if (preference instanceof SwitchPreference switchPreference) {
BooleanSetting boolSetting = (BooleanSetting) setting;
switchPreference.setChecked(boolSetting.get());
} else if (preference instanceof EditTextPreference editTextPreference) {
editTextPreference.setText(setting.get().toString());
} else if (preference instanceof ListPreference listPreference) {
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) {
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries());
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues());
}
if (!(preference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
updateListPreferenceSummary(listPreference, setting);
}
}
}
// Register preference change listener
mSharedPreferences.registerOnSharedPreferenceChangeListener(listener);
originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getActivity());
copyPreferences(getPreferenceScreen(), originalPreferenceScreen);
} catch (Exception th) {
Logger.printException(() -> "Error during onCreate()", th);
}
}
private void copyPreferences(PreferenceScreen source, PreferenceScreen destination) {
for (Preference preference : getAllPreferencesBy(source)) {
destination.addPreference(preference);
}
}
@Override
public void onDestroy() {
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
super.onDestroy();
}
/**
* Recursively stores all preferences and their dependencies grouped by their parent PreferenceGroup.
*
* @param preferenceGroup The preference group to scan.
*/
private void storeAllPreferences(PreferenceGroup preferenceGroup) {
// Check if this is the root PreferenceScreen
boolean isRootScreen = preferenceGroup == getPreferenceScreen();
// Use the special top-level group only for the root PreferenceScreen
PreferenceGroup groupKey = isRootScreen
? new PreferenceCategory(preferenceGroup.getContext())
: preferenceGroup;
if (isRootScreen) {
groupKey.setTitle(ResourceUtils.getString("revanced_extended_settings_title"));
}
// Initialize a list to hold preferences of the current group
List<Preference> currentGroupPreferences = groupedPreferences.computeIfAbsent(groupKey, k -> new ArrayList<>());
for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) {
Preference preference = preferenceGroup.getPreference(i);
// Add preference to the current group if not already added
if (!currentGroupPreferences.contains(preference)) {
currentGroupPreferences.add(preference);
}
// Store dependencies
if (preference.getDependency() != null) {
String dependencyKey = preference.getDependency();
dependencyMap.computeIfAbsent(dependencyKey, k -> new ArrayList<>()).add(preference);
}
// Recursively handle nested PreferenceGroups
if (preference instanceof PreferenceGroup nestedGroup) {
storeAllPreferences(nestedGroup);
}
}
}
/**
* Filters preferences based on the search query, displaying grouped results with group titles.
*
* @param query The search query.
*/
public void filterPreferences(String query) {
// If the query is null or empty, reset preferences to their default state
if (query == null || query.isEmpty()) {
resetPreferences();
return;
}
// Convert the query to lowercase for case-insensitive search
query = query.toLowerCase();
// Get the preference screen to modify
PreferenceScreen preferenceScreen = getPreferenceScreen();
// Remove all current preferences from the screen
preferenceScreen.removeAll();
// Clear the list of added preferences to start fresh
addedPreferences.clear();
// Create a map to store matched preferences for each group
Map<PreferenceGroup, List<Preference>> matchedGroupPreferences = new LinkedHashMap<>();
// Create a set to store all keys that should be included
Set<String> keysToInclude = new HashSet<>();
// First pass: identify all preferences that match the query and their dependencies
for (Map.Entry<PreferenceGroup, List<Preference>> entry : groupedPreferences.entrySet()) {
List<Preference> preferences = entry.getValue();
for (Preference preference : preferences) {
if (preferenceMatches(preference, query)) {
addPreferenceAndDependencies(preference, keysToInclude);
}
}
}
// Second pass: add all identified preferences to matchedGroupPreferences
for (Map.Entry<PreferenceGroup, List<Preference>> entry : groupedPreferences.entrySet()) {
PreferenceGroup group = entry.getKey();
List<Preference> preferences = entry.getValue();
List<Preference> matchedPreferences = new ArrayList<>();
for (Preference preference : preferences) {
if (keysToInclude.contains(preference.getKey())) {
matchedPreferences.add(preference);
}
}
if (!matchedPreferences.isEmpty()) {
matchedGroupPreferences.put(group, matchedPreferences);
}
}
// Add matched preferences to the screen, maintaining the original order
for (Map.Entry<PreferenceGroup, List<Preference>> entry : matchedGroupPreferences.entrySet()) {
PreferenceGroup group = entry.getKey();
List<Preference> matchedPreferences = entry.getValue();
// Add the category for this group
PreferenceCategory category = new PreferenceCategory(preferenceScreen.getContext());
category.setTitle(group.getTitle());
preferenceScreen.addPreference(category);
// Add matched preferences for this group
for (Preference preference : matchedPreferences) {
if (preference.isSelectable()) {
addPreferenceWithDependencies(category, preference);
} else {
// For non-selectable preferences, just add them directly
category.addPreference(preference);
}
}
}
}
/**
* Checks if a preference matches the given query.
*
* @param preference The preference to check.
* @param query The search query.
* @return True if the preference matches the query, false otherwise.
*/
private boolean preferenceMatches(Preference preference, String query) {
// Check if the title contains the query string
if (preference.getTitle().toString().toLowerCase().contains(query)) {
return true;
}
// Check if the summary contains the query string
if (preference.getSummary() != null && preference.getSummary().toString().toLowerCase().contains(query)) {
return true;
}
// Additional checks for SwitchPreference
if (preference instanceof SwitchPreference switchPreference) {
CharSequence summaryOn = switchPreference.getSummaryOn();
CharSequence summaryOff = switchPreference.getSummaryOff();
if ((summaryOn != null && summaryOn.toString().toLowerCase().contains(query)) ||
(summaryOff != null && summaryOff.toString().toLowerCase().contains(query))) {
return true;
}
}
// Additional checks for ListPreference
if (preference instanceof ListPreference listPreference) {
CharSequence[] entries = listPreference.getEntries();
if (entries != null) {
for (CharSequence entry : entries) {
if (entry.toString().toLowerCase().contains(query)) {
return true;
}
}
}
CharSequence[] entryValues = listPreference.getEntryValues();
if (entryValues != null) {
for (CharSequence entryValue : entryValues) {
if (entryValue.toString().toLowerCase().contains(query)) {
return true;
}
}
}
}
return false;
}
/**
* Recursively adds a preference and its dependencies to the set of keys to include.
*
* @param preference The preference to add.
* @param keysToInclude The set of keys to include.
*/
private void addPreferenceAndDependencies(Preference preference, Set<String> keysToInclude) {
String key = preference.getKey();
if (key != null && !keysToInclude.contains(key)) {
keysToInclude.add(key);
// Add the preference this one depends on
String dependencyKey = preference.getDependency();
if (dependencyKey != null) {
Preference dependency = findPreferenceInAllGroups(dependencyKey);
if (dependency != null) {
addPreferenceAndDependencies(dependency, keysToInclude);
}
}
// Add preferences that depend on this one
if (dependencyMap.containsKey(key)) {
for (Preference dependentPreference : Objects.requireNonNull(dependencyMap.get(key))) {
addPreferenceAndDependencies(dependentPreference, keysToInclude);
}
}
}
}
/**
* Recursively adds a preference along with its dependencies
* (android:dependency attribute in XML).
*
* @param preferenceGroup The preference group to add to.
* @param preference The preference to add.
*/
private void addPreferenceWithDependencies(PreferenceGroup preferenceGroup, Preference preference) {
String key = preference.getKey();
// Instead of just using preference keys, we combine the category and key to ensure uniqueness
if (key != null && !addedPreferences.contains(preferenceGroup.getTitle() + ":" + key)) {
// Add dependencies first
if (preference.getDependency() != null) {
String dependencyKey = preference.getDependency();
Preference dependency = findPreferenceInAllGroups(dependencyKey);
if (dependency != null) {
addPreferenceWithDependencies(preferenceGroup, dependency);
} else {
return;
}
}
// Add the preference using a combination of the category and the key
preferenceGroup.addPreference(preference);
addedPreferences.add(preferenceGroup.getTitle() + ":" + key); // Track based on both category and key
// Handle dependent preferences
if (dependencyMap.containsKey(key)) {
for (Preference dependentPreference : Objects.requireNonNull(dependencyMap.get(key))) {
addPreferenceWithDependencies(preferenceGroup, dependentPreference);
}
}
}
}
/**
* Finds a preference in all groups based on its key.
*
* @param key The key of the preference to find.
* @return The found preference, or null if not found.
*/
private Preference findPreferenceInAllGroups(String key) {
for (List<Preference> preferences : groupedPreferences.values()) {
for (Preference preference : preferences) {
if (preference.getKey() != null && preference.getKey().equals(key)) {
return preference;
}
}
}
return null;
}
/**
* Resets the preference screen to its original state.
*/
private void resetPreferences() {
PreferenceScreen preferenceScreen = getPreferenceScreen();
preferenceScreen.removeAll();
for (Preference preference : getAllPreferencesBy(originalPreferenceScreen))
preferenceScreen.addPreference(preference);
}
private List<Preference> getAllPreferencesBy(PreferenceGroup preferenceGroup) {
List<Preference> preferences = new ArrayList<>();
for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++)
preferences.add(preferenceGroup.getPreference(i));
return preferences;
}
/**
* Add Preference to Import/Export settings submenu
*/
private void setBackupRestorePreference() {
findPreference("revanced_extended_settings_import").setOnPreferenceClickListener(pref -> {
importActivity();
return false;
});
findPreference("revanced_extended_settings_export").setOnPreferenceClickListener(pref -> {
exportActivity();
return false;
});
}
/**
* Invoke the SAF(Storage Access Framework) to export settings
*/
private void exportActivity() {
@SuppressLint("SimpleDateFormat") final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
final String appName = ExtendedUtils.getApplicationLabel();
final String versionName = ExtendedUtils.getVersionName();
final String formatDate = dateFormat.format(new Date(System.currentTimeMillis()));
final String fileName = String.format("%s_v%s_%s.txt", appName, versionName, formatDate);
final Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TITLE, fileName);
startActivityForResult(intent, WRITE_REQUEST_CODE);
}
/**
* Invoke the SAF(Storage Access Framework) to import settings
*/
private void importActivity() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(isSDKAbove(29) ? "text/plain" : "*/*");
startActivityForResult(intent, READ_REQUEST_CODE);
}
/**
* Activity should be done within the lifecycle of PreferenceFragment
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == WRITE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
exportText(data.getData());
} else if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) {
importText(data.getData());
}
}
private void exportText(Uri uri) {
final Context context = this.getActivity();
try {
@SuppressLint("Recycle")
FileWriter jsonFileWriter =
new FileWriter(
Objects.requireNonNull(context.getApplicationContext()
.getContentResolver()
.openFileDescriptor(uri, "w"))
.getFileDescriptor()
);
PrintWriter printWriter = new PrintWriter(jsonFileWriter);
printWriter.write(Setting.exportToJson(context));
printWriter.close();
jsonFileWriter.close();
showToastShort(str("revanced_extended_settings_export_success"));
} catch (IOException e) {
showToastShort(str("revanced_extended_settings_export_failed"));
}
}
private void importText(Uri uri) {
final Context context = this.getActivity();
StringBuilder sb = new StringBuilder();
String line;
try {
settingImportInProgress = true;
@SuppressLint("Recycle")
FileReader fileReader =
new FileReader(
Objects.requireNonNull(context.getApplicationContext()
.getContentResolver()
.openFileDescriptor(uri, "r"))
.getFileDescriptor()
);
BufferedReader bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
sb.append(line).append("\n");
}
bufferedReader.close();
fileReader.close();
final boolean restartNeeded = Setting.importFromJSON(sb.toString(), true);
if (restartNeeded) {
showRestartDialog(getActivity());
}
} catch (IOException e) {
showToastShort(str("revanced_extended_settings_import_failed"));
throw new RuntimeException(e);
} finally {
settingImportInProgress = false;
}
}
}

View File

@ -0,0 +1,277 @@
package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_1;
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_3;
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
import android.preference.Preference;
import android.preference.SwitchPreference;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch;
import app.revanced.extension.youtube.patches.general.MiniplayerPatch;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
import app.revanced.extension.youtube.patches.utils.ReturnYouTubeDislikePatch;
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.ExtendedUtils;
@SuppressWarnings("deprecation")
public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
private static void enableDisablePreferences() {
for (Setting<?> setting : Setting.allLoadedSettings()) {
final Preference preference = mPreferenceManager.findPreference(setting.key);
if (preference != null) {
preference.setEnabled(setting.isAvailable());
}
}
}
private static void enableDisablePreferences(final boolean isAvailable, final Setting<?>... unavailableEnum) {
if (!isAvailable) {
return;
}
for (Setting<?> setting : unavailableEnum) {
final Preference preference = mPreferenceManager.findPreference(setting.key);
if (preference != null) {
preference.setEnabled(false);
}
}
}
public static void initializeReVancedSettings() {
enableDisablePreferences();
AmbientModePreferenceLinks();
ChangeHeaderPreferenceLinks();
ExternalDownloaderPreferenceLinks();
FullScreenPanelPreferenceLinks();
LayoutOverrideLinks();
MiniPlayerPreferenceLinks();
NavigationPreferenceLinks();
RYDPreferenceLinks();
SeekBarPreferenceLinks();
SpeedOverlayPreferenceLinks();
QuickActionsPreferenceLinks();
TabletLayoutLinks();
WhitelistPreferenceLinks();
}
/**
* Enable/Disable Preference related to Ambient Mode
*/
private static void AmbientModePreferenceLinks() {
enableDisablePreferences(
Settings.DISABLE_AMBIENT_MODE.get(),
Settings.BYPASS_AMBIENT_MODE_RESTRICTIONS,
Settings.DISABLE_AMBIENT_MODE_IN_FULLSCREEN
);
}
/**
* Enable/Disable Preference related to Change header
*/
private static void ChangeHeaderPreferenceLinks() {
enableDisablePreferences(
PatchStatus.MinimalHeader(),
Settings.CHANGE_YOUTUBE_HEADER
);
}
/**
* Enable/Disable Preference for External downloader settings
*/
private static void ExternalDownloaderPreferenceLinks() {
// Override download button will not work if spoofed with YouTube 18.24.xx or earlier.
enableDisablePreferences(
isSpoofingToLessThan("18.24.00"),
Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON,
Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON
);
}
/**
* Enable/Disable Layout Override Preference
*/
private static void LayoutOverrideLinks() {
enableDisablePreferences(
ExtendedUtils.isTablet(),
Settings.FORCE_FULLSCREEN
);
}
/**
* Enable/Disable Preferences not working in tablet layout
*/
private static void TabletLayoutLinks() {
final boolean isTablet = ExtendedUtils.isTablet() &&
!LayoutSwitchPatch.phoneLayoutEnabled();
enableDisablePreferences(
isTablet,
Settings.DISABLE_ENGAGEMENT_PANEL,
Settings.HIDE_COMMUNITY_POSTS_HOME_RELATED_VIDEOS,
Settings.HIDE_COMMUNITY_POSTS_SUBSCRIPTIONS,
Settings.HIDE_MIX_PLAYLISTS,
Settings.HIDE_RELATED_VIDEO_OVERLAY,
Settings.SHOW_VIDEO_TITLE_SECTION
);
}
/**
* Enable/Disable Preference related to Fullscreen Panel
*/
private static void FullScreenPanelPreferenceLinks() {
enableDisablePreferences(
Settings.DISABLE_ENGAGEMENT_PANEL.get(),
Settings.HIDE_RELATED_VIDEO_OVERLAY,
Settings.HIDE_QUICK_ACTIONS,
Settings.HIDE_QUICK_ACTIONS_COMMENT_BUTTON,
Settings.HIDE_QUICK_ACTIONS_DISLIKE_BUTTON,
Settings.HIDE_QUICK_ACTIONS_LIKE_BUTTON,
Settings.HIDE_QUICK_ACTIONS_LIVE_CHAT_BUTTON,
Settings.HIDE_QUICK_ACTIONS_MORE_BUTTON,
Settings.HIDE_QUICK_ACTIONS_OPEN_MIX_PLAYLIST_BUTTON,
Settings.HIDE_QUICK_ACTIONS_OPEN_PLAYLIST_BUTTON,
Settings.HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON,
Settings.HIDE_QUICK_ACTIONS_SHARE_BUTTON
);
enableDisablePreferences(
Settings.DISABLE_LANDSCAPE_MODE.get(),
Settings.FORCE_FULLSCREEN
);
enableDisablePreferences(
Settings.FORCE_FULLSCREEN.get(),
Settings.DISABLE_LANDSCAPE_MODE
);
}
/**
* Enable/Disable Preference related to Hide Quick Actions
*/
private static void QuickActionsPreferenceLinks() {
final boolean isEnabled =
Settings.DISABLE_ENGAGEMENT_PANEL.get() || Settings.HIDE_QUICK_ACTIONS.get();
enableDisablePreferences(
isEnabled,
Settings.HIDE_QUICK_ACTIONS_COMMENT_BUTTON,
Settings.HIDE_QUICK_ACTIONS_DISLIKE_BUTTON,
Settings.HIDE_QUICK_ACTIONS_LIKE_BUTTON,
Settings.HIDE_QUICK_ACTIONS_LIVE_CHAT_BUTTON,
Settings.HIDE_QUICK_ACTIONS_MORE_BUTTON,
Settings.HIDE_QUICK_ACTIONS_OPEN_MIX_PLAYLIST_BUTTON,
Settings.HIDE_QUICK_ACTIONS_OPEN_PLAYLIST_BUTTON,
Settings.HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON,
Settings.HIDE_QUICK_ACTIONS_SHARE_BUTTON
);
}
/**
* Enable/Disable Preference related to Miniplayer settings
*/
private static void MiniPlayerPreferenceLinks() {
final MiniplayerPatch.MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get();
final boolean available =
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) &&
!Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() &&
!Settings.MINIPLAYER_DRAG_AND_DROP.get();
enableDisablePreferences(
!available,
Settings.MINIPLAYER_HIDE_EXPAND_CLOSE
);
}
/**
* Enable/Disable Preference related to Navigation settings
*/
private static void NavigationPreferenceLinks() {
enableDisablePreferences(
Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get(),
Settings.HIDE_NAVIGATION_CREATE_BUTTON
);
enableDisablePreferences(
!Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get(),
Settings.HIDE_NAVIGATION_NOTIFICATIONS_BUTTON,
Settings.REPLACE_TOOLBAR_CREATE_BUTTON,
Settings.REPLACE_TOOLBAR_CREATE_BUTTON_TYPE
);
enableDisablePreferences(
!isSDKAbove(31),
Settings.ENABLE_TRANSLUCENT_NAVIGATION_BAR
);
}
/**
* Enable/Disable Preference related to RYD settings
*/
private static void RYDPreferenceLinks() {
if (!(mPreferenceManager.findPreference(Settings.RYD_ENABLED.key) instanceof SwitchPreference enabledPreference)) {
return;
}
if (!(mPreferenceManager.findPreference(Settings.RYD_SHORTS.key) instanceof SwitchPreference shortsPreference)) {
return;
}
if (!(mPreferenceManager.findPreference(Settings.RYD_DISLIKE_PERCENTAGE.key) instanceof SwitchPreference percentagePreference)) {
return;
}
if (!(mPreferenceManager.findPreference(Settings.RYD_COMPACT_LAYOUT.key) instanceof SwitchPreference compactLayoutPreference)) {
return;
}
final Preference.OnPreferenceChangeListener clearAllUICaches = (pref, newValue) -> {
ReturnYouTubeDislike.clearAllUICaches();
return true;
};
enabledPreference.setOnPreferenceChangeListener((pref, newValue) -> {
ReturnYouTubeDislikePatch.onRYDStatusChange();
return true;
});
String shortsSummary = ReturnYouTubeDislikePatch.IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER
? str("revanced_ryd_shorts_summary_on")
: str("revanced_ryd_shorts_summary_on_disclaimer");
shortsPreference.setSummaryOn(shortsSummary);
percentagePreference.setOnPreferenceChangeListener(clearAllUICaches);
compactLayoutPreference.setOnPreferenceChangeListener(clearAllUICaches);
}
/**
* Enable/Disable Preference related to Seek bar settings
*/
private static void SeekBarPreferenceLinks() {
enableDisablePreferences(
Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get(),
Settings.ENABLE_SEEKBAR_THUMBNAILS_HIGH_QUALITY
);
}
/**
* Enable/Disable Preference related to Speed overlay settings
*/
private static void SpeedOverlayPreferenceLinks() {
enableDisablePreferences(
Settings.DISABLE_SPEED_OVERLAY.get(),
Settings.SPEED_OVERLAY_VALUE
);
}
private static void WhitelistPreferenceLinks() {
final boolean enabled = PatchStatus.RememberPlaybackSpeed() || PatchStatus.SponsorBlock();
final String[] whitelistKey = {Settings.OVERLAY_BUTTON_WHITELIST.key, "revanced_whitelist_settings"};
for (String key : whitelistKey) {
final Preference preference = mPreferenceManager.findPreference(key);
if (preference != null) {
preference.setEnabled(enabled);
}
}
}
}

View File

@ -0,0 +1,180 @@
package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.preference.ListPreference;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import java.util.Objects;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
@SuppressWarnings({"unused", "deprecation"})
public class SegmentCategoryListPreference extends ListPreference {
private SegmentCategory mCategory;
private EditText mEditText;
private int mClickedDialogEntryIndex;
private void init() {
final SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(getKey());
final boolean isHighlightCategory = segmentCategory == SegmentCategory.HIGHLIGHT;
mCategory = Objects.requireNonNull(segmentCategory);
// Edit: Using preferences to sync together multiple pieces
// of code together is messy and should be rethought.
setKey(segmentCategory.behaviorSetting.key);
setDefaultValue(segmentCategory.behaviorSetting.defaultValue);
setEntries(isHighlightCategory
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
: CategoryBehaviour.getBehaviorDescriptions());
setEntryValues(isHighlightCategory
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
: CategoryBehaviour.getBehaviorKeyValues());
updateTitle();
}
public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public SegmentCategoryListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SegmentCategoryListPreference(Context context) {
super(context);
init();
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try {
Utils.setEditTextDialogTheme(builder);
super.onPrepareDialogBuilder(builder);
Context context = builder.getContext();
TableLayout table = new TableLayout(context);
table.setOrientation(LinearLayout.HORIZONTAL);
table.setPadding(70, 0, 150, 0);
TableRow row = new TableRow(context);
TextView colorTextLabel = new TextView(context);
colorTextLabel.setText(str("revanced_sb_color_dot_label"));
row.addView(colorTextLabel);
TextView colorDotView = new TextView(context);
colorDotView.setText(mCategory.getCategoryColorDot());
colorDotView.setPadding(30, 0, 30, 0);
row.addView(colorDotView);
mEditText = new EditText(context);
mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
mEditText.setText(mCategory.colorString());
mEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
try {
String colorString = s.toString();
if (!colorString.startsWith("#")) {
s.insert(0, "#"); // recursively calls back into this method
return;
}
if (colorString.length() > 7) {
s.delete(7, colorString.length());
return;
}
final int color = Color.parseColor(colorString);
colorDotView.setText(SegmentCategory.getCategoryColorDot(color));
} catch (IllegalArgumentException ex) {
// ignore
}
}
});
mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f));
row.addView(mEditText);
table.addView(row);
builder.setView(table);
builder.setTitle(mCategory.title.toString());
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> onClick(dialog, DialogInterface.BUTTON_POSITIVE));
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
try {
mCategory.resetColor();
updateTitle();
Utils.showToastShort(str("revanced_sb_color_reset"));
} catch (Exception ex) {
Logger.printException(() -> "setNeutralButton failure", ex);
}
});
builder.setNegativeButton(android.R.string.cancel, null);
mClickedDialogEntryIndex = findIndexOfValue(getValue());
builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which);
} catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
}
}
@Override
protected void onDialogClosed(boolean positiveResult) {
try {
if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) {
String value = getEntryValues()[mClickedDialogEntryIndex].toString();
if (callChangeListener(value)) {
setValue(value);
mCategory.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
SegmentCategory.updateEnabledCategories();
}
String colorString = mEditText.getText().toString();
try {
if (!colorString.equals(mCategory.colorString())) {
mCategory.setColor(colorString);
Utils.showToastShort(str("revanced_sb_color_changed"));
}
} catch (IllegalArgumentException ex) {
Utils.showToastShort(str("revanced_sb_color_invalid"));
}
updateTitle();
}
} catch (Exception ex) {
Logger.printException(() -> "onDialogClosed failure", ex);
}
}
private void updateTitle() {
setTitle(mCategory.getTitleWithColorDot());
setEnabled(Settings.SB_ENABLED.get());
}
}

View File

@ -0,0 +1,108 @@
package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.EditText;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
@SuppressWarnings({"unused", "deprecation"})
public class SponsorBlockImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
private String existingSettings;
@TargetApi(26)
private void init() {
setSelectable(true);
EditText editText = getEditText();
editText.setTextIsSelectable(true);
editText.setAutofillHints((String) null);
editText.setInputType(editText.getInputType()
| InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_MULTI_LINE
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); // Use a smaller font to reduce text wrap.
// If the user has a private user id, then include a subtext that mentions not to share it.
String importExportSummary = SponsorBlockSettings.userHasSBPrivateId()
? str("revanced_sb_settings_ie_sum_warning")
: str("revanced_sb_settings_ie_sum");
setSummary(importExportSummary);
setOnPreferenceClickListener(this);
}
public SponsorBlockImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public SponsorBlockImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public SponsorBlockImportExportPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SponsorBlockImportExportPreference(Context context) {
super(context);
init();
}
@Override
public boolean onPreferenceClick(Preference preference) {
try {
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened.
existingSettings = SponsorBlockSettings.exportDesktopSettings();
getEditText().setText(existingSettings);
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
return true;
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try {
Utils.setEditTextDialogTheme(builder);
super.onPrepareDialogBuilder(builder);
// Show the user the settings in JSON format.
builder.setTitle(getTitle());
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) ->
Utils.setClipboard(getEditText().getText().toString(), str("revanced_sb_share_copy_settings_success")))
.setPositiveButton(android.R.string.ok, (dialog, which) ->
importSettings(getEditText().getText().toString()));
} catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
}
}
private void importSettings(String replacementSettings) {
try {
if (replacementSettings.equals(existingSettings)) {
return;
}
SponsorBlockSettings.importDesktopSettings(replacementSettings);
SponsorBlockSettingsPreference.updateSegmentCategories();
SponsorBlockSettingsPreference.fetchAndDisplayStats();
SponsorBlockSettingsPreference.updateUI();
} catch (Exception ex) {
Logger.printException(() -> "importSettings failure", ex);
}
}
}

View File

@ -0,0 +1,432 @@
package app.revanced.extension.youtube.settings.preference;
import static android.text.Html.fromHtml;
import static app.revanced.extension.shared.utils.ResourceUtils.getLayoutIdentifier;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import android.text.InputType;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
import app.revanced.extension.youtube.sponsorblock.objects.UserStats;
import app.revanced.extension.youtube.sponsorblock.requests.SBRequester;
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController;
@SuppressWarnings({"unused", "deprecation"})
public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment {
private static PreferenceCategory statsCategory;
private static final int preferencesCategoryLayout = getLayoutIdentifier("revanced_settings_preferences_category");
private static final Preference.OnPreferenceChangeListener updateUI = (pref, newValue) -> {
updateUI();
return true;
};
@NonNull
private static SwitchPreference findSwitchPreference(BooleanSetting setting) {
final String key = setting.key;
if (mPreferenceManager.findPreference(key) instanceof SwitchPreference switchPreference) {
switchPreference.setOnPreferenceChangeListener(updateUI);
return switchPreference;
} else {
throw new IllegalStateException("SwitchPreference is null: " + key);
}
}
@NonNull
private static ResettableEditTextPreference findResettableEditTextPreference(Setting<?> setting) {
final String key = setting.key;
if (mPreferenceManager.findPreference(key) instanceof ResettableEditTextPreference switchPreference) {
switchPreference.setOnPreferenceChangeListener(updateUI);
return switchPreference;
} else {
throw new IllegalStateException("ResettableEditTextPreference is null: " + key);
}
}
public static void updateUI() {
if (!Settings.SB_ENABLED.get()) {
SponsorBlockViewController.hideAll();
SegmentPlaybackController.clearData();
} else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) {
SponsorBlockViewController.hideNewSegmentLayout();
}
}
@TargetApi(26)
public static void init(Activity mActivity) {
if (!PatchStatus.SponsorBlock()) {
return;
}
final SwitchPreference sbEnabled = findSwitchPreference(Settings.SB_ENABLED);
sbEnabled.setOnPreferenceClickListener(preference -> {
updateUI();
fetchAndDisplayStats();
updateSegmentCategories();
return false;
});
if (!(sbEnabled.getParent() instanceof PreferenceScreen mPreferenceScreen)) {
return;
}
final SwitchPreference votingEnabled = findSwitchPreference(Settings.SB_VOTING_BUTTON);
final SwitchPreference compactSkipButton = findSwitchPreference(Settings.SB_COMPACT_SKIP_BUTTON);
final SwitchPreference autoHideSkipSegmentButton = findSwitchPreference(Settings.SB_AUTO_HIDE_SKIP_BUTTON);
final SwitchPreference showSkipToast = findSwitchPreference(Settings.SB_TOAST_ON_SKIP);
showSkipToast.setOnPreferenceClickListener(preference -> {
Utils.showToastShort(str("revanced_sb_skipped_sponsor"));
return false;
});
final SwitchPreference showTimeWithoutSegments = findSwitchPreference(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS);
final SwitchPreference addNewSegment = findSwitchPreference(Settings.SB_CREATE_NEW_SEGMENT);
addNewSegment.setOnPreferenceChangeListener((preference, newValue) -> {
if ((Boolean) newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
Context context = preference.getContext();
new AlertDialog.Builder(context)
.setTitle(str("revanced_sb_guidelines_popup_title"))
.setMessage(str("revanced_sb_guidelines_popup_content"))
.setNegativeButton(str("revanced_sb_guidelines_popup_already_read"), null)
.setPositiveButton(str("revanced_sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines(context))
.setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true))
.setCancelable(false)
.show();
}
updateUI();
return true;
});
final ResettableEditTextPreference newSegmentStep = findResettableEditTextPreference(Settings.SB_CREATE_NEW_SEGMENT_STEP);
newSegmentStep.setOnPreferenceChangeListener((preference, newValue) -> {
try {
final int newAdjustmentValue = Integer.parseInt(newValue.toString());
if (newAdjustmentValue != 0) {
Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue);
return true;
}
} catch (NumberFormatException ex) {
Logger.printInfo(() -> "Invalid new segment step", ex);
}
Utils.showToastLong(str("revanced_sb_general_adjusting_invalid"));
updateUI();
return false;
});
final Preference guidelinePreferences = Objects.requireNonNull(mPreferenceManager.findPreference("revanced_sb_guidelines_preference"));
guidelinePreferences.setDependency(Settings.SB_ENABLED.key);
guidelinePreferences.setOnPreferenceClickListener(preference -> {
openGuidelines(preference.getContext());
return true;
});
final SwitchPreference toastOnConnectionError = findSwitchPreference(Settings.SB_TOAST_ON_CONNECTION_ERROR);
final SwitchPreference trackSkips = findSwitchPreference(Settings.SB_TRACK_SKIP_COUNT);
final ResettableEditTextPreference minSegmentDuration = findResettableEditTextPreference(Settings.SB_SEGMENT_MIN_DURATION);
minSegmentDuration.setOnPreferenceChangeListener((preference, newValue) -> {
try {
Float minTimeDuration = Float.valueOf(newValue.toString());
Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration);
return true;
} catch (NumberFormatException ex) {
Logger.printInfo(() -> "Invalid minimum segment duration", ex);
}
Utils.showToastLong(str("revanced_sb_general_min_duration_invalid"));
updateUI();
return false;
});
final ResettableEditTextPreference privateUserId = findResettableEditTextPreference(Settings.SB_PRIVATE_USER_ID);
privateUserId.setOnPreferenceChangeListener((preference, newValue) -> {
String newUUID = newValue.toString();
if (!SponsorBlockSettings.isValidSBUserId(newUUID)) {
Utils.showToastLong(str("revanced_sb_general_uuid_invalid"));
return false;
}
Settings.SB_PRIVATE_USER_ID.save(newUUID);
try {
updateUI();
} catch (Exception e) {
throw new RuntimeException(e);
}
fetchAndDisplayStats();
return true;
});
final Preference apiUrl = mPreferenceManager.findPreference(Settings.SB_API_URL.key);
if (apiUrl != null) {
apiUrl.setOnPreferenceClickListener(preference -> {
Context context = preference.getContext();
TableLayout table = new TableLayout(context);
table.setOrientation(LinearLayout.HORIZONTAL);
table.setPadding(15, 0, 15, 0);
TableRow row = new TableRow(context);
EditText editText = new EditText(context);
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
editText.setText(Settings.SB_API_URL.get());
editText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f));
row.addView(editText);
table.addView(row);
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> {
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) {
Settings.SB_API_URL.resetToDefault();
Utils.showToastLong(str("revanced_sb_api_url_reset"));
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) {
String serverAddress = editText.getText().toString();
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
Utils.showToastLong(str("revanced_sb_api_url_invalid"));
} else if (!serverAddress.equals(Settings.SB_API_URL.get())) {
Settings.SB_API_URL.save(serverAddress);
Utils.showToastLong(str("revanced_sb_api_url_changed"));
}
}
};
Utils.getEditTextDialogBuilder(context)
.setView(table)
.setTitle(apiUrl.getTitle())
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(str("revanced_sb_reset"), urlChangeListener)
.setPositiveButton(android.R.string.ok, urlChangeListener)
.show();
return true;
});
}
statsCategory = new PreferenceCategory(mActivity);
statsCategory.setLayoutResource(preferencesCategoryLayout);
statsCategory.setTitle(str("revanced_sb_stats"));
mPreferenceScreen.addPreference(statsCategory);
fetchAndDisplayStats();
final PreferenceCategory aboutCategory = new PreferenceCategory(mActivity);
aboutCategory.setLayoutResource(preferencesCategoryLayout);
aboutCategory.setTitle(str("revanced_sb_about"));
mPreferenceScreen.addPreference(aboutCategory);
Preference aboutPreference = new Preference(mActivity);
aboutCategory.addPreference(aboutPreference);
aboutPreference.setTitle(str("revanced_sb_about_api"));
aboutPreference.setSummary(str("revanced_sb_about_api_sum"));
aboutPreference.setOnPreferenceClickListener(preference -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://sponsor.ajay.app"));
preference.getContext().startActivity(i);
return false;
});
updateUI();
}
public static void updateSegmentCategories() {
try {
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
final String key = category.keyValue;
if (mPreferenceManager.findPreference(key) instanceof SegmentCategoryListPreference segmentCategoryListPreference) {
segmentCategoryListPreference.setTitle(category.getTitleWithColorDot());
segmentCategoryListPreference.setEnabled(Settings.SB_ENABLED.get());
}
}
} catch (Exception ex) {
Logger.printException(() -> "updateSegmentCategories failure", ex);
}
}
private static void openGuidelines(Context context) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
context.startActivity(intent);
}
public static void fetchAndDisplayStats() {
try {
if (statsCategory == null) {
return;
}
statsCategory.removeAll();
if (!SponsorBlockSettings.userHasSBPrivateId()) {
// User has never voted or created any segments. No stats to show.
addLocalUserStats();
return;
}
Context context = statsCategory.getContext();
Preference loadingPlaceholderPreference = new Preference(context);
loadingPlaceholderPreference.setEnabled(false);
statsCategory.addPreference(loadingPlaceholderPreference);
if (Settings.SB_ENABLED.get()) {
loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_loading"));
Utils.runOnBackgroundThread(() -> {
UserStats stats = SBRequester.retrieveUserStats();
Utils.runOnMainThread(() -> { // get back on main thread to modify UI elements
addUserStats(loadingPlaceholderPreference, stats);
addLocalUserStats();
});
});
} else {
loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_sb_disabled"));
}
} catch (Exception ex) {
Logger.printException(() -> "fetchAndDisplayStats failure", ex);
}
}
private static void addUserStats(@NonNull Preference loadingPlaceholder, @Nullable UserStats stats) {
Utils.verifyOnMainThread();
try {
if (stats == null) {
loadingPlaceholder.setTitle(str("revanced_sb_stats_connection_failure"));
return;
}
statsCategory.removeAll();
Context context = statsCategory.getContext();
if (stats.totalSegmentCountIncludingIgnored > 0) {
// If user has not created any segments, there's no reason to set a username.
ResettableEditTextPreference preference = new ResettableEditTextPreference(context);
statsCategory.addPreference(preference);
String userName = stats.userName;
preference.setTitle(fromHtml(str("revanced_sb_stats_username", userName)));
preference.setSummary(str("revanced_sb_stats_username_change"));
preference.setText(userName);
preference.setOnPreferenceChangeListener((preference1, value) -> {
Utils.runOnBackgroundThread(() -> {
String newUserName = (String) value;
String errorMessage = SBRequester.setUsername(newUserName);
Utils.runOnMainThread(() -> {
if (errorMessage == null) {
preference.setTitle(fromHtml(str("revanced_sb_stats_username", newUserName)));
preference.setText(newUserName);
Utils.showToastLong(str("revanced_sb_stats_username_changed"));
} else {
preference.setText(userName); // revert to previous
Utils.showToastLong(errorMessage);
}
});
});
return true;
});
}
{
// number of segment submissions (does not include ignored segments)
Preference preference = new Preference(context);
statsCategory.addPreference(preference);
String formatted = SponsorBlockUtils.getNumberOfSkipsString(stats.segmentCount);
preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted)));
preference.setSummary(str("revanced_sb_stats_submissions_sum"));
if (stats.totalSegmentCountIncludingIgnored == 0) {
preference.setSelectable(false);
} else {
preference.setOnPreferenceClickListener(preference1 -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserId));
preference1.getContext().startActivity(i);
return true;
});
}
}
{
// "user reputation". Usually not useful, since it appears most users have zero reputation.
// But if there is a reputation, then show it here
Preference preference = new Preference(context);
preference.setTitle(fromHtml(str("revanced_sb_stats_reputation", stats.reputation)));
preference.setSelectable(false);
if (stats.reputation != 0) {
statsCategory.addPreference(preference);
}
}
{
// time saved for other users
Preference preference = new Preference(context);
statsCategory.addPreference(preference);
String stats_saved;
String stats_saved_sum;
if (stats.totalSegmentCountIncludingIgnored == 0) {
stats_saved = str("revanced_sb_stats_saved_zero");
stats_saved_sum = str("revanced_sb_stats_saved_sum_zero");
} else {
stats_saved = str("revanced_sb_stats_saved",
SponsorBlockUtils.getNumberOfSkipsString(stats.viewCount));
stats_saved_sum = str("revanced_sb_stats_saved_sum", SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved)));
}
preference.setTitle(fromHtml(stats_saved));
preference.setSummary(fromHtml(stats_saved_sum));
preference.setOnPreferenceClickListener(preference1 -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
preference1.getContext().startActivity(i);
return false;
});
}
} catch (Exception ex) {
Logger.printException(() -> "addUserStats failure", ex);
}
}
private static void addLocalUserStats() {
// time the user saved by using SB
Preference preference = new Preference(statsCategory.getContext());
statsCategory.addPreference(preference);
Runnable updateStatsSelfSaved = () -> {
String formatted = SponsorBlockUtils.getNumberOfSkipsString(Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get());
preference.setTitle(fromHtml(str("revanced_sb_stats_self_saved", formatted)));
String formattedSaved = SponsorBlockUtils.getTimeSavedString(Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() / 1000);
preference.setSummary(fromHtml(str("revanced_sb_stats_self_saved_sum", formattedSaved)));
};
updateStatsSelfSaved.run();
preference.setOnPreferenceClickListener(preference1 -> {
new AlertDialog.Builder(preference1.getContext())
.setTitle(str("revanced_sb_stats_self_saved_reset_title"))
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault();
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault();
updateStatsSelfSaved.run();
})
.setNegativeButton(android.R.string.no, null).show();
return true;
});
}
}

View File

@ -0,0 +1,78 @@
package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.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.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"deprecation", "unused"})
public class SpoofStreamingDataSideEffectsPreference extends Preference {
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
// Because this listener may run before the ReVanced settings fragment updates Settings,
// 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 Settings is up to date.
Utils.runOnMainThread(this::updateUI);
};
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SpoofStreamingDataSideEffectsPreference(Context context) {
super(context);
}
private void addChangeListener() {
Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener);
}
private void removeChangeListener() {
Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener);
}
@Override
protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
super.onAttachedToHierarchy(preferenceManager);
updateUI();
addChangeListener();
}
@Override
protected void onPrepareForRemoval() {
super.onPrepareForRemoval();
removeChangeListener();
}
private void updateUI() {
final ClientType clientType = Settings.SPOOF_STREAMING_DATA_TYPE.get();
final String summaryTextKey;
if (clientType == ClientType.IOS && Settings.SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK.get()) {
summaryTextKey = "revanced_spoof_streaming_data_side_effects_ios_skip_livestream_playback";
} else {
summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientType.name().toLowerCase();
}
setSummary(str(summaryTextKey));
setEnabled(Settings.SPOOF_STREAMING_DATA.get());
}
}

Some files were not shown because too many files have changed in this diff Show More