Commit graph

109 commits

Author SHA1 Message Date
Johannes Millan
941aa7c104 fix(android): store a real timestamp in KeyValStore KEY_CREATED_AT (#7901)
ContentValues binds "time('now')" as a literal string rather than letting
SQLite evaluate it, so every row's KEY_CREATED_AT held a constant text
value instead of a write timestamp. Store System.currentTimeMillis() so
the column is actually usable for ordering/debugging.
2026-06-01 18:36:18 +02:00
Johannes Millan
3d1e173f67 fix(android): drop action bar and inset system bars on WebView block screen
The WebView block / init-failure screen was the only activity using a theme
with an action bar (AppTheme / Theme.AppCompat.Light.DarkActionBar), so the app
title bar overlaid the message. Its content also drew under the status bar on
edge-to-edge devices (enforced and not opt-out-able at targetSdk 36), clipping
the title's first line with no way to scroll it into view.

Switch the activity to the existing AppTheme.NoActionBar and inset the scroll
container by the system bars + display cutout so the message is fully visible.

Reported in issue #7840.
2026-06-01 16:33:18 +02:00
Johannes Millan
90288b885e
fix(android): recover focus/pomodoro timer after app swipe (#7855) (#7866)
* fix(android): recover focus/pomodoro timer after app swipe (#7855)

After #7818 kept the foreground services alive across task removal, the
focus-mode timer still reset and its notification closed on reopen, while
time tracking survived. Two causes:

1. On cold start, syncFocusModeToNotification$ computed wasFocusModeActive
   from the startWith(null) seed as (undefined \!== null) === true, so the
   first idle emission fired stopFocusModeService() and tore down the
   surviving native notification.
2. Focus mode had no native read-back/recovery (unlike time tracking), so
   the idle store never re-adopted the still-running native countdown.

Mirror the time-tracking recovery design:
- FocusModeForegroundService: mirror live timer state into the companion
  object + liveRemainingMs(); cleared on stop.
- JavaScriptInterface.getFocusModeElapsed() returns JSON or "null"
  (task title intentionally omitted — no user content crosses the bridge).
- restoreFocusSessionFromNative action + reducer rebuild the timer
  (elapsed = duration - remaining, startedAt = now - elapsed; Flowtime =
  duration 0) and derive mode from duration so a restored Flowtime session
  is not auto-completed on the next tick.
- recoverFocusSession$ recovers on cold start + resume, gated on an idle
  store and settled hydration; the wasFocusModeActive seed bug is fixed.

* fix(android): trigger focus recovery on resume edge only, not store changes

recoverFocusSession$ put selectTimer in its combineLatest trigger list, so
every idle-store emission re-read native state. Ending a session
(cancel/complete) emits an idle store while the native stop
(stopFocusModeService -> stopService -> onDestroy) is still in flight, so
getFocusModeElapsed() saw isRunning === true and re-adopted the session that
just ended -- resurrecting a cancelled session or double-logging a completed
one.

Sample selectTimer via withLatestFrom instead of using it as a trigger, so only
a genuine resume/cold-start edge can trigger recovery.
2026-06-01 16:15:56 +02:00
Johannes Millan
2baf5fc512 fix(android): align startup overlay FAB/bar with web via WebView inset
The Capacitor 8 edge-to-edge migration put the native startup overlay
(android.R.id.content) behind the system navigation bar, so its FAB and
input bar rendered too low vs the web add-task button/bar they hand off
to. Lift them by the navigation-bar inset.

Use the WebView's measured bottom inset (overlay bottom - WebView bottom)
rather than navigationBars(): the latter is a system value that does not
match the inset the edge-to-edge plugin actually applies, and the two
diverge differently on gesture-nav vs 3-button-nav devices (too high on
one, too low on the other). Mirroring the WebView's real geometry is the
same safe area the web content uses, so it is device-independent.
2026-05-29 14:55:22 +02:00
Johannes Millan
3c01d585ca fix(android): auto-recover from transient WebView init failures
- refactor(android): unify automatic and manual WebView relaunch paths
- fix(android): add retry action to WebView init-failure screen (#7840)
- refactor(android): extract shared WebView recovery helper
- fix(android): auto-recover from transient WebView init failures
2026-05-29 14:55:22 +02:00
Johannes Millan
f10d0e9d48
Android soft-keyboard: fix dark-theme white flash on resize (#7839)
* test(e2e): raise timeouts for two CI-flaky waits

Both timeouts repeatedly hit their limit on saturated scheduled runners
without indicating a real product regression:

- supersync.page.ts:781 — final sync-state icon waitFor went 10s → 30s.
  The race above it already consumed a 30s budget, so falling back to
  10s here is too tight when the runner is hot (e.g. SuperSync 5/6 in
  run 26514574130, "Client A can migrate multiple times" failure).
- repeat-task-day-change #6230 — post-midnight visibility went 30s → 60s.
  Even with the focus-event fallback added in 46ac873570, the 1s tick
  + debounce + sync chain can still exceed 30s under contention.

* fix(android): keep foreground services alive across task removal

The focus-mode and tracking foreground services overrode onTaskRemoved
to stop themselves when the app was swiped from recents, which defeats
the purpose of running them as foreground services. Remove the
overrides so the native countdown continues ticking and the notification
persists after task removal.

Refs #7818, #4513

* fix(theme): stabilize Android keyboard-height tracking

Per-event commits during the Android IME open animation sampled
partial keyboard amounts from `window.innerHeight - visualViewport.height`
(layout-viewport adjustResize and visual-viewport resize fire at slightly
different times), parking the global add-task bar mid-page. Debounce the
open path 200ms so only the final value lands; commit synchronously when
the value falls back to zero so the bar drops the moment the IME is gone.

* fix(theme): prevent white flash on Android keyboard resize

The Android WebView surface defaulted to white, so adjustResize keyboard
animations briefly exposed it before the page repainted at the new size —
jarring in dark theme. Paint the surface in the theme background: a
values/values-night color resource provides the cold-start default, and a
new NavigationBar.setWebViewBackgroundColor push keeps it in sync on live
theme switches (the activity is not recreated since uiMode is in
configChanges).

Also promote the full-viewport gradient backdrop to its own compositor
layer as a first-pass mitigation for resize choppiness, pending deeper work.

Not yet verified on-device.

* docs(android): plan to smooth soft-keyboard resize jank

Research + multi-reviewed plan for the remaining keyboard-resize choppiness
(the white-flash half is already fixed in 80b08f0e96). Root cause: adjustResize
resizes the WebView window per frame (WebView is excluded from Chrome 108's
visual-viewport fix). Recommends a KISS core — flip only CapacitorMainActivity
to adjustNothing + reuse the existing visualViewport/--keyboard-height model and
scroll-into-view — gated behind a baseline trace, with VirtualKeyboard API and
CSS containment kept as contingencies behind proven need.

* perf(theme): dedupe Android keyboard-visibility emissions

The native OnGlobalLayoutListener pushes isKeyboardShown$ on every layout pass
(every frame of the IME slide), so the subscriber rewrote <body> classes and
re-triggered change detection each frame. distinctUntilChanged collapses it to
actual show/hide transitions.

* docs(android): correct keyboard-resize plan for min-Chrome-107

Implementation review found MIN_CHROMIUM_VERSION=107, but WebView only
auto-resizes the visual viewport for the IME at ~Chrome 139. So a static
adjustNothing flip would leave Chrome 107-138 with no keyboard-height signal
(inputs silently covered). Corrected the plan: VirtualKeyboard API is required
(not optional), the switch must be runtime-gated via a native setSoftInputMode
method keeping adjustResize as the fallback, and the cheap containment route is
now Phase 1 (try first) with the bigger flip as Phase 2. distinctUntilChanged
win shipped (f486496b7b).

* style(theme): drop no-op keyboard backdrop compositing

The will-change/backface-visibility on body::before was added to reduce
keyboard-resize choppiness, but review showed it's a no-op: the backdrop
resizes every frame so it re-rasterizes regardless of layer promotion, while
the hint allocates an always-on compositor layer on every platform. The
white-flash fix (WebView background color) is unaffected.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-28 16:56:21 +02:00
Johannes Millan
cc4776a715 fix(android): keep foreground services alive across task removal
The focus-mode and tracking foreground services overrode onTaskRemoved
to stop themselves when the app was swiped from recents, which defeats
the purpose of running them as foreground services. Remove the
overrides so the native countdown continues ticking and the notification
persists after task removal.

Refs #7818, #4513
2026-05-28 13:58:19 +02:00
Johannes Millan
334b14aa26 feat: migrate to capacitor 8
- fix(sync): unblock @sp/sync-core/@sp/sync-providers typecheck under vitest 4
- chore(mobile): pin @capacitor/keyboard to 8.0.1
- chore(mobile): migrate to Capacitor 8
- Merge branch 'feat/issue-7749-3341d1'
- docs(plugins): plan for wiring PERSISTED_DATA_CHANGED hook
- test(plugins): cover document-mode bundled load and PLUGIN_USER_DATA LWW
- chore(plugins): re-bundle document-mode and document Stage A path
2026-05-23 20:42:00 +02:00
Johannes Millan
508998c6a1
Improve on sync (#7736)
* fix(android): restore share title derivation and dedupe shared tasks

Commit d32f7037a3 accidentally reverted the EXTRA_SUBJECT handling from
edb102534e, so the share handler stopped sending the page subject and
defaulted the title to the literal "Shared Content". The frontend's
subject -> title -> derived title chain then always fell through to that
placeholder, producing blank-looking shared tasks.

- Restore EXTRA_SUBJECT extraction; leave title/subject empty when absent
  so the frontend can derive a meaningful title from the URL or note.
- Ignore empty/blank shared text instead of creating a useless task.
- Skip handleIntent() on Activity recreation (config change) so the same
  share Intent isn't re-processed into a duplicate task.
- Extract buildTaskTitle/readableUrl as pure functions with unit tests
  and guard the effect against empty payloads.

* fix(snack): scale error/warning snack duration with message length

Long error messages (e.g. multi-sentence sync errors) were auto-dismissed
after a fixed 8s, too short to read. Error/warning snacks now stay visible
proportional to message length (~90ms/char), clamped to 10-30s.

* feat(tasks): skip undo snack when deleting a blank task

A sub task or parent task with an empty title and no data (notes, time,
estimate, attachments, issue link, reminder, repeat, scheduling,
deadline, non-blank sub tasks) no longer shows the undo-delete snack.

* fix(sync): include archive data in REPAIR operations

validateAndRepairCurrentState built the REPAIR op from the synchronous
getStateSnapshot(), which hardcodes empty archiveYoung/archiveOld
(archives live in IndexedDB, not NgRx state). The resulting REPAIR op
carried empty archives, so every other client that applied it
overwrote its archive with nothing — wiping archived tasks on all
devices except the one that ran the repair.

- Use getStateSnapshotAsync() so the REPAIR op carries real archives.
- Extend the empty-archive overwrite guard in
  ArchiveOperationHandler._handleLoadAllData() to also cover OpType.Repair
  (previously only SYNC_IMPORT/BACKUP_IMPORT), as defense in depth.

* test(sync): add archive REPAIR round-trip integration test

Wires the real StateSnapshotService, ArchiveDbAdapter, ArchiveStoreService
and ArchiveOperationHandler against real IndexedDB to verify archive data
survives the REPAIR-op round-trip:

- getStateSnapshotAsync() loads IndexedDB archives; getStateSnapshot() does not
- archive round-trips from client A's IndexedDB through a REPAIR op into a
  fresh client B's IndexedDB
- a REPAIR op carrying empty archives no longer wipes a client that has
  archive data (empty-archive guard regression)

* perf(sync): skip archive IndexedDB reads when post-sync state is valid

validateAndRepairCurrentState validated the full async snapshot (two
IndexedDB archive reads + structured-clone deserialization) on every
Checkpoint D, even when state was valid and no repair was needed. It now
validates the cheap synchronous snapshot first and only loads the async
snapshot (with archives) when a repair is actually required — the rare
path. The REPAIR op still carries archive data.

Also addresses multi-review follow-ups:
- archive-operation-handler: reword the empty-archive guard comment so it
  no longer over-promises reconciliation for REPAIR ops.
- archive-repair-roundtrip test: add isPersistent to the applied-op meta
  to match the real applier; scope the file docstring accurately.

* fix(task-repeat-cfg): schedule inbox task for today when made recurring

When an Inbox task (no dueDay) was made repeatable via the dialog with a
recurrence starting today, it stayed unscheduled. The TODAY-first-occurrence
branch of updateTaskAfterMakingItRepeatable$ derived currentDueDay from
task.created as a fallback, so a task created today looked already scheduled
and dueDay was never set.

Key the decision on task.dueDay directly. Skip timed tasks and tasks that
already have dueWithTime, since dueDay/dueWithTime are mutually exclusive and
timed scheduling is handled by addRepeatCfgToTaskUpdateTask$.

Closes #7725

* fix(tasks): correct monthly first/last-day recurrence anchoring

The "Every month on the first day" and "Every month on the last day"
quick settings scheduled the first task instance in the past. Both
presets produced a backdated startDate (1st of the current month;
hardcoded January 31), which getFirstRepeatOccurrence returns verbatim
for monthly recurrences.

- MONTHLY_FIRST_DAY now anchors startDate to the next 1st-of-month
  that is today or later.
- MONTHLY_LAST_DAY anchors startDate to the current month's last day
  and sets a new monthlyLastDay flag, so the occurrence engine clamps
  to month-end every month regardless of startDate's day-of-month.
- _normalizeMonthlyAnchor strips a stale monthlyLastDay flag when a
  config leaves the preset (CUSTOM mode has no control for it).

Closes #7726

* fix(task-repeat-cfg): re-anchor start date after instance deleted

When the user moved a repeat config's startDate earlier after deleting
its only live task instance, the stale lastTaskCreationDay anchor kept
suppressing every projected/created instance between the new startDate
and the old anchor.

rescheduleTaskOnRepeatCfgUpdate$ only re-anchored lastTaskCreationDay
when a live task instance existed (the #7423 fix) — it returned early
before the re-anchoring when there was none. Hoist the
isStartDateMovedEarlier detection above that early return: when no live
instance exists but startDate moved earlier, re-anchor to the day
before the new first occurrence so it and every following day is
created and projected fresh.

Closes #7724

* test(task-repeat-cfg): cover startDate re-anchor with no live instance

Add coverage for the #7724 fix beyond the effect unit test:

- Selector integration tests: feed a config re-anchored to the day
  before the new startDate through selectTaskRepeatCfgsForExactDay and
  assert it projects the new startDate and every following day, while
  still excluding the anchor day and earlier. Also documents that the
  stale anchor suppresses the gap days.
- E2E reproduction (recurring-move-start-date-earlier-no-instance):
  create a recurring task, delete its live instance, move startDate
  earlier via a transparent projection, and assert the new days appear
  in the planner. Verified to fail on pre-fix code.

* feat(sync): move clientId from pf into SUP_OPS for atomic rotation

Migrate the sync clientId out of the legacy `pf` IndexedDB database into
`SUP_OPS` (new `client_id` store, schema v6). The clientId write now joins
the atomic transaction in `runDestructiveStateReplacement`, so destructive
flows (clean-slate, backup-restore) rotate it atomically with
OPS/STATE_CACHE/VECTOR_CLOCK instead of a hand-rolled cross-database
two-phase commit.

- ClientIdService rewritten: SUP_OPS-backed via an independent connection,
  inline one-time pf->SUP_OPS migration, error-aware resolver. Read
  failures propagate (getOrGenerateClientId never mints a fresh id over a
  transient error — that would orphan the device's non-regenerable
  identity); loadClientId never throws.
- Delete withRotation, generateNewClientId and the CAS/rollback machinery.
- Extract pure generateClientId() + isValidClientIdFormat() into
  core/util/generate-client-id.ts.
- pf becomes a read-only, one-time migration source (never written/deleted).

Closes #7732

* fix(sync): dedup SUP_OPS connection open in ClientIdService

Address multi-agent review findings on the clientId migration:

- _getSupOpsDb() shares a single in-flight open via _supOpsDbPromise;
  concurrent cold-start callers previously each opened their own
  SUP_OPS connection, leaking all but the last.
- _putClientIdIfAbsent() collapsed to a single tx.done / exit point.
- db-upgrade.spec.ts: cover the v6 client_id store; the createObjectStore
  count assertions were stale and failing after the schema bump.
- operation-log-migration.service.ts: correct a misleading comment about
  the genesis-op clientId fallback.

* refactor(sync): align ClientIdService SUP_OPS open with in-house idiom

Re-review of the connection-leak fix recommended matching
OperationLogStoreService._ensureInit's pattern:

- _getSupOpsDb() clears the in-flight promise in .catch (failure only)
  instead of an unconditional finally — the resolved handle lives in
  _supOpsDb, so the promise field is pure in-flight coordination state.
- close/versionchange handlers now also null _supOpsDbPromise, so a
  stale (closed) connection is never re-handed-out.
- Add a regression test asserting _openSupOpsDb runs exactly once for
  concurrent cold-start callers.

* docs(sync): link the single-connection follow-up to #7735

Reference the tracked follow-up issue from the ClientIdService JSDoc
and the plan's out-of-scope section, so the deliberate trade-off (one
extra SUP_OPS connection) is traceable rather than forgotten.

* test(sync): update ClientIdService spies for getOrGenerateClientId

The op-log capture effect now resolves the clientId via
getOrGenerateClientId() (was loadClientId() ?? generateNewClientId()).
These two specs still mocked only loadClientId, so the effect called an
undefined method, captured no op, and 4 tests failed in the full suite.

- task-done-replay.integration.spec.ts
- operation-log-lock-reentry.regression.spec.ts

* test(sync): open SUP_OPS versionless in e2e read helpers

DB_VERSION was bumped 5->6; five e2e helpers still opened SUP_OPS at
the hardcoded old version 5 to read state after the app had already
upgraded it to v6, throwing VersionError. Open versionless instead —
matches the ~10 other e2e files that already do, and is future-proof
against the next schema bump.

- migration/legacy-data-migration.spec.ts (x2)
- sync/supersync-legacy-migration-sync.spec.ts
- sync/webdav-legacy-migration-sync.spec.ts
- recurring/invalid-clock-string-bug-7067.spec.ts
2026-05-22 17:49:25 +02:00
spike
9a9f2c116e fix(android): credit wall-clock time on focus-mode bridge events
Merge an internal wake-up subject into GlobalTrackingIntervalService.tick$
so a tick can be force-emitted with a cap that prevents over-crediting
when the WebView was suspended past session end. Wire the capped tick
into focus-mode pause/complete bridge callbacks, plumb a wall-clock-correct
completedDuration onto completeFocusSession, and have the pause reducer
freeze elapsed via updateTimer.
2026-05-16 23:46:59 +02:00
johannesjo
518ceb6f20 fix(android): prevent restored legacy launch mode 2026-05-14 12:41:09 +02:00
Johannes Millan
2ae4666955 fix(android): handle WebView init failures safely 2026-05-11 20:32:12 +02:00
Johannes Millan
348dc47e3e fix(android): handle foreground service failures 2026-05-11 14:10:51 +02:00
Johannes Millan
eb1131048f feat(android): surface WebView block detection source for triage
Show which detection path produced the BLOCK (USER_AGENT, PACKAGE,
UNKNOWN, or INIT_FAILURE) on the WebView block screen so user-submitted
screenshots for issue #7518 are actionable. Tag forced non-version
blocks with INIT_FAILURE so they no longer falsely report a
version-detection source. Adds tests for parseMajorVersion(String?).
2026-05-08 22:30:59 +02:00
Johannes Millan
7c5b58ecf6 fix(android): add WebView block recovery paths
Builds on the prior authoritative-vs-fallback fix with three layered
recovery mechanisms so users hit by a false BLOCK are never locked out
of their data:

- Last-known-good auto-recovery: persist the highest WebView version
  that has ever loaded the app on this device. A later transient
  mis-read that drops below MIN_CHROMIUM_VERSION is downgraded to WARN.
- "Try anyway" override: third button on the block screen opens an
  AlertDialog with an explicit risk acknowledgment (crashes, render
  failures, possible data loss). Confirming persists an override and
  relaunches the app. Hardened against tapjacking via
  filterTouchesWhenObscured on both the activity and dialog window.
- Override auto-clears once a healthy version is detected, so a future
  genuine block is not silently bypassed.

Also tightens the UA regex (drops the misleading Safari Version/X
fallback that always reads "4.0" and would falsely block) and adds
diagnostic logging gated by Log.isLoggable for field debugging.

Tests: 12 unit tests covering statusForVersion branches, all
applyOverrides paths, and parseMajorVersion edge cases.

Refs #7229
2026-04-29 16:17:55 +02:00
Johannes Millan
10257fc21a fix(android): avoid false WebView version lockout 2026-04-29 16:17:55 +02:00
Johannes Millan
01e30b9c7e
Feat/to me it looks like there are lots of 60dd04 (#7280)
* fix(issue): prevent crash from orphan issueProviderId (#7135)

The Jira image-headers effect in task-detail-panel subscribed to
selectIssueProviderById without an error handler, so a task with an
issueProviderId pointing at a deleted provider (e.g. after sync
convergence where taskIdsToUnlink didn't cover all local tasks)
propagated the selector throw to Zone.js as a crash dialog. Wrap the
inner selector observable in catchError that logs and falls back to
of(null); the downstream jiraCfg?.isEnabled guard handles the fallback.

Also drop IssueLog.log(issueProviderKey, issueProvider) from the
throwing variant of the selector: providers may carry credentials
(host, token, apiKey) and IssueLog history is exportable.

* fix(focus-mode): sync tray countdown with in-app timer during breaks

Tray title was rebuilt from a cached currentFocusSessionTime that only
refreshed when CURRENT_TASK_UPDATED fired. addTimeSpent is gated on an
active current task, so during focus-mode breaks or task-less focus
sessions the cache froze while the in-app timer kept ticking.

Add the tick action to taskChangeElectron$ so the cache refreshes every
second whenever the focus timer is running.

Fixes #7278

* fix(ci): restore GitHub Actions SHA pins undone by 0e9218bd68

Commit 0e9218bd68 silently reverted PR #7212 (github-actions-minor group
bump) along with its stated sync/client-id work. This restores the 15
workflow files to their pre-revert state.

Actions restored to newer pinned SHAs:
- actions/upload-artifact v7.0.0 -> v7.0.1
- step-security/harden-runner v2.16.1 -> v2.17.0
- softprops/action-gh-release v2.6.1 -> v3.0.0
- signpath/github-action-submit-signing-request v2.0 -> v2.1
- anthropics/claude-code-action v1.0.89 -> v1.0.93
- docker/build-push-action v7.0.0 -> v7.1.0
- easingthemes/ssh-deploy v5.1.1 -> v6.0.3

* fix: restore i18n, UI, and docs work undone by 0e9218bd68

Commit 0e9218bd68 silently reverted the following work alongside its stated
sync/client-id changes. Files where later master commits (fec7b25f23, etc.)
already re-applied the reverted work are intentionally left untouched.

Restored:
- #7232 docs/long-term-plans/location-based-reminders.md (513 lines)
- #7199 Romanian i18n phase 3 (ro.json + ro-md.json, ~1168 lines)
- #7049 Polish translation improvements
- #7143 planner component styling (4 scss files)
- #7211 add-task-bar preserve time estimate when typing title
- #7208 task.reducer roll-up estimates for added subtasks
- #6767 focus-mode pomodoro reset button
- #7205 plugin-dev github-issue-provider TOKEN description
- #7231 mobile-bottom-nav FAB fix
- a4fe03272 iOS keyboard accessory bar (global-theme + dialog-fullscreen-markdown)
- 309670db3 ShortSyntaxEffects undefined guard
- 667a7986f Dropbox PKCE auth comment/behavior

* fix(electron): restore electron + e2e work undone by 0e9218bd68

Commit 0e9218bd68 silently reverted the following electron/e2e work.
Files already re-fixed by later master commits are left as-is:
- e2e/tests/sync/supersync-archive-conflict.spec.ts (de33234976 + 191d129ff3)

Restored:
- e8a3e156eb fix(electron): Linux autostart IDB backing-store recovery
  (re-adds electron/clear-stale-idb-locks.ts + start-app.ts wiring)
- 5ce78a5b63 fix(electron): macOS Cmd+Q / Dock > Quit hang
  (setIsQuiting + before-quit delegate to close-handler)
- 46e0fa2d01 fix(sync): FILE_SYNC_LIST_FILES IPC contract
  (electronAPI.d.ts + local-file-sync + preload + ipc-events)
- ea1ef16307 fix(android): session-only SAF permissions on OEM devices
- 8865dc0a50 test(e2e): supersync parallel-worker stampede guard
  (SUPERSYNC_SERVER_HEALTHY env-var fallback + goto retry loop)
- af7c7687e2 test(e2e): block WS-triggered downloads in non-WS specs
- 265b44db5d test(e2e): premature waitForURL on daily-summary
  (this is literally the fix the bad commit's message claimed to add)

Conflict resolutions:
- e2e/utils/supersync-helpers.ts: kept the refined getDoneTaskElement
  checks from d64014d086 (later than c558bcab5e) while restoring the
  goto retry loop from 8865dc0a50.
- electron/start-app.ts: unioned imports (setIsQuiting +
  clearStaleLevelDbLocks from theirs, fs from ours).

* fix(sync): restore sync-core work undone by 0e9218bd68

Commit 0e9218bd68 silently reverted parts of several sync fixes. Most
sync-core reverts have already been re-addressed differently on master
by later commits (1f5184f6e7, 05cd875dd6, 09f5ced2c9, 7df43358ab,
d9158d6adb, 32dbc95ed9, 8c3b08e016, f89fe1ebc3) — those files are
intentionally left untouched to avoid reverting master's newer work.

This PR restores only the pieces that are genuinely still missing:

- e8a3e156eb fix(electron): IDB backing-store autoreload (in-app piece)
  operation-log-hydrator.service.ts + .spec.ts (the electron/clear-
  stale-idb-locks.ts piece was restored in the prior commit)

Plus three low-risk documentation/cleanup restorations:
- operation-sync.util.ts — add "Nextcloud" to isFileBasedProvider JSDoc
- dropbox.ts — restore improved _getRedirectUri JSDoc (667a7986fb)
- dialog-get-and-enter-auth-code.component.ts — restore isNativePlatform
  comment explaining why manual code entry flow is used (667a7986fb)
- file-adapter.interface.ts — remove stray "// NEW" comment (46e0fa2d01)

Intentionally NOT restored (master's newer work covers or supersedes):
- sync-trigger.service.ts / sync.effects.ts (05cd875dd6)
- sync-wrapper.service.ts (1f5184f6e7)
- sync-errors.ts (1f5184f6e7 re-added LegacySyncFormatDetectedError)
- file-based-sync-adapter.service.ts + spec (1f5184f6e7 + d9158d6adb)
- file-based-sync.types.ts (1f5184f6e7)
- operation-log.const.ts (32dbc95ed9 bumped IDB_OPEN_RETRIES to 5)
- dialog-sync-initial-cfg.component.ts (f89fe1ebc3)
2026-04-20 12:04:38 +02:00
Johannes Millan
6171a98646 refactor(android): route specialUse FGS through ServiceCompat helper
Both FocusModeForegroundService and TrackingForegroundService declare
android:foregroundServiceType="specialUse" in the manifest. The 2-arg
startForeground() overload already falls back to the manifest type on
Android 14+, but passing the type explicitly via ServiceCompat is
Google's documented best practice and removes OEM-dependent fallback
ambiguity.

Extract the Build-gated call into a Service.startForegroundSpecialUse
extension in ForegroundServiceExt.kt so a future third FGS picks up
the API-version gating automatically. Callers shrink to one-liners.
2026-04-17 19:45:20 +02:00
Johannes Millan
f81858b488 fix(android): guard broken WebView package probe
WebView.getCurrentWebViewPackage() is documented to return null on
devices without an installed provider, but in practice it can throw
AndroidRuntimeException, MissingWebViewPackageException, or NPE on
devices with a corrupt or updating provider. An unhandled throw there
short-circuits WebViewCompatibilityChecker.evaluate() before
FullscreenActivity's recovery path (added in 5a5c3bca89) has a chance
to run, re-crashing the app on launch.

Wrap the call in a narrow catch for RuntimeException so the documented
throwables fall through to the PackageManager lookup while Error
subclasses still propagate. Log at debug level so broken-provider
telemetry survives.
2026-04-17 19:45:10 +02:00
Johannes Millan
a2f45b0d69 refactor(android): narrow broad catch clauses so unknown failures surface in Play Console
The recent WebView, FGS, and JS-dialog guards each caught Exception,
which hid every thrown type behind a log line. Play Console only
surfaces uncaught throws, so a future regression inside any of these
paths would be silently swallowed.

Narrow to the known recoverable types:
- FullscreenActivity.initWebView: AndroidRuntimeException (incl.
  MissingWebViewPackageException) + IllegalStateException.
- FullscreenActivity.onJsAlert/onJsConfirm: WindowManager.BadTokenException
  + IllegalStateException; JsResult.cancel() still fires via the
  handled flag so the WebView JS thread never deadlocks.
- TrackingForegroundService / FocusModeForegroundService
  ensureForegroundNotification: IllegalArgumentException (Samsung One UI
  / Android 14 blank-title case) + SecurityException. Anything else,
  including ForegroundServiceStartNotAllowedException, now propagates.
2026-04-17 18:13:49 +02:00
Johannes Millan
5a5c3bca89 fix(android): recover from WebView init crash via WebViewBlockActivity
Observed on Play Console: AndroidRuntimeException thrown from
WebViewFactory.getProvider during FullscreenActivity.initWebView on
devices where the system WebView package itself is broken (seen with a
WebView Beta build throwing "Already registered a list of actions in
this process" from WebViewChromiumFactoryProvider.<init>). The activity
had no recovery - the constructor threw and the app died at launch.

Mirror the existing CapacitorMainActivity pattern: catch around the
WebHelper().instanceView(this) call, and on failure present the existing
WebViewBlockActivity using compatibility.copy(status = BLOCK) so the
block screen keeps the detected provider/version, then finish().

Also harden lifecycle callbacks that dereference the lateinit webView /
javaScriptInterface fields so the new failure path can't trigger an
UninitializedPropertyAccessException via OEM-specific lifecycle quirks:
::isInitialized guards in onSaveInstanceState, onRestoreInstanceState,
onBackPressed, and callJSInterfaceFunctionIfExists (the last matching
the existing guard in CapacitorMainActivity).
2026-04-17 18:13:02 +02:00
Johannes Millan
b9f91a5327 fix(android): guard FGS contract unconditionally in onStartCommand
Every Context.startForegroundService() call creates a per-call FGS token
that must be answered with startForeground() within the system timeout,
even if the service is already a foreground service. Earlier fixes closed
specific branches one at a time and left gaps — most notably ACTION_START
with null taskId while already tracking, which returned without calling
startForeground().

Satisfy the contract once at the top of onStartCommand in both
TrackingForegroundService and FocusModeForegroundService, so every branch
below (known and future) already has the token answered. Add
stopForegroundAndSelf() on the previously silent no-op branches
(ACTION_UPDATE / ACTION_STOP when idle) so the guard does not leak the
service as foreground with a placeholder notification.

The placeholder notification carries a content title — Samsung One UI can
render title-less notifications as blank and a few Android 14 builds throw
IllegalArgumentException from startForeground() without one, which would
be swallowed by the guard's try/catch and re-trigger the very timeout we
are preventing. Matches the crash's heavy Samsung/Lenovo-tablet bias.
2026-04-17 18:13:02 +02:00
Johannes Millan
367342edd6 fix(android): guard webchrome dialogs against activity teardown
Prevents WindowManager$BadTokenException in FullscreenActivity.onJsConfirm
and onJsAlert when JS fires alert()/confirm() after the activity starts
finishing. Also ensures JsResult is always released so the WebView JS
thread never deadlocks on lifecycle-driven dismissal.
2026-04-17 18:13:02 +02:00
Johannes Millan
5787437a9a
fix(reminder): restore Android alarms after app update (#7254)
Android clears all AlarmManager alarms (and may cancel WorkManager jobs)
whenever the app's APK is replaced — i.e. on every Play Store auto-update
or manual reinstall. BootReceiver already knows how to re-register
everything from ReminderAlarmStore and re-enqueue SyncReminderWorker, but
its intent-filter only listened for BOOT_COMPLETED, so after an update
reminders stayed silently unregistered until the user next opened the app.

Add MY_PACKAGE_REPLACED to the manifest and broaden the receiver's action
guard so alarm restoration runs on both events. No new permissions or
helpers required — MY_PACKAGE_REPLACED is a protected broadcast delivered
only by the system to the owning app.

Refs #6832

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 17:39:59 +02:00
Johannes Millan
0e9218bd68 fix(sync): handle data validation errors and improve client ID management
- Separate JsonParseError and SyncDataCorruptedError handlers in sync wrapper
- Handle corrupted remote data gracefully instead of throwing
- Add getOrGenerateClientId() as unified entry point, eliminating dual injection
  of ClientIdService + CLIENT_ID_PROVIDER in snapshot-upload, file-based-encryption,
  and sync-hydration services
- Use crypto.getRandomValues() instead of Math.random() for client ID generation
- Warn user when stored client ID is invalid and must be regenerated
- Fix flaky e2e supersync tests (premature waitForURL match on daily-summary URL)
2026-04-16 17:41:39 +02:00
Johannes Millan
ea1ef16307 fix(android): support session-only SAF permissions on OEM devices
Some OEM ROMs throw SecurityException when takePersistableUriPermission is
called, even with FLAG_GRANT_PERSISTABLE_URI_PERMISSION requested. Instead of
rejecting the call, fall through and return the URI — it remains valid for the
session. Also fix checkUriPermission to use context.checkUriPermission() which
detects both persistent grants and active session grants, so isReady() no longer
clears a working (session-only) SAF URI on those devices.
2026-04-14 22:09:02 +02:00
Johannes Millan
d32f7037a3 fix(sync): preserve own vector clock counter across full-state op resets
When a full-state op (SYNC_IMPORT/BACKUP_IMPORT) arrived from another
client, mergeRemoteOpClocks reset the local clock to a "minimal" form
but only preserved the current client's counter from the incoming op's
clock. If the current client had issued ops (e.g. GLOBAL_CONFIG) not
reflected in the incoming full-state op's clock, its counter was dropped,
causing subsequent ops to reuse the same counter value. Downstream clients
then saw these ops as EQUAL (duplicate) and skipped them silently.

Fix: take max(mergedClock[clientId], currentClock[clientId]) when
rebuilding the clock after a full-state op reset.

Also add __SP_E2E_BLOCK_WS_DOWNLOAD flag to WsTriggeredDownloadService
to allow E2E tests to block automatic WS-triggered downloads during
concurrent conflict scenarios.

Fix archive conflict test by blocking WS downloads on Client A during
the concurrent edit phase so it doesn't auto-receive B's rename via
WebSocket before archiving (restoring the intended conflict scenario).

Fix LWW singleton test to assert convergence rather than specific winner.
Fix renameTask helper to avoid Playwright/Angular re-render races.
Fix shepherd.js import paths that broke the Angular dev server build.
2026-04-01 15:41:13 +02:00
Johannes Millan
edb102534e
feat(android): improve task title from share intent using EXTRA_SUBJECT (#7052)
* feat(android): improve task title from share intent using EXTRA_SUBJECT

Browsers and many apps send the page title via Intent.EXTRA_SUBJECT, which
was previously ignored. Now the share handler uses subject > title > content
to build a meaningful task title instead of the generic "Check: Shared Content".
For links without metadata, a readable URL (domain + path) is used instead.

https://claude.ai/code/session_01LzFepWFjLKQSWKZn4iXEpS

* chore: update package-lock.json after npm install

https://claude.ai/code/session_01LzFepWFjLKQSWKZn4iXEpS

* refactor(android): simplify _buildTaskTitle and fix double spaces in URL

Deduplicate the subject/title preference logic and collapse multiple
spaces in _readableUrl output caused by leading slashes in pathnames.

https://claude.ai/code/session_01LzFepWFjLKQSWKZn4iXEpS

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-31 13:12:34 +02:00
Johannes Millan
136bb51ea0 fix(android): prevent crashes from null bridge and foreground service contract violations
Guard against null bridge/webView after onCreate by showing WebViewBlockActivity
with actionable guidance instead of crashing. Satisfy Android's foreground service
contract in TrackingForegroundService and FocusModeForegroundService by posting a
minimal notification before stopping when started without a valid action. Handle
null taskId on ACTION_START without killing an active tracking session.
2026-03-29 19:57:10 +02:00
Johannes Millan
9f8cf0a3d0 fix(oauth): use platform-specific redirect URIs and iOS client ID
Google rejects custom scheme redirect URIs that don't match the
platform's app identifier. Use the Android applicationId
(com.superproductivity.superproductivity) on Android and the iOS
bundle ID (com.super-productivity.app) on iOS, with single-slash
URI format per Google's documentation.

- Add iosClientId to OAuthFlowConfig and plugin API types
- Add iOS OAuth client ID to Google Calendar plugin
- Select client ID per platform (Android/iOS/Desktop) in bridge service
- Add Android package name intent filter in AndroidManifest
- Accept both URI schemes in OAuth callback handler
- Fix redirect handler to use IS_NATIVE_PLATFORM consistently
2026-03-27 17:33:38 +01:00
Johannes Millan
bbf3c741a7 feat(tasks): lock drag-and-drop Y-axis on small screens and throttle tree drag
Lock cdkDragLockAxis to Y on small screens (<600px) for task-list and
planner components, matching the breakpoint where planner switches to
single-column and bottom nav appears. Nav menu tree always locks Y.
Uses reactive LayoutService.isXs() so orientation changes work correctly.

Throttle tree-dnd onDragMoved with requestAnimationFrame to reduce
layout thrashing from getBoundingClientRect/elementFromPoint calls.
2026-03-24 16:25:42 +01:00
Johannes Millan
f20ebea583 fix(android): prevent ForegroundServiceDidNotStartInTimeException and bridge NPE
Add safeStopSelf() to TrackingForegroundService and FocusModeForegroundService
that calls startForeground() before stopSelf() in fallback branches (missing
task ID, stop when not running, null/unknown action). Android requires
startForeground() within 10s of startForegroundService().

Replace ambiguous NullPointerException in CapacitorMainActivity.onCreate with
a descriptive IllegalStateException when Capacitor bridge fails to initialize.
Add safe-call operators in onSaveInstanceState/onRestoreInstanceState.
2026-03-24 16:25:42 +01:00
Johannes Millan
02bc3e88e3 feat(two-way-sync): add plugin OAuth, two-way field sync, and remote issue deletion
Add OAuth support for plugins with PKCE, token persistence in local-only
IndexedDB, and Electron/web redirect handling. Extend two-way sync with
dueDay, dueWithTime, and timeEstimate field mappings including mutually
exclusive field clearing. Support remote issue deletion on task delete
and remote deletion detection during polling.

Add Google Calendar plugin (disabled from bundled builds pending legal
review) as a development reference for the plugin OAuth and two-way
sync APIs.

Additional fixes:
- Restore accidentally deleted focus-mode translation keys
- Remove full Task[] from deleteTasks action to prevent op-log bloat
- Add dueDay/deadlineDay format validation guards (#6908)
- Fix Android reminder alarm cancel intent matching
- Validate dueDay format to prevent false overdue from corrupted data
- Fix PKCE test expectation after utility consolidation
2026-03-22 13:02:41 +01:00
Johannes Millan
da41ad7603 fix(android): match cancel intent action to schedule intent for reminder alarms
cancelReminder() created a PendingIntent without setting the action,
while scheduleReminder() set ACTION_SHOW_REMINDER. Android's
Intent.filterEquals() treats null != "action" so alarmManager.cancel()
silently failed to find the scheduled alarm.
2026-03-22 10:10:19 +01:00
Johannes Millan
c13bf03d37
fix(android): prevent stale check from suppressing valid reminder notifications (#6905)
* fix(android): prevent stale check from suppressing valid reminder notifications

The isTaskStale check in ReminderAlarmReceiver was incorrectly suppressing
all locally-scheduled reminders. After 6fd0696 enhanced extractRemindersToSchedule
to detect schedule ops from actionPayload, the stale check would find the
task's own schedule op and mark it as stale — even though the alarm IS the
current schedule.

Fix by passing triggerAtMs through the alarm intent and comparing it with
schedule ops' remindAt. A task is only considered rescheduled (stale) when
the schedule op's remindAt differs from the firing alarm's trigger time.

https://claude.ai/code/session_013216HfgtetW9KKzFr5ieLQ

* fix(android): handle dual-reminder edge case and seq cursor reset

Two additional fixes found during review:

1. Dual-reminder edge case: A task can have both a standard reminder
   (remindAt) and a deadline reminder (deadlineRemindAt) with different
   times. The stale check now matches on isDueDate to prevent one
   reminder type from suppressing the other.

2. Seq cursor stuck after server reset: If the server is wiped (e.g.,
   clean sync import), latestSeq drops below the stored lastSeq. The
   worker now resets the cursor when the server's seq is lower, instead
   of being permanently stuck.

https://claude.ai/code/session_013216HfgtetW9KKzFr5ieLQ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-21 01:08:44 +01:00
Johannes Millan
6fd069658e fix(android): detect schedule/unschedule ops in background sync reminder worker
The op-log system returns empty entityChanges for most actions — only
TIME_TRACKING and addTask (CRT) ops populate it. The background sync
provider was relying on entityChanges to detect remindAt changes, so
schedule/unschedule/deadline actions were invisible to the worker.

Fix by matching on actionType strings and parsing actionPayload directly:
- unscheduleTask, removeDeadline → cancel reminders
- scheduleTaskWithTime, reScheduleTaskWithTime → parse actionPayload.remindAt
- setDeadline → parse actionPayload.deadlineRemindAt
2026-03-20 21:36:31 +01:00
Johannes Millan
a87e9ea7a9 fix(android): fix background sync reminder ordering and title extraction
Let later ops win when dismiss and reschedule are in the same batch
(previously cancellation always won, suppressing rescheduled reminders).
Also extract task title from actionPayload as fallback for UPD ops.
2026-03-20 21:36:30 +01:00
Johannes Millan
6d3006dd28 fix(android): use full server format for background sync op parsing
The SuperSync server returns operations in full format (full field names
and NgRx action strings), but the parser was using compact format codes.
This caused all background sync parsing to silently fail, preventing
reminder cancellation and scheduling.
2026-03-20 21:36:30 +01:00
Johannes Millan
167bdedec1 feat(android): check-before-show and schedule reminders from background sync
Add server-side stale check to ReminderAlarmReceiver before showing
notifications. When an alarm fires, the receiver does a quick server
fetch (5s OkHttp timeout, fail-open) to suppress notifications for
tasks that were deleted, done, dismissed, or rescheduled on another
device.

Also extend the background sync worker to schedule new/updated
reminders discovered in sync operations, not just cancel stale ones.

Key design decisions:
- Fail-open: any error or timeout still shows the notification
- Receiver only checks its own task; Worker owns all other reminder
  management and the seq cursor
- Last-writer-wins dedup via Map keyed by (taskId, isDueDate)
- Cancellation wins over scheduling in the same batch
- Independent try/catch per cancel/schedule to prevent cascading failures
- SupervisorJob on receiver's CoroutineScope
2026-03-20 21:36:30 +01:00
Johannes Millan
eb72992d3e fix(android): delay debug toast by 10 seconds 2026-03-20 21:36:30 +01:00
Johannes Millan
10a3db97ea
feat(droid): background sync notifications for android
* docs: add plan for Android background sync via WorkManager

Outlines the implementation plan for using WorkManager to periodically
sync with SuperSync server in the background, cancelling stale reminders
for tasks that were completed/deleted on other devices.

https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe

* docs: refine plan based on multi-agent architecture review

- Pin frontend hook to SyncProviderManager.currentProviderPrivateCfg$
- Add skipWhileApplyingRemoteOps() guard for selector-based effect
- Add HRX (dismiss reminder) and deadlineRemindAt to parser
- Add batch operation ds field handling
- Add ProGuard rules step for release builds
- Add error handling note for BootReceiver WorkManager enqueue

https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe

* docs: improve plan after second review round

Key changes:
- Remove Step 5 (ReminderAlarmStore lookup) - hash-based cancellation is
  sufficient, no store lookup needed
- Add BackgroundSyncProvider interface for extensibility to Dropbox/WebDAV
- Per-account lastServerSeq keyed by baseUrl hash
- Add SyncReminderScheduler helper to deduplicate scheduling logic
- Document edge cases: account switching, token expiry, force-kill,
  gapDetected, notification race conditions
- Add extensibility section explaining Dropbox feasibility and architecture

https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe

* feat(android): add background sync to cancel stale reminders via WorkManager

When a task is completed/deleted on desktop, Android reminders for that task
would still fire because the mobile app didn't know about the change. This adds
a WorkManager periodic job (every 15 min) that fetches operations from the
SuperSync server and cancels AlarmManager alarms + notifications for tasks that
are done, deleted, archived, or had their reminders dismissed.

Architecture:
- BackgroundSyncProvider interface for extensibility (Dropbox/WebDAV possible later)
- SuperSyncBackgroundProvider: lightweight HTTP client + operation parser
- SyncReminderWorker: CoroutineWorker that fetches ops and cancels reminders
- SyncReminderScheduler: helper to schedule/cancel the WorkManager job
- AndroidSyncBridgeEffects: mirrors SuperSync credentials to native SharedPreferences
- Per-account lastServerSeq prevents account-switching bugs

https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe

* chore: update package-lock.json after npm install

https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe

* fix(android): address review issues and add tests for background sync

Fixes from code review:
- Fix distinctUntilChanged to properly handle provider switching
  (non-SuperSync emissions now treated as equal to prevent repeated clears)
- Add null-safe ops array parsing in SuperSyncBackgroundProvider
- Add write/call timeouts to OkHttpClient (30s write, 60s call)
- Remove no-op `hash and hash` line in Kotlin hash function

Tests:
- Add cross-platform parity tests for notification ID hash function
  (pinned expected values that must match Kotlin implementation)
- Add AndroidSyncBridgeEffects unit tests covering:
  - distinctUntilChanged comparator behavior
  - credential set/clear decision logic
  - observable filtering integration (dedup, null filtering)

https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe

* fix(android): fix hash overflow and operation parser payload paths

Two critical bugs found during final review:

1. Hash function abs(Int.MIN_VALUE) bug: In Kotlin, abs(Int.MIN_VALUE)
   returns Int.MIN_VALUE (still negative) due to 32-bit overflow.
   Fixed by converting to Long before abs(), matching JS behavior where
   Math.abs works on 64-bit floats.

2. Operation parser payload structure: The parser was looking at
   p.isDone and p.task.changes directly, but the actual compact
   operation format wraps everything in p.entityChanges[] and
   p.actionPayload. Rewrote to use p.entityChanges as primary source
   (consistent structure with entityType, entityId, changes fields)
   and p.actionPayload.task.changes as secondary source.

https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe

* docs: add long-term plan for Android background sync improvements

Covers three phases: fast app startup via cached sync seq,
push-based notification cancellation via FCM, and extending
background sync to Dropbox/WebDAV providers.

https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe

* fix(android): address review feedback for background reminder sync

- Remove PLAN.md from repo root (C1)
- Use EncryptedSharedPreferences for access token storage with
  fallback to standard SharedPreferences on broken KeyStore (C2)
- Reset lastServerSeq when access token changes to prevent stale
  seq on account switch with same server URL (I1)
- Add max iteration guard (100) to pagination loop to prevent
  infinite loops from server bugs (I2)
- Use type predicate filter instead of non-null assertions (I3)
- Add exponential backoff (5min) to WorkManager schedule (S1)
- Add comments linking action type codes to frontend source (S2)
- Extract distinctUntilChanged comparator to named function with
  JSDoc explaining the suppression semantics (S3)

https://claude.ai/code/session_01RkrVBB492k8RBA8zt5swwe

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-20 10:24:02 +01:00
Johannes Millan
06b8831948
fix(android): match navigation bar color to system theme on startup (#6872)
Add values-night/colors.xml with dark theme colors (#131314) so the
navigation bar and status bar match the system dark theme before the
Angular app boots. Also fix light-mode status bar initial color to
#f8f8f7 to match what the app sets dynamically.

https://claude.ai/code/session_017CVEn9yTUqcPiRzoafP24R

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 01:43:14 +01:00
Johannes Millan
945b8ed15c fix(android): improve startup overlay to webapp add task bar transition
- Match webapp styling: replace colored border stroke with 12dp
  elevation shadow matching the webapp's global bar variant
- Show webapp add task bar when native overlay bar is open but empty,
  not just when it contains text
- Target global add task bar specifically (add-task-bar.global) to
  avoid focusing an inline bar that may already exist on the page
2026-03-18 13:14:10 +01:00
Johannes Millan
49952c5c6a feat(android): add native startup overlay for quick task entry
Show a native FAB button during app startup so users can add tasks
while Angular bootstraps. Tapping the FAB expands a themed input bar
with keyboard tracking. Submitted tasks queue in SharedPreferences
via WidgetTaskQueue and are processed by AndroidEffects after
hydration. Partial (unsubmitted) text transfers to the web AddTaskBar
via sessionStorage.

Design:
- Two-phase dismiss keeps native overlay visible until web input mounts
- MutationObserver with resolved guard detects AddTaskBar readiness
- Day/night theme support with primary-colored border
- FAB color (#6495ED) matches the loading spinner
- Overlay cleaned up in onDestroy to prevent listener leaks

Also removes the old Quick Add Widget (QuickAddActivity,
QuickAddWidgetProvider, and all associated resources).

Also modernizes the loading spinner from double-bounce to a subtle
pulse animation.
2026-03-18 13:14:10 +01:00
Johannes Millan
8a4473541e fix(android): set navigation bar color to match app theme
Add NavigationBarPlugin Capacitor plugin to dynamically update the
Android system navigation bar color and icon style when the app theme
changes. Set initial color in native XML styles for all themes
including the launch theme. Reduce bottom nav and FAB shadows to
prevent visual cutoff at the system navigation bar boundary.
2026-03-18 13:14:10 +01:00
Johannes Millan
42dc92b10d fix(sync): prevent empty-upload to WebDAV on Android
Add defense-in-depth guards to prevent the Capacitor bridge from
silently uploading zero-byte files to WebDAV, which corrupts sync
state for all clients.

- Add empty-data guard in WebdavApi.upload() with tests
- Reject empty PUT body in Android WebDavHttpPlugin (Kotlin)
- Validate compressAndEncryptData() output for all providers
- Add payload size diagnostic logging in WebDavHttpAdapter
- Use android.util.Log instead of println for proper logcat filtering

Closes #6855
2026-03-17 13:59:40 +01:00
Johannes Millan
6768eaeb22 fix(android): show correct notification text for due date reminders
The notification body incorrectly showed "Note reminder" for all
non-TASK reminder types, including DUE_DATE reminders. Replace the
if/else with a when expression that handles each type explicitly.
2026-03-10 15:58:22 +01:00
Johannes Millan
fd28c04cce feat(reminder): overhaul mobile notification UX for Android and iOS
- Add notification actions: Done, Snooze 10m, Snooze 1h (Android + iOS)
- Add BootReceiver to re-register alarms after device reboot
- Add SharedPreferences queues for background-to-frontend communication
  (ReminderDoneQueue, ReminderSnoozeQueue, ReminderTapQueue)
- Add "fire and verify" pattern: sync before acting on stale notifications
- Add due-date notification scheduling (configurable hour, default 9 AM)
- Add exact alarm permission warning for Android 12+
- Add notification grouping (Android groups, iOS thread identifiers)
- Add isOngoing flag for persistent alarm-style notifications
- Sanitize REMINDER_TASK_ID to prevent JS injection
2026-03-06 21:19:55 +01:00
Johannes Millan
de03a2d11e fix(android): prevent crash on SAF folder selection (#6545)
Wrap takePersistableUriPermission() in try/catch to handle SecurityException
thrown on certain Samsung/Android 13 devices. Reject the Capacitor call on
failure so the error propagates cleanly. Add .catch() to FormlyBtnComponent
promise chain to prevent unhandled rejections.
2026-02-16 18:44:27 +01:00
Johannes Millan
02fe1a5ff2 fix(android): persist share intent to SharedPreferences for cold-start reliability
Share-to-app via Android SEND intent was unreliable on cold start because
share data was stored only in memory (pendingShareIntent). If the process
died before Angular loaded, the data was lost. Now share data is persisted
to SharedPreferences immediately and pulled synchronously by the frontend.
2026-02-13 14:06:43 +01:00