Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build_dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
run: ./gradlew :patches:buildAndroid --no-daemon

- name: Upload patch artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: patches-dev
path: patches/build/libs/patches-*.mpp
Expand Down
92 changes: 81 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion patches-list.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
],
"compatiblePackages": {
"com.google.android.apps.magazines": [
"5.158.0.908428942"
"5.161.0.931240252"
]
},
"options": []
Expand All @@ -63,6 +63,7 @@
],
"compatiblePackages": {
"com.google.android.apps.magazines": [
"5.161.0.931240252",
"5.158.0.908428942",
"5.156.0.892791979"
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import app.morphe.patcher.patch.AppTarget
import app.morphe.patcher.patch.Compatibility
import app.morphe.patcher.patch.bytecodePatch
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction10t
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
Expand All @@ -15,7 +17,7 @@ private val COMPAT = Compatibility(
packageName = "com.google.android.apps.magazines",
appIconColor = 0x4285F4,
targets = listOf(
AppTarget(version = "5.158.0.908428942"),
AppTarget(version = "5.161.0.931240252"),
),
)

Expand All @@ -30,52 +32,48 @@ val enableCustomTabsPatch = bytecodePatch(

execute {
with(InstructionExtensions) {
// Step 1: Patch Ladqf.b() — replace IF_EQZ experiment flag check with NOP.
// Ladqf.b() normally returns the disabled Ladqi picker when the experiment is OFF,
// causing Ladpx.c=null and binding failure. With NOP, it always returns the
// enabled Ladql picker, which uses PackageManager to find a CT-supporting browser.
val laecrMethod = LaecrFingerprint.method
// Step 1: Patch Ladvb.a() — replace IF_EQZ experiment flag check with NOP.
// Ladvb.a() reads Laqrp experiment flag and returns the disabled Ladvf picker
// when the flag is OFF. With NOP, it always returns the enabled Ladvi picker.
val advbMethod = LadvbFingerprint.method
var ifEqzIndex = -1
for ((i, instr) in laecrMethod.implementation!!.instructions.withIndex()) {
for ((i, instr) in advbMethod.implementation!!.instructions.withIndex()) {
if (instr.opcode == Opcode.IF_EQZ) {
ifEqzIndex = i
break
}
}
check(ifEqzIndex != -1) { "IF_EQZ not found in Ladqf.b()" }
laecrMethod.replaceInstruction(ifEqzIndex, "nop")
check(ifEqzIndex != -1) { "IF_EQZ not found in Ladvb.a()" }
advbMethod.replaceInstruction(ifEqzIndex, "nop")

// Step 2: Bypass experiment allowlist in Ladql.a() (enabled Ladqg picker).
// Ladql.a() picks the best CT-supporting browser via resolveActivity, then checks
// if it is in the experiment allowlist. Non-Chrome browsers are not in that list.
// NOP-ing the IF_EQZ makes it always return whatever resolveActivity found.
LaecxFingerprint.methodOrNull?.let { method ->
// Step 2: Bypass experiment allowlist in Ladvi.a() (enabled browser picker).
// Ladvi.a() resolves the OS default browser, then checks if it's in the experiment
// allowlist. NOP-ing the IF_EQZ makes it always return whatever the OS resolved.
LadviFingerprint.methodOrNull?.let { method ->
val idx = method.implementation!!.instructions.indexOfFirst {
it.opcode == Opcode.IF_EQZ
}
if (idx != -1) method.replaceInstruction(idx, "nop")
}

// Step 3: Bypass allowlist in Ladqi.a() (disabled Ladqg picker, belt+suspenders).
// Ladqi.a() filters installed browsers against the allowlist first.
// NOP-ing the IF_EQZ always attempts resolveActivity so the default browser is tried.
LaecuFingerprint.methodOrNull?.let { method ->
// Step 3: Bypass allowlist-only filtering in Ladvf.a() (disabled picker, belt+suspenders).
// Ladvf.a() returns null when no installed browser matches the allowlist. NOP-ing the
// IF_EQZ always proceeds to compute a candidate instead of bailing out early.
LadvfFingerprint.methodOrNull?.let { method ->
val idx = method.implementation!!.instructions.indexOfFirst {
it.opcode == Opcode.IF_EQZ
}
if (idx != -1) method.replaceInstruction(idx, "nop")
}

// Step 4: In every method that reads Ladpx.i, replace iget-boolean Ladpx;->i:Z
// Step 4: In every method that reads Ladut.i, replace iget-boolean Ladut;->i:Z
// with const/4 vX, 0x1 so the CustomTabs branch is always taken.
// Covers click handlers, navigation, ReadNow, and CustomTabsTrampolineActivity.
val methods = listOf(
LajdkFingerprint.method,
LaedzFingerprint.methodOrNull,
LajdrFingerprint.methodOrNull,
LajgzFingerprint.methodOrNull,
LajhkFingerprint.methodOrNull,
CustomTabsTrampolineFingerprint.methodOrNull,
LajdqFingerprint.method,
LadwcFingerprint.methodOrNull,
LajdxFingerprint.methodOrNull,
LajheFingerprint.methodOrNull,
LajhlFingerprint.methodOrNull,
).filterNotNull()

for (method in methods) {
Expand All @@ -84,7 +82,7 @@ val enableCustomTabsPatch = bytecodePatch(
for (instr in method.implementation!!.instructions) {
if (instr.opcode == Opcode.IGET_BOOLEAN) {
val ref = (instr as ReferenceInstruction).reference
if (ref is FieldReference && ref.definingClass == "Ladpx;" && ref.name == "i") {
if (ref is FieldReference && ref.definingClass == "Ladut;" && ref.name == "i") {
targets.add(index)
}
}
Expand All @@ -95,6 +93,28 @@ val enableCustomTabsPatch = bytecodePatch(
method.replaceInstruction(i, "const/4 v$reg, 0x1")
}
}

// Step 5: CustomTabsTrampolineActivity.onCreate() reads field a (Ladvd), calls
// Ladvd.a(), and does an IF_NEZ on the (possibly null) result; if null, it logs
// "Unexpected intent; activity is not enabled" and finishes. Convert the IF_NEZ into
// an unconditional GOTO to its existing target so the enabled path is always taken.
val trampolineMethod = CustomTabsTrampolineFingerprint.method
val fieldReadIndex = trampolineMethod.implementation!!.instructions.indexOfFirst {
if (it.opcode != Opcode.IGET_OBJECT) return@indexOfFirst false
val ref = (it as ReferenceInstruction).reference
ref is FieldReference && ref.definingClass ==
"Lcom/google/apps/dots/android/modules/reading/customtabs/CustomTabsTrampolineActivity;" &&
ref.name == "a" && ref.type == "Ladvd;"
}
check(fieldReadIndex != -1) { "Field read of CustomTabsTrampolineActivity.a not found" }

val ifNezIndex = trampolineMethod.implementation!!.instructions
.drop(fieldReadIndex)
.indexOfFirst { it.opcode == Opcode.IF_NEZ } + fieldReadIndex
check(ifNezIndex >= fieldReadIndex) { "IF_NEZ not found after CustomTabsTrampolineActivity.a read" }

val target = trampolineMethod.getInstruction<BuilderOffsetInstruction>(ifNezIndex).target
trampolineMethod.replaceInstruction(ifNezIndex, BuilderInstruction10t(Opcode.GOTO, target))
}
}
}
Loading
Loading