Skip to content

Commit 4c4ba1c

Browse files
xehpukoSumAtrIX
andauthored
feat(Strava): Add Add 'Give Kudos' button to 'Group Activity' patch (#6475)
Co-authored-by: oSumAtrIX <[email protected]>
1 parent 7cef24a commit 4c4ba1c

File tree

3 files changed

+219
-0
lines changed

3 files changed

+219
-0
lines changed

patches/api/patches.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,10 @@ public final class app/revanced/patches/stocard/layout/HideStoryBubblesPatchKt {
12041204
public static final fun getHideStoryBubblesPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
12051205
}
12061206

1207+
public final class app/revanced/patches/strava/groupkudos/AddGiveGroupKudosButtonToGroupActivityKt {
1208+
public static final fun getAddGiveGroupKudosButtonToGroupActivity ()Lapp/revanced/patcher/patch/BytecodePatch;
1209+
}
1210+
12071211
public final class app/revanced/patches/strava/media/download/AddMediaDownloadPatchKt {
12081212
public static final fun getAddMediaDownloadPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
12091213
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package app.revanced.patches.strava.groupkudos
2+
3+
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
4+
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
5+
import app.revanced.patcher.extensions.InstructionExtensions.instructions
6+
import app.revanced.patcher.patch.bytecodePatch
7+
import app.revanced.patcher.patch.resourcePatch
8+
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
9+
import app.revanced.util.childElementsSequence
10+
import app.revanced.util.findElementByAttributeValueOrThrow
11+
import app.revanced.util.getReference
12+
import com.android.tools.smali.dexlib2.AccessFlags.*
13+
import com.android.tools.smali.dexlib2.Opcode
14+
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
15+
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction11x
16+
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
17+
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction31i
18+
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction35c
19+
import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction
20+
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
21+
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
22+
import com.android.tools.smali.dexlib2.immutable.ImmutableField
23+
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
24+
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
25+
import org.w3c.dom.Element
26+
27+
private const val VIEW_CLASS_DESCRIPTOR = "Landroid/view/View;"
28+
private const val ON_CLICK_LISTENER_CLASS_DESCRIPTOR = "Landroid/view/View\$OnClickListener;"
29+
30+
private var shakeToKudosStringId = -1
31+
private var kudosIdId = -1
32+
private var leaveIdId = -1
33+
34+
private val addGiveKudosButtonToLayoutPatch = resourcePatch {
35+
fun String.toResourceId() = substring(2).toInt(16)
36+
37+
execute {
38+
document("res/values/public.xml").use { public ->
39+
fun Sequence<Element>.firstByName(name: String) = first {
40+
it.getAttribute("name") == name
41+
}
42+
43+
val publicElements = public.documentElement.childElementsSequence().filter {
44+
it.tagName == "public"
45+
}
46+
val idElements = publicElements.filter {
47+
it.getAttribute("type") == "id"
48+
}
49+
val stringElements = publicElements.filter {
50+
it.getAttribute("type") == "string"
51+
}
52+
53+
shakeToKudosStringId =
54+
stringElements.firstByName("shake_to_kudos_dialog_title").getAttribute("id").toResourceId()
55+
56+
val kudosIdNode = idElements.firstByName("kudos").apply {
57+
kudosIdId = getAttribute("id").toResourceId()
58+
}
59+
60+
document("res/layout/grouped_activities_dialog_group_tab.xml").use { layout ->
61+
layout.childNodes.findElementByAttributeValueOrThrow("android:id", "@id/leave_group_button_container")
62+
.apply {
63+
// Change from "FrameLayout".
64+
layout.renameNode(this, namespaceURI, "LinearLayout")
65+
66+
val leaveButton = childElementsSequence().first()
67+
// Get "Leave Group" button ID for bytecode matching.
68+
val leaveButtonIdName = leaveButton.getAttribute("android:id").substringAfter('/')
69+
leaveIdId = idElements.firstByName(leaveButtonIdName).getAttribute("id").toResourceId()
70+
71+
// Add surrounding padding to offset decrease on buttons.
72+
setAttribute("android:paddingHorizontal", "@dimen/space_2xs")
73+
74+
// Place buttons next to each other with equal width.
75+
val kudosButton = leaveButton.apply {
76+
setAttribute("android:layout_width", "0dp")
77+
setAttribute("android:layout_weight", "1")
78+
// Decrease padding between buttons from "@dimen/button_large_padding" ...
79+
setAttribute("android:paddingHorizontal", "@dimen/space_xs")
80+
}.cloneNode(true) as Element
81+
kudosButton.apply {
82+
setAttribute("android:id", "@id/${kudosIdNode.getAttribute("name")}")
83+
setAttribute("android:text", "@string/kudos_button")
84+
}.let(::appendChild)
85+
86+
// Downgrade emphasis of "Leave Group" button from "primary".
87+
leaveButton.setAttribute("app:emphasis", "secondary")
88+
}
89+
}
90+
}
91+
}
92+
}
93+
94+
@Suppress("unused")
95+
val addGiveGroupKudosButtonToGroupActivity = bytecodePatch(
96+
name = "Add 'Give Kudos' button to 'Group Activity'",
97+
description = "Adds a button that triggers the same action as shaking your phone would."
98+
) {
99+
compatibleWith("com.strava")
100+
101+
dependsOn(addGiveKudosButtonToLayoutPatch)
102+
103+
execute {
104+
val className = initFingerprint.originalClassDef.type
105+
val onClickListenerClassName = "${className.substringBeforeLast(';')}\$GiveKudosOnClickListener;"
106+
107+
initFingerprint.method.apply {
108+
val constLeaveIdInstruction = instructions.filterIsInstance<BuilderInstruction31i>().first {
109+
it.narrowLiteral == leaveIdId
110+
}
111+
val findViewByIdInstruction =
112+
getInstruction<BuilderInstruction35c>(constLeaveIdInstruction.location.index + 1)
113+
val moveViewInstruction = getInstruction<BuilderInstruction11x>(constLeaveIdInstruction.location.index + 2)
114+
val checkCastButtonInstruction =
115+
getInstruction<BuilderInstruction21c>(constLeaveIdInstruction.location.index + 3)
116+
117+
val buttonClassName = checkCastButtonInstruction.getReference<TypeReference>()!!.type
118+
119+
addInstructions(
120+
constLeaveIdInstruction.location.index,
121+
"""
122+
${constLeaveIdInstruction.opcode.name} v${constLeaveIdInstruction.registerA}, $kudosIdId
123+
${findViewByIdInstruction.opcode.name} { v${findViewByIdInstruction.registerC}, v${findViewByIdInstruction.registerD} }, ${findViewByIdInstruction.reference}
124+
${moveViewInstruction.opcode.name} v${moveViewInstruction.registerA}
125+
${checkCastButtonInstruction.opcode.name} v${checkCastButtonInstruction.registerA}, ${checkCastButtonInstruction.reference}
126+
new-instance v0, $onClickListenerClassName
127+
invoke-direct { v0, p0 }, $onClickListenerClassName-><init>($className)V
128+
invoke-virtual { p3, v0 }, $buttonClassName->setOnClickListener($ON_CLICK_LISTENER_CLASS_DESCRIPTOR)V
129+
"""
130+
)
131+
}
132+
133+
val actionHandlerMethod = actionHandlerFingerprint.match(initFingerprint.originalClassDef).method
134+
val constShakeToKudosStringIndex = actionHandlerMethod.instructions.indexOfFirst {
135+
it is NarrowLiteralInstruction && it.narrowLiteral == shakeToKudosStringId
136+
}
137+
val getSingletonInstruction = actionHandlerMethod.instructions.filterIsInstance<BuilderInstruction21c>().last {
138+
it.opcode == Opcode.SGET_OBJECT && it.location.index < constShakeToKudosStringIndex
139+
}
140+
141+
val outerThisField = ImmutableField(
142+
onClickListenerClassName,
143+
"outerThis",
144+
className,
145+
PUBLIC.value or FINAL.value or SYNTHETIC.value,
146+
null,
147+
listOf(),
148+
setOf()
149+
)
150+
151+
val initFieldMethod = ImmutableMethod(
152+
onClickListenerClassName,
153+
"<init>",
154+
listOf(ImmutableMethodParameter(className, setOf(), "outerThis")),
155+
"V",
156+
PUBLIC.value or SYNTHETIC.value or CONSTRUCTOR.value,
157+
setOf(),
158+
setOf(),
159+
MutableMethodImplementation(2)
160+
).toMutable().apply {
161+
addInstructions(
162+
"""
163+
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
164+
iput-object p1, p0, $outerThisField
165+
return-void
166+
"""
167+
)
168+
}
169+
170+
val onClickMethod = ImmutableMethod(
171+
onClickListenerClassName,
172+
"onClick",
173+
listOf(ImmutableMethodParameter(VIEW_CLASS_DESCRIPTOR, setOf(), "v")),
174+
"V",
175+
PUBLIC.value or FINAL.value,
176+
setOf(),
177+
setOf(),
178+
MutableMethodImplementation(2)
179+
).toMutable().apply {
180+
addInstructions(
181+
"""
182+
sget-object p1, ${getSingletonInstruction.reference}
183+
iget-object p0, p0, $outerThisField
184+
invoke-virtual { p0, p1 }, ${actionHandlerFingerprint.method}
185+
return-void
186+
"""
187+
)
188+
}
189+
190+
ImmutableClassDef(
191+
onClickListenerClassName,
192+
PUBLIC.value or FINAL.value or SYNTHETIC.value,
193+
"Ljava/lang/Object;",
194+
listOf(ON_CLICK_LISTENER_CLASS_DESCRIPTOR),
195+
"ProGuard", // Same as source file name of other classes.
196+
listOf(),
197+
setOf(outerThisField),
198+
setOf(initFieldMethod, onClickMethod)
199+
).let(classes::add)
200+
}
201+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package app.revanced.patches.strava.groupkudos
2+
3+
import app.revanced.patcher.fingerprint
4+
5+
internal val initFingerprint = fingerprint {
6+
parameters("Lcom/strava/feed/view/modal/GroupTabFragment;" , "Z" , "Landroidx/fragment/app/FragmentManager;")
7+
custom { method, _ ->
8+
method.name == "<init>"
9+
}
10+
}
11+
12+
internal val actionHandlerFingerprint = fingerprint {
13+
strings("state")
14+
}

0 commit comments

Comments
 (0)