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
16 changed files with 601 additions and 56 deletions

View File

@@ -0,0 +1,227 @@
package app.revanced.extension.strava;
import android.annotation.SuppressLint;
import com.strava.modularframework.data.Destination;
import com.strava.modularframework.data.GenericLayoutModule;
import com.strava.modularframework.data.GenericModuleField;
import com.strava.modularframework.data.ListField;
import com.strava.modularframework.data.ListProperties;
import com.strava.modularframework.data.ModularComponent;
import com.strava.modularframework.data.ModularEntry;
import com.strava.modularframework.data.ModularEntryContainer;
import com.strava.modularframework.data.ModularMenuItem;
import com.strava.modularframework.data.Module;
import com.strava.modularframework.data.MultiStateFieldDescriptor;
import com.strava.modularframeworknetwork.ModularEntryNetworkContainer;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@SuppressLint("NewApi")
public class HideDistractionsPatch {
public static boolean upselling;
public static boolean promo;
public static boolean followSuggestions;
public static boolean challengeSuggestions;
public static boolean joinChallenge;
public static boolean joinClub;
public static boolean activityLookback;
public static List<ModularEntry> filterChildrenEntries(ModularEntry modularEntry) {
if (hideModularEntry(modularEntry)) {
return Collections.emptyList();
}
return modularEntry.getChildrenEntries$original().stream()
.filter(childrenEntry -> !hideModularEntry(childrenEntry))
.collect(Collectors.toList());
}
public static List<ModularEntry> filterEntries(ModularEntryContainer modularEntryContainer) {
if (hideModularEntryContainer(modularEntryContainer)) {
return Collections.emptyList();
}
return modularEntryContainer.getEntries$original().stream()
.filter(entry -> !hideModularEntry(entry))
.collect(Collectors.toList());
}
public static List<ModularEntry> filterEntries(ModularEntryNetworkContainer modularEntryNetworkContainer) {
if (hideModularEntryNetworkContainer(modularEntryNetworkContainer)) {
return Collections.emptyList();
}
return modularEntryNetworkContainer.getEntries$original().stream()
.filter(entry -> !hideModularEntry(entry))
.collect(Collectors.toList());
}
public static List<ModularMenuItem> filterMenuItems(ModularEntryContainer modularEntryContainer) {
if (hideModularEntryContainer(modularEntryContainer)) {
return Collections.emptyList();
}
return modularEntryContainer.getMenuItems$original().stream()
.filter(menuItem -> !hideModularMenuItem(menuItem))
.collect(Collectors.toList());
}
public static ListProperties filterProperties(ModularEntryContainer modularEntryContainer) {
if (hideModularEntryContainer(modularEntryContainer)) {
return null;
}
return modularEntryContainer.getProperties$original();
}
public static ListProperties filterProperties(ModularEntryNetworkContainer modularEntryNetworkContainer) {
if (hideModularEntryNetworkContainer(modularEntryNetworkContainer)) {
return null;
}
return modularEntryNetworkContainer.getProperties$original();
}
public static ListField filterField(ListProperties listProperties, String key) {
ListField listField = listProperties.getField$original(key);
if (hideListField(listField)) {
return null;
}
return listField;
}
public static List<ListField> filterFields(ListField listField) {
if (hideListField(listField)) {
return null;
}
return listField.getFields$original().stream()
.filter(field -> !hideListField(field))
.collect(Collectors.toList());
}
public static List<Module> filterModules(ModularEntry modularEntry) {
if (hideModularEntry(modularEntry)) {
return Collections.emptyList();
}
return modularEntry.getModules$original().stream()
.filter(module -> !hideModule(module))
.collect(Collectors.toList());
}
public static GenericModuleField filterField(GenericLayoutModule genericLayoutModule, String key) {
if (hideGenericLayoutModule(genericLayoutModule)) {
return null;
}
GenericModuleField field = genericLayoutModule.getField$original(key);
if (hideGenericModuleField(field)) {
return null;
}
return field;
}
public static GenericModuleField[] filterFields(GenericLayoutModule genericLayoutModule) {
if (hideGenericLayoutModule(genericLayoutModule)) {
return new GenericModuleField[0];
}
return Arrays.stream(genericLayoutModule.getFields$original())
.filter(field -> !hideGenericModuleField(field))
.toArray(GenericModuleField[]::new);
}
public static GenericLayoutModule[] filterSubmodules(GenericLayoutModule genericLayoutModule) {
if (hideGenericLayoutModule(genericLayoutModule)) {
return new GenericLayoutModule[0];
}
return Arrays.stream(genericLayoutModule.getSubmodules$original())
.filter(submodule -> !hideGenericLayoutModule(submodule))
.toArray(GenericLayoutModule[]::new);
}
public static List<Module> filterSubmodules(ModularComponent modularComponent) {
if (hideByName(modularComponent.getPage()) || hideByName(modularComponent.getElement())) {
return Collections.emptyList();
}
return modularComponent.getSubmodules$original().stream()
.filter(submodule -> !hideModule(submodule))
.collect(Collectors.toList());
}
public static Map<String, GenericModuleField> filterStateMap(MultiStateFieldDescriptor multiStateFieldDescriptor) {
return multiStateFieldDescriptor.getStateMap$original().entrySet().stream()
.filter(entry -> !hideGenericModuleField(entry.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private static boolean hideModule(Module module) {
return module == null ||
hideByName(module.getPage()) ||
hideByName(module.getElement());
}
private static boolean hideModularEntry(ModularEntry modularEntry) {
return modularEntry == null ||
hideByName(modularEntry.getPage()) ||
hideByName(modularEntry.getElement()) ||
hideByDestination(modularEntry.getDestination());
}
private static boolean hideGenericLayoutModule(GenericLayoutModule genericLayoutModule) {
try {
return genericLayoutModule == null ||
hideByName(genericLayoutModule.getPage()) ||
hideByName(genericLayoutModule.getElement()) ||
hideByDestination(genericLayoutModule.getDestination());
} catch (RuntimeException getParentEntryOrThrowException) {
return false;
}
}
private static boolean hideListField(ListField listField) {
return listField == null ||
hideByName(listField.getElement()) ||
hideByDestination(listField.getDestination());
}
private static boolean hideGenericModuleField(GenericModuleField genericModuleField) {
return genericModuleField == null ||
hideByName(genericModuleField.getElement()) ||
hideByDestination(genericModuleField.getDestination());
}
private static boolean hideModularEntryContainer(ModularEntryContainer modularEntryContainer) {
return modularEntryContainer == null ||
hideByName(modularEntryContainer.getPage());
}
private static boolean hideModularEntryNetworkContainer(ModularEntryNetworkContainer modularEntryNetworkContainer) {
return modularEntryNetworkContainer == null ||
hideByName(modularEntryNetworkContainer.getPage());
}
private static boolean hideModularMenuItem(ModularMenuItem modularMenuItem) {
return modularMenuItem == null ||
hideByName(modularMenuItem.getElementName()) ||
hideByDestination(modularMenuItem.getDestination());
}
private static boolean hideByName(String name) {
return name != null && (
upselling && name.contains("_upsell") ||
promo && (name.equals("promo") || name.equals("top_of_tab_promo")) ||
followSuggestions && name.equals("suggested_follows") ||
challengeSuggestions && name.equals("suggested_challenges") ||
joinChallenge && name.equals("challenge") ||
joinClub && name.equals("club") ||
activityLookback && name.equals("highlighted_activity_lookback")
);
}
private static boolean hideByDestination(Destination destination) {
if (destination == null) {
return false;
}
String url = destination.getUrl();
return url != null && (
upselling && url.startsWith("strava://subscription/checkout")
);
}
}

View File

@@ -0,0 +1,7 @@
package com.strava.modularframework.data;
import java.io.Serializable;
public abstract class Destination implements Serializable {
public abstract String getUrl();
}

View File

@@ -0,0 +1,28 @@
package com.strava.modularframework.data;
import java.io.Serializable;
public abstract class GenericLayoutModule implements Serializable, Module {
public abstract Destination getDestination();
@Override
public abstract String getElement();
public abstract GenericModuleField getField(String key);
// Added by patch.
public abstract GenericModuleField getField$original(String key);
public abstract GenericModuleField[] getFields();
// Added by patch.
public abstract GenericModuleField[] getFields$original();
@Override
public abstract String getPage();
public abstract GenericLayoutModule[] getSubmodules();
// Added by patch.
public abstract GenericLayoutModule[] getSubmodules$original();
}

View File

@@ -0,0 +1,9 @@
package com.strava.modularframework.data;
import java.io.Serializable;
public abstract class GenericModuleField implements Serializable {
public abstract Destination getDestination();
public abstract String getElement();
}

View File

@@ -0,0 +1,14 @@
package com.strava.modularframework.data;
import java.util.List;
public abstract class ListField {
public abstract Destination getDestination();
public abstract String getElement();
public abstract List<ListField> getFields();
// Added by patch.
public abstract List<ListField> getFields$original();
}

View File

@@ -0,0 +1,8 @@
package com.strava.modularframework.data;
public abstract class ListProperties {
public abstract ListField getField(String key);
// Added by patch.
public abstract ListField getField$original(String key);
}

View File

@@ -0,0 +1,16 @@
package com.strava.modularframework.data;
import java.util.List;
public abstract class ModularComponent implements Module {
@Override
public abstract String getElement();
@Override
public abstract String getPage();
public abstract List<Module> getSubmodules();
// Added by patch.
public abstract List<Module> getSubmodules$original();
}

View File

@@ -0,0 +1,21 @@
package com.strava.modularframework.data;
import java.util.List;
public interface ModularEntry {
List<ModularEntry> getChildrenEntries();
// Added by patch.
List<ModularEntry> getChildrenEntries$original();
Destination getDestination();
String getElement();
List<Module> getModules();
// Added by patch.
List<Module> getModules$original();
String getPage();
}

View File

@@ -0,0 +1,22 @@
package com.strava.modularframework.data;
import java.util.List;
public abstract class ModularEntryContainer {
public abstract List<ModularEntry> getEntries();
// Added by patch.
public abstract List<ModularEntry> getEntries$original();
public abstract List<ModularMenuItem> getMenuItems();
// Added by patch.
public abstract List<ModularMenuItem> getMenuItems$original();
public abstract String getPage();
public abstract ListProperties getProperties();
// Added by patch.
public abstract ListProperties getProperties$original();
}

View File

@@ -0,0 +1,7 @@
package com.strava.modularframework.data;
public abstract class ModularMenuItem {
public abstract Destination getDestination();
public abstract String getElementName();
}

View File

@@ -0,0 +1,7 @@
package com.strava.modularframework.data;
public interface Module {
String getElement();
String getPage();
}

View File

@@ -0,0 +1,10 @@
package com.strava.modularframework.data;
import java.util.Map;
public abstract class MultiStateFieldDescriptor {
public abstract Map<String, GenericModuleField> getStateMap();
// Added by patch.
public abstract Map<String, GenericModuleField> getStateMap$original();
}

View File

@@ -0,0 +1,19 @@
package com.strava.modularframeworknetwork;
import com.strava.modularframework.data.ListProperties;
import com.strava.modularframework.data.ModularEntry;
import java.util.List;
public abstract class ModularEntryNetworkContainer {
public abstract List<ModularEntry> getEntries();
// Added by patch.
public abstract List<ModularEntry> getEntries$original();
public abstract String getPage();
public abstract ListProperties getProperties();
// Added by patch.
public abstract ListProperties getProperties$original();
}

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