Tag: v0.20.0 (HEAD)
Latest APK: textra2_v0.20.0.apk (74 MB, signed with textrcs.keystore)
- Renamed Textra Premium 4.84 (cracked, audit-cleared) installs side-by-side
with the original
com.textra. Package iscom.textra2. App label is "Textra 2". All 9 manifest authorities + 17 smali string sites renamed. Play Store deep-links and 12com.textra.emoji*external queries preserved. Slide-animation fade-out restored (alpha 0.15↔1). InitialSyncActivity(the "make us the default SMS app" prompt) is disabled in the manifest. The app boots straight to its messaging UI without that intercept.
PairingActivityis a single-button flow per user spec (no URL/credential text inputs):- Tap Connect to Google Messages → full-screen WebView opens
accounts.google.com. - User signs in. CookieManager harvests
SAPISID/__Secure-3PAPISID. - Background thread runs
SignInGaiaClient.signIn()— real HTTPS POST toinstantmessaging-pa.clients6.google.com/.../SignInGaiawith PBLite body + SAPISIDHASH auth header + realx-goog-api-key. GaiaPairingOrchestrator.beginPairing()opens the receive long-poll, sends UKEY2CLIENT_INITwrapped inGaiaPairingRequestContainer→OutgoingRPCData{action=CREATE_GAIA_PAIRING_CLIENT_INIT, unencrypted}→OutgoingRPCMessage{mobile, data, auth, ttl=300s}toMessaging/SendMessage. ReceivesSERVER_INITon the long-poll, matches byresponseID, ECDH P-256 + HKDF + emoji index = real emoji.- Activity transitions to the emoji panel showing the single emoji centered on screen with caption: "Open Google Messages on your phone and tap the matching emoji."
- User taps the matching emoji on their phone in the real Google Messages app.
GaiaPairingOrchestrator.finishPairing()sendsCLIENT_FINISH(90s timeout), checksfinishErrorType, derives Ditto session keys viaSessionCrypto.deriveSessionKeys(nextKey, confirmedKeyDerivationVersion)(both v0 and v1 implemented).- Returns
GMessagesSession{tachyonAuthToken, aesKey, hmacKey, mobileDevice, browserUuid, cookies}→SessionStore.save()persists toSharedPreferences("textrcs_session").
- Tap Connect to Google Messages → full-screen WebView opens
- On next launch,
SessionStore.load()returns the saved session →ReceiveServiceis started automatically. No re-pair needed.
- Smali patch on
e5/d.smali::m()(Textra'sC5677d.mo6177m— the immediate caller ofSmsManager.sendMultipartTextMessage):.method public m(Ljava/lang/String;Ljava/util/List;Ljava/util/ArrayList;Ljava/util/ArrayList;I)V .locals 1 invoke-static {}, App;->getAppContext()Landroid/content/Context; move-result-object v0 invoke-static {v0, p1, p2}, SendManager;->sendSmsBridge(Context, String, List)V return-void .end method
SendManager.sendSmsBridge(ctx, dest, parts)joins parts → single body →SendManager.sendText(dest, body)→ loads session → runsGetOrCreateConversation{ContactNumber{number=phone}}+SendMessage{ conversationID, tmpID, messagePayload{messageContent{content=body}}}. Both wrapped inOutgoingRPCData{action, encryptedProtoData=AESCTRHelper .encrypt(innerProto), sessionID}, outer envelope identical to pairing exceptmessageType=BUGLE_MESSAGEand TTL stays 300s.SmsManageris never called. Cellular radio doesn't fire.
ReceiveServiceis a foreground service (manifest typedataSync) started automatically on every launch where a session exists (and right after pairing completes).- Maintains a continuous
LongPollReceiverthread. On each frame: parseRPCMessageData, decryptencryptedDatavia session crypto, parseUpdateEventswhen action isGET_UPDATES, dispatch toIncomingMessageHandler. IncomingMessageHandlerlogs per-event details with tagTextRCSIncoming(messageID, conversationID, timestamp, tmpID, messageInfoCount).
TextraDbBridge.writeIncoming(phone, body, ts)uses reflection (verified against current Textra smali) to construct acom.mplus.lib.r4.s0with the rightj0fields populated and callsH.X().F0(s0). Wired fromIncomingMessageHandler: everyMessageEventwith emptytmpIDgets the body extracted frommessageInfoList[*].messageContent.contentand written into Textra's DB.
TokenRefreshClient.refresh()ports mautrix'sdoRefreshAuthToken, signs"$requestID:$timestampMicros"with the EC P-256 keypair from pairing (Signature.NONEwithECDSAover the pre-hashed SHA-256 digest → matches Go'secdsa.SignASN1DER shape).GMessagesSession.refreshKeyPkcs8is now persisted via SessionStore.ReceiveServiceschedules the refresh attokenTtlSeconds - 3600s(1h safety margin) on connect. On success, saves the new session + reschedules; on failure, retries in 5min.
- 4 new anim XMLs in
res/anim/textrcs_overlay_*.xml. - New screen slides 100%→0 in 280ms; old screen slides 0→-30% in 280ms
(parallax push-underneath). Both use
fast_out_slow_in. - Close: new screen 0→100%, old screen -30%→0, 240ms,
fast_out_linear_in. - Wired via
<style TextrcsOverlayWindowAnimation>referenced fromAppTheme.ConvoActivityasandroid:windowAnimationStyle. No smali patch — purely XML at the OS animation layer. - Settings + new-conv FAB animations are unaffected (different themes).
- Smali patch at
e5/d.smali::m()now forwards parameter p3 (sentIntents: ArrayList<PendingIntent>) intoSendManager.sendSmsBridge. - After every GMessages POST: SendManager fires each PendingIntent with
Activity.RESULT_OKon success orSmsManager.RESULT_ERROR_GENERIC_FAILUREon failure. - This drives Textra's existing
reportSentIntentreconciliation chain (C5894fe→SmsMgr$Worker→C5217d.m7452N→m7459W→ DB state). - Net effect: messages transition pending→sent in the UI immediately after a successful POST.
- Sessions saved before v0.18.0 don't carry
refreshKeyPkcs8. The refresh code logs and skips for those; user re-pairs to enable automatic token refresh. Not a bug — a one-time schema migration.
/root/agent-work/projects/textrcs/
├── textra_base/ # The full apktool decompile we ship (modified)
├── inject_src/ # Our Kotlin/Java added to smali_classes4
│ └── com/textrcs/
│ ├── protocol/ # Constants, Session, http client, PBLite, crypto, longpoll, pairing
│ ├── ui/pairing/ # PairingActivity (button + WebView + emoji UI)
│ ├── send/ # SendManager (outgoing bridge)
│ ├── receive/ # ReceiveService + IncomingMessageHandler
│ └── gmproto/ # 317 protoc-generated Java classes
├── build.sh # javac → kotlinc → d8 → baksmali → merge → apktool b → apksigner
├── textrcs.keystore # RSA-2048, alias `textrcs`, pass `textrcs-pass`
├── reference/beeper.apk # Beeper APK as research reference
├── docs/
│ ├── AUDIT_REPORT.md # Pre-cleared malware audit
│ └── ANIMATION_TODO.md # OnePlus/iOS overlay parallax notes
├── PLAN.md, RCS_TRANSPORT_RESEARCH.md # Original architecture docs
├── CHANGELOG.md # Per-version log
└── textra2_v0.16.0.apk # Latest signed build
adb install textra2_v0.16.0.apk(works side-by-side with original Textra)- Launch Textra 2 Pair from app drawer (separate icon from regular Textra 2)
- Tap Connect to Google Messages
- Sign in with your Google account in the WebView
- The emoji panel appears with one large emoji
- Open the real Google Messages app on your phone, you'll see a notification asking you to confirm. Tap the matching emoji.
- Activity transitions to a result screen confirming session is paired and saved.
- Launch regular Textra 2 → conversation list opens (no SMS-app prompt)
- Open any conversation → type a message → tap send
- Behind the scenes:
SendManager.sendSmsBridgefires, message is encrypted with session keys and POSTed to Google'sinstantmessaging-pa.clients6.google.com/.../SendMessage. Recipient receives it via Google Messages on their device. - Incoming messages:
ReceiveServicelong-poll receives them. They are decrypted and visible inadb logcat | grep TextRCSIncoming. They do NOT yet appear in the Textra 2 conversation list (DB-write bridge is the outstanding work).