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 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 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 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