SHARE_PARAMETERS_TO_REMOVE = List.of(
+ "si", // Share tracking parameter.
+ "utm_source" // Share source, such as "copy-link".
+ );
+
+ /**
+ * Injection point.
+ */
+ public static String sanitizeUrl(String url) {
+ try {
+ Uri uri = Uri.parse(url);
+ Uri.Builder builder = uri.buildUpon().clearQuery();
+
+ for (String paramName : uri.getQueryParameterNames()) {
+ if (!SHARE_PARAMETERS_TO_REMOVE.contains(paramName)) {
+ for (String value : uri.getQueryParameters(paramName)) {
+ builder.appendQueryParameter(paramName, value);
+ }
+ }
+ }
+
+ return builder.build().toString();
+ } catch (Exception ex) {
+ Logger.printException(() -> "sanitizeUrl failure", ex);
+
+ return url;
+ }
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java
index f541b2347..0177c9cbc 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java
@@ -1,10 +1,18 @@
package app.revanced.extension.youtube;
+import static app.revanced.extension.shared.Utils.clamp;
+
import android.app.Activity;
+import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
import android.os.Build;
+import android.text.style.ReplacementSpan;
+import android.text.TextPaint;
import android.view.Window;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
@@ -121,4 +129,43 @@ public class ThemeHelper {
window.setNavigationBarContrastEnforced(true);
}
}
+
+ /**
+ * Adjusts the brightness of a color by lightening or darkening it based on the given factor.
+ *
+ * If the factor is greater than 1, the color is lightened by interpolating toward white (#FFFFFF).
+ * If the factor is less than or equal to 1, the color is darkened by scaling its RGB components toward black (#000000).
+ * The alpha channel remains unchanged.
+ *
+ * @param color The input color to adjust, in ARGB format.
+ * @param factor The adjustment factor. Use values > 1.0f to lighten (e.g., 1.11f for slight lightening)
+ * or values <= 1.0f to darken (e.g., 0.95f for slight darkening).
+ * @return The adjusted color in ARGB format.
+ */
+ public static int adjustColorBrightness(int color, float factor) {
+ final int alpha = Color.alpha(color);
+ int red = Color.red(color);
+ int green = Color.green(color);
+ int blue = Color.blue(color);
+
+ if (factor > 1.0f) {
+ // Lighten: Interpolate toward white (255)
+ final float t = 1.0f - (1.0f / factor); // Interpolation parameter
+ red = Math.round(red + (255 - red) * t);
+ green = Math.round(green + (255 - green) * t);
+ blue = Math.round(blue + (255 - blue) * t);
+ } else {
+ // Darken or no change: Scale toward black
+ red = (int) (red * factor);
+ green = (int) (green * factor);
+ blue = (int) (blue * factor);
+ }
+
+ // Ensure values are within [0, 255]
+ red = clamp(red, 0, 255);
+ green = clamp(green, 0, 255);
+ blue = clamp(blue, 0, 255);
+
+ return Color.argb(alpha, red, green, blue);
+ }
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideRelatedVideoOverlayPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideRelatedVideoOverlayPatch.java
new file mode 100644
index 000000000..423a7951c
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideRelatedVideoOverlayPatch.java
@@ -0,0 +1,13 @@
+package app.revanced.extension.youtube.patches;
+
+import app.revanced.extension.youtube.settings.Settings;
+
+@SuppressWarnings("unused")
+public final class HideRelatedVideoOverlayPatch {
+ /**
+ * Injection point.
+ */
+ public static boolean hideRelatedVideoOverlay() {
+ return Settings.HIDE_RELATED_VIDEO_OVERLAY.get();
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java
index c5ba1c033..2db744ae6 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java
@@ -18,7 +18,6 @@ import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch;
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
-import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
@@ -69,13 +68,6 @@ public class ReturnYouTubeDislikePatch {
@Nullable
private static volatile String lastPrefetchedVideoId;
- public static void onRYDStatusChange(boolean rydEnabled) {
- 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;
@@ -274,7 +266,7 @@ public class ReturnYouTubeDislikePatch {
Logger.printDebug(() -> "Adding rolling number TextView changes");
view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels);
ShapeDrawable separator = ReturnYouTubeDislike.getLeftSeparatorDrawable();
- if (Utils.isRightToLeftTextLayout()) {
+ if (Utils.isRightToLeftLocale()) {
view.setCompoundDrawables(null, null, separator, null);
} else {
view.setCompoundDrawables(separator, null, null, null);
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java
index aeff4ac26..90f18a6c2 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java
@@ -36,7 +36,7 @@ public final class WideSearchbarPatch {
final int paddingStart = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
8, Resources.getSystem().getDisplayMetrics());
- if (Utils.isRightToLeftTextLayout()) {
+ if (Utils.isRightToLeftLocale()) {
searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom);
} else {
searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom);
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/AdsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/AdsFilter.java
index 5f47290e9..78015ea79 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/AdsFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/AdsFilter.java
@@ -177,10 +177,7 @@ public final class AdsFilter extends Filter {
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == playerShoppingShelf) {
- if (contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered()) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
- return false;
+ return contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered();
}
// Check for the index because of likelihood of false positives.
@@ -198,13 +195,10 @@ public final class AdsFilter extends Filter {
}
if (matchedGroup == channelProfile) {
- if (visitStoreButton.check(protobufBufferArray).isFiltered()) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
- return false;
+ return visitStoreButton.check(protobufBufferArray).isFiltered();
}
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ return true;
}
/**
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/ButtonsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/ButtonsFilter.java
index 308be8ce8..3ad2070ed 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/ButtonsFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/ButtonsFilter.java
@@ -99,29 +99,23 @@ final class ButtonsFilter extends Filter {
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == likeSubscribeGlow) {
- if ((path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
- && path.contains(ANIMATED_VECTOR_TYPE_PATH)) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
-
- return false;
+ return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
+ && path.contains(ANIMATED_VECTOR_TYPE_PATH);
}
// If the current matched group is the action bar group,
// in case every filter group is enabled, hide the action bar.
if (matchedGroup == actionBarGroup) {
- if (!isEveryFilterGroupEnabled()) {
- return false;
- }
- } else if (matchedGroup == bufferFilterPathGroup) {
- // Make sure the current path is the right one
- // to avoid false positives.
- if (!path.startsWith(VIDEO_ACTION_BAR_PATH)) return false;
-
- // In case the group list has no match, return false.
- if (!bufferButtonsGroupList.check(protobufBufferArray).isFiltered()) return false;
+ return isEveryFilterGroupEnabled();
}
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ if (matchedGroup == bufferFilterPathGroup) {
+ // Make sure the current path is the right one
+ // to avoid false positives.
+ return path.startsWith(VIDEO_ACTION_BAR_PATH)
+ && bufferButtonsGroupList.check(protobufBufferArray).isFiltered();
+ }
+
+ return true;
}
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/CommentsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/CommentsFilter.java
index ec58b2ee2..d30504c2c 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/CommentsFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/CommentsFilter.java
@@ -88,22 +88,15 @@ final class CommentsFilter extends Filter {
if (matchedGroup == commentComposer) {
// To completely hide the emoji buttons (and leave no empty space), the timestamp button is
// also hidden because the buffer is exactly the same and there's no way selectively hide.
- if (contentIndex == 0
+ return contentIndex == 0
&& path.endsWith(TIMESTAMP_OR_EMOJI_BUTTONS_ENDS_WITH_PATH)
- && emojiPickerBufferGroup.check(protobufBufferArray).isFiltered()) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
-
- return false;
+ && emojiPickerBufferGroup.check(protobufBufferArray).isFiltered();
}
if (matchedGroup == filterChipBar) {
- if (aiCommentsSummary.check(protobufBufferArray).isFiltered()) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
- return false;
+ return aiCommentsSummary.check(protobufBufferArray).isFiltered();
}
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ return true;
}
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/CustomFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/CustomFilter.java
index 37062d6e2..263921fff 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/CustomFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/CustomFilter.java
@@ -153,9 +153,11 @@ final class CustomFilter extends Filter {
if (custom.startsWith && contentIndex != 0) {
return false;
}
- if (custom.bufferSearch != null && !custom.bufferSearch.matches(protobufBufferArray)) {
- return false;
+
+ if (custom.bufferSearch == null) {
+ return true; // No buffer filter, only path filtering.
}
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+
+ return custom.bufferSearch.matches(protobufBufferArray);
}
}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionComponentsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionComponentsFilter.java
index 3ccdd97f8..8e69ac407 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionComponentsFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionComponentsFilter.java
@@ -28,6 +28,11 @@ final class DescriptionComponentsFilter extends Filter {
"cell_expandable_metadata.eml"
);
+ final StringFilterGroup askSection = new StringFilterGroup(
+ Settings.HIDE_ASK_SECTION,
+ "youchat_entrypoint.eml"
+ );
+
final StringFilterGroup attributesSection = new StringFilterGroup(
Settings.HIDE_ATTRIBUTES_SECTION,
"gaming_section",
@@ -73,6 +78,7 @@ final class DescriptionComponentsFilter extends Filter {
addPathCallbacks(
aiGeneratedVideoSummarySection,
+ askSection,
attributesSection,
infoCardsSection,
howThisWasMadeSection,
@@ -88,13 +94,9 @@ final class DescriptionComponentsFilter extends Filter {
if (exceptions.matches(path)) return false;
if (matchedGroup == macroMarkersCarousel) {
- if (contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered()) {
- return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
-
- return false;
+ return contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered();
}
- return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ return true;
}
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/Filter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/Filter.java
index 42b86d589..ddec956f0 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/Filter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/Filter.java
@@ -6,9 +6,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.settings.BaseSettings;
-
/**
* Filters litho based components.
*
@@ -62,10 +59,7 @@ abstract class Filter {
* Called after an enabled filter has been matched.
* Default implementation is to always filter the matched component and log the action.
* Subclasses can perform additional or different checks if needed.
- *
- * If the content is to be filtered, subclasses should always
- * call this method (and never return a plain 'true').
- * That way the logs will always show when a component was filtered and which filter hide it.
+ *
*
* Method is called off the main thread.
*
@@ -76,14 +70,6 @@ abstract class Filter {
*/
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
- if (BaseSettings.DEBUG.get()) {
- String filterSimpleName = getClass().getSimpleName();
- if (contentType == FilterContentType.IDENTIFIER) {
- Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier);
- } else {
- Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path);
- }
- }
return true;
}
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java
index d365d6802..45fdcd7d2 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java
@@ -576,7 +576,7 @@ final class KeywordContentFilter extends Filter {
MutableReference matchRef = new MutableReference<>();
if (bufferSearch.matches(protobufBufferArray, matchRef)) {
updateStats(true, matchRef.value);
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ return true;
}
updateStats(false, null);
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java
index 5f6bffd1a..347491a46 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java
@@ -34,12 +34,11 @@ public final class LayoutComponentsFilter extends Filter {
private final StringFilterGroup notifyMe;
private final StringFilterGroup singleItemInformationPanel;
private final StringFilterGroup expandableMetadata;
- private final ByteArrayFilterGroup searchResultRecommendations;
- private final StringFilterGroup searchResultVideo;
private final StringFilterGroup compactChannelBarInner;
private final StringFilterGroup compactChannelBarInnerButton;
private final ByteArrayFilterGroup joinMembershipButton;
private final StringFilterGroup horizontalShelves;
+ private final ByteArrayFilterGroup ticketShelf;
public LayoutComponentsFilter() {
exceptions.addPatterns(
@@ -233,14 +232,9 @@ public final class LayoutComponentsFilter extends Filter {
"mixed_content_shelf"
);
- searchResultVideo = new StringFilterGroup(
- Settings.HIDE_SEARCH_RESULT_RECOMMENDATIONS,
- "search_video_with_context.eml"
- );
-
- searchResultRecommendations = new ByteArrayFilterGroup(
- Settings.HIDE_SEARCH_RESULT_RECOMMENDATIONS,
- "endorsement_header_footer"
+ final var searchResultRecommendationLabels = new StringFilterGroup(
+ Settings.HIDE_SEARCH_RESULT_RECOMMENDATION_LABELS,
+ "endorsement_header_footer.eml"
);
horizontalShelves = new StringFilterGroup(
@@ -251,6 +245,11 @@ public final class LayoutComponentsFilter extends Filter {
"horizontal_tile_shelf.eml"
);
+ ticketShelf = new ByteArrayFilterGroup(
+ Settings.HIDE_TICKET_SHELF,
+ "ticket"
+ );
+
addPathCallbacks(
expandableMetadata,
inFeedSurvey,
@@ -258,7 +257,7 @@ public final class LayoutComponentsFilter extends Filter {
compactChannelBar,
communityPosts,
paidPromotion,
- searchResultVideo,
+ searchResultRecommendationLabels,
latestPosts,
channelWatermark,
communityGuidelines,
@@ -293,50 +292,29 @@ public final class LayoutComponentsFilter extends Filter {
// From 2025, the medical information panel is no longer shown in the search results.
// Therefore, this identifier does not filter when the search bar is activated.
if (matchedGroup == singleItemInformationPanel) {
- if (PlayerType.getCurrent().isMaximizedOrFullscreen() || !NavigationBar.isSearchBarActive()) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
-
- return false;
- }
-
- if (matchedGroup == searchResultVideo) {
- if (searchResultRecommendations.check(protobufBufferArray).isFiltered()) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
- return false;
+ return PlayerType.getCurrent().isMaximizedOrFullscreen() || !NavigationBar.isSearchBarActive();
}
// The groups are excluded from the filter due to the exceptions list below.
// Filter them separately here.
- if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata)
- {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata) {
+ return true;
}
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
if (matchedGroup == compactChannelBarInner) {
- if (compactChannelBarInnerButton.check(path).isFiltered()) {
- // The filter may be broad, but in the context of a compactChannelBarInnerButton,
- // it's safe to assume that the button is the only thing that should be hidden.
- if (joinMembershipButton.check(protobufBufferArray).isFiltered()) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
- }
-
- return false;
+ return compactChannelBarInnerButton.check(path).isFiltered()
+ // The filter may be broad, but in the context of a compactChannelBarInnerButton,
+ // it's safe to assume that the button is the only thing that should be hidden.
+ && joinMembershipButton.check(protobufBufferArray).isFiltered();
}
if (matchedGroup == horizontalShelves) {
- if (contentIndex == 0 && hideShelves()) {
- return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
-
- return false;
+ return contentIndex == 0 && (hideShelves() || ticketShelf.check(protobufBufferArray).isFiltered());
}
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ return true;
}
/**
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LithoFilterPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LithoFilterPatch.java
index 1edd27509..ac88185cf 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LithoFilterPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LithoFilterPatch.java
@@ -7,6 +7,7 @@ import java.nio.ByteBuffer;
import java.util.List;
import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.youtube.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
@@ -87,6 +88,10 @@ public final class LithoFilterPatch {
* the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
*/
private static final ThreadLocal bufferThreadLocal = new ThreadLocal<>();
+ /**
+ * Results of calling {@link #filter(String, StringBuilder)}.
+ */
+ private static final ThreadLocal filterResult = new ThreadLocal<>();
static {
for (Filter filter : filters) {
@@ -110,12 +115,29 @@ public final class LithoFilterPatch {
if (!group.includeInSearch()) {
continue;
}
+
for (String pattern : group.filters) {
- pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
+ String filterSimpleName = filter.getClass().getSimpleName();
+
+ pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex,
+ matchedLength, callbackParameter) -> {
if (!group.isEnabled()) return false;
+
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
- return filter.isFiltered(parameters.identifier, parameters.path, parameters.protoBuffer,
- group, type, matchedStartIndex);
+ final boolean isFiltered = filter.isFiltered(parameters.identifier,
+ parameters.path, parameters.protoBuffer, group, type, matchedStartIndex);
+
+ if (isFiltered && BaseSettings.DEBUG.get()) {
+ if (type == Filter.FilterContentType.IDENTIFIER) {
+ Logger.printDebug(() -> "Filtered " + filterSimpleName
+ + " identifier: " + parameters.identifier);
+ } else {
+ Logger.printDebug(() -> "Filtered " + filterSimpleName
+ + " path: " + parameters.path);
+ }
+ }
+
+ return isFiltered;
}
);
}
@@ -140,11 +162,22 @@ public final class LithoFilterPatch {
}
}
+ /**
+ * Injection point.
+ */
+ public static boolean shouldFilter() {
+ Boolean shouldFilter = filterResult.get();
+ return shouldFilter != null && shouldFilter;
+ }
+
/**
* Injection point. Called off the main thread, and commonly called by multiple threads at the same time.
*/
- @SuppressWarnings("unused")
- public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) {
+ public static void filter(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
+ filterResult.set(handleFiltering(lithoIdentifier, pathBuilder));
+ }
+
+ private static boolean handleFiltering(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
try {
if (pathBuilder.length() == 0) {
return false;
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java
index d7c8e6caa..e1401f3ae 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java
@@ -99,7 +99,7 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == videoQualityMenuFooter) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ return true;
}
if (contentIndex != 0) {
@@ -111,11 +111,6 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
return false;
}
- if (flyoutFilterGroupList.check(protobufBufferArray).isFiltered()) {
- // Super class handles logging.
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
-
- return false;
+ return flyoutFilterGroupList.check(protobufBufferArray).isFiltered();
}
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/ShortsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/ShortsFilter.java
index b647d48c4..ef1cd5bb5 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/ShortsFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/ShortsFilter.java
@@ -278,27 +278,18 @@ public final class ShortsFilter extends Filter {
if (contentType == FilterContentType.PATH) {
if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) {
// Selectively filter to avoid false positive filtering of other subscribe/join buttons.
- if (path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH)) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
- return false;
+ return path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH);
}
if (matchedGroup == shortsCompactFeedVideoPath) {
- if (shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered()) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
- return false;
+ return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered();
}
// Video action buttons (comment, share, remix) have the same path.
// Like and dislike are separate path filters and don't require buffer searching.
if (matchedGroup == shortsActionBar) {
- if (actionButton.check(path).isFiltered()
- && videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
- return false;
+ return actionButton.check(path).isFiltered()
+ && videoActionButtonGroupList.check(protobufBufferArray).isFiltered();
}
if (matchedGroup == suggestedAction) {
@@ -306,28 +297,23 @@ public final class ShortsFilter extends Filter {
// This has a secondary effect of hiding all new un-identified actions
// under the assumption that the user wants all suggestions hidden.
if (isEverySuggestedActionFilterEnabled()) {
- return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ return true;
}
- if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) {
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
- }
- return false;
+ return suggestedActionsGroupList.check(protobufBufferArray).isFiltered();
}
- } else {
- // Feed/search identifier components.
- if (matchedGroup == shelfHeader) {
- // 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 (!shouldHideShortsFeedItems()) return false;
+ return true;
}
- // Super class handles logging.
- return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ // Feed/search identifier components.
+ if (matchedGroup == shelfHeader) {
+ // 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;
+ }
+
+ return shouldHideShortsFeedItems();
}
private static boolean shouldHideShortsFeedItems() {
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java
index 8cde513bd..9b6224106 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java
@@ -1,16 +1,12 @@
package app.revanced.extension.youtube.patches.playback.speed;
-import static app.revanced.extension.shared.StringRef.sf;
import static app.revanced.extension.shared.StringRef.str;
-import android.preference.ListPreference;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
-import androidx.annotation.NonNull;
-
import java.util.Arrays;
import app.revanced.extension.shared.Logger;
@@ -21,8 +17,6 @@ import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class CustomPlaybackSpeedPatch {
- private static final float PLAYBACK_SPEED_AUTO = Settings.PLAYBACK_SPEED_DEFAULT.defaultValue;
-
/**
* Maximum playback speed, exclusive value. Custom speeds must be less than this value.
*
@@ -47,11 +41,6 @@ public class CustomPlaybackSpeedPatch {
*/
private static long lastTimeOldPlaybackMenuInvoked;
- /**
- * PreferenceList entries and values, of all available playback speeds.
- */
- private static String[] preferenceListEntries, preferenceListEntryValues;
-
static {
final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get();
@@ -117,33 +106,6 @@ public class CustomPlaybackSpeedPatch {
return false;
}
- /**
- * Initialize a settings preference list with the available playback speeds.
- */
- @SuppressWarnings("deprecation")
- public static void initializeListPreference(ListPreference preference) {
- if (preferenceListEntries == null) {
- final int numberOfEntries = customPlaybackSpeeds.length + 1;
- preferenceListEntries = new String[numberOfEntries];
- preferenceListEntryValues = new String[numberOfEntries];
-
- // Auto speed (same behavior as unpatched).
- preferenceListEntries[0] = sf("revanced_custom_playback_speeds_auto").toString();
- preferenceListEntryValues[0] = String.valueOf(PLAYBACK_SPEED_AUTO);
-
- int i = 1;
- for (float speed : customPlaybackSpeeds) {
- String speedString = String.valueOf(speed);
- preferenceListEntries[i] = speedString + "x";
- preferenceListEntryValues[i] = speedString;
- i++;
- }
- }
-
- preference.setEntries(preferenceListEntries);
- preference.setEntryValues(preferenceListEntryValues);
- }
-
/**
* Injection point.
*/
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java
index 2283106bf..4d036509e 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java
@@ -1,6 +1,7 @@
package app.revanced.extension.youtube.patches.theme;
import static app.revanced.extension.shared.StringRef.str;
+import static app.revanced.extension.shared.Utils.clamp;
import android.content.res.Resources;
import android.graphics.Color;
@@ -378,14 +379,4 @@ public final class SeekbarColorPatch {
return originalColor;
}
}
-
- /** @noinspection SameParameterValue */
- private static int clamp(int value, int lower, int upper) {
- return Math.max(lower, Math.min(value, upper));
- }
-
- /** @noinspection SameParameterValue */
- private static float clamp(float value, float lower, float upper) {
- return Math.max(lower, Math.min(value, upper));
- }
}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
index a258dffd2..a0730c055 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
@@ -235,7 +235,7 @@ public class ReturnYouTubeDislike {
final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get();
if (!compactLayout) {
- String leftSeparatorString = getTextDirectionString();
+ String leftSeparatorString = Utils.getTextDirectionString();
final Spannable leftSeparatorSpan;
if (isRollingNumber) {
leftSeparatorSpan = new SpannableString(leftSeparatorString);
@@ -279,12 +279,6 @@ public class ReturnYouTubeDislike {
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.
*/
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java
new file mode 100644
index 000000000..c37b472d3
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java
@@ -0,0 +1,29 @@
+package app.revanced.extension.youtube.returnyoutubedislike.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import app.revanced.extension.youtube.settings.preference.UrlLinkPreference;
+
+/**
+ * Allows tapping the RYD about preference to open the website.
+ */
+@SuppressWarnings("unused")
+public class ReturnYouTubeDislikeAboutPreference extends UrlLinkPreference {
+ {
+ externalUrl = "https://returnyoutubedislike.com";
+ }
+
+ public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+ public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+ public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ public ReturnYouTubeDislikeAboutPreference(Context context) {
+ super(context);
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeDebugStatsPreferenceCategory.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeDebugStatsPreferenceCategory.java
new file mode 100644
index 000000000..dac275e4f
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeDebugStatsPreferenceCategory.java
@@ -0,0 +1,126 @@
+package app.revanced.extension.youtube.returnyoutubedislike.ui;
+
+import static app.revanced.extension.shared.StringRef.str;
+
+import android.content.Context;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.settings.BaseSettings;
+import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
+
+@SuppressWarnings({"unused", "deprecation"})
+public class ReturnYouTubeDislikeDebugStatsPreferenceCategory extends PreferenceCategory {
+
+ private static final boolean SHOW_RYD_DEBUG_STATS = BaseSettings.DEBUG.get();
+
+ private static String createSummaryText(int value, String summaryStringZeroKey, String summaryStringOneOrMoreKey) {
+ if (value == 0) {
+ return str(summaryStringZeroKey);
+ }
+ return str(summaryStringOneOrMoreKey, value);
+ }
+
+ private static String createMillisecondStringFromNumber(long number) {
+ return String.format(str("revanced_ryd_statistics_millisecond_text"), number);
+ }
+
+ public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected View onCreateView(ViewGroup parent) {
+ if (!SHOW_RYD_DEBUG_STATS) {
+ // Use an empty view to hide without removing.
+ return new View(getContext());
+ }
+
+ return super.onCreateView(parent);
+ }
+
+ protected void onAttachedToActivity() {
+ try {
+ super.onAttachedToActivity();
+ if (!SHOW_RYD_DEBUG_STATS) {
+ return;
+ }
+
+ Logger.printDebug(() -> "Updating stats preferences");
+ removeAll();
+
+ addStatisticPreference(
+ "revanced_ryd_statistics_getFetchCallResponseTimeAverage_title",
+ createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage())
+ );
+
+ addStatisticPreference(
+ "revanced_ryd_statistics_getFetchCallResponseTimeMin_title",
+ createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin())
+ );
+
+ addStatisticPreference(
+ "revanced_ryd_statistics_getFetchCallResponseTimeMax_title",
+ createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax())
+ );
+
+ String fetchCallTimeWaitingLastSummary;
+ final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast();
+ if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) {
+ fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary");
+ } else {
+ fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast);
+ }
+ addStatisticPreference(
+ "revanced_ryd_statistics_getFetchCallResponseTimeLast_title",
+ fetchCallTimeWaitingLastSummary
+ );
+
+ addStatisticPreference(
+ "revanced_ryd_statistics_getFetchCallCount_title",
+ createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(),
+ "revanced_ryd_statistics_getFetchCallCount_zero_summary",
+ "revanced_ryd_statistics_getFetchCallCount_non_zero_summary"
+ )
+ );
+
+ addStatisticPreference(
+ "revanced_ryd_statistics_getFetchCallNumberOfFailures_title",
+ createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(),
+ "revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary",
+ "revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary"
+ )
+ );
+
+ addStatisticPreference(
+ "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title",
+ createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(),
+ "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary",
+ "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary"
+ )
+ );
+ } catch (Exception ex) {
+ Logger.printException(() -> "onAttachedToActivity failure", ex);
+ }
+ }
+
+ private void addStatisticPreference(String titleKey, String SummaryText) {
+ Preference statisticPreference = new Preference(getContext());
+ statisticPreference.setSelectable(false);
+ statisticPreference.setTitle(str(titleKey));
+ statisticPreference.setSummary(SummaryText);
+ addPreference(statisticPreference);
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java
index b9c193d1a..5a14ca39c 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java
@@ -11,8 +11,6 @@ import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toolbar;
-import java.util.Objects;
-
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.AppLanguage;
@@ -21,8 +19,6 @@ import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.patches.VersionCheckPatch;
import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch;
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
-import app.revanced.extension.youtube.settings.preference.ReturnYouTubeDislikePreferenceFragment;
-import app.revanced.extension.youtube.settings.preference.SponsorBlockPreferenceFragment;
/**
* Hooks LicenseActivity.
@@ -88,28 +84,15 @@ public class LicenseActivityHook {
licenseActivity.setContentView(getResourceIdentifier(
"revanced_settings_with_toolbar", "layout"));
- PreferenceFragment fragment;
- String toolbarTitleResourceName;
- String dataString = Objects.requireNonNull(licenseActivity.getIntent().getDataString());
- switch (dataString) {
- case "revanced_sb_settings_intent":
- toolbarTitleResourceName = "revanced_sb_settings_title";
- fragment = new SponsorBlockPreferenceFragment();
- break;
- case "revanced_ryd_settings_intent":
- toolbarTitleResourceName = "revanced_ryd_settings_title";
- fragment = new ReturnYouTubeDislikePreferenceFragment();
- break;
- case "revanced_settings_intent":
- toolbarTitleResourceName = "revanced_settings_title";
- fragment = new ReVancedPreferenceFragment();
- break;
- default:
- Logger.printException(() -> "Unknown setting: " + dataString);
- return;
+ // Sanity check.
+ String dataString = licenseActivity.getIntent().getDataString();
+ if (!"revanced_settings_intent".equals(dataString)) {
+ Logger.printException(() -> "Unknown intent: " + dataString);
+ return;
}
- createToolbar(licenseActivity, toolbarTitleResourceName);
+ PreferenceFragment fragment = new ReVancedPreferenceFragment();
+ createToolbar(licenseActivity, fragment);
//noinspection deprecation
licenseActivity.getFragmentManager()
@@ -122,7 +105,7 @@ public class LicenseActivityHook {
}
@SuppressLint("UseCompatLoadingForDrawables")
- private static void createToolbar(Activity activity, String toolbarTitleResourceName) {
+ private static void createToolbar(Activity activity, PreferenceFragment fragment) {
// Replace dummy placeholder toolbar.
// This is required to fix submenu title alignment issue with Android ASOP 15+
ViewGroup toolBarParent = activity.findViewById(
@@ -134,8 +117,7 @@ public class LicenseActivityHook {
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
toolbar.setBackgroundColor(ThemeHelper.getToolbarBackgroundColor());
toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable());
- toolbar.setNavigationOnClickListener(view -> activity.onBackPressed());
- toolbar.setTitle(getResourceIdentifier(toolbarTitleResourceName, "string"));
+ toolbar.setTitle(getResourceIdentifier("revanced_settings_title", "string"));
final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16,
Utils.getContext().getResources().getDisplayMetrics());
@@ -148,6 +130,11 @@ public class LicenseActivityHook {
}
setToolbarLayoutParams(toolbar);
+ // Add Search Icon and EditText for ReVancedPreferenceFragment only.
+ if (fragment instanceof ReVancedPreferenceFragment) {
+ SearchViewController.addSearchViewComponents(activity, toolbar, (ReVancedPreferenceFragment) fragment);
+ }
+
toolBarParent.addView(toolbar, 0);
}
-}
\ No newline at end of file
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/SearchViewController.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/SearchViewController.java
new file mode 100644
index 000000000..cb2b42d7a
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/SearchViewController.java
@@ -0,0 +1,381 @@
+package app.revanced.extension.youtube.settings;
+
+import static app.revanced.extension.shared.StringRef.str;
+import static app.revanced.extension.shared.Utils.getResourceIdentifier;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.graphics.drawable.GradientDrawable;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.SearchView;
+import android.widget.TextView;
+import android.widget.Toolbar;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.LinkedList;
+import java.util.List;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.AppLanguage;
+import app.revanced.extension.shared.settings.BaseSettings;
+import app.revanced.extension.shared.settings.StringSetting;
+import app.revanced.extension.youtube.ThemeHelper;
+import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
+
+/**
+ * Controller for managing the search view in ReVanced settings.
+ */
+@SuppressWarnings({"deprecated", "DiscouragedApi"})
+public class SearchViewController {
+ private static final int MAX_HISTORY_SIZE = 5;
+
+ private final SearchView searchView;
+ private final FrameLayout searchContainer;
+ private final Toolbar toolbar;
+ private final Activity activity;
+ private boolean isSearchActive;
+ private final CharSequence originalTitle;
+ private final Deque searchHistory;
+ private final AutoCompleteTextView autoCompleteTextView;
+ private final boolean showSettingsSearchHistory;
+
+ /**
+ * Creates a background drawable for the SearchView with rounded corners.
+ */
+ private static GradientDrawable createBackgroundDrawable(Context context) {
+ GradientDrawable background = new GradientDrawable();
+ background.setShape(GradientDrawable.RECTANGLE);
+ background.setCornerRadius(28 * context.getResources().getDisplayMetrics().density); // 28dp corner radius.
+ int baseColor = ThemeHelper.getBackgroundColor();
+ int adjustedColor = ThemeHelper.isDarkTheme()
+ ? ThemeHelper.adjustColorBrightness(baseColor, 1.11f) // Lighten for dark theme.
+ : ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
+ background.setColor(adjustedColor);
+ return background;
+ }
+
+ /**
+ * Creates a background drawable for suggestion items with rounded corners.
+ */
+ private static GradientDrawable createSuggestionBackgroundDrawable(Context context) {
+ GradientDrawable background = new GradientDrawable();
+ background.setShape(GradientDrawable.RECTANGLE);
+ background.setCornerRadius(8 * context.getResources().getDisplayMetrics().density); // 8dp corner radius.
+ return background;
+ }
+
+ /**
+ * Adds search view components to the activity.
+ */
+ public static void addSearchViewComponents(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
+ new SearchViewController(activity, toolbar, fragment);
+ }
+
+ private SearchViewController(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
+ this.activity = activity;
+ this.toolbar = toolbar;
+ this.originalTitle = toolbar.getTitle();
+ this.showSettingsSearchHistory = Settings.SETTINGS_SEARCH_HISTORY.get();
+ this.searchHistory = new LinkedList<>();
+ StringSetting searchEntries = Settings.SETTINGS_SEARCH_ENTRIES;
+ if (showSettingsSearchHistory) {
+ String entries = searchEntries.get();
+ if (!entries.isBlank()) {
+ searchHistory.addAll(Arrays.asList(entries.split("\n")));
+ }
+ } else {
+ // Clear old saved history if the user turns off the feature.
+ searchEntries.resetToDefault();
+ }
+
+ // Retrieve SearchView and container from XML.
+ searchView = activity.findViewById(getResourceIdentifier(
+ "revanced_search_view", "id"));
+ searchContainer = activity.findViewById(getResourceIdentifier(
+ "revanced_search_view_container", "id"));
+
+ // Initialize AutoCompleteTextView.
+ autoCompleteTextView = searchView.findViewById(
+ searchView.getContext().getResources().getIdentifier(
+ "android:id/search_src_text", null, null));
+
+ // Set background and query hint.
+ searchView.setBackground(createBackgroundDrawable(toolbar.getContext()));
+ searchView.setQueryHint(str("revanced_settings_search_hint"));
+
+ // Configure RTL support based on app language.
+ AppLanguage appLanguage = BaseSettings.REVANCED_LANGUAGE.get();
+ if (Utils.isRightToLeftLocale(appLanguage.getLocale())) {
+ searchView.setTextDirection(View.TEXT_DIRECTION_RTL);
+ searchView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
+ }
+
+ // Set up search history suggestions.
+ if (showSettingsSearchHistory) {
+ setupSearchHistory();
+ }
+
+ // Set up query text listener.
+ searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ try {
+ String queryTrimmed = query.trim();
+ if (!queryTrimmed.isEmpty()) {
+ saveSearchQuery(queryTrimmed);
+ }
+ // Hide suggestions on submit.
+ if (showSettingsSearchHistory && autoCompleteTextView != null) {
+ autoCompleteTextView.dismissDropDown();
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "onQueryTextSubmit failure", ex);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ try {
+ Logger.printDebug(() -> "Search query: " + newText);
+ fragment.filterPreferences(newText);
+ // Prevent suggestions from showing during text input.
+ if (showSettingsSearchHistory && autoCompleteTextView != null) {
+ if (!newText.isEmpty()) {
+ autoCompleteTextView.dismissDropDown();
+ autoCompleteTextView.setThreshold(Integer.MAX_VALUE); // Disable autocomplete suggestions.
+ } else {
+ autoCompleteTextView.setThreshold(1); // Re-enable for empty input.
+ }
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "onQueryTextChange failure", ex);
+ }
+ return true;
+ }
+ });
+
+ // Set menu and search icon.
+ final int actionSearchId = getResourceIdentifier("action_search", "id");
+ toolbar.inflateMenu(getResourceIdentifier("revanced_search_menu", "menu"));
+ MenuItem searchItem = toolbar.getMenu().findItem(actionSearchId);
+ searchItem.setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme()
+ ? "yt_outline_search_white_24"
+ : "yt_outline_search_black_24",
+ "drawable")).setTooltipText(null);
+
+ // Set menu item click listener.
+ toolbar.setOnMenuItemClickListener(item -> {
+ try {
+ if (item.getItemId() == actionSearchId) {
+ if (!isSearchActive) {
+ openSearch();
+ }
+ return true;
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "menu click failure", ex);
+ }
+ return false;
+ });
+
+ // Set navigation click listener.
+ toolbar.setNavigationOnClickListener(view -> {
+ try {
+ if (isSearchActive) {
+ closeSearch();
+ } else {
+ activity.onBackPressed();
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "navigation click failure", ex);
+ }
+ });
+ }
+
+ /**
+ * Sets up the search history suggestions for the SearchView with custom adapter.
+ */
+ private void setupSearchHistory() {
+ if (autoCompleteTextView != null) {
+ SearchHistoryAdapter adapter = new SearchHistoryAdapter(activity, new ArrayList<>(searchHistory));
+ autoCompleteTextView.setAdapter(adapter);
+ autoCompleteTextView.setThreshold(1); // Initial threshold for empty input.
+ autoCompleteTextView.setLongClickable(true);
+
+ // Show suggestions only when search bar is active and query is empty.
+ autoCompleteTextView.setOnFocusChangeListener((v, hasFocus) -> {
+ if (hasFocus && isSearchActive && autoCompleteTextView.getText().length() == 0) {
+ autoCompleteTextView.showDropDown();
+ }
+ });
+ }
+ }
+
+ /**
+ * Saves a search query to the search history.
+ * @param query The search query to save.
+ */
+ private void saveSearchQuery(String query) {
+ if (!showSettingsSearchHistory) {
+ return;
+ }
+ searchHistory.remove(query); // Remove if already exists to update position.
+ searchHistory.addFirst(query); // Add to the most recent.
+
+ // Remove extra old entries.
+ while (searchHistory.size() > MAX_HISTORY_SIZE) {
+ String last = searchHistory.removeLast();
+ Logger.printDebug(() -> "Removing search history query: " + last);
+ }
+
+ saveSearchHistory();
+
+ updateSearchHistoryAdapter();
+ }
+
+ /**
+ * Removes a search query from the search history.
+ * @param query The search query to remove.
+ */
+ private void removeSearchQuery(String query) {
+ searchHistory.remove(query);
+
+ saveSearchHistory();
+
+ updateSearchHistoryAdapter();
+ }
+
+ /**
+ * Save the search history to the shared preferences.
+ */
+ private void saveSearchHistory() {
+ Logger.printDebug(() -> "Saving search history: " + searchHistory);
+
+ Settings.SETTINGS_SEARCH_ENTRIES.save(
+ String.join("\n", searchHistory)
+ );
+ }
+
+ /**
+ * Updates the search history adapter with the latest history.
+ */
+ private void updateSearchHistoryAdapter() {
+ if (autoCompleteTextView == null) {
+ return;
+ }
+
+ SearchHistoryAdapter adapter = (SearchHistoryAdapter) autoCompleteTextView.getAdapter();
+ if (adapter != null) {
+ adapter.clear();
+ adapter.addAll(searchHistory);
+ adapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Opens the search view and shows the keyboard.
+ */
+ private void openSearch() {
+ isSearchActive = true;
+ toolbar.getMenu().findItem(getResourceIdentifier(
+ "action_search", "id")).setVisible(false);
+ toolbar.setTitle("");
+ searchContainer.setVisibility(View.VISIBLE);
+ searchView.requestFocus();
+
+ // Show keyboard.
+ InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT);
+
+ // Show suggestions with a slight delay.
+ if (showSettingsSearchHistory && autoCompleteTextView != null && autoCompleteTextView.getText().length() == 0) {
+ searchView.postDelayed(() -> {
+ if (isSearchActive && autoCompleteTextView.getText().length() == 0) {
+ autoCompleteTextView.showDropDown();
+ }
+ }, 100); // 100ms delay to ensure focus is stable.
+ }
+ }
+
+ /**
+ * Closes the search view and hides the keyboard.
+ */
+ private void closeSearch() {
+ isSearchActive = false;
+ toolbar.getMenu().findItem(getResourceIdentifier(
+ "action_search", "id"))
+ .setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme()
+ ? "yt_outline_search_white_24"
+ : "yt_outline_search_black_24",
+ "drawable")
+ ).setVisible(true);
+ toolbar.setTitle(originalTitle);
+ searchContainer.setVisibility(View.GONE);
+ searchView.setQuery("", false);
+
+ // Hide keyboard.
+ InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(searchView.getWindowToken(), 0);
+ }
+
+ /**
+ * Custom ArrayAdapter for search history.
+ */
+ private class SearchHistoryAdapter extends ArrayAdapter {
+ public SearchHistoryAdapter(Context context, List history) {
+ super(context, 0, history);
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
+ if (convertView == null) {
+ convertView = LinearLayout.inflate(getContext(), getResourceIdentifier(
+ "revanced_search_suggestion_item", "layout"), null);
+ }
+
+ // Apply rounded corners programmatically.
+ convertView.setBackground(createSuggestionBackgroundDrawable(getContext()));
+ String query = getItem(position);
+
+ // Set query text.
+ TextView textView = convertView.findViewById(getResourceIdentifier(
+ "suggestion_text", "id"));
+ if (textView != null) {
+ textView.setText(query);
+ }
+
+ // Set click listener for inserting query into SearchView.
+ convertView.setOnClickListener(v -> {
+ searchView.setQuery(query, true); // Insert selected query and submit.
+ });
+
+ // Set long click listener for deletion confirmation.
+ convertView.setOnLongClickListener(v -> {
+ new AlertDialog.Builder(activity)
+ .setTitle(query)
+ .setMessage(str("revanced_settings_search_remove_message"))
+ .setPositiveButton(android.R.string.ok,
+ (dialog, which) -> removeSearchQuery(query))
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ return true;
+ });
+
+ return convertView;
+ }
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
index 29debf67e..4a36e6458 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
@@ -25,6 +25,8 @@ import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehavi
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 app.revanced.extension.shared.settings.preference.SharedPrefCategory;
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
import android.graphics.Color;
@@ -103,8 +105,9 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_MOVIES_SECTION = new BooleanSetting("revanced_hide_movies_section", TRUE);
public static final BooleanSetting HIDE_NOTIFY_ME_BUTTON = new BooleanSetting("revanced_hide_notify_me_button", TRUE);
public static final BooleanSetting HIDE_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE);
- public static final BooleanSetting HIDE_SEARCH_RESULT_RECOMMENDATIONS = new BooleanSetting("revanced_hide_search_result_recommendations", TRUE);
+ public static final BooleanSetting HIDE_SEARCH_RESULT_RECOMMENDATION_LABELS = new BooleanSetting("revanced_hide_search_result_recommendation_labels", TRUE);
public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true);
+ public static final BooleanSetting HIDE_TICKET_SHELF = new BooleanSetting("revanced_hide_ticket_shelf", FALSE);
// Alternative thumbnails
public static final EnumSetting ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL);
public static final EnumSetting ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscription", ThumbnailOption.ORIGINAL);
@@ -139,6 +142,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_EMERGENCY_BOX = new BooleanSetting("revanced_hide_emergency_box", TRUE);
public static final BooleanSetting HIDE_ENDSCREEN_CARDS = new BooleanSetting("revanced_hide_endscreen_cards", FALSE);
public static final BooleanSetting HIDE_END_SCREEN_SUGGESTED_VIDEO = new BooleanSetting("revanced_end_screen_suggested_video", FALSE, true);
+ public static final BooleanSetting HIDE_RELATED_VIDEO_OVERLAY = new BooleanSetting("revanced_hide_related_video_overlay", FALSE, true);
public static final BooleanSetting HIDE_HIDE_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_channel_guidelines", TRUE);
public static final BooleanSetting HIDE_INFO_PANELS = new BooleanSetting("revanced_hide_info_panels", TRUE);
public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", FALSE);
@@ -182,6 +186,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_COMMENTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_comments_thanks_button", TRUE);
// Description
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_ASK_SECTION = new BooleanSetting("revanced_hide_ask_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", TRUE);
public static final BooleanSetting HIDE_HOW_THIS_WAS_MADE_SECTION = new BooleanSetting("revanced_hide_how_this_was_made_section", FALSE);
@@ -217,6 +222,8 @@ public class Settings extends BaseSettings {
// General layout
public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("revanced_restore_old_settings_menus", FALSE, true);
+ public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true);
+ public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "", true);
public static final EnumSetting CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
@@ -340,19 +347,17 @@ public class Settings extends BaseSettings {
public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_swipe_lowest_value_enable_auto_brightness", FALSE, true, parent(SWIPE_BRIGHTNESS));
// ReturnYoutubeDislike
- public static final BooleanSetting RYD_ENABLED = new BooleanSetting("ryd_enabled", TRUE);
- public static final StringSetting RYD_USER_ID = new StringSetting("ryd_user_id", "", false, false);
- 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", TRUE, parent(RYD_ENABLED));
- public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("ryd_toast_on_connection_error", TRUE, parent(RYD_ENABLED));
+ public static final BooleanSetting RYD_ENABLED = new BooleanSetting("revanced_ryd_enabled", TRUE);
+ public static final StringSetting RYD_USER_ID = new StringSetting("revanced_ryd_user_id", "", false, false);
+ public static final BooleanSetting RYD_SHORTS = new BooleanSetting("revanced_ryd_shorts", TRUE, parent(RYD_ENABLED));
+ public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("revanced_ryd_dislike_percentage", FALSE, true, parent(RYD_ENABLED));
+ public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("revanced_ryd_compact_layout", FALSE, true, parent(RYD_ENABLED));
+ public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("revanced_ryd_estimated_like", TRUE, true, parent(RYD_ENABLED));
+ public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("revanced_ryd_toast_on_connection_error", TRUE, parent(RYD_ENABLED));
// SponsorBlock
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
- /**
- * Do not use directly, instead use {@link SponsorBlockSettings}
- */
+ /** Do not use id setting directly. Instead use {@link SponsorBlockSettings}. */
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "");
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));
@@ -416,12 +421,10 @@ public class Settings extends BaseSettings {
// region Migration
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_BUTTONS, HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS);
-
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER, HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER);
-
migrateOldSettingToNew(DEPRECATED_DISABLE_SUGGESTED_VIDEO_END_SCREEN, HIDE_END_SCREEN_SUGGESTED_VIDEO);
-
migrateOldSettingToNew(DEPRECATED_RESTORE_OLD_VIDEO_QUALITY_MENU, ADVANCED_VIDEO_QUALITY_MENU);
+ migrateOldSettingToNew(DEPRECATED_AUTO_CAPTIONS, DISABLE_AUTO_CAPTIONS);
// Migrate renamed enum.
//noinspection deprecation
@@ -464,10 +467,15 @@ public class Settings extends BaseSettings {
SPOOF_APP_VERSION_TARGET.resetToDefault();
}
- if (!DEPRECATED_AUTO_CAPTIONS.isSetToDefault()) {
- DISABLE_AUTO_CAPTIONS.save(true);
- DEPRECATED_AUTO_CAPTIONS.resetToDefault();
- }
+ // RYD requires manually migrating old settings since the lack of
+ // a "revanced_" on the old setting causes duplicate key exceptions during export.
+ SharedPrefCategory revancedPrefs = Setting.preferences;
+ Setting.migrateFromOldPreferences(revancedPrefs, RYD_USER_ID, "ryd_user_id");
+ Setting.migrateFromOldPreferences(revancedPrefs, RYD_ENABLED, "ryd_enabled");
+ Setting.migrateFromOldPreferences(revancedPrefs, RYD_DISLIKE_PERCENTAGE, "ryd_dislike_percentage");
+ Setting.migrateFromOldPreferences(revancedPrefs, RYD_COMPACT_LAYOUT, "ryd_compact_layout");
+ Setting.migrateFromOldPreferences(revancedPrefs, RYD_ESTIMATED_LIKE, "ryd_estimated_like");
+ Setting.migrateFromOldPreferences(revancedPrefs, RYD_TOAST_ON_CONNECTION_ERROR, "ryd_toast_on_connection_error");
// endregion
@@ -478,4 +486,3 @@ public class Settings extends BaseSettings {
// endregion
}
}
-
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java
index 5ca2e65dc..e5a16947f 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java
@@ -1,23 +1,15 @@
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 {
+@SuppressWarnings("unused")
+public class AlternativeThumbnailsAboutDeArrowPreference extends UrlLinkPreference {
{
- setOnPreferenceClickListener(pref -> {
- Intent i = new Intent(Intent.ACTION_VIEW);
- i.setData(Uri.parse("https://dearrow.ajay.app"));
- pref.getContext().startActivity(i);
- return false;
- });
+ externalUrl = "https://dearrow.ajay.app";
}
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java
new file mode 100644
index 000000000..eee8d2bca
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java
@@ -0,0 +1,62 @@
+package app.revanced.extension.youtube.settings.preference;
+
+import static app.revanced.extension.shared.StringRef.sf;
+
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+
+import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
+import app.revanced.extension.youtube.settings.Settings;
+
+/**
+ * Custom video speeds used by {@link CustomPlaybackSpeedPatch}.
+ */
+@SuppressWarnings({"unused", "deprecation"})
+public final class CustomVideoSpeedListPreference extends ListPreference {
+
+ /**
+ * Initialize a settings preference list with the available playback speeds.
+ */
+ private void initializeEntryValues() {
+ float[] customPlaybackSpeeds = CustomPlaybackSpeedPatch.customPlaybackSpeeds;
+ final int numberOfEntries = customPlaybackSpeeds.length + 1;
+ String[] preferenceListEntries = new String[numberOfEntries];
+ String[] preferenceListEntryValues = new String[numberOfEntries];
+
+ // Auto speed (same behavior as unpatched).
+ preferenceListEntries[0] = sf("revanced_custom_playback_speeds_auto").toString();
+ preferenceListEntryValues[0] = String.valueOf(Settings.PLAYBACK_SPEED_DEFAULT.defaultValue);
+
+ int i = 1;
+ for (float speed : customPlaybackSpeeds) {
+ String speedString = String.valueOf(speed);
+ preferenceListEntries[i] = speedString + "x";
+ preferenceListEntryValues[i] = speedString;
+ i++;
+ }
+
+ setEntries(preferenceListEntries);
+ setEntryValues(preferenceListEntryValues);
+ }
+
+ {
+ initializeEntryValues();
+ }
+
+ public CustomVideoSpeedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public CustomVideoSpeedListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public CustomVideoSpeedListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CustomVideoSpeedListPreference(Context context) {
+ super(context);
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java
index ecdcf03cf..1994bf368 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java
@@ -19,12 +19,15 @@ public class HtmlPreference extends Preference {
public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
+
public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
+
public HtmlPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
+
public HtmlPreference(Context context) {
super(context);
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
index a2b6dbbfb..916378bf5 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
@@ -1,5 +1,6 @@
package app.revanced.extension.youtube.settings.preference;
+import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
import android.annotation.SuppressLint;
@@ -9,34 +10,66 @@ import android.graphics.drawable.Drawable;
import android.os.Build;
import android.preference.ListPreference;
import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
-import android.util.Pair;
+import android.preference.SwitchPreference;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.BackgroundColorSpan;
import android.util.TypedValue;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.TextView;
import android.widget.Toolbar;
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
+import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
import app.revanced.extension.youtube.ThemeHelper;
-import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
import app.revanced.extension.youtube.settings.LicenseActivityHook;
-import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
/**
* Preference fragment for ReVanced settings.
- *
- * @noinspection deprecation
*/
+@SuppressWarnings("deprecation")
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
+ /**
+ * The main PreferenceScreen used to display the current set of preferences.
+ * This screen is manipulated during initialization and filtering to show or hide preferences.
+ */
+ private PreferenceScreen preferenceScreen;
+
+ /**
+ * A copy of the original PreferenceScreen created during initialization.
+ * Used to restore the preference structure to its initial state after filtering or other modifications.
+ */
+ private PreferenceScreen originalPreferenceScreen;
+
+ /**
+ * Used for searching preferences. A Collection of all preferences including nested preferences.
+ * Root preferences are excluded (no need to search what's on the root screen),
+ * but their sub preferences are included.
+ */
+ private final List> allPreferences = new ArrayList<>();
+
@SuppressLint("UseCompatLoadingForDrawables")
public static Drawable getBackButtonDrawable() {
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme()
@@ -47,85 +80,140 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
}
/**
- * Sorts a preference list by menu entries, but preserves the first value as the first entry.
- *
- * @noinspection SameParameterValue
+ * Initializes the preference fragment, copying the original screen to allow full restoration.
*/
- private static void sortListPreferenceByValues(ListPreference listPreference, int firstEntriesToPreserve) {
- CharSequence[] entries = listPreference.getEntries();
- CharSequence[] entryValues = listPreference.getEntryValues();
- final int entrySize = entries.length;
-
- if (entrySize != entryValues.length) {
- // Xml array declaration has a missing/extra entry.
- throw new IllegalStateException();
- }
-
- List> firstPairs = new ArrayList<>(firstEntriesToPreserve);
- List> pairsToSort = new ArrayList<>(entrySize);
-
- for (int i = 0; i < entrySize; i++) {
- Pair pair = new Pair<>(entries[i].toString(), entryValues[i].toString());
- if (i < firstEntriesToPreserve) {
- firstPairs.add(pair);
- } else {
- pairsToSort.add(pair);
- }
- }
-
- pairsToSort.sort((pair1, pair2)
- -> pair1.first.compareToIgnoreCase(pair2.first));
-
- CharSequence[] sortedEntries = new CharSequence[entrySize];
- CharSequence[] sortedEntryValues = new CharSequence[entrySize];
-
- int i = 0;
- for (Pair pair : firstPairs) {
- sortedEntries[i] = pair.first;
- sortedEntryValues[i] = pair.second;
- i++;
- }
-
- for (Pair pair : pairsToSort) {
- sortedEntries[i] = pair.first;
- sortedEntryValues[i] = pair.second;
- i++;
- }
-
- listPreference.setEntries(sortedEntries);
- listPreference.setEntryValues(sortedEntryValues);
- }
-
@Override
protected void initialize() {
super.initialize();
try {
- setPreferenceScreenToolbar(getPreferenceScreen());
+ preferenceScreen = getPreferenceScreen();
+ Utils.sortPreferenceGroups(preferenceScreen);
- // If the preference was included, then initialize it based on the available playback speed.
- Preference preference = findPreference(Settings.PLAYBACK_SPEED_DEFAULT.key);
- if (preference instanceof ListPreference playbackPreference) {
- CustomPlaybackSpeedPatch.initializeListPreference(playbackPreference);
+ // Store the original structure for restoration after filtering.
+ originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getContext());
+ for (int i = 0, count = preferenceScreen.getPreferenceCount(); i < count; i++) {
+ originalPreferenceScreen.addPreference(preferenceScreen.getPreference(i));
}
- sortPreferenceListMenu(Settings.CHANGE_START_PAGE);
- sortPreferenceListMenu(Settings.SPOOF_VIDEO_STREAMS_LANGUAGE);
- sortPreferenceListMenu(BaseSettings.REVANCED_LANGUAGE);
+ setPreferenceScreenToolbar(preferenceScreen);
} catch (Exception ex) {
Logger.printException(() -> "initialize failure", ex);
}
}
- private void sortPreferenceListMenu(EnumSetting> setting) {
- Preference preference = findPreference(setting.key);
- if (preference instanceof ListPreference languagePreference) {
- sortListPreferenceByValues(languagePreference, 1);
+ /**
+ * Called when the fragment starts, ensuring all preferences are collected after initialization.
+ */
+ @Override
+ public void onStart() {
+ super.onStart();
+ try {
+ if (allPreferences.isEmpty()) {
+ // Must collect preferences on start and not in initialize since
+ // legacy SB settings are not loaded yet.
+ Logger.printDebug(() -> "Collecting preferences to search");
+
+ // Do not show root menu preferences in search results.
+ // Instead search for everything that's not shown when search is not active.
+ collectPreferences(preferenceScreen, 1, 0);
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "onStart failure", ex);
}
}
+ /**
+ * Recursively collects all preferences from the screen or group.
+ * @param includeDepth Menu depth to start including preferences.
+ * A value of 0 adds all preferences.
+ */
+ private void collectPreferences(PreferenceGroup group, int includeDepth, int currentDepth) {
+ for (int i = 0, count = group.getPreferenceCount(); i < count; i++) {
+ Preference preference = group.getPreference(i);
+ if (includeDepth <= currentDepth && !(preference instanceof PreferenceCategory)
+ && !(preference instanceof SponsorBlockPreferenceGroup)) {
+
+ AbstractPreferenceSearchData> data;
+ if (preference instanceof SwitchPreference switchPref) {
+ data = new SwitchPreferenceSearchData(switchPref);
+ } else if (preference instanceof ListPreference listPref) {
+ data = new ListPreferenceSearchData(listPref);
+ } else {
+ data = new PreferenceSearchData(preference);
+ }
+
+ allPreferences.add(data);
+ }
+
+ if (preference instanceof PreferenceGroup subGroup) {
+ collectPreferences(subGroup, includeDepth, currentDepth + 1);
+ }
+ }
+ }
+
+ /**
+ * Filters the preferences using the given query string and applies highlighting.
+ */
+ public void filterPreferences(String query) {
+ preferenceScreen.removeAll();
+
+ if (TextUtils.isEmpty(query)) {
+ // Restore original preferences and their titles/summaries/entries.
+ for (int i = 0, count = originalPreferenceScreen.getPreferenceCount(); i < count; i++) {
+ preferenceScreen.addPreference(originalPreferenceScreen.getPreference(i));
+ }
+
+ for (AbstractPreferenceSearchData> data : allPreferences) {
+ data.clearHighlighting();
+ }
+
+ return;
+ }
+
+ // Navigation path -> Category
+ Map categoryMap = new HashMap<>();
+ String queryLower = Utils.removePunctuationToLowercase(query);
+
+ Pattern queryPattern = Pattern.compile(Pattern.quote(Utils.removePunctuationToLowercase(query)),
+ Pattern.CASE_INSENSITIVE);
+
+ for (AbstractPreferenceSearchData> data : allPreferences) {
+ if (data.matchesSearchQuery(queryLower)) {
+ data.applyHighlighting(queryLower, queryPattern);
+
+ String navigationPath = data.navigationPath;
+ PreferenceCategory group = categoryMap.computeIfAbsent(navigationPath, key -> {
+ PreferenceCategory newGroup = new PreferenceCategory(preferenceScreen.getContext());
+ newGroup.setTitle(navigationPath);
+ preferenceScreen.addPreference(newGroup);
+ return newGroup;
+ });
+ group.addPreference(data.preference);
+ }
+ }
+
+ // Show 'No results found' if search results are empty.
+ if (categoryMap.isEmpty()) {
+ Preference noResultsPreference = new Preference(preferenceScreen.getContext());
+ noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
+ noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
+ noResultsPreference.setSelectable(false);
+ // Set icon for the placeholder preference.
+ noResultsPreference.setLayoutResource(getResourceIdentifier(
+ "revanced_preference_with_icon_no_search_result", "layout"));
+ noResultsPreference.setIcon(getResourceIdentifier(
+ ThemeHelper.isDarkTheme() ? "yt_outline_search_white_24" : "yt_outline_search_black_24",
+ "drawable"));
+ preferenceScreen.addPreference(noResultsPreference);
+ }
+ }
+
+ /**
+ * Sets toolbar for all nested preference screens.
+ */
private void setPreferenceScreenToolbar(PreferenceScreen parentScreen) {
- for (int i = 0, preferenceCount = parentScreen.getPreferenceCount(); i < preferenceCount; i++) {
+ for (int i = 0, count = parentScreen.getPreferenceCount(); i < count; i++) {
Preference childPreference = parentScreen.getPreference(i);
if (childPreference instanceof PreferenceScreen) {
// Recursively set sub preferences.
@@ -156,6 +244,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
toolbar.setTitle(childScreen.getTitle());
toolbar.setNavigationIcon(getBackButtonDrawable());
toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
+
final int margin = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()
);
@@ -177,3 +266,277 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
}
}
}
+
+@SuppressWarnings("deprecation")
+class AbstractPreferenceSearchData {
+ /**
+ * @return The navigation path for the given preference, such as "Player > Action buttons".
+ */
+ private static String getPreferenceNavigationString(Preference preference) {
+ Deque pathElements = new ArrayDeque<>();
+
+ while (true) {
+ preference = preference.getParent();
+
+ if (preference == null) {
+ if (pathElements.isEmpty()) {
+ return "";
+ }
+ Locale locale = BaseSettings.REVANCED_LANGUAGE.get().getLocale();
+ return Utils.getTextDirectionString(locale) + String.join(" > ", pathElements);
+ }
+
+ if (!(preference instanceof NoTitlePreferenceCategory)
+ && !(preference instanceof SponsorBlockPreferenceGroup)) {
+ CharSequence title = preference.getTitle();
+ if (title != null && title.length() > 0) {
+ pathElements.addFirst(title);
+ }
+ }
+ }
+ }
+
+ /**
+ * Highlights the search query in the given text by applying color span.
+ * @param text The original text to process.
+ * @param queryPattern The search query to highlight.
+ * @return The text with highlighted query matches as a SpannableStringBuilder.
+ */
+ static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
+ if (TextUtils.isEmpty(text)) {
+ return text;
+ }
+
+ final int baseColor = ThemeHelper.getBackgroundColor();
+ final int adjustedColor = ThemeHelper.isDarkTheme()
+ ? ThemeHelper.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme.
+ : ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
+ BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
+
+ SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+ Matcher matcher = queryPattern.matcher(text);
+
+ while (matcher.find()) {
+ spannable.setSpan(
+ highlightSpan,
+ matcher.start(),
+ matcher.end(),
+ SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+ }
+
+ return spannable;
+ }
+
+ final T preference;
+ final String key;
+ final String navigationPath;
+ boolean highlightingApplied;
+
+ @Nullable
+ CharSequence originalTitle;
+ @Nullable
+ String searchTitle;
+
+ AbstractPreferenceSearchData(T pref) {
+ preference = pref;
+ key = Utils.removePunctuationToLowercase(pref.getKey());
+ navigationPath = getPreferenceNavigationString(pref);
+ }
+
+ @CallSuper
+ void updateSearchDataIfNeeded() {
+ if (highlightingApplied) {
+ // Must clear, otherwise old highlighting is still applied.
+ clearHighlighting();
+ }
+
+ CharSequence title = preference.getTitle();
+ if (originalTitle != title) { // Check using reference equality.
+ originalTitle = title;
+ searchTitle = Utils.removePunctuationToLowercase(title);
+ }
+ }
+
+ @CallSuper
+ boolean matchesSearchQuery(String query) {
+ updateSearchDataIfNeeded();
+
+ return key.contains(query)
+ || searchTitle != null && searchTitle.contains(query);
+ }
+
+ @CallSuper
+ void applyHighlighting(String query, Pattern queryPattern) {
+ preference.setTitle(highlightSearchQuery(originalTitle, queryPattern));
+ highlightingApplied = true;
+ }
+
+ @CallSuper
+ void clearHighlighting() {
+ if (highlightingApplied) {
+ preference.setTitle(originalTitle);
+ highlightingApplied = false;
+ }
+ }
+}
+
+/**
+ * Regular preference type that only uses the base preference summary.
+ * Should only be used if a more specific data class does not exist.
+ */
+@SuppressWarnings("deprecation")
+class PreferenceSearchData extends AbstractPreferenceSearchData {
+ @Nullable
+ CharSequence originalSummary;
+ @Nullable
+ String searchSummary;
+
+ PreferenceSearchData(Preference pref) {
+ super(pref);
+ }
+
+ void updateSearchDataIfNeeded() {
+ super.updateSearchDataIfNeeded();
+
+ CharSequence summary = preference.getSummary();
+ if (originalSummary != summary) {
+ originalSummary = summary;
+ searchSummary = Utils.removePunctuationToLowercase(summary);
+ }
+ }
+
+ boolean matchesSearchQuery(String query) {
+ return super.matchesSearchQuery(query)
+ || searchSummary != null && searchSummary.contains(query);
+ }
+
+ @Override
+ void applyHighlighting(String query, Pattern queryPattern) {
+ super.applyHighlighting(query, queryPattern);
+
+ preference.setSummary(highlightSearchQuery(originalSummary, queryPattern));
+ }
+
+ @CallSuper
+ void clearHighlighting() {
+ if (highlightingApplied) {
+ preference.setSummary(originalSummary);
+ }
+
+ super.clearHighlighting();
+ }
+}
+
+/**
+ * Switch preference type that uses summaryOn and summaryOff.
+ */
+@SuppressWarnings("deprecation")
+class SwitchPreferenceSearchData extends AbstractPreferenceSearchData {
+ @Nullable
+ CharSequence originalSummaryOn, originalSummaryOff;
+ @Nullable
+ String searchSummaryOn, searchSummaryOff;
+
+ SwitchPreferenceSearchData(SwitchPreference pref) {
+ super(pref);
+ }
+
+ void updateSearchDataIfNeeded() {
+ super.updateSearchDataIfNeeded();
+
+ CharSequence summaryOn = preference.getSummaryOn();
+ if (originalSummaryOn != summaryOn) {
+ originalSummaryOn = summaryOn;
+ searchSummaryOn = Utils.removePunctuationToLowercase(summaryOn);
+ }
+
+ CharSequence summaryOff = preference.getSummaryOff();
+ if (originalSummaryOff != summaryOff) {
+ originalSummaryOff = summaryOff;
+ searchSummaryOff = Utils.removePunctuationToLowercase(summaryOff);
+ }
+ }
+
+ boolean matchesSearchQuery(String query) {
+ return super.matchesSearchQuery(query)
+ || searchSummaryOn != null && searchSummaryOn.contains(query)
+ || searchSummaryOff != null && searchSummaryOff.contains(query);
+ }
+
+ @Override
+ void applyHighlighting(String query, Pattern queryPattern) {
+ super.applyHighlighting(query, queryPattern);
+
+ preference.setSummaryOn(highlightSearchQuery(originalSummaryOn, queryPattern));
+ preference.setSummaryOff(highlightSearchQuery(originalSummaryOff, queryPattern));
+ }
+
+ @CallSuper
+ void clearHighlighting() {
+ if (highlightingApplied) {
+ preference.setSummaryOn(originalSummaryOn);
+ preference.setSummaryOff(originalSummaryOff);
+ }
+
+ super.clearHighlighting();
+ }
+}
+
+/**
+ * List preference type that uses entries.
+ */
+@SuppressWarnings("deprecation")
+class ListPreferenceSearchData extends AbstractPreferenceSearchData {
+ @Nullable
+ CharSequence[] originalEntries;
+ @Nullable
+ String searchEntries;
+
+ ListPreferenceSearchData(ListPreference pref) {
+ super(pref);
+ }
+
+ void updateSearchDataIfNeeded() {
+ super.updateSearchDataIfNeeded();
+
+ CharSequence[] entries = preference.getEntries();
+ if (originalEntries != entries) {
+ originalEntries = entries;
+ searchEntries = Utils.removePunctuationToLowercase(String.join(" ", entries));
+ }
+ }
+
+ boolean matchesSearchQuery(String query) {
+ return super.matchesSearchQuery(query)
+ || searchEntries != null && searchEntries.contains(query);
+ }
+
+ @Override
+ void applyHighlighting(String query, Pattern queryPattern) {
+ super.applyHighlighting(query, queryPattern);
+
+ if (originalEntries != null) {
+ final int length = originalEntries.length;
+ CharSequence[] highlightedEntries = new CharSequence[length];
+
+ for (int i = 0; i < length; i++) {
+ highlightedEntries[i] = highlightSearchQuery(originalEntries[i], queryPattern);
+
+ // Cannot highlight the summary text, because ListPreference uses
+ // the toString() of the summary CharSequence which strips away all formatting.
+ }
+
+ preference.setEntries(highlightedEntries);
+ }
+ }
+
+ @CallSuper
+ void clearHighlighting() {
+ if (highlightingApplied) {
+ preference.setEntries(originalEntries);
+ }
+
+ super.clearHighlighting();
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java
deleted file mode 100644
index bb62386ac..000000000
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java
+++ /dev/null
@@ -1,257 +0,0 @@
-package app.revanced.extension.youtube.settings.preference;
-
-import static app.revanced.extension.shared.StringRef.str;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.preference.Preference;
-import android.preference.PreferenceCategory;
-import android.preference.PreferenceFragment;
-import android.preference.PreferenceManager;
-import android.preference.PreferenceScreen;
-import android.preference.SwitchPreference;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.Setting;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.youtube.patches.ReturnYouTubeDislikePatch;
-import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
-import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
-import app.revanced.extension.youtube.settings.Settings;
-
-/** @noinspection deprecation*/
-public class ReturnYouTubeDislikePreferenceFragment extends PreferenceFragment {
-
- /**
- * If dislikes are shown on Shorts.
- */
- private SwitchPreference shortsPreference;
-
- /**
- * If dislikes are shown as percentage.
- */
- private SwitchPreference percentagePreference;
-
- /**
- * If segmented like/dislike button uses smaller compact layout.
- */
- private SwitchPreference compactLayoutPreference;
-
- /**
- * If hidden likes are replaced with an estimated value.
- */
- private SwitchPreference estimatedLikesPreference;
-
- /**
- * If segmented like/dislike button uses smaller compact layout.
- */
- private SwitchPreference toastOnRYDNotAvailable;
-
- private void updateUIState() {
- shortsPreference.setEnabled(Settings.RYD_SHORTS.isAvailable());
- percentagePreference.setEnabled(Settings.RYD_DISLIKE_PERCENTAGE.isAvailable());
- compactLayoutPreference.setEnabled(Settings.RYD_COMPACT_LAYOUT.isAvailable());
- estimatedLikesPreference.setEnabled(Settings.RYD_ESTIMATED_LIKE.isAvailable());
- toastOnRYDNotAvailable.setEnabled(Settings.RYD_TOAST_ON_CONNECTION_ERROR.isAvailable());
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- try {
- Activity context = getActivity();
- PreferenceManager manager = getPreferenceManager();
- manager.setSharedPreferencesName(Setting.preferences.name);
- PreferenceScreen preferenceScreen = manager.createPreferenceScreen(context);
- setPreferenceScreen(preferenceScreen);
-
- SwitchPreference enabledPreference = new SwitchPreference(context);
- enabledPreference.setChecked(Settings.RYD_ENABLED.get());
- enabledPreference.setTitle(str("revanced_ryd_enable_title"));
- enabledPreference.setSummaryOn(str("revanced_ryd_enable_summary_on"));
- enabledPreference.setSummaryOff(str("revanced_ryd_enable_summary_off"));
- enabledPreference.setOnPreferenceChangeListener((pref, newValue) -> {
- final Boolean rydIsEnabled = (Boolean) newValue;
- Settings.RYD_ENABLED.save(rydIsEnabled);
- ReturnYouTubeDislikePatch.onRYDStatusChange(rydIsEnabled);
-
- updateUIState();
- return true;
- });
- preferenceScreen.addPreference(enabledPreference);
-
- shortsPreference = new SwitchPreference(context);
- shortsPreference.setChecked(Settings.RYD_SHORTS.get());
- shortsPreference.setTitle(str("revanced_ryd_shorts_title"));
- String shortsSummary = str("revanced_ryd_shorts_summary_on_disclaimer");
- shortsPreference.setSummaryOn(shortsSummary);
- shortsPreference.setSummaryOff(str("revanced_ryd_shorts_summary_off"));
- shortsPreference.setOnPreferenceChangeListener((pref, newValue) -> {
- Settings.RYD_SHORTS.save((Boolean) newValue);
- updateUIState();
- return true;
- });
- preferenceScreen.addPreference(shortsPreference);
-
- percentagePreference = new SwitchPreference(context);
- percentagePreference.setChecked(Settings.RYD_DISLIKE_PERCENTAGE.get());
- percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title"));
- percentagePreference.setSummaryOn(str("revanced_ryd_dislike_percentage_summary_on"));
- percentagePreference.setSummaryOff(str("revanced_ryd_dislike_percentage_summary_off"));
- percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> {
- Settings.RYD_DISLIKE_PERCENTAGE.save((Boolean) newValue);
- ReturnYouTubeDislike.clearAllUICaches();
- updateUIState();
- return true;
- });
- preferenceScreen.addPreference(percentagePreference);
-
- compactLayoutPreference = new SwitchPreference(context);
- compactLayoutPreference.setChecked(Settings.RYD_COMPACT_LAYOUT.get());
- compactLayoutPreference.setTitle(str("revanced_ryd_compact_layout_title"));
- compactLayoutPreference.setSummaryOn(str("revanced_ryd_compact_layout_summary_on"));
- compactLayoutPreference.setSummaryOff(str("revanced_ryd_compact_layout_summary_off"));
- compactLayoutPreference.setOnPreferenceChangeListener((pref, newValue) -> {
- Settings.RYD_COMPACT_LAYOUT.save((Boolean) newValue);
- ReturnYouTubeDislike.clearAllUICaches();
- updateUIState();
- return true;
- });
- preferenceScreen.addPreference(compactLayoutPreference);
-
- estimatedLikesPreference = new SwitchPreference(context);
- estimatedLikesPreference.setChecked(Settings.RYD_ESTIMATED_LIKE.get());
- estimatedLikesPreference.setTitle(str("revanced_ryd_estimated_like_title"));
- estimatedLikesPreference.setSummaryOn(str("revanced_ryd_estimated_like_summary_on"));
- estimatedLikesPreference.setSummaryOff(str("revanced_ryd_estimated_like_summary_off"));
- estimatedLikesPreference.setOnPreferenceChangeListener((pref, newValue) -> {
- Settings.RYD_ESTIMATED_LIKE.save((Boolean) newValue);
- ReturnYouTubeDislike.clearAllUICaches();
- updateUIState();
- return true;
- });
- preferenceScreen.addPreference(estimatedLikesPreference);
-
- toastOnRYDNotAvailable = new SwitchPreference(context);
- toastOnRYDNotAvailable.setChecked(Settings.RYD_TOAST_ON_CONNECTION_ERROR.get());
- toastOnRYDNotAvailable.setTitle(str("revanced_ryd_toast_on_connection_error_title"));
- toastOnRYDNotAvailable.setSummaryOn(str("revanced_ryd_toast_on_connection_error_summary_on"));
- toastOnRYDNotAvailable.setSummaryOff(str("revanced_ryd_toast_on_connection_error_summary_off"));
- toastOnRYDNotAvailable.setOnPreferenceChangeListener((pref, newValue) -> {
- Settings.RYD_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue);
- updateUIState();
- return true;
- });
- preferenceScreen.addPreference(toastOnRYDNotAvailable);
-
- updateUIState();
-
-
- // About category
-
- PreferenceCategory aboutCategory = new PreferenceCategory(context);
- aboutCategory.setTitle(str("revanced_ryd_about"));
- preferenceScreen.addPreference(aboutCategory);
-
- // ReturnYouTubeDislike Website
-
- Preference aboutWebsitePreference = new Preference(context);
- aboutWebsitePreference.setTitle(str("revanced_ryd_attribution_title"));
- aboutWebsitePreference.setSummary(str("revanced_ryd_attribution_summary"));
- aboutWebsitePreference.setOnPreferenceClickListener(pref -> {
- Intent i = new Intent(Intent.ACTION_VIEW);
- i.setData(Uri.parse("https://returnyoutubedislike.com"));
- pref.getContext().startActivity(i);
- return false;
- });
- aboutCategory.addPreference(aboutWebsitePreference);
-
- // RYD API connection statistics
-
- if (BaseSettings.DEBUG.get()) {
- PreferenceCategory emptyCategory = new PreferenceCategory(context); // vertical padding
- preferenceScreen.addPreference(emptyCategory);
-
- PreferenceCategory statisticsCategory = new PreferenceCategory(context);
- statisticsCategory.setTitle(str("revanced_ryd_statistics_category_title"));
- preferenceScreen.addPreference(statisticsCategory);
-
- Preference statisticPreference;
-
- statisticPreference = new Preference(context);
- statisticPreference.setSelectable(false);
- statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeAverage_title"));
- statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage()));
- preferenceScreen.addPreference(statisticPreference);
-
- statisticPreference = new Preference(context);
- statisticPreference.setSelectable(false);
- statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMin_title"));
- statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin()));
- preferenceScreen.addPreference(statisticPreference);
-
- statisticPreference = new Preference(context);
- statisticPreference.setSelectable(false);
- statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMax_title"));
- statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax()));
- preferenceScreen.addPreference(statisticPreference);
-
- String fetchCallTimeWaitingLastSummary;
- final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast();
- if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) {
- fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary");
- } else {
- fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast);
- }
- statisticPreference = new Preference(context);
- statisticPreference.setSelectable(false);
- statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeLast_title"));
- statisticPreference.setSummary(fetchCallTimeWaitingLastSummary);
- preferenceScreen.addPreference(statisticPreference);
-
- statisticPreference = new Preference(context);
- statisticPreference.setSelectable(false);
- statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallCount_title"));
- statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(),
- "revanced_ryd_statistics_getFetchCallCount_zero_summary",
- "revanced_ryd_statistics_getFetchCallCount_non_zero_summary"));
- preferenceScreen.addPreference(statisticPreference);
-
- statisticPreference = new Preference(context);
- statisticPreference.setSelectable(false);
- statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallNumberOfFailures_title"));
- statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(),
- "revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary",
- "revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary"));
- preferenceScreen.addPreference(statisticPreference);
-
- statisticPreference = new Preference(context);
- statisticPreference.setSelectable(false);
- statisticPreference.setTitle(str("revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title"));
- statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(),
- "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary",
- "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary"));
- preferenceScreen.addPreference(statisticPreference);
- }
-
- Utils.setPreferenceTitlesToMultiLineIfNeeded(preferenceScreen);
- } catch (Exception ex) {
- Logger.printException(() -> "onCreate failure", ex);
- }
- }
-
- private static String createSummaryText(int value, String summaryStringZeroKey, String summaryStringOneOrMoreKey) {
- if (value == 0) {
- return str(summaryStringZeroKey);
- }
- return String.format(str(summaryStringOneOrMoreKey), value);
- }
-
- private static String createMillisecondStringFromNumber(long number) {
- return String.format(str("revanced_ryd_statistics_millisecond_text"), number);
- }
-
-}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockPreferenceFragment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockPreferenceFragment.java
deleted file mode 100644
index 751125274..000000000
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockPreferenceFragment.java
+++ /dev/null
@@ -1,629 +0,0 @@
-package app.revanced.extension.youtube.settings.preference;
-
-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.os.Bundle;
-import android.preference.*;
-import android.text.Html;
-import android.text.InputType;
-import android.util.TypedValue;
-import android.widget.EditText;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.Setting;
-import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
-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.SegmentCategoryListPreference;
-import app.revanced.extension.youtube.sponsorblock.objects.UserStats;
-import app.revanced.extension.youtube.sponsorblock.requests.SBRequester;
-import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController;
-
-import static android.text.Html.fromHtml;
-import static app.revanced.extension.shared.StringRef.str;
-
-@SuppressWarnings("deprecation")
-public class SponsorBlockPreferenceFragment extends PreferenceFragment {
-
- private SwitchPreference sbEnabled;
- private SwitchPreference addNewSegment;
- private SwitchPreference votingEnabled;
- private SwitchPreference autoHideSkipSegmentButton;
- private SwitchPreference compactSkipButton;
- private SwitchPreference squareLayout;
- private SwitchPreference showSkipToast;
- private SwitchPreference trackSkips;
- private SwitchPreference showTimeWithoutSegments;
- private SwitchPreference toastOnConnectionError;
-
- private ResettableEditTextPreference newSegmentStep;
- private ResettableEditTextPreference minSegmentDuration;
- private EditTextPreference privateUserId;
- private EditTextPreference importExport;
- private Preference apiUrl;
-
- private PreferenceCategory statsCategory;
- private PreferenceCategory segmentCategory;
-
- private void updateUI() {
- try {
- final boolean enabled = Settings.SB_ENABLED.get();
- if (!enabled) {
- SponsorBlockViewController.hideAll();
- SegmentPlaybackController.setCurrentVideoId(null);
- } else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) {
- SponsorBlockViewController.hideNewSegmentLayout();
- }
- // Voting and add new segment buttons automatically show/hide themselves.
-
- SponsorBlockViewController.updateLayout();
-
- sbEnabled.setChecked(enabled);
-
- addNewSegment.setChecked(Settings.SB_CREATE_NEW_SEGMENT.get());
- addNewSegment.setEnabled(enabled);
-
- votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get());
- votingEnabled.setEnabled(enabled);
-
- autoHideSkipSegmentButton.setEnabled(enabled);
- autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get());
-
- compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get());
- compactSkipButton.setEnabled(enabled);
-
- squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get());
- squareLayout.setEnabled(enabled);
-
- showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get());
- showSkipToast.setEnabled(enabled);
-
- toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get());
- toastOnConnectionError.setEnabled(enabled);
-
- trackSkips.setChecked(Settings.SB_TRACK_SKIP_COUNT.get());
- trackSkips.setEnabled(enabled);
-
- showTimeWithoutSegments.setChecked(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get());
- showTimeWithoutSegments.setEnabled(enabled);
-
- newSegmentStep.setText((Settings.SB_CREATE_NEW_SEGMENT_STEP.get()).toString());
- newSegmentStep.setEnabled(enabled);
-
- minSegmentDuration.setText((Settings.SB_SEGMENT_MIN_DURATION.get()).toString());
- minSegmentDuration.setEnabled(enabled);
-
- privateUserId.setText(Settings.SB_PRIVATE_USER_ID.get());
- privateUserId.setEnabled(enabled);
-
- // 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");
- importExport.setSummary(importExportSummary);
-
- apiUrl.setEnabled(enabled);
- importExport.setEnabled(enabled);
- segmentCategory.setEnabled(enabled);
- statsCategory.setEnabled(enabled);
- } catch (Exception ex) {
- Logger.printException(() -> "update settings UI failure", ex);
- }
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- try {
- Activity context = getActivity();
- PreferenceManager manager = getPreferenceManager();
- manager.setSharedPreferencesName(Setting.preferences.name);
- PreferenceScreen preferenceScreen = manager.createPreferenceScreen(context);
- setPreferenceScreen(preferenceScreen);
-
- SponsorBlockSettings.initialize();
-
- sbEnabled = new SwitchPreference(context);
- sbEnabled.setTitle(str("revanced_sb_enable_sb"));
- sbEnabled.setSummary(str("revanced_sb_enable_sb_sum"));
- preferenceScreen.addPreference(sbEnabled);
- sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
- Settings.SB_ENABLED.save((Boolean) newValue);
- updateUI();
- return true;
- });
-
- addAppearanceCategory(context, preferenceScreen);
-
- segmentCategory = new PreferenceCategory(context);
- segmentCategory.setTitle(str("revanced_sb_diff_segments"));
- preferenceScreen.addPreference(segmentCategory);
- updateSegmentCategories();
-
- addCreateSegmentCategory(context, preferenceScreen);
-
- addGeneralCategory(context, preferenceScreen);
-
- statsCategory = new PreferenceCategory(context);
- statsCategory.setTitle(str("revanced_sb_stats"));
- preferenceScreen.addPreference(statsCategory);
- fetchAndDisplayStats();
-
- addAboutCategory(context, preferenceScreen);
-
- Utils.setPreferenceTitlesToMultiLineIfNeeded(preferenceScreen);
-
- updateUI();
- } catch (Exception ex) {
- Logger.printException(() -> "onCreate failure", ex);
- }
- }
-
- private void addAppearanceCategory(Context context, PreferenceScreen screen) {
- PreferenceCategory category = new PreferenceCategory(context);
- screen.addPreference(category);
- category.setTitle(str("revanced_sb_appearance_category"));
-
- votingEnabled = new SwitchPreference(context);
- votingEnabled.setTitle(str("revanced_sb_enable_voting"));
- votingEnabled.setSummaryOn(str("revanced_sb_enable_voting_sum_on"));
- votingEnabled.setSummaryOff(str("revanced_sb_enable_voting_sum_off"));
- category.addPreference(votingEnabled);
- votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
- Settings.SB_VOTING_BUTTON.save((Boolean) newValue);
- updateUI();
- return true;
- });
-
- autoHideSkipSegmentButton = new SwitchPreference(context);
- autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button"));
- autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on"));
- autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off"));
- category.addPreference(autoHideSkipSegmentButton);
- autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
- Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
- updateUI();
- return true;
- });
-
- compactSkipButton = new SwitchPreference(context);
- compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button"));
- compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on"));
- compactSkipButton.setSummaryOff(str("revanced_sb_enable_compact_skip_button_sum_off"));
- category.addPreference(compactSkipButton);
- compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> {
- Settings.SB_COMPACT_SKIP_BUTTON.save((Boolean) newValue);
- updateUI();
- return true;
- });
-
- squareLayout = new SwitchPreference(context);
- squareLayout.setTitle(str("revanced_sb_square_layout"));
- squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on"));
- squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off"));
- category.addPreference(squareLayout);
- squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
- Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
- updateUI();
- return true;
- });
-
- showSkipToast = new SwitchPreference(context);
- showSkipToast.setTitle(str("revanced_sb_general_skiptoast"));
- showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on"));
- showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off"));
- showSkipToast.setOnPreferenceClickListener(preference1 -> {
- Utils.showToastShort(str("revanced_sb_skipped_sponsor"));
- return false;
- });
- showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
- Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue);
- updateUI();
- return true;
- });
- category.addPreference(showSkipToast);
-
- showTimeWithoutSegments = new SwitchPreference(context);
- showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without"));
- showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on"));
- showTimeWithoutSegments.setSummaryOff(str("revanced_sb_general_time_without_sum_off"));
- showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> {
- Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save((Boolean) newValue);
- updateUI();
- return true;
- });
- category.addPreference(showTimeWithoutSegments);
- }
-
- private void addCreateSegmentCategory(Context context, PreferenceScreen screen) {
- PreferenceCategory category = new PreferenceCategory(context);
- screen.addPreference(category);
- category.setTitle(str("revanced_sb_create_segment_category"));
-
- addNewSegment = new SwitchPreference(context);
- addNewSegment.setTitle(str("revanced_sb_enable_create_segment"));
- addNewSegment.setSummaryOn(str("revanced_sb_enable_create_segment_sum_on"));
- addNewSegment.setSummaryOff(str("revanced_sb_enable_create_segment_sum_off"));
- category.addPreference(addNewSegment);
- addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
- Boolean newValue = (Boolean) o;
- if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
- new AlertDialog.Builder(preference1.getContext())
- .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())
- .setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true))
- .setCancelable(false)
- .show();
- }
- Settings.SB_CREATE_NEW_SEGMENT.save(newValue);
- updateUI();
- return true;
- });
-
- newSegmentStep = new ResettableEditTextPreference(context);
- newSegmentStep.setSetting(Settings.SB_CREATE_NEW_SEGMENT_STEP);
- newSegmentStep.setTitle(str("revanced_sb_general_adjusting"));
- newSegmentStep.setSummary(str("revanced_sb_general_adjusting_sum"));
- newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
- newSegmentStep.setOnPreferenceChangeListener((preference1, 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;
- });
- category.addPreference(newSegmentStep);
-
- Preference guidelinePreferences = new Preference(context);
- guidelinePreferences.setTitle(str("revanced_sb_guidelines_preference_title"));
- guidelinePreferences.setSummary(str("revanced_sb_guidelines_preference_sum"));
- guidelinePreferences.setOnPreferenceClickListener(preference1 -> {
- openGuidelines();
- return true;
- });
- category.addPreference(guidelinePreferences);
- }
-
- private void addGeneralCategory(final Context context, PreferenceScreen screen) {
- PreferenceCategory category = new PreferenceCategory(context);
- screen.addPreference(category);
- category.setTitle(str("revanced_sb_general"));
-
- toastOnConnectionError = new SwitchPreference(context);
- toastOnConnectionError.setTitle(str("revanced_sb_toast_on_connection_error_title"));
- toastOnConnectionError.setSummaryOn(str("revanced_sb_toast_on_connection_error_summary_on"));
- toastOnConnectionError.setSummaryOff(str("revanced_sb_toast_on_connection_error_summary_off"));
- toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> {
- Settings.SB_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue);
- updateUI();
- return true;
- });
- category.addPreference(toastOnConnectionError);
-
- trackSkips = new SwitchPreference(context);
- trackSkips.setTitle(str("revanced_sb_general_skipcount"));
- trackSkips.setSummaryOn(str("revanced_sb_general_skipcount_sum_on"));
- trackSkips.setSummaryOff(str("revanced_sb_general_skipcount_sum_off"));
- trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> {
- Settings.SB_TRACK_SKIP_COUNT.save((Boolean) newValue);
- updateUI();
- return true;
- });
- category.addPreference(trackSkips);
-
- minSegmentDuration = new ResettableEditTextPreference(context);
- minSegmentDuration.setSetting(Settings.SB_SEGMENT_MIN_DURATION);
- minSegmentDuration.setTitle(str("revanced_sb_general_min_duration"));
- minSegmentDuration.setSummary(str("revanced_sb_general_min_duration_sum"));
- minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
- minSegmentDuration.setOnPreferenceChangeListener((preference1, 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;
- });
- category.addPreference(minSegmentDuration);
-
- privateUserId = new EditTextPreference(context) {
- protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
- Utils.setEditTextDialogTheme(builder);
-
- builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
- Utils.setClipboard(getEditText().getText().toString());
- });
- }
- };
- privateUserId.setTitle(str("revanced_sb_general_uuid"));
- privateUserId.setSummary(str("revanced_sb_general_uuid_sum"));
- privateUserId.setOnPreferenceChangeListener((preference1, 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);
- updateUI();
- fetchAndDisplayStats();
- return true;
- });
- category.addPreference(privateUserId);
-
- apiUrl = new Preference(context);
- apiUrl.setTitle(str("revanced_sb_general_api_url"));
- apiUrl.setSummary(Html.fromHtml(str("revanced_sb_general_api_url_sum")));
- apiUrl.setOnPreferenceClickListener(preference1 -> {
- EditText editText = new EditText(context);
- editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
- editText.setText(Settings.SB_API_URL.get());
-
- 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"));
- }
- }
- };
- new AlertDialog.Builder(context)
- .setTitle(apiUrl.getTitle())
- .setView(editText)
- .setNegativeButton(android.R.string.cancel, null)
- .setNeutralButton(str("revanced_sb_reset"), urlChangeListener)
- .setPositiveButton(android.R.string.ok, urlChangeListener)
- .show();
- return true;
- });
- category.addPreference(apiUrl);
-
- importExport = new EditTextPreference(context) {
- protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
- Utils.setEditTextDialogTheme(builder);
-
- builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
- Utils.setClipboard(getEditText().getText().toString());
- });
- }
- };
- importExport.setTitle(str("revanced_sb_settings_ie"));
- // Summary is set in updateUI()
- importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT
- | InputType.TYPE_TEXT_FLAG_MULTI_LINE
- | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
- importExport.getEditText().setAutofillHints((String) null);
- importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8);
- importExport.setOnPreferenceClickListener(preference1 -> {
- importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings());
- return true;
- });
- importExport.setOnPreferenceChangeListener((preference1, newValue) -> {
- SponsorBlockSettings.importDesktopSettings((String) newValue);
- updateSegmentCategories();
- fetchAndDisplayStats();
- updateUI();
- return true;
- });
- category.addPreference(importExport);
- }
-
- private void updateSegmentCategories() {
- try {
- segmentCategory.removeAll();
-
- Activity activity = getActivity();
- for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
- segmentCategory.addPreference(new SegmentCategoryListPreference(activity, category));
- }
- } catch (Exception ex) {
- Logger.printException(() -> "updateSegmentCategories failure", ex);
- }
- }
-
- private void addAboutCategory(Context context, PreferenceScreen screen) {
- PreferenceCategory category = new PreferenceCategory(context);
- screen.addPreference(category);
- category.setTitle(str("revanced_sb_about"));
-
- {
- Preference preference = new Preference(context);
- category.addPreference(preference);
- preference.setTitle(str("revanced_sb_about_api"));
- preference.setSummary(str("revanced_sb_about_api_sum"));
- preference.setOnPreferenceClickListener(preference1 -> {
- Intent i = new Intent(Intent.ACTION_VIEW);
- i.setData(Uri.parse("https://sponsor.ajay.app"));
- preference1.getContext().startActivity(i);
- return false;
- });
- }
- }
-
- private void openGuidelines() {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
- getActivity().startActivity(intent);
- }
-
- private void fetchAndDisplayStats() {
- try {
- statsCategory.removeAll();
- if (!SponsorBlockSettings.userHasSBPrivateId()) {
- // User has never voted or created any segments. No stats to show.
- addLocalUserStats();
- return;
- }
-
- Preference loadingPlaceholderPreference = new Preference(this.getActivity());
- 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 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.
- EditTextPreference 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
- SponsorBlockUtils.showErrorDialog(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 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;
- });
- }
-
-}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/UrlLinkPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/UrlLinkPreference.java
new file mode 100644
index 000000000..9570883cb
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/UrlLinkPreference.java
@@ -0,0 +1,44 @@
+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;
+
+import app.revanced.extension.shared.Logger;
+
+/**
+ * Simple preference that opens a url when clicked.
+ */
+@SuppressWarnings("deprecation")
+public class UrlLinkPreference extends Preference {
+
+ protected String externalUrl;
+
+ {
+ setOnPreferenceClickListener(pref -> {
+ if (externalUrl == null) {
+ Logger.printException(() -> "URL not set " + getClass().getSimpleName());
+ return false;
+ }
+ Intent i = new Intent(Intent.ACTION_VIEW);
+ i.setData(Uri.parse(externalUrl));
+ pref.getContext().startActivity(i);
+ return true;
+ });
+ }
+
+ public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+ public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+ public UrlLinkPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ public UrlLinkPreference(Context context) {
+ super(context);
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java
index e8d64fb50..d03bde3bb 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java
@@ -18,6 +18,7 @@ import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
@@ -31,6 +32,7 @@ public class SponsorBlockSettings {
@Override
public void settingsImported(@Nullable Context context) {
SegmentCategory.loadAllCategoriesFromSettings();
+ SponsorBlockPreferenceGroup.settingsImported = true;
}
@Override
public void settingsExported(@Nullable Context context) {
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java
index ae92caaff..36204319c 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java
@@ -53,9 +53,9 @@ public class SegmentCategoryListPreference extends ListPreference {
setEntryValues(isHighlightCategory
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
: CategoryBehaviour.getBehaviorKeyValues());
- setSummary(category.description.toString());
+ super.setSummary(category.description.toString());
- updateTitleFromCategory();
+ updateUI();
}
@Override
@@ -202,7 +202,7 @@ public class SegmentCategoryListPreference extends ListPreference {
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
try {
category.resetColorAndOpacity();
- updateTitleFromCategory();
+ updateUI();
Utils.showToastShort(str("revanced_sb_color_reset"));
} catch (Exception ex) {
Logger.printException(() -> "setNeutralButton failure", ex);
@@ -240,7 +240,7 @@ public class SegmentCategoryListPreference extends ListPreference {
Utils.showToastShort(str("revanced_sb_color_invalid"));
}
- updateTitleFromCategory();
+ updateUI();
}
} catch (Exception ex) {
Logger.printException(() -> "onDialogClosed failure", ex);
@@ -251,7 +251,7 @@ public class SegmentCategoryListPreference extends ListPreference {
categoryColor = applyOpacityToColor(categoryColor, categoryOpacity);
}
- private void updateTitleFromCategory() {
+ public void updateUI() {
categoryColor = category.getColorNoOpacity();
categoryOpacity = category.getOpacity();
applyOpacityToCategoryColor();
@@ -268,4 +268,13 @@ public class SegmentCategoryListPreference extends ListPreference {
private void updateOpacityText() {
opacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity));
}
+
+ @Override
+ public void setSummary(CharSequence summary) {
+ // Ignore calls to set the summary.
+ // Summary is always the description of the category.
+ //
+ // This is required otherwise the ReVanced preference fragment
+ // sets all ListPreference summaries to show the current selection.
+ }
}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java
index 4889c7671..ff0eaffcf 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java
@@ -5,13 +5,19 @@ import androidx.annotation.NonNull;
import org.json.JSONException;
import org.json.JSONObject;
+import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
+
/**
* SponsorBlock user stats
*/
public class UserStats {
- @NonNull
+ /**
+ * How long to cache user stats objects.
+ */
+ private static final long STATS_EXPIRATION_MILLISECONDS = 60 * 60 * 1000; // 60 minutes.
+
+ private final String privateUserId;
public final String publicUserId;
- @NonNull
public final String userName;
/**
* "User reputation". Unclear how SB determines this value.
@@ -26,7 +32,13 @@ public class UserStats {
public final int viewCount;
public final double minutesSaved;
- public UserStats(@NonNull JSONObject json) throws JSONException {
+ /**
+ * When this stat was fetched.
+ */
+ public final long fetchTime;
+
+ public UserStats(String privateSbId, @NonNull JSONObject json) throws JSONException {
+ privateUserId = privateSbId;
publicUserId = json.getString("userID");
userName = json.getString("userName");
reputation = (float)json.getDouble("reputation");
@@ -35,11 +47,23 @@ public class UserStats {
totalSegmentCountIncludingIgnored = segmentCount + ignoredSegmentCount;
viewCount = json.getInt("viewCount");
minutesSaved = json.getDouble("minutesSaved");
+ fetchTime = System.currentTimeMillis();
+ }
+
+ public boolean isExpired() {
+ if (STATS_EXPIRATION_MILLISECONDS < System.currentTimeMillis() - fetchTime) {
+ return true;
+ }
+
+ // User changed their SB private user id.
+ return !SponsorBlockSettings.userHasSBPrivateId()
+ || !SponsorBlockSettings.getSBPrivateUserID().equals(privateUserId);
}
@NonNull
@Override
public String toString() {
+ // Do not include private user id in toString().
return "UserStats{"
+ "publicUserId='" + publicUserId + '\''
+ ", userName='" + userName + '\''
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java
index 445b711dc..fea7664f1 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java
@@ -47,6 +47,9 @@ public class SBRequester {
*/
private static final int HTTP_STATUS_CODE_SUCCESS = 200;
+ @Nullable
+ private static volatile UserStats lastFetchedStats;
+
private SBRequester() {
}
@@ -181,6 +184,8 @@ public class SBRequester {
Utils.showToastLong(str("revanced_sb_submit_failed_unknown_error", 0, ex.getMessage()));
} catch (Exception ex) {
Logger.printException(() -> "failed to submit segments", ex); // Should never happen.
+ } finally {
+ lastFetchedStats = null; // Fetch updated stats if needed.
}
}
@@ -252,9 +257,17 @@ public class SBRequester {
public static UserStats retrieveUserStats() {
Utils.verifyOffMainThread();
try {
- UserStats stats = new UserStats(getJSONObject(SBRoutes.GET_USER_STATS, SponsorBlockSettings.getSBPrivateUserID()));
- Logger.printDebug(() -> "user stats: " + stats);
- return stats;
+ UserStats stats = lastFetchedStats;
+ if (stats != null && !stats.isExpired()) {
+ return stats;
+ }
+
+ String privateUserID = SponsorBlockSettings.getSBPrivateUserID();
+ UserStats fetchedStats = new UserStats(privateUserID,
+ getJSONObject(SBRoutes.GET_USER_STATS, privateUserID));
+ Logger.printDebug(() -> "user stats: " + fetchedStats);
+ lastFetchedStats = fetchedStats;
+ return fetchedStats;
} catch (IOException ex) {
Logger.printInfo(() -> "failed to retrieve user stats", ex); // info level, do not show a toast
} catch (Exception ex) {
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java
new file mode 100644
index 000000000..098f8d599
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java
@@ -0,0 +1,26 @@
+package app.revanced.extension.youtube.sponsorblock.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import app.revanced.extension.youtube.settings.preference.UrlLinkPreference;
+
+@SuppressWarnings("unused")
+public class SponsorBlockAboutPreference extends UrlLinkPreference {
+ {
+ externalUrl = "https://sponsor.ajay.app";
+ }
+
+ public SponsorBlockAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+ public SponsorBlockAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+ public SponsorBlockAboutPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ public SponsorBlockAboutPreference(Context context) {
+ super(context);
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java
new file mode 100644
index 000000000..4ed3bf238
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java
@@ -0,0 +1,471 @@
+package app.revanced.extension.youtube.sponsorblock.ui;
+
+import static app.revanced.extension.shared.StringRef.str;
+
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.preference.*;
+import android.text.Html;
+import android.text.InputType;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
+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.objects.SegmentCategory;
+import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategoryListPreference;
+
+/**
+ * Lots of old code that could be converted to a half dozen custom preferences,
+ * but instead it's wrapped in this group container and all logic is handled here.
+ */
+@SuppressWarnings({"unused", "deprecation"})
+public class SponsorBlockPreferenceGroup extends PreferenceGroup {
+
+ /**
+ * ReVanced settings were recently imported and the UI needs to be updated.
+ */
+ public static boolean settingsImported;
+
+ /**
+ * If the preferences have been created and added to this group.
+ */
+ private boolean preferencesInitialized;
+
+ private SwitchPreference sbEnabled;
+ private SwitchPreference addNewSegment;
+ private SwitchPreference votingEnabled;
+ private SwitchPreference autoHideSkipSegmentButton;
+ private SwitchPreference compactSkipButton;
+ private SwitchPreference squareLayout;
+ private SwitchPreference showSkipToast;
+ private SwitchPreference trackSkips;
+ private SwitchPreference showTimeWithoutSegments;
+ private SwitchPreference toastOnConnectionError;
+
+ private ResettableEditTextPreference newSegmentStep;
+ private ResettableEditTextPreference minSegmentDuration;
+ private EditTextPreference privateUserId;
+ private EditTextPreference importExport;
+ private Preference apiUrl;
+
+ private final List segmentCategories = new ArrayList<>();
+ private PreferenceCategory segmentCategory;
+
+ public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ @SuppressLint("MissingSuperCall")
+ protected View onCreateView(ViewGroup parent) {
+ // Title is not shown.
+ return new View(getContext());
+ }
+
+ private void updateUI() {
+ try {
+ Logger.printDebug(() -> "updateUI");
+
+ final boolean enabled = Settings.SB_ENABLED.get();
+ if (!enabled) {
+ SponsorBlockViewController.hideAll();
+ SegmentPlaybackController.setCurrentVideoId(null);
+ } else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) {
+ SponsorBlockViewController.hideNewSegmentLayout();
+ }
+ // Voting and add new segment buttons automatically show/hide themselves.
+
+ SponsorBlockViewController.updateLayout();
+
+ sbEnabled.setChecked(enabled);
+
+ addNewSegment.setChecked(Settings.SB_CREATE_NEW_SEGMENT.get());
+ addNewSegment.setEnabled(enabled);
+
+ votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get());
+ votingEnabled.setEnabled(enabled);
+
+ autoHideSkipSegmentButton.setEnabled(enabled);
+ autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get());
+
+ compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get());
+ compactSkipButton.setEnabled(enabled);
+
+ squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get());
+ squareLayout.setEnabled(enabled);
+
+ showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get());
+ showSkipToast.setEnabled(enabled);
+
+ toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get());
+ toastOnConnectionError.setEnabled(enabled);
+
+ trackSkips.setChecked(Settings.SB_TRACK_SKIP_COUNT.get());
+ trackSkips.setEnabled(enabled);
+
+ showTimeWithoutSegments.setChecked(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get());
+ showTimeWithoutSegments.setEnabled(enabled);
+
+ newSegmentStep.setText((Settings.SB_CREATE_NEW_SEGMENT_STEP.get()).toString());
+ newSegmentStep.setEnabled(enabled);
+
+ minSegmentDuration.setText((Settings.SB_SEGMENT_MIN_DURATION.get()).toString());
+ minSegmentDuration.setEnabled(enabled);
+
+ privateUserId.setText(Settings.SB_PRIVATE_USER_ID.get());
+ privateUserId.setEnabled(enabled);
+
+ // 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");
+ importExport.setSummary(importExportSummary);
+
+ apiUrl.setEnabled(enabled);
+ importExport.setEnabled(enabled);
+ segmentCategory.setEnabled(enabled);
+
+ for (SegmentCategoryListPreference category : segmentCategories) {
+ category.updateUI();
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "updateUI failure", ex);
+ }
+ }
+
+ protected void onAttachedToActivity() {
+ try {
+ super.onAttachedToActivity();
+
+ if (preferencesInitialized) {
+ if (settingsImported) {
+ settingsImported = false;
+ updateUI();
+ }
+ return;
+ }
+
+ preferencesInitialized = true;
+
+ Logger.printDebug(() -> "Creating settings preferences");
+ Context context = getContext();
+ SponsorBlockSettings.initialize();
+
+ sbEnabled = new SwitchPreference(context);
+ sbEnabled.setTitle(str("revanced_sb_enable_sb"));
+ sbEnabled.setSummary(str("revanced_sb_enable_sb_sum"));
+ addPreference(sbEnabled);
+ sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
+ Settings.SB_ENABLED.save((Boolean) newValue);
+ updateUI();
+ return true;
+ });
+
+ PreferenceCategory appearanceCategory = new PreferenceCategory(context);
+ appearanceCategory.setTitle(str("revanced_sb_appearance_category"));
+ addPreference(appearanceCategory);
+
+ votingEnabled = new SwitchPreference(context);
+ votingEnabled.setTitle(str("revanced_sb_enable_voting"));
+ votingEnabled.setSummaryOn(str("revanced_sb_enable_voting_sum_on"));
+ votingEnabled.setSummaryOff(str("revanced_sb_enable_voting_sum_off"));
+ votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
+ Settings.SB_VOTING_BUTTON.save((Boolean) newValue);
+ updateUI();
+ return true;
+ });
+ appearanceCategory.addPreference(votingEnabled);
+
+ autoHideSkipSegmentButton = new SwitchPreference(context);
+ autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button"));
+ autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on"));
+ autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off"));
+ autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
+ Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
+ updateUI();
+ return true;
+ });
+ appearanceCategory.addPreference(autoHideSkipSegmentButton);
+
+ compactSkipButton = new SwitchPreference(context);
+ compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button"));
+ compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on"));
+ compactSkipButton.setSummaryOff(str("revanced_sb_enable_compact_skip_button_sum_off"));
+ compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> {
+ Settings.SB_COMPACT_SKIP_BUTTON.save((Boolean) newValue);
+ updateUI();
+ return true;
+ });
+ appearanceCategory.addPreference(compactSkipButton);
+
+ squareLayout = new SwitchPreference(context);
+ squareLayout.setTitle(str("revanced_sb_square_layout"));
+ squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on"));
+ squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off"));
+ squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
+ Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
+ updateUI();
+ return true;
+ });
+ appearanceCategory.addPreference(squareLayout);
+
+ showSkipToast = new SwitchPreference(context);
+ showSkipToast.setTitle(str("revanced_sb_general_skiptoast"));
+ showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on"));
+ showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off"));
+ showSkipToast.setOnPreferenceClickListener(preference1 -> {
+ Utils.showToastShort(str("revanced_sb_skipped_sponsor"));
+ return false;
+ });
+ showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
+ Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue);
+ updateUI();
+ return true;
+ });
+ appearanceCategory.addPreference(showSkipToast);
+
+ showTimeWithoutSegments = new SwitchPreference(context);
+ showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without"));
+ showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on"));
+ showTimeWithoutSegments.setSummaryOff(str("revanced_sb_general_time_without_sum_off"));
+ showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> {
+ Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save((Boolean) newValue);
+ updateUI();
+ return true;
+ });
+ appearanceCategory.addPreference(showTimeWithoutSegments);
+
+ segmentCategory = new PreferenceCategory(context);
+ segmentCategory.setTitle(str("revanced_sb_diff_segments"));
+ addPreference(segmentCategory);
+
+ for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
+ SegmentCategoryListPreference categoryPreference = new SegmentCategoryListPreference(context, category);
+ segmentCategories.add(categoryPreference);
+ segmentCategory.addPreference(categoryPreference);
+ }
+
+ PreferenceCategory createSegmentCategory = new PreferenceCategory(context);
+ createSegmentCategory.setTitle(str("revanced_sb_create_segment_category"));
+ addPreference(createSegmentCategory);
+
+ addNewSegment = new SwitchPreference(context);
+ addNewSegment.setTitle(str("revanced_sb_enable_create_segment"));
+ addNewSegment.setSummaryOn(str("revanced_sb_enable_create_segment_sum_on"));
+ addNewSegment.setSummaryOff(str("revanced_sb_enable_create_segment_sum_off"));
+ addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
+ Boolean newValue = (Boolean) o;
+ if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
+ new AlertDialog.Builder(preference1.getContext())
+ .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())
+ .setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true))
+ .setCancelable(false)
+ .show();
+ }
+ Settings.SB_CREATE_NEW_SEGMENT.save(newValue);
+ updateUI();
+ return true;
+ });
+ createSegmentCategory.addPreference(addNewSegment);
+
+ newSegmentStep = new ResettableEditTextPreference(context);
+ newSegmentStep.setSetting(Settings.SB_CREATE_NEW_SEGMENT_STEP);
+ newSegmentStep.setTitle(str("revanced_sb_general_adjusting"));
+ newSegmentStep.setSummary(str("revanced_sb_general_adjusting_sum"));
+ newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
+ newSegmentStep.setOnPreferenceChangeListener((preference1, 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;
+ });
+ createSegmentCategory.addPreference(newSegmentStep);
+
+ Preference guidelinePreferences = new Preference(context);
+ guidelinePreferences.setTitle(str("revanced_sb_guidelines_preference_title"));
+ guidelinePreferences.setSummary(str("revanced_sb_guidelines_preference_sum"));
+ guidelinePreferences.setOnPreferenceClickListener(preference1 -> {
+ openGuidelines();
+ return true;
+ });
+ createSegmentCategory.addPreference(guidelinePreferences);
+
+ PreferenceCategory generalCategory = new PreferenceCategory(context);
+ generalCategory.setTitle(str("revanced_sb_general"));
+ addPreference(generalCategory);
+
+ toastOnConnectionError = new SwitchPreference(context);
+ toastOnConnectionError.setTitle(str("revanced_sb_toast_on_connection_error_title"));
+ toastOnConnectionError.setSummaryOn(str("revanced_sb_toast_on_connection_error_summary_on"));
+ toastOnConnectionError.setSummaryOff(str("revanced_sb_toast_on_connection_error_summary_off"));
+ toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> {
+ Settings.SB_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue);
+ updateUI();
+ return true;
+ });
+ generalCategory.addPreference(toastOnConnectionError);
+
+ trackSkips = new SwitchPreference(context);
+ trackSkips.setTitle(str("revanced_sb_general_skipcount"));
+ trackSkips.setSummaryOn(str("revanced_sb_general_skipcount_sum_on"));
+ trackSkips.setSummaryOff(str("revanced_sb_general_skipcount_sum_off"));
+ trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> {
+ Settings.SB_TRACK_SKIP_COUNT.save((Boolean) newValue);
+ updateUI();
+ return true;
+ });
+ generalCategory.addPreference(trackSkips);
+
+ minSegmentDuration = new ResettableEditTextPreference(context);
+ minSegmentDuration.setSetting(Settings.SB_SEGMENT_MIN_DURATION);
+ minSegmentDuration.setTitle(str("revanced_sb_general_min_duration"));
+ minSegmentDuration.setSummary(str("revanced_sb_general_min_duration_sum"));
+ minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
+ minSegmentDuration.setOnPreferenceChangeListener((preference1, 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;
+ });
+ generalCategory.addPreference(minSegmentDuration);
+
+ privateUserId = new EditTextPreference(context) {
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ Utils.setEditTextDialogTheme(builder);
+
+ builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
+ Utils.setClipboard(getEditText().getText().toString());
+ });
+ }
+ };
+ privateUserId.setTitle(str("revanced_sb_general_uuid"));
+ privateUserId.setSummary(str("revanced_sb_general_uuid_sum"));
+ privateUserId.setOnPreferenceChangeListener((preference1, 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);
+ updateUI();
+ return true;
+ });
+ generalCategory.addPreference(privateUserId);
+
+ apiUrl = new Preference(context);
+ apiUrl.setTitle(str("revanced_sb_general_api_url"));
+ apiUrl.setSummary(Html.fromHtml(str("revanced_sb_general_api_url_sum")));
+ apiUrl.setOnPreferenceClickListener(preference1 -> {
+ EditText editText = new EditText(context);
+ editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
+ editText.setText(Settings.SB_API_URL.get());
+
+ 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"));
+ }
+ }
+ };
+ new AlertDialog.Builder(context)
+ .setTitle(apiUrl.getTitle())
+ .setView(editText)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setNeutralButton(str("revanced_sb_reset"), urlChangeListener)
+ .setPositiveButton(android.R.string.ok, urlChangeListener)
+ .show();
+ return true;
+ });
+ generalCategory.addPreference(apiUrl);
+
+ importExport = new EditTextPreference(context) {
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ Utils.setEditTextDialogTheme(builder);
+
+ builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
+ Utils.setClipboard(getEditText().getText().toString());
+ });
+ }
+ };
+ importExport.setTitle(str("revanced_sb_settings_ie"));
+ // Summary is set in updateUI()
+ importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT
+ | InputType.TYPE_TEXT_FLAG_MULTI_LINE
+ | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+ importExport.getEditText().setAutofillHints((String) null);
+ importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8);
+ importExport.setOnPreferenceClickListener(preference1 -> {
+ importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings());
+ return true;
+ });
+ importExport.setOnPreferenceChangeListener((preference1, newValue) -> {
+ SponsorBlockSettings.importDesktopSettings((String) newValue);
+ updateUI();
+ return true;
+ });
+ generalCategory.addPreference(importExport);
+
+ Utils.setPreferenceTitlesToMultiLineIfNeeded(this);
+
+ updateUI();
+ } catch (Exception ex) {
+ Logger.printException(() -> "onAttachedToActivity failure", ex);
+ }
+ }
+
+ private void openGuidelines() {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
+ getContext().startActivity(intent);
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java
new file mode 100644
index 000000000..d26a6f6be
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java
@@ -0,0 +1,210 @@
+package app.revanced.extension.youtube.sponsorblock.ui;
+
+import static android.text.Html.fromHtml;
+import static app.revanced.extension.shared.StringRef.str;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
+import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
+import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
+import app.revanced.extension.youtube.sponsorblock.objects.UserStats;
+import app.revanced.extension.youtube.sponsorblock.requests.SBRequester;
+
+/**
+ * User skip stats.
+ *
+ * None of the preferences here show up in search results because
+ * a category cannot be added to another category for the search results.
+ * Additionally the stats must load remotely on a background thread which means the
+ * preferences are not available to collect for search when the settings first load.
+ */
+@SuppressWarnings({"unused", "deprecation"})
+public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
+
+ public SponsorBlockStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public SponsorBlockStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public SponsorBlockStatsPreferenceCategory(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ protected void onAttachedToActivity() {
+ try {
+ super.onAttachedToActivity();
+
+ Logger.printDebug(() -> "Updating SB stats UI");
+ final boolean enabled = Settings.SB_ENABLED.get();
+ setEnabled(enabled);
+ removeAll();
+
+ if (!SponsorBlockSettings.userHasSBPrivateId()) {
+ // User has never voted or created any segments. Only local stats exist.
+ addLocalUserStats();
+ return;
+ }
+
+ Preference loadingPlaceholderPreference = new Preference(getContext());
+ loadingPlaceholderPreference.setEnabled(false);
+ addPreference(loadingPlaceholderPreference);
+
+ if (enabled) {
+ 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(() -> "onAttachedToActivity failure", ex);
+ }
+ }
+
+ private void addUserStats(Preference loadingPlaceholder, @Nullable UserStats stats) {
+ Utils.verifyOnMainThread();
+ try {
+ if (stats == null) {
+ loadingPlaceholder.setTitle(str("revanced_sb_stats_connection_failure"));
+ return;
+ }
+ removeAll();
+ Context context = getContext();
+
+ if (stats.totalSegmentCountIncludingIgnored > 0) {
+ // If user has not created any segments, there's no reason to set a username.
+ String userName = stats.userName;
+ EditTextPreference preference = new ResettableEditTextPreference(context);
+ 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
+ SponsorBlockUtils.showErrorDialog(errorMessage);
+ }
+ });
+ });
+ return true;
+ });
+ addPreference(preference);
+ }
+
+ {
+ // Number of segment submissions (does not include ignored segments).
+ Preference preference = new Preference(context);
+ 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;
+ });
+ }
+ addPreference(preference);
+ }
+
+ {
+ // "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) {
+ addPreference(preference);
+ }
+ }
+
+ {
+ // Time saved for other users.
+ Preference preference = new Preference(context);
+
+ 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;
+ });
+ addPreference(preference);
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "addUserStats failure", ex);
+ }
+ }
+
+ private void addLocalUserStats() {
+ // Time the user saved by using SB.
+ Preference preference = new Preference(getContext());
+ 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;
+ });
+
+ addPreference(preference);
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index ce1b08bf3..d18a538e2 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.22.0
+version = 5.24.0
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 382490791..ea1df7c74 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -240,6 +240,10 @@ public final class app/revanced/patches/instagram/ads/HideAdsPatchKt {
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/instagram/misc/signature/SignatureCheckPatchKt {
+ public static final fun getSignatureCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/irplus/ad/RemoveAdsPatchKt {
public static final fun getRemoveAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -449,6 +453,14 @@ public final class app/revanced/patches/openinghours/misc/fix/crash/FixCrashPatc
public static final fun getFixCrashPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/pandora/ads/DisableAudioAdsPatchKt {
+ public static final fun getDisableAudioAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
+public final class app/revanced/patches/pandora/misc/EnableUnlimitedSkipsPatchKt {
+ public static final fun getEnableUnlimitedSkipsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/photomath/detection/deviceid/SpoofDeviceIdPatchKt {
public static final fun getGetDeviceIdPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -481,6 +493,14 @@ public final class app/revanced/patches/pixiv/ads/HideAdsPatchKt {
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/primevideo/ads/SkipAdsPatchKt {
+ public static final fun getSkipAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
+public final class app/revanced/patches/primevideo/misc/extension/ExtensionPatchKt {
+ public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/protonmail/signature/RemoveSentFromSignaturePatchKt {
public static final fun getRemoveSentFromSignaturePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
@@ -921,6 +941,14 @@ public final class app/revanced/patches/spotify/misc/fix/SpoofSignaturePatchKt {
public static final fun getSpoofSignaturePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt {
+ public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
+public final class app/revanced/patches/spotify/misc/widgets/FixThirdPartyLaunchersWidgetsKt {
+ public static final fun getFixThirdPartyLaunchersWidgets ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/spotify/navbar/PremiumNavbarTabPatchKt {
public static final fun getPremiumNavbarTabPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -1265,6 +1293,10 @@ public final class app/revanced/patches/youtube/layout/hide/player/flyoutmenupan
public static final fun getHidePlayerFlyoutMenuPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatchKt {
+ public static final fun getHideRelatedVideoOverlayPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatchKt {
public static final fun getDisableRollingNumberAnimationPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -1509,8 +1541,10 @@ public final class app/revanced/patches/youtube/misc/settings/PreferenceScreen :
public final fun getGENERAL_LAYOUT ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
public final fun getMISC ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
public final fun getPLAYER ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
+ public final fun getRETURN_YOUTUBE_DISLIKE ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
public final fun getSEEKBAR ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
public final fun getSHORTS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
+ public final fun getSPONSORBLOCK ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
public final fun getSWIPE_CONTROLS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
public final fun getVIDEO ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
}
@@ -1603,6 +1637,7 @@ public final class app/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPat
public final class app/revanced/util/BytecodeUtilsKt {
public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;)V
+ public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;[Lapp/revanced/patcher/util/smali/ExternalLabel;)V
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;D)Z
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;F)Z
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;J)Z
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/ads/Fingerprints.kt
index 65d052729..1e5eb6d04 100644
--- a/patches/src/main/kotlin/app/revanced/patches/instagram/ads/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/ads/Fingerprints.kt
@@ -9,6 +9,5 @@ internal val adInjectorFingerprint = fingerprint {
parameters("L", "L")
strings(
"SponsoredContentController.insertItem",
- "SponsoredContentController::Delivery",
)
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/misc/signature/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/signature/Fingerprints.kt
new file mode 100644
index 000000000..47ebe189b
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/signature/Fingerprints.kt
@@ -0,0 +1,20 @@
+package app.revanced.patches.instagram.misc.signature
+
+import app.revanced.patcher.fingerprint
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstruction
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+
+internal val isValidSignatureClassFingerprint = fingerprint {
+ strings("The provider for uri '", "' is not trusted: ")
+}
+
+internal val isValidSignatureMethodFingerprint = fingerprint {
+ parameters("L", "Z")
+ returns("Z")
+ custom { method, _ ->
+ method.indexOfFirstInstruction {
+ getReference()?.name == "keySet"
+ } >= 0
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/misc/signature/SignatureCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/signature/SignatureCheckPatch.kt
new file mode 100644
index 000000000..5bc077c4b
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/signature/SignatureCheckPatch.kt
@@ -0,0 +1,19 @@
+package app.revanced.patches.instagram.misc.signature
+
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.util.returnEarly
+
+@Suppress("unused")
+val signatureCheckPatch = bytecodePatch(
+ name = "Disable signature check",
+ description = "Disables the signature check that causes the app to crash on startup."
+) {
+ compatibleWith("com.instagram.android"("378.0.0.52.68"))
+
+ execute {
+ isValidSignatureMethodFingerprint
+ .match(isValidSignatureClassFingerprint.classDef)
+ .method
+ .returnEarly(true)
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch.kt b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch.kt
index a5a1cf8f3..1ba1c2048 100644
--- a/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch.kt
@@ -7,7 +7,7 @@ import app.revanced.patcher.patch.bytecodePatch
val disableMandatoryLoginPatch = bytecodePatch(
name = "Disable mandatory login",
) {
- compatibleWith("com.adobe.lrmobile")
+ compatibleWith("com.adobe.lrmobile"("10.0.2"))
execute {
isLoggedInFingerprint.method.apply {
diff --git a/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch.kt
index b9187af27..4561cee41 100644
--- a/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch.kt
@@ -7,7 +7,7 @@ import app.revanced.patcher.patch.bytecodePatch
val unlockPremiumPatch = bytecodePatch(
name = "Unlock premium",
) {
- compatibleWith("com.adobe.lrmobile")
+ compatibleWith("com.adobe.lrmobile"("10.0.2"))
execute {
// Set hasPremium = true.
diff --git a/patches/src/main/kotlin/app/revanced/patches/nunl/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/nunl/ads/Fingerprints.kt
index 8332f2f24..109b973e0 100644
--- a/patches/src/main/kotlin/app/revanced/patches/nunl/ads/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/nunl/ads/Fingerprints.kt
@@ -4,10 +4,10 @@ import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
-internal val jwUtilCreateAdvertisementFingerprint = fingerprint {
- accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC)
+internal val jwPlayerConfigFingerprint = fingerprint {
+ accessFlags(AccessFlags.PUBLIC)
custom { methodDef, classDef ->
- classDef.type == "Lnl/sanomamedia/android/nu/video/util/JWUtil;" && methodDef.name == "createAdvertising"
+ classDef.type == "Lcom/jwplayer/pub/api/configuration/PlayerConfig${'$'}Builder;" && methodDef.name == "advertisingConfig"
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/nunl/ads/HideAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/nunl/ads/HideAdsPatch.kt
index c09ce25e9..7aef3b3b9 100644
--- a/patches/src/main/kotlin/app/revanced/patches/nunl/ads/HideAdsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/nunl/ads/HideAdsPatch.kt
@@ -2,8 +2,11 @@ package app.revanced.patches.nunl.ads
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@Suppress("unused")
@@ -11,23 +14,15 @@ val hideAdsPatch = bytecodePatch(
name = "Hide ads",
description = "Hide ads and sponsored articles in list pages and remove pre-roll ads on videos.",
) {
- compatibleWith("nl.sanomamedia.android.nu"("11.0.0", "11.0.1", "11.1.0"))
+ compatibleWith("nl.sanomamedia.android.nu"("11.3.0"))
dependsOn(sharedExtensionPatch("nunl", mainActivityOnCreateHook))
execute {
// Disable video pre-roll ads.
- // Whenever the app tries to create an ad via JWUtils.createAdvertising, don't actually tell the underlying JWPlayer library to do so => JWPlayer will not display ads.
- jwUtilCreateAdvertisementFingerprint.method.addInstructions(
- 0,
- """
- new-instance v0, Lcom/jwplayer/pub/api/configuration/ads/VastAdvertisingConfig${'$'}Builder;
- invoke-direct { v0 }, Lcom/jwplayer/pub/api/configuration/ads/VastAdvertisingConfig${'$'}Builder;->()V
- invoke-virtual { v0 }, Lcom/jwplayer/pub/api/configuration/ads/VastAdvertisingConfig${'$'}Builder;->build()Lcom/jwplayer/pub/api/configuration/ads/VastAdvertisingConfig;
- move-result-object v0
- return-object v0
- """,
- )
+ // Whenever the app tries to define the advertising config for JWPlayer, don't set the advertising config and directly return.
+ val iputInstructionIndex = jwPlayerConfigFingerprint.method.indexOfFirstInstructionOrThrow(Opcode.IPUT_OBJECT)
+ jwPlayerConfigFingerprint.method.removeInstructions(iputInstructionIndex, 1)
// Filter injected content from API calls out of lists.
arrayOf(screenMapperFingerprint, nextPageRepositoryImplFingerprint).forEach {
diff --git a/patches/src/main/kotlin/app/revanced/patches/pandora/ads/DisableAudioAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/pandora/ads/DisableAudioAdsPatch.kt
new file mode 100644
index 000000000..a25a8880a
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/pandora/ads/DisableAudioAdsPatch.kt
@@ -0,0 +1,30 @@
+package app.revanced.patches.pandora.ads
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.pandora.shared.constructUserDataFingerprint
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+
+@Suppress("unused")
+val disableAudioAdsPatch = bytecodePatch(
+ name = "Disable audio ads",
+) {
+ compatibleWith("com.pandora.android")
+
+ execute {
+ constructUserDataFingerprint.method.apply {
+ // First match is "hasAudioAds".
+ val hasAudioAdsStringIndex = constructUserDataFingerprint.stringMatches!!.first().index
+ val moveResultIndex = indexOfFirstInstructionOrThrow(hasAudioAdsStringIndex, Opcode.MOVE_RESULT)
+ val hasAudioAdsRegister = getInstruction(moveResultIndex).registerA
+
+ addInstruction(
+ moveResultIndex + 1,
+ "const/4 v$hasAudioAdsRegister, 0"
+ )
+ }
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/pandora/misc/EnableUnlimitedSkipsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/pandora/misc/EnableUnlimitedSkipsPatch.kt
new file mode 100644
index 000000000..aedb5a9c2
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/pandora/misc/EnableUnlimitedSkipsPatch.kt
@@ -0,0 +1,31 @@
+package app.revanced.patches.pandora.misc
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.pandora.shared.constructUserDataFingerprint
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+
+@Suppress("unused")
+val enableUnlimitedSkipsPatch = bytecodePatch(
+ name = "Enable unlimited skips",
+) {
+ compatibleWith("com.pandora.android")
+
+ execute {
+ constructUserDataFingerprint.method.apply {
+ // Last match is "skipLimitBehavior".
+ val skipLimitBehaviorStringIndex = constructUserDataFingerprint.stringMatches!!.last().index
+ val moveResultObjectIndex =
+ indexOfFirstInstructionOrThrow(skipLimitBehaviorStringIndex, Opcode.MOVE_RESULT_OBJECT)
+ val skipLimitBehaviorRegister = getInstruction(moveResultObjectIndex).registerA
+
+ addInstruction(
+ moveResultObjectIndex + 1,
+ "const-string v$skipLimitBehaviorRegister, \"unlimited\""
+ )
+ }
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/pandora/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/pandora/shared/Fingerprints.kt
new file mode 100644
index 000000000..c045e0841
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/pandora/shared/Fingerprints.kt
@@ -0,0 +1,7 @@
+package app.revanced.patches.pandora.shared
+
+import app.revanced.patcher.fingerprint
+
+internal val constructUserDataFingerprint = fingerprint {
+ strings("hasAudioAds", "skipLimitBehavior")
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/Fingerprints.kt
new file mode 100644
index 000000000..ac3a1c43a
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/Fingerprints.kt
@@ -0,0 +1,33 @@
+package app.revanced.patches.primevideo.ads
+
+import app.revanced.patcher.fingerprint
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+
+internal val enterServerInsertedAdBreakStateFingerprint = fingerprint {
+ accessFlags(AccessFlags.PUBLIC)
+ parameters("Lcom/amazon/avod/fsm/Trigger;")
+ returns("V")
+ opcodes(
+ Opcode.INVOKE_VIRTUAL,
+ Opcode.MOVE_RESULT_OBJECT,
+ Opcode.CONST_4,
+ Opcode.CONST_4
+ )
+ custom { method, classDef ->
+ method.name == "enter" && classDef.type == "Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;"
+ }
+}
+
+internal val doTriggerFingerprint = fingerprint {
+ accessFlags(AccessFlags.PROTECTED)
+ returns("V")
+ opcodes(
+ Opcode.IGET_OBJECT,
+ Opcode.INVOKE_INTERFACE,
+ Opcode.RETURN_VOID
+ )
+ custom { method, classDef ->
+ method.name == "doTrigger" && classDef.type == "Lcom/amazon/avod/fsm/StateBase;"
+ }
+}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/SkipAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/SkipAdsPatch.kt
new file mode 100644
index 000000000..e43828932
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/primevideo/ads/SkipAdsPatch.kt
@@ -0,0 +1,45 @@
+package app.revanced.patches.primevideo.ads
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.primevideo.misc.extension.sharedExtensionPatch
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+
+@Suppress("unused")
+val skipAdsPatch = bytecodePatch(
+ name = "Skip ads",
+ description = "Automatically skips video stream ads.",
+) {
+ compatibleWith("com.amazon.avod.thirdpartyclient"("3.0.403.257"))
+
+ dependsOn(sharedExtensionPatch)
+
+ // Skip all the logic in ServerInsertedAdBreakState.enter(), which plays all the ad clips in this
+ // ad break. Instead, force the video player to seek over the entire break and reset the state machine.
+ execute {
+ // Force doTrigger() access to public so we can call it from our extension.
+ doTriggerFingerprint.method.accessFlags = AccessFlags.PUBLIC.value;
+
+ val getPlayerIndex = enterServerInsertedAdBreakStateFingerprint.patternMatch!!.startIndex
+ enterServerInsertedAdBreakStateFingerprint.method.apply {
+ // Get register that stores VideoPlayer:
+ // invoke-virtual ->getPrimaryPlayer()
+ // move-result-object { playerRegister }
+ val playerRegister = getInstruction(getPlayerIndex + 1).registerA
+
+ // Reuse the params from the original method:
+ // p0 = ServerInsertedAdBreakState
+ // p1 = AdBreakTrigger
+ addInstructions(
+ getPlayerIndex + 2,
+ """
+ invoke-static { p0, p1, v$playerRegister }, Lapp/revanced/extension/primevideo/ads/SkipAdsPatch;->enterServerInsertedAdBreakState(Lcom/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState;Lcom/amazon/avod/media/ads/internal/state/AdBreakTrigger;Lcom/amazon/avod/media/playback/VideoPlayer;)V
+ return-void
+ """
+ )
+ }
+ }
+}
+
diff --git a/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/ExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/ExtensionPatch.kt
new file mode 100644
index 000000000..34f4f9b36
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/ExtensionPatch.kt
@@ -0,0 +1,5 @@
+package app.revanced.patches.primevideo.misc.extension
+
+import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
+
+val sharedExtensionPatch = sharedExtensionPatch("primevideo", applicationInitHook)
diff --git a/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/Hooks.kt b/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/Hooks.kt
new file mode 100644
index 000000000..763c2bfd5
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/primevideo/misc/extension/Hooks.kt
@@ -0,0 +1,9 @@
+package app.revanced.patches.primevideo.misc.extension
+
+import app.revanced.patches.shared.misc.extension.extensionHook
+
+internal val applicationInitHook = extensionHook {
+ custom { method, classDef ->
+ method.name == "onCreate" && classDef.endsWith("/SplashScreenActivity;")
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt
index c6fe5fcec..daddd2841 100644
--- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/Fingerprints.kt
@@ -2,8 +2,12 @@ package app.revanced.patches.spotify.misc
import app.revanced.patcher.fingerprint
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
internal val accountAttributeFingerprint = fingerprint {
custom { _, classDef ->
@@ -15,7 +19,7 @@ internal val accountAttributeFingerprint = fingerprint {
}
}
-internal val productStateProtoFingerprint = fingerprint {
+internal val productStateProtoGetMapFingerprint = fingerprint {
returns("Ljava/util/Map;")
custom { _, classDef ->
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
@@ -56,16 +60,40 @@ internal val readPlayerOptionOverridesFingerprint = fingerprint {
}
}
-internal val homeSectionFingerprint = fingerprint {
- custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
-}
-
internal val protobufListsFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
custom { method, _ -> method.name == "emptyProtobufList" }
}
-internal val homeStructureFingerprint = fingerprint {
- opcodes(Opcode.IGET_OBJECT, Opcode.RETURN_OBJECT)
- custom { _, classDef -> classDef.endsWith("homeapi/proto/HomeStructure;") }
+internal val protobufListRemoveFingerprint = fingerprint {
+ custom { method, _ -> method.name == "remove" }
}
+
+internal val homeSectionFingerprint = fingerprint {
+ custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
+}
+
+internal val homeStructureGetSectionsFingerprint = fingerprint {
+ custom { method, classDef ->
+ classDef.endsWith("homeapi/proto/HomeStructure;") && method.indexOfFirstInstruction {
+ opcode == Opcode.IGET_OBJECT && getReference()?.name == "sections_"
+ } >= 0
+ }
+}
+
+internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint {
+ returns("Ljava/lang/Object;")
+ parameters("Ljava/lang/Object;")
+ custom { method, _ -> method.name == "apply" && method.indexOfFirstInstruction {
+ opcode == Opcode.NEW_INSTANCE && getReference()?.type?.endsWith(className) == true
+ } >= 0
+ }
+}
+
+internal const val PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME = "FetchMessageRequest;"
+internal val pendragonJsonFetchMessageRequestFingerprint =
+ reactivexFunctionApplyWithClassInitFingerprint(PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME)
+
+internal const val PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME = "FetchMessageListRequest;"
+internal val pendragonProtoFetchMessageListRequestFingerprint =
+ reactivexFunctionApplyWithClassInitFingerprint(PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME)
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt
index 8678517f9..90bfc1694 100644
--- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/UnlockPremiumPatch.kt
@@ -4,22 +4,25 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
-import app.revanced.patcher.fingerprint
+import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
-import app.revanced.patches.spotify.misc.check.checkEnvironmentPatch
+import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
+import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
-import com.android.tools.smali.dexlib2.AccessFlags
+import app.revanced.util.toPublicAccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
import java.util.logging.Logger
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/misc/UnlockPremiumPatch;"
@@ -42,14 +45,18 @@ val unlockPremiumPatch = bytecodePatch(
)
execute {
- // Make _value accessible so that it can be overridden in the extension.
- accountAttributeFingerprint.classDef.fields.first { it.name == "value_" }.apply {
- // Add public flag and remove private.
- accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
+ fun MutableClass.publicizeField(fieldName: String) {
+ fields.first { it.name == fieldName }.apply {
+ // Add public and remove private flag.
+ accessFlags = accessFlags.toPublicAccessFlags()
+ }
}
+ // Make _value accessible so that it can be overridden in the extension.
+ accountAttributeFingerprint.classDef.publicizeField("value_")
+
// Override the attributes map in the getter method.
- productStateProtoFingerprint.method.apply {
+ productStateProtoGetMapFingerprint.method.apply {
val getAttributesMapIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
val attributesMapRegister = getInstruction(getAttributesMapIndex).registerA
@@ -62,19 +69,20 @@ val unlockPremiumPatch = bytecodePatch(
// Add the query parameter trackRows to show popular tracks in the artist page.
- buildQueryParametersFingerprint.apply {
- val addQueryParameterConditionIndex = method.indexOfFirstInstructionReversedOrThrow(
- stringMatches!!.first().index, Opcode.IF_EQZ
+ buildQueryParametersFingerprint.method.apply {
+ val addQueryParameterConditionIndex = indexOfFirstInstructionReversedOrThrow(
+ buildQueryParametersFingerprint.stringMatches!!.first().index, Opcode.IF_EQZ
)
- method.replaceInstruction(addQueryParameterConditionIndex, "nop")
+ replaceInstruction(addQueryParameterConditionIndex, "nop")
}
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
- return@execute Logger.getLogger(this::class.java.name).warning(
+ Logger.getLogger(this::class.java.name).warning(
"Patching a legacy Spotify version. Patch functionality may be limited."
)
+ return@execute
}
@@ -105,48 +113,39 @@ val unlockPremiumPatch = bytecodePatch(
val shufflingContextCallIndex = indexOfFirstInstructionOrThrow {
getReference()?.name == "shufflingContext"
}
+ val boolRegister = getInstruction(shufflingContextCallIndex).registerD
- val registerBool = getInstruction(shufflingContextCallIndex).registerD
addInstruction(
shufflingContextCallIndex,
- "sget-object v$registerBool, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;"
+ "sget-object v$boolRegister, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;"
)
}
// Disable the "Spotify Premium" upsell experiment in context menus.
- contextMenuExperimentsFingerprint.apply {
- val moveIsEnabledIndex = method.indexOfFirstInstructionOrThrow(
- stringMatches!!.first().index, Opcode.MOVE_RESULT
+ contextMenuExperimentsFingerprint.method.apply {
+ val moveIsEnabledIndex = indexOfFirstInstructionOrThrow(
+ contextMenuExperimentsFingerprint.stringMatches!!.first().index, Opcode.MOVE_RESULT
)
- val isUpsellEnabledRegister = method.getInstruction(moveIsEnabledIndex).registerA
+ val isUpsellEnabledRegister = getInstruction(moveIsEnabledIndex).registerA
- method.replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0")
+ replaceInstruction(moveIsEnabledIndex, "const/4 v$isUpsellEnabledRegister, 0")
}
- // Make featureTypeCase_ accessible so we can check the home section type in the extension.
- homeSectionFingerprint.classDef.fields.first { it.name == "featureTypeCase_" }.apply {
- // Add public flag and remove private.
- accessFlags = accessFlags.or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
- }
-
- val protobufListClassName = with(protobufListsFingerprint.originalMethod) {
+ val protobufListClassDef = with(protobufListsFingerprint.originalMethod) {
val emptyProtobufListGetIndex = indexOfFirstInstructionOrThrow(Opcode.SGET_OBJECT)
- getInstruction(emptyProtobufListGetIndex).getReference()!!.definingClass
- }
+ // Find the protobuffer list class using the definingClass which contains the empty list static value.
+ val classType = getInstruction(emptyProtobufListGetIndex).getReference()!!.definingClass
- val protobufListRemoveFingerprint = fingerprint {
- custom { method, classDef ->
- method.name == "remove" && classDef.type == protobufListClassName
- }
+ classes.find { it.type == classType } ?: throw PatchException("Could not find protobuffer list class.")
}
// Need to allow mutation of the list so the home ads sections can be removed.
// Protobuffer list has an 'isMutable' boolean parameter that sets the mutability.
// Forcing that always on breaks unrelated code in strange ways.
// Instead, remove the method call that checks if the list is unmodifiable.
- protobufListRemoveFingerprint.method.apply {
+ protobufListRemoveFingerprint.match(protobufListClassDef).method.apply {
val invokeThrowUnmodifiableIndex = indexOfFirstInstructionOrThrow {
val reference = getReference()
opcode == Opcode.INVOKE_VIRTUAL &&
@@ -157,8 +156,12 @@ val unlockPremiumPatch = bytecodePatch(
removeInstruction(invokeThrowUnmodifiableIndex)
}
+
+ // Make featureTypeCase_ accessible so we can check the home section type in the extension.
+ homeSectionFingerprint.classDef.publicizeField("featureTypeCase_")
+
// Remove ads sections from home.
- homeStructureFingerprint.method.apply {
+ homeStructureGetSectionsFingerprint.method.apply {
val getSectionsIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT)
val sectionsRegister = getInstruction(getSectionsIndex).registerA
@@ -168,5 +171,56 @@ val unlockPremiumPatch = bytecodePatch(
"$EXTENSION_CLASS_DESCRIPTOR->removeHomeSections(Ljava/util/List;)V"
)
}
+
+
+ // Replace a fetch request that returns and maps Singles with their static onErrorReturn value.
+ fun MutableMethod.replaceFetchRequestSingleWithError(requestClassName: String) {
+ // The index of where the request class is being instantiated.
+ val requestInstantiationIndex = indexOfFirstInstructionOrThrow {
+ getReference()?.type?.endsWith(requestClassName) == true
+ }
+
+ // The index of where the onErrorReturn method is called with the error static value.
+ val onErrorReturnCallIndex = indexOfFirstInstructionOrThrow(requestInstantiationIndex) {
+ getReference()?.name == "onErrorReturn"
+ }
+ val onErrorReturnCallInstruction = getInstruction(onErrorReturnCallIndex)
+
+ // The error static value register.
+ val onErrorReturnValueRegister = onErrorReturnCallInstruction.registerD
+
+ // The index where the error static value starts being constructed.
+ // Because the Singles are mapped, the error static value starts being constructed right after the first
+ // move-result-object of the map call, before the onErrorReturn method call.
+ val onErrorReturnValueConstructionIndex =
+ indexOfFirstInstructionReversedOrThrow(onErrorReturnCallIndex, Opcode.MOVE_RESULT_OBJECT) + 1
+
+ val singleClassName = onErrorReturnCallInstruction.getReference()!!.definingClass
+ // The index where the request is firstly called, before its result is mapped to other values.
+ val requestCallIndex = indexOfFirstInstructionOrThrow(requestInstantiationIndex) {
+ getReference()?.returnType == singleClassName
+ }
+
+ // Construct a new single with the error static value and return it.
+ addInstructions(
+ onErrorReturnCallIndex,
+ "invoke-static { v$onErrorReturnValueRegister }, " +
+ "$singleClassName->just(Ljava/lang/Object;)$singleClassName\n" +
+ "move-result-object v$onErrorReturnValueRegister\n" +
+ "return-object v$onErrorReturnValueRegister"
+ )
+
+ // Remove every instruction from the request call to right before the error static value construction.
+ val removeCount = onErrorReturnValueConstructionIndex - requestCallIndex
+ removeInstructions(requestCallIndex, removeCount)
+ }
+
+ // Remove pendragon (pop up ads) requests and return the errors instead.
+ pendragonJsonFetchMessageRequestFingerprint.method.replaceFetchRequestSingleWithError(
+ PENDRAGON_JSON_FETCH_MESSAGE_REQUEST_CLASS_NAME
+ )
+ pendragonProtoFetchMessageListRequestFingerprint.method.replaceFetchRequestSingleWithError(
+ PENDRAGON_PROTO_FETCH_MESSAGE_LIST_REQUEST_CLASS_NAME
+ )
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt
index 2836a4872..58606777d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatch.kt
@@ -60,4 +60,4 @@ val spoofPackageInfoPatch = bytecodePatch(
// endregion
}
}
-}
\ No newline at end of file
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt
new file mode 100644
index 000000000..3d60abf9b
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/Fingerprints.kt
@@ -0,0 +1,41 @@
+package app.revanced.patches.spotify.misc.privacy
+
+import app.revanced.patcher.fingerprint
+import app.revanced.util.literal
+import com.android.tools.smali.dexlib2.AccessFlags
+
+internal val shareCopyUrlFingerprint = fingerprint {
+ returns("Ljava/lang/Object;")
+ parameters("Ljava/lang/Object;")
+ strings("clipboard", "Spotify Link")
+ custom { method, _ ->
+ method.name == "invokeSuspend"
+ }
+}
+
+internal val shareCopyUrlLegacyFingerprint = fingerprint {
+ returns("Ljava/lang/Object;")
+ parameters("Ljava/lang/Object;")
+ strings("clipboard", "createNewSession failed")
+ custom { method, _ ->
+ method.name == "apply"
+ }
+}
+
+internal val formatAndroidShareSheetUrlFingerprint = fingerprint {
+ accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
+ returns("Ljava/lang/String;")
+ parameters("L", "Ljava/lang/String;")
+ literal {
+ '\n'.code.toLong()
+ }
+}
+
+internal val formatAndroidShareSheetUrlLegacyFingerprint = fingerprint {
+ accessFlags(AccessFlags.PUBLIC)
+ returns("Ljava/lang/String;")
+ parameters("Lcom/spotify/share/social/sharedata/ShareData;", "Ljava/lang/String;")
+ literal {
+ '\n'.code.toLong()
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt
new file mode 100644
index 000000000..8df4c7720
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatch.kt
@@ -0,0 +1,70 @@
+package app.revanced.patches.spotify.misc.privacy
+
+import app.revanced.patcher.Fingerprint
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
+import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+
+private const val EXTENSION_CLASS_DESCRIPTOR =
+ "Lapp/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch;"
+
+@Suppress("unused")
+val sanitizeSharingLinksPatch = bytecodePatch(
+ name = "Sanitize sharing links",
+ description = "Removes the tracking query parameters from links before they are shared.",
+) {
+ compatibleWith("com.spotify.music")
+
+ dependsOn(sharedExtensionPatch)
+
+ execute {
+ val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" +
+ "sanitizeUrl(Ljava/lang/String;)Ljava/lang/String;"
+
+ val copyFingerprint = if (IS_SPOTIFY_LEGACY_APP_TARGET) {
+ shareCopyUrlLegacyFingerprint
+ } else {
+ shareCopyUrlFingerprint
+ }
+
+ copyFingerprint.method.apply {
+ val newPlainTextInvokeIndex = indexOfFirstInstructionOrThrow {
+ getReference()?.name == "newPlainText"
+ }
+ val register = getInstruction(newPlainTextInvokeIndex).registerD
+
+ addInstructions(
+ newPlainTextInvokeIndex,
+ """
+ invoke-static { v$register }, $extensionMethodDescriptor
+ move-result-object v$register
+ """
+ )
+ }
+
+ // Android native share sheet is used for all other quick share types (X, WhatsApp, etc).
+ val shareUrlParameter : String
+ val shareSheetFingerprint : Fingerprint
+ if (IS_SPOTIFY_LEGACY_APP_TARGET) {
+ shareSheetFingerprint = formatAndroidShareSheetUrlLegacyFingerprint
+ shareUrlParameter = "p2"
+ } else {
+ shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint
+ shareUrlParameter = "p1"
+ }
+
+ shareSheetFingerprint.method.addInstructions(
+ 0,
+ """
+ invoke-static { $shareUrlParameter }, $extensionMethodDescriptor
+ move-result-object $shareUrlParameter
+ """
+ )
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/widgets/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/widgets/Fingerprints.kt
new file mode 100644
index 000000000..0fc536047
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/widgets/Fingerprints.kt
@@ -0,0 +1,10 @@
+package app.revanced.patches.spotify.misc.widgets
+
+import app.revanced.patcher.fingerprint
+import app.revanced.util.indexOfFirstInstruction
+import com.android.tools.smali.dexlib2.Opcode
+
+internal val canBindAppWidgetPermissionFingerprint = fingerprint {
+ strings("android.permission.BIND_APPWIDGET")
+ opcodes(Opcode.AND_INT_LIT8)
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/misc/widgets/FixThirdPartyLaunchersWidgets.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/widgets/FixThirdPartyLaunchersWidgets.kt
new file mode 100644
index 000000000..ad40f24e2
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/misc/widgets/FixThirdPartyLaunchersWidgets.kt
@@ -0,0 +1,19 @@
+package app.revanced.patches.spotify.misc.widgets
+
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.util.returnEarly
+
+@Suppress("unused")
+val fixThirdPartyLaunchersWidgets = bytecodePatch(
+ name = "Fix third party launchers widgets",
+ description = "Fixes Spotify widgets not working in third party launchers, like Nova Launcher.",
+) {
+ compatibleWith("com.spotify.music")
+
+ execute {
+ // Only system app launchers are granted the BIND_APPWIDGET permission.
+ // Override the method that checks for it to always return true, as this permission is not actually required
+ // for the widgets to work.
+ canBindAppWidgetPermissionFingerprint.method.returnEarly(true)
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt
index 14149ac7a..1afbcde45 100644
--- a/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/spotify/shared/Fingerprints.kt
@@ -14,4 +14,4 @@ internal val mainActivityOnCreateFingerprint = fingerprint {
method.name == "onCreate" && (classDef.type == SPOTIFY_MAIN_ACTIVITY
|| classDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY)
}
-}
\ No newline at end of file
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt
index 59d05ef5e..4eef1ea0b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt
@@ -143,6 +143,7 @@ val hideLayoutComponentsPatch = bytecodePatch(
key = "revanced_hide_description_components_screen",
preferences = setOf(
SwitchPreference("revanced_hide_ai_generated_video_summary_section"),
+ SwitchPreference("revanced_hide_ask_section"),
SwitchPreference("revanced_hide_attributes_section"),
SwitchPreference("revanced_hide_chapters_section"),
SwitchPreference("revanced_hide_info_cards_section"),
@@ -222,8 +223,9 @@ val hideLayoutComponentsPatch = bytecodePatch(
SwitchPreference("revanced_hide_movies_section"),
SwitchPreference("revanced_hide_notify_me_button"),
SwitchPreference("revanced_hide_playables"),
- SwitchPreference("revanced_hide_search_result_recommendations"),
+ SwitchPreference("revanced_hide_search_result_recommendation_labels"),
SwitchPreference("revanced_hide_show_more_button"),
+ SwitchPreference("revanced_hide_ticket_shelf"),
SwitchPreference("revanced_hide_doodles"),
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/Fingerprints.kt
new file mode 100644
index 000000000..0ca129863
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/Fingerprints.kt
@@ -0,0 +1,18 @@
+package app.revanced.patches.youtube.layout.hide.relatedvideooverlay
+
+import app.revanced.patcher.fingerprint
+import app.revanced.util.literal
+
+internal val relatedEndScreenResultsParentFingerprint = fingerprint {
+ returns("V")
+ literal{ appRelatedEndScreenResults }
+}
+
+internal val relatedEndScreenResultsFingerprint = fingerprint {
+ returns("V")
+ parameters(
+ "I",
+ "Z",
+ "I",
+ )
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatch.kt
new file mode 100644
index 000000000..06ffc0ca5
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatch.kt
@@ -0,0 +1,83 @@
+package app.revanced.patches.youtube.layout.hide.relatedvideooverlay
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.patch.resourcePatch
+import app.revanced.patches.all.misc.resources.addResources
+import app.revanced.patches.all.misc.resources.addResourcesPatch
+import app.revanced.patches.shared.misc.mapping.get
+import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
+import app.revanced.patches.shared.misc.mapping.resourceMappings
+import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
+import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
+import app.revanced.patches.youtube.misc.settings.PreferenceScreen
+import app.revanced.patches.youtube.misc.settings.settingsPatch
+import app.revanced.patcher.util.smali.ExternalLabel
+
+internal var appRelatedEndScreenResults = -1L
+ private set
+
+private val hideRelatedVideoOverlayResourcePatch = resourcePatch {
+ dependsOn(
+ resourceMappingPatch,
+ )
+
+ execute {
+ appRelatedEndScreenResults = resourceMappings[
+ "layout",
+ "app_related_endscreen_results",
+ ]
+ }
+}
+
+private const val EXTENSION_CLASS_DESCRIPTOR =
+ "Lapp/revanced/extension/youtube/patches/HideRelatedVideoOverlayPatch;"
+
+@Suppress("unused")
+val hideRelatedVideoOverlayPatch = bytecodePatch(
+ name = "Hide related video overlay",
+ description = "Adds an option to hide the related video overlay shown when swiping up in fullscreen.",
+) {
+ dependsOn(
+ settingsPatch,
+ sharedExtensionPatch,
+ addResourcesPatch,
+ hideRelatedVideoOverlayResourcePatch,
+ )
+
+ compatibleWith(
+ "com.google.android.youtube"(
+ "19.16.39",
+ "19.25.37",
+ "19.34.42",
+ "19.43.41",
+ "19.47.53",
+ "20.07.39",
+ "20.12.46",
+ )
+ )
+
+ execute {
+ addResources("youtube", "layout.hide.relatedvideooverlay.hideRelatedVideoOverlayPatch")
+
+ PreferenceScreen.PLAYER.addPreferences(
+ SwitchPreference("revanced_hide_related_video_overlay")
+ )
+
+ relatedEndScreenResultsFingerprint.match(
+ relatedEndScreenResultsParentFingerprint.originalClassDef
+ ).method.apply {
+ addInstructionsWithLabels(
+ 0,
+ """
+ invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->hideRelatedVideoOverlay()Z
+ move-result v0
+ if-eqz v0, :show
+ return-void
+ """,
+ ExternalLabel("show", getInstruction(0))
+ )
+ }
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt
index 3943d58ca..54fda75c8 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt
@@ -5,18 +5,6 @@ import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
-internal val conversionContextFingerprint = fingerprint {
- returns("Ljava/lang/String;")
- parameters()
- strings(
- ", widthConstraint=",
- ", heightConstraint=",
- ", templateLoggerFactory=",
- ", rootDisposableContainer=",
- "ConversionContext{containerInternal=",
- )
-}
-
internal val dislikeFingerprint = fingerprint {
returns("V")
strings("like/dislike")
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt
index fc01bf804..bfedf0d1c 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt
@@ -6,7 +6,10 @@ import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
-import app.revanced.patches.shared.misc.settings.preference.IntentPreference
+import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference
+import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory
+import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
+import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
@@ -15,14 +18,18 @@ import app.revanced.patches.youtube.misc.playservice.is_19_33_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_07_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_10_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
-import app.revanced.patches.youtube.misc.settings.addSettingPreference
-import app.revanced.patches.youtube.misc.settings.newIntent
+import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
+import app.revanced.patches.youtube.shared.conversionContextFingerprintToString
import app.revanced.patches.youtube.shared.rollingNumberTextViewAnimationUpdateFingerprint
import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId
import app.revanced.patches.youtube.video.videoid.hookVideoId
import app.revanced.patches.youtube.video.videoid.videoIdPatch
-import app.revanced.util.*
+import app.revanced.util.addInstructionsAtControlFlowLabel
+import app.revanced.util.findFreeRegister
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import app.revanced.util.returnLate
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@@ -67,15 +74,24 @@ val returnYouTubeDislikePatch = bytecodePatch(
execute {
addResources("youtube", "layout.returnyoutubedislike.returnYouTubeDislikePatch")
- addSettingPreference(
- IntentPreference(
- key = "revanced_settings_screen_09",
- titleKey = "revanced_ryd_settings_title",
- summaryKey = null,
- icon = "@drawable/revanced_settings_screen_09_ryd",
- layout = "@layout/preference_with_icon",
- intent = newIntent("revanced_ryd_settings_intent"),
+ PreferenceScreen.RETURN_YOUTUBE_DISLIKE.addPreferences(
+ SwitchPreference("revanced_ryd_enabled"),
+ SwitchPreference("revanced_ryd_shorts"),
+ SwitchPreference("revanced_ryd_dislike_percentage"),
+ SwitchPreference("revanced_ryd_compact_layout"),
+ SwitchPreference("revanced_ryd_estimated_like"),
+ SwitchPreference("revanced_ryd_toast_on_connection_error"),
+ NonInteractivePreference(
+ key = "revanced_ryd_attribution",
+ tag = "app.revanced.extension.youtube.returnyoutubedislike.ui.ReturnYouTubeDislikeAboutPreference",
+ selectable = true,
),
+ PreferenceCategory(
+ key = "revanced_ryd_statistics_category",
+ sorting = PreferenceScreenPreference.Sorting.UNSORTED,
+ preferences = emptySet(), // Preferences are added by custom class at runtime.
+ tag = "app.revanced.extension.youtube.returnyoutubedislike.ui.ReturnYouTubeDislikeDebugStatsPreferenceCategory"
+ )
)
// region Inject newVideoLoaded event handler to update dislikes when a new video is loaded.
@@ -113,11 +129,11 @@ val returnYouTubeDislikePatch = bytecodePatch(
// This hook handles all situations, as it's where the created Spans are stored and later reused.
// Find the field name of the conversion context.
val conversionContextField = textComponentConstructorFingerprint.originalClassDef.fields.find {
- it.type == conversionContextFingerprint.originalClassDef.type
+ it.type == conversionContextFingerprintToString.originalClassDef.type
} ?: throw PatchException("Could not find conversion context field")
textComponentLookupFingerprint.match(textComponentConstructorFingerprint.originalClassDef)
- textComponentLookupFingerprint.method.apply {
+ .method.apply {
// Find the instruction for creating the text data object.
val textDataClassType = textComponentDataFingerprint.originalClassDef.type
@@ -160,12 +176,12 @@ val returnYouTubeDislikePatch = bytecodePatch(
addInstructionsAtControlFlowLabel(
insertIndex,
"""
- # Copy conversion context
- move-object/from16 v$tempRegister, p0
- iget-object v$tempRegister, v$tempRegister, $conversionContextField
- invoke-static { v$tempRegister, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
- move-result-object v$charSequenceRegister
- """,
+ # Copy conversion context
+ move-object/from16 v$tempRegister, p0
+ iget-object v$tempRegister, v$tempRegister, $conversionContextField
+ invoke-static { v$tempRegister, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
+ move-result-object v$charSequenceRegister
+ """
)
}
@@ -201,11 +217,9 @@ val returnYouTubeDislikePatch = bytecodePatch(
val charSequenceFieldReference =
getInstruction(dislikesIndex).reference
- val registerCount = implementation!!.registerCount
+ val conversionContextRegister = implementation!!.registerCount - parameters.size + 1
- // This register is being overwritten, so it is free to use.
- val freeRegister = registerCount - 1
- val conversionContextRegister = registerCount - parameters.size + 1
+ val freeRegister = findFreeRegister(insertIndex, charSequenceInstanceRegister, conversionContextRegister)
addInstructions(
insertIndex,
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt
index 832b70dcf..a4ae22d35 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt
@@ -12,12 +12,13 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
-import app.revanced.patches.shared.misc.settings.preference.IntentPreference
+import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference
+import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory
+import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playercontrols.*
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
-import app.revanced.patches.youtube.misc.settings.addSettingPreference
-import app.revanced.patches.youtube.misc.settings.newIntent
+import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.patches.youtube.shared.*
import app.revanced.patches.youtube.video.information.onCreateHook
@@ -43,15 +44,32 @@ private val sponsorBlockResourcePatch = resourcePatch {
execute {
addResources("youtube", "layout.sponsorblock.sponsorBlockResourcePatch")
- addSettingPreference(
- IntentPreference(
- key = "revanced_settings_screen_10",
- titleKey = "revanced_sb_settings_title",
- summaryKey = null,
- icon = "@drawable/revanced_settings_screen_10_sb",
- layout = "@layout/preference_with_icon",
- intent = newIntent("revanced_sb_settings_intent"),
+ PreferenceScreen.SPONSORBLOCK.addPreferences(
+ // SB setting is old code with lots of custom preferences and updating behavior.
+ // Added as a preference group and not a fragment so the preferences are searchable.
+ PreferenceCategory(
+ key = "revanced_settings_screen_10_sponsorblock",
+ sorting = PreferenceScreenPreference.Sorting.UNSORTED,
+ preferences = emptySet(), // Preferences are added by custom class at runtime.
+ tag = "app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup"
),
+ PreferenceCategory(
+ key = "revanced_sb_stats",
+ sorting = PreferenceScreenPreference.Sorting.UNSORTED,
+ preferences = emptySet(), // Preferences are added by custom class at runtime.
+ tag = "app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockStatsPreferenceCategory"
+ ),
+ PreferenceCategory(
+ key = "revanced_sb_about",
+ sorting = PreferenceScreenPreference.Sorting.UNSORTED,
+ preferences = setOf(
+ NonInteractivePreference(
+ key = "revanced_sb_about_api",
+ tag = "app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockAboutPreference",
+ selectable = true,
+ )
+ )
+ )
)
arrayOf(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt
index 7dda5c308..a6cf8283a 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt
@@ -54,6 +54,7 @@ val changeStartPagePatch = bytecodePatch(
ListPreference(
key = "revanced_change_start_page",
summaryKey = null,
+ tag = "app.revanced.extension.shared.settings.preference.SortedListPreference"
),
SwitchPreference("revanced_change_start_page_always")
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt
index 89db01f0e..d14955e9c 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt
@@ -5,10 +5,6 @@ import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
-/**
- * In 19.17 and earlier, this resolves to the same method as [readComponentIdentifierFingerprint].
- * In 19.18+ this resolves to a different method.
- */
internal val componentContextParserFingerprint = fingerprint {
strings(
"TreeNode result must be set.",
@@ -17,11 +13,21 @@ internal val componentContextParserFingerprint = fingerprint {
)
}
+/**
+ * Resolves to the class found in [componentContextParserFingerprint].
+ * When patching 19.16 this fingerprint matches the same method as [componentContextParserFingerprint].
+ */
+internal val componentContextSubParserFingerprint = fingerprint {
+ strings(
+ "Number of bits must be positive"
+ )
+}
+
internal val lithoFilterFingerprint = fingerprint {
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
returns("V")
custom { _, classDef ->
- classDef.endsWith("LithoFilterPatch;")
+ classDef.endsWith("/LithoFilterPatch;")
}
}
@@ -37,14 +43,6 @@ internal val protobufBufferReferenceFingerprint = fingerprint {
)
}
-/**
-* In 19.17 and earlier, this resolves to the same method as [componentContextParserFingerprint].
-* In 19.18+ this resolves to a different method.
-*/
-internal val readComponentIdentifierFingerprint = fingerprint {
- strings("Number of bits must be positive")
-}
-
internal val emptyComponentFingerprint = fingerprint {
accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR)
parameters()
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt
index 1ede2fe5d..f8b8d05d8 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt
@@ -4,25 +4,25 @@ package app.revanced.patches.youtube.misc.litho.filter
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
-import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
-import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
-import app.revanced.patches.youtube.misc.playservice.is_19_18_or_greater
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_05_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
+import app.revanced.patches.youtube.shared.conversionContextFingerprintToString
+import app.revanced.util.addInstructionsAtControlFlowLabel
import app.revanced.util.findFreeRegister
+import app.revanced.util.findInstructionIndicesReversedOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
@@ -53,42 +53,33 @@ val lithoFilterPatch = bytecodePatch(
* The buffer is a large byte array that represents the component tree.
* This byte array is searched for strings that indicate the current component.
*
- * The following pseudocode shows how the patch works:
+ * All modifications done here must allow all the original code to still execute
+ * even when filtering, otherwise memory leaks or poor app performance may occur.
+ *
+ * The following pseudocode shows how this patch works:
*
* class SomeOtherClass {
- * // Called before ComponentContextParser.parseBytesToComponentContext method.
+ * // Called before ComponentContextParser.parseComponent() method.
* public void someOtherMethod(ByteBuffer byteBuffer) {
* ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch.
* ...
* }
* }
*
- * When patching 19.17 and earlier:
- *
* class ComponentContextParser {
- * public ComponentContext ReadComponentIdentifierFingerprint(...) {
+ * public Component parseComponent() {
* ...
- * if (extensionClass.filter(identifier, pathBuilder)); // Inserted by this patch.
+ *
+ * // Checks if the component should be filtered.
+ * // Sets a thread local with the filtering result.
+ * extensionClass.filter(identifier, pathBuilder); // Inserted by this patch.
+ *
+ * ...
+ *
+ * if (extensionClass.shouldFilter()) { // Inserted by this patch.
* return emptyComponent;
- * ...
- * }
- * }
- *
- * When patching 19.18 and later:
- *
- * class ComponentContextParser {
- * public ComponentContext parseBytesToComponentContext(...) {
- * ...
- * if (ReadComponentIdentifierFingerprint() == null); // Inserted by this patch.
- * return emptyComponent;
- * ...
- * }
- *
- * public ComponentIdentifierObj readComponentIdentifier(...) {
- * ...
- * if (extensionClass.filter(identifier, pathBuilder)); // Inserted by this patch.
- * return null;
- * ...
+ * }
+ * return originalUnpatchedComponent; // Original code.
* }
* }
*/
@@ -103,7 +94,7 @@ val lithoFilterPatch = bytecodePatch(
2,
"""
new-instance v1, $classDescriptor
- invoke-direct {v1}, $classDescriptor->()V
+ invoke-direct { v1 }, $classDescriptor->()V
const/16 v2, ${filterCount++}
aput-object v1, v0, v2
""",
@@ -115,110 +106,105 @@ val lithoFilterPatch = bytecodePatch(
protobufBufferReferenceFingerprint.method.addInstruction(
0,
- " invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V",
+ "invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V",
)
// endregion
// region Hook the method that parses bytes into a ComponentContext.
- val readComponentMethod = readComponentIdentifierFingerprint.originalMethod
- // Get the only static method in the class.
- val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.first { method ->
- AccessFlags.STATIC.isSet(method.accessFlags)
- }
- // Only one field.
- val emptyComponentField = classBy { classDef ->
- builderMethodDescriptor.returnType == classDef.type
- }!!.immutableClass.fields.single()
-
- // Returns an empty component instead of the original component.
- fun createReturnEmptyComponentInstructions(register: Int): String =
- """
- move-object/from16 v$register, p1
- invoke-static { v$register }, $builderMethodDescriptor
- move-result-object v$register
- iget-object v$register, v$register, $emptyComponentField
- return-object v$register
- """
-
+ // Allow the method to run to completion, and override the
+ // return value with an empty component if it should be filtered.
+ // It is important to allow the original code to always run to completion,
+ // otherwise memory leaks and poor app performance can occur.
+ //
+ // The extension filtering result needs to be saved off somewhere, but cannot
+ // save to a class field since the target class is called by multiple threads.
+ // It would be great if there was a way to change the register count of the
+ // method implementation and save the result to a high register to later use
+ // in the method, but there is no simple way to do that.
+ // Instead save the extension filter result to a thread local and check the
+ // filtering result at each method return index.
+ // String field for the litho identifier.
componentContextParserFingerprint.method.apply {
- // 19.18 and later require patching 2 methods instead of one.
- // Otherwise the modifications done here are the same for all targets.
- if (is_19_18_or_greater) {
- // Get the method name of the ReadComponentIdentifierFingerprint call.
- val readComponentMethodCallIndex = indexOfFirstInstructionOrThrow {
- val reference = getReference()
- reference?.definingClass == readComponentMethod.definingClass &&
- reference.name == readComponentMethod.name
+ val conversionContextClass = conversionContextFingerprintToString.originalClassDef
+
+ val conversionContextIdentifierField = componentContextSubParserFingerprint.match(
+ componentContextParserFingerprint.originalClassDef
+ ).let {
+ // Identifier field is loaded just before the string declaration.
+ val index = it.method.indexOfFirstInstructionReversedOrThrow(
+ it.stringMatches!!.first().index
+ ) {
+ val reference = getReference()
+ reference?.definingClass == conversionContextClass.type
+ && reference.type == "Ljava/lang/String;"
}
-
- // Result of read component, and also a free register.
- val register = getInstruction(readComponentMethodCallIndex + 1).registerA
-
- // Insert after 'move-result-object'
- val insertHookIndex = readComponentMethodCallIndex + 2
-
- // Return an EmptyComponent instead of the original component if the filterState method returns true.
- addInstructionsWithLabels(
- insertHookIndex,
- """
- if-nez v$register, :unfiltered
-
- # Component was filtered in ReadComponentIdentifierFingerprint hook
- ${createReturnEmptyComponentInstructions(register)}
- """,
- ExternalLabel("unfiltered", getInstruction(insertHookIndex)),
- )
+ it.method.getInstruction(index).getReference()
}
- }
- // endregion
+ // StringBuilder field for the litho path.
+ val conversionContextPathBuilderField = conversionContextClass.fields
+ .single { field -> field.type == "Ljava/lang/StringBuilder;" }
- // region Read component then store the result.
+ val conversionContextResultIndex = indexOfFirstInstructionOrThrow {
+ val reference = getReference()
+ reference?.returnType == conversionContextClass.type
+ } + 1
- readComponentIdentifierFingerprint.method.apply {
- val insertHookIndex = indexOfFirstInstructionOrThrow {
- opcode == Opcode.IPUT_OBJECT &&
- getReference()?.type == "Ljava/lang/StringBuilder;"
- }
- val stringBuilderRegister = getInstruction(insertHookIndex).registerA
-
- // Identifier is saved to a field just before the string builder.
- val identifierRegister = getInstruction(
- indexOfFirstInstructionReversedOrThrow(insertHookIndex) {
- opcode == Opcode.IPUT_OBJECT &&
- getReference()?.type == "Ljava/lang/String;"
- },
+ val conversionContextResultRegister = getInstruction(
+ conversionContextResultIndex
).registerA
- val freeRegister = findFreeRegister(insertHookIndex, identifierRegister, stringBuilderRegister)
- val invokeFilterInstructions = """
- invoke-static { v$identifierRegister, v$stringBuilderRegister }, $EXTENSION_CLASS_DESCRIPTOR->filter(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
- move-result v$freeRegister
- if-eqz v$freeRegister, :unfiltered
- """
-
- addInstructionsWithLabels(
- insertHookIndex,
- if (is_19_18_or_greater) {
- """
- $invokeFilterInstructions
-
- # Return null, and the ComponentContextParserFingerprint hook
- # handles returning an empty component.
- const/4 v$freeRegister, 0x0
- return-object v$freeRegister
- """
- } else {
- """
- $invokeFilterInstructions
-
- ${createReturnEmptyComponentInstructions(freeRegister)}
- """
- },
- ExternalLabel("unfiltered", getInstruction(insertHookIndex)),
+ val identifierRegister = findFreeRegister(
+ conversionContextResultIndex, conversionContextResultRegister
)
+ val stringBuilderRegister = findFreeRegister(
+ conversionContextResultIndex, conversionContextResultRegister, identifierRegister
+ )
+
+ // Check if the component should be filtered, and save the result to a thread local.
+ addInstructionsAtControlFlowLabel(
+ conversionContextResultIndex + 1,
+ """
+ iget-object v$identifierRegister, v$conversionContextResultRegister, $conversionContextIdentifierField
+ iget-object v$stringBuilderRegister, v$conversionContextResultRegister, $conversionContextPathBuilderField
+ invoke-static { v$identifierRegister, v$stringBuilderRegister }, $EXTENSION_CLASS_DESCRIPTOR->filter(Ljava/lang/String;Ljava/lang/StringBuilder;)V
+ """
+ )
+
+ // Get the only static method in the class.
+ val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.single {
+ method -> AccessFlags.STATIC.isSet(method.accessFlags)
+ }
+ // Only one field.
+ val emptyComponentField = classBy { classDef ->
+ classDef.type == builderMethodDescriptor.returnType
+ }!!.immutableClass.fields.single()
+
+ // Check at each return value if the component is filtered,
+ // and return an empty component if filtering is needed.
+ findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { returnIndex ->
+ val freeRegister = findFreeRegister(returnIndex)
+
+ addInstructionsAtControlFlowLabel(
+ returnIndex,
+ """
+ invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->shouldFilter()Z
+ move-result v$freeRegister
+ if-eqz v$freeRegister, :unfiltered
+
+ move-object/from16 v$freeRegister, p1
+ invoke-static { v$freeRegister }, $builderMethodDescriptor
+ move-result-object v$freeRegister
+ iget-object v$freeRegister, v$freeRegister, $emptyComponentField
+ return-object v$freeRegister
+
+ :unfiltered
+ nop
+ """
+ )
+ }
}
// endregion
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt
index ec7d89363..06a28e7f3 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt
@@ -74,6 +74,7 @@ private val settingsResourcePatch = resourcePatch {
arrayOf(
ResourceGroup("drawable",
+ "revanced_settings_cursor.xml",
"revanced_settings_icon.xml",
"revanced_settings_screen_00_about.xml",
"revanced_settings_screen_01_ads.xml",
@@ -84,12 +85,16 @@ private val settingsResourcePatch = resourcePatch {
"revanced_settings_screen_06_shorts.xml",
"revanced_settings_screen_07_seekbar.xml",
"revanced_settings_screen_08_swipe_controls.xml",
- "revanced_settings_screen_09_ryd.xml",
- "revanced_settings_screen_10_sb.xml",
+ "revanced_settings_screen_09_return_youtube_dislike.xml",
+ "revanced_settings_screen_10_sponsorblock.xml",
"revanced_settings_screen_11_misc.xml",
"revanced_settings_screen_12_video.xml",
),
- ResourceGroup("layout", "revanced_settings_with_toolbar.xml"),
+ ResourceGroup("layout",
+ "revanced_preference_with_icon_no_search_result.xml",
+ "revanced_search_suggestion_item.xml",
+ "revanced_settings_with_toolbar.xml"),
+ ResourceGroup("menu", "revanced_search_menu.xml")
).forEach { resourceGroup ->
copyResources("settings", resourceGroup)
}
@@ -188,6 +193,7 @@ val settingsPatch = bytecodePatch(
}
PreferenceScreen.GENERAL_LAYOUT.addPreferences(
+ SwitchPreference("revanced_settings_search_history"),
SwitchPreference("revanced_show_menu_icons")
)
@@ -201,7 +207,8 @@ val settingsPatch = bytecodePatch(
),
ListPreference(
key = "revanced_language",
- summaryKey = null
+ summaryKey = null,
+ tag = "app.revanced.extension.shared.settings.preference.SortedListPreference"
)
)
@@ -347,10 +354,20 @@ object PreferenceScreen : BasePreferenceScreen() {
layout = "@layout/preference_with_icon",
sorting = Sorting.UNSORTED,
)
-
- // RYD and SB are items 9 and 10.
- // Menus are added in their own patch because they use an Intent and not a Screen.
-
+ val RETURN_YOUTUBE_DISLIKE = Screen(
+ key = "revanced_settings_screen_09_return_youtube_dislike",
+ summaryKey = null,
+ icon = "@drawable/revanced_settings_screen_09_return_youtube_dislike",
+ layout = "@layout/preference_with_icon",
+ sorting = Sorting.UNSORTED,
+ )
+ val SPONSORBLOCK = Screen(
+ key = "revanced_settings_screen_10_sponsorblock",
+ summaryKey = null,
+ icon = "@drawable/revanced_settings_screen_10_sponsorblock",
+ layout = "@layout/preference_with_icon",
+ sorting = Sorting.UNSORTED,
+ )
val MISC = Screen(
key = "revanced_settings_screen_11_misc",
summaryKey = null,
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt
index f75eef328..9fbf5ddca 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt
@@ -62,7 +62,8 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch({
summaryKey = null,
// Language strings are declared in Setting patch.
entriesKey = "revanced_language_entries",
- entryValuesKey = "revanced_language_entry_values"
+ entryValuesKey = "revanced_language_entry_values",
+ tag = "app.revanced.extension.shared.settings.preference.SortedListPreference"
),
SwitchPreference("revanced_spoof_video_streams_ios_force_avc"),
SwitchPreference("revanced_spoof_streaming_data_stats_for_nerds"),
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt
index d0c7d30a2..29f6ea4c7 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt
@@ -4,6 +4,21 @@ import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
+internal val conversionContextFingerprintToString = fingerprint {
+ parameters()
+ strings(
+ "ConversionContext{containerInternal=",
+ ", widthConstraint=",
+ ", heightConstraint=",
+ ", templateLoggerFactory=",
+ ", rootDisposableContainer=",
+ ", identifierProperty="
+ )
+ custom { method, _ ->
+ method.name == "toString"
+ }
+}
+
internal val autoRepeatFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt
index ad6ee4a6c..9f601c74a 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt
@@ -38,6 +38,7 @@ internal val rememberPlaybackSpeedPatch = bytecodePatch {
// Entries and values are set by the extension code based on the actual speeds available.
entriesKey = null,
entryValuesKey = null,
+ tag = "app.revanced.extension.youtube.settings.preference.CustomVideoSpeedListPreference"
),
SwitchPreference("revanced_remember_playback_speed_last_selected")
)
diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
index c6de63c83..0ca022201 100644
--- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
+++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt
@@ -11,12 +11,14 @@ import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
+import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.shared.misc.mapping.get
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
import app.revanced.patches.shared.misc.mapping.resourceMappings
import app.revanced.util.InstructionUtils.Companion.branchOpcodes
import app.revanced.util.InstructionUtils.Companion.returnOpcodes
import app.revanced.util.InstructionUtils.Companion.writeOpcodes
+import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.Opcode.*
import com.android.tools.smali.dexlib2.iface.Method
@@ -63,35 +65,6 @@ fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude: Int): In
val instruction = getInstruction(i)
val instructionRegisters = instruction.registersUsed
- if (instruction.isReturnInstruction) {
- usedRegisters.addAll(instructionRegisters)
-
- // Use lowest register that hasn't been encountered.
- val freeRegister = (0 until implementation!!.registerCount).find {
- it !in usedRegisters
- }
- if (freeRegister != null) {
- return freeRegister
- }
- if (bestFreeRegisterFound != null) {
- return bestFreeRegisterFound
- }
-
- // Somehow every method register was read from before any register was wrote to.
- // In practice this never occurs.
- throw IllegalArgumentException("Could not find a free register from startIndex: " +
- "$startIndex excluding: $registersToExclude")
- }
-
- if (instruction.isBranchInstruction) {
- if (bestFreeRegisterFound != null) {
- return bestFreeRegisterFound
- }
- // This method is simple and does not follow branching.
- throw IllegalArgumentException("Encountered a branch statement before a free register could be found")
- }
-
-
val writeRegister = instruction.writeRegister
if (writeRegister != null) {
if (writeRegister !in usedRegisters) {
@@ -112,6 +85,32 @@ fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude: Int): In
}
usedRegisters.addAll(instructionRegisters)
+
+ if (instruction.isBranchInstruction) {
+ if (bestFreeRegisterFound != null) {
+ return bestFreeRegisterFound
+ }
+ // This method is simple and does not follow branching.
+ throw IllegalArgumentException("Encountered a branch statement before a free register could be found")
+ }
+
+ if (instruction.isReturnInstruction) {
+ // Use lowest register that hasn't been encountered.
+ val freeRegister = (0 until implementation!!.registerCount).find {
+ it !in usedRegisters
+ }
+ if (freeRegister != null) {
+ return freeRegister
+ }
+ if (bestFreeRegisterFound != null) {
+ return bestFreeRegisterFound
+ }
+
+ // Somehow every method register was read from before any register was wrote to.
+ // In practice this never occurs.
+ throw IllegalArgumentException("Could not find a free register from startIndex: " +
+ "$startIndex excluding: $registersToExclude")
+ }
}
// Some methods can have array payloads at the end of the method after a return statement.
@@ -168,6 +167,15 @@ internal val Instruction.isBranchInstruction: Boolean
internal val Instruction.isReturnInstruction: Boolean
get() = this.opcode in returnOpcodes
+/**
+ * Adds public [AccessFlags] and removes private and protected flags (if present).
+ */
+internal fun Int.toPublicAccessFlags() : Int {
+ return this.or(AccessFlags.PUBLIC.value)
+ .and(AccessFlags.PROTECTED.value.inv())
+ .and(AccessFlags.PRIVATE.value.inv())
+}
+
/**
* Find the [MutableMethod] from a given [Method] in a [MutableClass].
*
@@ -207,6 +215,26 @@ fun MutableMethod.injectHideViewCall(
"invoke-static { v$viewRegister }, $classDescriptor->$targetMethod(Landroid/view/View;)V",
)
+
+/**
+ * Inserts instructions at a given index, using the existing control flow label at that index.
+ * Inserted instructions can have it's own control flow labels as well.
+ *
+ * Effectively this changes the code from:
+ * :label
+ * (original code)
+ *
+ * Into:
+ * :label
+ * (patch code)
+ * (original code)
+ */
+// TODO: delete this on next major version bump.
+fun MutableMethod.addInstructionsAtControlFlowLabel(
+ insertIndex: Int,
+ instructions: String
+) = addInstructionsAtControlFlowLabel(insertIndex, instructions, *arrayOf())
+
/**
* Inserts instructions at a given index, using the existing control flow label at that index.
* Inserted instructions can have it's own control flow labels as well.
@@ -223,13 +251,14 @@ fun MutableMethod.injectHideViewCall(
fun MutableMethod.addInstructionsAtControlFlowLabel(
insertIndex: Int,
instructions: String,
+ vararg externalLabels: ExternalLabel
) {
// Duplicate original instruction and add to +1 index.
addInstruction(insertIndex + 1, getInstruction(insertIndex))
// Add patch code at same index as duplicated instruction,
// so it uses the original instruction control flow label.
- addInstructionsWithLabels(insertIndex + 1, instructions)
+ addInstructionsWithLabels(insertIndex + 1, instructions, *externalLabels)
// Remove original non duplicated instruction.
removeInstruction(insertIndex)
@@ -472,7 +501,7 @@ fun Method.indexOfFirstInstruction(startIndex: Int = 0, targetOpcode: Opcode): I
* @see indexOfFirstInstructionOrThrow
*/
fun Method.indexOfFirstInstruction(startIndex: Int = 0, filter: Instruction.() -> Boolean): Int {
- var instructions = this.implementation!!.instructions
+ var instructions = this.implementation?.instructions ?: return -1
if (startIndex != 0) {
instructions = instructions.drop(startIndex)
}
@@ -538,7 +567,7 @@ fun Method.indexOfFirstInstructionReversed(startIndex: Int? = null, targetOpcode
* @see indexOfFirstInstructionReversedOrThrow
*/
fun Method.indexOfFirstInstructionReversed(startIndex: Int? = null, filter: Instruction.() -> Boolean): Int {
- var instructions = this.implementation!!.instructions
+ var instructions = this.implementation?.instructions ?: return -1
if (startIndex != null) {
instructions = instructions.take(startIndex + 1)
}
diff --git a/patches/src/main/resources/addresources/values-af-rZA/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/strings.xml
index ed07bda61..24e47c741 100644
--- a/patches/src/main/resources/addresources/values-af-rZA/strings.xml
+++ b/patches/src/main/resources/addresources/values-af-rZA/strings.xml
@@ -44,7 +44,7 @@ Second \"item\" text"
-
+
@@ -134,6 +134,8 @@ Second \"item\" text"
+
+
-
+
@@ -134,6 +134,8 @@ Second \"item\" text"
+
+
إعادة تعيين إعدادات ReVanced إلى الوضع الافتراضي
تم استيراد %d إعدادات
فشل الاستيراد: %s
+ إعدادات البحث
+ لم يتم العثور على نتائج لـ \".%s\"
+ جرّب كلمة مفتاحية أخرى
+ إزالة من سجل البحث؟
عرض أيقونات إعدادات ReVanced
يتم عرض أيقونات الإعدادات
لا يتم عرض أيقونات الإعدادات
@@ -93,6 +97,9 @@ Second \"item\" text"
استعادة قوائم الإعدادات القديمة
يتم عرض قوائم الإعدادات القديمة
لا يتم عرض قوائم الإعدادات القديمة
+ إظهار سجل البحث في الإعدادات
+ سجل البحث في الإعدادات معروض
+ لم يتم عرض سجل البحث في الإعدادات
تعطيل تشغيل فيديوهات Shorts في الخلفية
@@ -153,15 +160,18 @@ Second \"item\" text"
إخفاء زر \'تنبيهي\'
تم إخفاء الزر
يتم عرض الزر
-
- إخفاء علامة \'الأشخاص الذين شاهدوا أيضًا\'
- تم إخفاء العلامة
- يتم عرض العلامة
+
+ إخفاء علامات اقتراحات الفيديو
+ تم إخفاء علامات \'اقتراحات للمشاهدة\' و\'قد يعجبك أيضًا\'
+ يتم عرض علامات \'اقتراحات للمشاهدة\' و\'قد يعجبك أيضًا\'
إخفاء زر \'عرض المزيد\'
تم إخفاء الزر
يتم عرض الزر
+ إخفاء رف التذاكر
+ تم إخفاء رف التذاكر
+ يتم عرض رف التذاكر
إخفاء ردود الفعل المؤقتة
تم إخفاء ردود الفعل المؤقتة
يتم عرض ردود الفعل المؤقتة
@@ -231,6 +241,9 @@ Second \"item\" text"
إخفاء \'ملخص الفيديو الذي تم إنشاؤه بواسطة الذكاء الاصطناعي\'
تم إخفاء قسم ملخص الفيديو
يتم عرض قسم ملخص الفيديو
+ إخفاء \"Ask\"
+ تم إخفاء قسم \"Ask\"
+ يتم عرض قسم \"Ask\"
إخفاء الصفات
تم إخفاء أقسام الأماكن المميزة، الألعاب، الموسيقى والأشخاص المذكورون
يتم عرض أقسام الأماكن المميزة، الألعاب، الموسيقى والأشخاص المذكورون
@@ -794,6 +807,11 @@ Second \"item\" text"
الإعدادات ← التشغيل ← تشغيل الفيديو التالي تلقائيًا"