Repository navigation
Release/1.33.6 - #2238
Merged
Merged
Release/1.33.6#2238
Conversation
It sat on the ComposeView host in XML while the tap target is the Avatar inside it. Compose publishes its own semantics tree, so whether the host's description survived depended on composition timing -- intermittently leaving the avatar unlabelled for accessibility services, and unfindable by anything addressing it by description. Removed from the XML rather than left in both places: the same description on two nodes of one tree is the ambiguity being fixed, not redundancy. Nothing read it there -- the id is used as a constraint anchor and for setThemedContent only.
… failing quietly - `sessionProBackendStatus=active` mapped to AUTO_GOOGLE, an auto-renewing plan with a fixed +14d renewal rendering `proAutoRenewTime`. iOS's `active` is not auto-renewing, so the shared token meant different things per platform. Map it to EXPIRING_GOOGLE_LATER, which matches iOS. Side effects, neither asserted today: the cancel-access row disappears (it is auto-renew only, as on iOS) and the plan length becomes 12 months. - New `sessionProAccessExpiry` extra, applied over whichever fixture was selected: epoch seconds, or a relative `+<n><s|m|h|d>`, or `useactual`. Rejects a resolved instant more than ten years out, which catches milliseconds passed as seconds. Seconds because that is what iOS's field is; the preference keeps storing millis internally. - Log loudly on any unrecognised `session*` extra. Previously a typo was a silent no-op, so it failed as a wrong assertion rather than a setup error. - EXPIRED and EXPIRED_APPLE were labelled "Expired 2 days ago" while the code used now - 14 days. Correct the labels; the offsets are the contract. - Build every instant in the debug branch from `snodeClock`, read once per recomputation. The render side already used it, so the fixture's device-clock instants left the offset between the two clocks in the result — a 30-day fixture could render "31 days". `quickRefundExpiry` had the same defect. - Robolectric coverage over the new extra, driven through `QaLaunchConfig.apply` with real Intents so the parse, the range guard and the preference write are tested together.
… remaining surfaces - `SessionProSettingsHeader` applied `clearAndSetSemantics` to the whole column, which erases descendants rather than merging them — so the status banner was absent from the tree entirely, text and all, and TalkBack announced "Session Pro" over a message users never heard. Narrow it to the decorative logo and badge, leaving `extraContent` a sibling inside the column that carries `onSizeChanged` (moving it out would change the gradient ratio). - Tag both banner variants with `pro-settings-status-banner`. One id for the slot; the four states are told apart by their text. - Section-header ids for Stats, Manage and Pro Beta Features, via one optional `CategoryCell` parameter so the other call sites are untouched. - Name the conversation-header badge, matching iOS.
…de-qa Pro: QA backend override, a proof-acquisition floor, and a unit-test class-init fix
…v/androidx.compose-compose-bom-2026.06.01 Bump androidx.compose:compose-bom from 2026.03.01 to 2026.06.01
…v/com.squareup.okhttp3-okhttp-5.4.0 Bump com.squareup.okhttp3:okhttp from 5.3.2 to 5.4.0
…v/lifecycleVersion-2.11.0 Bump lifecycleVersion from 2.10.0 to 2.11.0
…otlinx-collections-immutable-0.5.1
…v/org.jetbrains.kotlinx-kotlinx-collections-immutable-0.5.1 Bump org.jetbrains.kotlinx:kotlinx-collections-immutable from 0.4.0 to 0.5.1
…v/coilVersion-3.5.0 Bump coilVersion from 3.4.0 to 3.5.0
Two independent dependabot bumps each broke the build on dev, and the first masked the second. - compileSdk 36 -> 37, required by lifecycle 2.11.0 (#2157), which fails checkAarMetadata below 37. targetSdk stays at 36. Reverting #2157 was considered and declined: compileSdk is a change we control. Note there is no `platforms;android-37` package — it is android-37.0, and AGP resolves compileSdk = 37 to it with no compileSdkMinor needed. - Dagger/Hilt 2.59.2 -> 2.60.1, to read the 2.4.0 class metadata that coil 3.5.0 (#2159) pulls in via kotlin-stdlib 2.4.0; 2.59.2's bundled reader caps at 2.3.0 and failed hiltJavaCompile. Kotlin stays at 2.3.20 deliberately — bumping it would emit 2.4.0 metadata from our own classes too. CI provisions no Android SDK (no setup step in build_and_test.yml, and gradle.properties sets no sdkDownload flag), so it relies on the runner image plus AGP auto-download. If the image lacks android-37.0, CI will fail on the SDK and that reads as unrelated to this change.
Fix the dev build: compileSdk 37 and Hilt 2.60.1
The comment here asserts the backend judges status against expiry + grace_period_duration. It does not — it folds grace into the stored expiry before sending it, so the wire expiry is coverage end, not paid-through. Two consequences below it: inGracePeriod sits in a branch that requires now <= expiry so it is unreachable, and the renewal date renders one grace period late (an hour on Apple, operator-configured days on Google Play). Comment only. Desktop and iOS encode the same premise in different wordings, so correcting the logic is a three-client change and is with the architect. The superseded text is kept verbatim because the other clients' comments echo it and it needs to stay greppable.
Two couplings removed. It refreshed get_pro_status after minting a proof, which is redundant — the access-expiry write it already makes is a config change that fires the status trigger on its own. And it gated on the in-memory load state holding an ACTIVE status, which is wrong in a way that only shows after a restart: WorkManager persists the proof schedule across process death but that state restarts empty, so a renewal falling due while the app was dead read as "not Pro, no purchase" and returned without renewing. It now asks libsession the same question the scheduler asks — pro_renewal_target, from config.
…startup gate Two new name/value rows, no migration. Both stamp the ATTEMPT rather than the success: a request that reached the server costs it the same whether or not it succeeded, and stamping on success makes a client retry hardest exactly when the server is least able to take it. They are separate keys on purpose. A routine refresh must not consume the gate's 24h budget, and a startup fetch from twenty hours ago must not satisfy the 60s floor — one value cannot answer both. Neither can reuse pro_status_updated_at, which is written in the same statement as the response blob and is unreadable without it, so a failed fetch has nothing to record there.
…an unconfirmed status as confirmed One commit because force -> immediate renames a parameter every one of these files calls, so they cannot compile apart. - Gate the startup get_pro_status fetch on whether a home CTA could plausibly fire, from synced config, with a persisted 24h minimum. Every cold start used to fetch, including for users who have never subscribed, and mobile cold-starts constantly. - Make the 60s floor apply. It asked whether the load state was Loading or Loaded; a cold start begins at Init, which is neither, so it was skipped on exactly the path it exists for — and every background caller passed force=true anyway. It now reads the persisted attempt timestamp, and a process that has never fetched is exempt so the floor cannot become the "is one already running" mutex. - force -> immediate, reserved to three callers named in the KDoc. - Only report a confirmed status when THIS process confirmed it. The state mapping ended in a catch-all that swept up both Init and a Loaded restored from WorkManager's persisted work state, so at launch the refresh state read Success against a cached response and a renewal that happened while the app was closed splashed the Expired CTA off stale data. Reported in the wild. The catch-all is gone: the mapping is exhaustive, so the next state added has to declare which it is. - Pro settings: refresh on open, and while open poll once a minute from when the renewal falls due until it lands, coverage ends or the screen closes. The grace label now needs a completed fetch at or after the crossing — a snapshot predating it cannot have seen the failure it warns about. - Persist auto_renewing into config alongside the access expiry, unconditionally: libsession short-circuits a no-change write on a clean config, and a presence-based guard would be wrong because the key is erased rather than stored when false. Two gate rows are deliberately built wrong and pinned by tests that will fail when the decisions land: row 1 cannot see the real grace window (the expiry is grace-inclusive), and the not-auto-renewing-but-active row declines to fetch while auto_renewing is presence-only. Both match Desktop and iOS; the PR description carries them.
…e renewal is actually due The access expiry get_pro_status sends is grace-INCLUSIVE — the backend folds grace in before sending it and judges "active" against that same value, and its own test subtracts grace from the wire value to recover the store's date. So the expiry is coverage end, and the renewal falls due a grace period earlier. Three things were wrong as a result: - The renewal date rendered a whole grace period late. - The grace indicator was unreachable: it tested now >= expiry inside a branch that requires now <= expiry, so only a single instant satisfied both. - The startup gate's first row declined to fetch for the whole grace window, because that window lies inside "now < coverage end" — the gate was blind to exactly the state this rework exists to surface. Subtracting is unconditional and needs no provider branching: the wire sends grace = 0 whenever the subscription is not auto-renewing. Not cosmetic where it mattered. Apple configures no grace, so the backend's ~1h stand-in is the whole of it. Google's is the operator-configured base-plan value in DAYS, fetched exactly when the subscriber enters grace — so the date was days wrong on the screen whose purpose is that date, and most wrong precisely when someone was looking at it. Also moves the grace warning's "a completed fetch at or after the renewal fell due" condition into toProStatus, where inGracePeriod is produced, instead of applying it at each consumer. Five call sites read that flag; a sixth would have inherited no protection. Needs the grace key in synced config, so the wrapper's submodule pin moves with it.
The proof response refreshes the cached access expiry, and the two fields that must travel with it cannot be written yet. The intended lines are in the source, commented, with the blocker named at the site. Both incoherences are live today rather than introduced here: - auto-renewing goes stale: an account whose expiry only came from a proof reads back as not-renewing while it is renewing, because the key is presence-only and nothing on this path sets it. - grace pairs with the wrong expiry: paid-through is derived as E - G everywhere, so a fresh E beside a G from an older response is wrong by the difference. Worst where it matters, since Google reports its real multi-day grace only once the subscriber enters grace — a proof landing first pairs a new coverage end with the old stand-in. The hole is in the middle of the chain: the backend sends both account_auto_renewing and account_grace_period_duration, but parse_pro_proof reads only account_expiry_ts, so neither reaches the response type or the wrapper. A core commit lands first. No client-side workaround: reading the raw response behind libsession, or sourcing the flag from anywhere other than this outcome, are both worse than the staleness.
… the access expiry A proof response refreshes the cached access expiry, and the two fields that must travel with it are now written alongside: the renewing flag, and the grace period the expiry has folded into it. Without them an account whose expiry only ever came from a proof read back as terminal while it was renewing, and `E - G` -- which everything now uses for the paid-through instant -- was wrong by the difference between two responses. Worst where it matters: Google reports its real multi-day grace only once the subscriber enters grace, so a proof landing first paired a new coverage end with the ~1h stand-in. Written ONLY when the backend actually sent them. Absent means "did not say", not false or zero, and collapsing that is destructive rather than merely lossy: both config keys are presence-only, so writing false or zero erases them, and a backend predating these fields sends neither. A `?: false` in this path would make every proof fetch wipe a value correctly learned from get_pro_status. Needs the wrapper's matching commit, which re-pins libsession to the parse.
The single user_expiry wake fired 30s after the access expiry the backend sends. That value is grace-inclusive, so it is the instant coverage ENDS -- the renewal became overdue a whole grace period earlier, and the wake fired after the window it exists to catch had already closed. It was the last trigger still keyed to the pre-correction reading of that field. Now wakes 30s after the renewal falls due. Subtracting grace is a no-op for a non-auto-renewing account, where the wire sends zero. Note what this does NOT add: nothing wakes at the end of coverage once grace has been entered. A failed renewal is learned at the renewal date, and after that the account is covered by the startup gate -- which now fetches for exactly that state -- plus on-enter and the while-open poll. Whether a second wake at coverage end is wanted is an open cross-client question.
…y comment Trigger #6 becomes two instants: 30s after the renewal falls due, and 30s after coverage ends. The second is guarded on the two coinciding, which is the case for every non-auto-renewing account, where grace is zero. It looks redundant with the proof loop and isn't. The backend issues a proof good until roughly an hour past coverage end and the renewal target sits ~1h before that, so a proof attempt lands near coverage end and its config write fires the config-change trigger -- but only when E actually MOVES. A succeeded renewal advances E and is covered; a FAILED one leaves E unchanged and nothing fires. The uncovered branch is the one the grace warning exists for. No wake handles to leak: transformLatest cancels the whole body when E moves, so a wake armed against a superseded expiry cannot outlive it. A scheduler holding ids would need a collection for two instants -- holding one and scheduling two orphans a timer every period, while both wakes still fire correctly. Also corrects the severity claim in the grace comment. The multi-day operator-configured value is written only on Google's IN_GRACE_PERIOD notification and reset to the ~1h stand-in on RECOVERED/RENEWED, so it exists only during the grace window. A healthy auto-renewing subscriber has grace of about an hour on both stores, so the renewal date was an hour late, not days. The unreachable grace state was the real defect, and the gate's blindness to the window is the consequence that genuinely spans days.
… test backend Both user_expiry wake emits reach the network through the floored path, so when the grace period is shorter than the 60s freshness floor the second wake lands inside the floor the first one just armed and its fetch is dropped. The wake fires; the fetch does not, which looks exactly like the wake was never scheduled. Only reachable on a compressed testing backend -- google_play/mule.py overrides grace with testing_grace_period_duration_ms = 10s. Production grace is either the backend's ~1h stand-in or an operator-configured value in days, both far outside the floor. Documented in two places because the two readers arrive from opposite directions and neither has reason to open the other's file: at the floor constant, for someone tuning or removing it, and at the wake, for someone debugging why it "didn't fire". Left alone deliberately. If Pro UI-test work needs to exercise the coverage-end wake, the sanctioned escape hatch is an env-var override of the floor, owned by that work. Not an immediate fetch for a scheduled trigger -- that is what force -> immediate was introduced to stop.
libsession now REQUIRES both fields on a successful proof rather than treating them as optional, so there is no "the backend did not say" state left to guard against and the presence checks come out. That is safe for a specific reason rather than by simplification: a missing or malformed field fails the parse, so we never reach this write. Writing false to a presence-only config key ERASES it, so a defaulted false would have been destructive rather than inert -- requiring the field is what makes an unconditional write correct here. Inside the success branch both values are truthful. Needs the wrapper's matching commit, which re-pins libsession to 799f1972.
The write is already inside the success branch and its behaviour is unchanged. The comment justifying it was wrong: it said a missing or malformed field fails the parse so the write is never reached. That is not what protects it. libsession's parse_pro_proof returns on the failure path BEFORE filling the renewal flag and the grace period, so on every non-OK outcome they hold struct defaults of false and zero. There is no presence flag on the C struct and the Kotlin type is non-nullable, so a read outside the success branch gets false and cannot tell it from a backend that genuinely said "not renewing" -- and writing false to a presence-only config key ERASES it. Erasing is truthful for subscription_expired, not_subscribed and revoked. For a protocol error, a stale request or a transport failure it would wipe a flag get_pro_status had correctly learned. So the comment now says the protection is PLACEMENT, not the parse and not the type, and not to hoist the two writes out of the success branch. Needs the wrapper's matching commit.
The backend no longer folds grace into the stored expiry — that fold was removed upstream, and `grace_period_duration` now reports how much longer we serve PAST the expiry sent. The backend states it directly: "expiry_ts + grace_period_duration is exactly when we stop serving". So every subtraction goes away and the display gets simpler: - the renewal date is `expiry`, rendered as sent - the grace window is `[expiry, expiry + grace)` - coverage ends at `expiry + grace` - trigger #6 wakes at `expiry`, then again at `expiry + grace` Grace now enters the startup gate in exactly one row, and earns its place there: the auto-renewing row is bounded from COVERAGE end rather than the payment date, so an account inside a multi-day grace is never mistaken for a long-dead one, and an account whose renewing flag was never cleared stops fetching on every cold start forever. The earlier reading was traced against a checkout 70 commits behind `origin/dev`, which is the whole lesson: the staleness trap already documented for the client repos was never applied to the backend.
The status fetch wrote E and A but never G, so the expiry it stored paired with whatever grace a proof outcome last left — or with nothing at all. Everything downstream reads coverage as `E + G`, so the second wake at coverage end and the settings grace poll's termination bound were both computed against a grace this path never supplied. Takes the ACCOUNT-level field, not `latestPayment.gracePeriod`: they share a name and answer different questions — how much longer we serve, versus what a store declared about one transaction.
…ooldown Renew config message TTLs at most once an hour per swarm
Brings back the post-Pro-launch preference (pref_force_post_pro) removed in 0b751f7, now off by default in every build, with a `sessionPro` launch extra (the iOS key) so QA harnesses can turn it on. Off, this account can neither use nor buy Pro and nothing is restricted for lacking it; other people's Pro (badges, message features) is still honoured. One commit so it reverts cleanly. - Every self-facing guard 0b751f7 deleted, reapplied in the code's current shape. The logged-in Pro loops share one gate; turning it off cancels the status and proof workers, while the revocation list keeps polling for other people's proofs. Clearing our own revoked proof stays gated. - Recipients always get a Pro data context; only our own proof is skipped, so a proof synced from another device grants nothing here and is left in config. - Gates Pro code added since the removal: the display plan seeded from config, proof renewal scheduling and the proof worker, the access source (outgoing proof and declared features), the rotating-key signature, and the purchase-in-flight path. - The conversation list's pref-event filter regains SET_FORCE_POST_PRO with the `||` it was missing, so the two force-Pro events are no longer ignored.
…in-translations [Automated] Update translations from Crowdin
…gate Pro: restore the Session Pro gate, off by default
…n them Messages the client positively identifies as unprocessable are now kept instead of dropped. Random decryption failures still produce nothing. - One-to-one data starting 0x00 is a newer wire format. It's retained without decrypting and raises an account-level "update" banner on Home, with no bubble, since its sender can't be known. - Decrypted content with a top-level field above 18 and nothing usable gets a placeholder bubble in an existing conversation (never a new one), incoming or outgoing for our own syncs and group messages. Previously it was rejected as an invalid message. - Raw swarm data is kept in `unsupported_message` (same schema on all clients, for the future 2.0 import), capped at 256 MB and expired with disappearing settings. An mms delete trigger keeps it in step with placeholder deletion. - Retained messages are replayed at startup after an update, replacing placeholders in place. The bubble and banner strings are hard-coded English and need adding to Crowdin.
- Push metadata `t` is already milliseconds; stop converting it, and pass the swarm expiry `z` through as the server expiry on both push paths. - No placeholder in a hidden conversation; the record is still retained and the conversation stays hidden. - Add `sender` and `sent_timestamp_ms` to `unsupported_message`, set for unknown types and NULL for the newer format. - An unsend request deletes retained records with the same sender and sent timestamp. - A failed reprocess keeps the record and stamps the version. The placeholder is detached before it's deleted, so a failed replay leaves the record in place without one. - Enforce the retention limits and reprocess on launch and on returning to the foreground; limit failures are caught and logged.
- Add unsupported_message_stats (seeded single row), kept exact by insert/delete/update triggers, and the unsupported_message_kind_id index - Count each row as length(data) + 256 against the 256 MB budget, evicting oldest newerFormat rows first in batches of up to 100 - Cap newerFormat rows at 10,000, evicting the oldest - Check the caps from the stats row after each insert, with no table scan; scheduled maintenance still removes expired rows first - Stamp newerFormat rows in one UPDATE and replay unknownType rows in pages of 50, choosing by stored kind rather than data bytes - Name the protobuf wire types in the field scan
…ased them
When an admin deletes a group, the deleting device destroys the group's info, pushes it, then erases
the group from UserGroups. A linked device usually sees the destroyed info first and marks its
UserGroups entry destroyed. libsession merges the two diverged configs by replaying each diff, and if
the erase applies first the destroyed-flag diff recreates the erased key holding only that field: an
entry with the removed status and no name or keys. That entry then syncs back, so the group reappears
on the deleting device ("Unknown Group" on iOS, the "deleted by a group admin" preview elsewhere) and
stays on the linked device.
A real kicked or destroyed entry always keeps its name, so a removed entry with no name, no admin key
and no auth data can only be this. Erase it as part of handling the merge and never build a
conversation from it; any conversation we still have for it is removed through the normal path. Other
admins keep their full entry and still see the deleted state.
The same rule is applied on iOS, Android and Desktop. The underlying merge behaviour is tracked
separately for libsession.
The daemon JVM was pinned to JetBrains 21, fetched through api.foojay.io package-id redirects. foojay stopped serving those ids (foojayio/discoapi#165), and CI's setup-java Temurin 21 doesn't meet a JetBrains criterion, so every CI build failed provisioning the daemon before it started. Drop the vendor so any JDK 21 runs the daemon: CI's Temurin 21 is used as is, and locally Android Studio's JBR 21 still is. For machines with no JDK 21, the Linux/macOS/Windows URLs now go straight to Adoptium's API instead of through foojay; FreeBSD/Unix keep foojay, as Adoptium has no builds for them. Re-running updateDaemonJvm overwrites the file.
The api.adoptium.net 'latest' URLs would hand a different JDK to a fresh machine whenever Adoptium publishes a 21.x update. Point each platform at the 21.0.12.1+1 release asset instead, so a provisioned daemon JDK only changes when this file does.
The previous commits edited gradle-daemon-jvm.properties by hand, so running updateDaemonJvm (to move to a newer JDK, say) would have put the JetBrains vendor and foojay URLs back. Set languageVersion and the Temurin download URLs on the updateDaemonJvm task instead, leaving the vendor unset; the properties file is again plain generated output, and regenerating it reproduces it exactly. Adoptium has no FreeBSD or other Unix builds, so those platforms get the Linux ones. That matches what the generated foojay entries did (FREE_BSD/UNIX shared the Linux package ids), and those ids now return 400 as well.
Run the Gradle daemon on any JDK 21 and download it from Adoptium
The erase ran inside the merge's write, which ConfigUploader doesn't push (it drops fromMerge notifications) and which only dumps the merged config type: the UserGroups erase waited for some later local change to be pushed, and the convo-info-volatile erase could be lost on restart. Finish through removeGroup, whose write dumps and pushes like any local change; the erase inside the merge stays so nothing reads the stub back first. Also state the name assumption as the convention it is: libsession's header calls the name invite-only.
Erase group entries a config merge recreated after another device erased them
…in-translations [Automated] Update translations from Crowdin
…bubble Show a placeholder for messages this version can't process, and retain them
1.34.0 was cancelled and its changes ship as a patch instead. The code stays above 453, which the cancelled 1.34.0 used (Play 4531-4535).
Bump version to 1.33.6 (454)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.