diff --git a/.github/workflows/build_dev.yml b/.github/workflows/build_dev.yml index ff8b340..9b6d3f3 100644 --- a/.github/workflows/build_dev.yml +++ b/.github/workflows/build_dev.yml @@ -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 diff --git a/package-lock.json b/package-lock.json index 3486ebd..44db9b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,14 +51,15 @@ }, "node_modules/@MorpheApp/changelog": { "version": "0.0.1", - "resolved": "git+ssh://git@github.com/MorpheApp/changelog.git#3ea03385cb2f591ae39fcaa8ab2a90469fac4004", + "resolved": "git+ssh://git@github.com/MorpheApp/changelog.git#caa1e931730f097bb6c4dee636b01c2c24ccd72d", + "integrity": "sha512-IWCprzvzVotJocBVNVOgwGNzRf+rF+IrrfeOKPLyq1C6XrqvP7treSI13PQT+kupf8fV6TL8WP4dwFTxW38JqA==", "dev": true, "license": "MIT", "dependencies": { - "@semantic-release/error": "^3.0.0", - "aggregate-error": "^3.0.0", - "fs-extra": "^11.0.0", - "lodash": "^4.17.4" + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^5.0.0", + "fs-extra": "^11.3.4", + "lodash": "^4.18.1" }, "engines": { "node": ">=14.17" @@ -67,6 +68,75 @@ "semantic-release": ">=18.0.0" } }, + "node_modules/@MorpheApp/changelog/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@MorpheApp/changelog/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@MorpheApp/changelog/node_modules/clean-stack": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.3.0.tgz", + "integrity": "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@MorpheApp/changelog/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@MorpheApp/changelog/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2855,9 +2925,9 @@ } }, "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3515,9 +3585,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, diff --git a/patches-list.json b/patches-list.json index 206c6db..25b873b 100644 --- a/patches-list.json +++ b/patches-list.json @@ -48,7 +48,7 @@ ], "compatiblePackages": { "com.google.android.apps.magazines": [ - "5.158.0.908428942" + "5.161.0.931240252" ] }, "options": [] @@ -63,6 +63,7 @@ ], "compatiblePackages": { "com.google.android.apps.magazines": [ + "5.161.0.931240252", "5.158.0.908428942", "5.156.0.892791979" ] diff --git a/patches/src/main/kotlin/app/docbt/patched_up/googlenews/customtabs/EnableCustomTabsPatch.kt b/patches/src/main/kotlin/app/docbt/patched_up/googlenews/customtabs/EnableCustomTabsPatch.kt index 6d5dfe1..b311de3 100644 --- a/patches/src/main/kotlin/app/docbt/patched_up/googlenews/customtabs/EnableCustomTabsPatch.kt +++ b/patches/src/main/kotlin/app/docbt/patched_up/googlenews/customtabs/EnableCustomTabsPatch.kt @@ -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 @@ -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"), ), ) @@ -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) { @@ -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) } } @@ -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(ifNezIndex).target + trampolineMethod.replaceInstruction(ifNezIndex, BuilderInstruction10t(Opcode.GOTO, target)) } } } diff --git a/patches/src/main/kotlin/app/docbt/patched_up/googlenews/customtabs/Fingerprints.kt b/patches/src/main/kotlin/app/docbt/patched_up/googlenews/customtabs/Fingerprints.kt index 77a6879..3253584 100644 --- a/patches/src/main/kotlin/app/docbt/patched_up/googlenews/customtabs/Fingerprints.kt +++ b/patches/src/main/kotlin/app/docbt/patched_up/googlenews/customtabs/Fingerprints.kt @@ -4,73 +4,82 @@ import app.morphe.patcher.Fingerprint import app.morphe.patcher.fieldAccess import com.android.tools.smali.dexlib2.Opcode -// All methods in Google News v5.158.0 that read Ladpx;->i:Z (iget-boolean). -// Ladpx = CustomTabsArticleLauncher; field i controls CustomTabs (true) vs WebView (false). +// 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). // // Verified read sites (DEX bytecode scan): -// classes.dex: Laiud (StartActivity handler) -// classes3.dex: Ladrm (handler) -// Laiuk (handler) -// Laixq (handler) -// Laixz (handler) -// CustomTabsTrampolineActivity (onCreate) +// classes.dex: Lajdq (handler, method D, two read sites) +// classes3.dex: Ladwc (handler) +// Lajdx (handler) +// Lajhe (handler) +// Lajhl (handler) -internal object LajdkFingerprint : Fingerprint( - filters = listOf(fieldAccess("Ladpx;", "i", "Z", Opcode.IGET_BOOLEAN)), - custom = { _, classDef -> classDef.type == "Laiud;" }, +internal object LajdqFingerprint : Fingerprint( + filters = listOf(fieldAccess("Ladut;", "i", "Z", Opcode.IGET_BOOLEAN)), + custom = { _, classDef -> classDef.type == "Lajdq;" }, ) -internal object LaedzFingerprint : Fingerprint( - filters = listOf(fieldAccess("Ladpx;", "i", "Z", Opcode.IGET_BOOLEAN)), - custom = { _, classDef -> classDef.type == "Ladrm;" }, +internal object LadwcFingerprint : Fingerprint( + filters = listOf(fieldAccess("Ladut;", "i", "Z", Opcode.IGET_BOOLEAN)), + custom = { _, classDef -> classDef.type == "Ladwc;" }, ) -internal object LajdrFingerprint : Fingerprint( - filters = listOf(fieldAccess("Ladpx;", "i", "Z", Opcode.IGET_BOOLEAN)), - custom = { _, classDef -> classDef.type == "Laiuk;" }, +internal object LajdxFingerprint : Fingerprint( + filters = listOf(fieldAccess("Ladut;", "i", "Z", Opcode.IGET_BOOLEAN)), + custom = { _, classDef -> classDef.type == "Lajdx;" }, ) -internal object LajgzFingerprint : Fingerprint( - filters = listOf(fieldAccess("Ladpx;", "i", "Z", Opcode.IGET_BOOLEAN)), - custom = { _, classDef -> classDef.type == "Laixq;" }, +internal object LajheFingerprint : Fingerprint( + filters = listOf(fieldAccess("Ladut;", "i", "Z", Opcode.IGET_BOOLEAN)), + custom = { _, classDef -> classDef.type == "Lajhe;" }, ) -internal object LajhkFingerprint : Fingerprint( - filters = listOf(fieldAccess("Ladpx;", "i", "Z", Opcode.IGET_BOOLEAN)), - custom = { _, classDef -> classDef.type == "Laixz;" }, +internal object LajhlFingerprint : Fingerprint( + filters = listOf(fieldAccess("Ladut;", "i", "Z", Opcode.IGET_BOOLEAN)), + custom = { _, classDef -> classDef.type == "Lajhl;" }, ) -// CustomTabsTrampolineActivity.onCreate() reads Ladpx.i; if i==0 it logs -// "Unexpected intent; activity is not enabled" and finishes. Patch i-read to always true. -internal object CustomTabsTrampolineFingerprint : Fingerprint( - filters = listOf(fieldAccess("Ladpx;", "i", "Z", Opcode.IGET_BOOLEAN)), - custom = { _, classDef -> - classDef.type == "Lcom/google/apps/dots/android/modules/reading/customtabs/CustomTabsTrampolineActivity;" - }, +// 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;" }, ) -// Ladqf.b() returns a Ladqg implementation based on experiment flag Laqhh.a.get(). -// When the experiment is OFF, IF_EQZ branches to the disabled Ladqi (filters all browsers -// against an allowlist, returns null if none match), causing Ladpx.c=null → binding failure. -// Patching the IF_EQZ to NOP forces the enabled Ladql path (picks browser via resolveActivity). -internal object LaecrFingerprint : Fingerprint( - filters = listOf(fieldAccess("Laqhh;", "a", "Laqhh;", Opcode.SGET_OBJECT)), - custom = { _, classDef -> classDef.type == "Ladqf;" }, +// 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 +// 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;" }, ) -// Ladqi.a() — "disabled" Ladqg: filters installed browsers against an experiment allowlist, -// returns null if none match. IF_EQZ returns null when the filtered list is empty. -// NOP-ing it forces the fallback resolveActivity call which picks the default browser. -internal object LaecuFingerprint : Fingerprint( - filters = listOf(fieldAccess("Ladqi;", "a", "Landroid/content/Context;", Opcode.IGET_OBJECT)), - custom = { _, classDef -> classDef.type == "Ladqi;" }, +// Ladvf.a() — "disabled" Ladvd 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;" }, ) -// Ladql.a() — "enabled" Ladqg: picks the best CT-supporting browser via resolveActivity, -// then checks if it's in the experiment allowlist. IF_EQZ redirects to allowlist-only path -// when the default browser isn't allowlisted (e.g. Firefox/Brave). NOP-ing it always returns -// whatever resolveActivity found (default browser has priority). -internal object LaecxFingerprint : Fingerprint( - filters = listOf(fieldAccess("Ladql;", "a", "Landroid/content/Context;", Opcode.IGET_OBJECT)), - custom = { _, classDef -> classDef.type == "Ladql;" }, +// CustomTabsTrampolineActivity.onCreate() reads field a (Ladvd) 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. +internal object CustomTabsTrampolineFingerprint : Fingerprint( + filters = listOf( + fieldAccess( + "Lcom/google/apps/dots/android/modules/reading/customtabs/CustomTabsTrampolineActivity;", + "a", + "Ladvd;", + Opcode.IGET_OBJECT, + ), + ), + custom = { _, classDef -> + classDef.type == "Lcom/google/apps/dots/android/modules/reading/customtabs/CustomTabsTrampolineActivity;" + }, ) diff --git a/patches/src/main/kotlin/app/docbt/patched_up/googlenews/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/docbt/patched_up/googlenews/gms/GmsCoreSupportPatch.kt index 433691b..ef1a963 100644 --- a/patches/src/main/kotlin/app/docbt/patched_up/googlenews/gms/GmsCoreSupportPatch.kt +++ b/patches/src/main/kotlin/app/docbt/patched_up/googlenews/gms/GmsCoreSupportPatch.kt @@ -23,6 +23,7 @@ private val COMPAT = Compatibility( packageName = FROM_PACKAGE, appIconColor = 0x4285F4, targets = listOf( + AppTarget(version = "5.161.0.931240252"), AppTarget(version = "5.158.0.908428942"), AppTarget(version = "5.156.0.892791979"), ), diff --git a/versions.json b/versions.json index 7effb07..f985ba1 100644 --- a/versions.json +++ b/versions.json @@ -1,4 +1,4 @@ { - "googleNews": "5.158.0.908428942", + "googleNews": "5.161.0.931240252", "kleinanzeigen": "2026.16.1" }