Compare commits
42 commits
main
...
feat/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14a16efa0f | ||
|
|
467a62f4ac | ||
|
|
0371f7164f | ||
|
|
1ebd990051 | ||
|
|
f063cf69bd | ||
|
|
09bce22aeb | ||
|
|
e52114076e | ||
|
|
58898b92fe | ||
|
|
53318c48ee | ||
|
|
009cf71462 | ||
|
|
136cecc290 | ||
|
|
7f1874ebe0 | ||
|
|
733f3bb2cd | ||
|
|
9fa641ed81 | ||
|
|
7b8a3061a2 | ||
|
|
3514039e8a | ||
|
|
14545c874d | ||
|
|
b71e4f95ff | ||
|
|
5c7fc97059 | ||
|
|
9b66637a58 | ||
|
|
cc019ec78b | ||
|
|
b1ae92cddd | ||
|
|
a9aeb325de | ||
|
|
b435564fd4 | ||
|
|
7fe7ce437c | ||
|
|
15096257ca | ||
|
|
23676746a6 | ||
|
|
e7196e54b0 | ||
|
|
7f52ec2cea | ||
|
|
20079d267a | ||
|
|
af95a58009 | ||
|
|
636698c96e | ||
|
|
81b24642be | ||
|
|
7495528815 | ||
|
|
5aec6cd3b7 | ||
|
|
6d8c94d0d2 | ||
|
|
8c2445f92f | ||
|
|
bc08ecf785 | ||
|
|
f22ea5507d | ||
|
|
f10f5e2910 | ||
|
|
3c46a2d2e8 | ||
|
|
b826db02e3 |
167 changed files with 3339 additions and 1596 deletions
|
|
@ -1,3 +1,10 @@
|
|||
## [6.1.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v6.1.0...v6.1.1-dev.1) (2026-03-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Spoof video streams:** Set `ANDROID_REEL` client as default ([#6878](https://github.com/ReVanced/revanced-patches/issues/6878)) ([a9aeb32](https://github.com/ReVanced/revanced-patches/commit/a9aeb325de1160262c4db9b4b60c6c5e39730620))
|
||||
|
||||
# [6.1.0](https://github.com/ReVanced/revanced-patches/compare/v6.0.1...v6.1.0) (2026-03-18)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package app.revanced.extension.music.patches;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.ResourceType;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
|
||||
public class ChangeHeaderPatch {
|
||||
public enum HeaderLogo {
|
||||
DEFAULT(null),
|
||||
REVANCED("revanced_header_dark"),
|
||||
CUSTOM("revanced_header_custom_dark");
|
||||
|
||||
private final String drawableName;
|
||||
|
||||
HeaderLogo(String drawableName) {
|
||||
this.drawableName = drawableName;
|
||||
}
|
||||
|
||||
private Integer getDrawableId() {
|
||||
if (drawableName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int id = Utils.getResourceIdentifier(ResourceType.DRAWABLE, drawableName);
|
||||
if (id == 0) {
|
||||
Logger.printException(() ->
|
||||
"Header drawable not found: " + drawableName
|
||||
);
|
||||
Settings.HEADER_LOGO.resetToDefault();
|
||||
return null;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static int getHeaderDrawableId(int original) {
|
||||
return Objects.requireNonNullElse(
|
||||
Settings.HEADER_LOGO.get().getDrawableId(),
|
||||
original
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package app.revanced.extension.music.patches;
|
||||
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class ChangeStartPagePatch {
|
||||
private static final String SHORTCUT_ACTION = "com.google.android.youtube.music.action.shortcut";
|
||||
private static final String SHORTCUT_CLASS_DESCRIPTOR = "com.google.android.apps.youtube.music.activities.InternalMusicActivity";
|
||||
private static final String SHORTCUT_TYPE = "com.google.android.youtube.music.action.shortcut_type";
|
||||
private static final String SHORTCUT_ID_SEARCH = "Eh4IBRDTnQEYmgMiEwiZn+H0r5WLAxVV5OcDHcHRBmPqpd25AQA=";
|
||||
private static final int SHORTCUT_TYPE_SEARCH = 1;
|
||||
|
||||
|
||||
public enum StartPage {
|
||||
DEFAULT("", null),
|
||||
CHARTS("FEmusic_charts", TRUE),
|
||||
EXPLORE("FEmusic_explore", TRUE),
|
||||
HISTORY("FEmusic_history", TRUE),
|
||||
LIBRARY("FEmusic_library_landing", TRUE),
|
||||
PLAYLISTS("FEmusic_liked_playlists", TRUE),
|
||||
PODCASTS("FEmusic_non_music_audio", TRUE),
|
||||
SUBSCRIPTIONS("FEmusic_library_corpus_artists", TRUE),
|
||||
EPISODES_FOR_LATER("VLSE", TRUE),
|
||||
LIKED_MUSIC("VLLM", TRUE),
|
||||
SEARCH("", false);
|
||||
|
||||
@NonNull
|
||||
final String id;
|
||||
|
||||
@Nullable
|
||||
final Boolean isBrowseId;
|
||||
|
||||
StartPage(@NonNull String id, @Nullable Boolean isBrowseId) {
|
||||
this.id = id;
|
||||
this.isBrowseId = isBrowseId;
|
||||
}
|
||||
|
||||
private boolean isBrowseId() {
|
||||
return TRUE.equals(isBrowseId);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String ACTION_MAIN = "android.intent.action.MAIN";
|
||||
|
||||
public static String overrideBrowseId(@Nullable String original) {
|
||||
var startPage = Settings.CHANGE_START_PAGE.get();
|
||||
|
||||
if (!startPage.isBrowseId()) {
|
||||
return original;
|
||||
}
|
||||
|
||||
if (!"FEmusic_home".equals(original)) {
|
||||
return original;
|
||||
}
|
||||
|
||||
String overrideBrowseId = startPage.id;
|
||||
if (overrideBrowseId.isEmpty()) {
|
||||
return original;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Changing browseId to: " + startPage.name());
|
||||
return overrideBrowseId;
|
||||
}
|
||||
|
||||
public static void overrideIntentActionOnCreate(@NonNull Activity activity,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
if (savedInstanceState != null) return;
|
||||
|
||||
var startPage = Settings.CHANGE_START_PAGE.get();
|
||||
if (startPage != StartPage.SEARCH) return;
|
||||
|
||||
var originalIntent = activity.getIntent();
|
||||
if (originalIntent == null) return;
|
||||
|
||||
if (ACTION_MAIN.equals(originalIntent.getAction())) {
|
||||
Logger.printDebug(() -> "Cold start: Launching search activity directly");
|
||||
var searchIntent = new Intent();
|
||||
|
||||
searchIntent.setAction(SHORTCUT_ACTION);
|
||||
searchIntent.setClassName(activity, SHORTCUT_CLASS_DESCRIPTOR);
|
||||
searchIntent.setPackage(activity.getPackageName());
|
||||
searchIntent.putExtra(SHORTCUT_TYPE, SHORTCUT_TYPE_SEARCH);
|
||||
searchIntent.putExtra(SHORTCUT_ACTION, SHORTCUT_ID_SEARCH);
|
||||
|
||||
activity.startActivity(searchIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package app.revanced.extension.music.patches;
|
||||
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ForciblyEnableMiniplayerPatch {
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static boolean enableForcedMiniplayerPatch(boolean original) {
|
||||
return Settings.FORCIBLY_ENABLE_MINIPLAYER.get() || original;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package app.revanced.extension.music.settings;
|
|||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static app.revanced.extension.music.patches.ChangeHeaderPatch.*;
|
||||
import static app.revanced.extension.music.patches.ChangeStartPagePatch.*;
|
||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||
|
||||
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
|
||||
|
|
@ -16,6 +18,7 @@ public class Settings extends YouTubeAndMusicSettings {
|
|||
public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true);
|
||||
|
||||
// General
|
||||
public static final EnumSetting<StartPage> CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.DEFAULT, true);
|
||||
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_music_hide_cast_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_music_hide_category_bar", FALSE, true);
|
||||
public static final BooleanSetting HIDE_HISTORY_BUTTON = new BooleanSetting("revanced_music_hide_history_button", FALSE, true);
|
||||
|
|
@ -28,9 +31,11 @@ public class Settings extends YouTubeAndMusicSettings {
|
|||
public static final BooleanSetting HIDE_NAVIGATION_BAR_UPGRADE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_upgrade_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_music_hide_navigation_bar", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_LABEL = new BooleanSetting("revanced_music_hide_navigation_bar_labels", FALSE, true);
|
||||
public static final EnumSetting<HeaderLogo> HEADER_LOGO = new EnumSetting<>("revanced_header_logo", HeaderLogo.DEFAULT, true);
|
||||
|
||||
// Player
|
||||
public static final BooleanSetting CHANGE_MINIPLAYER_COLOR = new BooleanSetting("revanced_music_change_miniplayer_color", FALSE, true);
|
||||
public static final BooleanSetting FORCIBLY_ENABLE_MINIPLAYER = new BooleanSetting("revanced_music_forcibly_enable_miniplayer", FALSE, true);
|
||||
public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true);
|
||||
|
||||
// Miscellaneous
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package app.revanced.extension.shared;
|
||||
|
||||
public final class ConversionContext {
|
||||
/**
|
||||
* Interface to use obfuscated methods.
|
||||
*/
|
||||
public interface ContextInterface {
|
||||
// Methods implemented by patch.
|
||||
StringBuilder patch_getPathBuilder();
|
||||
|
||||
String patch_getIdentifier();
|
||||
|
||||
default boolean isHomeFeedOrRelatedVideo() {
|
||||
return toString().contains("horizontalCollectionSwipeProtector=null");
|
||||
}
|
||||
|
||||
default boolean isSubscriptionOrLibrary() {
|
||||
return toString().contains("heightConstraint=null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -358,6 +358,41 @@ public class Utils {
|
|||
return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(ResourceType.ARRAY, resourceIdentifierName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific app package is installed and enabled on the device.
|
||||
*
|
||||
* @param packageName The application package name to check (e.g., "app.revanced.android.apps.youtube.music").
|
||||
* @return True if the package is installed and enabled, false otherwise.
|
||||
*/
|
||||
public static boolean isPackageEnabled(String packageName) {
|
||||
Context context = getContext();
|
||||
if (context == null || !isNotEmpty(packageName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return pm.getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(0)).enabled;
|
||||
} else {
|
||||
return pm.getApplicationInfo(packageName, 0).enabled;
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean startsWithAny(String value, String...targets) {
|
||||
if (isNotEmpty(value)) {
|
||||
for (String string : targets) {
|
||||
if (isNotEmpty(string) && value.startsWith(string)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface MatchFilter<T> {
|
||||
boolean matches(T object);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public final class SanitizeSharingLinksPatch {
|
|||
|
||||
private static final LinkSanitizer sanitizer = new LinkSanitizer(
|
||||
"si",
|
||||
"is", // New (localized?) tracking parameter.
|
||||
"feature" // Old tracking parameter name, and may be obsolete.
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import java.util.Map;
|
|||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext;
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
|
@ -172,14 +174,21 @@ public final class CustomFilter extends Filter {
|
|||
|
||||
if (!groups.isEmpty()) {
|
||||
CustomFilterGroup[] groupsArray = groups.toArray(new CustomFilterGroup[0]);
|
||||
Logger.printDebug(()-> "Using Custom filters: " + Arrays.toString(groupsArray));
|
||||
Logger.printDebug(() -> "Using Custom filters: " + Arrays.toString(groupsArray));
|
||||
addPathCallbacks(groupsArray);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
public boolean isFiltered(
|
||||
ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
// All callbacks are custom filter groups.
|
||||
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package app.revanced.extension.shared.patches.litho;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext;
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
|
||||
|
|
@ -9,16 +11,16 @@ import java.util.List;
|
|||
|
||||
/**
|
||||
* Filters litho based components.
|
||||
*
|
||||
* <p>
|
||||
* Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)}
|
||||
* and {@link #addPathCallbacks(StringFilterGroup...)}.
|
||||
*
|
||||
* <p>
|
||||
* To filter {@link FilterContentType#PROTOBUFFER} or {@link FilterContentType#ACCESSIBILITY}, first add a callback to
|
||||
* either an identifier or a path.
|
||||
* Then inside {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||
* Then inside {@link #isFiltered(ContextInterface, String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||
* search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
|
||||
* or a {@link FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern).
|
||||
*
|
||||
* <p>
|
||||
* All callbacks must be registered before the constructor completes.
|
||||
*/
|
||||
public abstract class Filter {
|
||||
|
|
@ -42,7 +44,7 @@ public abstract class Filter {
|
|||
public final List<StringFilterGroup> pathCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||
* Adds callbacks to {@link #isFiltered(ContextInterface, String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||
* if any of the groups are found.
|
||||
*/
|
||||
protected final void addIdentifierCallbacks(StringFilterGroup... groups) {
|
||||
|
|
@ -50,7 +52,7 @@ public abstract class Filter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||
* Adds callbacks to {@link #isFiltered(ContextInterface, String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||
* if any of the groups are found.
|
||||
*/
|
||||
protected final void addPathCallbacks(StringFilterGroup... groups) {
|
||||
|
|
@ -64,6 +66,7 @@ public abstract class Filter {
|
|||
* <p>
|
||||
* Method is called off the main thread.
|
||||
*
|
||||
* @param contextInterface The interface to get the Litho conversion context.
|
||||
* @param identifier Litho identifier.
|
||||
* @param accessibility Accessibility string, or an empty string if not present for the component.
|
||||
* @param buffer Protocol buffer.
|
||||
|
|
@ -72,8 +75,8 @@ public abstract class Filter {
|
|||
* @param contentIndex Matched index of the identifier or path.
|
||||
* @return True if the litho component should be filtered out.
|
||||
*/
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
public boolean isFiltered(ContextInterface contextInterface, String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
|
|
@ -65,23 +63,48 @@ public final class LithoFilterPatch {
|
|||
final int minimumAscii = 32; // 32 = space character
|
||||
final int maximumAscii = 126; // 127 = delete character
|
||||
final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include.
|
||||
// Logger ignores text past 4096 bytes on each line. Must wrap lines otherwise logging is clipped.
|
||||
final int preferredLineLength = 3000; // Preferred length before wrapping on next substring.
|
||||
final int maxLineLength = 3300; // Hard limit to line wrap in the middle of substring.
|
||||
String delimitingCharacter = "❙"; // Non ascii character, to allow easier log filtering.
|
||||
|
||||
final int length = buffer.length;
|
||||
final int lastIndex = length - 1;
|
||||
int start = 0;
|
||||
int end = 0;
|
||||
while (end < length) {
|
||||
int value = buffer[end];
|
||||
if (value < minimumAscii || value > maximumAscii || end == length - 1) {
|
||||
if (end - start >= minimumAsciiStringLength) {
|
||||
for (int i = start; i < end; i++) {
|
||||
int currentLineLength = 0;
|
||||
|
||||
for (int end = 0; end < length; end++) {
|
||||
final int value = buffer[end];
|
||||
final boolean isAscii = (value >= minimumAscii && value <= maximumAscii);
|
||||
final boolean atEnd = (end == lastIndex);
|
||||
|
||||
if (!isAscii || atEnd) {
|
||||
int wordEnd = end + ((atEnd && isAscii) ? 1 : 0);
|
||||
|
||||
if (wordEnd - start >= minimumAsciiStringLength) {
|
||||
for (int i = start; i < wordEnd; i++) {
|
||||
builder.append((char) buffer[i]);
|
||||
currentLineLength++;
|
||||
|
||||
// Hard line limit. Hard wrap the current substring to next logger line.
|
||||
if (currentLineLength >= maxLineLength) {
|
||||
builder.append('\n');
|
||||
currentLineLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap after substring if over preferred limit.
|
||||
if (currentLineLength >= preferredLineLength) {
|
||||
builder.append('\n');
|
||||
currentLineLength = 0;
|
||||
}
|
||||
|
||||
builder.append(delimitingCharacter);
|
||||
currentLineLength++;
|
||||
}
|
||||
|
||||
start = end + 1;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -123,11 +146,6 @@ public final class LithoFilterPatch {
|
|||
*/
|
||||
private static final boolean EXTRACT_IDENTIFIER_FROM_BUFFER = false;
|
||||
|
||||
/**
|
||||
* Turns on additional logging, used for development purposes only.
|
||||
*/
|
||||
public static final boolean DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER = false;
|
||||
|
||||
/**
|
||||
* String suffix for components.
|
||||
* Can be any of: ".eml", ".eml-fe", ".e-b", ".eml-js", "e-js-b"
|
||||
|
|
@ -146,19 +164,6 @@ public final class LithoFilterPatch {
|
|||
*/
|
||||
private static final ThreadLocal<byte[]> bufferThreadLocal = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* Identifier to protocol buffer mapping. Only used for 20.22+.
|
||||
* Thread local is needed because filtering is multithreaded and each thread can load
|
||||
* a different component with the same identifier.
|
||||
*/
|
||||
private static final ThreadLocal<Map<String, byte[]>> identifierToBufferThread = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* Global shared buffer. Used only if the buffer is not found in the ThreadLocal.
|
||||
*/
|
||||
private static final Map<String, byte[]> identifierToBufferGlobal
|
||||
= Collections.synchronizedMap(createIdentifierToBufferMap());
|
||||
|
||||
private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
|
||||
private static final StringTrieSearch identifierSearchTree = new StringTrieSearch();
|
||||
|
||||
|
|
@ -211,126 +216,11 @@ public final class LithoFilterPatch {
|
|||
}
|
||||
}
|
||||
|
||||
private static Map<String, byte[]> createIdentifierToBufferMap() {
|
||||
// It's unclear how many items should be cached. This is a guess.
|
||||
return Utils.createSizeRestrictedMap(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that differs from {@link Character#isDigit(char)}
|
||||
* as this only matches ascii and not Unicode numbers.
|
||||
*/
|
||||
private static boolean isAsciiNumber(byte character) {
|
||||
return '0' <= character && character <= '9';
|
||||
}
|
||||
|
||||
private static boolean isAsciiLowerCaseLetter(byte character) {
|
||||
return 'a' <= character && character <= 'z';
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
* Targets 20.22+
|
||||
*/
|
||||
public static void setProtoBuffer(byte[] buffer) {
|
||||
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
LithoFilterParameters.findAsciiStrings(builder, buffer);
|
||||
Logger.printDebug(() -> "New buffer: " + builder);
|
||||
}
|
||||
|
||||
// The identifier always seems to start very close to the buffer start.
|
||||
// Highest identifier start index ever observed is 50, with most around 30 to 40.
|
||||
// The buffer can be very large with up to 200kb has been observed,
|
||||
// so the search is restricted to only the start.
|
||||
final int maxBufferStartIndex = 500; // 10x expected upper bound.
|
||||
|
||||
// Could use Boyer-Moore-Horspool since the string is ASCII and has a limited number of
|
||||
// unique characters, but it seems to be slower since the extra overhead of checking the
|
||||
// bad character array negates any performance gain of skipping a few extra subsearches.
|
||||
int emlIndex = -1;
|
||||
final int emlStringLength = LITHO_COMPONENT_EXTENSION_BYTES.length;
|
||||
final int lastBufferIndexToCheckFrom = Math.min(maxBufferStartIndex, buffer.length - emlStringLength);
|
||||
for (int i = 0; i < lastBufferIndexToCheckFrom; i++) {
|
||||
boolean match = true;
|
||||
for (int j = 0; j < emlStringLength; j++) {
|
||||
if (buffer[i + j] != LITHO_COMPONENT_EXTENSION_BYTES[j]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
emlIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (emlIndex < 0) {
|
||||
// Buffer is not used for creating a new litho component.
|
||||
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||
Logger.printDebug(() -> "Could not find eml index");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int startIndex = emlIndex - 1;
|
||||
while (startIndex > 0) {
|
||||
final byte character = buffer[startIndex];
|
||||
int startIndexFinal = startIndex;
|
||||
if (isAsciiLowerCaseLetter(character) || isAsciiNumber(character) || character == '_') {
|
||||
// Valid character for the first path element.
|
||||
startIndex--;
|
||||
} else {
|
||||
startIndex++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Strip away any numbers on the start of the identifier, which can
|
||||
// be from random data in the buffer before the identifier starts.
|
||||
while (true) {
|
||||
final byte character = buffer[startIndex];
|
||||
if (isAsciiNumber(character)) {
|
||||
startIndex++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the pipe character after the identifier.
|
||||
int endIndex = -1;
|
||||
for (int i = emlIndex, length = buffer.length; i < length; i++) {
|
||||
if (buffer[i] == '|') {
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (endIndex < 0) {
|
||||
if (BaseSettings.DEBUG.get()) {
|
||||
Logger.printException(() -> "Debug: Could not find buffer identifier");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String identifier = new String(buffer, startIndex, endIndex - startIndex, StandardCharsets.US_ASCII);
|
||||
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||
Logger.printDebug(() -> "Found buffer for identifier: " + identifier);
|
||||
}
|
||||
identifierToBufferGlobal.put(identifier, buffer);
|
||||
|
||||
Map<String, byte[]> map = identifierToBufferThread.get();
|
||||
if (map == null) {
|
||||
map = createIdentifierToBufferMap();
|
||||
identifierToBufferThread.set(map);
|
||||
}
|
||||
map.put(identifier, buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
* Targets 20.21 and lower.
|
||||
*/
|
||||
public static void setProtoBuffer(@Nullable ByteBuffer buffer) {
|
||||
public static void setProtobufBuffer(@Nullable ByteBuffer buffer) {
|
||||
if (buffer == null || !buffer.hasArray()) {
|
||||
// It appears the buffer can be cleared out just before the call to #filter()
|
||||
// Ignore this null value and retain the last buffer that was set.
|
||||
|
|
@ -347,44 +237,18 @@ public final class LithoFilterPatch {
|
|||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isFiltered(String identifier, @Nullable String accessibilityId,
|
||||
@Nullable String accessibilityText, StringBuilder pathBuilder) {
|
||||
public static boolean isFiltered(ContextInterface contextInterface, @Nullable byte[] bytes,
|
||||
@Nullable String accessibilityId, @Nullable String accessibilityText) {
|
||||
try {
|
||||
String identifier = contextInterface.patch_getIdentifier();
|
||||
StringBuilder pathBuilder = contextInterface.patch_getPathBuilder();
|
||||
if (identifier.isEmpty() || pathBuilder.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] buffer = null;
|
||||
if (EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||
final int pipeIndex = identifier.indexOf('|');
|
||||
if (pipeIndex >= 0) {
|
||||
// If the identifier contains no pipe, then it's not an ".eml" identifier
|
||||
// and the buffer is not uniquely identified. Typically, this only happens
|
||||
// for subcomponents where buffer filtering is not used.
|
||||
String identifierKey = identifier.substring(0, pipeIndex);
|
||||
|
||||
var map = identifierToBufferThread.get();
|
||||
if (map != null) {
|
||||
buffer = map.get(identifierKey);
|
||||
}
|
||||
|
||||
if (buffer == null) {
|
||||
// Buffer for thread local not found. Use the last buffer found from any thread.
|
||||
buffer = identifierToBufferGlobal.get(identifierKey);
|
||||
|
||||
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER && buffer == null) {
|
||||
// No buffer is found for some components, such as
|
||||
// shorts_lockup_cell.eml on channel profiles.
|
||||
// For now, just ignore this and filter without a buffer.
|
||||
if (BaseSettings.DEBUG.get()) {
|
||||
Logger.printException(() -> "Debug: Could not find buffer for identifier: " + identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buffer = bufferThreadLocal.get();
|
||||
}
|
||||
byte[] buffer = EXTRACT_IDENTIFIER_FROM_BUFFER
|
||||
? bytes
|
||||
: bufferThreadLocal.get();
|
||||
|
||||
// Potentially the buffer may have been null or never set up until now.
|
||||
// Use an empty buffer so the litho id/path filters that do not use a buffer still work.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package app.revanced.extension.shared.settings;
|
|||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.content.Context;
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
|
@ -122,18 +122,18 @@ public abstract class Setting<T> {
|
|||
/**
|
||||
* Called after all settings have been imported.
|
||||
*/
|
||||
void settingsImported(@Nullable Context context);
|
||||
void settingsImported(@Nullable Activity context);
|
||||
|
||||
/**
|
||||
* Called after all settings have been exported.
|
||||
*/
|
||||
void settingsExported(@Nullable Context context);
|
||||
void settingsExported(@Nullable Activity context);
|
||||
}
|
||||
|
||||
private static final List<ImportExportCallback> importExportCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}.
|
||||
* Adds a callback for {@link #importFromJSON(Activity, String)} and {@link #exportToJson(Activity)}.
|
||||
*/
|
||||
public static void addImportExportCallback(ImportExportCallback callback) {
|
||||
importExportCallbacks.add(Objects.requireNonNull(callback));
|
||||
|
|
@ -413,7 +413,7 @@ public abstract class Setting<T> {
|
|||
json.put(importExportKey, value);
|
||||
}
|
||||
|
||||
public static String exportToJson(@Nullable Context alertDialogContext) {
|
||||
public static String exportToJson(@Nullable Activity alertDialogContext) {
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
for (Setting<?> setting : allLoadedSettingsSorted()) {
|
||||
|
|
@ -439,11 +439,17 @@ public abstract class Setting<T> {
|
|||
|
||||
String export = json.toString(0);
|
||||
|
||||
// Remove the outer JSON braces to make the output more compact,
|
||||
// and leave less chance of the user forgetting to copy it
|
||||
return export.substring(2, export.length() - 2);
|
||||
if (export.startsWith("{") && export.endsWith("}")) {
|
||||
// Remove the outer JSON braces to make the output more compact,
|
||||
// and leave less chance of the user forgetting to copy it
|
||||
export = export.substring(1, export.length() - 1);
|
||||
}
|
||||
|
||||
export = export.replaceAll("^\\n+", "").replaceAll("\\n+$", "");
|
||||
|
||||
return export + ",";
|
||||
} catch (JSONException e) {
|
||||
Logger.printException(() -> "Export failure", e); // should never happen
|
||||
Logger.printException(() -> "Export failure", e); // Should never happen
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
@ -451,10 +457,16 @@ public abstract class Setting<T> {
|
|||
/**
|
||||
* @return if any settings that require a reboot were changed.
|
||||
*/
|
||||
public static boolean importFromJSON(Context alertDialogContext, String settingsJsonString) {
|
||||
public static boolean importFromJSON(Activity alertDialogContext, String settingsJsonString) {
|
||||
try {
|
||||
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
|
||||
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
|
||||
settingsJsonString = settingsJsonString.trim();
|
||||
|
||||
if (settingsJsonString.endsWith(",")) {
|
||||
settingsJsonString = settingsJsonString.substring(0, settingsJsonString.length() - 1);
|
||||
}
|
||||
|
||||
if (!settingsJsonString.trim().startsWith("{")) {
|
||||
settingsJsonString = "{\n" + settingsJsonString + "\n}"; // Restore outer JSON braces
|
||||
}
|
||||
JSONObject json = new JSONObject(settingsJsonString);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,24 +3,38 @@ package app.revanced.extension.shared.settings.preference;
|
|||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceGroup;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.preference.SwitchPreference;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.util.Scanner;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.ResourceType;
|
||||
|
|
@ -33,6 +47,36 @@ import app.revanced.extension.shared.ui.CustomDialog;
|
|||
@SuppressWarnings("deprecation")
|
||||
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
private static class DebouncedListView extends ListView {
|
||||
private long lastClick;
|
||||
|
||||
public DebouncedListView(Context context) {
|
||||
super(context);
|
||||
|
||||
setId(android.R.id.list); // Required so PreferenceFragment recognizes it.
|
||||
|
||||
// Match the default layout params
|
||||
setLayoutParams(new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performItemClick(View view, int position, long id) {
|
||||
final long now = SystemClock.elapsedRealtime();
|
||||
if (now - lastClick < 500) {
|
||||
return true; // Ignore fast double click.
|
||||
}
|
||||
lastClick = now;
|
||||
|
||||
return super.performItemClick(view, position, id);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public static AbstractPreferenceFragment instance;
|
||||
|
||||
/**
|
||||
* Indicates that if a preference changes,
|
||||
* to apply the change from the Setting to the UI component.
|
||||
|
|
@ -56,6 +100,12 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||
@Nullable
|
||||
protected static CharSequence restartDialogTitle, restartDialogMessage, restartDialogButtonText, confirmDialogTitle;
|
||||
|
||||
private static final int READ_REQUEST_CODE = 42;
|
||||
private static final int WRITE_REQUEST_CODE = 43;
|
||||
private String existingSettings = "";
|
||||
|
||||
private EditText currentImportExportEditText;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||
try {
|
||||
if (updatingPreference) {
|
||||
|
|
@ -198,8 +248,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||
return listPref.getValue().equals(defaultValueString);
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Must override method to handle "
|
||||
+ "preference type: " + pref.getClass());
|
||||
throw new IllegalStateException("Must override method to handle preference type: " + pref.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -332,10 +381,235 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||
dialogPair.first.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Import / Export Subroutines
|
||||
*/
|
||||
@NonNull
|
||||
private Button createDialogButton(Context context, String text, int marginLeft, int marginRight, View.OnClickListener listener) {
|
||||
int height = (int) android.util.TypedValue.applyDimension(android.util.TypedValue.COMPLEX_UNIT_DIP, 36f, context.getResources().getDisplayMetrics());
|
||||
int paddingHorizontal = (int) android.util.TypedValue.applyDimension(android.util.TypedValue.COMPLEX_UNIT_DIP, 16f, context.getResources().getDisplayMetrics());
|
||||
float radius = android.util.TypedValue.applyDimension(android.util.TypedValue.COMPLEX_UNIT_DIP, 20f, context.getResources().getDisplayMetrics());
|
||||
|
||||
Button btn = new Button(context, null, 0);
|
||||
btn.setText(text);
|
||||
btn.setAllCaps(false);
|
||||
btn.setTextSize(14);
|
||||
btn.setSingleLine(true);
|
||||
btn.setEllipsize(android.text.TextUtils.TruncateAt.END);
|
||||
btn.setGravity(android.view.Gravity.CENTER);
|
||||
btn.setPadding(paddingHorizontal, 0, paddingHorizontal, 0);
|
||||
btn.setTextColor(Utils.isDarkModeEnabled() ? android.graphics.Color.WHITE : android.graphics.Color.BLACK);
|
||||
|
||||
android.graphics.drawable.GradientDrawable bg = new android.graphics.drawable.GradientDrawable();
|
||||
bg.setCornerRadius(radius);
|
||||
bg.setColor(Utils.getCancelOrNeutralButtonBackgroundColor());
|
||||
btn.setBackground(bg);
|
||||
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, height, 1.0f);
|
||||
params.setMargins(marginLeft, 0, marginRight, 0);
|
||||
btn.setLayoutParams(params);
|
||||
btn.setOnClickListener(listener);
|
||||
|
||||
return btn;
|
||||
}
|
||||
public void showImportExportTextDialog() {
|
||||
try {
|
||||
Activity context = getActivity();
|
||||
// Must set text before showing dialog,
|
||||
// otherwise text is non-selectable if this preference is later reopened.
|
||||
existingSettings = Setting.exportToJson(context);
|
||||
currentImportExportEditText = getEditText(context);
|
||||
|
||||
// Create a custom dialog with the EditText.
|
||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||
context,
|
||||
str("revanced_pref_import_export_title"), // Title.
|
||||
null, // No message (EditText replaces it).
|
||||
currentImportExportEditText, // Pass the EditText.
|
||||
str("revanced_settings_save"), // OK button text.
|
||||
() -> importSettingsText(context, currentImportExportEditText.getText().toString()), // OK button action.
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
str("revanced_settings_import_copy"), // Neutral button (Copy) text.
|
||||
() -> Utils.setClipboard(currentImportExportEditText.getText().toString()), // Neutral button (Copy) action. Show the user the settings in JSON format.
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
LinearLayout fileButtonsContainer = getLinearLayout(context);
|
||||
int margin = (int) android.util.TypedValue.applyDimension(android.util.TypedValue.COMPLEX_UNIT_DIP, 4f, context.getResources().getDisplayMetrics());
|
||||
|
||||
Button btnExport = createDialogButton(context, str("revanced_settings_export_file"), 0, margin, v -> exportActivity());
|
||||
Button btnImport = createDialogButton(context, str("revanced_settings_import_file"), margin, 0, v -> importActivity());
|
||||
|
||||
fileButtonsContainer.addView(btnExport);
|
||||
fileButtonsContainer.addView(btnImport);
|
||||
|
||||
dialogPair.second.addView(fileButtonsContainer, 2);
|
||||
|
||||
dialogPair.first.setOnDismissListener(d -> currentImportExportEditText = null);
|
||||
|
||||
// If there are no settings yet, then show the on-screen keyboard and bring focus to
|
||||
// the edit text. This makes it easier to paste saved settings after a reinstallation.
|
||||
dialogPair.first.setOnShowListener(dialogInterface -> {
|
||||
if (existingSettings.isEmpty() && currentImportExportEditText != null) {
|
||||
currentImportExportEditText.postDelayed(() -> {
|
||||
if (currentImportExportEditText != null) {
|
||||
currentImportExportEditText.requestFocus();
|
||||
android.view.inputmethod.InputMethodManager imm = (android.view.inputmethod.InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null) imm.showSoftInput(currentImportExportEditText, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showImportExportTextDialog failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static LinearLayout getLinearLayout(Context context) {
|
||||
LinearLayout fileButtonsContainer = new LinearLayout(context);
|
||||
fileButtonsContainer.setOrientation(LinearLayout.HORIZONTAL);
|
||||
LinearLayout.LayoutParams fbParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
int marginTop = (int) android.util.TypedValue.applyDimension(android.util.TypedValue.COMPLEX_UNIT_DIP, 16f, context.getResources().getDisplayMetrics());
|
||||
fbParams.setMargins(0, marginTop, 0, 0);
|
||||
fileButtonsContainer.setLayoutParams(fbParams);
|
||||
return fileButtonsContainer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private EditText getEditText(Context context) {
|
||||
EditText editText = new EditText(context);
|
||||
editText.setText(existingSettings);
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
editText.setAutofillHints((String) null);
|
||||
}
|
||||
editText.setInputType(android.text.InputType.TYPE_CLASS_TEXT |
|
||||
android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS |
|
||||
android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE);
|
||||
editText.setSingleLine(false);
|
||||
editText.setTextSize(14);
|
||||
return editText;
|
||||
}
|
||||
|
||||
public void exportActivity() {
|
||||
try {
|
||||
Setting.exportToJson(getActivity());
|
||||
|
||||
String formatDate = new java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.US).format(new java.util.Date());
|
||||
String fileName = "revanced_Settings_" + formatDate + ".txt";
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TITLE, fileName);
|
||||
startActivityForResult(intent, WRITE_REQUEST_CODE);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "exportActivity failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void importActivity() {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, READ_REQUEST_CODE);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "importActivity failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return new DebouncedListView(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == WRITE_REQUEST_CODE && resultCode == android.app.Activity.RESULT_OK && data != null) {
|
||||
exportTextToFile(data.getData());
|
||||
} else if (requestCode == READ_REQUEST_CODE && resultCode == android.app.Activity.RESULT_OK && data != null) {
|
||||
importTextFromFile(data.getData());
|
||||
}
|
||||
}
|
||||
|
||||
protected static void showLocalizedToast(String resourceKey, String fallbackMessage) {
|
||||
if (Utils.getResourceIdentifier(ResourceType.STRING, resourceKey) != 0) {
|
||||
Utils.showToastLong(str(resourceKey));
|
||||
} else {
|
||||
Utils.showToastLong(fallbackMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportTextToFile(android.net.Uri uri) {
|
||||
try {
|
||||
OutputStream out = getContext().getContentResolver().openOutputStream(uri);
|
||||
if (out != null) {
|
||||
String textToExport = existingSettings;
|
||||
if (currentImportExportEditText != null) {
|
||||
textToExport = currentImportExportEditText.getText().toString();
|
||||
}
|
||||
out.write(textToExport.getBytes(StandardCharsets.UTF_8));
|
||||
out.close();
|
||||
|
||||
showLocalizedToast("revanced_settings_export_file_success", "Settings exported successfully");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
showLocalizedToast("revanced_settings_export_file_failed", "Failed to export settings");
|
||||
Logger.printException(() -> "exportTextToFile failure", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("CharsetObjectCanBeUsed")
|
||||
private void importTextFromFile(android.net.Uri uri) {
|
||||
try {
|
||||
InputStream in = getContext().getContentResolver().openInputStream(uri);
|
||||
if (in != null) {
|
||||
Scanner scanner = new Scanner(in, StandardCharsets.UTF_8.name()).useDelimiter("\\A");
|
||||
String result = scanner.hasNext() ? scanner.next() : "";
|
||||
in.close();
|
||||
|
||||
if (currentImportExportEditText != null) {
|
||||
currentImportExportEditText.setText(result);
|
||||
showLocalizedToast("revanced_settings_import_file_success", "Settings imported successfully, tap Save to apply");
|
||||
} else {
|
||||
importSettingsText(getContext(), result);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
showLocalizedToast("revanced_settings_import_file_failed", "Failed to import settings");
|
||||
Logger.printException(() -> "importTextFromFile failure", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void importSettingsText(Context context, String replacementSettings) {
|
||||
try {
|
||||
existingSettings = Setting.exportToJson(null);
|
||||
if (replacementSettings.equals(existingSettings)) {
|
||||
return;
|
||||
}
|
||||
settingImportInProgress = true;
|
||||
final boolean rebootNeeded = Setting.importFromJSON(getActivity(), replacementSettings);
|
||||
if (rebootNeeded) {
|
||||
showRestartDialog(context);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "importSettingsText failure", ex);
|
||||
} finally {
|
||||
settingImportInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
instance = this;
|
||||
try {
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setSharedPreferencesName(Setting.preferences.name);
|
||||
|
|
@ -354,6 +628,9 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (instance == this) {
|
||||
instance = null;
|
||||
}
|
||||
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,40 +4,13 @@ import static app.revanced.extension.shared.StringRef.str;
|
|||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.Preference;
|
||||
import android.text.InputType;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.ui.CustomDialog;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
||||
|
||||
private String existingSettings;
|
||||
|
||||
private void init() {
|
||||
setSelectable(true);
|
||||
|
||||
EditText editText = getEditText();
|
||||
editText.setTextIsSelectable(true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
editText.setAutofillHints((String) null);
|
||||
}
|
||||
editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
editText.setTextSize(14);
|
||||
|
||||
setOnPreferenceClickListener(this);
|
||||
}
|
||||
public class ImportExportPreference extends Preference implements Preference.OnPreferenceClickListener {
|
||||
|
||||
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
|
|
@ -56,78 +29,20 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setOnPreferenceClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
try {
|
||||
// Must set text before showing dialog,
|
||||
// otherwise text is non-selectable if this preference is later reopened.
|
||||
existingSettings = Setting.exportToJson(getContext());
|
||||
getEditText().setText(existingSettings);
|
||||
if (AbstractPreferenceFragment.instance != null) {
|
||||
AbstractPreferenceFragment.instance.showImportExportTextDialog();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showDialog failure", ex);
|
||||
Logger.printException(() -> "onPreferenceClick failure", ex);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showDialog(Bundle state) {
|
||||
try {
|
||||
Context context = getContext();
|
||||
EditText editText = getEditText();
|
||||
|
||||
// Create a custom dialog with the EditText.
|
||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||
context,
|
||||
str("revanced_pref_import_export_title"), // Title.
|
||||
null, // No message (EditText replaces it).
|
||||
editText, // Pass the EditText.
|
||||
str("revanced_settings_import"), // OK button text.
|
||||
() -> importSettings(context, editText.getText().toString()), // OK button action.
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
str("revanced_settings_import_copy"), // Neutral button (Copy) text.
|
||||
() -> {
|
||||
// Neutral button (Copy) action. Show the user the settings in JSON format.
|
||||
Utils.setClipboard(editText.getText());
|
||||
},
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// If there are no settings yet, then show the on screen keyboard and bring focus to
|
||||
// the edit text. This makes it easier to paste saved settings after a reinstall.
|
||||
dialogPair.first.setOnShowListener(dialogInterface -> {
|
||||
if (existingSettings.isEmpty()) {
|
||||
editText.postDelayed(() -> {
|
||||
editText.requestFocus();
|
||||
|
||||
InputMethodManager inputMethodManager = (InputMethodManager)
|
||||
editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showDialog failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void importSettings(Context context, String replacementSettings) {
|
||||
try {
|
||||
if (replacementSettings.equals(existingSettings)) {
|
||||
return;
|
||||
}
|
||||
AbstractPreferenceFragment.settingImportInProgress = true;
|
||||
|
||||
final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings);
|
||||
if (rebootNeeded) {
|
||||
AbstractPreferenceFragment.showRestartDialog(context);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "importSettings failure", ex);
|
||||
} finally {
|
||||
AbstractPreferenceFragment.settingImportInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public enum ClientType {
|
|||
Build.VERSION.RELEASE,
|
||||
String.valueOf(Build.VERSION.SDK_INT),
|
||||
Build.ID,
|
||||
"20.44.38",
|
||||
"20.45.36",
|
||||
// This client has been used by most open-source YouTube stream extraction tools since 2024, including NewPipe Extractor, SmartTube, and Grayjay.
|
||||
// This client can log in, but if an access token is used in the request, GVS can more easily identify the request as coming from ReVanced.
|
||||
// This means that the GVS server can strengthen its validation of the ANDROID_REEL client.
|
||||
|
|
|
|||
|
|
@ -2,13 +2,10 @@ package app.revanced.extension.youtube.patches;
|
|||
|
||||
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
|
@ -20,7 +17,7 @@ public class ChangeFormFactorPatch {
|
|||
/**
|
||||
* Unmodified, and same as un-patched.
|
||||
*/
|
||||
DEFAULT(null),
|
||||
DEFAULT(null, false),
|
||||
/**
|
||||
* <pre>
|
||||
* Some changes include:
|
||||
|
|
@ -28,60 +25,56 @@ public class ChangeFormFactorPatch {
|
|||
* - watch history is missing.
|
||||
* - feed thumbnails fade in.
|
||||
*/
|
||||
UNKNOWN(0),
|
||||
SMALL(1),
|
||||
LARGE(2),
|
||||
UNKNOWN(0, true),
|
||||
SMALL(1, false),
|
||||
LARGE(2, false),
|
||||
/**
|
||||
* Cars with 'Google built-in'.
|
||||
* Layout seems identical to {@link #UNKNOWN}
|
||||
* even when using an Android Automotive device.
|
||||
*/
|
||||
AUTOMOTIVE(3),
|
||||
WEARABLE(4);
|
||||
AUTOMOTIVE(3, true),
|
||||
WEARABLE(4, true);
|
||||
|
||||
@Nullable
|
||||
final Integer formFactorType;
|
||||
final boolean isBroken;
|
||||
|
||||
FormFactor(@Nullable Integer formFactorType) {
|
||||
FormFactor(@Nullable Integer formFactorType, boolean isBroken) {
|
||||
this.formFactorType = formFactorType;
|
||||
this.isBroken = isBroken;
|
||||
}
|
||||
}
|
||||
|
||||
private static final FormFactor FORM_FACTOR = Settings.CHANGE_FORM_FACTOR.get();
|
||||
|
||||
@Nullable
|
||||
private static final Integer FORM_FACTOR_TYPE = Settings.CHANGE_FORM_FACTOR.get().formFactorType;
|
||||
private static final boolean USING_AUTOMOTIVE_TYPE = Objects.requireNonNull(
|
||||
FormFactor.AUTOMOTIVE.formFactorType).equals(FORM_FACTOR_TYPE);
|
||||
private static final Integer FORM_FACTOR_TYPE = FORM_FACTOR.formFactorType;
|
||||
private static final boolean IS_BROKEN_FORM_FACTOR = FORM_FACTOR.isBroken;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Called before {@link #replaceBrokenFormFactor(int)}.
|
||||
* Called from all endpoints.
|
||||
*/
|
||||
public static int getFormFactor(int original) {
|
||||
if (FORM_FACTOR_TYPE == null) return original;
|
||||
|
||||
if (USING_AUTOMOTIVE_TYPE) {
|
||||
// Do not change if the player is opening or is opened,
|
||||
// otherwise the video description cannot be opened.
|
||||
PlayerType current = PlayerType.getCurrent();
|
||||
if (current.isMaximizedOrFullscreen() || current == PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED) {
|
||||
Logger.printDebug(() -> "Using original form factor for player");
|
||||
return original;
|
||||
}
|
||||
|
||||
if (!NavigationBar.isSearchBarActive()) {
|
||||
// Automotive type shows error 400 when opening a channel page and using some explore tab.
|
||||
// This is a bug in unpatched YouTube that occurs on actual Android Automotive devices.
|
||||
// Work around the issue by using the original form factor if not in search and the
|
||||
// navigation back button is present.
|
||||
if (NavigationBar.isBackButtonVisible()) {
|
||||
Logger.printDebug(() -> "Using original form factor, as back button is visible without search present");
|
||||
return original;
|
||||
}
|
||||
|
||||
// Do not change library tab otherwise watch history is hidden.
|
||||
// Do this check last since the current navigation button is required.
|
||||
if (NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY) {
|
||||
return original;
|
||||
}
|
||||
public static int getUniversalFormFactor(int original) {
|
||||
if (FORM_FACTOR_TYPE == null) {
|
||||
return original;
|
||||
}
|
||||
if (IS_BROKEN_FORM_FACTOR
|
||||
&& !PlayerType.getCurrent().isMaximizedOrFullscreen()
|
||||
&& !NavigationBar.isSearchBarActive()) {
|
||||
// Automotive type shows error 400 when opening a channel page and using some explore tab.
|
||||
// This is a bug in unpatched YouTube that occurs on actual Android Automotive devices.
|
||||
// Work around the issue by using the tablet form factor if not in search and the
|
||||
// navigation back button is present.
|
||||
if (NavigationBar.isBackButtonVisible()
|
||||
// Do not change library tab otherwise watch history is hidden.
|
||||
// Do this check last since the current navigation button is required.
|
||||
|| NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY) {
|
||||
// The form factor most similar to AUTOMOTIVE is LARGE, so it is replaced with LARGE.
|
||||
return Optional.ofNullable(FormFactor.LARGE.formFactorType).orElse(original);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,16 +83,31 @@ public class ChangeFormFactorPatch {
|
|||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Called after {@link #getUniversalFormFactor(int)}.
|
||||
* Called from the '/get_watch', '/guide', '/next' and '/reel' endpoints.
|
||||
* <p>
|
||||
* The '/guide' endpoint relates to navigation buttons.
|
||||
* If {@link #IS_BROKEN_FORM_FACTOR} is true in this endpoint,
|
||||
* the explore button (which is deprecated) will be shown.
|
||||
* <p>
|
||||
* The '/get_watch' and '/next' endpoints relate to elements below the player (channel bar, comments, related videos).
|
||||
* If {@link #IS_BROKEN_FORM_FACTOR} is true in this endpoint,
|
||||
* the video description panel will not open.
|
||||
* <p>
|
||||
* The '/reel' endpoint relates to Shorts player.
|
||||
* If {@link #IS_BROKEN_FORM_FACTOR} is true in this endpoint,
|
||||
* the Shorts comment panel will not open.
|
||||
*/
|
||||
public static void navigationTabCreated(NavigationButton button, View tabView) {
|
||||
// On first startup of the app the navigation buttons are fetched and updated.
|
||||
// If the user immediately opens the 'You' or opens a video, then the call to
|
||||
// update the navigation buttons will use the non-automotive form factor
|
||||
// and the explore tab is missing.
|
||||
// Fixing this is not so simple because of the concurrent calls for the player and You tab.
|
||||
// For now, always hide the explore tab.
|
||||
if (USING_AUTOMOTIVE_TYPE && button == NavigationButton.EXPLORE) {
|
||||
tabView.setVisibility(View.GONE);
|
||||
public static int replaceBrokenFormFactor(int original) {
|
||||
if (FORM_FACTOR_TYPE == null) {
|
||||
return original;
|
||||
}
|
||||
if (IS_BROKEN_FORM_FACTOR) {
|
||||
// The form factor most similar to AUTOMOTIVE is LARGE, so it is replaced with LARGE.
|
||||
return Optional.ofNullable(FormFactor.LARGE.formFactorType).orElse(original);
|
||||
} else {
|
||||
return original;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DisableResumingShortsOnStartupPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableResumingShortsOnStartup() {
|
||||
return Settings.DISABLE_RESUMING_SHORTS_ON_STARTUP.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableResumingShortsOnStartup(boolean original) {
|
||||
return original && !Settings.DISABLE_RESUMING_SHORTS_ON_STARTUP.get();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DisableResumingStartupShortsPlayerPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableResumingStartupShortsPlayer() {
|
||||
return Settings.DISABLE_RESUMING_SHORTS_PLAYER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableResumingStartupShortsPlayer(boolean original) {
|
||||
return original && !Settings.DISABLE_RESUMING_SHORTS_PLAYER.get();
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,6 @@ public final class HideRelatedVideoOverlayPatch {
|
|||
* Injection point.
|
||||
*/
|
||||
public static boolean hideRelatedVideoOverlay() {
|
||||
return Settings.HIDE_RELATED_VIDEOS_OVERLAY.get();
|
||||
return Settings.HIDE_PLAYER_RELATED_VIDEOS_OVERLAY.get();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
/**
|
||||
* Here is an unintended behavior:
|
||||
* <p>
|
||||
* 1. The user does not hide Shorts in the Subscriptions tab, but hides them otherwise.
|
||||
* 2. Goes to the Subscriptions tab and scrolls to where Shorts is.
|
||||
* 3. Opens a regular video.
|
||||
* 4. Minimizes the video and turns off the screen.
|
||||
* 5. Turns the screen on and maximizes the video.
|
||||
* 6. Shorts belonging to related videos are not hidden.
|
||||
* <p>
|
||||
* Here is an explanation of this special issue:
|
||||
* <p>
|
||||
* When the user minimizes the video, turns off the screen, and then turns it back on,
|
||||
* the components below the player are reloaded, and at this moment the PlayerType is [WATCH_WHILE_MINIMIZED].
|
||||
* (Shorts belonging to related videos are also reloaded)
|
||||
* Since the PlayerType is [WATCH_WHILE_MINIMIZED] at this moment, the navigation tab is checked.
|
||||
* (Even though PlayerType is [WATCH_WHILE_MINIMIZED], this is a Shorts belonging to a related video)
|
||||
* <p>
|
||||
* As a workaround for this special issue, if a video actionbar is detected, which is one of the components below the player,
|
||||
* it is treated as being in the same state as [WATCH_WHILE_MAXIMIZED].
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class LayoutReloadObserverPatch {
|
||||
private static final String COMPACTIFY_VIDEO_ACTION_BAR_PREFIX = "compactify_video_action_bar.e";
|
||||
private static final String VIDEO_ACTION_BAR_PREFIX = "video_action_bar.e";
|
||||
public static final AtomicBoolean isActionBarVisible = new AtomicBoolean(false);
|
||||
|
||||
public static void onLazilyConvertedElementLoaded(@NonNull String identifier,
|
||||
@NonNull List<Object> treeNodeResultList) {
|
||||
if (!Utils.startsWithAny(identifier, COMPACTIFY_VIDEO_ACTION_BAR_PREFIX, VIDEO_ACTION_BAR_PREFIX)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerType playerType = PlayerType.getCurrent();
|
||||
if (playerType == PlayerType.WATCH_WHILE_MINIMIZED || playerType == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) {
|
||||
if (isActionBarVisible.compareAndSet(false, true)) {
|
||||
Utils.runOnMainThreadDelayed(() -> isActionBarVisible.compareAndSet(true, false), 250);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class LazilyConvertedElementPatch {
|
||||
private static final String LAZILY_CONVERTED_ELEMENT = "LazilyConvertedElement";
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void onTreeNodeResultLoaded(ContextInterface contextInterface, List<Object> treeNodeResultList) {
|
||||
if (treeNodeResultList == null || treeNodeResultList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String firstElement = treeNodeResultList.get(0).toString();
|
||||
if (!LAZILY_CONVERTED_ELEMENT.equals(firstElement)) {
|
||||
return;
|
||||
}
|
||||
String identifier = contextInterface.patch_getIdentifier();
|
||||
if (Utils.isNotEmpty(identifier)) {
|
||||
onLazilyConvertedElementLoaded(identifier, treeNodeResultList);
|
||||
}
|
||||
}
|
||||
|
||||
private static void onLazilyConvertedElementLoaded(String identifier, List<Object> treeNodeResultList) {
|
||||
// Code added by patch.
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class OverrideOpenInYouTubeMusicButtonPatch {
|
||||
|
||||
private static final String YOUTUBE_MUSIC_PACKAGE_NAME = "com.google.android.apps.youtube.music";
|
||||
|
||||
private static final Boolean overrideButton = Settings.OVERRIDE_OPEN_IN_YOUTUBE_MUSIC_BUTTON.get();
|
||||
|
||||
private static final String overridePackageName = getOverridePackageName();
|
||||
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
public static String getOverridePackageName() {
|
||||
return ""; // Value is replaced during patching.
|
||||
}
|
||||
|
||||
public static @Nullable Intent overrideSetPackage(@Nullable Intent intent, @Nullable String packageName) {
|
||||
if (intent == null || !overrideButton) return intent;
|
||||
|
||||
if (YOUTUBE_MUSIC_PACKAGE_NAME.equals(packageName)) {
|
||||
if (Utils.isNotEmpty(overridePackageName) && Utils.isPackageEnabled(overridePackageName)) {
|
||||
return intent.setPackage(overridePackageName);
|
||||
}
|
||||
|
||||
return intent.setPackage(null);
|
||||
}
|
||||
|
||||
return intent.setPackage(packageName);
|
||||
}
|
||||
|
||||
public static @Nullable Intent overrideSetData(@Nullable Intent intent, @Nullable Uri uri) {
|
||||
if (intent == null || uri == null || !overrideButton) return intent;
|
||||
|
||||
String uriString = uri.toString();
|
||||
if (uriString.contains(YOUTUBE_MUSIC_PACKAGE_NAME)) {
|
||||
if ("market".equals(uri.getScheme()) || uriString.contains("play.google.com/store/apps")) {
|
||||
intent.setData(Uri.parse("https://music.youtube.com/"));
|
||||
|
||||
if (Utils.isNotEmpty(overridePackageName) && Utils.isPackageEnabled(overridePackageName)) {
|
||||
intent.setPackage(overridePackageName);
|
||||
} else {
|
||||
intent.setPackage(null);
|
||||
}
|
||||
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
|
||||
return intent.setData(uri);
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +54,6 @@ public class PlayerControlsPatch {
|
|||
|
||||
// noinspection EmptyMethod
|
||||
private static void fullscreenButtonVisibilityChanged(boolean isVisible) {
|
||||
// Code added during patching.
|
||||
// Code added by patch.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,24 +19,25 @@ import app.revanced.extension.shared.Utils;
|
|||
import app.revanced.extension.youtube.patches.litho.ReturnYouTubeDislikeFilter;
|
||||
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
/**
|
||||
* Handles all interaction of UI patch components.
|
||||
*
|
||||
* <p>
|
||||
* Known limitation:
|
||||
* The implementation of Shorts litho requires blocking the loading the first Short until RYD has completed.
|
||||
* This is because it modifies the dislikes text synchronously, and if the RYD fetch has
|
||||
* not completed yet then the UI will be temporarily frozen.
|
||||
*
|
||||
* <p>
|
||||
* A (yet to be implemented) solution that fixes this problem. Any one of:
|
||||
* - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes text asynchronously.
|
||||
* - Find a way to force Litho to rebuild it's component tree,
|
||||
* and use that hook to force the shorts dislikes to update after the fetch is completed.
|
||||
* and use that hook to force the shorts dislikes to update after the fetch is completed.
|
||||
* - Hook into the dislikes button image view, and replace the dislikes thumb down image with a
|
||||
* generated image of the number of dislikes, then update the image asynchronously. This Could
|
||||
* also be used for the regular video player to give a better UI layout and completely remove
|
||||
* the need for the Rolling Number patches.
|
||||
* generated image of the number of dislikes, then update the image asynchronously. This Could
|
||||
* also be used for the regular video player to give a better UI layout and completely remove
|
||||
* the need for the Rolling Number patches.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ReturnYouTubeDislikePatch {
|
||||
|
|
@ -100,7 +101,7 @@ public class ReturnYouTubeDislikePatch {
|
|||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* <p>
|
||||
* Logs if new litho text layout is used.
|
||||
*/
|
||||
public static boolean useNewLithoTextCreation(boolean useNewLithoTextCreation) {
|
||||
|
|
@ -113,28 +114,28 @@ public class ReturnYouTubeDislikePatch {
|
|||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* <p>
|
||||
* For Litho segmented buttons and Litho Shorts player.
|
||||
*/
|
||||
@NonNull
|
||||
public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
|
||||
@NonNull CharSequence original) {
|
||||
return onLithoTextLoaded(conversionContext, original, false);
|
||||
public static CharSequence onLithoTextLoaded(ContextInterface contextInterface,
|
||||
CharSequence original) {
|
||||
return onLithoTextLoaded(contextInterface, original, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a litho text component is initially created,
|
||||
* and also when a Span is later reused again (such as scrolling off/on screen).
|
||||
*
|
||||
* <p>
|
||||
* This method is sometimes called on the main thread, but it is usually called _off_ the main thread.
|
||||
* This method can be called multiple times for the same UI element (including after dislikes was added).
|
||||
*
|
||||
* @param original Original char sequence was created or reused by Litho.
|
||||
* @param original Original char sequence was created or reused by Litho.
|
||||
* @param isRollingNumber If the span is for a Rolling Number.
|
||||
* @return The original char sequence (if nothing should change), or a replacement char sequence that contains dislikes.
|
||||
*/
|
||||
@NonNull
|
||||
private static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
|
||||
private static CharSequence onLithoTextLoaded(ContextInterface contextInterface,
|
||||
@NonNull CharSequence original,
|
||||
boolean isRollingNumber) {
|
||||
try {
|
||||
|
|
@ -142,17 +143,16 @@ public class ReturnYouTubeDislikePatch {
|
|||
return original;
|
||||
}
|
||||
|
||||
String conversionContextString = conversionContext.toString();
|
||||
|
||||
if (Settings.RYD_ENABLED.get()) { // FIXME: Remove this.
|
||||
Logger.printDebug(() -> "RYD conversion context: " + conversionContext);
|
||||
}
|
||||
|
||||
if (isRollingNumber && !conversionContextString.contains("video_action_bar.e")) {
|
||||
String identifier = contextInterface.patch_getIdentifier();
|
||||
if (isRollingNumber && (identifier == null || !identifier.contains("video_action_bar.e"))) {
|
||||
return original;
|
||||
}
|
||||
|
||||
if (conversionContextString.contains("segmented_like_dislike_button.e")) {
|
||||
StringBuilder pathBuilder = contextInterface.patch_getPathBuilder();
|
||||
String path = pathBuilder.toString();
|
||||
|
||||
if (path.contains("segmented_like_dislike_button.e")) {
|
||||
|
||||
// Regular video.
|
||||
ReturnYouTubeDislike videoData = currentVideoData;
|
||||
if (videoData == null) {
|
||||
|
|
@ -169,13 +169,11 @@ public class ReturnYouTubeDislikePatch {
|
|||
return original; // No need to check for Shorts in the context.
|
||||
}
|
||||
|
||||
if (Utils.containsAny(conversionContextString,
|
||||
"|shorts_dislike_button.e", "|reel_dislike_button.e")) {
|
||||
if (Utils.containsAny(path, "|shorts_dislike_button.e", "|reel_dislike_button.e")) {
|
||||
return getShortsSpan(original, true);
|
||||
}
|
||||
|
||||
if (Utils.containsAny(conversionContextString,
|
||||
"|shorts_like_button.e", "|reel_like_button.e")) {
|
||||
if (Utils.containsAny(path, "|shorts_like_button.e", "|reel_like_button.e")) {
|
||||
if (!Utils.containsNumber(original)) {
|
||||
Logger.printDebug(() -> "Replacing hidden likes count");
|
||||
return getShortsSpan(original, false);
|
||||
|
|
@ -230,10 +228,9 @@ public class ReturnYouTubeDislikePatch {
|
|||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String onRollingNumberLoaded(@NonNull Object conversionContext,
|
||||
@NonNull String original) {
|
||||
public static String onRollingNumberLoaded(ContextInterface contextInterface, String original) {
|
||||
try {
|
||||
CharSequence replacement = onLithoTextLoaded(conversionContext, original, true);
|
||||
CharSequence replacement = onLithoTextLoaded(contextInterface, original, true);
|
||||
|
||||
String replacementString = replacement.toString();
|
||||
if (!replacementString.equals(original)) {
|
||||
|
|
@ -248,7 +245,7 @@ public class ReturnYouTubeDislikePatch {
|
|||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* <p>
|
||||
* Called for all usage of Rolling Number.
|
||||
* Modifies the measured String text width to include the left separator and padding, if needed.
|
||||
*/
|
||||
|
|
@ -489,7 +486,7 @@ public class ReturnYouTubeDislikePatch {
|
|||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* <p>
|
||||
* Called when the user likes or dislikes.
|
||||
*
|
||||
* @param vote int that matches {@link Vote#value}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import static app.revanced.extension.youtube.patches.VersionCheckPatch.IS_21_10_OR_GREATER;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
|
@ -24,7 +26,12 @@ public class ShortsAutoplayPatch {
|
|||
/**
|
||||
* Pause playback after 1 play.
|
||||
*/
|
||||
END_SCREEN;
|
||||
END_SCREEN,
|
||||
/**
|
||||
* Play once, then advanced to the next Short.
|
||||
* Only found in 21.10+
|
||||
*/
|
||||
AUTO_ADVANCE;
|
||||
|
||||
static void setYTEnumValue(Enum<?> ytBehavior) {
|
||||
for (ShortsLoopBehavior rvBehavior : values()) {
|
||||
|
|
@ -93,8 +100,12 @@ public class ShortsAutoplayPatch {
|
|||
autoplay = Settings.SHORTS_AUTOPLAY.get();
|
||||
}
|
||||
|
||||
ShortsLoopBehavior autoPlayBehavior = IS_21_10_OR_GREATER
|
||||
? ShortsLoopBehavior.AUTO_ADVANCE
|
||||
: ShortsLoopBehavior.SINGLE_PLAY;
|
||||
|
||||
Enum<?> overrideBehavior = (autoplay
|
||||
? ShortsLoopBehavior.SINGLE_PLAY
|
||||
? autoPlayBehavior
|
||||
: ShortsLoopBehavior.REPEAT).ytEnumValue;
|
||||
|
||||
if (overrideBehavior != null) {
|
||||
|
|
|
|||
|
|
@ -27,4 +27,6 @@ public class VersionCheckPatch {
|
|||
public static final boolean IS_20_31_OR_GREATER = isVersionOrGreater("20.31.00");
|
||||
|
||||
public static final boolean IS_20_37_OR_GREATER = isVersionOrGreater("20.37.00");
|
||||
|
||||
public static final boolean IS_21_10_OR_GREATER = isVersionOrGreater("21.10.00");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,23 +4,22 @@ import app.revanced.extension.youtube.settings.Settings;
|
|||
|
||||
@SuppressWarnings("unused")
|
||||
public class VideoAdsPatch {
|
||||
|
||||
private static final boolean SHOW_VIDEO_ADS = !Settings.HIDE_VIDEO_ADS.get();
|
||||
private static final boolean HIDE_VIDEO_ADS = Settings.HIDE_VIDEO_ADS.get();
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean shouldShowAds() {
|
||||
return SHOW_VIDEO_ADS;
|
||||
public static boolean hideVideoAds() {
|
||||
return HIDE_VIDEO_ADS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String hideShortsAds(String osName) {
|
||||
return SHOW_VIDEO_ADS
|
||||
? osName
|
||||
: "Android Automotive";
|
||||
public static String hideVideoAds(String osName) {
|
||||
return HIDE_VIDEO_ADS
|
||||
? "Android Automotive"
|
||||
: osName;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
|
@ -167,8 +168,14 @@ public final class AdsFilter extends Filter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
public boolean isFiltered(ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
if (matchedGroup == buyMovieAd) {
|
||||
return contentIndex == 0 && buyMovieAdBuffer.check(buffer).isFiltered();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package app.revanced.extension.youtube.patches.litho;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext;
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQualityMenuPatch;
|
||||
|
|
@ -21,8 +23,14 @@ public final class AdvancedVideoQualityMenuFilter extends Filter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
public boolean isFiltered(ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
isVideoQualityMenuVisible = true;
|
||||
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
package app.revanced.extension.youtube.patches.litho;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
|
@ -8,11 +14,10 @@ import app.revanced.extension.youtube.shared.PlayerType;
|
|||
@SuppressWarnings("unused")
|
||||
public final class CommentsFilter extends Filter {
|
||||
|
||||
private static final String CHIP_BAR_PATH_PREFIX = "chip_bar.e";
|
||||
private static final String COMMENT_COMPOSER_PATH = "comment_composer.e";
|
||||
private static final String VIDEO_LOCKUP_WITH_ATTACHMENT_PATH = "video_lockup_with_attachment.e";
|
||||
|
||||
private final StringFilterGroup chipBar;
|
||||
private final ByteArrayFilterGroup aiCommentsSummary;
|
||||
private final StringFilterGroup comments;
|
||||
private final StringFilterGroup emojiAndTimestampButtons;
|
||||
|
||||
|
|
@ -22,16 +27,6 @@ public final class CommentsFilter extends Filter {
|
|||
"live_chat_summary_banner.e"
|
||||
);
|
||||
|
||||
chipBar = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_AI_SUMMARY,
|
||||
"chip_bar.e"
|
||||
);
|
||||
|
||||
aiCommentsSummary = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"yt_fill_spark_"
|
||||
);
|
||||
|
||||
var channelGuidelines = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_CHANNEL_GUIDELINES,
|
||||
"channel_guidelines_entry_banner"
|
||||
|
|
@ -79,7 +74,6 @@ public final class CommentsFilter extends Filter {
|
|||
addPathCallbacks(
|
||||
channelGuidelines,
|
||||
chatSummary,
|
||||
chipBar,
|
||||
commentsByMembers,
|
||||
comments,
|
||||
communityGuidelines,
|
||||
|
|
@ -92,25 +86,44 @@ public final class CommentsFilter extends Filter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == chipBar) {
|
||||
// Playlist sort button uses same components and must only filter if the player is opened.
|
||||
return PlayerType.getCurrent().isMaximizedOrFullscreen()
|
||||
&& aiCommentsSummary.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
public boolean isFiltered(ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
if (matchedGroup == comments) {
|
||||
if (path.startsWith(VIDEO_LOCKUP_WITH_ATTACHMENT_PATH)) {
|
||||
return Settings.HIDE_COMMENTS_SECTION_IN_HOME_FEED.get();
|
||||
}
|
||||
return Settings.HIDE_COMMENTS_SECTION.get();
|
||||
}
|
||||
|
||||
if (matchedGroup == emojiAndTimestampButtons) {
|
||||
} else if (matchedGroup == emojiAndTimestampButtons) {
|
||||
return path.startsWith(COMMENT_COMPOSER_PATH);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void sanitizeCommentsCategoryBar(@NonNull String identifier,
|
||||
@NonNull List<Object> treeNodeResultList) {
|
||||
try {
|
||||
if (Settings.SANITIZE_COMMENTS_CATEGORY_BAR.get()
|
||||
&& identifier.startsWith(CHIP_BAR_PATH_PREFIX)
|
||||
// Playlist sort button uses same components and must only filter if the player is opened.
|
||||
&& PlayerType.getCurrent().isMaximizedOrFullscreen()
|
||||
) {
|
||||
int treeNodeResultListSize = treeNodeResultList.size();
|
||||
if (treeNodeResultListSize > 2) {
|
||||
treeNodeResultList.subList(1, treeNodeResultListSize - 1).clear();
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to sanitize comment category bar", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package app.revanced.extension.youtube.patches.litho;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||
|
|
@ -57,12 +58,13 @@ public final class DescriptionComponentsFilter extends Filter {
|
|||
playlistSectionGroupList.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_EXPLORE_COURSE_SECTION,
|
||||
"yt_outline_creator_academy", // For Disable bold icons.
|
||||
"yt_outline_creator_academy",
|
||||
"yt_outline_experimental_graduation_cap"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_EXPLORE_PODCAST_SECTION,
|
||||
"FEpodcasts_destination"
|
||||
"FEpodcasts_destination",
|
||||
"yt_outline_experimental_podcast"
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -132,28 +134,17 @@ public final class DescriptionComponentsFilter extends Filter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
// The description panel can be opened in both the regular player and Shorts.
|
||||
// If the description panel is opened in a Shorts, PlayerType is 'HIDDEN',
|
||||
// so 'PlayerType.getCurrent().isMaximizedOrFullscreen()' does not guarantee that the description panel is open.
|
||||
// Instead, use the engagement id to check if the description panel is opened.
|
||||
if (!EngagementPanel.isDescription()
|
||||
// The user can minimize the player while the engagement panel is open.
|
||||
//
|
||||
// In this case, the engagement panel is treated as open.
|
||||
// (If the player is dismissed, the engagement panel is considered closed)
|
||||
//
|
||||
// Therefore, the following exceptions can occur:
|
||||
// 1. The user opened a regular video and opened the description panel.
|
||||
// 2. The 'horizontalShelf' elements were hidden.
|
||||
// 3. The user minimized the player.
|
||||
// 4. The user manually refreshed the library tab without dismissing the player.
|
||||
// 5. Since the engagement panel is treated as open, the history shelf is filtered.
|
||||
//
|
||||
// To handle these exceptions, filtering is not performed even when the player is minimized.
|
||||
|| PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED
|
||||
) {
|
||||
public boolean isFiltered(ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
// Immediately after the layout is refreshed, litho components are updated before the UI is drawn.
|
||||
// In this case, EngagementPanel.isDescription() cannot be used, and isActionBarVisible.get() should be used.
|
||||
if (!EngagementPanel.isDescription() && !(PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get() || ShortsPlayerState.isOpen())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
package app.revanced.extension.youtube.patches.litho;
|
||||
|
||||
import static app.revanced.extension.youtube.patches.LayoutReloadObserverPatch.isActionBarVisible;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.EngagementPanel;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class HorizontalShelvesFilter extends Filter {
|
||||
private final ByteArrayFilterGroupList descriptionBuffers = new ByteArrayFilterGroupList();
|
||||
private final ByteArrayFilterGroupList generalBuffers = new ByteArrayFilterGroupList();
|
||||
|
||||
public HorizontalShelvesFilter() {
|
||||
StringFilterGroup horizontalShelves = new StringFilterGroup(null, "horizontal_shelf.e");
|
||||
addPathCallbacks(horizontalShelves);
|
||||
|
||||
descriptionBuffers.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_ATTRIBUTES_SECTION,
|
||||
// May no longer work on v20.31+, even though the component is still there.
|
||||
"cell_video_attribute"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_FEATURED_PLACES_SECTION,
|
||||
"yt_fill_experimental_star",
|
||||
"yt_fill_star"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_GAMING_SECTION,
|
||||
"yt_outline_experimental_gaming",
|
||||
"yt_outline_gaming"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_MUSIC_SECTION,
|
||||
"yt_outline_experimental_audio",
|
||||
"yt_outline_audio"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_QUIZZES_SECTION,
|
||||
"post_base_wrapper_slim"
|
||||
)
|
||||
);
|
||||
|
||||
generalBuffers.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_CREATOR_STORE_SHELF,
|
||||
"shopping_item_card_list"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYABLES,
|
||||
"FEmini_app_destination"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_TICKET_SHELF,
|
||||
"ticket_item.e"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private boolean hideShelves(ContextInterface contextInterface) {
|
||||
if (!Settings.HIDE_HORIZONTAL_SHELVES.get()) {
|
||||
return false;
|
||||
}
|
||||
return contextInterface.isHomeFeedOrRelatedVideo()
|
||||
|| PlayerType.getCurrent().isMaximizedOrFullscreen()
|
||||
|| isActionBarVisible.get()
|
||||
|| NavigationBar.isSearchBarActive()
|
||||
|| NavigationBar.isBackButtonVisible()
|
||||
|| NavigationButton.getSelectedNavigationButton() != NavigationButton.LIBRARY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
if (contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
if (generalBuffers.check(buffer).isFiltered()) {
|
||||
return true;
|
||||
}
|
||||
if (descriptionBuffers.check(buffer).isFiltered()) {
|
||||
return EngagementPanel.isDescription() || PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get() || ShortsPlayerState.isOpen();
|
||||
}
|
||||
return hideShelves(contextInterface);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ import java.util.LinkedHashMap;
|
|||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext;
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
|
|
@ -47,7 +49,7 @@ public final class KeywordContentFilter extends Filter {
|
|||
|
||||
/**
|
||||
* Strings found in the buffer for every video. Full strings should be specified.
|
||||
*
|
||||
* <p>
|
||||
* This list does not include every common buffer string, and this can be added/changed as needed.
|
||||
* Words must be entered with the exact casing as found in the buffer.
|
||||
*/
|
||||
|
|
@ -122,7 +124,7 @@ public final class KeywordContentFilter extends Filter {
|
|||
/**
|
||||
* Path components to not filter. Cannot filter the buffer when these are present,
|
||||
* otherwise text in UI controls can be filtered as a keyword (such as using "Playlist" as a keyword).
|
||||
*
|
||||
* <p>
|
||||
* This is also a small performance improvement since
|
||||
* the buffer of the parent component was already searched and passed.
|
||||
*/
|
||||
|
|
@ -156,10 +158,10 @@ public final class KeywordContentFilter extends Filter {
|
|||
* Rolling average of how many videos were filtered by a keyword.
|
||||
* Used to detect if a keyword passes the initial check against {@link #STRINGS_IN_EVERY_BUFFER}
|
||||
* but a keyword is still hiding all videos.
|
||||
*
|
||||
* <p>
|
||||
* This check can still fail if some extra UI elements pass the keywords,
|
||||
* such as the video chapter preview or any other elements.
|
||||
*
|
||||
* <p>
|
||||
* To test this, add a filter that appears in all videos (such as 'ovd='),
|
||||
* and open the subscription feed. In practice this does not always identify problems
|
||||
* in the home feed and search, because the home feed has a finite amount of content and
|
||||
|
|
@ -226,7 +228,7 @@ public final class KeywordContentFilter extends Filter {
|
|||
* @return If the string contains any characters from languages that do not use spaces between words.
|
||||
*/
|
||||
private static boolean isLanguageWithNoSpaces(String text) {
|
||||
for (int i = 0, length = text.length(); i < length;) {
|
||||
for (int i = 0, length = text.length(); i < length; ) {
|
||||
final int codePoint = text.codePointAt(i);
|
||||
|
||||
Character.UnicodeBlock block = Character.UnicodeBlock.of(codePoint);
|
||||
|
|
@ -277,7 +279,7 @@ public final class KeywordContentFilter extends Filter {
|
|||
|
||||
/**
|
||||
* @return If the start and end indexes are not surrounded by other letters.
|
||||
* If the indexes are surrounded by numbers/symbols/punctuation it is considered a whole word.
|
||||
* If the indexes are surrounded by numbers/symbols/punctuation it is considered a whole word.
|
||||
*/
|
||||
private static boolean keywordMatchIsWholeWord(byte[] text, int keywordStartIndex, int keywordLength) {
|
||||
final Integer codePointBefore = getUtf8CodePointBefore(text, keywordStartIndex);
|
||||
|
|
@ -296,7 +298,7 @@ public final class KeywordContentFilter extends Filter {
|
|||
|
||||
/**
|
||||
* @return The UTF8 character point immediately before the index,
|
||||
* or null if the bytes before the index is not a valid UTF8 character.
|
||||
* or null if the bytes before the index is not a valid UTF8 character.
|
||||
*/
|
||||
@Nullable
|
||||
private static Integer getUtf8CodePointBefore(byte[] data, int index) {
|
||||
|
|
@ -312,7 +314,7 @@ public final class KeywordContentFilter extends Filter {
|
|||
|
||||
/**
|
||||
* @return The UTF8 character point at the index,
|
||||
* or null if the index holds no valid UTF8 character.
|
||||
* or null if the index holds no valid UTF8 character.
|
||||
*/
|
||||
@Nullable
|
||||
private static Integer getUtf8CodePointAt(byte[] data, int index) {
|
||||
|
|
@ -528,7 +530,7 @@ public final class KeywordContentFilter extends Filter {
|
|||
}
|
||||
|
||||
return switch (selectedNavButton) {
|
||||
case HOME, EXPLORE -> hideHome;
|
||||
case HOME -> hideHome;
|
||||
case SUBSCRIPTIONS -> hideSubscriptions;
|
||||
// User is in the Library or notifications.
|
||||
default -> false;
|
||||
|
|
@ -556,8 +558,14 @@ public final class KeywordContentFilter extends Filter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
public boolean isFiltered(ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import android.graphics.drawable.Drawable;
|
|||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
|
@ -16,16 +15,13 @@ import android.widget.TextView;
|
|||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
|
|
@ -38,10 +34,6 @@ import app.revanced.extension.youtube.shared.PlayerType;
|
|||
|
||||
@SuppressWarnings("unused")
|
||||
public final class LayoutComponentsFilter extends Filter {
|
||||
private static final StringTrieSearch mixPlaylistsContextExceptions = new StringTrieSearch(
|
||||
"V.ED", // Playlist browseId.
|
||||
"java.lang.ref.WeakReference"
|
||||
);
|
||||
private static final ByteArrayFilterGroup mixPlaylistsBufferExceptions = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"cell_description_body",
|
||||
|
|
@ -84,11 +76,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
private final StringFilterGroup chipBar;
|
||||
private final StringFilterGroup channelProfile;
|
||||
private final StringFilterGroupList channelProfileGroupList;
|
||||
private final StringFilterGroup horizontalShelves;
|
||||
private final ByteArrayFilterGroup playablesBuffer;
|
||||
private final ByteArrayFilterGroup ticketShelfBuffer;
|
||||
private final ByteArrayFilterGroup playerShoppingShelfBuffer;
|
||||
private final ByteTrieSearch descriptionSearch;
|
||||
|
||||
public LayoutComponentsFilter() {
|
||||
exceptions.addPatterns(
|
||||
|
|
@ -254,7 +241,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
);
|
||||
|
||||
final var relatedVideos = new StringFilterGroup(
|
||||
Settings.HIDE_RELATED_VIDEOS,
|
||||
Settings.HIDE_QUICK_ACTIONS_RELATED_VIDEOS,
|
||||
"fullscreen_related_videos"
|
||||
);
|
||||
|
||||
|
|
@ -264,12 +251,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
"mini_game_card.e"
|
||||
);
|
||||
|
||||
// Playable horizontal shelf header.
|
||||
playablesBuffer = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"FEmini_app_destination"
|
||||
);
|
||||
|
||||
final var quickActions = new StringFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS,
|
||||
"quick_actions"
|
||||
|
|
@ -317,7 +298,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
);
|
||||
|
||||
final var forYouShelf = new StringFilterGroup(
|
||||
Settings.HIDE_FOR_YOU_SHELF,
|
||||
Settings.HIDE_HORIZONTAL_SHELVES,
|
||||
"mixed_content_shelf"
|
||||
);
|
||||
|
||||
|
|
@ -361,51 +342,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
)
|
||||
);
|
||||
|
||||
horizontalShelves = new StringFilterGroup(
|
||||
null, // Setting is checked in isFiltered()
|
||||
"horizontal_video_shelf.e",
|
||||
"horizontal_shelf.e",
|
||||
"horizontal_shelf_inline.e",
|
||||
"horizontal_tile_shelf.e"
|
||||
);
|
||||
|
||||
ticketShelfBuffer = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"ticket_item.e"
|
||||
);
|
||||
|
||||
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"shopping_item_card_list"
|
||||
);
|
||||
|
||||
// Work around for unique situation where filtering is based on the setting,
|
||||
// but it must not fall over to other filters if the setting is _not_ enabled.
|
||||
// This is only needed for the horizontal shelf that is used so extensively everywhere.
|
||||
descriptionSearch = new ByteTrieSearch();
|
||||
List.of(
|
||||
new Pair<>(Settings.HIDE_FEATURED_PLACES_SECTION, "yt_fill_star"),
|
||||
new Pair<>(Settings.HIDE_FEATURED_PLACES_SECTION, "yt_fill_experimental_star"),
|
||||
new Pair<>(Settings.HIDE_GAMING_SECTION, "yt_outline_gaming"),
|
||||
new Pair<>(Settings.HIDE_GAMING_SECTION, "yt_outline_experimental_gaming"),
|
||||
new Pair<>(Settings.HIDE_MUSIC_SECTION, "yt_outline_audio"),
|
||||
new Pair<>(Settings.HIDE_MUSIC_SECTION, "yt_outline_experimental_audio"),
|
||||
new Pair<>(Settings.HIDE_QUIZZES_SECTION, "post_base_wrapper_slim"),
|
||||
// May no longer work on v20.31+, even though the component is still there.
|
||||
new Pair<>(Settings.HIDE_ATTRIBUTES_SECTION, "cell_video_attribute")
|
||||
).forEach(pair -> {
|
||||
BooleanSetting setting = pair.first;
|
||||
descriptionSearch.addPattern(pair.second.getBytes(StandardCharsets.UTF_8),
|
||||
(textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
|
||||
//noinspection unchecked
|
||||
AtomicReference<Boolean> hide = (AtomicReference<Boolean>) callbackParameter;
|
||||
hide.set(setting.get());
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
artistCard,
|
||||
audioTrackButton,
|
||||
|
|
@ -422,7 +358,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
emergencyBox,
|
||||
expandableMetadata,
|
||||
forYouShelf,
|
||||
horizontalShelves,
|
||||
imageShelf,
|
||||
infoPanel,
|
||||
latestPosts,
|
||||
|
|
@ -445,8 +380,14 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
public boolean isFiltered(ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
// This identifier is used not only in players but also in search results:
|
||||
// https://github.com/ReVanced/revanced-patches/issues/3245
|
||||
// Until 2024, medical information panels such as Covid-19 also used this identifier and were shown in the search results.
|
||||
|
|
@ -466,13 +407,8 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
return channelProfileGroupList.check(accessibility).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == communityPosts
|
||||
&& NavigationBar.isBackButtonVisible()
|
||||
&& !NavigationBar.isSearchBarActive()
|
||||
&& PlayerType.getCurrent() != PlayerType.WATCH_WHILE_MAXIMIZED) {
|
||||
// Allow community posts on channel profile page,
|
||||
// or if viewing an individual channel in the feed.
|
||||
return false;
|
||||
if (matchedGroup == communityPosts) {
|
||||
return contextInterface.isHomeFeedOrRelatedVideo() || contextInterface.isSubscriptionOrLibrary();
|
||||
}
|
||||
|
||||
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
|
||||
|
|
@ -484,47 +420,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
&& joinMembershipButton.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
// Horizontal shelves are used everywhere in the app. And to prevent the generic "hide shelves"
|
||||
// from incorrectly hiding other stuff that has its own hide filters,
|
||||
// the more specific shelf filters must check first _and_ they must halt falling over
|
||||
// to other filters if the buffer matches but the setting is off.
|
||||
if (matchedGroup == horizontalShelves) {
|
||||
if (contentIndex != 0) return false;
|
||||
|
||||
AtomicReference<Boolean> descriptionFilterResult = new AtomicReference<>(null);
|
||||
if (descriptionSearch.matches(buffer, descriptionFilterResult)) {
|
||||
return descriptionFilterResult.get();
|
||||
}
|
||||
|
||||
// Check if others are off before searching.
|
||||
final boolean hideShelves = Settings.HIDE_HORIZONTAL_SHELVES.get();
|
||||
final boolean hideTickets = Settings.HIDE_TICKET_SHELF.get();
|
||||
final boolean hidePlayables = Settings.HIDE_PLAYABLES.get();
|
||||
final boolean hidePlayerShoppingShelf = Settings.HIDE_CREATOR_STORE_SHELF.get();
|
||||
if (!hideShelves && !hideTickets && !hidePlayables && !hidePlayerShoppingShelf)
|
||||
return false;
|
||||
|
||||
if (ticketShelfBuffer.check(buffer).isFiltered()) return hideTickets;
|
||||
if (playablesBuffer.check(buffer).isFiltered()) return hidePlayables;
|
||||
if (playerShoppingShelfBuffer.check(buffer).isFiltered())
|
||||
return hidePlayerShoppingShelf;
|
||||
|
||||
// 20.31+ when exiting fullscreen after watching for a while or when resuming the app,
|
||||
// then sometimes the buffer isn't correct and the player shopping shelf is shown.
|
||||
// If filtering reaches this point then there are no more shelves that could be in the player.
|
||||
// If shopping shelves are set to hidden and the player is active, then assume
|
||||
// it's the shopping shelf.
|
||||
if (hidePlayerShoppingShelf) {
|
||||
PlayerType type = PlayerType.getCurrent();
|
||||
if (type == PlayerType.WATCH_WHILE_MAXIMIZED || type == PlayerType.WATCH_WHILE_FULLSCREEN
|
||||
|| type == PlayerType.WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return hideShelves && hideShelves();
|
||||
}
|
||||
|
||||
if (matchedGroup == chipBar) {
|
||||
return contentIndex == 0 && NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY;
|
||||
}
|
||||
|
|
@ -536,7 +431,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
* Injection point.
|
||||
* Called from a different place then the other filters.
|
||||
*/
|
||||
public static boolean filterMixPlaylists(Object conversionContext, @Nullable byte[] buffer) {
|
||||
public static boolean filterMixPlaylists(@Nullable byte[] buffer) {
|
||||
// Edit: This hook may no longer be needed, and mix playlist filtering
|
||||
// might be possible using the existing litho filters.
|
||||
try {
|
||||
|
|
@ -551,13 +446,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
|
||||
if (mixPlaylists.check(buffer).isFiltered()
|
||||
// Prevent hiding the description of some videos accidentally.
|
||||
&& !mixPlaylistsBufferExceptions.check(buffer).isFiltered()
|
||||
// Prevent playlist items being hidden, if a mix playlist is present in it.
|
||||
// Check last since it requires creating a context string.
|
||||
//
|
||||
// FIXME: The conversion context passed in does not always generate a valid toString.
|
||||
// This string check may no longer be needed, or the patch may be broken.
|
||||
&& !mixPlaylistsContextExceptions.matches(conversionContext.toString())) {
|
||||
&& !mixPlaylistsBufferExceptions.check(buffer).isFiltered()) {
|
||||
Logger.printDebug(() -> "Filtered mix playlist");
|
||||
return true;
|
||||
}
|
||||
|
|
@ -593,6 +482,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
* Injection point.
|
||||
*/
|
||||
public static boolean hideFloatingMicrophoneButton(final boolean original) {
|
||||
// FIXME? Is this feature still relevant? When/where does this microphone appear?
|
||||
return original || Settings.HIDE_FLOATING_MICROPHONE_BUTTON.get();
|
||||
}
|
||||
|
||||
|
|
@ -757,31 +647,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
: original;
|
||||
}
|
||||
|
||||
private static boolean hideShelves() {
|
||||
// Horizontal shelves are used for music/game links in video descriptions,
|
||||
// such as https://youtube.com/watch?v=W8kI1na3S2M
|
||||
if (PlayerType.getCurrent().isMaximizedOrFullscreen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must check search bar after player type, since search results
|
||||
// can be in the background behind an open player.
|
||||
if (NavigationBar.isSearchBarActive()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Do not hide if the navigation back button is visible,
|
||||
// otherwise the content shelves in the explore/music/courses pages are hidden.
|
||||
if (NavigationBar.isBackButtonVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check navigation button last.
|
||||
// Only filter if the library tab is not selected.
|
||||
// This check is important as the shelf layout is used for the library tab playlists.
|
||||
return NavigationButton.getSelectedNavigationButton() != NavigationButton.LIBRARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
|
|
@ -919,8 +784,8 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param typedString Keywords typed in the search bar.
|
||||
* @return Whether the setting is enabled and the typed string is empty.
|
||||
* @param typedString Keywords typed in the search bar.
|
||||
* @return Whether the setting is enabled and the typed string is empty.
|
||||
*/
|
||||
public static boolean hideYouMayLikeSection(String typedString) {
|
||||
return Settings.HIDE_YOU_MAY_LIKE_SECTION.get()
|
||||
|
|
@ -932,13 +797,13 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param searchTerm This class contains information related to search terms.
|
||||
* The {@code toString()} method of this class overrides the search term.
|
||||
* @param endpoint Endpoint related with the search term.
|
||||
* For search history, this value is:
|
||||
* '/complete/deleteitems?client=youtube-android-pb&delq=${searchTerm}&deltok=${token}'.
|
||||
* For search suggestions, this value is null or empty.
|
||||
* @return Whether search term is a search history or not.
|
||||
* @param searchTerm This class contains information related to search terms.
|
||||
* The {@code toString()} method of this class overrides the search term.
|
||||
* @param endpoint Endpoint related with the search term.
|
||||
* For search history, this value is:
|
||||
* '/complete/deleteitems?client=youtube-android-pb&delq=${searchTerm}&deltok=${token}'.
|
||||
* For search suggestions, this value is null or empty.
|
||||
* @return Whether search term is a search history or not.
|
||||
*/
|
||||
public static boolean isSearchHistory(Object searchTerm, String endpoint) {
|
||||
boolean isSearchHistory = endpoint != null && endpoint.contains("/delete");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package app.revanced.extension.youtube.patches.litho;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
||||
|
|
@ -38,8 +39,14 @@ public final class PlaybackSpeedMenuFilter extends Filter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
public boolean isFiltered(ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
if (matchedGroup == oldPlaybackMenuGroup) {
|
||||
isOldPlaybackSpeedMenuVisible = true;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package app.revanced.extension.youtube.patches.litho;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
|
|
@ -112,8 +113,14 @@ public final class PlayerFlyoutMenuItemsFilter extends Filter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
public boolean isFiltered(ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
if (matchedGroup == videoQualityMenuFooter) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -127,6 +134,9 @@ public final class PlayerFlyoutMenuItemsFilter extends Filter {
|
|||
return false;
|
||||
}
|
||||
|
||||
// 21.x+ fix.
|
||||
if (path.contains("bottom_sheet_list_option.e")) return false;
|
||||
|
||||
return flyoutFilterGroupList.check(buffer).isFiltered();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import androidx.annotation.Nullable;
|
|||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext;
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.TrieSearch;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
|
@ -19,14 +21,14 @@ import app.revanced.extension.youtube.settings.Settings;
|
|||
|
||||
/**
|
||||
* Searches for video IDs in the proto buffer of Shorts dislike.
|
||||
*
|
||||
* <p>
|
||||
* Because multiple litho dislike spans are created in the background
|
||||
* (and also anytime litho refreshes the components, which is somewhat arbitrary),
|
||||
* that makes the value of {@link VideoInformation#getVideoId()} and {@link VideoInformation#getPlayerResponseVideoId()}
|
||||
* unreliable to determine which video ID a Shorts litho span belongs to.
|
||||
*
|
||||
* <p>
|
||||
* But the correct video ID does appear in the protobuffer just before a Shorts litho span is created.
|
||||
*
|
||||
* <p>
|
||||
* Once a way to asynchronously update litho text is found, this strategy will no longer be needed.
|
||||
*/
|
||||
public final class ReturnYouTubeDislikeFilter extends Filter {
|
||||
|
|
@ -88,8 +90,14 @@ public final class ReturnYouTubeDislikeFilter extends Filter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
public boolean isFiltered(ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,25 @@
|
|||
package app.revanced.extension.youtube.patches.litho;
|
||||
|
||||
import static app.revanced.extension.shared.ConversionContext.*;
|
||||
import static app.revanced.extension.youtube.patches.LayoutReloadObserverPatch.isActionBarVisible;
|
||||
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.ConversionContext;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList;
|
||||
import app.revanced.extension.shared.patches.litho.LithoFilterPatch;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroupList.StringFilterGroupList;
|
||||
|
||||
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.youtube.patches.VersionCheckPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.EngagementPanel;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||
|
|
@ -30,20 +29,6 @@ import app.revanced.extension.youtube.shared.PlayerType;
|
|||
public final class ShortsFilter extends Filter {
|
||||
private static final boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get();
|
||||
private static final String COMPONENT_TYPE = "ComponentType";
|
||||
private static final String[] REEL_ACTION_BAR_PATHS = {
|
||||
"reel_action_bar.", // Regular Shorts.
|
||||
"reels_player_overlay_layout." // Shorts ads.
|
||||
};
|
||||
private static final Map<Integer, BooleanSetting> REEL_ACTION_BUTTONS_MAP = new HashMap<>() {
|
||||
{
|
||||
// Like button and Dislike button can be hidden with Litho filter.
|
||||
// put(0, Settings.HIDE_SHORTS_LIKE_BUTTON);
|
||||
// put(1, Settings.HIDE_SHORTS_DISLIKE_BUTTON);
|
||||
put(2, Settings.HIDE_SHORTS_COMMENTS_BUTTON);
|
||||
put(3, Settings.HIDE_SHORTS_SHARE_BUTTON);
|
||||
put(4, Settings.HIDE_SHORTS_REMIX_BUTTON);
|
||||
}
|
||||
};
|
||||
private final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.e";
|
||||
|
||||
/**
|
||||
|
|
@ -71,11 +56,7 @@ public final class ShortsFilter extends Filter {
|
|||
private final StringFilterGroup shortsCompactFeedVideo;
|
||||
private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer;
|
||||
private final StringFilterGroup channelProfile;
|
||||
private final ByteArrayFilterGroup channelProfileShelfHeaderBuffer;
|
||||
private final StringFilterGroup useSoundButton;
|
||||
private final ByteArrayFilterGroup useSoundButtonBuffer;
|
||||
private final StringFilterGroup useTemplateButton;
|
||||
private final ByteArrayFilterGroup useTemplateButtonBuffer;
|
||||
private final ByteArrayFilterGroup channelProfileShelfHeader;
|
||||
|
||||
private final StringFilterGroup autoDubbedLabel;
|
||||
private final StringFilterGroup subscribeButton;
|
||||
|
|
@ -90,9 +71,12 @@ public final class ShortsFilter extends Filter {
|
|||
private final StringFilterGroup suggestedAction;
|
||||
private final ByteArrayFilterGroupList suggestedActionsBuffer = new ByteArrayFilterGroupList();
|
||||
|
||||
private final StringFilterGroup useButtons;
|
||||
private final ByteArrayFilterGroupList useButtonsBuffer = new ByteArrayFilterGroupList();
|
||||
|
||||
private final StringFilterGroup shortsActionBar;
|
||||
private final StringFilterGroup videoActionButton;
|
||||
private final ByteArrayFilterGroupList videoActionButtonBuffer = new ByteArrayFilterGroupList();
|
||||
private final StringFilterGroup shortsActionButton;
|
||||
private final StringFilterGroupList shortsActionButtonGroupList = new StringFilterGroupList();
|
||||
|
||||
public ShortsFilter() {
|
||||
//
|
||||
|
|
@ -112,7 +96,7 @@ public final class ShortsFilter extends Filter {
|
|||
"shorts_pivot_item"
|
||||
);
|
||||
|
||||
channelProfileShelfHeaderBuffer = new ByteArrayFilterGroup(
|
||||
channelProfileShelfHeader = new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_CHANNEL,
|
||||
"Shorts"
|
||||
);
|
||||
|
|
@ -270,32 +254,7 @@ public final class ShortsFilter extends Filter {
|
|||
)
|
||||
);
|
||||
|
||||
useSoundButton = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_USE_SOUND_BUTTON,
|
||||
// First filter needed for "Use this sound" that can appear when viewing Shorts
|
||||
// through the "Short remixing this video" section.
|
||||
"floating_action_button.e",
|
||||
// Second filter needed for "Use this sound" that can appear below the video title.
|
||||
REEL_METAPANEL_PATH
|
||||
);
|
||||
|
||||
useSoundButtonBuffer = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"yt_outline_camera_"
|
||||
);
|
||||
|
||||
useTemplateButton = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_USE_TEMPLATE_BUTTON,
|
||||
// Second filter needed for "Use this template" that can appear below the video title.
|
||||
REEL_METAPANEL_PATH
|
||||
);
|
||||
|
||||
useTemplateButtonBuffer = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"yt_outline_template_add_"
|
||||
);
|
||||
|
||||
videoActionButton = new StringFilterGroup(
|
||||
shortsActionButton = new StringFilterGroup(
|
||||
null,
|
||||
// Can be any of:
|
||||
// button.eml
|
||||
|
|
@ -305,6 +264,26 @@ public final class ShortsFilter extends Filter {
|
|||
"button.e"
|
||||
);
|
||||
|
||||
useButtons = new StringFilterGroup(
|
||||
null,
|
||||
REEL_PLAYER_OVERLAY_PATH,
|
||||
REEL_METAPANEL_PATH,
|
||||
"floating_action_button.e"
|
||||
);
|
||||
|
||||
useButtonsBuffer.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_USE_SOUND_BUTTON,
|
||||
"yt_outline_camera_",
|
||||
"yt_outline_experimental_camera_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_USE_TEMPLATE_BUTTON,
|
||||
"yt_outline_template_add_",
|
||||
"yt_outline_experimental_template_add_"
|
||||
)
|
||||
);
|
||||
|
||||
suggestedAction = new StringFilterGroup(
|
||||
null,
|
||||
"suggested_action.e"
|
||||
|
|
@ -313,37 +292,27 @@ public final class ShortsFilter extends Filter {
|
|||
addPathCallbacks(
|
||||
shortsCompactFeedVideo, shelfHeaderPath, joinButton, subscribeButton, paidPromotionLabel,
|
||||
livePreview, suggestedAction, pausedOverlayButtons, channelBar, infoPanel, previewComment,
|
||||
autoDubbedLabel, fullVideoLinkLabel, videoTitle, useSoundButton, soundButton, stickers,
|
||||
reelCarousel, reelSoundMetadata, likeFountain, likeButton, dislikeButton
|
||||
autoDubbedLabel, fullVideoLinkLabel, videoTitle, soundButton, stickers, useButtons,
|
||||
reelCarousel, reelSoundMetadata, likeFountain, likeButton, dislikeButton, shortsActionBar
|
||||
);
|
||||
|
||||
// Legacy hiding of Shorts action buttons. Because of 20.31+ buffer changes
|
||||
// it's currently not possible to hide these using buffer filtering.
|
||||
// See alternative hiding strategy in hideActionButtons().
|
||||
if (!VersionCheckPatch.IS_20_22_OR_GREATER) {
|
||||
addPathCallbacks(shortsActionBar);
|
||||
|
||||
//
|
||||
// All other action buttons.
|
||||
//
|
||||
videoActionButtonBuffer.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
|
||||
"reel_comment_button",
|
||||
"youtube_shorts_comment_outline"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_SHARE_BUTTON,
|
||||
"reel_share_button",
|
||||
"youtube_shorts_share_outline"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_REMIX_BUTTON,
|
||||
"reel_remix_button",
|
||||
"youtube_shorts_remix_outline"
|
||||
)
|
||||
);
|
||||
}
|
||||
//
|
||||
// All other action buttons.
|
||||
//
|
||||
shortsActionButtonGroupList.addAll(
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
|
||||
"id.reel_comment_button"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_SHARE_BUTTON,
|
||||
"id.reel_share_button"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_REMIX_BUTTON,
|
||||
"id.reel_remix_button"
|
||||
)
|
||||
);
|
||||
|
||||
//
|
||||
// Suggested actions.
|
||||
|
|
@ -434,8 +403,14 @@ public final class ShortsFilter extends Filter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
public boolean isFiltered(ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
if (contentType == FilterContentType.IDENTIFIER) {
|
||||
if (matchedGroup == shelfHeaderIdentifier) {
|
||||
// Shelf header reused in history/channel/etc.
|
||||
|
|
@ -443,8 +418,12 @@ public final class ShortsFilter extends Filter {
|
|||
if (contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (matchedGroup == channelProfile) {
|
||||
// Check ConversationContext to not hide shelf header in channel profile
|
||||
// This value does not exist in the shelf header in the channel profile
|
||||
if (!contextInterface.isHomeFeedOrRelatedVideo()) {
|
||||
return false;
|
||||
}
|
||||
} else if (matchedGroup == channelProfile) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -463,16 +442,16 @@ public final class ShortsFilter extends Filter {
|
|||
return reelCarouselBuffer.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == useSoundButton) {
|
||||
return useSoundButtonBuffer.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == useTemplateButton) {
|
||||
return useTemplateButtonBuffer.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == shortsCompactFeedVideo) {
|
||||
return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(buffer).isFiltered();
|
||||
return shouldHideShortsFeedItems()
|
||||
&& shortsCompactFeedVideoBuffer.check(buffer).isFiltered()
|
||||
// The litho path of the feed video is 'video_lockup_with_attachment.e'.
|
||||
// It appears shortsCompactFeedVideoBuffer is used after 20 seconds during autoplay in the feed in YouTube 20.44.38.
|
||||
// If the Shorts shelf is hidden on the Home feed, the video in the feed will be hidden after 20 seconds have passed since autoplay began in the feed.
|
||||
//
|
||||
// When a video is autoplaying in the feed, no new components are drawn on the screen.
|
||||
// Therefore, filtering is skipped when the current PlayerType is [INLINE_MINIMAL].
|
||||
&& PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL;
|
||||
}
|
||||
|
||||
if (matchedGroup == shelfHeaderPath) {
|
||||
|
|
@ -481,8 +460,10 @@ public final class ShortsFilter extends Filter {
|
|||
if (contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
if (!channelProfileShelfHeaderBuffer.check(buffer).isFiltered()) {
|
||||
return false;
|
||||
// Check ConversationContext to not hide shelf header in channel profile
|
||||
// This value does not exist in the shelf header in the channel profile
|
||||
if (!contextInterface.isHomeFeedOrRelatedVideo()) {
|
||||
return channelProfileShelfHeader.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
return shouldHideShortsFeedItems();
|
||||
|
|
@ -491,8 +472,14 @@ public final class ShortsFilter extends Filter {
|
|||
// 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) {
|
||||
return videoActionButton.check(path).isFiltered()
|
||||
&& videoActionButtonBuffer.check(buffer).isFiltered();
|
||||
if (shortsActionButton.check(path).isFiltered()) {
|
||||
return shortsActionButtonGroupList.check(accessibility).isFiltered();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (matchedGroup == useButtons) {
|
||||
return path.contains("|button.e") && useButtonsBuffer.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == suggestedAction) {
|
||||
|
|
@ -534,7 +521,7 @@ public final class ShortsFilter extends Filter {
|
|||
}
|
||||
|
||||
// Must check player type first, as search bar can be active behind the player.
|
||||
if (PlayerType.getCurrent().isMaximizedOrFullscreen()) {
|
||||
if (PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get()) {
|
||||
return EngagementPanel.isDescription()
|
||||
? hideVideoDescription // Player video description panel opened.
|
||||
: hideHome; // For now, consider Shorts under video player the same as the home feed.
|
||||
|
|
@ -557,65 +544,13 @@ public final class ShortsFilter extends Filter {
|
|||
}
|
||||
|
||||
return switch (selectedNavButton) {
|
||||
case HOME, EXPLORE -> hideHome;
|
||||
case HOME -> hideHome;
|
||||
case SUBSCRIPTIONS -> hideSubscriptions;
|
||||
case LIBRARY -> hideHistory;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Hide action buttons by index.
|
||||
* <p>
|
||||
* Regular video action buttons vary in order by video, country, and account.
|
||||
* Therefore, hiding buttons by index may hide unintended buttons.
|
||||
* <p>
|
||||
* Shorts action buttons are almost always in the same order.
|
||||
* (From top to bottom: Like, Dislike, Comment, Share, Remix).
|
||||
* Therefore, we can hide Shorts action buttons by index.
|
||||
*
|
||||
* @param pathBuilder Same as pathBuilder used in {@link LithoFilterPatch}.
|
||||
* @param treeNodeResultList List containing Litho components.
|
||||
*/
|
||||
public static void hideActionButtons(StringBuilder pathBuilder, List<Object> treeNodeResultList) {
|
||||
try {
|
||||
if (pathBuilder == null || pathBuilder.length() == 0 || treeNodeResultList == null) {
|
||||
return;
|
||||
}
|
||||
int size = treeNodeResultList.size();
|
||||
|
||||
// The minimum size of the target List is 4.
|
||||
if (size < 4) {
|
||||
return;
|
||||
}
|
||||
String path = pathBuilder.toString();
|
||||
|
||||
if (!Utils.containsAny(path, REEL_ACTION_BAR_PATHS)
|
||||
// Regular Shorts: [ComponentType, ComponentType, ComponentType, ComponentType, ComponentType]
|
||||
// Shorts ads: [ComponentType, ComponentType, ComponentType, ComponentType] (No Remix button)
|
||||
|| !COMPONENT_TYPE.equals(treeNodeResultList.get(0).toString())) {
|
||||
return;
|
||||
}
|
||||
// Removing elements without iterating through the list in reverse order will throw an exception.
|
||||
for (int i = size - 1; i > -1; i--) {
|
||||
// treeNodeResult is each button.
|
||||
Object treeNodeResult = treeNodeResultList.get(i);
|
||||
if (treeNodeResult != null) {
|
||||
BooleanSetting setting = REEL_ACTION_BUTTONS_MAP.get(i);
|
||||
if (setting != null && setting.get()) {
|
||||
int finalI = i;
|
||||
Logger.printDebug(() -> "Hiding action button by index: " + finalI + ", key: " + setting.key);
|
||||
treeNodeResultList.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "hideActionButtons failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package app.revanced.extension.youtube.patches.litho;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext;
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
|
|
@ -158,8 +160,14 @@ public final class VideoActionButtonsFilter extends Filter {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
public boolean isFiltered(ContextInterface contextInterface,
|
||||
String identifier,
|
||||
String accessibility,
|
||||
String path,
|
||||
byte[] buffer,
|
||||
StringFilterGroup matchedGroup,
|
||||
FilterContentType contentType,
|
||||
int contentIndex) {
|
||||
if (matchedGroup == likeSubscribeGlow) {
|
||||
return path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX)
|
||||
|| path.startsWith(COMPACTIFY_VIDEO_ACTION_BAR_PATH);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ import app.revanced.extension.youtube.patches.VideoInformation;
|
|||
import app.revanced.extension.youtube.patches.VideoInformation.*;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
import j$.util.Optional;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@SuppressWarnings({"rawtypes", "unused"})
|
||||
public class RememberVideoQualityPatch {
|
||||
|
||||
private static final IntegerSetting videoQualityWifi = Settings.VIDEO_QUALITY_DEFAULT_WIFI;
|
||||
|
|
@ -66,6 +67,25 @@ public class RememberVideoQualityPatch {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Overrides the initial video quality to not follow the 'Video quality preferences' in YouTube settings.
|
||||
* (e.g. 'Auto (recommended)' - 360p/480p, 'Higher picture quality' - 720p/1080p...)
|
||||
* If the maximum video quality available is 1080p and the default video quality is 2160p,
|
||||
* 1080p is used as an initial video quality.
|
||||
* <p>
|
||||
* Called before {@link #newVideoStarted(VideoInformation.PlaybackController)}.
|
||||
*/
|
||||
public static Optional getInitialVideoQuality(Optional optional) {
|
||||
int preferredQuality = getDefaultQualityResolution();
|
||||
if (preferredQuality != VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE) {
|
||||
Logger.printDebug(() -> "initialVideoQuality: " + preferredQuality);
|
||||
return Optional.of(preferredQuality);
|
||||
}
|
||||
return optional;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}.
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ public class CustomPlaybackSpeedPatch {
|
|||
*/
|
||||
private static final float PROGRESS_BAR_VALUE_SCALE = 100;
|
||||
|
||||
/**
|
||||
* Disable tap and hold speed, true when TAP_AND_HOLD_SPEED is 0.
|
||||
*/
|
||||
private static final boolean DISABLE_TAP_AND_HOLD_SPEED;
|
||||
|
||||
/**
|
||||
* Tap and hold speed.
|
||||
*/
|
||||
|
|
@ -76,9 +81,9 @@ public class CustomPlaybackSpeedPatch {
|
|||
private static final float customPlaybackSpeedsMin, customPlaybackSpeedsMax;
|
||||
|
||||
/**
|
||||
* The last time the old playback menu was forcefully called.
|
||||
* The last time the playback menu was forcefully called.
|
||||
*/
|
||||
private static volatile long lastTimeOldPlaybackMenuInvoked;
|
||||
private static volatile long lastTimePlaybackMenuInvoked;
|
||||
|
||||
/**
|
||||
* Formats speeds to UI strings.
|
||||
|
|
@ -96,7 +101,12 @@ public class CustomPlaybackSpeedPatch {
|
|||
speedFormatter.setMaximumFractionDigits(2);
|
||||
|
||||
final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get();
|
||||
if (holdSpeed > 0 && holdSpeed <= PLAYBACK_SPEED_MAXIMUM) {
|
||||
DISABLE_TAP_AND_HOLD_SPEED = holdSpeed == 0;
|
||||
|
||||
if (DISABLE_TAP_AND_HOLD_SPEED) {
|
||||
// A value for handling exceptions, but this is not used.
|
||||
TAP_AND_HOLD_SPEED = Settings.SPEED_TAP_AND_HOLD.defaultValue;
|
||||
} else if (holdSpeed > 0 && holdSpeed <= PLAYBACK_SPEED_MAXIMUM) {
|
||||
TAP_AND_HOLD_SPEED = holdSpeed;
|
||||
} else {
|
||||
showInvalidCustomSpeedToast();
|
||||
|
|
@ -108,6 +118,22 @@ public class CustomPlaybackSpeedPatch {
|
|||
customPlaybackSpeedsMax = customPlaybackSpeeds[customPlaybackSpeeds.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Called before {@link #getTapAndHoldSpeed()}
|
||||
*/
|
||||
public static boolean disableTapAndHoldSpeed(boolean original) {
|
||||
return !DISABLE_TAP_AND_HOLD_SPEED && original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean useNewFlyoutMenu(boolean useNewFlyout) {
|
||||
// If using old speed turn off A/B flyout that breaks old playback speed menu.
|
||||
return useNewFlyout && !Settings.RESTORE_OLD_SPEED_MENU.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
|
|
@ -212,6 +238,15 @@ public class CustomPlaybackSpeedPatch {
|
|||
return false;
|
||||
}
|
||||
|
||||
// This method is sometimes used multiple times.
|
||||
// To prevent this, ignore method reuse within 1 second.
|
||||
final long now = System.currentTimeMillis();
|
||||
if (now - lastTimePlaybackMenuInvoked < 1000) {
|
||||
Logger.printDebug(() -> "Ignoring call to hideLithoMenuAndShowSpeedMenu");
|
||||
return true;
|
||||
}
|
||||
lastTimePlaybackMenuInvoked = now;
|
||||
|
||||
// Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView.
|
||||
// This only shows in phone layout.
|
||||
var touchInsidedView = parentView4th.getChildAt(0);
|
||||
|
|
@ -235,16 +270,8 @@ public class CustomPlaybackSpeedPatch {
|
|||
}
|
||||
|
||||
public static void showOldPlaybackSpeedMenu() {
|
||||
// This method is sometimes used multiple times.
|
||||
// To prevent this, ignore method reuse within 1 second.
|
||||
final long now = System.currentTimeMillis();
|
||||
if (now - lastTimeOldPlaybackMenuInvoked < 1000) {
|
||||
Logger.printDebug(() -> "Ignoring call to showOldPlaybackSpeedMenu");
|
||||
return;
|
||||
}
|
||||
lastTimeOldPlaybackMenuInvoked = now;
|
||||
|
||||
// Rest of the implementation added by patch.
|
||||
Logger.printDebug(() -> "showOldPlaybackSpeedMenu");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -147,7 +147,6 @@ public class Settings extends YouTubeAndMusicSettings {
|
|||
public static final BooleanSetting HIDE_CHANNEL_TAB = new BooleanSetting("revanced_hide_channel_tab", FALSE);
|
||||
public static final StringSetting HIDE_CHANNEL_TAB_FILTER_STRINGS = new StringSetting("revanced_hide_channel_tab_filter_strings", "", true, parent(HIDE_CHANNEL_TAB));
|
||||
public static final BooleanSetting HIDE_COMMUNITY_BUTTON = new BooleanSetting("revanced_hide_community_button", TRUE);
|
||||
public static final BooleanSetting HIDE_FOR_YOU_SHELF = new BooleanSetting("revanced_hide_for_you_shelf", FALSE);
|
||||
public static final BooleanSetting HIDE_JOIN_BUTTON = new BooleanSetting("revanced_hide_join_button", FALSE);
|
||||
public static final BooleanSetting HIDE_LINKS_PREVIEW = new BooleanSetting("revanced_hide_links_preview", TRUE);
|
||||
public static final BooleanSetting HIDE_MEMBERS_SHELF = new BooleanSetting("revanced_hide_members_shelf", TRUE);
|
||||
|
|
@ -188,8 +187,8 @@ public class Settings extends YouTubeAndMusicSettings {
|
|||
public static final BooleanSetting HIDE_PLAYER_CONTROL_BUTTONS_BACKGROUND = new BooleanSetting("revanced_hide_player_control_buttons_background", FALSE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS = new BooleanSetting("revanced_hide_player_previous_next_buttons", FALSE, true);
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS = new BooleanSetting("revanced_hide_quick_actions", FALSE);
|
||||
public static final BooleanSetting HIDE_RELATED_VIDEOS_OVERLAY = new BooleanSetting("revanced_hide_related_videos_overlay", FALSE, true);
|
||||
public static final BooleanSetting HIDE_RELATED_VIDEOS = new BooleanSetting("revanced_hide_related_videos", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_RELATED_VIDEOS_OVERLAY = new BooleanSetting("revanced_hide_player_related_videos_overlay", FALSE, true);
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS_RELATED_VIDEOS = new BooleanSetting("revanced_hide_quick_actions_related_videos", FALSE);
|
||||
public static final BooleanSetting HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES = new BooleanSetting("revanced_hide_subscribers_community_guidelines", TRUE);
|
||||
public static final BooleanSetting HIDE_TIMED_REACTIONS = new BooleanSetting("revanced_hide_timed_reactions", TRUE);
|
||||
public static final BooleanSetting HIDE_VIDEO_TITLE = new BooleanSetting("revanced_hide_video_title", FALSE);
|
||||
|
|
@ -228,6 +227,7 @@ public class Settings extends YouTubeAndMusicSettings {
|
|||
public static final BooleanSetting HIDE_COMMENTS_SECTION = new BooleanSetting("revanced_hide_comments_section", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_SECTION_IN_HOME_FEED = new BooleanSetting("revanced_hide_comments_section_in_home_feed", FALSE, parentNot(HIDE_COMMENTS_SECTION));
|
||||
public static final BooleanSetting HIDE_COMMENTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_comments_thanks_button", TRUE);
|
||||
public static final BooleanSetting SANITIZE_COMMENTS_CATEGORY_BAR = new BooleanSetting("revanced_sanitize_comments_category_bar", FALSE);
|
||||
|
||||
// Description
|
||||
public static final BooleanSetting HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION = new BooleanSetting("revanced_hide_ai_generated_video_summary_section", FALSE);
|
||||
|
|
@ -296,6 +296,7 @@ public class Settings extends YouTubeAndMusicSettings {
|
|||
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE,
|
||||
"revanced_remove_viewer_discretion_dialog_user_dialog_message");
|
||||
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", FALSE, true, "revanced_spoof_app_version_user_dialog_message");
|
||||
public static final BooleanSetting OVERRIDE_OPEN_IN_YOUTUBE_MUSIC_BUTTON = new BooleanSetting("revanced_override_open_in_youtube_music_button", TRUE, true);
|
||||
public static final EnumSetting<StartPage> CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.DEFAULT, true);
|
||||
public static final BooleanSetting CHANGE_START_PAGE_ALWAYS = new BooleanSetting("revanced_change_start_page_always", FALSE, true,
|
||||
new ChangeStartPageTypeAvailability());
|
||||
|
|
@ -323,7 +324,7 @@ public class Settings extends YouTubeAndMusicSettings {
|
|||
public static final BooleanSetting WIDE_SEARCHBAR = new BooleanSetting("revanced_wide_searchbar", FALSE, true);
|
||||
|
||||
// Shorts
|
||||
public static final BooleanSetting DISABLE_RESUMING_SHORTS_PLAYER = new BooleanSetting("revanced_disable_resuming_shorts_player", FALSE);
|
||||
public static final BooleanSetting DISABLE_RESUMING_SHORTS_ON_STARTUP = new BooleanSetting("revanced_disable_resuming_shorts_on_startup", FALSE);
|
||||
public static final BooleanSetting DISABLE_SHORTS_BACKGROUND_PLAYBACK = new BooleanSetting("revanced_shorts_disable_background_playback", FALSE);
|
||||
public static final EnumSetting<ShortsPlayerType> SHORTS_PLAYER_TYPE = new EnumSetting<>("revanced_shorts_player_type", ShortsPlayerType.SHORTS_PLAYER);
|
||||
public static final BooleanSetting HIDE_SHORTS_AI_BUTTON = new BooleanSetting("revanced_hide_shorts_ai_button", FALSE);
|
||||
|
|
@ -391,7 +392,7 @@ public class Settings extends YouTubeAndMusicSettings {
|
|||
public static final BooleanSetting EXTERNAL_BROWSER = new BooleanSetting("revanced_external_browser", TRUE, true);
|
||||
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true,
|
||||
"revanced_spoof_device_dimensions_user_dialog_message");
|
||||
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
|
||||
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_REEL, true, parent(SPOOF_VIDEO_STREAMS));
|
||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS_AV1 = new BooleanSetting("revanced_spoof_video_streams_av1", FALSE, true,
|
||||
"revanced_spoof_video_streams_av1_user_dialog_message", new SpoofClientAv1Availability());
|
||||
|
||||
|
|
|
|||
|
|
@ -310,10 +310,6 @@ public final class NavigationBar {
|
|||
* This tab will never be in a selected state, even if the Create video UI is on screen.
|
||||
*/
|
||||
CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"),
|
||||
/**
|
||||
* Only shown to automotive layout.
|
||||
*/
|
||||
EXPLORE("TAB_EXPLORE"),
|
||||
SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", "TAB_SUBSCRIPTIONS_CAIRO"),
|
||||
/**
|
||||
* Notifications tab. Only present when
|
||||
|
|
|
|||
|
|
@ -138,5 +138,6 @@ enum class PlayerType {
|
|||
|
||||
fun isMaximizedOrFullscreen(): Boolean {
|
||||
return this == WATCH_WHILE_MAXIMIZED || this == WATCH_WHILE_FULLSCREEN
|
||||
|| this == WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package app.revanced.extension.youtube.sponsorblock;
|
|||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
import android.util.Patterns;
|
||||
import android.widget.LinearLayout;
|
||||
|
|
@ -34,12 +34,12 @@ public class SponsorBlockSettings {
|
|||
|
||||
public static final Setting.ImportExportCallback SB_IMPORT_EXPORT_CALLBACK = new Setting.ImportExportCallback() {
|
||||
@Override
|
||||
public void settingsImported(@Nullable Context context) {
|
||||
public void settingsImported(@Nullable Activity context) {
|
||||
SegmentCategory.loadAllCategoriesFromSettings();
|
||||
SponsorBlockPreferenceGroup.settingsImported = true;
|
||||
}
|
||||
@Override
|
||||
public void settingsExported(@Nullable Context context) {
|
||||
public void settingsExported(@Nullable Activity context) {
|
||||
showExportWarningIfNeeded(context);
|
||||
}
|
||||
};
|
||||
|
|
@ -184,16 +184,16 @@ public class SponsorBlockSettings {
|
|||
/**
|
||||
* Export the categories using flatten JSON (no embedded dictionaries or arrays).
|
||||
*/
|
||||
private static void showExportWarningIfNeeded(@Nullable Context dialogContext) {
|
||||
private static void showExportWarningIfNeeded(@Nullable Activity activity) {
|
||||
Utils.verifyOnMainThread();
|
||||
initialize();
|
||||
|
||||
// If user has a SponsorBlock user ID then show a warning.
|
||||
if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateID()
|
||||
if (activity != null && SponsorBlockSettings.userHasSBPrivateID()
|
||||
&& !Settings.SB_HIDE_EXPORT_WARNING.get()) {
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||
dialogContext,
|
||||
activity,
|
||||
null, // No title.
|
||||
str("revanced_sb_settings_revanced_export_user_id_warning"), // Message.
|
||||
null, // No EditText.
|
||||
|
|
@ -205,11 +205,7 @@ public class SponsorBlockSettings {
|
|||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Set dialog as non-cancelable.
|
||||
dialogPair.first.setCancelable(false);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
Utils.showDialog(activity, dialogPair.first, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
18
extensions/youtube/stub/src/main/java/j$/util/Optional.java
Normal file
18
extensions/youtube/stub/src/main/java/j$/util/Optional.java
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package j$.util;
|
||||
|
||||
public final class Optional<T> {
|
||||
|
||||
/**
|
||||
* Returns an {@code Optional} describing the given non-{@code null}
|
||||
* value.
|
||||
*
|
||||
* @param value the value to describe, which must be non-{@code null}
|
||||
* @param <T> the type of the value
|
||||
* @return an {@code Optional} with the value present
|
||||
* @throws NullPointerException if value is {@code null}
|
||||
*/
|
||||
public static <T> Optional<T> of(T value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,4 +4,4 @@ org.gradle.parallel = true
|
|||
android.useAndroidX = true
|
||||
android.uniquePackageNames = false
|
||||
kotlin.code.style = official
|
||||
version = 6.1.0
|
||||
version = 6.1.1-dev.1
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ val hideMusicVideoAdsPatch = bytecodePatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ val enableExclusiveAudioPlaybackPatch = bytecodePatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ val permanentRepeatPatch = bytecodePatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import app.revanced.patcher.patch.bytecodePatch
|
|||
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.music.misc.gms.Constants.MUSIC_MAIN_ACTIVITY_NAME
|
||||
import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME
|
||||
import app.revanced.patches.music.misc.gms.musicActivityOnCreateMethod
|
||||
import app.revanced.patches.music.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.music.shared.mainActivityOnCreateMethod
|
||||
import app.revanced.patches.shared.layout.branding.EXTENSION_CLASS_DESCRIPTOR
|
||||
import app.revanced.patches.shared.layout.branding.baseCustomBrandingPatch
|
||||
import app.revanced.patches.shared.misc.mapping.ResourceType
|
||||
|
|
@ -61,11 +61,10 @@ val customBrandingPatch = baseCustomBrandingPatch(
|
|||
originalAppPackageName = MUSIC_PACKAGE_NAME,
|
||||
isYouTubeMusic = true,
|
||||
numberOfPresetAppNames = 5,
|
||||
getMainActivityOnCreate = { musicActivityOnCreateMethod },
|
||||
getMainActivityOnCreate = { mainActivityOnCreateMethod },
|
||||
mainActivityName = MUSIC_MAIN_ACTIVITY_NAME,
|
||||
activityAliasNameWithIntents = MUSIC_MAIN_ACTIVITY_NAME,
|
||||
preferenceScreen = PreferenceScreen.GENERAL,
|
||||
|
||||
block = {
|
||||
dependsOn(sharedExtensionPatch, disableSplashAnimationPatch)
|
||||
|
||||
|
|
@ -75,6 +74,7 @@ val customBrandingPatch = baseCustomBrandingPatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
package app.revanced.patches.music.layout.branding.header
|
||||
|
||||
import app.revanced.patcher.extensions.addInstructions
|
||||
import app.revanced.patcher.extensions.getInstruction
|
||||
import app.revanced.patcher.extensions.wideLiteral
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.music.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.shared.layout.branding.header.changeHeaderPatch
|
||||
import app.revanced.patches.shared.misc.mapping.ResourceType
|
||||
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||
import app.revanced.util.forEachInstructionAsSequence
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
|
||||
private val targetResourceDirectoryNames = mapOf(
|
||||
"drawable-hdpi" to "121x36 px",
|
||||
"drawable-xhdpi" to "160x48 px",
|
||||
"drawable-xxhdpi" to "240x72 px",
|
||||
"drawable-xxxhdpi" to "320x96 px"
|
||||
)
|
||||
|
||||
private val variants = arrayOf("dark")
|
||||
|
||||
private val logoResourceNames = arrayOf(
|
||||
"revanced_header_minimal",
|
||||
"revanced_header_rounded",
|
||||
)
|
||||
|
||||
private val headerDrawableNames = arrayOf(
|
||||
"action_bar_logo_ringo2",
|
||||
"ytm_logo_ringo2"
|
||||
)
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/music/patches/ChangeHeaderPatch;"
|
||||
|
||||
private val changeHeaderBytecodePatch = bytecodePatch {
|
||||
dependsOn(resourceMappingPatch)
|
||||
|
||||
apply {
|
||||
headerDrawableNames.forEach { drawableName ->
|
||||
val drawableId = ResourceType.DRAWABLE[drawableName]
|
||||
|
||||
forEachInstructionAsSequence({ _, method, instruction, index ->
|
||||
if (instruction.wideLiteral != drawableId) return@forEachInstructionAsSequence null
|
||||
|
||||
val register = method.getInstruction<OneRegisterInstruction>(index).registerA
|
||||
|
||||
return@forEachInstructionAsSequence index to register
|
||||
}) { method, (index, register) ->
|
||||
method.addInstructions(
|
||||
index + 1,
|
||||
"""
|
||||
invoke-static { v$register }, ${EXTENSION_CLASS_DESCRIPTOR}->getHeaderDrawableId(I)I
|
||||
move-result v$register
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
val changeHeaderPatch = changeHeaderPatch(
|
||||
targetResourceDirectoryNames = targetResourceDirectoryNames,
|
||||
changeHeaderBytecodePatch = changeHeaderBytecodePatch,
|
||||
logoResourceNames = logoResourceNames,
|
||||
variants = variants,
|
||||
preferenceScreen = PreferenceScreen.GENERAL,
|
||||
compatiblePackages = arrayOf(
|
||||
"com.google.android.apps.youtube.music" to setOf(
|
||||
"7.29.52",
|
||||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
),
|
||||
resourcesAppId = "music",
|
||||
)
|
||||
|
|
@ -52,6 +52,7 @@ val hideButtonsPatch = bytecodePatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ val hideCategoryBarPatch = bytecodePatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch(
|
|||
"7.29.52",
|
||||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54"
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
)
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
@file:Suppress("SpellCheckingInspection")
|
||||
|
||||
package app.revanced.patches.music.layout.miniplayercolor
|
||||
package app.revanced.patches.music.layout.miniplayer
|
||||
|
||||
import app.revanced.patcher.accessFlags
|
||||
import app.revanced.patcher.extensions.getInstruction
|
||||
|
|
@ -43,11 +43,12 @@ val changeMiniplayerColorPatch = bytecodePatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
|
||||
apply {
|
||||
addResources("music", "layout.miniplayercolor.changeMiniplayerColor")
|
||||
addResources("music", "layout.miniplayer.changeMiniplayerColor")
|
||||
|
||||
PreferenceScreen.PLAYER.addPreferences(
|
||||
SwitchPreference("revanced_music_change_miniplayer_color"),
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package app.revanced.patches.music.layout.miniplayercolor
|
||||
package app.revanced.patches.music.layout.miniplayer
|
||||
|
||||
import app.revanced.patcher.*
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
|
|
@ -28,3 +28,10 @@ internal val ClassDef.switchToggleColorMethodMatch by ClassDefComposing.composin
|
|||
Opcode.IGET,
|
||||
)
|
||||
}
|
||||
|
||||
internal val BytecodePatchContext.minimizedPlayerMethod by gettingFirstMethodDeclaratively {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
returnType("V")
|
||||
parameterTypes("L", "L")
|
||||
instructions("w_st"())
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
@file:Suppress("SpellCheckingInspection")
|
||||
|
||||
package app.revanced.patches.music.layout.miniplayer
|
||||
|
||||
import app.revanced.patcher.extensions.addInstructions
|
||||
import app.revanced.patcher.extensions.getInstruction
|
||||
import app.revanced.patcher.extensions.methodReference
|
||||
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.music.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.music.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.music.misc.settings.settingsPatch
|
||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/music/patches/ForciblyEnableMiniplayerPatch;"
|
||||
|
||||
@Suppress("unused")
|
||||
val forciblyEnableMiniplayerPatch = bytecodePatch(
|
||||
name = "Forcibly enable miniplayer",
|
||||
description = "Adds an option to forcibly enable the miniplayer when switching between music videos, podcasts, or songs."
|
||||
) {
|
||||
dependsOn(
|
||||
sharedExtensionPatch,
|
||||
settingsPatch,
|
||||
addResourcesPatch,
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
"com.google.android.apps.youtube.music"(
|
||||
"7.29.52",
|
||||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
|
||||
apply {
|
||||
addResources("music", "layout.miniplayer.forciblyEnableMiniplayer")
|
||||
|
||||
PreferenceScreen.PLAYER.addPreferences(
|
||||
SwitchPreference("revanced_music_forcibly_enable_miniplayer")
|
||||
)
|
||||
|
||||
minimizedPlayerMethod.apply {
|
||||
val invokeIndex = indexOfFirstInstructionOrThrow {
|
||||
opcode == Opcode.INVOKE_VIRTUAL && methodReference?.name == "booleanValue"
|
||||
}
|
||||
|
||||
val moveResultIndex = invokeIndex + 1
|
||||
val moveResultInstr = getInstruction<OneRegisterInstruction>(moveResultIndex)
|
||||
val targetRegister = moveResultInstr.registerA
|
||||
|
||||
addInstructions(
|
||||
moveResultIndex + 1,
|
||||
"""
|
||||
invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->forciblyEnableMiniplayerPatch(Z)Z
|
||||
move-result v$targetRegister
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,7 @@ val navigationBarPatch = bytecodePatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ val hideGetMusicPremiumPatch = bytecodePatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
package app.revanced.patches.music.layout.startpage
|
||||
|
||||
import app.revanced.patcher.extensions.addInstruction
|
||||
import app.revanced.patcher.extensions.addInstructions
|
||||
import app.revanced.patcher.extensions.fieldReference
|
||||
import app.revanced.patcher.extensions.getInstruction
|
||||
import app.revanced.patcher.extensions.instructions
|
||||
import app.revanced.patcher.extensions.string
|
||||
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.music.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.music.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.music.misc.settings.settingsPatch
|
||||
import app.revanced.patches.music.shared.mainActivityOnCreateMethod
|
||||
import app.revanced.patches.shared.misc.settings.preference.ListPreference
|
||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory
|
||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
||||
import app.revanced.util.indexOfFirstInstruction
|
||||
import app.revanced.util.indexOfFirstInstructionReversed
|
||||
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
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/music/patches/ChangeStartPagePatch;"
|
||||
|
||||
val changeStartPagePatch = bytecodePatch(
|
||||
name = "Change start page",
|
||||
description = "Adds an option to set which page the app opens in instead of the homepage.",
|
||||
) {
|
||||
dependsOn(
|
||||
sharedExtensionPatch,
|
||||
settingsPatch,
|
||||
addResourcesPatch
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
"com.google.android.apps.youtube.music"(
|
||||
"7.29.52",
|
||||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
)
|
||||
)
|
||||
|
||||
apply {
|
||||
addResources("music", "layout.startpage.changeStartPagePatch")
|
||||
|
||||
PreferenceScreen.GENERAL.addPreferences(
|
||||
PreferenceCategory(
|
||||
titleKey = null,
|
||||
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
|
||||
tag = "app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory",
|
||||
preferences = setOf(
|
||||
ListPreference(
|
||||
key = "revanced_change_start_page",
|
||||
tag = "app.revanced.extension.shared.settings.preference.SortedListPreference"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
coldStartUpMethodMatch.let { match ->
|
||||
match.method.apply {
|
||||
val defaultBrowseIdIndex = match[-1]
|
||||
|
||||
val browseIdIndex = indexOfFirstInstructionReversed(defaultBrowseIdIndex) {
|
||||
opcode == Opcode.IGET_OBJECT && fieldReference?.type == "Ljava/lang/String;"
|
||||
}
|
||||
|
||||
if (browseIdIndex != -1) {
|
||||
val browseIdRegister =
|
||||
getInstruction<TwoRegisterInstruction>(browseIdIndex).registerA
|
||||
addInstructions(
|
||||
browseIdIndex + 1,
|
||||
"""
|
||||
invoke-static/range { v$browseIdRegister .. v$browseIdRegister }, $EXTENSION_CLASS_DESCRIPTOR->overrideBrowseId(Ljava/lang/String;)Ljava/lang/String;
|
||||
move-result-object v$browseIdRegister
|
||||
"""
|
||||
)
|
||||
} else {
|
||||
instructions.mapIndexedNotNull { index, instr ->
|
||||
if (instr.opcode == Opcode.RETURN_OBJECT) index else null
|
||||
}.reversed().forEach { returnIndex ->
|
||||
val returnRegister =
|
||||
getInstruction<OneRegisterInstruction>(returnIndex).registerA
|
||||
|
||||
addInstructions(
|
||||
returnIndex,
|
||||
"""
|
||||
invoke-static/range { v$returnRegister .. v$returnRegister }, $EXTENSION_CLASS_DESCRIPTOR->overrideBrowseId(Ljava/lang/String;)Ljava/lang/String;
|
||||
move-result-object v$returnRegister
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainActivityOnCreateMethod.apply {
|
||||
val p0 = implementation!!.registerCount - 2
|
||||
val p1 = p0 + 1
|
||||
|
||||
addInstruction(
|
||||
0,
|
||||
"invoke-static/range { v$p0 .. v$p1 }, " +
|
||||
"$EXTENSION_CLASS_DESCRIPTOR->" +
|
||||
"overrideIntentActionOnCreate(Landroid/app/Activity;Landroid/os/Bundle;)V"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package app.revanced.patches.music.layout.startpage
|
||||
|
||||
import app.revanced.patcher.composingFirstMethod
|
||||
import app.revanced.patcher.instructions
|
||||
import app.revanced.patcher.invoke
|
||||
import app.revanced.patcher.parameterTypes
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.returnType
|
||||
|
||||
internal val BytecodePatchContext.coldStartUpMethodMatch by composingFirstMethod {
|
||||
returnType("Ljava/lang/String;")
|
||||
parameterTypes()
|
||||
instructions(
|
||||
"FEmusic_library_sideloaded_tracks"(),
|
||||
"FEmusic_home"()
|
||||
)
|
||||
}
|
||||
|
|
@ -7,7 +7,8 @@ import app.revanced.patches.shared.layout.theme.baseThemeResourcePatch
|
|||
import app.revanced.patches.shared.layout.theme.darkThemeBackgroundColorOption
|
||||
import app.revanced.patches.shared.misc.settings.overrideThemeColors
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/theme/ThemePatch;"
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/music/patches/theme/ThemePatch;"
|
||||
|
||||
@Suppress("unused")
|
||||
val themePatch = baseThemePatch(
|
||||
|
|
@ -33,7 +34,8 @@ val themePatch = baseThemePatch(
|
|||
"7.29.52",
|
||||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54"
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,14 +7,13 @@ import app.revanced.patcher.invoke
|
|||
import app.revanced.patcher.parameterTypes
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.returnType
|
||||
import app.revanced.patcher.strings
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
|
||||
internal val BytecodePatchContext.checkCertificateMethod by gettingFirstMethodDeclaratively(
|
||||
"X509",
|
||||
) {
|
||||
internal val BytecodePatchContext.checkCertificateMethod by gettingFirstMethodDeclaratively {
|
||||
returnType("Z")
|
||||
parameterTypes("Ljava/lang/String;")
|
||||
instructions("Failed to get certificate"(String::contains))
|
||||
parameterTypes("L")
|
||||
strings("X509", "isPartnerSHAFingerprint")
|
||||
}
|
||||
|
||||
internal val BytecodePatchContext.searchMediaItemsConstructorMethod by gettingFirstMethodDeclaratively(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ val unlockAndroidAutoMediaBrowserPatch = bytecodePatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ val forceOriginalAudioPatch = forceOriginalAudioPatch(
|
|||
"7.29.52",
|
||||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54"
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ val removeBackgroundPlaybackRestrictionsPatch = bytecodePatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ val enableDebuggingPatch = enableDebuggingPatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ val checkWatchHistoryDomainNameResolutionPatch = checkWatchHistoryDomainNameReso
|
|||
"7.29.52",
|
||||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54"
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
package app.revanced.patches.music.misc.gms
|
||||
|
||||
import app.revanced.patcher.*
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
|
||||
internal val BytecodePatchContext.musicActivityOnCreateMethod by gettingFirstMethodDeclaratively {
|
||||
name("onCreate")
|
||||
definingClass("/MusicActivity;")
|
||||
returnType("V")
|
||||
parameterTypes("Landroid/os/Bundle;")
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME
|
|||
import app.revanced.patches.music.misc.gms.Constants.REVANCED_MUSIC_PACKAGE_NAME
|
||||
import app.revanced.patches.music.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.music.misc.settings.settingsPatch
|
||||
import app.revanced.patches.music.shared.mainActivityOnCreateMethod
|
||||
import app.revanced.patches.shared.castContextFetchMethod
|
||||
import app.revanced.patches.shared.misc.gms.gmsCoreSupportPatch
|
||||
import app.revanced.patches.shared.misc.settings.preference.IntentPreference
|
||||
|
|
@ -23,7 +24,7 @@ val gmsCoreSupportPatch = gmsCoreSupportPatch(
|
|||
toPackageName = REVANCED_MUSIC_PACKAGE_NAME,
|
||||
getPrimeMethod = { primeMethod },
|
||||
getEarlyReturnMethods = setOf(BytecodePatchContext::castContextFetchMethod::get),
|
||||
getMainActivityOnCreateMethodToGetInsertIndex = BytecodePatchContext::musicActivityOnCreateMethod::get to { 0 },
|
||||
getMainActivityOnCreateMethodToGetInsertIndex = BytecodePatchContext::mainActivityOnCreateMethod::get to { 0 },
|
||||
extensionPatch = sharedExtensionPatch,
|
||||
gmsCoreSupportResourcePatchFactory = ::gmsCoreSupportResourcePatch,
|
||||
) {
|
||||
|
|
@ -34,6 +35,7 @@ val gmsCoreSupportPatch = gmsCoreSupportPatch(
|
|||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,16 @@
|
|||
package app.revanced.patches.music.misc.litho.filter
|
||||
|
||||
import app.revanced.patcher.extensions.addInstruction
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.music.shared.conversionContextToStringMethod
|
||||
import app.revanced.patches.shared.misc.litho.filter.EXTENSION_CLASS_DESCRIPTOR
|
||||
import app.revanced.patches.shared.misc.litho.filter.lithoFilterPatch
|
||||
import app.revanced.patches.shared.misc.litho.filter.protobufBufferReferenceLegacyMethod
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import app.revanced.patches.shared.misc.litho.filter.protobufBufferReferenceMethod
|
||||
|
||||
val lithoFilterPatch = lithoFilterPatch(
|
||||
componentCreateInsertionIndex = {
|
||||
// No supported version clobbers p2 so we can just do our things before the return instruction.
|
||||
indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT)
|
||||
},
|
||||
getConversionContextToStringMethod = BytecodePatchContext::conversionContextToStringMethod::get,
|
||||
insertProtobufHook = {
|
||||
protobufBufferReferenceLegacyMethod.addInstruction(
|
||||
insertLegacyProtobufHook = {
|
||||
protobufBufferReferenceMethod.addInstruction(
|
||||
0,
|
||||
"invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V",
|
||||
"invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtobufBuffer(Ljava/nio/ByteBuffer;)V",
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ val sanitizeSharingLinksPatch = sanitizeSharingLinksPatch(
|
|||
"7.29.52",
|
||||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54"
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package app.revanced.patches.music.misc.spoof
|
|||
import app.revanced.patches.all.misc.resources.addResources
|
||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.music.misc.gms.musicActivityOnCreateMethod
|
||||
import app.revanced.patches.music.shared.mainActivityOnCreateMethod
|
||||
import app.revanced.patches.music.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.music.misc.settings.settingsPatch
|
||||
import app.revanced.patches.music.playservice.*
|
||||
|
|
@ -14,7 +14,7 @@ import app.revanced.patches.shared.misc.spoof.spoofVideoStreamsPatch
|
|||
|
||||
val spoofVideoStreamsPatch = spoofVideoStreamsPatch(
|
||||
extensionClassDescriptor = "Lapp/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch;",
|
||||
getMainActivityOnCreateMethod = { musicActivityOnCreateMethod },
|
||||
getMainActivityOnCreateMethod = { mainActivityOnCreateMethod },
|
||||
fixMediaFetchHotConfig = { is_7_16_or_greater },
|
||||
fixMediaFetchHotConfigAlternative = { is_8_11_or_greater && !is_8_15_or_greater },
|
||||
fixParsePlaybackResponseFeatureFlag = { is_7_33_or_greater },
|
||||
|
|
@ -33,7 +33,8 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch(
|
|||
"7.29.52",
|
||||
"8.10.52",
|
||||
"8.37.56",
|
||||
"8.40.54"
|
||||
"8.40.54",
|
||||
"8.44.54"
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
package app.revanced.patches.shared.layout.branding.header
|
||||
|
||||
import app.revanced.patcher.patch.Package
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.ResourcePatchContext
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.patcher.patch.stringOption
|
||||
import app.revanced.patches.all.misc.resources.addResources
|
||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||
import app.revanced.patches.shared.misc.settings.preference.BasePreferenceScreen
|
||||
import app.revanced.patches.shared.misc.settings.preference.ListPreference
|
||||
import app.revanced.util.ResourceGroup
|
||||
import app.revanced.util.Utils.trimIndentMultiline
|
||||
import app.revanced.util.copyResources
|
||||
import java.io.File
|
||||
|
||||
internal const val CUSTOM_HEADER_RESOURCE_NAME = "revanced_header_custom"
|
||||
|
||||
@Suppress("unused")
|
||||
fun changeHeaderPatch(
|
||||
targetResourceDirectoryNames: Map<String, String>,
|
||||
changeHeaderBytecodePatch: Patch,
|
||||
vararg compatiblePackages: Package,
|
||||
variants: Array<String>,
|
||||
logoResourceNames: Array<String>,
|
||||
preferenceScreen: BasePreferenceScreen.Screen,
|
||||
resourcesAppId: String,
|
||||
applyBlock: ResourcePatchContext.() -> Unit = {},
|
||||
): Patch {
|
||||
val customHeaderResourceFileNames = variants.map { variant ->
|
||||
"${CUSTOM_HEADER_RESOURCE_NAME}_$variant.png"
|
||||
}.toTypedArray()
|
||||
|
||||
return resourcePatch(
|
||||
name = "Change header",
|
||||
description = "Adds an option to change the header logo in the top left corner of the app.",
|
||||
) {
|
||||
dependsOn(addResourcesPatch, changeHeaderBytecodePatch)
|
||||
|
||||
compatibleWith(packages = compatiblePackages)
|
||||
|
||||
val custom by stringOption(
|
||||
name = "Custom header logo",
|
||||
description = """
|
||||
Folder with images to use as a custom header logo.
|
||||
|
||||
The folder must contain one or more of the following folders, depending on the DPI of the device:
|
||||
${targetResourceDirectoryNames.keys.joinToString("\n") { "- $it" }}
|
||||
|
||||
Each of the folders must contain all of the following files:
|
||||
${customHeaderResourceFileNames.joinToString("\n")}
|
||||
|
||||
The image dimensions must be as follows:
|
||||
${targetResourceDirectoryNames.map { (dpi, dim) -> "- $dpi: $dim" }.joinToString("\n")}
|
||||
""".trimIndentMultiline(),
|
||||
)
|
||||
|
||||
apply {
|
||||
addResources(resourcesAppId, "layout.branding.header.changeHeaderPatch")
|
||||
|
||||
preferenceScreen.addPreferences(
|
||||
if (custom == null) {
|
||||
ListPreference("revanced_header_logo")
|
||||
} else {
|
||||
ListPreference(
|
||||
key = "revanced_header_logo",
|
||||
entriesKey = "revanced_header_logo_custom_entries",
|
||||
entryValuesKey = "revanced_header_logo_custom_entry_values",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
logoResourceNames.forEach { logo ->
|
||||
variants.forEach { variant ->
|
||||
copyResources(
|
||||
"change-header",
|
||||
ResourceGroup(
|
||||
"drawable",
|
||||
logo + "_" + variant + ".xml",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy custom template. Images are only used if settings
|
||||
// are imported and a custom header is enabled.
|
||||
targetResourceDirectoryNames.keys.forEach { dpi ->
|
||||
variants.forEach { variant ->
|
||||
copyResources(
|
||||
"change-header",
|
||||
ResourceGroup(
|
||||
dpi,
|
||||
resources = customHeaderResourceFileNames,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
applyBlock()
|
||||
|
||||
// Copy user provided images last, so if an exception is thrown due to bad input.
|
||||
if (custom != null) {
|
||||
val customFile = File(custom!!.trim())
|
||||
if (!customFile.exists()) {
|
||||
throw PatchException(
|
||||
"The custom header path cannot be found: " +
|
||||
customFile.absolutePath,
|
||||
)
|
||||
}
|
||||
|
||||
if (!customFile.isDirectory) {
|
||||
throw PatchException(
|
||||
"The custom header path must be a folder: " +
|
||||
customFile.absolutePath,
|
||||
)
|
||||
}
|
||||
|
||||
var copiedFiles = false
|
||||
|
||||
// For each source folder, copy the files to the target resource directories.
|
||||
customFile.listFiles { file ->
|
||||
file.isDirectory && file.name in targetResourceDirectoryNames
|
||||
}!!.forEach { dpiSourceFolder ->
|
||||
val targetDpiFolder = get("res").resolve(dpiSourceFolder.name)
|
||||
if (!targetDpiFolder.exists()) {
|
||||
// Should never happen.
|
||||
throw IllegalStateException("Resource not found: $dpiSourceFolder")
|
||||
}
|
||||
|
||||
val customFiles = dpiSourceFolder.listFiles { file ->
|
||||
file.isFile && file.name in customHeaderResourceFileNames
|
||||
}!!
|
||||
|
||||
if (customFiles.isNotEmpty() && customFiles.size != variants.size) {
|
||||
throw PatchException(
|
||||
"Both light/dark mode images " +
|
||||
"must be specified but only found: " + customFiles.map { it.name },
|
||||
)
|
||||
}
|
||||
|
||||
customFiles.forEach { imgSourceFile ->
|
||||
val imgTargetFile = targetDpiFolder.resolve(imgSourceFile.name)
|
||||
imgSourceFile.copyTo(target = imgTargetFile, overwrite = true)
|
||||
|
||||
copiedFiles = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!copiedFiles) {
|
||||
throw PatchException(
|
||||
"Expected to find directories and files: " +
|
||||
customHeaderResourceFileNames.contentToString() +
|
||||
"\nBut none were found in the provided option file path: " + customFile.absolutePath,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +62,10 @@ internal val darkThemeBackgroundColorOption = stringOption(
|
|||
default = "@android:color/black",
|
||||
values = mapOf(
|
||||
"Pure black" to "@android:color/black",
|
||||
"Material You" to "@android:color/system_neutral1_900",
|
||||
"Material You (Neutral)" to "@android:color/system_neutral1_900",
|
||||
"Material You - Primary" to "@android:color/system_accent1_800",
|
||||
"Material You - Secondary" to "@android:color/system_accent2_800",
|
||||
"Material You - Tertiary" to "@android:color/system_accent3_800",
|
||||
"Classic (old YouTube)" to "#212121",
|
||||
"Catppuccin (Mocha)" to "#181825",
|
||||
"Dark pink" to "#290025",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
@file:Suppress("SpellCheckingInspection")
|
||||
|
||||
package app.revanced.patches.shared.misc.litho.context
|
||||
|
||||
|
||||
import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableClassDef
|
||||
import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
|
||||
import app.revanced.patcher.after
|
||||
import app.revanced.patcher.allOf
|
||||
import app.revanced.patcher.classDef
|
||||
import app.revanced.patcher.extensions.addInstructions
|
||||
import app.revanced.patcher.field
|
||||
import app.revanced.patcher.firstClassDef
|
||||
import app.revanced.patcher.firstMethodDeclaratively
|
||||
import app.revanced.patcher.instructions
|
||||
import app.revanced.patcher.invoke
|
||||
import app.revanced.patcher.parameterTypes
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.returnType
|
||||
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.util.findFieldFromToString
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
||||
|
||||
internal const val EXTENSION_CONTEXT_INTERFACE =
|
||||
$$"Lapp/revanced/extension/shared/ConversionContext$ContextInterface;"
|
||||
|
||||
internal lateinit var conversionContextClassDef: MutableClassDef
|
||||
|
||||
val conversionContextPatch = bytecodePatch(
|
||||
description = "Hooks the method to use the conversion context in an extension.",
|
||||
) {
|
||||
dependsOn(sharedExtensionPatch())
|
||||
|
||||
apply {
|
||||
conversionContextClassDef = conversionContextToStringMethod.classDef
|
||||
|
||||
val identifierField = conversionContextToStringMethod
|
||||
.findFieldFromToString(IDENTIFIER_PROPERTY)
|
||||
val stringBuilderField = conversionContextClassDef
|
||||
.fields.single { field -> field.type == "Ljava/lang/StringBuilder;" }
|
||||
|
||||
// The conversionContext class can be used as is in most versions.
|
||||
if (conversionContextClassDef.superclass == "Ljava/lang/Object;") {
|
||||
arrayOf(
|
||||
identifierField,
|
||||
stringBuilderField
|
||||
).map {
|
||||
|
||||
}
|
||||
conversionContextClassDef.apply {
|
||||
// Add interface and helper methods to allow extension code to call obfuscated methods.
|
||||
interfaces += EXTENSION_CONTEXT_INTERFACE
|
||||
|
||||
arrayOf(
|
||||
Triple(
|
||||
"patch_getIdentifier",
|
||||
"Ljava/lang/String;",
|
||||
identifierField
|
||||
),
|
||||
Triple(
|
||||
"patch_getPathBuilder",
|
||||
"Ljava/lang/StringBuilder;",
|
||||
stringBuilderField
|
||||
)
|
||||
).forEach { (interfaceMethodName, interfaceMethodReturnType, classFieldReference) ->
|
||||
ImmutableMethod(
|
||||
type,
|
||||
interfaceMethodName,
|
||||
listOf(),
|
||||
interfaceMethodReturnType,
|
||||
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
|
||||
null,
|
||||
null,
|
||||
MutableMethodImplementation(2),
|
||||
).toMutable().apply {
|
||||
addInstructions(
|
||||
0,
|
||||
"""
|
||||
iget-object v0, p0, $classFieldReference
|
||||
return-object v0
|
||||
"""
|
||||
)
|
||||
}.let(methods::add)
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// In some special versions, such as YouTube 20.41, it inherits from an abstract class,
|
||||
// in which case a helper method is added to the abstract class.
|
||||
|
||||
// Since fields cannot be accessed directly in an abstract class, abstract methods are linked.
|
||||
val stringBuilderMethodName = conversionContextClassDef.firstMethodDeclaratively {
|
||||
parameterTypes()
|
||||
returnType("Ljava/lang/String;")
|
||||
instructions(
|
||||
allOf(Opcode.IGET_OBJECT(), field { this == identifierField }),
|
||||
after(Opcode.RETURN_OBJECT()),
|
||||
)
|
||||
}.name
|
||||
|
||||
val identifierMethodName = conversionContextClassDef.firstMethodDeclaratively {
|
||||
parameterTypes()
|
||||
returnType("Ljava/lang/StringBuilder;")
|
||||
instructions(
|
||||
allOf(Opcode.IGET_OBJECT(), field { this == stringBuilderField }),
|
||||
after(Opcode.RETURN_OBJECT()),
|
||||
)
|
||||
}.name
|
||||
|
||||
conversionContextClassDef = firstClassDef(conversionContextClassDef.superclass!!)
|
||||
|
||||
conversionContextClassDef.apply {
|
||||
// Add interface and helper methods to allow extension code to call obfuscated methods.
|
||||
interfaces += EXTENSION_CONTEXT_INTERFACE
|
||||
|
||||
arrayOf(
|
||||
Triple(
|
||||
"patch_getIdentifier",
|
||||
"Ljava/lang/String;",
|
||||
identifierMethodName
|
||||
),
|
||||
Triple(
|
||||
"patch_getPathBuilder",
|
||||
"Ljava/lang/StringBuilder;",
|
||||
stringBuilderMethodName
|
||||
)
|
||||
).forEach { (interfaceMethodName, interfaceMethodReturnType, classMethodName) ->
|
||||
ImmutableMethod(
|
||||
type,
|
||||
interfaceMethodName,
|
||||
listOf(),
|
||||
interfaceMethodReturnType,
|
||||
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
|
||||
null,
|
||||
null,
|
||||
MutableMethodImplementation(2),
|
||||
).toMutable().apply {
|
||||
addInstructions(
|
||||
0,
|
||||
"""
|
||||
invoke-virtual {p0}, $type->$classMethodName()$interfaceMethodReturnType
|
||||
move-result-object v0
|
||||
return-object v0
|
||||
"""
|
||||
)
|
||||
}.let(methods::add)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package app.revanced.patches.shared.misc.litho.context
|
||||
|
||||
import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively
|
||||
import app.revanced.patcher.instructions
|
||||
import app.revanced.patcher.invoke
|
||||
import app.revanced.patcher.name
|
||||
import app.revanced.patcher.parameterTypes
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.returnType
|
||||
|
||||
internal const val IDENTIFIER_PROPERTY = ", identifierProperty="
|
||||
|
||||
|
||||
internal val BytecodePatchContext.conversionContextToStringMethod by gettingFirstImmutableMethodDeclaratively(
|
||||
", widthConstraint=",
|
||||
", heightConstraint=",
|
||||
", templateLoggerFactory=",
|
||||
", rootDisposableContainer=",
|
||||
IDENTIFIER_PROPERTY
|
||||
) {
|
||||
name("toString")
|
||||
parameterTypes()
|
||||
returnType("Ljava/lang/String;")
|
||||
instructions("ConversionContext{"(String::contains))
|
||||
}
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
package app.revanced.patches.shared.misc.litho.filter
|
||||
|
||||
import app.revanced.patcher.*
|
||||
import app.revanced.patcher.firstMethodComposite
|
||||
import app.revanced.patcher.instructions
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.returnType
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
|
||||
internal val BytecodePatchContext.accessibilityIdMethodMatch by composingFirstMethod {
|
||||
|
|
@ -16,29 +20,59 @@ internal val BytecodePatchContext.accessibilityIdMethodMatch by composingFirstMe
|
|||
)
|
||||
}
|
||||
|
||||
internal fun BytecodePatchContext.getAccessibilityTextMethodMatch(accessibilityIdMethod: MethodReference) = firstMethodComposite {
|
||||
returnType("V")
|
||||
custom {
|
||||
// 'public final synthetic' or 'public final bridge synthetic'.
|
||||
AccessFlags.SYNTHETIC.isSet(accessFlags)
|
||||
internal fun BytecodePatchContext.getAccessibilityTextMethodMatch(accessibilityIdMethod: MethodReference) =
|
||||
firstMethodComposite {
|
||||
returnType("V")
|
||||
custom {
|
||||
// 'public final synthetic' or 'public final bridge synthetic'.
|
||||
AccessFlags.SYNTHETIC.isSet(accessFlags)
|
||||
}
|
||||
instructions(
|
||||
allOf(
|
||||
Opcode.INVOKE_INTERFACE(),
|
||||
method { parameterTypes.isEmpty() && returnType == "Ljava/lang/String;" }
|
||||
),
|
||||
afterAtMost(5, method { this == accessibilityIdMethod })
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
internal fun ClassDef.getEmptyComponentMethod() = firstMethodDeclaratively {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||
returnType("L")
|
||||
parameterTypes("L")
|
||||
}
|
||||
|
||||
internal val BytecodePatchContext.emptyComponentParentMethod by gettingFirstMethodDeclaratively {
|
||||
accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR)
|
||||
parameterTypes()
|
||||
instructions("EmptyComponent"())
|
||||
}
|
||||
|
||||
fun BytecodePatchContext.getComponentCreateMethodMatch(accessibilityIdMethod: MethodReference) = firstMethodComposite {
|
||||
returnType("L")
|
||||
instructions(
|
||||
allOf(
|
||||
Opcode.INVOKE_INTERFACE(),
|
||||
method { parameterTypes.isEmpty() && returnType == "Ljava/lang/String;" }
|
||||
Opcode.IF_EQZ(),
|
||||
afterAtMost(
|
||||
5,
|
||||
allOf(Opcode.CHECK_CAST(), type(accessibilityIdMethod.definingClass))
|
||||
),
|
||||
afterAtMost(5, method { this == accessibilityIdMethod })
|
||||
Opcode.RETURN_OBJECT(),
|
||||
"Element missing correct type extension"(),
|
||||
"Element missing type"()
|
||||
)
|
||||
}
|
||||
|
||||
internal val BytecodePatchContext.lithoFilterInitMethod by gettingFirstMethodDeclaratively {
|
||||
definingClass("/LithoFilterPatch;")
|
||||
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
|
||||
}
|
||||
|
||||
internal val BytecodePatchContext.protobufBufferReferenceMethodMatch by composingFirstMethod {
|
||||
internal val BytecodePatchContext.protobufBufferEncodeMethod by gettingFirstMethodDeclaratively {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
returnType("V")
|
||||
parameterTypes("[B")
|
||||
returnType("[B")
|
||||
parameterTypes()
|
||||
|
||||
var methodDefiningClass = ""
|
||||
custom {
|
||||
|
|
@ -51,32 +85,17 @@ internal val BytecodePatchContext.protobufBufferReferenceMethodMatch by composin
|
|||
Opcode.IGET_OBJECT(),
|
||||
field { definingClass == methodDefiningClass && type == "Lcom/google/android/libraries/elements/adl/UpbMessage;" },
|
||||
),
|
||||
method { definingClass == "Lcom/google/android/libraries/elements/adl/UpbMessage;" && name == "jniDecode" },
|
||||
method { definingClass == "Lcom/google/android/libraries/elements/adl/UpbMessage;" && name == "jniEncode" },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches a method that use the protobuf of our component.
|
||||
*/
|
||||
internal val BytecodePatchContext.protobufBufferReferenceLegacyMethod by gettingFirstMethodDeclaratively {
|
||||
internal val BytecodePatchContext.protobufBufferReferenceMethod by gettingFirstMethodDeclaratively {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
returnType("V")
|
||||
parameterTypes("I", "Ljava/nio/ByteBuffer;")
|
||||
opcodes(Opcode.IPUT, Opcode.INVOKE_VIRTUAL, Opcode.MOVE_RESULT, Opcode.SUB_INT_2ADDR)
|
||||
}
|
||||
|
||||
internal val BytecodePatchContext.emptyComponentMethod by gettingFirstImmutableMethodDeclaratively {
|
||||
accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR)
|
||||
parameterTypes()
|
||||
instructions("EmptyComponent"())
|
||||
custom { immutableClassDef.methods.filter { AccessFlags.STATIC.isSet(it.accessFlags) }.size == 1 }
|
||||
}
|
||||
|
||||
internal val BytecodePatchContext.componentCreateMethod by gettingFirstMethod(
|
||||
"Element missing correct type extension",
|
||||
"Element missing type",
|
||||
)
|
||||
|
||||
internal val BytecodePatchContext.lithoThreadExecutorMethod by gettingFirstMethodDeclaratively {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
|
||||
parameterTypes("I", "I", "I")
|
||||
|
|
|
|||
|
|
@ -2,34 +2,22 @@
|
|||
|
||||
package app.revanced.patches.shared.misc.litho.filter
|
||||
|
||||
import app.revanced.util.getFreeRegisterProvider
|
||||
import app.revanced.com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
|
||||
import app.revanced.patcher.afterAtMost
|
||||
import app.revanced.patcher.allOf
|
||||
import app.revanced.patcher.classDef
|
||||
import app.revanced.patcher.custom
|
||||
import app.revanced.util.getFreeRegisterProvider
|
||||
import app.revanced.patcher.extensions.addInstructions
|
||||
import app.revanced.patcher.extensions.getInstruction
|
||||
import app.revanced.patcher.extensions.methodReference
|
||||
import app.revanced.patcher.extensions.removeInstructions
|
||||
import app.revanced.patcher.extensions.typeReference
|
||||
import app.revanced.patcher.firstImmutableClassDef
|
||||
import app.revanced.patcher.firstMethodComposite
|
||||
import app.revanced.patcher.firstClassDef
|
||||
import app.revanced.patcher.immutableClassDef
|
||||
import app.revanced.patcher.instructions
|
||||
import app.revanced.patcher.invoke
|
||||
import app.revanced.patcher.method
|
||||
import app.revanced.patcher.patch.BytecodePatchBuilder
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.returnType
|
||||
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.shared.misc.litho.context.EXTENSION_CONTEXT_INTERFACE
|
||||
import app.revanced.patches.shared.misc.litho.context.conversionContextPatch
|
||||
import app.revanced.util.addInstructionsAtControlFlowLabel
|
||||
import app.revanced.util.findFieldFromToString
|
||||
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.Method
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.immutable.value.ImmutableBooleanEncodedValue
|
||||
|
|
@ -51,26 +39,20 @@ internal const val EXTENSION_CLASS_DESCRIPTOR =
|
|||
/**
|
||||
* A patch that allows to filter Litho components based on their identifier or path.
|
||||
*
|
||||
* @param componentCreateInsertionIndex The index to insert the filtering code in the component create method.
|
||||
* @param insertProtobufHook This method injects a setProtoBuffer call in the protobuf decoding logic.
|
||||
* @param getConversionContextToStringMethod The getter of the conversion context toString method.
|
||||
* @param insertLegacyProtobufHook Hook legacy protobuf buffer into the extension to be used for filtering for older versions of the app.
|
||||
* @param getExtractIdentifierFromBuffer Whether to extract the identifier from the protobuf buffer.
|
||||
* @param executeBlock The additional execution block of the patch.
|
||||
* @param block The additional block to build the patch.
|
||||
*/
|
||||
internal fun lithoFilterPatch(
|
||||
componentCreateInsertionIndex: Method.() -> Int,
|
||||
insertProtobufHook: BytecodePatchContext.() -> Unit,
|
||||
insertLegacyProtobufHook: BytecodePatchContext.() -> Unit,
|
||||
executeBlock: BytecodePatchContext.() -> Unit = {},
|
||||
getConversionContextToStringMethod: BytecodePatchContext.() -> Method,
|
||||
getExtractIdentifierFromBuffer: () -> Boolean = { false },
|
||||
block: BytecodePatchBuilder.() -> Unit = {},
|
||||
) = bytecodePatch(
|
||||
description = "Hooks the method which parses the bytes into a ComponentContext to filter components.",
|
||||
) {
|
||||
dependsOn(
|
||||
sharedExtensionPatch(),
|
||||
)
|
||||
dependsOn(sharedExtensionPatch(), conversionContextPatch)
|
||||
|
||||
/**
|
||||
* The following patch inserts a hook into the method that parses the bytes into a ComponentContext.
|
||||
|
|
@ -92,7 +74,7 @@ internal fun lithoFilterPatch(
|
|||
* class SomeOtherClass {
|
||||
* // Called before ComponentContextParser.parseComponent() method.
|
||||
* public void someOtherMethod(ByteBuffer byteBuffer) {
|
||||
* ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch.
|
||||
* ExtensionClass.setProtobugBuffer(byteBuffer); // Inserted by this patch.
|
||||
* ...
|
||||
* }
|
||||
* }
|
||||
|
|
@ -134,45 +116,23 @@ internal fun lithoFilterPatch(
|
|||
}
|
||||
}
|
||||
|
||||
// Tell the extension whether to extract the identifier from the buffer.
|
||||
if (getExtractIdentifierFromBuffer()) {
|
||||
lithoFilterInitMethod.classDef.fields.first { it.name == "EXTRACT_IDENTIFIER_FROM_BUFFER" }
|
||||
.initialValue = ImmutableBooleanEncodedValue.forBoolean(true).toMutable()
|
||||
}
|
||||
// region Pass the buffer into extension.
|
||||
|
||||
// Add an interceptor to steal the protobuf of our component.
|
||||
insertProtobufHook()
|
||||
insertLegacyProtobufHook()
|
||||
|
||||
// Hook the method that parses bytes into a ComponentContext.
|
||||
// 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 high memory usage and poor app performance can occur.
|
||||
// endregion
|
||||
|
||||
val conversionContextToStringMethod = getConversionContextToStringMethod()
|
||||
// region Modify the create component method and
|
||||
// if the component is filtered then return an empty component.
|
||||
|
||||
// Find the identifier/path fields of the conversion context.
|
||||
val conversionContextIdentifierField = conversionContextToStringMethod
|
||||
.findFieldFromToString("identifierProperty=")
|
||||
val builderMethodDescriptor =
|
||||
emptyComponentParentMethod.immutableClassDef.getEmptyComponentMethod()
|
||||
|
||||
val conversionContextPathBuilderField = conversionContextToStringMethod.immutableClassDef
|
||||
.fields.single { field -> field.type == "Ljava/lang/StringBuilder;" }
|
||||
|
||||
// Find class and methods to create an empty component.
|
||||
val builderMethodDescriptor = emptyComponentMethod.immutableClassDef.methods.single {
|
||||
// The only static method in the class.
|
||||
method ->
|
||||
AccessFlags.STATIC.isSet(method.accessFlags)
|
||||
}
|
||||
|
||||
val emptyComponentField = firstImmutableClassDef {
|
||||
// Only one field that matches.
|
||||
type == builderMethodDescriptor.returnType
|
||||
}.fields.single()
|
||||
val emptyComponentField = firstClassDef(builderMethodDescriptor.returnType).fields.single()
|
||||
|
||||
// Find the method call that gets the value of 'buttonViewModel.accessibilityId'.
|
||||
val accessibilityIdMethod = accessibilityIdMethodMatch.let {
|
||||
it.immutableMethod.getInstruction<ReferenceInstruction>(it[0]).methodReference!!
|
||||
it.method.getInstruction<ReferenceInstruction>(it[0]).methodReference!!
|
||||
}
|
||||
|
||||
// There's a method in the same class that gets the value of 'buttonViewModel.accessibilityText'.
|
||||
|
|
@ -182,35 +142,26 @@ internal fun lithoFilterPatch(
|
|||
it.method.getInstruction<ReferenceInstruction>(it[0]).methodReference
|
||||
}
|
||||
|
||||
componentCreateMethod.apply {
|
||||
val insertIndex = componentCreateInsertionIndex()
|
||||
getComponentCreateMethodMatch(accessibilityIdMethod).let {
|
||||
val insertIndex = it[2]
|
||||
val buttonViewModelIndex = it[1]
|
||||
val nullCheckIndex = it[0]
|
||||
|
||||
// Directly access the class related with the buttonViewModel from this method.
|
||||
// This is within 10 lines of insertIndex.
|
||||
val buttonViewModelIndex = indexOfFirstInstructionReversedOrThrow(insertIndex) {
|
||||
opcode == Opcode.CHECK_CAST &&
|
||||
typeReference?.type == accessibilityIdMethod.definingClass
|
||||
}
|
||||
val buttonViewModelRegister =
|
||||
getInstruction<OneRegisterInstruction>(buttonViewModelIndex).registerA
|
||||
it.method.getInstruction<OneRegisterInstruction>(buttonViewModelIndex).registerA
|
||||
val accessibilityIdIndex = buttonViewModelIndex + 2
|
||||
|
||||
// This is an index that checks if there is accessibility-related text.
|
||||
// This is within 10 lines of buttonViewModelIndex.
|
||||
val nullCheckIndex = indexOfFirstInstructionReversedOrThrow(
|
||||
buttonViewModelIndex, Opcode.IF_EQZ
|
||||
)
|
||||
|
||||
val registerProvider = getFreeRegisterProvider(
|
||||
val registerProvider = it.method.getFreeRegisterProvider(
|
||||
insertIndex, 3, buttonViewModelRegister
|
||||
)
|
||||
val contextRegister = registerProvider.getFreeRegister()
|
||||
val bufferRegister = registerProvider.getFreeRegister()
|
||||
val freeRegister = registerProvider.getFreeRegister()
|
||||
val identifierRegister = registerProvider.getFreeRegister()
|
||||
val pathRegister = registerProvider.getFreeRegister()
|
||||
|
||||
|
||||
// Find a free register to store the accessibilityId and accessibilityText.
|
||||
// This is before the insertion index.
|
||||
val accessibilityRegisterProvider = getFreeRegisterProvider(
|
||||
val accessibilityRegisterProvider = it.method.getFreeRegisterProvider(
|
||||
nullCheckIndex,
|
||||
2,
|
||||
registerProvider.getUsedAndUnAvailableRegisters()
|
||||
|
|
@ -218,23 +169,31 @@ internal fun lithoFilterPatch(
|
|||
val accessibilityIdRegister = accessibilityRegisterProvider.getFreeRegister()
|
||||
val accessibilityTextRegister = accessibilityRegisterProvider.getFreeRegister()
|
||||
|
||||
addInstructionsAtControlFlowLabel(
|
||||
it.method.addInstructionsAtControlFlowLabel(
|
||||
insertIndex,
|
||||
"""
|
||||
move-object/from16 v$freeRegister, p2 # ConversionContext parameter
|
||||
|
||||
# In YouTube 20.41 the field is the abstract superclass.
|
||||
# Verify it's the expected subclass just in case.
|
||||
instance-of v$identifierRegister, v$freeRegister, ${conversionContextToStringMethod.immutableClassDef.type}
|
||||
if-eqz v$identifierRegister, :unfiltered
|
||||
|
||||
iget-object v$identifierRegister, v$freeRegister, $conversionContextIdentifierField
|
||||
iget-object v$pathRegister, v$freeRegister, $conversionContextPathBuilderField
|
||||
invoke-static { v$identifierRegister, v$accessibilityIdRegister, v$accessibilityTextRegister, v$pathRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->isFiltered(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/StringBuilder;)Z
|
||||
move-object/from16 v$bufferRegister, p3
|
||||
|
||||
# Verify it's the expected subclass just in case.
|
||||
instance-of v$freeRegister, v$bufferRegister, ${protobufBufferEncodeMethod.definingClass}
|
||||
if-eqz v$freeRegister, :empty_buffer
|
||||
|
||||
check-cast v$bufferRegister, ${protobufBufferEncodeMethod.definingClass}
|
||||
invoke-virtual { v$bufferRegister }, $protobufBufferEncodeMethod
|
||||
move-result-object v$bufferRegister
|
||||
goto :hook
|
||||
|
||||
:empty_buffer
|
||||
const/4 v$freeRegister, 0x0
|
||||
new-array v$bufferRegister, v$freeRegister, [B
|
||||
|
||||
:hook
|
||||
move-object/from16 v$contextRegister, p2
|
||||
invoke-static { v$contextRegister, v$bufferRegister, v$accessibilityIdRegister, v$accessibilityTextRegister }, $EXTENSION_CLASS_DESCRIPTOR->isFiltered(${EXTENSION_CONTEXT_INTERFACE}[BLjava/lang/String;Ljava/lang/String;)Z
|
||||
move-result v$freeRegister
|
||||
if-eqz v$freeRegister, :unfiltered
|
||||
|
||||
# Return an empty component
|
||||
# Return an empty component.
|
||||
move-object/from16 v$freeRegister, p1
|
||||
invoke-static { v$freeRegister }, $builderMethodDescriptor
|
||||
move-result-object v$freeRegister
|
||||
|
|
@ -247,7 +206,7 @@ internal fun lithoFilterPatch(
|
|||
)
|
||||
|
||||
// If there is text related to accessibility, get the accessibilityId and accessibilityText.
|
||||
addInstructions(
|
||||
it.method.addInstructions(
|
||||
accessibilityIdIndex,
|
||||
"""
|
||||
# Get accessibilityId
|
||||
|
|
@ -262,7 +221,7 @@ internal fun lithoFilterPatch(
|
|||
|
||||
// If there is no accessibility-related text,
|
||||
// both accessibilityId and accessibilityText use empty values.
|
||||
addInstructions(
|
||||
it.method.addInstructions(
|
||||
nullCheckIndex,
|
||||
"""
|
||||
const-string v$accessibilityIdRegister, ""
|
||||
|
|
@ -271,6 +230,13 @@ internal fun lithoFilterPatch(
|
|||
)
|
||||
}
|
||||
|
||||
if (getExtractIdentifierFromBuffer()) {
|
||||
lithoFilterInitMethod.classDef.fields.first { it.name == "EXTRACT_IDENTIFIER_FROM_BUFFER" }
|
||||
.initialValue = ImmutableBooleanEncodedValue.forBoolean(true).toMutable()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// TODO: Check if needed in music.
|
||||
// Change Litho thread executor to 1 thread to fix layout issue in unpatched YouTube.
|
||||
lithoThreadExecutorMethod.addInstructions(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
|||
import app.revanced.patches.youtube.misc.contexthook.Endpoint
|
||||
import app.revanced.patches.youtube.misc.contexthook.addOSNameHook
|
||||
import app.revanced.patches.shared.misc.litho.filter.addLithoFilter
|
||||
import app.revanced.patches.youtube.layout.hide.shelves.hideHorizontalShelvesPatch
|
||||
import app.revanced.patches.youtube.misc.contexthook.hookClientContextPatch
|
||||
import app.revanced.patches.youtube.misc.engagement.addEngagementPanelIdHook
|
||||
import app.revanced.patches.youtube.misc.engagement.engagementPanelHookPatch
|
||||
|
|
@ -50,6 +51,7 @@ private val hideAdsResourcePatch = resourcePatch {
|
|||
addResourcesPatch,
|
||||
hookClientContextPatch,
|
||||
engagementPanelHookPatch,
|
||||
hideHorizontalShelvesPatch
|
||||
)
|
||||
|
||||
apply {
|
||||
|
|
@ -94,7 +96,9 @@ val hideAdsPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -212,6 +216,7 @@ val hideAdsPatch = bytecodePatch(
|
|||
setOf(
|
||||
Endpoint.BROWSE,
|
||||
Endpoint.SEARCH,
|
||||
Endpoint.NEXT,
|
||||
).forEach { endpoint ->
|
||||
addOSNameHook(
|
||||
endpoint,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
package app.revanced.patches.youtube.ad.video
|
||||
|
||||
import app.revanced.patcher.gettingFirstMethodDeclaratively
|
||||
import app.revanced.patcher.parameterTypes
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.returnType
|
||||
|
||||
internal val BytecodePatchContext.loadVideoAdsMethod by gettingFirstMethodDeclaratively(
|
||||
"TriggerBundle doesn't have the required metadata specified by the trigger ",
|
||||
"Ping migration no associated ping bindings for activated trigger: ",
|
||||
)
|
||||
|
||||
internal val BytecodePatchContext.playerBytesAdLayoutMethod by gettingFirstMethodDeclaratively(
|
||||
"Bootstrapped layout construction resulted in non PlayerBytesLayout. PlayerAds count: "
|
||||
) {
|
||||
returnType("V")
|
||||
parameterTypes("L")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
package app.revanced.patches.youtube.ad.video
|
||||
|
||||
import app.revanced.patcher.extensions.ExternalLabel
|
||||
import app.revanced.patcher.extensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.getInstruction
|
||||
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.SwitchPreference
|
||||
import app.revanced.patches.youtube.misc.contexthook.Endpoint
|
||||
import app.revanced.patches.youtube.misc.contexthook.addOSNameHook
|
||||
import app.revanced.patches.youtube.misc.contexthook.hookClientContextPatch
|
||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/youtube/patches/VideoAdsPatch;"
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
val videoAdsPatch = bytecodePatch(
|
||||
name = "Video ads",
|
||||
|
|
@ -20,6 +24,7 @@ val videoAdsPatch = bytecodePatch(
|
|||
sharedExtensionPatch,
|
||||
settingsPatch,
|
||||
addResourcesPatch,
|
||||
hookClientContextPatch
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
|
|
@ -29,7 +34,9 @@ val videoAdsPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -40,15 +47,33 @@ val videoAdsPatch = bytecodePatch(
|
|||
SwitchPreference("revanced_hide_video_ads"),
|
||||
)
|
||||
|
||||
loadVideoAdsMethod.addInstructionsWithLabels(
|
||||
0,
|
||||
"""
|
||||
invoke-static { }, Lapp/revanced/extension/youtube/patches/VideoAdsPatch;->shouldShowAds()Z
|
||||
move-result v0
|
||||
if-nez v0, :show_video_ads
|
||||
return-void
|
||||
""",
|
||||
ExternalLabel("show_video_ads", loadVideoAdsMethod.getInstruction(0)),
|
||||
)
|
||||
setOf(
|
||||
loadVideoAdsMethod,
|
||||
playerBytesAdLayoutMethod,
|
||||
).forEach { method ->
|
||||
method.addInstructionsWithLabels(
|
||||
0,
|
||||
"""
|
||||
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->hideVideoAds()Z
|
||||
move-result v0
|
||||
if-eqz v0, :show_video_ads
|
||||
return-void
|
||||
:show_video_ads
|
||||
nop
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
setOf(
|
||||
Endpoint.GET_WATCH,
|
||||
Endpoint.PLAYER,
|
||||
Endpoint.REEL,
|
||||
).forEach { endpoint ->
|
||||
addOSNameHook(
|
||||
endpoint,
|
||||
"$EXTENSION_CLASS_DESCRIPTOR->hideVideoAds(Ljava/lang/String;)Ljava/lang/String;",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,9 @@ val copyVideoURLPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@ val removeViewerDiscretionDialogPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,18 +22,15 @@ val addMoreDoubleTapToSeekLengthOptionsPatch = resourcePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
)
|
||||
)
|
||||
|
||||
execute {
|
||||
// Values are hard coded to keep patching simple.
|
||||
val doubleTapLengthOptionsString = "3, 5, 10, 15, 20, 30, 60, 120, 180, 240"
|
||||
|
||||
val doubleTapLengths = doubleTapLengthOptionsString
|
||||
.replace(" ", "")
|
||||
.split(",")
|
||||
if (doubleTapLengths.isEmpty()) throw PatchException("Invalid double-tap length elements")
|
||||
val doubleTapLengths = listOf(3, 5, 10, 15, 20, 30, 60, 120, 180, 240)
|
||||
|
||||
document("res/values/arrays.xml").use { document ->
|
||||
fun Element.removeAllChildren() {
|
||||
|
|
@ -56,10 +53,9 @@ val addMoreDoubleTapToSeekLengthOptionsPatch = resourcePatch(
|
|||
entries.removeAllChildren()
|
||||
|
||||
doubleTapLengths.forEach { length ->
|
||||
val item = document.createElement("item")
|
||||
item.textContent = length
|
||||
entries.appendChild(item)
|
||||
values.appendChild(item.cloneNode(true))
|
||||
document.createElement("item").apply { textContent = length.toString() }
|
||||
.also(entries::appendChild)
|
||||
.cloneNode(true).let(values::appendChild)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ val disableDoubleTapActionsPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,9 @@ val downloadsPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,9 @@ val disableHapticFeedbackPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ val seekbarPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,9 @@ val swipeControlsPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ val disableAutoCaptionsPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ val customBrandingPatch = baseCustomBrandingPatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,27 +3,24 @@ package app.revanced.patches.youtube.layout.branding.header
|
|||
import app.revanced.patcher.extensions.addInstructions
|
||||
import app.revanced.patcher.extensions.getInstruction
|
||||
import app.revanced.patcher.extensions.wideLiteral
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.patcher.patch.stringOption
|
||||
import app.revanced.patcher.util.Document
|
||||
import app.revanced.patches.all.misc.resources.addResources
|
||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||
import app.revanced.patches.shared.layout.branding.addBrandLicensePatch
|
||||
import app.revanced.patches.shared.layout.branding.header.CUSTOM_HEADER_RESOURCE_NAME
|
||||
import app.revanced.patches.shared.layout.branding.header.changeHeaderPatch
|
||||
import app.revanced.patches.shared.misc.mapping.ResourceType
|
||||
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||
import app.revanced.patches.shared.misc.settings.preference.ListPreference
|
||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||
import app.revanced.util.ResourceGroup
|
||||
import app.revanced.util.Utils.trimIndentMultiline
|
||||
import app.revanced.util.copyResources
|
||||
import app.revanced.util.findElementByAttributeValueOrThrow
|
||||
import app.revanced.util.forEachInstructionAsSequence
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import java.io.File
|
||||
|
||||
private val variants = arrayOf("light", "dark")
|
||||
internal val variants = arrayOf("light", "dark")
|
||||
|
||||
private val logoResourceNames = arrayOf(
|
||||
"revanced_header_minimal",
|
||||
"revanced_header_rounded",
|
||||
)
|
||||
|
||||
private val targetResourceDirectoryNames = mapOf(
|
||||
"drawable-hdpi" to "194x72 px",
|
||||
|
|
@ -32,25 +29,6 @@ private val targetResourceDirectoryNames = mapOf(
|
|||
"drawable-xxxhdpi" to "512x192 px",
|
||||
)
|
||||
|
||||
/**
|
||||
* Header logos built into this patch.
|
||||
*/
|
||||
private val logoResourceNames = arrayOf(
|
||||
"revanced_header_minimal",
|
||||
"revanced_header_rounded",
|
||||
)
|
||||
|
||||
/**
|
||||
* Custom header resource/file name.
|
||||
*/
|
||||
private const val CUSTOM_HEADER_RESOURCE_NAME = "revanced_header_custom"
|
||||
|
||||
/**
|
||||
* Custom header resource/file names.
|
||||
*/
|
||||
private val customHeaderResourceFileNames = variants.map { variant ->
|
||||
"${CUSTOM_HEADER_RESOURCE_NAME}_$variant.png"
|
||||
}.toTypedArray()
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/youtube/patches/ChangeHeaderPatch;"
|
||||
|
|
@ -97,182 +75,63 @@ private val changeHeaderBytecodePatch = bytecodePatch {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
val changeHeaderPatch = resourcePatch(
|
||||
name = "Change header",
|
||||
description = "Adds an option to change the header logo in the top left corner of the app.",
|
||||
) {
|
||||
dependsOn(addResourcesPatch, changeHeaderBytecodePatch)
|
||||
|
||||
compatibleWith(
|
||||
"com.google.android.youtube"(
|
||||
val changeHeaderPatch = changeHeaderPatch(
|
||||
targetResourceDirectoryNames = targetResourceDirectoryNames,
|
||||
changeHeaderBytecodePatch = changeHeaderBytecodePatch,
|
||||
compatiblePackages = arrayOf(
|
||||
"com.google.android.youtube" to setOf(
|
||||
"20.14.43",
|
||||
"20.21.37",
|
||||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
),
|
||||
)
|
||||
|
||||
val custom by stringOption(
|
||||
name = "Custom header logo",
|
||||
description = """
|
||||
Folder with images to use as a custom header logo.
|
||||
|
||||
The folder must contain one or more of the following folders, depending on the DPI of the device:
|
||||
${targetResourceDirectoryNames.keys.joinToString("\n") { "- $it" }}
|
||||
|
||||
Each of the folders must contain all of the following files:
|
||||
${customHeaderResourceFileNames.joinToString("\n")}
|
||||
|
||||
The image dimensions must be as follows:
|
||||
${targetResourceDirectoryNames.map { (dpi, dim) -> "- $dpi: $dim" }.joinToString("\n")}
|
||||
""".trimIndentMultiline(),
|
||||
)
|
||||
|
||||
apply {
|
||||
addResources("youtube", "layout.branding.changeHeaderPatch")
|
||||
|
||||
PreferenceScreen.GENERAL.addPreferences(
|
||||
if (custom == null) {
|
||||
ListPreference("revanced_header_logo")
|
||||
} else {
|
||||
ListPreference(
|
||||
key = "revanced_header_logo",
|
||||
entriesKey = "revanced_header_logo_custom_entries",
|
||||
entryValuesKey = "revanced_header_logo_custom_entry_values",
|
||||
)
|
||||
},
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
)
|
||||
),
|
||||
variants = variants,
|
||||
logoResourceNames = logoResourceNames,
|
||||
preferenceScreen = PreferenceScreen.GENERAL,
|
||||
resourcesAppId = "youtube",
|
||||
) {
|
||||
// Logo is replaced using an attribute reference.
|
||||
document("res/values/attrs.xml").use { document ->
|
||||
val resources = document.childNodes.item(0)
|
||||
|
||||
logoResourceNames.forEach { logo ->
|
||||
variants.forEach { variant ->
|
||||
copyResources(
|
||||
"change-header",
|
||||
ResourceGroup(
|
||||
"drawable",
|
||||
logo + "_" + variant + ".xml",
|
||||
),
|
||||
)
|
||||
}
|
||||
fun addAttributeReference(logoName: String) {
|
||||
val item = document.createElement("attr")
|
||||
item.setAttribute("format", "reference")
|
||||
item.setAttribute("name", logoName)
|
||||
resources.appendChild(item)
|
||||
}
|
||||
|
||||
// Copy custom template. Images are only used if settings
|
||||
// are imported and a custom header is enabled.
|
||||
targetResourceDirectoryNames.keys.forEach { dpi ->
|
||||
variants.forEach { variant ->
|
||||
copyResources(
|
||||
"change-header",
|
||||
ResourceGroup(
|
||||
dpi,
|
||||
*customHeaderResourceFileNames,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
logoResourceNames.forEach { logoName -> addAttributeReference(logoName) }
|
||||
|
||||
// Logo is replaced using an attribute reference.
|
||||
document("res/values/attrs.xml").use { document ->
|
||||
val resources = document.childNodes.item(0)
|
||||
addAttributeReference(CUSTOM_HEADER_RESOURCE_NAME)
|
||||
}
|
||||
|
||||
fun addAttributeReference(logoName: String) {
|
||||
val item = document.createElement("attr")
|
||||
item.setAttribute("format", "reference")
|
||||
// Add custom drawables to all styles that use the regular and premium logo.
|
||||
document("res/values/styles.xml").use { document ->
|
||||
arrayOf(
|
||||
"Base.Theme.YouTube.Light" to "light",
|
||||
"Base.Theme.YouTube.Dark" to "dark",
|
||||
"CairoLightThemeRingo2Updates" to "light",
|
||||
"CairoDarkThemeRingo2Updates" to "dark",
|
||||
).forEach { (style, mode) ->
|
||||
val styleElement = document.childNodes.findElementByAttributeValueOrThrow("name", style)
|
||||
|
||||
fun addDrawableElement(document: Document, logoName: String, mode: String) {
|
||||
val item = document.createElement("item")
|
||||
item.setAttribute("name", logoName)
|
||||
resources.appendChild(item)
|
||||
item.textContent = "@drawable/${logoName}_$mode"
|
||||
styleElement.appendChild(item)
|
||||
}
|
||||
|
||||
logoResourceNames.forEach { logoName ->
|
||||
addAttributeReference(logoName)
|
||||
}
|
||||
logoResourceNames.forEach { logoName -> addDrawableElement(document, logoName, mode) }
|
||||
|
||||
addAttributeReference(CUSTOM_HEADER_RESOURCE_NAME)
|
||||
}
|
||||
|
||||
// Add custom drawables to all styles that use the regular and premium logo.
|
||||
document("res/values/styles.xml").use { document ->
|
||||
arrayOf(
|
||||
"Base.Theme.YouTube.Light" to "light",
|
||||
"Base.Theme.YouTube.Dark" to "dark",
|
||||
"CairoLightThemeRingo2Updates" to "light",
|
||||
"CairoDarkThemeRingo2Updates" to "dark",
|
||||
).forEach { (style, mode) ->
|
||||
val styleElement = document.childNodes.findElementByAttributeValueOrThrow(
|
||||
"name",
|
||||
style,
|
||||
)
|
||||
|
||||
fun addDrawableElement(document: Document, logoName: String, mode: String) {
|
||||
val item = document.createElement("item")
|
||||
item.setAttribute("name", logoName)
|
||||
item.textContent = "@drawable/${logoName}_$mode"
|
||||
styleElement.appendChild(item)
|
||||
}
|
||||
|
||||
logoResourceNames.forEach { logoName ->
|
||||
addDrawableElement(document, logoName, mode)
|
||||
}
|
||||
|
||||
addDrawableElement(document, CUSTOM_HEADER_RESOURCE_NAME, mode)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy user provided images last, so if an exception is thrown due to bad input.
|
||||
if (custom != null) {
|
||||
val customFile = File(custom!!.trim())
|
||||
if (!customFile.exists()) {
|
||||
throw PatchException(
|
||||
"The custom header path cannot be found: " +
|
||||
customFile.absolutePath,
|
||||
)
|
||||
}
|
||||
|
||||
if (!customFile.isDirectory) {
|
||||
throw PatchException(
|
||||
"The custom header path must be a folder: " +
|
||||
customFile.absolutePath,
|
||||
)
|
||||
}
|
||||
|
||||
var copiedFiles = false
|
||||
|
||||
// For each source folder, copy the files to the target resource directories.
|
||||
customFile.listFiles { file ->
|
||||
file.isDirectory && file.name in targetResourceDirectoryNames
|
||||
}!!.forEach { dpiSourceFolder ->
|
||||
val targetDpiFolder = get("res").resolve(dpiSourceFolder.name)
|
||||
if (!targetDpiFolder.exists()) {
|
||||
// Should never happen.
|
||||
throw IllegalStateException("Resource not found: $dpiSourceFolder")
|
||||
}
|
||||
|
||||
val customFiles = dpiSourceFolder.listFiles { file ->
|
||||
file.isFile && file.name in customHeaderResourceFileNames
|
||||
}!!
|
||||
|
||||
if (customFiles.isNotEmpty() && customFiles.size != variants.size) {
|
||||
throw PatchException(
|
||||
"Both light/dark mode images " +
|
||||
"must be specified but only found: " + customFiles.map { it.name },
|
||||
)
|
||||
}
|
||||
|
||||
customFiles.forEach { imgSourceFile ->
|
||||
val imgTargetFile = targetDpiFolder.resolve(imgSourceFile.name)
|
||||
imgSourceFile.copyTo(target = imgTargetFile, overwrite = true)
|
||||
|
||||
copiedFiles = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!copiedFiles) {
|
||||
throw PatchException(
|
||||
"Expected to find directories and files: " +
|
||||
customHeaderResourceFileNames.contentToString() +
|
||||
"\nBut none were found in the provided option file path: " + customFile.absolutePath,
|
||||
)
|
||||
}
|
||||
addDrawableElement(document, CUSTOM_HEADER_RESOURCE_NAME, mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,9 @@ val hideVideoActionButtonsPatch = resourcePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package app.revanced.patches.youtube.layout.buttons.music
|
||||
|
||||
import app.revanced.patcher.accessFlags
|
||||
import app.revanced.patcher.definingClass
|
||||
import app.revanced.patcher.gettingFirstMethodDeclaratively
|
||||
import app.revanced.patcher.name
|
||||
import app.revanced.patcher.parameterTypes
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.returnType
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
internal val BytecodePatchContext.getOverridePackageNameMethod by gettingFirstMethodDeclaratively {
|
||||
name("getOverridePackageName")
|
||||
definingClass(EXTENSION_CLASS_DESCRIPTOR)
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||
returnType("Ljava/lang/String;")
|
||||
parameterTypes()
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package app.revanced.patches.youtube.layout.buttons.music
|
||||
|
||||
import app.revanced.patcher.extensions.getInstruction
|
||||
import app.revanced.patcher.extensions.methodReference
|
||||
import app.revanced.patcher.extensions.replaceInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.patcher.patch.stringOption
|
||||
import app.revanced.patches.all.misc.resources.addResources
|
||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory
|
||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||
import app.revanced.util.forEachInstructionAsSequence
|
||||
import app.revanced.util.returnEarly
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction
|
||||
import org.w3c.dom.Element
|
||||
|
||||
internal const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/youtube/patches/OverrideOpenInYouTubeMusicButtonPatch;"
|
||||
|
||||
val packageNameOption = stringOption(
|
||||
name = "YouTube Music package name",
|
||||
description = "The package name of the YouTube Music app to open when clicking the 'Open in YouTube Music' button.",
|
||||
default = "app.revanced.android.apps.youtube.music",
|
||||
values = mapOf(
|
||||
"Original package name" to "com.google.android.apps.youtube.music",
|
||||
"ReVanced default package name" to "app.revanced.android.apps.youtube.music"
|
||||
),
|
||||
required = true,
|
||||
)
|
||||
|
||||
private val overrideOpenInYouTubeMusicManifestResourcePatch = resourcePatch {
|
||||
apply {
|
||||
val packageName by packageNameOption
|
||||
|
||||
document("AndroidManifest.xml").use { document ->
|
||||
val queriesList = document.getElementsByTagName("queries")
|
||||
|
||||
val queries = if (queriesList.length > 0) queriesList.item(0) as Element
|
||||
else document.createElement("queries").also(document::appendChild)
|
||||
|
||||
document.createElement("package").apply {
|
||||
setAttribute("android:name", packageName)
|
||||
}.let(queries::appendChild)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
val overrideOpenInYouTubeMusicButtonPatch = bytecodePatch(
|
||||
name = "Override 'Open in YouTube Music' button",
|
||||
description = "Overrides the button to open YouTube Music under a different package name. " +
|
||||
"By default, it overrides to the ReVanced default package name of YouTube Music.",
|
||||
) {
|
||||
dependsOn(
|
||||
settingsPatch,
|
||||
addResourcesPatch,
|
||||
overrideOpenInYouTubeMusicManifestResourcePatch
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
"com.google.android.youtube"(
|
||||
"20.14.43",
|
||||
"20.21.37",
|
||||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
val packageName by packageNameOption()
|
||||
|
||||
apply {
|
||||
addResources("youtube", "layout.buttons.music.overrideOpenInYouTubeMusicButtonPatch")
|
||||
|
||||
PreferenceScreen.GENERAL.addPreferences(
|
||||
PreferenceCategory(
|
||||
titleKey = null,
|
||||
tag = "app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory",
|
||||
preferences = setOf(SwitchPreference(key = "revanced_override_open_in_youtube_music_button"))
|
||||
)
|
||||
)
|
||||
|
||||
getOverridePackageNameMethod.returnEarly(packageName!!)
|
||||
|
||||
forEachInstructionAsSequence({ _, _, instruction, index ->
|
||||
if (instruction.opcode != Opcode.INVOKE_VIRTUAL) return@forEachInstructionAsSequence null
|
||||
val reference = instruction.methodReference ?: return@forEachInstructionAsSequence null
|
||||
if (reference.definingClass != "Landroid/content/Intent;") return@forEachInstructionAsSequence null
|
||||
|
||||
when (reference.name) {
|
||||
"setPackage" if reference.parameterTypes == listOf("Ljava/lang/String;") ->
|
||||
index to "overrideSetPackage(Landroid/content/Intent;Ljava/lang/String;)Landroid/content/Intent;"
|
||||
|
||||
"setData" if reference.parameterTypes == listOf("Landroid/net/Uri;") ->
|
||||
index to "overrideSetData(Landroid/content/Intent;Landroid/net/Uri;)Landroid/content/Intent;"
|
||||
|
||||
else -> null
|
||||
}
|
||||
}) { method, (index, methodDescriptor) ->
|
||||
if (method.definingClass == EXTENSION_CLASS_DESCRIPTOR) return@forEachInstructionAsSequence
|
||||
|
||||
val invokeString = when (val instruction = method.getInstruction(index)) {
|
||||
is RegisterRangeInstruction ->
|
||||
"invoke-static/range { v${instruction.startRegister} .. v${instruction.startRegister + instruction.registerCount - 1} }"
|
||||
|
||||
is FiveRegisterInstruction ->
|
||||
"invoke-static { v${instruction.registerC}, v${instruction.registerD} }"
|
||||
|
||||
else -> return@forEachInstructionAsSequence
|
||||
}
|
||||
|
||||
method.replaceInstruction(
|
||||
index,
|
||||
"$invokeString, $EXTENSION_CLASS_DESCRIPTOR->$methodDescriptor"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -60,7 +60,9 @@ val navigationBarPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -146,7 +148,8 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch(
|
|||
titleAnchorMethodMatch.let {
|
||||
it.method.apply {
|
||||
val titleAnchorIndex = it[-1]
|
||||
val titleAnchorRegister = getInstruction<OneRegisterInstruction>(titleAnchorIndex).registerA
|
||||
val titleAnchorRegister =
|
||||
getInstruction<OneRegisterInstruction>(titleAnchorIndex).registerA
|
||||
|
||||
addInstruction(
|
||||
titleAnchorIndex + 1,
|
||||
|
|
@ -154,7 +157,8 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch(
|
|||
)
|
||||
|
||||
val playerCollapseButtonIndex = it[1]
|
||||
val playerCollapseButtonRegister = getInstruction<OneRegisterInstruction>(playerCollapseButtonIndex).registerA
|
||||
val playerCollapseButtonRegister =
|
||||
getInstruction<OneRegisterInstruction>(playerCollapseButtonIndex).registerA
|
||||
|
||||
addInstruction(
|
||||
playerCollapseButtonIndex + 1,
|
||||
|
|
@ -173,7 +177,8 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch(
|
|||
// so match on move-result-object after findViewById instead of check-cast.
|
||||
val moveResultIndex = it[2]
|
||||
val insertIndex = moveResultIndex + 1
|
||||
val insertRegister = getInstruction<OneRegisterInstruction>(moveResultIndex).registerA
|
||||
val insertRegister =
|
||||
getInstruction<OneRegisterInstruction>(moveResultIndex).registerA
|
||||
|
||||
addInstructionsWithLabels(
|
||||
insertIndex,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ 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.ListPreference
|
||||
import app.revanced.patches.youtube.misc.contexthook.Endpoint
|
||||
import app.revanced.patches.youtube.misc.contexthook.addClientFormFactorHook
|
||||
import app.revanced.patches.youtube.misc.contexthook.hookClientContextPatch
|
||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.youtube.misc.navigation.hookNavigationButtonCreated
|
||||
import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch
|
||||
|
|
@ -27,6 +30,7 @@ val changeFormFactorPatch = bytecodePatch(
|
|||
sharedExtensionPatch,
|
||||
settingsPatch,
|
||||
addResourcesPatch,
|
||||
hookClientContextPatch,
|
||||
navigationBarHookPatch
|
||||
)
|
||||
|
||||
|
|
@ -37,7 +41,9 @@ val changeFormFactorPatch = bytecodePatch(
|
|||
"20.26.46",
|
||||
"20.31.42",
|
||||
"20.37.48",
|
||||
"20.40.45"
|
||||
"20.40.45",
|
||||
"20.44.38",
|
||||
"20.45.36"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -48,8 +54,6 @@ val changeFormFactorPatch = bytecodePatch(
|
|||
ListPreference("revanced_change_form_factor"),
|
||||
)
|
||||
|
||||
hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR)
|
||||
|
||||
val formFactorEnumConstructorClass = formFactorEnumConstructorMethod.definingClass
|
||||
|
||||
val createPlayerRequestBodyWithModelMatch = firstMethodComposite {
|
||||
|
|
@ -70,11 +74,23 @@ val changeFormFactorPatch = bytecodePatch(
|
|||
addInstructions(
|
||||
index + 1,
|
||||
"""
|
||||
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getFormFactor(I)I
|
||||
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getUniversalFormFactor(I)I
|
||||
move-result v$register
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setOf(
|
||||
Endpoint.GET_WATCH,
|
||||
Endpoint.NEXT,
|
||||
Endpoint.GUIDE,
|
||||
Endpoint.REEL,
|
||||
).forEach { endpoint ->
|
||||
addClientFormFactorHook(
|
||||
endpoint,
|
||||
"$EXTENSION_CLASS_DESCRIPTOR->replaceBrokenFormFactor(I)I",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue