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
7 changes: 7 additions & 0 deletions .github/workflows/build_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew :patches:buildAndroid --no-daemon

- name: Upload patch artifact
uses: actions/upload-artifact@v7
with:
name: patches-pr
path: patches/build/libs/patches-*.mpp
retention-days: 7
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.161.0.931240252"
"5.163.0.947799485"
]
},
"options": []
Expand All @@ -63,6 +63,7 @@
],
"compatiblePackages": {
"com.google.android.apps.magazines": [
"5.163.0.947799485",
"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 @@ -17,7 +17,7 @@ private val COMPAT = Compatibility(
packageName = "com.google.android.apps.magazines",
appIconColor = 0x4285F4,
targets = listOf(
AppTarget(version = "5.161.0.931240252"),
AppTarget(version = "5.163.0.947799485"),
),
)

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

execute {
with(InstructionExtensions) {
// 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
// Step 1: Patch Laeah.a() — replace IF_EQZ experiment flag check with NOP.
// Laeah.a() reads Lardw experiment flag and returns the disabled Laeal picker
// when the flag is OFF. With NOP, it always returns the enabled Laeao picker.
val laeahMethod = LaeahFingerprint.method
var ifEqzIndex = -1
for ((i, instr) in advbMethod.implementation!!.instructions.withIndex()) {
for ((i, instr) in laeahMethod.implementation!!.instructions.withIndex()) {
if (instr.opcode == Opcode.IF_EQZ) {
ifEqzIndex = i
break
}
}
check(ifEqzIndex != -1) { "IF_EQZ not found in Ladvb.a()" }
advbMethod.replaceInstruction(ifEqzIndex, "nop")
check(ifEqzIndex != -1) { "IF_EQZ not found in Laeah.a()" }
laeahMethod.replaceInstruction(ifEqzIndex, "nop")

// 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
// Step 2: Bypass experiment allowlist in Laeao.a() (enabled browser picker).
// Laeao.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 ->
LaeaoFingerprint.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-only filtering in Ladvf.a() (disabled picker, belt+suspenders).
// Ladvf.a() returns null when no installed browser matches the allowlist. NOP-ing the
// Step 3: Bypass allowlist-only filtering in Laeal.a() (disabled picker, belt+suspenders).
// Laeal.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 ->
LaealFingerprint.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 Ladut.i, replace iget-boolean Ladut;->i:Z
// Step 4: In every method that reads Ladzz.i, replace iget-boolean Ladzz;->i:Z
// with const/4 vX, 0x1 so the CustomTabs branch is always taken.
val methods = listOf(
LajdqFingerprint.method,
LadwcFingerprint.methodOrNull,
LajdxFingerprint.methodOrNull,
LajheFingerprint.methodOrNull,
LajhlFingerprint.methodOrNull,
LajpvFingerprint.method,
LajtqFingerprint.methodOrNull,
LajtjFingerprint.methodOrNull,
LaebiFingerprint.methodOrNull,
LajqcFingerprint.methodOrNull,
).filterNotNull()

for (method in methods) {
Expand All @@ -82,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 == "Ladut;" && ref.name == "i") {
if (ref is FieldReference && ref.definingClass == "Ladzz;" && ref.name == "i") {
targets.add(index)
}
}
Expand All @@ -94,8 +94,8 @@ val enableCustomTabsPatch = bytecodePatch(
}
}

// 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
// Step 5: CustomTabsTrampolineActivity.onCreate() reads field a (Laeaj), calls
// Laeaj.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
Expand All @@ -104,7 +104,7 @@ val enableCustomTabsPatch = bytecodePatch(
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;"
ref.name == "a" && ref.type == "Laeaj;"
}
check(fieldReadIndex != -1) { "Field read of CustomTabsTrampolineActivity.a not found" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,78 +4,78 @@ import app.morphe.patcher.Fingerprint
import app.morphe.patcher.fieldAccess
import com.android.tools.smali.dexlib2.Opcode

// All methods in Google News v5.161.0 that read Ladut;->i:Z (iget-boolean).
// Ladut = ArticleLauncher; field i controls CustomTabs (true) vs WebView (false).
// All methods in Google News v5.163.0 that read Ladzz;->i:Z (iget-boolean).
// Ladzz = ArticleLauncher; field i controls CustomTabs (true) vs WebView (false).
//
// Verified read sites (DEX bytecode scan):
// classes.dex: Lajdq (handler, method D, two read sites)
// classes3.dex: Ladwc (handler)
// Lajdx (handler)
// Lajhe (handler)
// Lajhl (handler)
// classes.dex: Lajpv (handler, two read sites)
// classes3.dex: Lajtq (handler)
// Lajtj (handler)
// Laebi (handler)
// Lajqc (handler)

internal object LajdqFingerprint : Fingerprint(
filters = listOf(fieldAccess("Ladut;", "i", "Z", Opcode.IGET_BOOLEAN)),
custom = { _, classDef -> classDef.type == "Lajdq;" },
internal object LajpvFingerprint : Fingerprint(
filters = listOf(fieldAccess("Ladzz;", "i", "Z", Opcode.IGET_BOOLEAN)),
custom = { _, classDef -> classDef.type == "Lajpv;" },
)

internal object LadwcFingerprint : Fingerprint(
filters = listOf(fieldAccess("Ladut;", "i", "Z", Opcode.IGET_BOOLEAN)),
custom = { _, classDef -> classDef.type == "Ladwc;" },
internal object LajtqFingerprint : Fingerprint(
filters = listOf(fieldAccess("Ladzz;", "i", "Z", Opcode.IGET_BOOLEAN)),
custom = { _, classDef -> classDef.type == "Lajtq;" },
)

internal object LajdxFingerprint : Fingerprint(
filters = listOf(fieldAccess("Ladut;", "i", "Z", Opcode.IGET_BOOLEAN)),
custom = { _, classDef -> classDef.type == "Lajdx;" },
internal object LajtjFingerprint : Fingerprint(
filters = listOf(fieldAccess("Ladzz;", "i", "Z", Opcode.IGET_BOOLEAN)),
custom = { _, classDef -> classDef.type == "Lajtj;" },
)

internal object LajheFingerprint : Fingerprint(
filters = listOf(fieldAccess("Ladut;", "i", "Z", Opcode.IGET_BOOLEAN)),
custom = { _, classDef -> classDef.type == "Lajhe;" },
internal object LaebiFingerprint : Fingerprint(
filters = listOf(fieldAccess("Ladzz;", "i", "Z", Opcode.IGET_BOOLEAN)),
custom = { _, classDef -> classDef.type == "Laebi;" },
)

internal object LajhlFingerprint : Fingerprint(
filters = listOf(fieldAccess("Ladut;", "i", "Z", Opcode.IGET_BOOLEAN)),
custom = { _, classDef -> classDef.type == "Lajhl;" },
internal object LajqcFingerprint : Fingerprint(
filters = listOf(fieldAccess("Ladzz;", "i", "Z", Opcode.IGET_BOOLEAN)),
custom = { _, classDef -> classDef.type == "Lajqc;" },
)

// Ladvb.a(Laqxf, Laqxf) — experiment flag switch.
// Reads Laqrp;->a:Laqrp; (experiment flag), calls .a().f() → boolean.
// IF_EQZ branches to the disabled Ladvf picker when the flag is OFF.
// NOP-ing the IF_EQZ forces the enabled Ladvi path (resolveActivity-based browser picker).
internal object LadvbFingerprint : Fingerprint(
filters = listOf(fieldAccess("Laqrp;", "a", "Laqrp;", Opcode.SGET_OBJECT)),
custom = { _, classDef -> classDef.type == "Ladvb;" },
// Laeah.a(Larjq, Larjq) — experiment flag switch.
// Reads Lardw;->a:Lardw; (experiment flag), calls .a().f() → boolean.
// IF_EQZ branches to the disabled Laeal picker when the flag is OFF.
// NOP-ing the IF_EQZ forces the enabled Laeao path (resolveActivity-based browser picker).
internal object LaeahFingerprint : Fingerprint(
filters = listOf(fieldAccess("Lardw;", "a", "Lardw;", Opcode.SGET_OBJECT)),
custom = { _, classDef -> classDef.type == "Laeah;" },
)

// Ladvi.a() — "enabled" Ladvd impl: resolves the OS default browser via Leu.a(...), then checks
// if it's in the experiment allowlist (Laqrp). IF_EQZ skips the immediate return when the default
// Laeao.a() — "enabled" Laeaj impl: resolves the OS default browser via Lew.a(...), then checks
// if it's in the experiment allowlist (Lardw). IF_EQZ skips the immediate return when the default
// browser isn't allowlisted (e.g. Firefox/Brave), falling back to an allowlist-only candidate list.
// NOP-ing it always returns the OS default browser, regardless of the allowlist.
internal object LadviFingerprint : Fingerprint(
filters = listOf(fieldAccess("Ladvi;", "a", "Landroid/content/Context;", Opcode.IGET_OBJECT)),
custom = { _, classDef -> classDef.type == "Ladvi;" },
internal object LaeaoFingerprint : Fingerprint(
filters = listOf(fieldAccess("Laeao;", "a", "Landroid/content/Context;", Opcode.IGET_OBJECT)),
custom = { _, classDef -> classDef.type == "Laeao;" },
)

// Ladvf.a() — "disabled" Ladvd impl: filters installed browsers against the experiment allowlist
// Laeal.a() — "disabled" Laeaj impl: filters installed browsers against the experiment allowlist
// only (no default-browser fallback), returns null if the filtered list is empty. IF_EQZ returns
// null in that case. NOP-ing it always proceeds to compute a candidate from the (possibly empty)
// filtered list instead of bailing out early.
internal object LadvfFingerprint : Fingerprint(
filters = listOf(fieldAccess("Ladvf;", "a", "Landroid/content/Context;", Opcode.IGET_OBJECT)),
custom = { _, classDef -> classDef.type == "Ladvf;" },
internal object LaealFingerprint : Fingerprint(
filters = listOf(fieldAccess("Laeal;", "a", "Landroid/content/Context;", Opcode.IGET_OBJECT)),
custom = { _, classDef -> classDef.type == "Laeal;" },
)

// CustomTabsTrampolineActivity.onCreate() reads field a (Ladvd) and calls a()Ljava/lang/String;
// CustomTabsTrampolineActivity.onCreate() reads field a (Laeaj) and calls a()Ljava/lang/String;
// then does if-nez on the result; if null, it logs "Unexpected intent; activity is not enabled"
// and finishes. Patch the if-nez into an unconditional goto to its existing target so the
// enabled path is always taken regardless of what Ladvd.a() returns.
// enabled path is always taken regardless of what Laeaj.a() returns.
internal object CustomTabsTrampolineFingerprint : Fingerprint(
filters = listOf(
fieldAccess(
"Lcom/google/apps/dots/android/modules/reading/customtabs/CustomTabsTrampolineActivity;",
"a",
"Ladvd;",
"Laeaj;",
Opcode.IGET_OBJECT,
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ private val COMPAT = Compatibility(
packageName = FROM_PACKAGE,
appIconColor = 0x4285F4,
targets = listOf(
AppTarget(version = "5.163.0.947799485"),
AppTarget(version = "5.161.0.931240252"),
AppTarget(version = "5.158.0.908428942"),
AppTarget(version = "5.156.0.892791979"),
Expand Down Expand Up @@ -110,7 +111,13 @@ private val ACTIONS = setOf(
"com.google.android.gms.asterism.service.START",
"com.google.android.gms.audiomodem.service.AudioModemService.START",
"com.google.android.gms.audit.service.START",
"com.google.android.gms.auth.aang.events.services.START",
// NOTE: com.google.android.gms.auth.aang.events.services.START is deliberately
// NOT transformed. The AANG events service (bound via getStartServiceAction() in
// the aang auth client) has no counterpart in MicroG RE, so rewriting it to
// "app.revanced.*" points the bind at a non-existent service and breaks the
// fresh-install account-add return flow (sign-in loops after login). Leaving it
// as "com.google.*" restores the behaviour of the first working releases.
// See regression from commit 7342da2.
"com.google.android.gms.auth.account.authapi.START",
"com.google.android.gms.auth.account.authenticator.auto.service.START",
"com.google.android.gms.auth.account.authenticator.chromeos.START",
Expand Down
2 changes: 1 addition & 1 deletion versions.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"googleNews": "5.161.0.931240252",
"googleNews": "5.163.0.947799485",
"kleinanzeigen": "2026.16.1"
}
Loading