diff --git a/src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/patch/DisableBlogNotificationReminderPatch.kt b/src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/DisableBlogNotificationReminderPatch.kt similarity index 94% rename from src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/patch/DisableBlogNotificationReminderPatch.kt rename to src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/DisableBlogNotificationReminderPatch.kt index 5003a372..e073da5f 100644 --- a/src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/patch/DisableBlogNotificationReminderPatch.kt +++ b/src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/DisableBlogNotificationReminderPatch.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.tumblr.annoyances.notifications.patch +package app.revanced.patches.tumblr.annoyances.notifications import app.revanced.extensions.exception import app.revanced.patcher.data.BytecodeContext diff --git a/src/main/kotlin/app/revanced/patches/tumblr/annoyances/popups/patch/DisableGiftMessagePopupPatch.kt b/src/main/kotlin/app/revanced/patches/tumblr/annoyances/popups/DisableGiftMessagePopupPatch.kt similarity index 94% rename from src/main/kotlin/app/revanced/patches/tumblr/annoyances/popups/patch/DisableGiftMessagePopupPatch.kt rename to src/main/kotlin/app/revanced/patches/tumblr/annoyances/popups/DisableGiftMessagePopupPatch.kt index f93597ca..17c15a79 100644 --- a/src/main/kotlin/app/revanced/patches/tumblr/annoyances/popups/patch/DisableGiftMessagePopupPatch.kt +++ b/src/main/kotlin/app/revanced/patches/tumblr/annoyances/popups/DisableGiftMessagePopupPatch.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.tumblr.annoyances.popups.patch +package app.revanced.patches.tumblr.annoyances.popups import app.revanced.extensions.exception import app.revanced.patcher.data.BytecodeContext diff --git a/src/main/kotlin/app/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch.kt b/src/main/kotlin/app/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch.kt new file mode 100644 index 00000000..77202534 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch.kt @@ -0,0 +1,116 @@ +package app.revanced.patches.tumblr.featureflags + +import app.revanced.extensions.exception +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.or +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patches.tumblr.featureflags.fingerprints.GetFeatureValueFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter + +@Patch(description = "Forcibly set the value of A/B testing features of your choice.") +object OverrideFeatureFlagsPatch : BytecodePatch( + setOf(GetFeatureValueFingerprint) +) { + /** + * Override a feature flag with a value. + * + * @param name The name of the feature flag to override. + * @param value The value to override the feature flag with. + */ + @Suppress("KDocUnresolvedReference") + internal lateinit var addOverride: (name: String, value: String) -> Unit private set + + override fun execute(context: BytecodeContext) = GetFeatureValueFingerprint.result?.let { + // The method we want to inject into does not have enough registers, so we inject a helper method + // and inject more instructions into it later, see addOverride. + // This is not in an integration since the unused variable would get compiled away and the method would + // get compiled to only have one register, which is not enough for our later injected instructions. + val helperMethod = ImmutableMethod( + it.method.definingClass, + "getValueOverride", + listOf(ImmutableMethodParameter("Lcom/tumblr/configuration/Feature;", null, "feature")), + "Ljava/lang/String;", + AccessFlags.PUBLIC or AccessFlags.FINAL, + null, + null, + MutableMethodImplementation(4) + ).toMutable().apply { + // This is the equivalent of + // String featureName = feature.toString() + // + // return null + addInstructions( + 0, + """ + # toString() the enum value + invoke-virtual {p1}, Lcom/tumblr/configuration/Feature;->toString()Ljava/lang/String; + move-result-object v0 + + # !!! If you add more instructions above this line, update helperInsertIndex below! + # Here we will insert one compare & return for every registered Feature override + # This is done below in the addOverride function + + # If none of the overrides returned a value, we should return null + const/4 v0, 0x0 + return-object v0 + """ + ) + }.also { helperMethod -> + it.mutableClass.methods.add(helperMethod) + } + + // Here we actually insert the hook to call our helper method and return its value if it returns not null + // This is equivalent to + // String forcedValue = getValueOverride(feature) + // if (forcedValue != null) return forcedValue + val getFeatureIndex = it.scanResult.patternScanResult!!.startIndex + it.mutableMethod.addInstructionsWithLabels( + getFeatureIndex, + """ + # Call the Helper Method with the Feature + invoke-virtual {p0, p1}, Lcom/tumblr/configuration/Configuration;->getValueOverride(Lcom/tumblr/configuration/Feature;)Ljava/lang/String; + move-result-object v0 + # If it returned null, skip + if-eqz v0, :is_null + # If it didnt return null, return that string + return-object v0 + + # If our override helper returned null, we let the function continue normally + :is_null + nop + """ + ) + + val helperInsertIndex = 2 + addOverride = { name, value -> + // For every added override, we add a few instructions in the middle of the helper method + // to check if the feature is the one we want and return the override value if it is. + // This is equivalent to + // if (featureName == {name}) return {value} + helperMethod.addInstructionsWithLabels( + helperInsertIndex, + """ + # v0 is still the string name of the currently checked feature from above + # Compare the current string with the override string + const-string v1, "$name" + invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z + move-result v1 + # If the current string is the one we want to override, we return the override value + if-eqz v1, :no_override + const-string v1, "$value" + return-object v1 + # Else we just continue... + :no_override + nop + """ + ) + } + } ?: throw GetFeatureValueFingerprint.exception +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/tumblr/featureflags/fingerprints/GetFeatureValueFingerprint.kt b/src/main/kotlin/app/revanced/patches/tumblr/featureflags/fingerprints/GetFeatureValueFingerprint.kt new file mode 100644 index 00000000..1507b5dd --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/tumblr/featureflags/fingerprints/GetFeatureValueFingerprint.kt @@ -0,0 +1,23 @@ +package app.revanced.patches.tumblr.featureflags.fingerprints + +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import com.android.tools.smali.dexlib2.Opcode + +// This fingerprint targets the method to get the value of a Feature in the class "com.tumblr.configuration.Feature". +// Features seem to be Tumblr's A/B testing program. +// Feature states are loaded from the server in the "api-http2.tumblr.com/v2/config" request on (first) startup. +// A lot of features are returned there, but most of them do not seem to do anything (anymore). +// They were likely removed in newer App versions to always be on, but are still returned +// as enabled for old App versions. +// Some features seem to be very old and never removed, though, such as Google Login. +// The startIndex of the opcode pattern is at the start of the function after the arg null check. +// we want to insert our instructions there. +object GetFeatureValueFingerprint : MethodFingerprint( + strings = listOf("feature"), + opcodes = listOf( + Opcode.IF_EQZ, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT + ), + customFingerprint = { method, _ -> method.definingClass == "Lcom/tumblr/configuration/Configuration;" } +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/tumblr/live/DisableTumblrLivePatch.kt b/src/main/kotlin/app/revanced/patches/tumblr/live/DisableTumblrLivePatch.kt new file mode 100644 index 00000000..f588d02a --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/tumblr/live/DisableTumblrLivePatch.kt @@ -0,0 +1,37 @@ +package app.revanced.patches.tumblr.live + +import app.revanced.extensions.exception +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.annotation.CompatiblePackage +import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patches.tumblr.featureflags.OverrideFeatureFlagsPatch +import app.revanced.patches.tumblr.live.fingerprints.LiveMarqueeFingerprint +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +@Patch( + name = "Disable Tumblr Live", + description = "Disable the Tumblr Live tab button and dashboard carousel.", + dependencies = [OverrideFeatureFlagsPatch::class], + compatiblePackages = [CompatiblePackage("com.tumblr")] +) +@Suppress("unused") +object DisableTumblrLivePatch : BytecodePatch( + setOf(LiveMarqueeFingerprint) +) { + override fun execute(context: BytecodeContext) = LiveMarqueeFingerprint.result?.let { + it.scanResult.stringsScanResult!!.matches.forEach { match -> + // Replace the string constant "live_marquee" + // with a dummy so the app doesn't recognize this type of element in the Dashboard and skips it + it.mutableMethod.apply { + val stringRegister = getInstruction(match.index).registerA + replaceInstruction(match.index, "const-string v$stringRegister, \"dummy2\"") + } + } + + // We hide the Tab button for Tumblr Live by forcing the feature flag to false + OverrideFeatureFlagsPatch.addOverride("liveStreaming", "false") + } ?: throw LiveMarqueeFingerprint.exception +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/tumblr/live/fingerprints/LiveMarqueeFingerprint.kt b/src/main/kotlin/app/revanced/patches/tumblr/live/fingerprints/LiveMarqueeFingerprint.kt new file mode 100644 index 00000000..3f4da4b0 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/tumblr/live/fingerprints/LiveMarqueeFingerprint.kt @@ -0,0 +1,6 @@ +package app.revanced.patches.tumblr.live.fingerprints + +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint + +// This works identically to the Tumblr AdWaterfallFingerprint, see comments there +object LiveMarqueeFingerprint : MethodFingerprint(strings = listOf("live_marquee")) \ No newline at end of file