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