Commit Graph
10 Commits
Author SHA1 Message Date
Jason Ross 5994ee965e Merge branch 'main' into perf-unified-inbox-paging 2026-07-02 09:35:11 -05:00
JMR-devandClaude Fable 5 f8d03a4343 perf(mailbox): page the unified "All inboxes" list (#124)
The unified inbox query (WHERE folder = ?, no accountId) has no folder-leading
index, so it scans in timestamp order and materializes the whole unified inbox
(~4k rows at a 20k cache) into memory on every emission. Apply Paging 3 to the
unified browse path so query, mapping, and recomposition cost scale with the
visible window, not the total cache.

- MessageDao.pagingUnifiedFolderSummaries: a PagingSource over the folder's
  synced rows (inInbox = 1); unified search keeps the whole-folder query so it
  can still surface transient server-search hits.
- MailRepository.pagedUnifiedFolderMessages: a Pager (pageSize 40, initialLoad
  120, no placeholders) mapping summaries to domain.
- MailboxViewModel.pagedMessages: paged while browsing the unified inbox, else
  empty; the messages list flow stays empty in that state so the whole cache is
  never materialized. Selection captures each row's accountId at tap time, so
  "Move" still resolves the selection's account without an in-memory list.
- MailboxScreen renders the unified browse list via collectAsLazyPagingItems;
  per-account and search views render the flat list unchanged (issue #86 stays
  flat).

Profiling (docs/perf/issue-124-unified-inbox-paging.md) on an api29 emulator:
current whole-inbox first-emit ~24.6 ms at a 20k cache vs. the paged first page
~6.8 ms and flat regardless of cache size (~3.6x). EXPLAIN QUERY PLAN shows the
paged query still stops early on the existing timestamp index, so no
(folder, ...) index and no schema migration are added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 08:49:52 -05:00
JMR-devandClaude Fable 5 b0bb942a02 test(imap): measure folder-open round-trip structure (#125)
Investigate IMAP folder-open latency (follow-up to #86). Localhost GreenMail
has ~0 RTT, so real wall-clock latency can't be measured here; instead this
pins the folder-open round-trip STRUCTURE deterministically.

Finding: ImapClient.withStore wraps every operation in its own short-lived
Store, so each folder-open pays a full CONNECT + TLS + LOGIN + EXAMINE +
FETCH + LOGOUT. Only EXAMINE + FETCH is intrinsic to opening a folder; the
whole connection-setup group is avoidable on the 2nd+ operation if a
connection were reused. Optimistic render-from-cache already exists
(selectFolder renders cached rows; the network sync is a background refresh).

Adds:
- CountingImapProxy: a localhost TCP proxy that forwards a cleartext IMAP
  session to GreenMail while counting TCP connections and parsing IMAP
  command words.
- ImapFolderOpenLatencyTest: asserts the current no-reuse behaviour (N opens
  => N connections and N LOGINs; list+read => 2 connections) against a real
  in-process IMAP server. Doubles as the harness to validate a future
  connection-reuse fix (flip the counts to assert reuse).
- docs/perf/issue-125-imap-folder-open.md: the per-open round-trip sequence,
  avoidable vs. necessary round-trips, and the recommended per-account
  connection-reuse/keep-alive mitigation with its IDLE / thread-safety /
  battery / stale-connection constraints.

Analysis + harness only; the connection-reuse fix is deferred pending
real-network + real-device measurement (see the doc's measurement plan), so
this references #125 without closing it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 08:24:18 -05:00
JMR-devandClaude Fable 5 e7bb69d2ac perf(mailbox): scope the message-list query to the viewed folder in SQL
The mailbox list observed the entire `messages` table (observeSummaries, no
WHERE/LIMIT), mapped every cached row to a domain Message, and filtered down to
the visible account+folder in MailboxViewModel — so its cost scaled with the
whole cache and re-ran on every write to `messages` (IDLE delivery, a flag
toggle, a backfill page, any folder sync). On a 20k-row cache that is ~125 ms of
work per unrelated write.

Push the account/folder filter into SQL (observeFolderSummaries /
observeUnifiedFolderSummaries, exposed via observeFolderMessages /
observeUnifiedFolderMessages) and flatMapLatest the ViewModel over the selected
account+folder. The only remaining client-side pass separates the normal list
from an active search over the small folder-scoped set.

Validated on an emulator against 1k/5k/20k-row caches (docs/perf/issue-86-
profiling.md): the account-scoped query is ~1.5 ms flat (~80x faster at 20k) and
is already served by the existing (accountId, folder, uid) index — so NO
composite index and NO schema migration are added. The ticket's proposed
(accountId, folder, inInbox, timestampMillis) index changes timing only within
noise and isn't even preferred by SQLite's planner. The unified "All inboxes"
view stays an O(N) folder scan (still 5.6x better) and is a follow-up for paging;
IMAP latency on folder open is a separate, unmeasured concern.

Closes #86

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:16:12 -05:00
Jason Ross 59e068e326 Merge branch 'main' into feat-release-workflow 2026-07-01 22:54:12 -05:00
Jason Ross 85b4597939 Merge branch 'main' into feat-play-compliance 2026-07-01 22:31:42 -05:00
JMR-devandClaude Fable 5 0b0f6b7018 ci(release): add tag-triggered signed-release and store-publish workflow
Rewrite the manual-dispatch release.yml into the issue-#19 pipeline:
v* tag push (or dispatch with dry-run/re-release inputs) runs the fast
CI gate, builds bundleRelease + assembleRelease signed from base64
keystore secrets (falling back to *-unsigned artifacts when unset),
generates a Conventional-Commit changelog and SHA-256 checksums, then
creates the GitHub release and fans out to secret-gated Google Play
publish (staged rollout supported), a documented Galaxy Store manual
stub, and an S3-compatible archive under releases/<tag>/. Every
credentialed stage skips with a clear notice while the store accounts
(#16/#17/#18) don't exist yet; no secret lives in the repo and
app/build.gradle.kts is unchanged. docs/release.md documents the
secrets, flows, and per-store manual fallbacks.

Part of #19

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 22:01:34 -05:00
JMR-devandClaude Fable 5 3f7024d05d docs(play): add privacy policy, data-safety mapping, and permissions justification
Repo-actionable deliverables for the Google Play compliance work (issue #17),
every claim verified against the code and the built release artifacts:

- PRIVACY.md: user-facing privacy policy (device-local mail cache, optional
  SQLCipher encryption, traffic only to the user's own mail provider,
  on-device-only contacts autocomplete, strictly local opt-in debug reports,
  no ads/analytics/tracking SDKs).
- docs/play-data-safety.md: Play Data safety questionnaire mapping -- answer
  'no data collected/shared' with per-category code evidence, the policy
  exemptions relied on, a dependency audit, and a conservative fallback.
- docs/play-permissions.md: merged-manifest permission audit (incl. the
  WorkManager-injected WAKE_LOCK / RECEIVE_BOOT_COMPLETED) with paste-ready
  Console justifications for READ_CONTACTS, POST_NOTIFICATIONS, and the
  FOREGROUND_SERVICE_DATA_SYNC declaration + demo-video script.
- docs/play-compliance.md: verified targetSdk 37 (requirement: 35+), 16 KB
  page-size compliance (all packaged .so PT_LOAD p_align=0x4000, incl.
  sqlcipher-android 4.16.0), bundleRelease AAB check, the Gmail-app-password /
  no-CASA OAuth note, the console-steps checklist with drafted content-rating
  and listing answers, and repo findings (push-mail default vs docs, README
  minSdk/app-lock drift, debug-key release fallback).
- README.md: link PRIVACY.md and note the no-Google-OAuth/no-CASA status
  (fuller README pass stays issue #20).

Part of #17.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 21:52:40 -05:00
JMR-devandClaude Fable 5 807f2402a5 chore(fdroid): tighten accuracy of CI and KnownVuln wording in audit doc
CI runs ktlint/detekt and the E2E suites (not Android lintDebug), and the
KnownVuln rationale should not imply blanket TLS enforcement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 21:46:06 -05:00
JMR-devandClaude Fable 5 db96134492 chore(fdroid): add F-Droid metadata, license audit, and anti-feature docs
Prepare for F-Droid publication (issue #16):

- docs/fdroid-compliance.md: full dependency license audit (release
  runtime classpath + buildscript classpath — all FOSS, no Play
  Services/Firebase, no non-free Gradle plugins), an anti-feature
  review of actual app behavior (none to declare: debug reporting is
  opt-in/local-only with no endpoint by default, Android Backup is
  gated off by default, Outlook OAuth is optional per-account with a
  public client id), a complete network-surface inventory, and the
  clean-room build verification (assembleRelease succeeds with no
  secrets.properties).
- app/build.gradle.kts: stop embedding AGP's dependency-info block (a
  Google-Play-encrypted dependency list in the APK signing block) in
  APKs/bundles — a known F-Droid inclusion/reproducibility blocker.
- fastlane/metadata/android/en-US/: store listing (title, short/full
  description, changelog for versionCode 1) that F-Droid reads from
  the repo; listing .txt files deliberately carry no license headers.
- docs/fdroid/org.libremail.app.yml: commented template + instructions
  for the eventual fdroiddata build recipe (submission out of scope).
- README.md: F-Droid section pointing at the above.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 21:43:58 -05:00