diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/actionbar/ActionBarPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/actionbar/ActionBarPatch.java
index d973918ee..16216189f 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/actionbar/ActionBarPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/actionbar/ActionBarPatch.java
@@ -7,6 +7,8 @@ import android.view.View;
import androidx.annotation.NonNull;
+import java.util.Map;
+
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.utils.VideoUtils;
@@ -40,6 +42,32 @@ public class ActionBarPatch {
);
}
+ private static final String senderView = "com.google.android.libraries.youtube.rendering.elements.sender_view";
+
+ public static boolean inAppDownloadButtonOnClick(Map mMap) {
+ if (!Settings.EXTERNAL_DOWNLOADER_ACTION_BUTTON.get()) {
+ return false;
+ }
+ if (mMap == null || mMap.isEmpty()) {
+ return false;
+ }
+ if (!mMap.containsKey(senderView)) {
+ return false;
+ }
+ if (!(getLithoViewFromMap(mMap, senderView, View.class) instanceof View view)) {
+ return false;
+ }
+ VideoUtils.launchExternalDownloader();
+ return true;
+ }
+
+ /**
+ * Rest of the implementation added by patch.
+ */
+ private static Object getLithoViewFromMap(Map mMap, Object mObject, Class> mClass) {
+ return null;
+ }
+
public static void inAppDownloadButtonOnClick(View view) {
if (!Settings.EXTERNAL_DOWNLOADER_ACTION_BUTTON.get()) {
return;
diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/ActionButtonsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/ActionButtonsFilter.java
new file mode 100644
index 000000000..8d6af2d49
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/ActionButtonsFilter.java
@@ -0,0 +1,98 @@
+package app.revanced.extension.music.patches.components;
+
+import androidx.annotation.Nullable;
+
+import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
+import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
+import app.revanced.extension.shared.patches.components.Filter;
+import app.revanced.extension.shared.patches.components.StringFilterGroup;
+import app.revanced.extension.music.settings.Settings;
+
+@SuppressWarnings("unused")
+public final class ActionButtonsFilter extends Filter {
+ private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.eml";
+
+ private final StringFilterGroup actionBarRule;
+ private final StringFilterGroup bufferFilterPathRule;
+ private final StringFilterGroup likeDislikeContainer;
+ private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList();
+ private final ByteArrayFilterGroup downloadButton;
+
+ public ActionButtonsFilter() {
+ actionBarRule = new StringFilterGroup(
+ null,
+ VIDEO_ACTION_BAR_PATH_PREFIX
+ );
+ addIdentifierCallbacks(actionBarRule);
+
+ bufferFilterPathRule = new StringFilterGroup(
+ null,
+ "|ContainerType|button.eml|"
+ );
+ likeDislikeContainer = new StringFilterGroup(
+ Settings.HIDE_ACTION_BUTTON_LIKE_DISLIKE,
+ "segmented_like_dislike_button.eml"
+ );
+ addPathCallbacks(
+ bufferFilterPathRule,
+ likeDislikeContainer
+ );
+
+ bufferButtonsGroupList.addAll(
+ new ByteArrayFilterGroup(
+ Settings.HIDE_ACTION_BUTTON_COMMENT,
+ "yt_outline_message_bubble"
+ ),
+ new ByteArrayFilterGroup(
+ Settings.HIDE_ACTION_BUTTON_ADD_TO_PLAYLIST,
+ "yt_outline_list_add"
+ ),
+ new ByteArrayFilterGroup(
+ Settings.HIDE_ACTION_BUTTON_SHARE,
+ "yt_outline_share"
+ ),
+ new ByteArrayFilterGroup(
+ Settings.HIDE_ACTION_BUTTON_RADIO,
+ "yt_outline_youtube_mix"
+ )
+ );
+ downloadButton = new ByteArrayFilterGroup(
+ Settings.HIDE_ACTION_BUTTON_DOWNLOAD,
+ "music_download_button"
+ );
+ }
+
+ private boolean isEveryFilterGroupEnabled() {
+ for (StringFilterGroup group : pathCallbacks)
+ if (!group.isEnabled()) return false;
+
+ for (ByteArrayFilterGroup group : bufferButtonsGroupList)
+ if (!group.isEnabled()) return false;
+
+ return downloadButton.isEnabled();
+ }
+
+ @Override
+ public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
+ StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
+ if (!path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX)) {
+ return false;
+ }
+ if (matchedGroup == actionBarRule && !isEveryFilterGroupEnabled()) {
+ return false;
+ }
+ if (contentType == FilterContentType.PATH) {
+ if (matchedGroup == bufferFilterPathRule) {
+ if (!bufferButtonsGroupList.check(protobufBufferArray).isFiltered()) {
+ return false;
+ }
+ } else if (matchedGroup != likeDislikeContainer) {
+ if (!downloadButton.check(protobufBufferArray).isFiltered()) {
+ return false;
+ }
+ }
+ }
+
+ return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/player/PlayerPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/player/PlayerPatch.java
index 0e31a45df..04658e6d8 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/player/PlayerPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/player/PlayerPatch.java
@@ -8,11 +8,16 @@ import android.annotation.SuppressLint;
import android.graphics.Color;
import android.view.View;
+import androidx.annotation.NonNull;
+
import java.util.Arrays;
+import java.util.Objects;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.shared.VideoType;
import app.revanced.extension.music.utils.VideoUtils;
+import app.revanced.extension.shared.utils.Logger;
+import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings({"unused"})
public class PlayerPatch {
@@ -150,9 +155,22 @@ public class PlayerPatch {
}
public static void shuffleTracks() {
+ shuffleTracks(false);
+ }
+
+ public static void shuffleTracksWithDelay() {
+ shuffleTracks(true);
+ }
+
+ private static void shuffleTracks(boolean needDelay) {
if (!Settings.ALWAYS_SHUFFLE.get())
return;
- VideoUtils.shuffleTracks();
+
+ if (needDelay) {
+ Utils.runOnMainThreadDelayed(VideoUtils::shuffleTracks, 1000);
+ } else {
+ VideoUtils.shuffleTracks();
+ }
}
public static boolean rememberRepeatState(boolean original) {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/ReturnYouTubeDislikePatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/ReturnYouTubeDislikePatch.java
index b864a9b3f..947269349 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/ReturnYouTubeDislikePatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/ReturnYouTubeDislikePatch.java
@@ -2,8 +2,10 @@ package app.revanced.extension.music.patches.utils;
import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote;
+import android.text.SpannableString;
import android.text.Spanned;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.music.returnyoutubedislike.ReturnYouTubeDislike;
@@ -18,6 +20,45 @@ import app.revanced.extension.shared.utils.Logger;
*/
@SuppressWarnings("unused")
public class ReturnYouTubeDislikePatch {
+
+ /**
+ * Injection point.
+ *
+ * Called when a litho text component is initially created,
+ * and also when a Span is later reused again (such as scrolling off/on screen).
+ *
+ * This method is sometimes called on the main thread, but it usually is called _off_ the main thread.
+ * This method can be called multiple times for the same UI element (including after dislikes was added).
+ *
+ * @param original Original char sequence was created or reused by Litho.
+ * @return The original char sequence (if nothing should change), or a replacement char sequence that contains dislikes.
+ */
+ public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
+ @NonNull CharSequence original) {
+ try {
+ if (!Settings.RYD_ENABLED.get()) {
+ return original;
+ }
+
+ String conversionContextString = conversionContext.toString();
+
+ if (!conversionContextString.contains("segmented_like_dislike_button.eml")) {
+ return original;
+ }
+ ReturnYouTubeDislike videoData = currentVideoData;
+ if (videoData == null) {
+ return original; // User enabled RYD while a video was on screen.
+ }
+ if (!(original instanceof Spanned)) {
+ original = new SpannableString(original);
+ }
+ return videoData.getDislikesSpan((Spanned) original, true);
+ } catch (Exception ex) {
+ Logger.printException(() -> "onLithoTextLoaded failure", ex);
+ }
+ return original;
+ }
+
/**
* RYD data for the current video on screen.
*/
@@ -49,7 +90,7 @@ public class ReturnYouTubeDislikePatch {
if (videoData == null) {
return original; // User enabled RYD while a video was on screen.
}
- return videoData.getDislikesSpan(original);
+ return videoData.getDislikesSpan(original, false);
} catch (Exception ex) {
Logger.printException(() -> "onSpannedCreated failure", ex);
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/shared/src/main/java/app/revanced/extension/music/returnyoutubedislike/ReturnYouTubeDislike.java
index bec27d1b3..69389d1a7 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/music/returnyoutubedislike/ReturnYouTubeDislike.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/returnyoutubedislike/ReturnYouTubeDislike.java
@@ -74,8 +74,6 @@ public class ReturnYouTubeDislike {
*/
private static final char MIDDLE_SEPARATOR_CHARACTER = '◎'; // 'bullseye'
- private static final int SEPARATOR_COLOR = 872415231;
-
/**
* Cached lookup of all video ids.
*/
@@ -99,19 +97,11 @@ public class ReturnYouTubeDislike {
@GuardedBy("ReturnYouTubeDislike.class")
private static NumberFormat dislikePercentageFormatter;
- public static final Rect leftSeparatorBounds;
- private static final Rect middleSeparatorBounds;
+ public static Rect leftSeparatorBounds;
+ private static Rect middleSeparatorBounds;
+
static {
- DisplayMetrics dp = Objects.requireNonNull(Utils.getContext()).getResources().getDisplayMetrics();
-
- leftSeparatorBounds = new Rect(0, 0,
- (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp),
- (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, dp));
- final int middleSeparatorSize =
- (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
- middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
-
ReturnYouTubeDislikeApi.toastOnConnectionError = Settings.RYD_TOAST_ON_CONNECTION_ERROR.get();
}
@@ -150,9 +140,26 @@ public class ReturnYouTubeDislike {
@GuardedBy("this")
private SpannableString replacementLikeDislikeSpan;
+
+ /**
+ * Color of the left and middle separator, based on the color of the right separator.
+ * It's unknown where YT gets the color from, and the values here are approximated by hand.
+ * Ideally, this would be the actual color YT uses at runtime.
+ *
+ * Older versions before the 'Me' library tab use a slightly different color.
+ * If spoofing was previously used and is now turned off,
+ * or an old version was recently upgraded then the old colors are sometimes still used.
+ */
+ private static int getSeparatorColor(boolean isLithoText) {
+ return isLithoText
+ ? 0x29AAAAAA
+ : 0x33FFFFFF;
+ }
+
@NonNull
private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable,
- @NonNull RYDVoteData voteData) {
+ @NonNull RYDVoteData voteData,
+ boolean isLithoText) {
CharSequence oldLikes = oldSpannable;
// YouTube creators can hide the like count on a video,
@@ -176,14 +183,27 @@ public class ReturnYouTubeDislike {
}
}
- SpannableStringBuilder builder = new SpannableStringBuilder("\u2009\u2009");
+ SpannableStringBuilder builder = new SpannableStringBuilder("\u2009");
+ if (!isLithoText) {
+ builder.append("\u2009");
+ }
final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get();
+ if (middleSeparatorBounds == null) {
+ final DisplayMetrics dp = Utils.getResources().getDisplayMetrics();
+ leftSeparatorBounds = new Rect(0, 0,
+ (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp),
+ (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, isLithoText ? 23 : 25, dp));
+ final int middleSeparatorSize =
+ (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
+ middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
+ }
+
if (!compactLayout) {
String leftSeparatorString = "\u200E "; // u200E = left to right character
Spannable leftSeparatorSpan = new SpannableString(leftSeparatorString);
ShapeDrawable shapeDrawable = new ShapeDrawable(new RectShape());
- shapeDrawable.getPaint().setColor(SEPARATOR_COLOR);
+ shapeDrawable.getPaint().setColor(getSeparatorColor(isLithoText));
shapeDrawable.setBounds(leftSeparatorBounds);
leftSeparatorSpan.setSpan(new VerticallyCenteredImageSpan(shapeDrawable), 1, 2,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE); // drawable cannot overwrite RTL or LTR character
@@ -200,7 +220,7 @@ public class ReturnYouTubeDislike {
final int shapeInsertionIndex = middleSeparatorString.length() / 2;
Spannable middleSeparatorSpan = new SpannableString(middleSeparatorString);
ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
- shapeDrawable.getPaint().setColor(SEPARATOR_COLOR);
+ shapeDrawable.getPaint().setColor(getSeparatorColor(isLithoText));
shapeDrawable.setBounds(middleSeparatorBounds);
// Use original text width if using Rolling Number,
// to ensure the replacement styled span has the same width as the measured String,
@@ -416,12 +436,12 @@ public class ReturnYouTubeDislike {
* @return the replacement span containing dislikes, or the original span if RYD is not available.
*/
@NonNull
- public synchronized Spanned getDislikesSpan(@NonNull Spanned original) {
- return waitForFetchAndUpdateReplacementSpan(original);
+ public synchronized Spanned getDislikesSpan(@NonNull Spanned original, boolean isLithoText) {
+ return waitForFetchAndUpdateReplacementSpan(original, isLithoText);
}
@NonNull
- private Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned original) {
+ private Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned original, boolean isLithoText) {
try {
RYDVoteData votingData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH);
if (votingData == null) {
@@ -456,7 +476,7 @@ public class ReturnYouTubeDislike {
votingData.updateUsingVote(userVote);
}
originalDislikeSpan = original;
- replacementLikeDislikeSpan = createDislikeSpan(original, votingData);
+ replacementLikeDislikeSpan = createDislikeSpan(original, votingData, isLithoText);
Logger.printDebug(() -> "Replaced: '" + originalDislikeSpan + "' with: '"
+ replacementLikeDislikeSpan + "'" + " using video: " + videoId);
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt
index 3fcca843d..0f5e0ad6d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt
@@ -1,6 +1,7 @@
package app.revanced.patches.music.actionbar.components
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
@@ -8,7 +9,11 @@ import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.extension.Constants.ACTIONBAR_CLASS_DESCRIPTOR
+import app.revanced.patches.music.utils.extension.Constants.COMPONENTS_PATH
import app.revanced.patches.music.utils.patch.PatchList.HIDE_ACTION_BAR_COMPONENTS
+import app.revanced.patches.music.utils.playservice.is_7_17_or_greater
+import app.revanced.patches.music.utils.playservice.is_7_25_or_greater
+import app.revanced.patches.music.utils.playservice.versionCheckPatch
import app.revanced.patches.music.utils.resourceid.likeDislikeContainer
import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.music.utils.settings.CategoryType
@@ -17,6 +22,9 @@ import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.patches.music.video.information.videoInformationPatch
+import app.revanced.patches.shared.litho.addLithoFilter
+import app.revanced.patches.shared.litho.lithoFilterPatch
+import app.revanced.util.findMethodOrThrow
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference
@@ -30,6 +38,9 @@ import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import kotlin.math.min
+private const val FILTER_CLASS_DESCRIPTOR =
+ "$COMPONENTS_PATH/ActionButtonsFilter;"
+
@Suppress("unused")
val actionBarComponentsPatch = bytecodePatch(
HIDE_ACTION_BAR_COMPONENTS.title,
@@ -39,94 +50,127 @@ val actionBarComponentsPatch = bytecodePatch(
dependsOn(
settingsPatch,
+ lithoFilterPatch,
sharedResourceIdPatch,
videoInformationPatch,
+ versionCheckPatch,
)
execute {
+ if (is_7_17_or_greater) {
+ browseSectionListReloadEndpointFingerprint.methodOrThrow().apply {
+ val targetIndex = indexOfGetLithoViewFromMapInstruction(this)
+ val targetReference = getInstruction(targetIndex).reference
- actionBarComponentFingerprint.matchOrThrow().let {
- it.method.apply {
- // hook download button
- val addViewIndex = indexOfFirstInstructionOrThrow {
- opcode == Opcode.INVOKE_VIRTUAL &&
- getReference()?.name == "addView"
- }
- val addViewRegister =
- getInstruction(addViewIndex).registerD
-
- addInstruction(
- addViewIndex + 1,
- "invoke-static {v$addViewRegister}, $ACTIONBAR_CLASS_DESCRIPTOR->inAppDownloadButtonOnClick(Landroid/view/View;)V"
+ findMethodOrThrow(ACTIONBAR_CLASS_DESCRIPTOR) {
+ name == "getLithoViewFromMap"
+ }.addInstructions(
+ 0, """
+ invoke-static {p0, p1, p2}, $targetReference
+ move-result-object p1
+ return-object p1
+ """
)
+ }
- // hide action button label
- val noLabelIndex = indexOfFirstInstructionOrThrow {
- val reference = (this as? ReferenceInstruction)?.reference.toString()
- opcode == Opcode.INVOKE_DIRECT &&
- reference.endsWith("(Landroid/content/Context;)V") &&
- !reference.contains("Lcom/google/android/libraries/youtube/common/ui/YouTubeButton;")
- } - 2
- val replaceIndex = indexOfFirstInstructionOrThrow {
- opcode == Opcode.INVOKE_DIRECT &&
- (this as? ReferenceInstruction)?.reference.toString()
- .endsWith("Lcom/google/android/libraries/youtube/common/ui/YouTubeButton;->(Landroid/content/Context;)V")
- } - 2
- val replaceInstruction = getInstruction(replaceIndex)
- val replaceReference = getInstruction(replaceIndex).reference
-
+ offlineVideoEndpointFingerprint.methodOrThrow().apply {
addInstructionsWithLabels(
- replaceIndex + 1, """
- invoke-static {}, $ACTIONBAR_CLASS_DESCRIPTOR->hideActionBarLabel()Z
- move-result v${replaceInstruction.registerA}
- if-nez v${replaceInstruction.registerA}, :hidden
- iget-object v${replaceInstruction.registerA}, v${replaceInstruction.registerB}, $replaceReference
- """, ExternalLabel("hidden", getInstruction(noLabelIndex))
+ 0, """
+ invoke-static {p2}, $ACTIONBAR_CLASS_DESCRIPTOR->inAppDownloadButtonOnClick(Ljava/util/Map;)Z
+ move-result v0
+ if-eqz v0, :ignore
+ return-void
+ """, ExternalLabel("ignore", getInstruction(0))
)
- removeInstruction(replaceIndex)
+ }
- // hide action button
- val hasNextIndex = indexOfFirstInstructionOrThrow {
- opcode == Opcode.INVOKE_INTERFACE &&
- getReference()?.name == "hasNext"
+ addLithoFilter(FILTER_CLASS_DESCRIPTOR)
+ }
+
+ if (!is_7_25_or_greater) {
+ actionBarComponentFingerprint.matchOrThrow().let {
+ it.method.apply {
+ // hook download button
+ val addViewIndex = indexOfFirstInstructionOrThrow {
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ getReference()?.name == "addView"
+ }
+ val addViewRegister =
+ getInstruction(addViewIndex).registerD
+
+ addInstruction(
+ addViewIndex + 1,
+ "invoke-static {v$addViewRegister}, $ACTIONBAR_CLASS_DESCRIPTOR->inAppDownloadButtonOnClick(Landroid/view/View;)V"
+ )
+
+ // hide action button label
+ val noLabelIndex = indexOfFirstInstructionOrThrow {
+ val reference = (this as? ReferenceInstruction)?.reference.toString()
+ opcode == Opcode.INVOKE_DIRECT &&
+ reference.endsWith("(Landroid/content/Context;)V") &&
+ !reference.contains("Lcom/google/android/libraries/youtube/common/ui/YouTubeButton;")
+ } - 2
+ val replaceIndex = indexOfFirstInstructionOrThrow {
+ opcode == Opcode.INVOKE_DIRECT &&
+ (this as? ReferenceInstruction)?.reference.toString()
+ .endsWith("Lcom/google/android/libraries/youtube/common/ui/YouTubeButton;->(Landroid/content/Context;)V")
+ } - 2
+ val replaceInstruction = getInstruction(replaceIndex)
+ val replaceReference = getInstruction(replaceIndex).reference
+
+ addInstructionsWithLabels(
+ replaceIndex + 1, """
+ invoke-static {}, $ACTIONBAR_CLASS_DESCRIPTOR->hideActionBarLabel()Z
+ move-result v${replaceInstruction.registerA}
+ if-nez v${replaceInstruction.registerA}, :hidden
+ iget-object v${replaceInstruction.registerA}, v${replaceInstruction.registerB}, $replaceReference
+ """, ExternalLabel("hidden", getInstruction(noLabelIndex))
+ )
+ removeInstruction(replaceIndex)
+
+ // hide action button
+ val hasNextIndex = indexOfFirstInstructionOrThrow {
+ opcode == Opcode.INVOKE_INTERFACE &&
+ getReference()?.name == "hasNext"
+ }
+ val freeRegister = min(implementation!!.registerCount - parameters.size - 2, 15)
+
+ val spannedIndex = indexOfFirstInstructionOrThrow {
+ getReference()?.returnType == "Landroid/text/Spanned;"
+ }
+ val spannedRegister =
+ getInstruction(spannedIndex).registerC
+ val spannedReference = getInstruction(spannedIndex).reference
+
+ addInstructionsWithLabels(
+ spannedIndex + 1, """
+ invoke-static {}, $ACTIONBAR_CLASS_DESCRIPTOR->hideActionButton()Z
+ move-result v$freeRegister
+ if-nez v$freeRegister, :hidden
+ invoke-static {v$spannedRegister}, $spannedReference
+ """, ExternalLabel("hidden", getInstruction(hasNextIndex))
+ )
+ removeInstruction(spannedIndex)
+
+ // set action button identifier
+ val buttonTypeDownloadIndex = it.patternMatch!!.startIndex + 1
+ val buttonTypeDownloadRegister =
+ getInstruction(buttonTypeDownloadIndex).registerA
+
+ val buttonTypeIndex = it.patternMatch!!.endIndex - 1
+ val buttonTypeRegister =
+ getInstruction(buttonTypeIndex).registerA
+
+ addInstruction(
+ buttonTypeIndex + 2,
+ "invoke-static {v$buttonTypeRegister}, $ACTIONBAR_CLASS_DESCRIPTOR->setButtonType(Ljava/lang/Object;)V"
+ )
+
+ addInstruction(
+ buttonTypeDownloadIndex,
+ "invoke-static {v$buttonTypeDownloadRegister}, $ACTIONBAR_CLASS_DESCRIPTOR->setButtonTypeDownload(I)V"
+ )
}
- val freeRegister = min(implementation!!.registerCount - parameters.size - 2, 15)
-
- val spannedIndex = indexOfFirstInstructionOrThrow {
- getReference()?.returnType == "Landroid/text/Spanned;"
- }
- val spannedRegister =
- getInstruction(spannedIndex).registerC
- val spannedReference = getInstruction(spannedIndex).reference
-
- addInstructionsWithLabels(
- spannedIndex + 1, """
- invoke-static {}, $ACTIONBAR_CLASS_DESCRIPTOR->hideActionButton()Z
- move-result v$freeRegister
- if-nez v$freeRegister, :hidden
- invoke-static {v$spannedRegister}, $spannedReference
- """, ExternalLabel("hidden", getInstruction(hasNextIndex))
- )
- removeInstruction(spannedIndex)
-
- // set action button identifier
- val buttonTypeDownloadIndex = it.patternMatch!!.startIndex + 1
- val buttonTypeDownloadRegister =
- getInstruction(buttonTypeDownloadIndex).registerA
-
- val buttonTypeIndex = it.patternMatch!!.endIndex - 1
- val buttonTypeRegister =
- getInstruction(buttonTypeIndex).registerA
-
- addInstruction(
- buttonTypeIndex + 2,
- "invoke-static {v$buttonTypeRegister}, $ACTIONBAR_CLASS_DESCRIPTOR->setButtonType(Ljava/lang/Object;)V"
- )
-
- addInstruction(
- buttonTypeDownloadIndex,
- "invoke-static {v$buttonTypeDownloadRegister}, $ACTIONBAR_CLASS_DESCRIPTOR->setButtonTypeDownload(I)V"
- )
}
}
@@ -171,11 +215,13 @@ val actionBarComponentsPatch = bytecodePatch(
"revanced_hide_action_button_radio",
"false"
)
- addSwitchPreference(
- CategoryType.ACTION_BAR,
- "revanced_hide_action_button_label",
- "false"
- )
+ if (!is_7_25_or_greater) {
+ addSwitchPreference(
+ CategoryType.ACTION_BAR,
+ "revanced_hide_action_button_label",
+ "false"
+ )
+ }
addSwitchPreference(
CategoryType.ACTION_BAR,
"revanced_external_downloader_action",
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/Fingerprints.kt
index e7e9eb934..9dbd61234 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/Fingerprints.kt
@@ -2,9 +2,13 @@ package app.revanced.patches.music.actionbar.components
import app.revanced.patches.music.utils.resourceid.likeDislikeContainer
import app.revanced.util.fingerprint.legacyFingerprint
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.or
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.reference.MethodReference
internal val actionBarComponentFingerprint = legacyFingerprint(
name = "actionBarComponentFingerprint",
@@ -22,9 +26,40 @@ internal val actionBarComponentFingerprint = legacyFingerprint(
literals = listOf(99180L),
)
+internal val browseSectionListReloadEndpointFingerprint = legacyFingerprint(
+ name = "browseSectionListReloadEndpointFingerprint",
+ returnType = "V",
+ parameters = listOf("L", "Ljava/util/Map;"),
+ strings = listOf("request_mutator"),
+ customFingerprint = { method, _ ->
+ indexOfGetLithoViewFromMapInstruction(method) >= 0
+ }
+)
+
+internal fun indexOfGetLithoViewFromMapInstruction(method: Method) =
+ method.indexOfFirstInstruction {
+ val reference = getReference()
+ opcode == Opcode.INVOKE_STATIC &&
+ reference?.returnType == "Ljava/lang/Object;" &&
+ reference.parameterTypes ==
+ listOf(
+ "Ljava/util/Map;",
+ "Ljava/lang/Object;",
+ "Ljava/lang/Class;"
+ )
+ }
+
internal val likeDislikeContainerFingerprint = legacyFingerprint(
name = "likeDislikeContainerFingerprint",
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
literals = listOf(likeDislikeContainer)
)
+
+internal val offlineVideoEndpointFingerprint = legacyFingerprint(
+ name = "offlineVideoEndpointFingerprint",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ returnType = "V",
+ parameters = listOf("L", "Ljava/util/Map;"),
+ strings = listOf("Object is not an offlineable video: %s")
+)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/redirection/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/redirection/Fingerprints.kt
index b7bb4d3a3..3b66075ad 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/general/redirection/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/general/redirection/Fingerprints.kt
@@ -1,5 +1,6 @@
package app.revanced.patches.music.general.redirection
+import app.revanced.util.containsLiteralInstruction
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags
@@ -9,9 +10,9 @@ internal val dislikeButtonOnClickListenerFingerprint = legacyFingerprint(
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf("Landroid/view/View;"),
- literals = listOf(53465L),
customFingerprint = { method, _ ->
- method.name == "onClick"
+ method.name == "onClick" &&
+ (method.containsLiteralInstruction(53465L) || method.containsLiteralInstruction(98173L))
}
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch.kt
index 118dd8735..d15a5a306 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch.kt
@@ -4,11 +4,12 @@ import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patches.music.general.oldstylelibraryshelf.oldStyleLibraryShelfPatch
-import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
+import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME
import app.revanced.patches.music.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR
import app.revanced.patches.music.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR
import app.revanced.patches.music.utils.patch.PatchList.SPOOF_APP_VERSION
-import app.revanced.patches.music.utils.playservice.is_7_18_or_greater
+import app.revanced.patches.music.utils.playservice.is_7_17_or_greater
+import app.revanced.patches.music.utils.playservice.is_7_25_or_greater
import app.revanced.patches.music.utils.playservice.versionCheckPatch
import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
@@ -30,7 +31,10 @@ private val spoofAppVersionBytecodePatch = bytecodePatch(
)
execute {
- if (is_7_18_or_greater) {
+ if (is_7_25_or_greater) {
+ return@execute
+ }
+ if (is_7_17_or_greater) {
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
name == "SpoofAppVersionDefaultString"
}.replaceInstruction(
@@ -54,7 +58,16 @@ val spoofAppVersionPatch = resourcePatch(
SPOOF_APP_VERSION.title,
SPOOF_APP_VERSION.summary,
) {
- compatibleWith(COMPATIBLE_PACKAGE)
+ compatibleWith(
+ YOUTUBE_MUSIC_PACKAGE_NAME(
+ "6.20.51",
+ "6.29.59",
+ "6.42.55",
+ "6.51.53",
+ "7.06.54",
+ "7.16.53",
+ ),
+ )
dependsOn(
spoofAppVersionBytecodePatch,
@@ -64,7 +77,11 @@ val spoofAppVersionPatch = resourcePatch(
)
execute {
- if (is_7_18_or_greater) {
+ if (is_7_25_or_greater) {
+ println("WARNING: \"${SPOOF_APP_VERSION.title}\" is not supported in this version. Use YouTube Music 7.24.51 or earlier.")
+ return@execute
+ }
+ if (is_7_17_or_greater) {
appendAppVersion("7.16.53")
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt
index 79779e4d6..3268a9f7f 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt
@@ -49,7 +49,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
execute {
if (!is_7_06_or_greater) {
- println("WARNING: This patch is not supported in this version. Use YouTube Music 7.06.54 or later.")
+ println("WARNING: \"${DISABLE_CAIRO_SPLASH_ANIMATION.title}\" is not supported in this version. Use YouTube Music 7.06.54 or later.")
return@execute
} else if (!is_7_20_or_greater) {
cairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall(
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/player/components/PlayerComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/player/components/PlayerComponentsPatch.kt
index a599a955c..b09e21d01 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/player/components/PlayerComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/player/components/PlayerComponentsPatch.kt
@@ -23,6 +23,7 @@ import app.revanced.patches.music.utils.pendingIntentReceiverFingerprint
import app.revanced.patches.music.utils.playservice.is_6_27_or_greater
import app.revanced.patches.music.utils.playservice.is_6_42_or_greater
import app.revanced.patches.music.utils.playservice.is_7_18_or_greater
+import app.revanced.patches.music.utils.playservice.is_7_25_or_greater
import app.revanced.patches.music.utils.playservice.versionCheckPatch
import app.revanced.patches.music.utils.resourceid.colorGrey
import app.revanced.patches.music.utils.resourceid.darkBackground
@@ -821,8 +822,8 @@ val playerComponentsPatch = bytecodePatch(
// region patch for remember repeat state
- repeatTrackFingerprint.matchOrThrow().let {
- it.method.apply {
+ val (repeatTrackMethod, repeatTrackIndex) = repeatTrackFingerprint.matchOrThrow().let {
+ with (it.method) {
val targetIndex = it.patternMatch!!.endIndex
val targetRegister = getInstruction(targetIndex).registerA
@@ -832,6 +833,7 @@ val playerComponentsPatch = bytecodePatch(
move-result v$targetRegister
"""
)
+ Pair(this, targetIndex)
}
}
@@ -919,6 +921,13 @@ val playerComponentsPatch = bytecodePatch(
"invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->shuffleTracks()V"
)
+ if (is_7_25_or_greater) {
+ repeatTrackMethod.addInstruction(
+ repeatTrackIndex,
+ "invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->shuffleTracksWithDelay()V"
+ )
+ }
+
addSwitchPreference(
CategoryType.PLAYER,
"revanced_remember_shuffle_state",
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt
index 83606bd71..b5b59cf50 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt
@@ -13,7 +13,8 @@ internal object Constants {
"6.29.59", // This is the latest version that supports the 'Restore old player layout' setting.
"6.42.55", // This is the latest version that supports Android 7.0
"6.51.53", // This is the latest version of YouTube Music 6.xx.xx
- "7.16.53", // This is the latest version supported by the RVX patch.
+ "7.16.53", // This is the latest version that supports the 'Spoof app version' patch.
+ "7.25.52", // This is the latest version supported by the RVX patch.
)
)
}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/playservice/VersionCheckPatch.kt
index dfb6de9f8..372251b2b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/playservice/VersionCheckPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/playservice/VersionCheckPatch.kt
@@ -15,12 +15,16 @@ var is_7_06_or_greater = false
private set
var is_7_13_or_greater = false
private set
+var is_7_17_or_greater = false
+ private set
var is_7_18_or_greater = false
private set
var is_7_20_or_greater = false
private set
var is_7_23_or_greater = false
private set
+var is_7_25_or_greater = false
+ private set
val versionCheckPatch = resourcePatch(
description = "versionCheckPatch",
@@ -41,8 +45,10 @@ val versionCheckPatch = resourcePatch(
is_6_42_or_greater = 240999000 <= playStoreServicesVersion
is_7_06_or_greater = 242499000 <= playStoreServicesVersion
is_7_13_or_greater = 243199000 <= playStoreServicesVersion
+ is_7_17_or_greater = 243530000 <= playStoreServicesVersion
is_7_18_or_greater = 243699000 <= playStoreServicesVersion
is_7_20_or_greater = 243899000 <= playStoreServicesVersion
is_7_23_or_greater = 244199000 <= playStoreServicesVersion
+ is_7_25_or_greater = 244399000 <= playStoreServicesVersion
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikePatch.kt
index 6daf407ae..eb4a694e2 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikePatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikePatch.kt
@@ -7,6 +7,8 @@ import app.revanced.patcher.patch.resourcePatch
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.extension.Constants.UTILS_PATH
import app.revanced.patches.music.utils.patch.PatchList.RETURN_YOUTUBE_DISLIKE
+import app.revanced.patches.music.utils.playservice.is_7_17_or_greater
+import app.revanced.patches.music.utils.playservice.is_7_25_or_greater
import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.ResourceUtils.PREFERENCE_CATEGORY_TAG_NAME
@@ -20,6 +22,8 @@ import app.revanced.patches.music.video.information.videoInformationPatch
import app.revanced.patches.shared.dislikeFingerprint
import app.revanced.patches.shared.likeFingerprint
import app.revanced.patches.shared.removeLikeFingerprint
+import app.revanced.patches.shared.textcomponent.hookSpannableString
+import app.revanced.patches.shared.textcomponent.textComponentPatch
import app.revanced.util.adoptChild
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.indexOfFirstInstructionOrThrow
@@ -37,7 +41,8 @@ private val returnYouTubeDislikeBytecodePatch = bytecodePatch(
dependsOn(
settingsPatch,
sharedResourceIdPatch,
- videoInformationPatch
+ videoInformationPatch,
+ textComponentPatch,
)
execute {
@@ -56,25 +61,31 @@ private val returnYouTubeDislikeBytecodePatch = bytecodePatch(
)
}
+ if (!is_7_25_or_greater) {
+ textComponentFingerprint.methodOrThrow().apply {
+ val insertIndex = indexOfFirstInstructionOrThrow {
+ opcode == Opcode.INVOKE_STATIC
+ && (this as ReferenceInstruction).reference.toString()
+ .endsWith("Ljava/lang/CharSequence;")
+ } + 2
+ val insertRegister =
+ getInstruction(insertIndex - 1).registerA
- textComponentFingerprint.methodOrThrow().apply {
- val insertIndex = indexOfFirstInstructionOrThrow {
- opcode == Opcode.INVOKE_STATIC
- && (this as ReferenceInstruction).reference.toString()
- .endsWith("Ljava/lang/CharSequence;")
- } + 2
- val insertRegister =
- getInstruction(insertIndex - 1).registerA
+ addInstructions(
+ insertIndex, """
+ invoke-static {v$insertRegister}, $EXTENSION_CLASS_DESCRIPTOR->onSpannedCreated(Landroid/text/Spanned;)Landroid/text/Spanned;
+ move-result-object v$insertRegister
+ """
+ )
+ }
+ }
- addInstructions(
- insertIndex, """
- invoke-static {v$insertRegister}, $EXTENSION_CLASS_DESCRIPTOR->onSpannedCreated(Landroid/text/Spanned;)Landroid/text/Spanned;
- move-result-object v$insertRegister
- """
- )
+ if (is_7_17_or_greater) {
+ hookSpannableString(EXTENSION_CLASS_DESCRIPTOR, "onLithoTextLoaded")
}
videoIdHook("$EXTENSION_CLASS_DESCRIPTOR->newVideoLoaded(Ljava/lang/String;)V")
+
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/Fingerprints.kt
index ba2b5bd6f..fd4281524 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/Fingerprints.kt
@@ -21,7 +21,6 @@ internal val musicPlaybackControlsTimeBarOnMeasureFingerprint = legacyFingerprin
name = "musicPlaybackControlsTimeBarOnMeasureFingerprint",
returnType = "V",
opcodes = listOf(
- Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.INVOKE_VIRTUAL,
Opcode.RETURN_VOID
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/SponsorBlockPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/SponsorBlockPatch.kt
index 8eb98a1e6..e48153c4b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/SponsorBlockPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/SponsorBlockPatch.kt
@@ -111,7 +111,10 @@ private val sponsorBlockBytecodePatch = bytecodePatch(
rectangleFieldName =
musicPlaybackControlsTimeBarOnMeasureFingerprint.matchOrThrow().let {
with(it.method) {
- val rectangleIndex = it.patternMatch!!.startIndex
+ val rectangleIndex = indexOfFirstInstructionReversedOrThrow(it.patternMatch!!.endIndex) {
+ opcode == Opcode.IGET_OBJECT &&
+ getReference()?.type == "Landroid/graphics/Rect;"
+ }
val rectangleReference =
getInstruction(rectangleIndex).reference
(rectangleReference as FieldReference).name