From 2f59df529dd3abf0a6f9a28d0258fd02990c16be Mon Sep 17 00:00:00 2001
From: ILoveOpenSouceApplications
<117499019+ILoveOpenSourceApplications@users.noreply.github.com>
Date: Fri, 24 Apr 2026 16:13:30 +0530
Subject: [PATCH 1/6] feat(YouTube): Introduce Litho text component and span
filtering framework
---
.../youtube/patches/spans/Filter.java | 69 ++++++
.../youtube/patches/spans/FilterGroup.java | 96 ++++++++
.../patches/spans/FilterGroupList.java | 82 +++++++
.../patches/spans/InclusiveSpanType.java | 224 ++++++++++++++++++
.../youtube/patches/spans/SpanType.java | 28 +++
.../extension/youtube/settings/Settings.java | 1 +
.../youtube/misc/spans/Fingerprints.kt | 38 +++
.../youtube/misc/spans/InclusiveSpanPatch.kt | 166 +++++++++++++
.../misc/textcomponent/Fingerprints.kt | 33 +++
.../misc/textcomponent/TextComponentPatch.kt | 131 ++++++++++
.../patches/youtube/shared/Fingerprints.kt | 26 ++
11 files changed, 894 insertions(+)
create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java
create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java
create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java
create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java
create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanType.java
create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/misc/spans/Fingerprints.kt
create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/misc/spans/InclusiveSpanPatch.kt
create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/misc/textcomponent/Fingerprints.kt
create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/misc/textcomponent/TextComponentPatch.kt
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java
new file mode 100644
index 000000000..4cbfd19e6
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * Original hard forked code:
+ * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+package app.morphe.extension.youtube.patches.spans;
+
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.SpannableString;
+import android.text.style.ImageSpan;
+import android.text.style.RelativeSizeSpan;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Filters litho based components.
+ *
+ * All callbacks must be registered before the constructor completes.
+ */
+abstract class Filter {
+ private static final RelativeSizeSpan relativeSizeSpanDummy = new RelativeSizeSpan(0f);
+ private static final Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT);
+ private static final ImageSpan imageSpanDummy = new ImageSpan(transparentDrawable);
+
+ /**
+ * Path callbacks. Do not add to this instance,
+ * and instead use {@link #addCallbacks(StringFilterGroup...)}.
+ */
+ protected final List callbacks = new ArrayList<>();
+
+ /**
+ * Adds callbacks to {@link #skip(String, SpannableString, Object, int, int, int, boolean, SpanType, StringFilterGroup)}
+ * if any of the groups are found.
+ */
+ protected final void addCallbacks(StringFilterGroup... groups) {
+ callbacks.addAll(Arrays.asList(groups));
+ }
+
+ protected final void hideSpan(SpannableString spannableString, int start, int end, int flags) {
+ spannableString.setSpan(relativeSizeSpanDummy, start, end, flags);
+ }
+
+ protected final void hideImageSpan(SpannableString spannableString, int start, int end, int flags) {
+ spannableString.setSpan(imageSpanDummy, start, end, flags);
+ }
+
+ /**
+ * Called after an enabled filter has been matched.
+ * Default implementation is to always filter the matched component and log the action.
+ * Subclasses can perform additional or different checks if needed.
+ *
+ * Method is called off the main thread.
+ *
+ * @param matchedGroup The actual filter that matched.
+ */
+ boolean skip(String conversionContext, SpannableString spannableString, Object span, int start, int end,
+ int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) {
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java
new file mode 100644
index 000000000..1df76925d
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * Original hard forked code:
+ * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+package app.morphe.extension.youtube.patches.spans;
+
+import androidx.annotation.NonNull;
+
+import app.morphe.extension.shared.settings.BooleanSetting;
+
+abstract class FilterGroup {
+ final static class FilterGroupResult {
+ private BooleanSetting setting;
+ private int matchedIndex;
+
+ FilterGroupResult() {
+ this(null, -1);
+ }
+
+ FilterGroupResult(BooleanSetting setting, int matchedIndex) {
+ setValues(setting, matchedIndex);
+ }
+
+ public void setValues(BooleanSetting setting, int matchedIndex) {
+ this.setting = setting;
+ this.matchedIndex = matchedIndex;
+ }
+
+ public BooleanSetting getSetting() {
+ return setting;
+ }
+
+ public boolean isFiltered() {
+ return matchedIndex >= 0;
+ }
+ }
+
+ protected final BooleanSetting setting;
+ protected final T[] filters;
+
+ @SafeVarargs
+ public FilterGroup(final BooleanSetting setting, final T... filters) {
+ this.setting = setting;
+ this.filters = filters;
+ if (filters.length == 0) {
+ throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
+ }
+ }
+
+ public boolean isEnabled() {
+ return setting == null || setting.get();
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ public boolean includeInSearch() {
+ return isEnabled() || !setting.rebootApp;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
+ }
+
+ public abstract FilterGroupResult check(final T stack);
+}
+
+class StringFilterGroup extends FilterGroup {
+
+ public StringFilterGroup(final BooleanSetting setting, final String... filters) {
+ super(setting, filters);
+ }
+
+ @Override
+ public FilterGroupResult check(final String string) {
+ int matchedIndex = -1;
+ if (isEnabled()) {
+ for (String pattern : filters) {
+ if (!string.isEmpty()) {
+ final int indexOf = string.indexOf(pattern);
+ if (indexOf >= 0) {
+ matchedIndex = indexOf;
+ break;
+ }
+ }
+ }
+ }
+ return new FilterGroupResult(setting, matchedIndex);
+ }
+}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java
new file mode 100644
index 000000000..09c84ba2a
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * Original hard forked code:
+ * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+package app.morphe.extension.youtube.patches.spans;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+
+import app.morphe.extension.shared.StringTrieSearch;
+import app.morphe.extension.shared.TrieSearch;
+
+abstract class FilterGroupList> implements Iterable {
+
+ private final List filterGroups = new ArrayList<>();
+ private final TrieSearch search = createSearchGraph();
+
+ @SafeVarargs
+ protected final void addAll(final T... groups) {
+ filterGroups.addAll(Arrays.asList(groups));
+
+ for (T group : groups) {
+ if (!group.includeInSearch()) {
+ continue;
+ }
+ for (V pattern : group.filters) {
+ search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
+ if (group.isEnabled()) {
+ FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
+ result.setValues(group.setting, matchedStartIndex);
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+ }
+
+ @NonNull
+ @Override
+ public Iterator iterator() {
+ return filterGroups.iterator();
+ }
+
+ @Override
+ public void forEach(@NonNull Consumer super T> action) {
+ filterGroups.forEach(action);
+ }
+
+ @NonNull
+ @Override
+ public Spliterator spliterator() {
+ return filterGroups.spliterator();
+ }
+
+ protected FilterGroup.FilterGroupResult check(V stack) {
+ FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
+ search.matches(stack, result);
+ return result;
+ }
+
+ protected abstract TrieSearch createSearchGraph();
+}
+
+final class StringFilterGroupList extends FilterGroupList {
+ @Override
+ protected StringTrieSearch createSearchGraph() {
+ return new StringTrieSearch();
+ }
+}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java
new file mode 100644
index 000000000..c7a26ba8c
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * Original hard forked code:
+ * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+package app.morphe.extension.youtube.patches.spans;
+
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.CharacterStyle;
+import android.text.style.ClickableSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.text.style.LineHeightSpan;
+import android.text.style.TypefaceSpan;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+import app.morphe.extension.shared.Logger;
+import app.morphe.extension.shared.StringTrieSearch;
+import app.morphe.extension.youtube.settings.Settings;
+
+/**
+ * Placeholder for actual filters.
+ */
+final class DummyFilter extends Filter {
+}
+
+@SuppressWarnings("unused")
+public final class InclusiveSpanType {
+
+ /**
+ * Simple wrapper to pass the litho parameters through the prefix search.
+ */
+ private static final class LithoFilterParameters {
+ final String conversionContext;
+ final SpannableString spannableString;
+ final Object span;
+ final int start;
+ final int end;
+ final int flags;
+ final String originalString;
+ final int originalLength;
+ final SpanType spanType;
+ final boolean isWord;
+
+ public LithoFilterParameters(String conversionContext, SpannableString spannableString,
+ Object span, int start, int end, int flags) {
+ this.conversionContext = conversionContext;
+ this.spannableString = spannableString;
+ this.span = span;
+ this.start = start;
+ this.end = end;
+ this.flags = flags;
+ this.originalString = spannableString.toString();
+ this.originalLength = spannableString.length();
+ this.spanType = getSpanType(span);
+ this.isWord = !(start == 0 && end == originalLength);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("CharSequence:'")
+ .append(originalString)
+ .append("'\nSpanType:'")
+ .append(getSpanType(spanType, span))
+ .append("'\nLength:'")
+ .append(originalLength)
+ .append("'\nStart:'")
+ .append(start)
+ .append("'\nEnd:'")
+ .append(end)
+ .append("'\nisWord:'")
+ .append(isWord)
+ .append("'");
+ if (isWord) {
+ builder.append("\nWord:'")
+ .append(originalString.substring(start, end))
+ .append("'");
+ }
+ return builder.toString();
+ }
+ }
+
+ private static SpanType getSpanType(Object span) {
+ if (span instanceof ClickableSpan) {
+ return SpanType.CLICKABLE;
+ } else if (span instanceof ForegroundColorSpan) {
+ return SpanType.FOREGROUND_COLOR;
+ } else if (span instanceof AbsoluteSizeSpan) {
+ return SpanType.ABSOLUTE_SIZE;
+ } else if (span instanceof TypefaceSpan) {
+ return SpanType.TYPEFACE;
+ } else if (span instanceof ImageSpan) {
+ return SpanType.IMAGE;
+ } else if (span instanceof LineHeightSpan) {
+ return SpanType.LINE_HEIGHT;
+ } else if (span instanceof CharacterStyle) { // Replaced by patch.
+ return SpanType.CUSTOM_CHARACTER_STYLE;
+ } else {
+ return SpanType.UNKNOWN;
+ }
+ }
+
+ private static String getSpanType(SpanType spanType, Object span) {
+ return spanType == SpanType.UNKNOWN
+ ? span.getClass().getSimpleName()
+ : spanType.type;
+ }
+
+ private static final Filter[] filters = new Filter[]{
+ new DummyFilter() // Replaced by patch.
+ };
+
+ private static final StringTrieSearch searchTree = new StringTrieSearch();
+
+ /**
+ * Because litho filtering is multi-threaded and the buffer is passed in from a different injection point,
+ * the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
+ */
+ private static final ThreadLocal conversionContextThreadLocal = new ThreadLocal<>();
+
+ static {
+ for (Filter filter : filters) {
+ filterUsingCallbacks(filter, filter.callbacks);
+ }
+
+ if (Settings.DEBUG_SPANNABLE.get()) {
+ Logger.printDebug(() -> "Using: "
+ + searchTree.numberOfPatterns() + " conversion context filters"
+ + " (" + searchTree.getEstimatedMemorySize() + " KB)");
+ }
+ }
+
+ private static void filterUsingCallbacks(Filter filter, List groups) {
+ String filterSimpleName = filter.getClass().getSimpleName();
+
+ for (StringFilterGroup group : groups) {
+ if (!group.includeInSearch()) {
+ continue;
+ }
+
+ for (String pattern : group.filters) {
+ InclusiveSpanType.searchTree.addPattern(pattern, (textSearched, matchedStartIndex,
+ matchedLength, callbackParameter) -> {
+ if (!group.isEnabled()) return false;
+
+ LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
+ final boolean isFiltered = filter.skip(parameters.conversionContext, parameters.spannableString,
+ parameters.span, parameters.start, parameters.end, parameters.flags, parameters.isWord,
+ parameters.spanType, group);
+
+ if (isFiltered && Settings.DEBUG_SPANNABLE.get()) {
+ Logger.printDebug(() -> "Removed " + filterSimpleName
+ + " setSpan: " + parameters.spanType);
+ }
+
+ return isFiltered;
+ }
+ );
+ }
+ }
+ }
+
+ /**
+ * Injection point.
+ *
+ * @param conversionContext ConversionContext is used to identify whether it is a comment thread or not.
+ */
+ public static CharSequence setConversionContext(@NonNull Object conversionContext,
+ @NonNull CharSequence original) {
+ conversionContextThreadLocal.set(conversionContext.toString());
+ return original;
+ }
+
+ private static boolean returnEarly(SpannableString spannableString, Object span, int start, int end, int flags) {
+ try {
+ final String conversionContext = conversionContextThreadLocal.get();
+ if (conversionContext == null || conversionContext.isEmpty()) {
+ return false;
+ }
+
+ LithoFilterParameters parameter =
+ new LithoFilterParameters(conversionContext, spannableString, span, start, end, flags);
+
+ if (Settings.DEBUG_SPANNABLE.get()) {
+ Logger.printDebug(() -> "Searching...\n\u200B\n" + parameter);
+ }
+
+ return searchTree.matches(parameter.conversionContext, parameter);
+ } catch (Exception ex) {
+ Logger.printException(() -> "Spans filter failure", ex);
+ }
+
+ return false;
+ }
+
+ /**
+ * Injection point.
+ *
+ * @param spannableString Original SpannableString.
+ * @param span Span such as {@link ClickableSpan}, {@link ForegroundColorSpan},
+ * {@link AbsoluteSizeSpan}, {@link TypefaceSpan}, {@link ImageSpan}.
+ * @param start Start index of {@link Spannable#setSpan(Object, int, int, int)}.
+ * @param end End index of {@link Spannable#setSpan(Object, int, int, int)}.
+ * @param flags Flags of {@link Spannable#setSpan(Object, int, int, int)}.
+ */
+ public static void setSpan(SpannableString spannableString, Object span, int start, int end, int flags) {
+ if (returnEarly(spannableString, span, start, end, flags)) {
+ return;
+ }
+ spannableString.setSpan(span, start, end, flags);
+ }
+}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanType.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanType.java
new file mode 100644
index 000000000..b63e07822
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanType.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+package app.morphe.extension.youtube.patches.spans;
+
+import androidx.annotation.NonNull;
+
+enum SpanType {
+ ABSOLUTE_SIZE("AbsoluteSizeSpan"),
+ CLICKABLE("ClickableSpan"),
+ CUSTOM_CHARACTER_STYLE("CustomCharacterStyle"),
+ FOREGROUND_COLOR("ForegroundColorSpan"),
+ IMAGE("ImageSpan"),
+ LINE_HEIGHT("LineHeightSpan"),
+ TYPEFACE("TypefaceSpan"),
+ UNKNOWN("Unknown");
+
+ @NonNull
+ public final String type;
+
+ SpanType(@NonNull String type) {
+ this.type = type;
+ }
+}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java
index 5d51ceed8..4e09bfbe1 100644
--- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java
@@ -463,6 +463,7 @@ public class Settings extends SharedYouTubeSettings {
public static final EnumSetting SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("morphe_spoof_video_streams_client_type", ClientType.ANDROID_REEL, true, parent(SPOOF_VIDEO_STREAMS));
public static final BooleanSetting SPOOF_VIDEO_STREAMS_AV1 = new BooleanSetting("morphe_spoof_video_streams_av1", FALSE, true,
"morphe_spoof_video_streams_av1_user_dialog_message", new SpoofClientAv1Availability());
+ public static final BooleanSetting DEBUG_SPANNABLE = new BooleanSetting("morphe_debug_spannable", FALSE, parent(BaseSettings.DEBUG));
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("morphe_debug_protobuffer", FALSE, false,
"morphe_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));
diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spans/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spans/Fingerprints.kt
new file mode 100644
index 000000000..b0fb571f5
--- /dev/null
+++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spans/Fingerprints.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+package app.morphe.patches.youtube.misc.spans
+
+import app.morphe.patcher.Fingerprint
+import app.morphe.patcher.fieldAccess
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+
+internal object CustomCharacterStyleFingerprint : Fingerprint(
+ returnType = "Landroid/graphics/Path;",
+ parameters = listOf("Landroid/text/Layout;")
+)
+
+internal object InclusiveSpanFilterFingerprint : Fingerprint(
+ definingClass = EXTENSION_SPANS_CLASS,
+ accessFlags = listOf(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR),
+ filters = listOf(
+ fieldAccess(
+ opcode = Opcode.SPUT_OBJECT,
+ definingClass = "this",
+ type = EXTENSION_FILTER_ARRAY
+ )
+ )
+)
+
+internal object GetSpanTypeFingerprint : Fingerprint(
+ definingClass = EXTENSION_SPANS_CLASS,
+ name = "getSpanType",
+ custom = { method, _ ->
+ method.returnType != "Ljava/lang/String;"
+ }
+)
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spans/InclusiveSpanPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spans/InclusiveSpanPatch.kt
new file mode 100644
index 000000000..f293dd647
--- /dev/null
+++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spans/InclusiveSpanPatch.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * Original hard forked code:
+ * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+@file:Suppress("SpellCheckingInspection")
+
+package app.morphe.patches.youtube.misc.spans
+
+import app.morphe.patcher.extensions.InstructionExtensions.addInstruction
+import app.morphe.patcher.extensions.InstructionExtensions.addInstructions
+import app.morphe.patcher.extensions.InstructionExtensions.getInstruction
+import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction
+import app.morphe.patcher.patch.bytecodePatch
+import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod
+import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
+import app.morphe.patches.youtube.misc.textcomponent.hookSpannableString
+import app.morphe.patches.youtube.misc.textcomponent.textComponentPatch
+import app.morphe.patches.youtube.shared.indexOfSpannableStringInstruction
+import app.morphe.patches.youtube.shared.SpannableStringBuilderFingerprint
+import app.morphe.util.fiveRegisters
+import app.morphe.util.getMutableMethod
+import app.morphe.util.getReference
+import app.morphe.util.indexOfFirstInstructionOrThrow
+import app.morphe.util.indexOfFirstInstructionReversedOrThrow
+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.iface.instruction.OneRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
+import java.lang.ref.WeakReference
+
+internal const val EXTENSION_SPANS_CLASS = "Lapp/morphe/extension/youtube/patches/spans/InclusiveSpanPatch;"
+
+internal const val EXTENSION_FILTER_ARRAY = "[Lapp/morphe/extension/youtube/patches/spans/Filter;"
+
+// Registers used in extension helperMethod.
+private const val REGISTER_FILTER_CLASS = 0
+private const val REGISTER_FILTER_COUNT = 1
+private const val REGISTER_FILTER_ARRAY = 2
+
+private lateinit var helperMethodRef: WeakReference
+private var addSpanFilterCount = 0
+
+fun addSpanFilter(classDescriptor: String) {
+ helperMethodRef.get()!!.addInstructions(
+ 0,
+ """
+ new-instance v$REGISTER_FILTER_CLASS, $classDescriptor
+ invoke-direct { v$REGISTER_FILTER_CLASS }, $classDescriptor->()V
+ const/16 v$REGISTER_FILTER_COUNT, ${addSpanFilterCount++}
+ aput-object v$REGISTER_FILTER_CLASS, v$REGISTER_FILTER_ARRAY, v$REGISTER_FILTER_COUNT
+ """
+ )
+}
+
+val inclusiveSpanPatch = bytecodePatch(
+ description = "Hooks SpannableString setting and filters specific span components.",
+) {
+ dependsOn(textComponentPatch)
+
+ execute {
+ hookSpannableString(
+ EXTENSION_SPANS_CLASS,
+ "setConversionContext"
+ )
+
+ SpannableStringBuilderFingerprint.method.apply {
+ val spannedIndex = indexOfSpannableStringInstruction(this)
+ val setInclusiveSpanIndex = indexOfFirstInstructionOrThrow(spannedIndex) {
+ val reference = getReference()
+ opcode == Opcode.INVOKE_STATIC &&
+ reference?.returnType == "V" &&
+ reference.parameterTypes.size > 3 &&
+ reference.parameterTypes.firstOrNull() == "Landroid/text/SpannableString;"
+ }
+ val setInclusiveSpanMethod = getInstruction(setInclusiveSpanIndex)
+ .getReference()!!
+ .getMutableMethod()
+
+ setInclusiveSpanMethod.apply {
+ val insertIndex = indexOfFirstInstructionReversedOrThrow {
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ getReference().toString() == "Landroid/text/SpannableString;->setSpan(Ljava/lang/Object;III)V"
+ }
+ replaceInstruction(
+ insertIndex,
+ "invoke-static { ${fiveRegisters(insertIndex)} }, $EXTENSION_SPANS_CLASS->setSpan(Landroid/text/SpannableString;Ljava/lang/Object;III)V"
+ )
+ }
+ }
+
+ val customCharacterStyle = CustomCharacterStyleFingerprint.classDef.type
+
+ GetSpanTypeFingerprint.method.apply {
+ val index = indexOfFirstInstructionOrThrow {
+ opcode == Opcode.INSTANCE_OF &&
+ (this as? ReferenceInstruction)?.reference?.toString() == "Landroid/text/style/CharacterStyle;"
+ }
+ val instruction = getInstruction(index)
+ replaceInstruction(
+ index,
+ "instance-of v${instruction.registerA}, v${instruction.registerB}, $customCharacterStyle"
+ )
+ }
+
+ // Remove dummy filter from extension static field and add the filters included during patching.
+ InclusiveSpanFilterFingerprint.let {
+ it.method.apply {
+ // Add a helper method to avoid finding multiple free registers.
+ // This fixes an issue with extension compiled with Android Gradle Plugin 8.3.0+.
+ val helperClass = definingClass
+ val helperName = "patch_getFilterArray"
+ val helperReturnType = EXTENSION_FILTER_ARRAY
+ val helperMethod = ImmutableMethod(
+ helperClass,
+ helperName,
+ listOf(),
+ helperReturnType,
+ AccessFlags.PRIVATE.value or AccessFlags.STATIC.value,
+ null,
+ null,
+ MutableMethodImplementation(3),
+ ).toMutable()
+ it.classDef.methods.add(helperMethod)
+ helperMethodRef = WeakReference(helperMethod)
+
+ val insertIndex = it.instructionMatches.first().index
+ val insertRegister = getInstruction(insertIndex).registerA
+
+ addInstructions(
+ insertIndex,
+ """
+ invoke-static {}, $EXTENSION_SPANS_CLASS->$helperName()$EXTENSION_FILTER_ARRAY
+ move-result-object v$insertRegister
+ """
+ )
+ }
+ }
+ }
+
+ finalize {
+ helperMethodRef.get()!!.apply {
+ addInstruction(
+ implementation!!.instructions.size,
+ "return-object v$REGISTER_FILTER_ARRAY"
+ )
+
+ addInstructions(
+ 0,
+ """
+ const/16 v$REGISTER_FILTER_COUNT, $addSpanFilterCount
+ new-array v$REGISTER_FILTER_ARRAY, v$REGISTER_FILTER_COUNT, $EXTENSION_FILTER_ARRAY
+ """
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/textcomponent/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/textcomponent/Fingerprints.kt
new file mode 100644
index 000000000..7adba6a84
--- /dev/null
+++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/textcomponent/Fingerprints.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+package app.morphe.patches.youtube.misc.textcomponent
+
+import app.morphe.patcher.Fingerprint
+import app.morphe.patcher.opcode
+import app.morphe.patcher.string
+import com.android.tools.smali.dexlib2.AccessFlags
+import com.android.tools.smali.dexlib2.Opcode
+
+internal object TextComponentConstructorFingerprint : Fingerprint(
+ returnType = "V",
+ accessFlags = listOf(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR),
+ filters = listOf(string("TextComponent"))
+)
+
+internal object TextComponentContextFingerprint : Fingerprint(
+ classFingerprint = TextComponentConstructorFingerprint,
+ returnType = "L",
+ accessFlags = listOf(AccessFlags.PROTECTED, AccessFlags.FINAL),
+ parameters = listOf("L"),
+ filters = listOf(
+ opcode(Opcode.IGET_OBJECT),
+ opcode(Opcode.IGET_OBJECT),
+ opcode(Opcode.IGET_OBJECT),
+ opcode(Opcode.IGET_BOOLEAN)
+ )
+)
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/textcomponent/TextComponentPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/textcomponent/TextComponentPatch.kt
new file mode 100644
index 000000000..beb689be2
--- /dev/null
+++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/textcomponent/TextComponentPatch.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+package app.morphe.patches.youtube.misc.textcomponent
+
+import app.morphe.patcher.extensions.InstructionExtensions.addInstruction
+import app.morphe.patcher.extensions.InstructionExtensions.addInstructions
+import app.morphe.patcher.extensions.InstructionExtensions.getInstruction
+import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction
+import app.morphe.patcher.patch.PatchException
+import app.morphe.patcher.patch.bytecodePatch
+import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod
+import app.morphe.patches.youtube.shared.SPANNABLE_STRING_REFERENCE
+import app.morphe.patches.youtube.shared.SpannableStringBuilderFingerprint
+import app.morphe.patches.youtube.shared.indexOfSpannableStringInstruction
+import app.morphe.util.getReference
+import app.morphe.util.indexOfFirstInstruction
+import app.morphe.util.indexOfFirstInstructionOrThrow
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+
+private lateinit var spannedMethod: MutableMethod
+private var spannedIndex = 0
+private var spannedRegister = 0
+private var spannedContextRegister = 0
+
+private lateinit var textComponentMethod: MutableMethod
+private var textComponentIndex = 0
+private var textComponentRegister = 0
+private var textComponentContextRegister = 0
+
+val textComponentPatch = bytecodePatch(
+ description = "Provides hooks into text components for extension filtering."
+) {
+ execute {
+ SpannableStringBuilderFingerprint.method.apply {
+ spannedMethod = this
+ spannedIndex = indexOfSpannableStringInstruction(this)
+ spannedRegister = getInstruction(spannedIndex).registerC
+ spannedContextRegister =
+ getInstruction(spannedIndex + 1).registerA
+
+ replaceInstruction(
+ spannedIndex,
+ "move-object/from16 v$spannedContextRegister, p0"
+ )
+ addInstruction(
+ ++spannedIndex,
+ "invoke-static {v$spannedRegister}, $SPANNABLE_STRING_REFERENCE"
+ )
+ }
+
+ TextComponentContextFingerprint.method.apply {
+ textComponentMethod = this
+ val conversionContextFieldIndex = indexOfFirstInstructionOrThrow {
+ getReference()?.type == "Ljava/util/Map;"
+ } - 1
+ val conversionContextFieldReference =
+ getInstruction(conversionContextFieldIndex).reference
+
+ // ~ YouTube 19.32.xx
+ val legacyCharSequenceIndex = indexOfFirstInstruction {
+ getReference()?.type == "Ljava/util/BitSet;"
+ } - 1
+ val charSequenceIndex = indexOfFirstInstruction {
+ val reference = getReference()
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ reference?.returnType == "V" &&
+ reference.parameterTypes.firstOrNull() == "Ljava/lang/CharSequence;"
+ }
+
+ val insertIndex: Int
+
+ if (legacyCharSequenceIndex > -2) {
+ textComponentRegister =
+ getInstruction(legacyCharSequenceIndex).registerA
+ insertIndex = legacyCharSequenceIndex - 1
+ } else if (charSequenceIndex > -1) {
+ textComponentRegister =
+ getInstruction(charSequenceIndex).registerD
+ insertIndex = charSequenceIndex
+ } else {
+ throw PatchException("Could not find insert index")
+ }
+
+ textComponentContextRegister = getInstruction(
+ indexOfFirstInstructionOrThrow(insertIndex, Opcode.IGET_OBJECT)
+ ).registerA
+
+ addInstructions(
+ insertIndex, """
+ move-object/from16 v$textComponentContextRegister, p0
+ iget-object v$textComponentContextRegister, v$textComponentContextRegister, $conversionContextFieldReference
+ """
+ )
+ textComponentIndex = insertIndex + 2
+ }
+ }
+}
+
+internal fun hookSpannableString(
+ classDescriptor: String,
+ methodName: String
+) = spannedMethod.addInstructions(
+ spannedIndex, """
+ invoke-static {v$spannedContextRegister, v$spannedRegister}, $classDescriptor->$methodName(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
+ move-result-object v$spannedRegister
+ """
+)
+
+internal fun hookTextComponent(
+ classDescriptor: String,
+ methodName: String = "onLithoTextLoaded"
+) = textComponentMethod.apply {
+ addInstructions(
+ textComponentIndex, """
+ invoke-static {v$textComponentContextRegister, v$textComponentRegister}, $classDescriptor->$methodName(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
+ move-result-object v$textComponentRegister
+ """
+ )
+ textComponentIndex += 2
+}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Fingerprints.kt
index fdd15aaa8..ea8d7138e 100644
--- a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Fingerprints.kt
@@ -23,8 +23,13 @@ import app.morphe.patcher.opcode
import app.morphe.patcher.string
import app.morphe.patches.all.misc.resources.ResourceType
import app.morphe.patches.all.misc.resources.resourceLiteral
+import app.morphe.util.getReference
+import app.morphe.util.indexOfFirstInstruction
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
+import com.android.tools.smali.dexlib2.iface.reference.StringReference
internal const val YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE = "Lcom/google/android/apps/youtube/app/watchwhile/MainActivity;"
@@ -254,3 +259,24 @@ internal object WatchNextResponseParserFingerprint : Fingerprint(
literal(46659098L),
)
)
+
+internal object SpannableStringBuilderFingerprint : Fingerprint(
+ returnType = "Ljava/lang/CharSequence;",
+ custom = { method, _ ->
+ method.indexOfFirstInstruction {
+ opcode == Opcode.CONST_STRING &&
+ getReference()
+ ?.string.toString()
+ .startsWith("Failed to set PB Style Run Extension in TextComponentSpec.")
+ } >= 0 &&
+ indexOfSpannableStringInstruction(method) >= 0
+ }
+)
+
+const val SPANNABLE_STRING_REFERENCE =
+ "Landroid/text/SpannableString;->valueOf(Ljava/lang/CharSequence;)Landroid/text/SpannableString;"
+
+fun indexOfSpannableStringInstruction(method: Method) = method.indexOfFirstInstruction {
+ opcode == Opcode.INVOKE_STATIC &&
+ getReference()?.toString() == SPANNABLE_STRING_REFERENCE
+}
From ae703c15c0fb0d309bc2619ba1c756afa6676109 Mon Sep 17 00:00:00 2001
From: ILoveOpenSouceApplications
<117499019+ILoveOpenSourceApplications@users.noreply.github.com>
Date: Fri, 24 Apr 2026 19:12:15 +0530
Subject: [PATCH 2/6] refactor: Use Java `record` and simplify `if else`
---
.../patches/components/CustomFilter.java | 6 +-
.../patches/components/LithoFilterPatch.java | 154 ++++++++----------
2 files changed, 72 insertions(+), 88 deletions(-)
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CustomFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CustomFilter.java
index 9fc7d96f9..b11cb8445 100644
--- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CustomFilter.java
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CustomFilter.java
@@ -200,10 +200,6 @@ boolean isFiltered(ContextInterface contextInterface,
}
// Check buffer if specified.
- if (custom.bufferSearch != null && !custom.bufferSearch.matches(buffer)) {
- return false;
- }
-
- return true; // All custom filter conditions passed.
+ return custom.bufferSearch == null || custom.bufferSearch.matches(buffer); // All custom filter conditions passed.
}
}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LithoFilterPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LithoFilterPatch.java
index 45065d368..bf487b381 100644
--- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LithoFilterPatch.java
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LithoFilterPatch.java
@@ -26,99 +26,86 @@
@SuppressWarnings("unused")
public final class LithoFilterPatch {
/**
- * Simple wrapper to pass the litho parameters through the prefix search.
- */
- private static final class LithoFilterParameters {
- final ContextInterface contextInterface;
- final String identifier;
- final String path;
- final String accessibility;
- final byte[] buffer;
-
- LithoFilterParameters(ContextInterface contextInterface, String identifier,
- String path, String accessibility, byte[] buffer) {
- this.contextInterface = contextInterface;
- this.identifier = identifier;
- this.path = path;
- this.accessibility = accessibility;
- this.buffer = buffer;
- }
+ * Simple wrapper to pass the litho parameters through the prefix search.
+ */
+ private record LithoFilterParameters(ContextInterface contextInterface, String identifier,
+ String path, String accessibility, byte[] buffer) {
@NonNull
- @Override
- public String toString() {
- // Estimate the percentage of the buffer that are Strings.
- StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
- builder.append( "ID: ");
- builder.append(identifier);
- if (!accessibility.isEmpty()) {
- // AccessibilityId and AccessibilityText are pieces of BufferStrings.
- builder.append(" Accessibility: ");
- builder.append(accessibility);
- }
- builder.append(" Path: ");
- builder.append(path);
- if (Settings.DEBUG_PROTOBUFFER.get()) {
- builder.append(" BufferStrings: ");
- findAsciiStrings(builder, buffer);
- }
+ @Override
+ public String toString() {
+ // Estimate the percentage of the buffer that are Strings.
+ StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
+ builder.append("ID: ");
+ builder.append(identifier);
+ if (!accessibility.isEmpty()) {
+ // AccessibilityId and AccessibilityText are pieces of BufferStrings.
+ builder.append(" Accessibility: ");
+ builder.append(accessibility);
+ }
+ builder.append(" Path: ");
+ builder.append(path);
+ if (Settings.DEBUG_PROTOBUFFER.get()) {
+ builder.append(" BufferStrings: ");
+ findAsciiStrings(builder, buffer);
+ }
- return builder.toString();
- }
+ return builder.toString();
+ }
- /**
- * Search through a byte array for all ASCII strings.
- */
- static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
- // Valid ASCII values (ignore control characters).
- final int minimumAscii = 32; // 32 = space character
- final int maximumAscii = 126; // 127 = delete character
- final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include.
- // 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 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++;
+ /**
+ * Search through a byte array for all ASCII strings.
+ */
+ static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
+ // Valid ASCII values (ignore control characters).
+ final int minimumAscii = 32; // 32 = space character
+ final int maximumAscii = 126; // 127 = delete character
+ final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include.
+ // 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 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;
+ }
+ }
- // Hard line limit. Hard wrap the current substring to next logger line.
- if (currentLineLength >= maxLineLength) {
+ // Wrap after substring if over preferred limit.
+ if (currentLineLength >= preferredLineLength) {
builder.append('\n');
currentLineLength = 0;
}
- }
- // Wrap after substring if over preferred limit.
- if (currentLineLength >= preferredLineLength) {
- builder.append('\n');
- currentLineLength = 0;
+ builder.append(delimitingCharacter);
+ currentLineLength++;
}
- builder.append(delimitingCharacter);
- currentLineLength++;
+ start = end + 1;
}
-
- start = end + 1;
}
}
}
- }
/**
* Placeholder for actual filters.
@@ -190,14 +177,14 @@ private static void filterUsingCallbacks(StringTrieSearch pathSearchTree,
if (!group.isEnabled()) return false;
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
- final boolean isFiltered = filter.isFiltered(parameters.contextInterface,
- parameters.identifier, parameters.accessibility, parameters.path,
- parameters.buffer, group, type, matchedStartIndex);
+ final boolean isFiltered = filter.isFiltered(parameters.contextInterface(),
+ parameters.identifier(), parameters.accessibility(), parameters.path(),
+ parameters.buffer(), group, type, matchedStartIndex);
if (isFiltered && BaseSettings.DEBUG.get()) {
Logger.printDebug(() -> type == Filter.FilterContentType.IDENTIFIER
- ? filterSimpleName + " filtered identifier: " + parameters.identifier
- : filterSimpleName + " filtered path: " + parameters.path);
+ ? filterSimpleName + " filtered identifier: " + parameters.identifier()
+ : filterSimpleName + " filtered path: " + parameters.path());
}
return isFiltered;
@@ -233,6 +220,7 @@ public static boolean isFiltered(ContextInterface contextInterface, @Nullable by
try {
String identifier = contextInterface.patch_getIdentifier();
StringBuilder pathBuilder = contextInterface.patch_getPathBuilder();
+ //noinspection SizeReplaceableByIsEmpty
if (identifier.isEmpty() || pathBuilder.length() == 0) {
return false;
}
From 1ec6590097fc16aedeb0b6d1d70d978d71237007 Mon Sep 17 00:00:00 2001
From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
Date: Sat, 2 May 2026 18:36:49 +0200
Subject: [PATCH 3/6] Formatting, no functional changes
---
.../patches/components/LithoFilterPatch.java | 132 +++++++++---------
1 file changed, 66 insertions(+), 66 deletions(-)
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LithoFilterPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LithoFilterPatch.java
index bf487b381..86fa6dc7c 100644
--- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LithoFilterPatch.java
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LithoFilterPatch.java
@@ -32,80 +32,80 @@ private record LithoFilterParameters(ContextInterface contextInterface, String i
String path, String accessibility, byte[] buffer) {
@NonNull
- @Override
- public String toString() {
- // Estimate the percentage of the buffer that are Strings.
- StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
- builder.append("ID: ");
- builder.append(identifier);
- if (!accessibility.isEmpty()) {
- // AccessibilityId and AccessibilityText are pieces of BufferStrings.
- builder.append(" Accessibility: ");
- builder.append(accessibility);
- }
- builder.append(" Path: ");
- builder.append(path);
- if (Settings.DEBUG_PROTOBUFFER.get()) {
- builder.append(" BufferStrings: ");
- findAsciiStrings(builder, buffer);
- }
-
- return builder.toString();
+ @Override
+ public String toString() {
+ // Estimate the percentage of the buffer that are Strings.
+ StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
+ builder.append("ID: ");
+ builder.append(identifier);
+ if (!accessibility.isEmpty()) {
+ // AccessibilityId and AccessibilityText are pieces of BufferStrings.
+ builder.append(" Accessibility: ");
+ builder.append(accessibility);
+ }
+ builder.append(" Path: ");
+ builder.append(path);
+ if (Settings.DEBUG_PROTOBUFFER.get()) {
+ builder.append(" BufferStrings: ");
+ findAsciiStrings(builder, buffer);
}
- /**
- * Search through a byte array for all ASCII strings.
- */
- static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
- // Valid ASCII values (ignore control characters).
- final int minimumAscii = 32; // 32 = space character
- final int maximumAscii = 126; // 127 = delete character
- final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include.
- // 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 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;
- }
- }
+ return builder.toString();
+ }
- // Wrap after substring if over preferred limit.
- if (currentLineLength >= preferredLineLength) {
+ /**
+ * Search through a byte array for all ASCII strings.
+ */
+ static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
+ // Valid ASCII values (ignore control characters).
+ final int minimumAscii = 32; // 32 = space character
+ final int maximumAscii = 126; // 127 = delete character
+ final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include.
+ // 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 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;
}
+ }
- builder.append(delimitingCharacter);
- currentLineLength++;
+ // Wrap after substring if over preferred limit.
+ if (currentLineLength >= preferredLineLength) {
+ builder.append('\n');
+ currentLineLength = 0;
}
- start = end + 1;
+ builder.append(delimitingCharacter);
+ currentLineLength++;
}
+
+ start = end + 1;
}
}
}
+ }
/**
* Placeholder for actual filters.
@@ -177,14 +177,14 @@ private static void filterUsingCallbacks(StringTrieSearch pathSearchTree,
if (!group.isEnabled()) return false;
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
- final boolean isFiltered = filter.isFiltered(parameters.contextInterface(),
- parameters.identifier(), parameters.accessibility(), parameters.path(),
- parameters.buffer(), group, type, matchedStartIndex);
+ final boolean isFiltered = filter.isFiltered(parameters.contextInterface,
+ parameters.identifier, parameters.accessibility, parameters.path,
+ parameters.buffer, group, type, matchedStartIndex);
if (isFiltered && BaseSettings.DEBUG.get()) {
Logger.printDebug(() -> type == Filter.FilterContentType.IDENTIFIER
- ? filterSimpleName + " filtered identifier: " + parameters.identifier()
- : filterSimpleName + " filtered path: " + parameters.path());
+ ? filterSimpleName + " filtered identifier: " + parameters.identifier
+ : filterSimpleName + " filtered path: " + parameters.path);
}
return isFiltered;
From 356c884d4bc7d06fc1faf374810965c351236163 Mon Sep 17 00:00:00 2001
From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
Date: Sat, 2 May 2026 18:49:57 +0200
Subject: [PATCH 4/6] refactor
---
...eSpanType.java => InclusiveSpanPatch.java} | 29 +++++++++----------
.../spans/{Filter.java => SpanFilter.java} | 12 ++++----
...{FilterGroup.java => SpanFilterGroup.java} | 8 ++---
...roupList.java => SpanFilterGroupList.java} | 14 +++++----
.../youtube/misc/spans/InclusiveSpanPatch.kt | 4 +--
.../misc/textcomponent/TextComponentPatch.kt | 19 +++++++-----
6 files changed, 45 insertions(+), 41 deletions(-)
rename extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/{InclusiveSpanType.java => InclusiveSpanPatch.java} (88%)
rename extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/{Filter.java => SpanFilter.java} (86%)
rename extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/{FilterGroup.java => SpanFilterGroup.java} (90%)
rename extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/{FilterGroupList.java => SpanFilterGroupList.java} (77%)
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanPatch.java
similarity index 88%
rename from extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java
rename to extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanPatch.java
index c7a26ba8c..c8200e99d 100644
--- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanPatch.java
@@ -31,11 +31,11 @@
/**
* Placeholder for actual filters.
*/
-final class DummyFilter extends Filter {
+final class DummySpanFilter extends SpanFilter {
}
@SuppressWarnings("unused")
-public final class InclusiveSpanType {
+public final class InclusiveSpanPatch {
/**
* Simple wrapper to pass the litho parameters through the prefix search.
@@ -118,20 +118,20 @@ private static String getSpanType(SpanType spanType, Object span) {
: spanType.type;
}
- private static final Filter[] filters = new Filter[]{
- new DummyFilter() // Replaced by patch.
+ private static final SpanFilter[] filters = new SpanFilter[]{
+ new DummySpanFilter() // Replaced by patch.
};
private static final StringTrieSearch searchTree = new StringTrieSearch();
/**
- * Because litho filtering is multi-threaded and the buffer is passed in from a different injection point,
+ * Because litho filtering is multithreaded and the buffer is passed in from a different injection point,
* the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
*/
private static final ThreadLocal conversionContextThreadLocal = new ThreadLocal<>();
static {
- for (Filter filter : filters) {
+ for (SpanFilter filter : filters) {
filterUsingCallbacks(filter, filter.callbacks);
}
@@ -142,17 +142,17 @@ private static String getSpanType(SpanType spanType, Object span) {
}
}
- private static void filterUsingCallbacks(Filter filter, List groups) {
+ private static void filterUsingCallbacks(SpanFilter filter, List groups) {
String filterSimpleName = filter.getClass().getSimpleName();
- for (StringFilterGroup group : groups) {
+ for (StringSpanFilterGroup group : groups) {
if (!group.includeInSearch()) {
continue;
}
for (String pattern : group.filters) {
- InclusiveSpanType.searchTree.addPattern(pattern, (textSearched, matchedStartIndex,
- matchedLength, callbackParameter) -> {
+ InclusiveSpanPatch.searchTree.addPattern(pattern, (textSearched, matchedStartIndex,
+ matchedLength, callbackParameter) -> {
if (!group.isEnabled()) return false;
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
@@ -177,21 +177,20 @@ private static void filterUsingCallbacks(Filter filter, List
*
* @param conversionContext ConversionContext is used to identify whether it is a comment thread or not.
*/
- public static CharSequence setConversionContext(@NonNull Object conversionContext,
- @NonNull CharSequence original) {
+ public static CharSequence setConversionContext(Object conversionContext, CharSequence original) {
conversionContextThreadLocal.set(conversionContext.toString());
return original;
}
private static boolean returnEarly(SpannableString spannableString, Object span, int start, int end, int flags) {
try {
- final String conversionContext = conversionContextThreadLocal.get();
+ String conversionContext = conversionContextThreadLocal.get();
if (conversionContext == null || conversionContext.isEmpty()) {
return false;
}
- LithoFilterParameters parameter =
- new LithoFilterParameters(conversionContext, spannableString, span, start, end, flags);
+ LithoFilterParameters parameter = new LithoFilterParameters(conversionContext,
+ spannableString, span, start, end, flags);
if (Settings.DEBUG_SPANNABLE.get()) {
Logger.printDebug(() -> "Searching...\n\u200B\n" + parameter);
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanFilter.java
similarity index 86%
rename from extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java
rename to extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanFilter.java
index 4cbfd19e6..5c66ff7f8 100644
--- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanFilter.java
@@ -26,22 +26,22 @@
*
* All callbacks must be registered before the constructor completes.
*/
-abstract class Filter {
+abstract class SpanFilter {
private static final RelativeSizeSpan relativeSizeSpanDummy = new RelativeSizeSpan(0f);
private static final Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT);
private static final ImageSpan imageSpanDummy = new ImageSpan(transparentDrawable);
/**
* Path callbacks. Do not add to this instance,
- * and instead use {@link #addCallbacks(StringFilterGroup...)}.
+ * and instead use {@link #addCallbacks(StringSpanFilterGroup...)}.
*/
- protected final List callbacks = new ArrayList<>();
+ protected final List callbacks = new ArrayList<>();
/**
- * Adds callbacks to {@link #skip(String, SpannableString, Object, int, int, int, boolean, SpanType, StringFilterGroup)}
+ * Adds callbacks to {@link #skip(String, SpannableString, Object, int, int, int, boolean, SpanType, StringSpanFilterGroup)}
* if any of the groups are found.
*/
- protected final void addCallbacks(StringFilterGroup... groups) {
+ protected final void addCallbacks(StringSpanFilterGroup... groups) {
callbacks.addAll(Arrays.asList(groups));
}
@@ -63,7 +63,7 @@ protected final void hideImageSpan(SpannableString spannableString, int start, i
* @param matchedGroup The actual filter that matched.
*/
boolean skip(String conversionContext, SpannableString spannableString, Object span, int start, int end,
- int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) {
+ int flags, boolean isWord, SpanType spanType, StringSpanFilterGroup matchedGroup) {
return true;
}
}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanFilterGroup.java
similarity index 90%
rename from extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java
rename to extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanFilterGroup.java
index 1df76925d..0ed0eb871 100644
--- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanFilterGroup.java
@@ -14,7 +14,7 @@
import app.morphe.extension.shared.settings.BooleanSetting;
-abstract class FilterGroup {
+abstract class SpanFilterGroup {
final static class FilterGroupResult {
private BooleanSetting setting;
private int matchedIndex;
@@ -45,7 +45,7 @@ public boolean isFiltered() {
protected final T[] filters;
@SafeVarargs
- public FilterGroup(final BooleanSetting setting, final T... filters) {
+ public SpanFilterGroup(final BooleanSetting setting, final T... filters) {
this.setting = setting;
this.filters = filters;
if (filters.length == 0) {
@@ -71,9 +71,9 @@ public String toString() {
public abstract FilterGroupResult check(final T stack);
}
-class StringFilterGroup extends FilterGroup {
+class StringSpanFilterGroup extends SpanFilterGroup {
- public StringFilterGroup(final BooleanSetting setting, final String... filters) {
+ public StringSpanFilterGroup(final BooleanSetting setting, final String... filters) {
super(setting, filters);
}
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanFilterGroupList.java
similarity index 77%
rename from extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java
rename to extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanFilterGroupList.java
index 09c84ba2a..da8aef5f1 100644
--- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/SpanFilterGroupList.java
@@ -22,7 +22,7 @@
import app.morphe.extension.shared.StringTrieSearch;
import app.morphe.extension.shared.TrieSearch;
-abstract class FilterGroupList> implements Iterable {
+abstract class SpanFilterGroupList> implements Iterable {
private final List filterGroups = new ArrayList<>();
private final TrieSearch search = createSearchGraph();
@@ -36,9 +36,11 @@ protected final void addAll(final T... groups) {
continue;
}
for (V pattern : group.filters) {
- search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
+ search.addPattern(pattern, (textSearched, matchedStartIndex,
+ matchedLength, callbackParameter) -> {
if (group.isEnabled()) {
- FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
+ SpanFilterGroup.FilterGroupResult result =
+ (SpanFilterGroup.FilterGroupResult) callbackParameter;
result.setValues(group.setting, matchedStartIndex);
return true;
}
@@ -65,8 +67,8 @@ public Spliterator spliterator() {
return filterGroups.spliterator();
}
- protected FilterGroup.FilterGroupResult check(V stack) {
- FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
+ protected SpanFilterGroup.FilterGroupResult check(V stack) {
+ SpanFilterGroup.FilterGroupResult result = new SpanFilterGroup.FilterGroupResult();
search.matches(stack, result);
return result;
}
@@ -74,7 +76,7 @@ protected FilterGroup.FilterGroupResult check(V stack) {
protected abstract TrieSearch createSearchGraph();
}
-final class StringFilterGroupList extends FilterGroupList {
+final class StringSpanFilterGroupList extends SpanFilterGroupList {
@Override
protected StringTrieSearch createSearchGraph() {
return new StringTrieSearch();
diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spans/InclusiveSpanPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spans/InclusiveSpanPatch.kt
index f293dd647..b5d2175c7 100644
--- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spans/InclusiveSpanPatch.kt
+++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spans/InclusiveSpanPatch.kt
@@ -21,8 +21,8 @@ import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod
import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.morphe.patches.youtube.misc.textcomponent.hookSpannableString
import app.morphe.patches.youtube.misc.textcomponent.textComponentPatch
-import app.morphe.patches.youtube.shared.indexOfSpannableStringInstruction
import app.morphe.patches.youtube.shared.SpannableStringBuilderFingerprint
+import app.morphe.patches.youtube.shared.indexOfSpannableStringInstruction
import app.morphe.util.fiveRegisters
import app.morphe.util.getMutableMethod
import app.morphe.util.getReference
@@ -40,7 +40,7 @@ import java.lang.ref.WeakReference
internal const val EXTENSION_SPANS_CLASS = "Lapp/morphe/extension/youtube/patches/spans/InclusiveSpanPatch;"
-internal const val EXTENSION_FILTER_ARRAY = "[Lapp/morphe/extension/youtube/patches/spans/Filter;"
+internal const val EXTENSION_FILTER_ARRAY = "[Lapp/morphe/extension/youtube/patches/spans/SpanFilter;"
// Registers used in extension helperMethod.
private const val REGISTER_FILTER_CLASS = 0
diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/textcomponent/TextComponentPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/textcomponent/TextComponentPatch.kt
index beb689be2..46337c32c 100644
--- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/textcomponent/TextComponentPatch.kt
+++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/textcomponent/TextComponentPatch.kt
@@ -97,10 +97,11 @@ val textComponentPatch = bytecodePatch(
).registerA
addInstructions(
- insertIndex, """
+ insertIndex,
+ """
move-object/from16 v$textComponentContextRegister, p0
iget-object v$textComponentContextRegister, v$textComponentContextRegister, $conversionContextFieldReference
- """
+ """
)
textComponentIndex = insertIndex + 2
}
@@ -111,10 +112,11 @@ internal fun hookSpannableString(
classDescriptor: String,
methodName: String
) = spannedMethod.addInstructions(
- spannedIndex, """
- invoke-static {v$spannedContextRegister, v$spannedRegister}, $classDescriptor->$methodName(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
+ spannedIndex,
+ """
+ invoke-static { v$spannedContextRegister, v$spannedRegister }, $classDescriptor->$methodName(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
move-result-object v$spannedRegister
- """
+ """
)
internal fun hookTextComponent(
@@ -122,10 +124,11 @@ internal fun hookTextComponent(
methodName: String = "onLithoTextLoaded"
) = textComponentMethod.apply {
addInstructions(
- textComponentIndex, """
- invoke-static {v$textComponentContextRegister, v$textComponentRegister}, $classDescriptor->$methodName(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
+ textComponentIndex,
+ """
+ invoke-static { v$textComponentContextRegister, v$textComponentRegister }, $classDescriptor->$methodName(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
move-result-object v$textComponentRegister
- """
+ """
)
textComponentIndex += 2
}
\ No newline at end of file
From f5e6736b0d86531a2294ea3a59ecc1408c5978cf Mon Sep 17 00:00:00 2001
From: ILoveOpenSouceApplications
<117499019+ILoveOpenSourceApplications@users.noreply.github.com>
Date: Fri, 24 Apr 2026 16:13:30 +0530
Subject: [PATCH 5/6] feat(YouTube): Introduce Litho text component and span
filtering framework
---
.../youtube/patches/spans/Filter.java | 66 ++++++
.../youtube/patches/spans/FilterGroup.java | 93 ++++++++
.../patches/spans/FilterGroupList.java | 79 +++++++
.../patches/spans/InclusiveSpanType.java | 221 ++++++++++++++++++
4 files changed, 459 insertions(+)
create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java
create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java
create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java
create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java
new file mode 100644
index 000000000..2afb2e95c
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+package app.morphe.extension.youtube.patches.spans;
+
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.SpannableString;
+import android.text.style.ImageSpan;
+import android.text.style.RelativeSizeSpan;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Filters litho based components.
+ *
+ * All callbacks must be registered before the constructor completes.
+ */
+abstract class Filter {
+ private static final RelativeSizeSpan relativeSizeSpanDummy = new RelativeSizeSpan(0f);
+ private static final Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT);
+ private static final ImageSpan imageSpanDummy = new ImageSpan(transparentDrawable);
+
+ /**
+ * Path callbacks. Do not add to this instance,
+ * and instead use {@link #addCallbacks(StringFilterGroup...)}.
+ */
+ protected final List callbacks = new ArrayList<>();
+
+ /**
+ * Adds callbacks to {@link #skip(String, SpannableString, Object, int, int, int, boolean, SpanType, StringFilterGroup)}
+ * if any of the groups are found.
+ */
+ protected final void addCallbacks(StringFilterGroup... groups) {
+ callbacks.addAll(Arrays.asList(groups));
+ }
+
+ protected final void hideSpan(SpannableString spannableString, int start, int end, int flags) {
+ spannableString.setSpan(relativeSizeSpanDummy, start, end, flags);
+ }
+
+ protected final void hideImageSpan(SpannableString spannableString, int start, int end, int flags) {
+ spannableString.setSpan(imageSpanDummy, start, end, flags);
+ }
+
+ /**
+ * Called after an enabled filter has been matched.
+ * Default implementation is to always filter the matched component and log the action.
+ * Subclasses can perform additional or different checks if needed.
+ *
+ * Method is called off the main thread.
+ *
+ * @param matchedGroup The actual filter that matched.
+ */
+ boolean skip(String conversionContext, SpannableString spannableString, Object span, int start, int end,
+ int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) {
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java
new file mode 100644
index 000000000..09968ab2d
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+package app.morphe.extension.youtube.patches.spans;
+
+import androidx.annotation.NonNull;
+
+import app.morphe.extension.shared.settings.BooleanSetting;
+
+abstract class FilterGroup {
+ final static class FilterGroupResult {
+ private BooleanSetting setting;
+ private int matchedIndex;
+
+ FilterGroupResult() {
+ this(null, -1);
+ }
+
+ FilterGroupResult(BooleanSetting setting, int matchedIndex) {
+ setValues(setting, matchedIndex);
+ }
+
+ public void setValues(BooleanSetting setting, int matchedIndex) {
+ this.setting = setting;
+ this.matchedIndex = matchedIndex;
+ }
+
+ public BooleanSetting getSetting() {
+ return setting;
+ }
+
+ public boolean isFiltered() {
+ return matchedIndex >= 0;
+ }
+ }
+
+ protected final BooleanSetting setting;
+ protected final T[] filters;
+
+ @SafeVarargs
+ public FilterGroup(final BooleanSetting setting, final T... filters) {
+ this.setting = setting;
+ this.filters = filters;
+ if (filters.length == 0) {
+ throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
+ }
+ }
+
+ public boolean isEnabled() {
+ return setting == null || setting.get();
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ public boolean includeInSearch() {
+ return isEnabled() || !setting.rebootApp;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
+ }
+
+ public abstract FilterGroupResult check(final T stack);
+}
+
+class StringFilterGroup extends FilterGroup {
+
+ public StringFilterGroup(final BooleanSetting setting, final String... filters) {
+ super(setting, filters);
+ }
+
+ @Override
+ public FilterGroupResult check(final String string) {
+ int matchedIndex = -1;
+ if (isEnabled()) {
+ for (String pattern : filters) {
+ if (!string.isEmpty()) {
+ final int indexOf = string.indexOf(pattern);
+ if (indexOf >= 0) {
+ matchedIndex = indexOf;
+ break;
+ }
+ }
+ }
+ }
+ return new FilterGroupResult(setting, matchedIndex);
+ }
+}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java
new file mode 100644
index 000000000..a70134be7
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+package app.morphe.extension.youtube.patches.spans;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+
+import app.morphe.extension.shared.StringTrieSearch;
+import app.morphe.extension.shared.TrieSearch;
+
+abstract class FilterGroupList> implements Iterable {
+
+ private final List filterGroups = new ArrayList<>();
+ private final TrieSearch search = createSearchGraph();
+
+ @SafeVarargs
+ protected final void addAll(final T... groups) {
+ filterGroups.addAll(Arrays.asList(groups));
+
+ for (T group : groups) {
+ if (!group.includeInSearch()) {
+ continue;
+ }
+ for (V pattern : group.filters) {
+ search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
+ if (group.isEnabled()) {
+ FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
+ result.setValues(group.setting, matchedStartIndex);
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+ }
+
+ @NonNull
+ @Override
+ public Iterator iterator() {
+ return filterGroups.iterator();
+ }
+
+ @Override
+ public void forEach(@NonNull Consumer super T> action) {
+ filterGroups.forEach(action);
+ }
+
+ @NonNull
+ @Override
+ public Spliterator spliterator() {
+ return filterGroups.spliterator();
+ }
+
+ protected FilterGroup.FilterGroupResult check(V stack) {
+ FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
+ search.matches(stack, result);
+ return result;
+ }
+
+ protected abstract TrieSearch createSearchGraph();
+}
+
+final class StringFilterGroupList extends FilterGroupList {
+ @Override
+ protected StringTrieSearch createSearchGraph() {
+ return new StringTrieSearch();
+ }
+}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java
new file mode 100644
index 000000000..18f368a93
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
+ */
+
+package app.morphe.extension.youtube.patches.spans;
+
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.CharacterStyle;
+import android.text.style.ClickableSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.text.style.LineHeightSpan;
+import android.text.style.TypefaceSpan;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+import app.morphe.extension.shared.Logger;
+import app.morphe.extension.shared.StringTrieSearch;
+import app.morphe.extension.youtube.settings.Settings;
+
+/**
+ * Placeholder for actual filters.
+ */
+final class DummyFilter extends Filter {
+}
+
+@SuppressWarnings("unused")
+public final class InclusiveSpanType {
+
+ /**
+ * Simple wrapper to pass the litho parameters through the prefix search.
+ */
+ private static final class LithoFilterParameters {
+ final String conversionContext;
+ final SpannableString spannableString;
+ final Object span;
+ final int start;
+ final int end;
+ final int flags;
+ final String originalString;
+ final int originalLength;
+ final SpanType spanType;
+ final boolean isWord;
+
+ public LithoFilterParameters(String conversionContext, SpannableString spannableString,
+ Object span, int start, int end, int flags) {
+ this.conversionContext = conversionContext;
+ this.spannableString = spannableString;
+ this.span = span;
+ this.start = start;
+ this.end = end;
+ this.flags = flags;
+ this.originalString = spannableString.toString();
+ this.originalLength = spannableString.length();
+ this.spanType = getSpanType(span);
+ this.isWord = !(start == 0 && end == originalLength);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("CharSequence:'")
+ .append(originalString)
+ .append("'\nSpanType:'")
+ .append(getSpanType(spanType, span))
+ .append("'\nLength:'")
+ .append(originalLength)
+ .append("'\nStart:'")
+ .append(start)
+ .append("'\nEnd:'")
+ .append(end)
+ .append("'\nisWord:'")
+ .append(isWord)
+ .append("'");
+ if (isWord) {
+ builder.append("\nWord:'")
+ .append(originalString.substring(start, end))
+ .append("'");
+ }
+ return builder.toString();
+ }
+ }
+
+ private static SpanType getSpanType(Object span) {
+ if (span instanceof ClickableSpan) {
+ return SpanType.CLICKABLE;
+ } else if (span instanceof ForegroundColorSpan) {
+ return SpanType.FOREGROUND_COLOR;
+ } else if (span instanceof AbsoluteSizeSpan) {
+ return SpanType.ABSOLUTE_SIZE;
+ } else if (span instanceof TypefaceSpan) {
+ return SpanType.TYPEFACE;
+ } else if (span instanceof ImageSpan) {
+ return SpanType.IMAGE;
+ } else if (span instanceof LineHeightSpan) {
+ return SpanType.LINE_HEIGHT;
+ } else if (span instanceof CharacterStyle) { // Replaced by patch.
+ return SpanType.CUSTOM_CHARACTER_STYLE;
+ } else {
+ return SpanType.UNKNOWN;
+ }
+ }
+
+ private static String getSpanType(SpanType spanType, Object span) {
+ return spanType == SpanType.UNKNOWN
+ ? span.getClass().getSimpleName()
+ : spanType.type;
+ }
+
+ private static final Filter[] filters = new Filter[]{
+ new DummyFilter() // Replaced by patch.
+ };
+
+ private static final StringTrieSearch searchTree = new StringTrieSearch();
+
+ /**
+ * Because litho filtering is multi-threaded and the buffer is passed in from a different injection point,
+ * the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
+ */
+ private static final ThreadLocal conversionContextThreadLocal = new ThreadLocal<>();
+
+ static {
+ for (Filter filter : filters) {
+ filterUsingCallbacks(filter, filter.callbacks);
+ }
+
+ if (Settings.DEBUG_SPANNABLE.get()) {
+ Logger.printDebug(() -> "Using: "
+ + searchTree.numberOfPatterns() + " conversion context filters"
+ + " (" + searchTree.getEstimatedMemorySize() + " KB)");
+ }
+ }
+
+ private static void filterUsingCallbacks(Filter filter, List groups) {
+ String filterSimpleName = filter.getClass().getSimpleName();
+
+ for (StringFilterGroup group : groups) {
+ if (!group.includeInSearch()) {
+ continue;
+ }
+
+ for (String pattern : group.filters) {
+ InclusiveSpanType.searchTree.addPattern(pattern, (textSearched, matchedStartIndex,
+ matchedLength, callbackParameter) -> {
+ if (!group.isEnabled()) return false;
+
+ LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
+ final boolean isFiltered = filter.skip(parameters.conversionContext, parameters.spannableString,
+ parameters.span, parameters.start, parameters.end, parameters.flags, parameters.isWord,
+ parameters.spanType, group);
+
+ if (isFiltered && Settings.DEBUG_SPANNABLE.get()) {
+ Logger.printDebug(() -> "Removed " + filterSimpleName
+ + " setSpan: " + parameters.spanType);
+ }
+
+ return isFiltered;
+ }
+ );
+ }
+ }
+ }
+
+ /**
+ * Injection point.
+ *
+ * @param conversionContext ConversionContext is used to identify whether it is a comment thread or not.
+ */
+ public static CharSequence setConversionContext(@NonNull Object conversionContext,
+ @NonNull CharSequence original) {
+ conversionContextThreadLocal.set(conversionContext.toString());
+ return original;
+ }
+
+ private static boolean returnEarly(SpannableString spannableString, Object span, int start, int end, int flags) {
+ try {
+ final String conversionContext = conversionContextThreadLocal.get();
+ if (conversionContext == null || conversionContext.isEmpty()) {
+ return false;
+ }
+
+ LithoFilterParameters parameter =
+ new LithoFilterParameters(conversionContext, spannableString, span, start, end, flags);
+
+ if (Settings.DEBUG_SPANNABLE.get()) {
+ Logger.printDebug(() -> "Searching...\n\u200B\n" + parameter);
+ }
+
+ return searchTree.matches(parameter.conversionContext, parameter);
+ } catch (Exception ex) {
+ Logger.printException(() -> "Spans filter failure", ex);
+ }
+
+ return false;
+ }
+
+ /**
+ * Injection point.
+ *
+ * @param spannableString Original SpannableString.
+ * @param span Span such as {@link ClickableSpan}, {@link ForegroundColorSpan},
+ * {@link AbsoluteSizeSpan}, {@link TypefaceSpan}, {@link ImageSpan}.
+ * @param start Start index of {@link Spannable#setSpan(Object, int, int, int)}.
+ * @param end End index of {@link Spannable#setSpan(Object, int, int, int)}.
+ * @param flags Flags of {@link Spannable#setSpan(Object, int, int, int)}.
+ */
+ public static void setSpan(SpannableString spannableString, Object span, int start, int end, int flags) {
+ if (returnEarly(spannableString, span, start, end, flags)) {
+ return;
+ }
+ spannableString.setSpan(span, start, end, flags);
+ }
+}
\ No newline at end of file
From e2ef914ed4e9c0e1c82a7418924521ac5e9ddeb2 Mon Sep 17 00:00:00 2001
From: ILoveOpenSouceApplications
<117499019+ILoveOpenSourceApplications@users.noreply.github.com>
Date: Mon, 4 May 2026 00:27:54 +0530
Subject: [PATCH 6/6] Revert "feat(YouTube): Introduce Litho text component and
span filtering framework"
This reverts commit f5e6736b0d86531a2294ea3a59ecc1408c5978cf.
---
.../youtube/patches/spans/Filter.java | 66 ------
.../youtube/patches/spans/FilterGroup.java | 93 --------
.../patches/spans/FilterGroupList.java | 79 -------
.../patches/spans/InclusiveSpanType.java | 221 ------------------
4 files changed, 459 deletions(-)
delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java
delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java
delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java
delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java
deleted file mode 100644
index 2afb2e95c..000000000
--- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/Filter.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 2026 Morphe.
- * https://github.com/MorpheApp/morphe-patches
- *
- * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
- */
-
-package app.morphe.extension.youtube.patches.spans;
-
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.text.SpannableString;
-import android.text.style.ImageSpan;
-import android.text.style.RelativeSizeSpan;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Filters litho based components.
- *
- * All callbacks must be registered before the constructor completes.
- */
-abstract class Filter {
- private static final RelativeSizeSpan relativeSizeSpanDummy = new RelativeSizeSpan(0f);
- private static final Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT);
- private static final ImageSpan imageSpanDummy = new ImageSpan(transparentDrawable);
-
- /**
- * Path callbacks. Do not add to this instance,
- * and instead use {@link #addCallbacks(StringFilterGroup...)}.
- */
- protected final List callbacks = new ArrayList<>();
-
- /**
- * Adds callbacks to {@link #skip(String, SpannableString, Object, int, int, int, boolean, SpanType, StringFilterGroup)}
- * if any of the groups are found.
- */
- protected final void addCallbacks(StringFilterGroup... groups) {
- callbacks.addAll(Arrays.asList(groups));
- }
-
- protected final void hideSpan(SpannableString spannableString, int start, int end, int flags) {
- spannableString.setSpan(relativeSizeSpanDummy, start, end, flags);
- }
-
- protected final void hideImageSpan(SpannableString spannableString, int start, int end, int flags) {
- spannableString.setSpan(imageSpanDummy, start, end, flags);
- }
-
- /**
- * Called after an enabled filter has been matched.
- * Default implementation is to always filter the matched component and log the action.
- * Subclasses can perform additional or different checks if needed.
- *
- * Method is called off the main thread.
- *
- * @param matchedGroup The actual filter that matched.
- */
- boolean skip(String conversionContext, SpannableString spannableString, Object span, int start, int end,
- int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) {
- return true;
- }
-}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java
deleted file mode 100644
index 09968ab2d..000000000
--- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroup.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright 2026 Morphe.
- * https://github.com/MorpheApp/morphe-patches
- *
- * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
- */
-
-package app.morphe.extension.youtube.patches.spans;
-
-import androidx.annotation.NonNull;
-
-import app.morphe.extension.shared.settings.BooleanSetting;
-
-abstract class FilterGroup {
- final static class FilterGroupResult {
- private BooleanSetting setting;
- private int matchedIndex;
-
- FilterGroupResult() {
- this(null, -1);
- }
-
- FilterGroupResult(BooleanSetting setting, int matchedIndex) {
- setValues(setting, matchedIndex);
- }
-
- public void setValues(BooleanSetting setting, int matchedIndex) {
- this.setting = setting;
- this.matchedIndex = matchedIndex;
- }
-
- public BooleanSetting getSetting() {
- return setting;
- }
-
- public boolean isFiltered() {
- return matchedIndex >= 0;
- }
- }
-
- protected final BooleanSetting setting;
- protected final T[] filters;
-
- @SafeVarargs
- public FilterGroup(final BooleanSetting setting, final T... filters) {
- this.setting = setting;
- this.filters = filters;
- if (filters.length == 0) {
- throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
- }
- }
-
- public boolean isEnabled() {
- return setting == null || setting.get();
- }
-
- @SuppressWarnings("BooleanMethodIsAlwaysInverted")
- public boolean includeInSearch() {
- return isEnabled() || !setting.rebootApp;
- }
-
- @NonNull
- @Override
- public String toString() {
- return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
- }
-
- public abstract FilterGroupResult check(final T stack);
-}
-
-class StringFilterGroup extends FilterGroup {
-
- public StringFilterGroup(final BooleanSetting setting, final String... filters) {
- super(setting, filters);
- }
-
- @Override
- public FilterGroupResult check(final String string) {
- int matchedIndex = -1;
- if (isEnabled()) {
- for (String pattern : filters) {
- if (!string.isEmpty()) {
- final int indexOf = string.indexOf(pattern);
- if (indexOf >= 0) {
- matchedIndex = indexOf;
- break;
- }
- }
- }
- }
- return new FilterGroupResult(setting, matchedIndex);
- }
-}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java
deleted file mode 100644
index a70134be7..000000000
--- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/FilterGroupList.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 2026 Morphe.
- * https://github.com/MorpheApp/morphe-patches
- *
- * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
- */
-
-package app.morphe.extension.youtube.patches.spans;
-
-import androidx.annotation.NonNull;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Spliterator;
-import java.util.function.Consumer;
-
-import app.morphe.extension.shared.StringTrieSearch;
-import app.morphe.extension.shared.TrieSearch;
-
-abstract class FilterGroupList> implements Iterable {
-
- private final List filterGroups = new ArrayList<>();
- private final TrieSearch search = createSearchGraph();
-
- @SafeVarargs
- protected final void addAll(final T... groups) {
- filterGroups.addAll(Arrays.asList(groups));
-
- for (T group : groups) {
- if (!group.includeInSearch()) {
- continue;
- }
- for (V pattern : group.filters) {
- search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
- if (group.isEnabled()) {
- FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
- result.setValues(group.setting, matchedStartIndex);
- return true;
- }
- return false;
- });
- }
- }
- }
-
- @NonNull
- @Override
- public Iterator iterator() {
- return filterGroups.iterator();
- }
-
- @Override
- public void forEach(@NonNull Consumer super T> action) {
- filterGroups.forEach(action);
- }
-
- @NonNull
- @Override
- public Spliterator spliterator() {
- return filterGroups.spliterator();
- }
-
- protected FilterGroup.FilterGroupResult check(V stack) {
- FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
- search.matches(stack, result);
- return result;
- }
-
- protected abstract TrieSearch createSearchGraph();
-}
-
-final class StringFilterGroupList extends FilterGroupList {
- @Override
- protected StringTrieSearch createSearchGraph() {
- return new StringTrieSearch();
- }
-}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java
deleted file mode 100644
index 18f368a93..000000000
--- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spans/InclusiveSpanType.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright 2026 Morphe.
- * https://github.com/MorpheApp/morphe-patches
- *
- * See the included NOTICE file for GPLv3 §7(b) and §7(c) terms that apply to Morphe contributions.
- */
-
-package app.morphe.extension.youtube.patches.spans;
-
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.style.AbsoluteSizeSpan;
-import android.text.style.CharacterStyle;
-import android.text.style.ClickableSpan;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.ImageSpan;
-import android.text.style.LineHeightSpan;
-import android.text.style.TypefaceSpan;
-
-import androidx.annotation.NonNull;
-
-import java.util.List;
-
-import app.morphe.extension.shared.Logger;
-import app.morphe.extension.shared.StringTrieSearch;
-import app.morphe.extension.youtube.settings.Settings;
-
-/**
- * Placeholder for actual filters.
- */
-final class DummyFilter extends Filter {
-}
-
-@SuppressWarnings("unused")
-public final class InclusiveSpanType {
-
- /**
- * Simple wrapper to pass the litho parameters through the prefix search.
- */
- private static final class LithoFilterParameters {
- final String conversionContext;
- final SpannableString spannableString;
- final Object span;
- final int start;
- final int end;
- final int flags;
- final String originalString;
- final int originalLength;
- final SpanType spanType;
- final boolean isWord;
-
- public LithoFilterParameters(String conversionContext, SpannableString spannableString,
- Object span, int start, int end, int flags) {
- this.conversionContext = conversionContext;
- this.spannableString = spannableString;
- this.span = span;
- this.start = start;
- this.end = end;
- this.flags = flags;
- this.originalString = spannableString.toString();
- this.originalLength = spannableString.length();
- this.spanType = getSpanType(span);
- this.isWord = !(start == 0 && end == originalLength);
- }
-
- @NonNull
- @Override
- public String toString() {
- StringBuilder builder = new StringBuilder();
- builder.append("CharSequence:'")
- .append(originalString)
- .append("'\nSpanType:'")
- .append(getSpanType(spanType, span))
- .append("'\nLength:'")
- .append(originalLength)
- .append("'\nStart:'")
- .append(start)
- .append("'\nEnd:'")
- .append(end)
- .append("'\nisWord:'")
- .append(isWord)
- .append("'");
- if (isWord) {
- builder.append("\nWord:'")
- .append(originalString.substring(start, end))
- .append("'");
- }
- return builder.toString();
- }
- }
-
- private static SpanType getSpanType(Object span) {
- if (span instanceof ClickableSpan) {
- return SpanType.CLICKABLE;
- } else if (span instanceof ForegroundColorSpan) {
- return SpanType.FOREGROUND_COLOR;
- } else if (span instanceof AbsoluteSizeSpan) {
- return SpanType.ABSOLUTE_SIZE;
- } else if (span instanceof TypefaceSpan) {
- return SpanType.TYPEFACE;
- } else if (span instanceof ImageSpan) {
- return SpanType.IMAGE;
- } else if (span instanceof LineHeightSpan) {
- return SpanType.LINE_HEIGHT;
- } else if (span instanceof CharacterStyle) { // Replaced by patch.
- return SpanType.CUSTOM_CHARACTER_STYLE;
- } else {
- return SpanType.UNKNOWN;
- }
- }
-
- private static String getSpanType(SpanType spanType, Object span) {
- return spanType == SpanType.UNKNOWN
- ? span.getClass().getSimpleName()
- : spanType.type;
- }
-
- private static final Filter[] filters = new Filter[]{
- new DummyFilter() // Replaced by patch.
- };
-
- private static final StringTrieSearch searchTree = new StringTrieSearch();
-
- /**
- * Because litho filtering is multi-threaded and the buffer is passed in from a different injection point,
- * the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
- */
- private static final ThreadLocal conversionContextThreadLocal = new ThreadLocal<>();
-
- static {
- for (Filter filter : filters) {
- filterUsingCallbacks(filter, filter.callbacks);
- }
-
- if (Settings.DEBUG_SPANNABLE.get()) {
- Logger.printDebug(() -> "Using: "
- + searchTree.numberOfPatterns() + " conversion context filters"
- + " (" + searchTree.getEstimatedMemorySize() + " KB)");
- }
- }
-
- private static void filterUsingCallbacks(Filter filter, List groups) {
- String filterSimpleName = filter.getClass().getSimpleName();
-
- for (StringFilterGroup group : groups) {
- if (!group.includeInSearch()) {
- continue;
- }
-
- for (String pattern : group.filters) {
- InclusiveSpanType.searchTree.addPattern(pattern, (textSearched, matchedStartIndex,
- matchedLength, callbackParameter) -> {
- if (!group.isEnabled()) return false;
-
- LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
- final boolean isFiltered = filter.skip(parameters.conversionContext, parameters.spannableString,
- parameters.span, parameters.start, parameters.end, parameters.flags, parameters.isWord,
- parameters.spanType, group);
-
- if (isFiltered && Settings.DEBUG_SPANNABLE.get()) {
- Logger.printDebug(() -> "Removed " + filterSimpleName
- + " setSpan: " + parameters.spanType);
- }
-
- return isFiltered;
- }
- );
- }
- }
- }
-
- /**
- * Injection point.
- *
- * @param conversionContext ConversionContext is used to identify whether it is a comment thread or not.
- */
- public static CharSequence setConversionContext(@NonNull Object conversionContext,
- @NonNull CharSequence original) {
- conversionContextThreadLocal.set(conversionContext.toString());
- return original;
- }
-
- private static boolean returnEarly(SpannableString spannableString, Object span, int start, int end, int flags) {
- try {
- final String conversionContext = conversionContextThreadLocal.get();
- if (conversionContext == null || conversionContext.isEmpty()) {
- return false;
- }
-
- LithoFilterParameters parameter =
- new LithoFilterParameters(conversionContext, spannableString, span, start, end, flags);
-
- if (Settings.DEBUG_SPANNABLE.get()) {
- Logger.printDebug(() -> "Searching...\n\u200B\n" + parameter);
- }
-
- return searchTree.matches(parameter.conversionContext, parameter);
- } catch (Exception ex) {
- Logger.printException(() -> "Spans filter failure", ex);
- }
-
- return false;
- }
-
- /**
- * Injection point.
- *
- * @param spannableString Original SpannableString.
- * @param span Span such as {@link ClickableSpan}, {@link ForegroundColorSpan},
- * {@link AbsoluteSizeSpan}, {@link TypefaceSpan}, {@link ImageSpan}.
- * @param start Start index of {@link Spannable#setSpan(Object, int, int, int)}.
- * @param end End index of {@link Spannable#setSpan(Object, int, int, int)}.
- * @param flags Flags of {@link Spannable#setSpan(Object, int, int, int)}.
- */
- public static void setSpan(SpannableString spannableString, Object span, int start, int end, int flags) {
- if (returnEarly(spannableString, span, start, end, flags)) {
- return;
- }
- spannableString.setSpan(span, start, end, flags);
- }
-}
\ No newline at end of file