Skip to content

Releases: ShapeShifter499/joan-volte-lineage

v0.4.0-alpha49 (versionCode 59) — sec-agree rebuilt against the reference stack; raw Security-Server diagnostics

Choose a tag to compare

@ShapeShifter499 ShapeShifter499 released this 19 Sep 06:54

versionCode 59. Flash joan-volte-recovery-0.4.0-alpha49.zip in recovery. The zip is unsigned, so recovery will ask you to confirm on the handset — error: 21 is the signature code and is expected.

Verify what actually ran by the state provider, not dumpsys package, which serves a stale version indefinitely:

adb shell content query --uri content://org.joan.ims.state/registration

build should read 0.4.0-alpha49 (59).

Why this build matters if you are on a carrier that does not register

Most of the work since alpha38 is the security negotiation between the handset and your carrier's P-CSCF, rebuilt against the reference IMS stack rather than guessed at.

  • The Security-Server header the network sends is now written to the trace verbatim, with sec_rows= and sec_parsed= beside it. Until now the log only showed mechanisms we managed to parse, so "the network offered one option" and "the network offered four and we understood one" looked identical. If those two numbers disagree, that is the bug.
  • Security-Client and Security-Verify now go out as one header row per mechanism, which is what a stock handset sends. joan was comma-joining them into a single row — legal SIP, but not the shape carriers qualified their cores against.
  • A sole mechanism offered by the network is no longer refused because the carrier profile did not list it.
  • Retry-After is honoured properly. When the network asks for a wait, joan now waits — previously any network event cancelled it and a "wait ten minutes" became a retry in one.
  • Route and Supported: gruu are sent where the carrier's own configuration asks for them, and not where it does not.
  • AKA synchronisation state is now explicit in the trace (aka_sync=0 when the card accepted the challenge), so "the network says a resync timed out" can be told apart from "we requested one".

If you are testing China Mobile

This build is what the open question needs. Please send the app register lines and the new sec-server raw: line from:

adb shell cat /data/user_de/0/org.joan.ims/files/joan-trace.log

Also in this build

Carrier profile settings no longer depend on the carrier having a TCP criterion — previously a network with no shipped profile silently inherited the previous SIM's User-Agent policy, sec-agree offer and P-CSCF port. A call now keeps its registration when the IMS network blips mid-call instead of having it torn down underneath.

Known unchanged: China Mobile still fails at REGISTER. Nothing here is claimed to fix it; it is built to find out why.

v0.4.0-alpha38 (versionCode 48) — EF_DIR read fixed; supersedes alpha37

Choose a tag to compare

@ShapeShifter499 ShapeShifter499 released this 19 Sep 03:19

Supersedes v0.4.0-alpha37, which shipped the EF_DIR read broken. If you pulled alpha37, take this one instead.

Install: flash joan-volte-recovery.zip in recovery (say yes to the signature-check prompt — it needs a tap on the handset), or push the APK over an adb remount overlay.

Verify you are running it by the state provider's build row, not dumpsys package, which serves a stale version on this device:

adb shell content query --uri content://org.joan.ims.state/state | grep key=build
→ 0.4.0-alpha38 (48)

APK md5 6c241a3e630cbd7e467aa3e48ddcc9e8 · zip md5 b17124628a89732fd45e471fab9617f1

To remove it: flash joan-volte-uninstall.zip (md5 bdc97671552f1b2ce0f7e488cd9ce0ab, unchanged from alpha37). It takes the ImsService, the permissions file and the overlays back off, returning the handset to stock LineageOS behaviour. Flash this first if a build makes things worse — it is the way back.


The fix

alpha37's EF_DIR read never worked. The parser understood only the UICC's BER-TLV FCP (tag 0x62, TS 102 221 §11.1.1.3), and the platform does not deliver one: iccExchangeSimIO reaches the card through RIL_REQUEST_SIM_IO, and the RIL normalises the card's answer into the flat 15-byte structure of TS 51.011 §9.2.1. AOSP's own IccFileHandler parses exactly that and contains no BER-TLV code anywhere — UsimFileHandler overrides only getEFPath and inherits the same parsing.

