6 Commits
8 changed files with 184 additions and 12 deletions
+18
View File
@@ -1,3 +1,21 @@
## [1.1.2](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.1.1...v1.1.2) (2026-08-22)
### 🐛 Bug Fixes
* manifest ([7062792](https://git.krtss.de/StanPonomarev/twitter-patch/commit/7062792215590cce79caa6e78354ee73375eaa79))
## [1.1.1](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.1.0...v1.1.1) (2026-08-21)
### 🐛 Bug Fixes
* update screen ([cb1230a](https://git.krtss.de/StanPonomarev/twitter-patch/commit/cb1230a7abf46edb11f2b8dad100b091dd672aee))
## [1.1.0](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.0.3...v1.1.0) (2026-08-21)
### ✨ New Features
* remove upgrade button ([723964b](https://git.krtss.de/StanPonomarev/twitter-patch/commit/723964bc29d0a9edc7ad1915c9cf5669fe9e6f95))
## [1.0.3](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.0.2...v1.0.3) (2026-08-21) ## [1.0.3](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.0.2...v1.0.3) (2026-08-21)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
+3 -2
View File
@@ -4,9 +4,9 @@ Patch bundle for X (Twitter), maintained by
[Stan Ponomarev](https://git.krtss.de/StanPonomarev). [Stan Ponomarev](https://git.krtss.de/StanPonomarev).
<!-- PATCHES_START --> <!-- PATCHES_START -->
> **[v1.0.3](https://git.krtss.de/StanPonomarev/twitter-patch/releases/tag/v1.0.3)**&nbsp;&nbsp;•&nbsp;&nbsp;`main`&nbsp;&nbsp;•&nbsp;&nbsp;1 patches total > **[v1.1.2](https://git.krtss.de/StanPonomarev/twitter-patch/releases/tag/v1.1.2)**&nbsp;&nbsp;•&nbsp;&nbsp;`main`&nbsp;&nbsp;•&nbsp;&nbsp;2 patches total
<details open> <details open>
<summary>📦 X&nbsp;&nbsp;•&nbsp;&nbsp;1 patch</summary> <summary>📦 X&nbsp;&nbsp;•&nbsp;&nbsp;2 patches</summary>
<br> <br>
**🎯 Supported versions:** **🎯 Supported versions:**
@@ -17,6 +17,7 @@ Patch bundle for X (Twitter), maintained by
| 💊&nbsp;Patch | 📜&nbsp;Description | ⚙️&nbsp;Options | | 💊&nbsp;Patch | 📜&nbsp;Description | ⚙️&nbsp;Options |
|----------|----------------|-----------| |----------|----------------|-----------|
| [Disable outdated version screen](#disable-outdated-version-screen) | Prevents X from blocking app access when it marks the client version as outdated. | | | [Disable outdated version screen](#disable-outdated-version-screen) | Prevents X from blocking app access when it marks the client version as outdated. | |
| [Remove timeline Upgrade button](#remove-timeline-upgrade-button) | Removes the Premium Upgrade button from the timeline's top bar. | |
</details> </details>
+1 -1
View File
@@ -1,4 +1,4 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
kotlin.code.style = official kotlin.code.style = official
version = 1.0.3 version = 1.1.2
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"created_at": "2026-08-21T16:15:06", "created_at": "2026-08-22T08:27:42",
"description": "## [1.0.3](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.0.2...v1.0.3) (2026-08-21)\n\n### 🐛 Bug Fixes\n\n* artifact ([0a2b472](https://git.krtss.de/StanPonomarev/twitter-patch/commit/0a2b4725fb53bf0fa741dc4be1a9914ad1e5be8b))", "description": "## [1.1.2](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.1.1...v1.1.2) (2026-08-22)\n\n### 🐛 Bug Fixes\n\n* manifest ([7062792](https://git.krtss.de/StanPonomarev/twitter-patch/commit/7062792215590cce79caa6e78354ee73375eaa79))",
"download_url": "https://git.krtss.de/StanPonomarev/twitter-patch/releases/download/v1.0.3/patches-1.0.3.mpp", "download_url": "https://git.krtss.de/StanPonomarev/twitter-patch/releases/download/v1.1.2/patches-1.1.2.mpp",
"signature_download_url": "", "signature_download_url": "",
"version": "1.0.3" "version": "1.1.2"
} }
+29 -1
View File
@@ -1,11 +1,39 @@
{ {
"NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.",
"version": "1.0.3", "version": "1.1.2",
"patches": [ "patches": [
{ {
"name": "Disable outdated version screen", "name": "Disable outdated version screen",
"description": "Prevents X from blocking app access when it marks the client version as outdated.", "description": "Prevents X from blocking app access when it marks the client version as outdated.",
"default": true, "default": true,
"dependencies": [
"ResourcePatch"
],
"compatiblePackages": [
{
"packageName": "com.twitter.android",
"name": "X",
"description": null,
"apkFileType": "APKM",
"appIconColor": "#000000",
"signatures": null,
"targets": [
{
"version": "12.7.1-release.0",
"versionCodes": null,
"isExperimental": false,
"minSdk": 28,
"description": null
}
]
}
],
"options": []
},
{
"name": "Remove timeline Upgrade button",
"description": "Removes the Premium Upgrade button from the timeline's top bar.",
"default": true,
"dependencies": [], "dependencies": [],
"compatiblePackages": [ "compatiblePackages": [
{ {
@@ -23,7 +23,7 @@ import kotlin.test.assertTrue
class MorphePatchIntegrationTest { class MorphePatchIntegrationTest {
@Test @Test
fun `applies outdated version patch to target APKM`() { fun `applies patches to target APKM`() {
val inputApkm = requiredPathProperty("integrationTest.inputApkm") val inputApkm = requiredPathProperty("integrationTest.inputApkm")
val patchBundle = requiredPathProperty("integrationTest.patchBundle") val patchBundle = requiredPathProperty("integrationTest.patchBundle")
val morpheCache = requiredPathProperty("integrationTest.morpheCache") val morpheCache = requiredPathProperty("integrationTest.morpheCache")
@@ -47,7 +47,8 @@ class MorphePatchIntegrationTest {
"patch", "patch",
"--patches=${patchBundle.absolutePathString()}", "--patches=${patchBundle.absolutePathString()}",
"--exclusive", "--exclusive",
"--enable=$PATCH_NAME", "--enable=$OUTDATED_PATCH_NAME",
"--enable=$UPGRADE_PATCH_NAME",
"--unsigned", "--unsigned",
"--out=${patchedApk.absolutePathString()}", "--out=${patchedApk.absolutePathString()}",
"--result-file=${resultFile.absolutePathString()}", "--result-file=${resultFile.absolutePathString()}",
@@ -91,7 +92,10 @@ class MorphePatchIntegrationTest {
val appliedPatchNames = result.getAsJsonArray("appliedPatches") val appliedPatchNames = result.getAsJsonArray("appliedPatches")
.map { it.asJsonObject.get("name").asString } .map { it.asJsonObject.get("name").asString }
assertTrue(PATCH_NAME in appliedPatchNames, "Expected patch was not applied: $appliedPatchNames") assertTrue(
appliedPatchNames.containsAll(listOf(OUTDATED_PATCH_NAME, UPGRADE_PATCH_NAME)),
"Expected patches were not applied: $appliedPatchNames"
)
assertTrue(result.getAsJsonArray("failedPatches").isEmpty, "Morphe reported failed patches") assertTrue(result.getAsJsonArray("failedPatches").isEmpty, "Morphe reported failed patches")
} finally { } finally {
workDirectory.toFile().deleteRecursively() workDirectory.toFile().deleteRecursively()
@@ -142,7 +146,8 @@ class MorphePatchIntegrationTest {
} }
private companion object { private companion object {
const val PATCH_NAME = "Disable outdated version screen" const val OUTDATED_PATCH_NAME = "Disable outdated version screen"
const val UPGRADE_PATCH_NAME = "Remove timeline Upgrade button"
const val MORPHE_VERSION = "1.13.1" const val MORPHE_VERSION = "1.13.1"
const val MORPHE_SHA256 = "a212531f68d3ada4b504bf9dac3e02460589f3bf8868ee66e1b4780152d3466b" const val MORPHE_SHA256 = "a212531f68d3ada4b504bf9dac3e02460589f3bf8868ee66e1b4780152d3466b"
const val MORPHE_URL = const val MORPHE_URL =
@@ -3,7 +3,9 @@ package app.twitter.patches
import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructions
import app.morphe.patcher.Fingerprint import app.morphe.patcher.Fingerprint
import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.bytecodePatch
import app.morphe.patcher.patch.resourcePatch
import app.twitter.patches.shared.Constants.COMPATIBILITY_X import app.twitter.patches.shared.Constants.COMPATIBILITY_X
import org.w3c.dom.Element
private val outdatedVersionEnforcementFingerprint = Fingerprint( private val outdatedVersionEnforcementFingerprint = Fingerprint(
definingClass = "Lcom/twitter/subsystem/clientshutdown/o;", definingClass = "Lcom/twitter/subsystem/clientshutdown/o;",
@@ -13,6 +15,41 @@ private val outdatedVersionEnforcementFingerprint = Fingerprint(
strings = listOf("Target version is ", "Detected shutdown") strings = listOf("Target version is ", "Detected shutdown")
) )
private val forceUpdateNavigationFingerprint = Fingerprint(
definingClass = "Lcom/x/android/main/helpers/o8;",
name = "a",
returnType = "Lcom/x/compose/core/o2;",
parameters = listOf(
"Lcom/x/navigation/RootNavigationArgs;",
"Lcom/arkivanov/decompose/c;",
"Lcom/x/navigation/f1;"
)
)
private val shutdownForegroundHandlerFingerprint = Fingerprint(
definingClass = "Lcom/twitter/subsystem/clientshutdown/m;",
name = "invoke",
returnType = "Ljava/lang/Object;",
parameters = listOf("Ljava/lang/Object;"),
strings = listOf("Entered foreground, shutdown is ", "Launching app update")
)
private val appUpdateLauncherFingerprint = Fingerprint(
definingClass = "Lcom/twitter/subsystem/clientshutdown/i;",
name = "a",
returnType = "V",
parameters = listOf("Ljava/lang/String;"),
strings = listOf("Launching app update")
)
private val shutdownEntryPointDisablerFingerprint = Fingerprint(
definingClass = "Lcom/twitter/subsystem/clientshutdown/i;",
name = "b",
returnType = "V",
parameters = emptyList(),
strings = listOf("com.twitter.clientshutdown.update.AppUpdateActivityAlias")
)
private val shutdownStateFingerprint = Fingerprint( private val shutdownStateFingerprint = Fingerprint(
definingClass = "Lcom/twitter/subsystem/clientshutdown/q;", definingClass = "Lcom/twitter/subsystem/clientshutdown/q;",
name = "isShutdown", name = "isShutdown",
@@ -20,6 +57,24 @@ private val shutdownStateFingerprint = Fingerprint(
parameters = emptyList() parameters = emptyList()
) )
private const val APP_UPDATE_ACTIVITY = "com.twitter.clientshutdown.update.AppUpdateActivity"
private val disableOutdatedVersionActivityPatch = resourcePatch {
execute {
document("AndroidManifest.xml").use { document ->
val appUpdateActivity = document.getElementsByTagName("activity")
.let { activities ->
(0 until activities.length)
.asSequence()
.map { activities.item(it) }
.filterIsInstance<Element>()
.single { it.getAttribute("android:name") == APP_UPDATE_ACTIVITY }
}
appUpdateActivity.setAttribute("android:enabled", "false")
}
}
}
@Suppress("unused") @Suppress("unused")
val disableOutdatedVersionScreenPatch = bytecodePatch( val disableOutdatedVersionScreenPatch = bytecodePatch(
name = "Disable outdated version screen", name = "Disable outdated version screen",
@@ -27,8 +82,41 @@ val disableOutdatedVersionScreenPatch = bytecodePatch(
default = true default = true
) { ) {
compatibleWith(COMPATIBILITY_X) compatibleWith(COMPATIBILITY_X)
dependsOn(disableOutdatedVersionActivityPatch)
execute { execute {
forceUpdateNavigationFingerprint.method.addInstructions(
0,
"""
move-object/from16 v2, p1
instance-of v0, v2, Lcom/x/navigation/ForceUpdateArgs;
if-eqz v0, :force_update_allowed
sget-object v0, Lcom/x/navigation/MainLandingArgs${'$'}TabType;->HOME:Lcom/x/navigation/MainLandingArgs${'$'}TabType;
const/4 v1, 0x0
new-instance v2, Lcom/x/navigation/MainLandingArgs;
invoke-direct {v2, v0, v1, v1, v1}, Lcom/x/navigation/MainLandingArgs;-><init>(Lcom/x/navigation/MainLandingArgs${'$'}TabType;Lcom/x/navigation/AcceptGroupInviteArgs;Lcom/x/navigation/RootNavigationArgs;Lcom/x/models/PostIdentifier;)V
move-object/16 p1, v2
:force_update_allowed
"""
)
shutdownForegroundHandlerFingerprint.method.addInstructions(
0,
"""
sget-object v0, Lkotlin/Unit;->a:Lkotlin/Unit;
return-object v0
"""
)
appUpdateLauncherFingerprint.method.addInstructions(
0,
"return-void"
)
shutdownEntryPointDisablerFingerprint.method.addInstructions(
0,
"return-void"
)
outdatedVersionEnforcementFingerprint.method.addInstructions( outdatedVersionEnforcementFingerprint.method.addInstructions(
0, 0,
""" """
@@ -0,0 +1,32 @@
package app.twitter.patches
import app.morphe.patcher.patch.resourcePatch
import app.twitter.patches.shared.Constants.COMPATIBILITY_X
import org.w3c.dom.Element
private const val TOOLBAR_ITEMS_PATH = "res/menu/toolbar_items.xml"
private const val UPGRADE_BUTTON_ID = "premium_upsell_upgrade"
@Suppress("unused")
val removeTimelineUpgradeButtonPatch = resourcePatch(
name = "Remove timeline Upgrade button",
description = "Removes the Premium Upgrade button from the timeline's top bar.",
default = true
) {
compatibleWith(COMPATIBILITY_X)
execute {
document(TOOLBAR_ITEMS_PATH).use { document ->
val menu = document.documentElement
val upgradeButton = (0 until menu.childNodes.length)
.asSequence()
.map { menu.childNodes.item(it) }
.filterIsInstance<Element>()
.single {
it.getAttribute("android:id").substringAfterLast('/') == UPGRADE_BUTTON_ID
}
menu.removeChild(upgradeButton)
}
}
}