feat(Strava): Add Hide distractions patch (#6479)

Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
Co-authored-by: ekaunt <62402760+ekaunt@users.noreply.github.com>
Co-authored-by: bengross <bengross@vecta.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
xehpuk 2026-01-22 19:29:33 +01:00 committed by oSumAtrIX
parent 421cb2899e
commit 66b0852f8f
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
16 changed files with 601 additions and 56 deletions

View file

@ -1236,6 +1236,10 @@ public final class app/revanced/patches/stocard/layout/HideStoryBubblesPatchKt {
public static final fun getHideStoryBubblesPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
public final class app/revanced/patches/strava/distractions/HideDistractionsPatchKt {
public static final fun getHideDistractionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/strava/groupkudos/AddGiveGroupKudosButtonToGroupActivityKt {
public static final fun getAddGiveGroupKudosButtonToGroupActivity ()Lapp/revanced/patcher/patch/BytecodePatch;
}

View file

@ -0,0 +1,191 @@
package app.revanced.patches.strava.distractions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.booleanOption
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableBooleanEncodedValue.Companion.toMutable
import app.revanced.patches.strava.misc.extension.sharedExtensionPatch
import app.revanced.util.findMutableMethodOf
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference
import com.android.tools.smali.dexlib2.immutable.value.ImmutableBooleanEncodedValue
import com.android.tools.smali.dexlib2.util.MethodUtil
import java.util.logging.Logger
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/strava/HideDistractionsPatch;"
private const val MODULAR_FRAMEWORK_CLASS_DESCRIPTOR_PREFIX = "Lcom/strava/modularframework"
private const val METHOD_SUFFIX = "\$original"
private data class FilterablePropertyFingerprint(
val name: String,
val parameterTypes: List<String> = listOf(),
)
private val fingerprints = arrayOf(
FilterablePropertyFingerprint("ChildrenEntries"),
FilterablePropertyFingerprint("Entries"),
FilterablePropertyFingerprint("Field", listOf("Ljava/lang/String;")),
FilterablePropertyFingerprint("Fields"),
FilterablePropertyFingerprint("MenuItems"),
FilterablePropertyFingerprint("Modules"),
FilterablePropertyFingerprint("Properties"),
FilterablePropertyFingerprint("StateMap"),
FilterablePropertyFingerprint("Submodules"),
)
@Suppress("unused")
val hideDistractionsPatch = bytecodePatch(
name = "Hide distractions",
description = "Hides elements that are not essential.",
) {
compatibleWith("com.strava")
dependsOn(sharedExtensionPatch)
val logger = Logger.getLogger(this::class.java.name)
val options = arrayOf(
booleanOption(
key = "upselling",
title = "Upselling",
description = "Elements that suggest you subscribe.",
default = true,
required = true,
),
booleanOption(
key = "promo",
title = "Promotions",
default = true,
required = true,
),
booleanOption(
key = "followSuggestions",
title = "Who to Follow",
description = "Popular athletes, followers, people near you etc.",
default = true,
required = true,
),
booleanOption(
key = "challengeSuggestions",
title = "Suggested Challenges",
description = "Random challenges Strava wants you to join.",
default = true,
required = true,
),
booleanOption(
key = "joinChallenge",
title = "Join Challenge",
description = "Challenges your follows have joined.",
default = false,
required = true,
),
booleanOption(
key = "joinClub",
title = "Joined a club",
description = "Clubs your follows have joined.",
default = false,
required = true,
),
booleanOption(
key = "activityLookback",
title = "Your activity from X years ago",
default = false,
required = true,
),
)
execute {
// region Write option values into extension class.
val extensionClass = classBy { it.type == EXTENSION_CLASS_DESCRIPTOR }!!.mutableClass.apply {
options.forEach { option ->
staticFields.first { field -> field.name == option.key }.initialValue =
ImmutableBooleanEncodedValue.forBoolean(option.value == true).toMutable()
}
}
// endregion
// region Intercept all classes' property getter calls.
fun MutableMethod.cloneAndIntercept(
classDef: MutableClass,
extensionMethodName: String,
extensionMethodParameterTypes: List<String>,
) {
val extensionMethodReference = ImmutableMethodReference(
EXTENSION_CLASS_DESCRIPTOR,
extensionMethodName,
extensionMethodParameterTypes,
returnType,
)
if (extensionClass.directMethods.none { method ->
MethodUtil.methodSignaturesMatch(method, extensionMethodReference)
}) {
logger.info { "Skipped interception of $this due to missing $extensionMethodReference" }
return
}
classDef.virtualMethods -= this
val clone = ImmutableMethod.of(this).toMutable()
classDef.virtualMethods += clone
if (implementation != null) {
val registers = List(extensionMethodParameterTypes.size) { index -> "p$index" }.joinToString(
separator = ",",
prefix = "{",
postfix = "}",
)
clone.addInstructions(
0,
"""
invoke-static $registers, $extensionMethodReference
move-result-object v0
return-object v0
"""
)
logger.fine { "Intercepted $this with $extensionMethodReference" }
}
name += METHOD_SUFFIX
classDef.virtualMethods += this
}
classes.filter { it.type.startsWith(MODULAR_FRAMEWORK_CLASS_DESCRIPTOR_PREFIX) }.forEach { classDef ->
val classDefProxy by lazy { proxy(classDef) }
classDef.virtualMethods.forEach { method ->
fingerprints.find { fingerprint ->
method.name == "get${fingerprint.name}" && method.parameterTypes == fingerprint.parameterTypes
}?.let { fingerprint ->
classDefProxy.mutableClass.let { mutableClass ->
// Upcast to the interface if this is an interface implementation.
val parameterType = classDef.interfaces.find {
classes.find { interfaceDef -> interfaceDef.type == it }?.virtualMethods?.any { interfaceMethod ->
MethodUtil.methodSignaturesMatch(interfaceMethod, method)
} == true
} ?: classDef.type
mutableClass.findMutableMethodOf(method).cloneAndIntercept(
mutableClass,
"filter${fingerprint.name}",
listOf(parameterType) + fingerprint.parameterTypes
)
}
}
}
}
// endregion
}
}

View file

@ -1,67 +1,22 @@
package app.revanced.patches.strava.upselling
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import app.revanced.patches.strava.distractions.hideDistractionsPatch
@Suppress("unused")
@Deprecated("Superseded by \"Hide distractions\" patch", ReplaceWith("hideDistractionsPatch"))
val disableSubscriptionSuggestionsPatch = bytecodePatch(
name = "Disable subscription suggestions",
) {
compatibleWith("com.strava")
execute {
val helperMethodName = "getModulesIfNotUpselling"
val pageSuffix = "_upsell"
val label = "original"
val className = getModulesFingerprint.originalClassDef.type
val originalMethod = getModulesFingerprint.method
val returnType = originalMethod.returnType
getModulesFingerprint.classDef.methods.add(
ImmutableMethod(
className,
helperMethodName,
emptyList(),
returnType,
AccessFlags.PRIVATE.value,
null,
null,
MutableMethodImplementation(3),
).toMutable().apply {
addInstructions(
"""
iget-object v0, p0, $className->page:Ljava/lang/String;
const-string v1, "$pageSuffix"
invoke-virtual {v0, v1}, Ljava/lang/String;->endsWith(Ljava/lang/String;)Z
move-result v0
if-eqz v0, :$label
invoke-static {}, Ljava/util/Collections;->emptyList()Ljava/util/List;
move-result-object v0
return-object v0
:$label
iget-object v0, p0, $className->modules:Ljava/util/List;
return-object v0
""",
)
},
)
val getModulesIndex = getModulesFingerprint.patternMatch!!.startIndex
with(originalMethod) {
removeInstruction(getModulesIndex)
addInstructions(
getModulesIndex,
"""
invoke-direct {p0}, $className->$helperMethodName()$returnType
move-result-object v0
""",
)
}
}
dependsOn(hideDistractionsPatch.apply {
options["upselling"] = true
options["promo"] = false
options["followSuggestions"] = false
options["challengeSuggestions"] = false
options["joinChallenge"] = false
options["joinClub"] = false
options["activityLookback"] = false
})
}