So every live read answered ef_dir: no record geometry and fell back to guessing the AID. Registration still worked, which is precisely why it went unnoticed: the fallback is good enough on a card whose AID we already know, and useless on one we do not.

Both formats are read now. On the bench card:

ef_dir: records=4 aids=[A0000000871002FFFFFFFF8906190000,
                        A0000000871004FFFFFFFF8907030000,
                        A000000063504B43532D3135]
aka via ISIM apdu aid=ef_dir

That last line is the one that matters: authentication now uses the AID the card named, not one we guessed. Note also that the card's USIM suffix is ...8906190000 — the value hardcoded before alpha37 was ...8907090000, which that card would never have matched.

The trace now also reports the response length and leading bytes when a read fails, so the next card that misbehaves can be diagnosed without a new build.

Everything else from alpha37

alpha35, alpha36 and alpha37 are all superseded; this is written against alpha34, the last build most testers have. Calls behave as they did in alpha34 — this is about reading the SIM and the network correctly.

  • the SIM applet is selected by 3GPP AID prefix, not one card's issuer suffix, with EF_DIR above it as the authoritative source
  • ADF_ISIM is read directly when the framework reports no ISIM, recovering IMPI, IMPU and the home domain, and supplying EF_PCSCF as a third P-CSCF source
  • a P-CSCF given as a hostname is resolved — on the IMS network, behind a 2 s deadline — instead of discarded
  • every Security-Server row is read, not only the first
  • algorithm= in Authorization is the carrier profile's decision, RFC 3310's default where nothing says otherwise

Two settings surfaces are read but deliberately not advertised: Ut/XCAP, and the carrier's RTT settings with a T.140 parser. Neither is reachable from a call and neither declares a capability to the network or the dialer. Announcing RTT the handset cannot render would be worse than not offering it.

Verified, and not verified

