|
| 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 | +} |
0 commit comments