Verified on the US998 bench handset, including a full uninstall-then-install pass on alpha37 (artifacts removed and restored on the mounted filesystem, not inferred from the installer's exit):

  • build row 0.4.0-alpha38 (48), on-device APK md5 matches the zip
  • EF_DIR reads 4 records; AKA authenticates with aid=ef_dir
  • registers on T-Mobile 310-260, reg1=401reg2=200 OK, spi_in=exact, aka=AKAv1-MD5

Not exercised on any handset, because this carrier and card do not reach them: the EF_PCSCF third source (the network link supplied all three addresses), hostname P-CSCF resolution (no named P-CSCF here), and multi-row Security-Server handling (T-Mobile sends one row).

For the China Mobile subscriber: worth trying, but for a narrower reason than the EF_DIR work above, and the honest odds are below even.

The change that actually touches your case is algorithm=. LG's own configuration for CMCC has that parameter off (sip_features 0x16000000, bit 24 clear), and joan used to send it unconditionally. From alpha38 it is omitted for CMCC and still sent for carriers whose profile asks for it. That alters the exact Authorization header of the REGISTER that draws the 404, which has never been varied before.

The EF_DIR fix is mostly diagnostic here rather than curative. Your AKA already succeeds, so the applet is already being found; what EF_DIR adds is a definitive statement of whether that card carries an ISIM at all, which has been an open unknown. The identity hypotheses remain closed for the reasons in docs/cmcc-wiring-and-trace-playbook.md — and 399 ... "Server Internal Error" describes their node failing after authenticating you, which is not "user unknown".

If it still fails, the trace lines to send are ef_dir:, aka via, and the full reg2_hdrs list.


Signed-off-by: Lance Gero3977@gmail.com
Assisted-by: Claude-Code:claude-opus-5

v0.4.0-alpha37 (versionCode 47) — SIM applet selection, ISIM reads, P-CSCF discovery

Choose a tag to compare

@ShapeShifter499 ShapeShifter499 released this 19 Sep 03:10

Superseded by v0.4.0-alpha38. The EF_DIR read in this build never worked — it parsed the wrong GET RESPONSE format and silently fell back to guessing the SIM's AID. Use alpha38 instead.

Install: flash joan-volte-recovery.zip in recovery (say yes to the signature-check prompt — it needs a tap on the handset), or push the APK over an adb remount overlay.

Verify you are running it by the state provider's build row, not dumpsys package, which serves a stale version on this device:

adb shell content query --uri content://org.joan.ims.state/state | grep key=build
→ 0.4.0-alpha37 (47)

APK md5 76a7babfbc9a4f5ce979e0f44488193e · zip md5 8db8551048a567a1597dfa4c110cc394

To remove it: flash joan-volte-uninstall.zip (md5 bdc97671552f1b2ce0f7e488cd9ce0ab). It takes the ImsService, the permissions file and the overlays back off, returning the handset to stock LineageOS behaviour. Flash this first if a build makes things worse — it is the way back.


What alpha37 changes

alpha35 and alpha36 were bench bumps that were never released, so this is written against alpha34, the last build testers have.

This release is mostly about reading the SIM and the network correctly, not about new call behaviour. Calls work the way they did in alpha34.

The SIM applet is selected by its 3GPP AID prefix, not one card's issuer suffix. The previous code matched a full AID ending ...8907090000. Our own bench card answers on ...8906190000, so any card whose ISIM sits behind a different issuer suffix was simply not found, and registration fell back to identities derived from the IMSI. Per ETSI TS 102 221 an AID is a registered prefix plus an issuer-specific tail, and the tail is the card's to choose — so it is matched as a prefix now.

EF_DIR is read, so the card is asked what applications it actually holds rather than guessed at from a candidate list.

ADF_ISIM is read directly when the framework reports no ISIM, which recovers IMPI, IMPU and the home domain from the card, and supplies EF_PCSCF as a third P-CSCF source behind the network link and the framework.

A P-CSCF delivered as a hostname is now resolved — on the IMS network rather than the default one, and behind a 2 s deadline — instead of being discarded for not being an IP literal.

Every Security-Server header row is read, not only the first. A proxy that lists its mechanisms across several rows was previously seen as offering just one.

algorithm= in Authorization is the carrier profile's decision. Where no profile says otherwise the RFC 3310 default applies. LG's own configuration turns this off for some carriers, so it is no longer emitted unconditionally.

Two settings surfaces are now read but deliberately not advertised: the Ut/XCAP configuration, and the carrier's RTT settings with a T.140 parser. Neither is reachable from a call, and neither declares a capability to the network or to the dialer. Announcing RTT the handset cannot render would be worse than not offering it.

Verified, and not verified

Verified on the US998 bench handset by a full uninstall then install pass:

  • uninstall removed the ImsService, the permissions file and both overlays — checked against the mounted filesystem, not inferred from the installer's exit
  • install restored all four; on-device APK md5 matches the zip
  • build row reads 0.4.0-alpha37 (47)
  • registers on T-Mobile 310-260, reg1=401reg2=200 OK, spi_in=exact, aka=AKAv1-MD5

Exercised live: the AID selection (the card answered on the full-length AID), the ADF_ISIM reads (impi=ok domain=ok impu=ok), and the carrier-profile algorithm= decision.

Not exercised on any handset, because this carrier and card do not reach them: the EF_PCSCF third source (the network link supplied all three addresses), hostname P-CSCF resolution (no named P-CSCF), and multi-row Security-Server handling (T-Mobile sends one row).

Known defect: the EF_DIR read runs and fails on the bench card with ef_dir: no record geometry — the record layout in the card's FCP response is not being parsed. This is not fatal: it falls back to the AID candidate list, which is what succeeded above. The feature simply does not deliver on this card yet.

For the China Mobile subscriber: this is the first build that can test the AID hypothesis for the post-auth 404, and none of that path has run against that card. If it still fails, the trace line to send is the one beginning aka via ISIM apdu together with any ef_dir: line.


Signed-off-by: Lance Gero3977@gmail.com
Assisted-by: Claude-Code:claude-opus-5

v0.4.0-alpha34

Choose a tag to compare

@ShapeShifter499 ShapeShifter499 released this 18 Sep 20:37

Install: flash joan-volte-recovery.zip in recovery (say yes to the signature-check prompt — it needs a tap on the handset), or push the APK over an adb remount overlay.

Verify you are running it by the state provider's build row, not dumpsys package, which serves a stale version on this device:

adb shell content query --uri content://org.joan.ims.state/state | grep key=build
→ 0.4.0-alpha34 (44)

APK md5 aefb29d04843984acb690c93e379e987 · zip md5 3b5df184e9bafd3446a7a9090014c9e6

To remove it: flash joan-volte-uninstall.zip (md5 9b705c4760cca2e9644f6bd32de8ce74). It takes the ImsService, the permissions file and the overlays back off, returning the handset to stock LineageOS behaviour. Flash this first if a build makes things worse — it is the way back.


What alpha34 changes

Bad calls sound better. PCMU calls (the codec cross-network calls often negotiate) now bridge lost audio frames instead of tearing: a lost frame is repeated as a fade of the last good frame over ~80 ms, then silence — and playback keeps its timing during packet loss instead of slipping. AMR calls already had decoder concealment; both now keep the audio queue draining at real time, which also stops the jitter buffer from discarding arrived audio as backlog during loss.

Failures say why. A refused INVITE now logs the carrier's reason phrase and Warning header (app invite reply=400 <reason>) — previously only the bare status code appeared, which could not say what the network objected to. Answered-into-silence calls are now visible: the trace notes "no audio Xms after answer" with packet counts while it happens, and every call's summary carries first_rx= and, when audio was late to start, blackout_ms=.

Fixed: the peer-reported jitter was being read from the wrong word of the RTCP receiver report — it showed the last-SR timestamp (huge numbers like 452198400 on clean calls). Now the actual interarrival jitter. Bench-validated with both-way AMR-WB and PCMU call sets at 0% loss.

For the Vietnam (Viettel) tester

Registration is fixed on this line of builds (REG2 200 OK over TCP). If your outbound call still fails with dial: start failed: invite refused, this build will record why in joan-trace.log — the line to send back is the app invite reply=... one.

For the Portugal (NOS) tester

Registration is fixed on this line of builds. For the audio-going-quiet report: the call-window trace lines (media start, media dl stopped ..., any IMS data_call lines during the call) are what's needed next.

For the China Mobile tester

Nothing new is asked of you on this build. The alpha33 verdict stands: the 404 arrives over TCP with all corrections in place, which points at the operator side — the question for China Mobile is whether this SIM's IMS subscription is provisioned, and the "Server Internal Error" from the zj.chinamobile.com node. Remember the network asks for long backoffs (retry_after=); let it rest between attempts.


Help support this project: real hardware, real SIMs, live-carrier testing and the AI compute to match — none of it sponsored. If this build put VoLTE on a phone that officially never had it, Ko-fi is the way to keep it moving.

v0.4.0-alpha33 (versionCode 43) — routing, caller-ID privacy, CMCC diagnostics

Choose a tag to compare

@ShapeShifter499 ShapeShifter499 released this 17 Sep 08:01

Install: flash joan-volte-recovery.zip in recovery (say yes to the signature-check prompt — it needs a tap on the handset), or push the APK over an adb remount overlay.

Verify you are running it by the state provider's build row, not dumpsys package, which serves a stale version on this device:

adb shell content query --uri content://org.joan.ims.state/state | grep key=build
→ 0.4.0-alpha33 (43)

APK md5 ce6bcdcccf8af813be627bfc38b39cfe · zip md5 5f1bea65f945975cdab064fbe0a8b5cf

To remove it: flash joan-volte-uninstall.zip (md5 c5970b27013aabc750051a482bb11774). It takes the ImsService, the permissions file and the overlays back off, returning the handset to stock LineageOS behaviour. Flash this first if a build makes things worse — it is the way back.


For the China Mobile tester

This build adds three fields to the registration trace so a failure can say why a transport was chosen, not just which one. They appear near the start of last_register:

reg1_crit=<n>  tpt_pol=<n>  plmn=<realm:NNNNNN | sim:NNNNNN | none>

Read plmn= first. none means no carrier-specific logic ran at all. sim:46002 is the expected China Mobile value.

docs/cmcc-wiring-and-trace-playbook.md has the full outcome table — what each trace shape means and what to do about it.

Changes since alpha29

Network routing. The REGISTER TCP criterion is now computed from the MTU exactly as AOSP's AosRegistration::SetTcpCriterionLength does, instead of being read from a 2017 vendor snapshot whose provisioned 0 we had been misreading as "unset" (it means always TCP; the unset sentinel is −10).

The home PLMN resolves from the SIM when the realm is branded. The important fix. T-Mobile's ISIM gives the realm as msg.pc.t-mobile.com, which carries no MCC or MNC — so every PLMN-scoped transport decision was silently skipped on the one carrier this project can test, including the T-Mobile exception itself, which had never once executed.

Encryption path. The protected TCP socket now closes with RST rather than FIN (SO_LINGER(0)), which is LG's own China Mobile remedy: under sec-agree both ends are fixed, so a lingering TIME_WAIT makes the next connect() fail. Switchable via setProtectedTcpLingerReset().

Retry-After is honoured on a REGISTER rejection. It previously applied only to a 503 on an INVITE, so a network asking for an hour was retried in sixty seconds.

Caller ID. Privacy is honoured on incoming calls (id/header withhold the identity, user drops only the display name) — previously a caller who withheld their number had it displayed. Every P-Asserted-Identity field is read with tel: preferred, and per-call caller-ID restriction now leaves the handset as Privacy: id.

Session-ID (RFC 7989) on call dialogs, HMAC-SHA-1-128 over the Call-ID, never on REGISTER.

Verified

LG US998, LineageOS 22.2, T-Mobile 310-260 — registered. With the PLMN exception bypassed in a throwaway build, the protected REGISTER went over TCP inside the IPsec SAs to a 200 OK, with the reset in place and the binding surviving three checks at 45-second intervals.

Not verified: China Mobile (needs the tester's SIM) and Session-ID on an INVITE (needs a placed call).

673 host checks; UA, registration, discovery, merge, xfrm and carrier suites clean.

v0.4.0-alpha26 (versionCode 34) — receive path

Choose a tag to compare

@ShapeShifter499 ShapeShifter499 released this 17 Sep 01:27

Prerelease. Offline suites pass (543 host checks, 260 UA checks); this is not a live-carrier qualifier.

alpha25 was never released, so this covers everything since alpha24.

After flashing, confirm the trace's build row reads 0.4.0-alpha26 (34).

Receive path (new in alpha26, and exercised on live calls both directions)

The trigger was a measurement error we had been living with: the loss=0% jitter=0 this stack printed on every call is the peer's report about our uplink — it says nothing about our own reception, which had never been computed. Once it was, the downlink turned out to have been lossy and jittery on nearly every call: interarrival jitter 174–229 ticks (~11–14 ms), with lost packets throughout.

  • Adaptive jitter buffer, constants taken from AOSP's JitterNetworkAnalyser rather than invented, bounded at depth + SLACK so it cannot accumulate latency it never gives back.
  • RTCP reception reports (RFC 3550) with a real report block. Every SR we sent previously went out with RC=0, so the network could not see what we received.
  • Packet-loss concealment — a gap is handed to the AMR decoder as an FT=14 SPEECH_LOST frame, which is what ImsMedia's IAudioPlayerNode does. Verified live at concealed=3 against lost=4, no decoder rejection.
  • Inbound telephone-event is no longer decoded as speech.
  • Our registration binding is matched by +sip.instance, not by address. Confirmed on-network: inst_seen=true inst_match=true.
  • AGC is out of the zip entirely and the installer no longer touches /vendor, where an earlier version could abort a whole flash.

New trace fields: rx{depth= queued= jitter= lost= reordered= late= trimmed=} and concealed=.

Call path (was alpha25, never released)

  • DTMF as RFC 4733 telephone-events, audio suppressed for the tone, digits queued so a post-dial string is not clipped.
  • Hold from the far end is accepted instead of refused 488, which ended calls on some cores.
  • Session timers (RFC 4028) from carrier config, with 422 retry.
  • b=AS / b=RS / b=RR in the SDP.
  • Registration event package (RFC 3680), SRVCC, local IP change during a call, 3xx redirects, 503 Retry-After, PRACK.

Flashing — three things that look like failure and are not

  1. The zip is unsigned. Recovery prompts on the handset and you must accept there; a host-driven adb sideload waits for that tap.
  2. error: 21 in last_log is recovery's signature-verification code, recorded even on installs that completed every step. Our installer's verdict is the last line on screen: Done. Reboot system., or an ERROR: naming the failed check.
  3. dumpsys package can report the previous version forever. Recovery's clock is wrong, so files land with a 2017 mtime that never invalidates PackageManager's cache. Check the trace build row and the APK md5 instead.

Known limits

  • CMCC (46002) reg2=404 is not fixed; this build adds diagnostics that should say why.
  • Session-timer refresh has never fired on a live call (needs one over 15 minutes).
  • Emergency calling is not carried by this app. See the README.
  • Visual voicemail is not provided by this app; a note in the README documents why activation fails on T-Mobile and why it is not caused by this stack.

v0.4.0-alpha24 (versionCode 32) — diagnostics only

Choose a tag to compare

@ShapeShifter499 ShapeShifter499 released this 16 Sep 11:13

This build adds logging only. No behaviour change from alpha23.
The same codecs are offered, the same selection is made, and calls that
worked before work exactly the same way. Nothing here fixes anything —
it makes one existing decision readable. If alpha23 is working for you,
flashing this changes nothing except what the trace says.

Sideload joan-volte-uninstall.zip first, then joan-volte-recovery.zip,
from LineageOS 22 recovery (not TWRP). Confirm build reads
0.4.0-alpha24 (32):

adb shell content query --uri content://org.joan.ims.state

What it adds

One field on the inbound INVITE trace line: what the caller offered, in
the caller's own order, with the octet-align state of each AMR entry.

app inbound INVITE tcp=true ... offer=AMR-WB/104(oct=1),PCMU/0

Why that one field

An answered call reporting codec=PCMU is unreadable on its own. It looks
identical in two cases that need opposite responses:

  • the caller listed PCMU first and we honoured their order — the design
    working, nothing to fix;
  • the caller offered AMR but bandwidth-efficient (oct=0), which is
    not implemented here, so it was skipped — the known blocker, and the
    thing that would need real work.

Until now nothing recorded which of those happened.

If you are chasing missing audio on answered calls

This is the line to send. On a US998 on T-Mobile, alpha23 chose PCMU in
both directions even though AMR-WB, AMR-NB and PCMU were all offered
outbound — so that network simply prefers G.711, and that much is now
settled. Whether the same is true on yours, or whether AMR is being
skipped because of the framing, is exactly what offer= answers.

Unchanged from alpha23

  • The SDP answer follows the offerer's preference (AMR-WB, AMR-NB, PCMU)
    and echoes their payload number.
  • The offered profile is narrowed at startup to codecs this ROM can both
    encode and decode, each proved by a round-trip. codec profile: at
    startup says which.
  • A negotiated codec that cannot be opened ends the call instead of
    silently carrying G.711 on another codec's payload type.

Known limits, unchanged

  • Only octet-aligned AMR. oct=0 in the new line means that codec was
    skipped for this reason.
  • No DTMF (RFC 4733).

v0.4.0-alpha23 (versionCode 31)

Pre-release

Choose a tag to compare

@ShapeShifter499 ShapeShifter499 released this 16 Sep 11:04

Supersedes v0.4.0-alpha22 — use this one instead. alpha22 shipped a
self-test that reported every working AMR codec as broken and reduced
every device to codec profile: PCMU, so it is safe but delivers none of
the codec negotiation it was cut for. If you already flashed alpha22, this
is the build that actually carries the change.

Sideload joan-volte-uninstall.zip first, then joan-volte-recovery.zip,
from LineageOS 22 recovery (not TWRP — see the README). Confirm
build reads 0.4.0-alpha23 (31):

adb shell content query --uri content://org.joan.ims.state

What this build is for

Answered calls used to run G.711 no matter what the caller offered — the
SDP answer was hardcoded to PCMU and the inbound path never recorded a
codec at all. The answer now walks the offerer's payload order and takes
the first codec we can carry (AMR-WB, AMR-NB, then PCMU), echoing their
payload number rather than ours.

If your answered calls connected but had no incoming audio, this is the
change to test.
A network that offers AMR alongside PCMU but only really
carries AMR produces exactly that, and this is the first build able to
answer AMR at all.

Two bugs found by flashing alpha22

Both were caught by the new startup probe, on a real device, and neither
was visible offline:

  • The self-test fed a single frame and demanded output. The codecs
    pipeline — their own contracts say a first frame may produce nothing —
    so it failed every healthy codec.
  • MIME_NB was "audio/amr-nb", which is not an Android MIME type.
    Narrowband is audio/3gpp. AMR-NB could never open on any Android build
    since the code was written, regardless of what the offer promised.

What we offer now matches what your ROM can run

AMR runs through the platform's MediaCodec, so a ROM update can add or
drop it without this app changing. At startup the profile is narrowed to
codecs the device can both encode and decode, and each is proved by
opening it and round-tripping frames before it is ever advertised. The
trace prints codec profile: at startup and a codec= line per call.

On the US998 bench this reports codec profile: AMR-WB,AMR,PCMU.

A silent failure is now an audible one

If a negotiated codec cannot be opened, the call used to continue while
streaming G.711 on the other codec's payload type — nothing decoded in
either direction, the call still showing as connected. It now ends the
call instead.

Known limits

  • Only octet-aligned AMR. An AMR offer without octet-align=1 means
    bandwidth-efficient, which is not implemented, so it is skipped and the
    next codec is used. This is the remaining reason AMR may still not run
    on your network.
  • No DTMF. Matters more once AMR carries a call: in-band tones do not
    survive a speech codec.

Verified, and not

Registration, calls both ways with two-way audio, hangup from either side
and the ~29 minute re-registration were verified on a US998 on T-Mobile.
The codec profile above was verified on that handset. What has not
happened yet on any device is a call actually carried over AMR — every
call this stack has ever made was PCMU, because until this build AMR-NB
could not open and AMR-WB was never selected. If an answered call now
fails where it used to connect silently, that is this change, and the
codec= line in the trace says why.

v0.4.0-alpha22 (versionCode 30)

Pre-release

Choose a tag to compare

@ShapeShifter499 ShapeShifter499 released this 16 Sep 10:54

Codec negotiation. Answered calls used to run G.711 no matter what the
caller offered; they now follow the offerer's preference. Sideload
joan-volte-uninstall.zip first, then joan-volte-recovery.zip, from
LineageOS 22 recovery (not TWRP — see the README).

Confirm build reads 0.4.0-alpha22 (30):

adb shell content query --uri content://org.joan.ims.state

dumpsys package can still report the previous version until
PackageManager rescans; the state provider reads the APK and is the one
to believe.

Answered calls ignored the negotiation entirely

sdpAnswer() emitted PCMU unconditionally and the inbound path never
recorded a codec at all, so every answered call ran as G.711 whatever the
caller asked for. The answer now walks the offerer's payload order and
takes the first entry we can carry — AMR-WB, AMR-NB, then PCMU — and
echoes their payload number, since AMR is a dynamic type and answering
with our own number leaves the far end sending what it named while we
decode something else.

If your answered calls had no incoming audio, this is the change to
test.
A network that offers AMR alongside PCMU but only really carries
AMR produced exactly that, and this is the first build that can answer
AMR at all.

AMR-NB is offered

It is the codec IR.92 makes mandatory and it was missing from the offer,
so a network wanting narrowband AMR could only fall back to G.711.

The offer now matches what your ROM can run

AMR goes through the platform's MediaCodec, so a ROM build can add or
drop it without this app changing. At startup the profile is narrowed to
codecs the device can both encode and decode, and each one is proved by
opening it and round-tripping a frame before it is ever advertised. The
trace prints codec profile: at startup and app ANSWER 200 codec= per
call.

A silent failure is now an audible one

If a negotiated codec cannot be opened, the call used to continue while
streaming G.711 on the other codec's payload type: nothing decoded in
either direction, the call looked connected, and one line in the log
explained it. It now ends the call instead.

Known limits

  • Only octet-aligned AMR. An AMR offer without octet-align=1 means
    bandwidth-efficient, which is not implemented, so it is skipped and the
    next codec is used — usually PCMU. This is the remaining reason AMR may
    still not run on your network.
  • No DTMF. Matters more once AMR carries a call: in-band tones do not
    survive a speech codec.

Verified, and not

Registration, calls both ways with two-way audio, hangup from either
side, and the ~29 minute re-registration were verified on a US998 on
T-Mobile — on alpha21. The codec work in this build has full offline
coverage and has not yet carried a call on a handset, because every call
this stack has ever made was PCMU, so the AMR path has never run. That is
the point of the release: the new trace lines say which codec was chosen
and whether it opened.

If an answered call now fails where it used to connect silently, that is
this change, and the codec= line says why.

v0.4.0-alpha21 (versionCode 29)

Pre-release

Choose a tag to compare

@ShapeShifter499 ShapeShifter499 released this 16 Sep 10:17

Two carrier-blocking defects, an AKA recovery path, and better diagnosis
of a rejected REGISTER. Sideload joan-volte-uninstall.zip first, then
joan-volte-recovery.zip, from LineageOS 22 recovery (not TWRP — see
the warning at the top of the README).

Check build reads 0.4.0-alpha21 (29):

adb shell content query --uri content://org.joan.ims.state

dumpsys package may still say alpha20 until PackageManager rescans. The
state provider reads the APK directly and is the one to believe.

Outbound calls failed with 400 after any incoming call

The top Via claimed SIP/2.0/TCP on requests that left over UDP, because
an inbound TCP connection from the P-CSCF flipped a global that fed the
Via of every outbound request. A P-CSCF answers that mismatch with 400
Bad Request — AOSP's IMS stack says so in as many words — so once a single
incoming call had arrived, every outbound call failed for the life of the
registration, and a re-registration cleared it until the next one.

The Via is now set where the message is actually written, so a request
names the socket it leaves on, and responses keep the Via they echo.

Viettel: outbound calling should work, including after an incoming
call.
That specific order is the thing to try.

REGISTER over TCP could never succeed

Building REG1 twice — a UDP variant and a TCP variant — produced two
different transactions, and the reply was matched against the variant that
had not been sent. Every REG1 sent over TCP failed as reg1 mismatch,
discarding a valid 401.

Carriers whose profile puts REG1 on TCP were blocked by this outright.
That includes CMCC, where registration was impossible by construction:
the only attempts that ever reached authentication were the ones where the
TCP connect timed out and the UDP fallback ran.

CMCC: look for reg1=401 on a line that also says reg1_tpt=tcp.
That single pairing is what says the fix landed. Registration may still
stop at reg2=404 — that is a separate, still-open problem, and this
release adds the logging to identify it (below).

A rejected protected REGISTER now says what was refused

A 404 to REG2 after a valid challenge means the core refused the identity
asserted, without saying which part. Non-2xx replies now carry the core's
own Warning and Reason, plus the To and Request-URI domains — enough
to separate a wrong home realm from a rejected registration. Domains only:
a derived IMPU contains the IMSI, so everything before the @ is dropped.

AKA synchronisation failure is recovered, not mislabelled

A card reporting that its sequence number had drifted was reported as
FAIL: aka parse, as if the parser were broken. Nothing retries out of
that state — the HSS keeps issuing vectors from the same stale batch — so
a resynchronisation REGISTER carrying auts= is now sent, once, and the
fresh challenge is required to carry a new nonce.

The AKA call is also timed (aka_ms=), which separates a real card
authentication from a telephony error that never reached the card.

One Call-ID and a rising CSeq per REGISTER series

Every attempt used to mint a fresh Call-ID and restart CSeq at 1, so a
registrar saw a burst of unrelated registrations rather than repeated
tries at one. RFC 3261 10.2 asks for a single Call-ID per registrar.

Verified, and not

Verified on a US998 on T-Mobile: registration, inbound and outbound calls
with two-way audio, hangup from either side, and three airplane-mode
cycles each re-registering cleanly with no rejections.

Not yet verified: the ~29-minute registration refresh under the new
Call-ID handling. If a build registers normally and then drops about half
an hour later, that is the suspect — please say so, with the state
provider output.

The Viettel and CMCC paths cannot be exercised on the bench handset, so
those two fixes are reasoned from tester logs and confirmed against AOSP's
IMS stack (packages/modules/ImsStack, tag android-17.0.0_r1) rather
than reproduced locally. See docs/upstream-references.md.