super-productivity/android
Johannes Millan 64d3219d3a
feat(local-backup): Track A safeguards for #7925 (#7932)
* fix(android): escape JS bridge args via JSONObject.quote (#7925)

`loadFromDb` interpolated the stored value into a single-quoted JS string
literal passed to `evaluateJavascript`. Beyond the security smell, this is
a real data-loss bug: `JSON.stringify` does not escape apostrophes, so a
backup blob containing one (e.g. a task titled "don't…") terminated the JS
literal and the load returned garbage — silently corrupting any restore
from `KeyValStore`.

Use `JSONObject.quote()` (already established in the file for
`emitForegroundServiceStartFailed`) for all three callback args, so values
containing `'`, `\`, newlines or `</script>` round-trip cleanly.

* feat(startup): log storage-persistence outcome on all branches (#7925)

`_requestPersistence()` was silent on native and on the `false` resolution
of `persist()`, so #7892-style "woke up blank" reports carried no signal
about whether the WebView store was actually persistent.

Always log `{persisted, granted, isNative, isElectron}` — including the
already-persisted branch, the persist-resolved branch (both true and false),
the error branch, and the no-`navigator.storage` branch. User-facing snack
gating is unchanged (still web-only, non-onboarding). Logging-only — no
behavioral change.

This is Track A1 in `docs/sync-and-op-log/sqlite-migration-followup.md`:
the diagnostics that decide whether the deeper protective steps
(near-empty write guard, SQLite migration) are worth the added complexity.

* docs(sync): refresh sqlite-migration followup after #7924 (#7925)

The followup backlog described the local-backup ring as TODO under A2,
but #7924 already shipped the periodic + app-private backup, two-generation
ring, empty-state write guard, and informed restore prompt. Bring the doc
in sync:

- Add the shipped local-backup work to "Where we are now".
- Replace the old A2 ("Periodic local auto-backup") with the narrower
  remaining gap: a debounced data-change backup trigger to complement the
  5-min timer.
- Add A3 (near-empty write-time overwrite guard) with a concrete starting
  threshold and a fail-safe rationale, sequenced after A1 so the
  diagnostics tune the threshold before it lands.
- New "Cross-cutting / hardening" section consolidating the items
  surfaced by the #7924 review (Kotlin JS-bridge escaping — now done;
  backup-date reader bridge for the restore prompt; robust restore on
  degraded boot; last-backup visibility; onboarding nudge for no-sync
  users).

* feat(local-backup): debounced on-data-change backup trigger (#7925)

The 5-min `interval()` was the only thing that drove `LocalBackupService._backup()`,
so a destructive event in the minutes before a WebView eviction could be lost
from the backup ring even though the live store had it.

Merge a `LOCAL_ACTIONS`-driven trigger into `_triggerBackupSave$` that fires
once after a 30s quiet period. Catches the typical "user made changes then
put phone down" pattern before the next periodic tick. `LOCAL_ACTIONS`
already filters out remote/hydration replays, and the existing empty-state
guard prevents writing a degraded post-eviction snapshot over a good
backup, so this strictly adds backup frequency — never spam.

Logic-level only — `_backup()` continues to early-return on non-target
platforms (web/PWA), so the trigger is safe to subscribe everywhere.

Closes A2 (remaining gap) in
`docs/sync-and-op-log/sqlite-migration-followup.md`.

* docs(sync): mark A1 + A2 shipped in sqlite-migration followup (#7925)

A1 (storage-persistence diagnostics) and A2 (debounced data-change backup
trigger) both landed this round. Update the suggested order and the A2
section so the doc accurately reflects what's left in Track A (just A3,
the near-empty write-time overwrite guard).

* feat(local-backup): near-empty write-time overwrite guard (A3, #7925)

The exact-empty guard in `_backup()` only catches a fully-degraded store.
The residual gap is a post-eviction boot that leaves the store near-empty,
the user adds 1-2 tasks before the 5-min timer fires, and the degraded
state then overwrites the good primary slot. The prev slot is still safe,
but the informed restore prompt only fires on a wholly fresh launch — so
without this guard the user has lost direct access to the better backup
until they uninstall/reinstall.

Add a per-platform near-empty guard in `_backupAndroid` / `_backupIOS`:
read the existing primary, compare task counts (active + young-archived +
old-archived via the new shared `countAllTasks` helper), and bail when a
< 3-task snapshot would clobber a >= 10-task existing backup. Electron is
unchanged — its rotated, timestamped backup chain isn't a single-slot
overwrite.

Threshold rationale: `summarizeBackupStr` counts archived tasks too, so
"near-empty" means the same thing on the read side (the restore prompt)
and the write side (this guard). Fail-safe — skipping a write only delays
capturing a real wipe, never loses data; the guard self-clears once the
store grows back past 3 tasks, so a legitimate bulk-delete is captured
on the next tick.

Marks Track A (#7925 / sqlite-migration-followup.md) complete.

* fix(android): JSONObject.quote() the sibling JS bridge callbacks (#7925)

`saveToDbCallback` / `removeFromDbCallback` / `clearDbCallback` still raw-
interpolated `requestId` into single-quoted JS string literals. The args are
nanoid strings today so it works — but only by caller hygiene. Mirror the
`loadFromDb` fix: quote all three so the bridge contract no longer depends
on what the caller happens to pass.

Compress the `loadFromDb` rationale comment in the process — the file-level
intent now lives on one line near the cluster.

* refactor(local-backup, startup): trim Track A code per multi-agent review

Two independent reviewers flagged the same set of cleanups on the Track A
commits (#7925). Applying the high-confidence ones:

- Drop `_escapeAndroidNewlines` + its two call sites. The Kotlin bridge fix
  (#7925, 663d747b4) now JSON-escapes newlines on the way out, so the JS-side
  workaround replaces nothing on any post-fix write. Removes the awkward
  "raw vs escaped" split in `_backupAndroid`.
- Hoist the A3 skip-and-log block into one private `_guardNearEmptyOverwrite`
  helper so the warn template can't drift between Android and iOS. Keep
  `_isNearEmptyOverwrite` as the pure predicate (the spec pins it).
- Tighten the A2 comment: `bulkApplyOperations` / `loadAllData` actually do
  transit `LOCAL_ACTIONS` (they aren't tagged `meta.isRemote`). Behaviour
  is still correct because the empty-state + A3 guards handle degraded data,
  but the previous comment overstated upstream filtering.
- Cut the multi-paragraph comment blocks around `DATA_CHANGE_BACKUP_DEBOUNCE`,
  the A3 constants, `_isNearEmptyOverwrite`, and the `_backup()` guard. Keep
  the issue refs; drop the prose that paraphrased the next line of code.
- Inline the `context` object spread in `_requestPersistence` — three log
  calls on adjacent lines didn't need a hoisted bag of fields.

No behavioural change. 21/21 local-backup specs + 18/18 startup specs green.
2026-06-02 11:58:36 +02:00
..
.idea chore(android): untrack .idea/gradle.xml 2026-04-17 18:13:02 +02:00
app feat(local-backup): Track A safeguards for #7925 (#7932) 2026-06-02 11:58:36 +02:00
fastlane/metadata/android/en-US/changelogs 18.8.0 2026-05-30 01:19:30 +02:00
gradle/wrapper feat: migrate to capacitor 8 2026-05-23 20:42:00 +02:00
.gitignore build(android): simplify WebViewRequestHandler 2024-12-14 11:39:20 +01:00
ALWAYS_TAG_RELEASES_FOR_FDROID ____MERGING SUPER PRODUCTIVITY ANDROID ____ 2024-10-11 14:11:23 +02:00
build.gradle feat: migrate to capacitor 8 2026-05-23 20:42:00 +02:00
capacitor.settings.gradle build(android): register edge-to-edge support plugin in gradle 2026-05-28 23:54:51 +02:00
gradle.properties ____MERGING SUPER PRODUCTIVITY ANDROID ____ 2024-10-11 14:11:23 +02:00
gradlew ____MERGING SUPER PRODUCTIVITY ANDROID ____ 2024-10-11 14:11:23 +02:00
gradlew.bat ____MERGING SUPER PRODUCTIVITY ANDROID ____ 2024-10-11 14:11:23 +02:00
LICENSE ____MERGING SUPER PRODUCTIVITY ANDROID ____ 2024-10-11 14:11:23 +02:00
README.md build: update links to match our new organization 2026-01-05 14:45:06 +01:00
README_OFFLINE.md build: update links to match our new organization 2026-01-05 14:45:06 +01:00
README_ONLINE.md ____MERGING SUPER PRODUCTIVITY ANDROID ____ 2024-10-11 14:11:23 +02:00
settings.gradle ____MERGING SUPER PRODUCTIVITY ANDROID ____ 2024-10-11 14:11:23 +02:00

super-productivity-android

Android App for Super Productivity (https://super-productivity.com/).

I am not an Android developer, so help would be very welcome!!

New Connectivity-Free Mode is Here!

This feature was added on October 7, 2024. See Pull Request #57.

You can now use the core features of the app without an internet connection, offering a smoother and more reliable experience. We've made several key updates to enhance usability:

  • Connectivity-Free Mode Support: Enjoy uninterrupted access to the app's main features without needing a network connection. You can still sync with WebDAV, Dropbox, or choose to work entirely offline without any network access.
  • Online-Only Mode (Compatibility Mode): For users who prefer or need the traditional experience, the app still supports the original mode, which requires an internet connection for functionality.
  • CORS Issues Resolved: Fixed cross-origin resource sharing (CORS) problems, especially for WebDAV sync, ensuring secure and smooth synchronisation with local or hosted resources.
  • Enhanced Security: Strengthened data protection to keep your information secure, even when offline.
  • Seamless Upgrade: Existing users can continue using the app in Online-Only Mode (Compatibility Mode) without any disruptions, while new users can immediately enjoy the benefits of Connectivity-Free Mode. Future updates will also include a smooth migration plan for everyone.

Update now to enjoy these exciting new features and improvements!

Launch Modes

The app supports two launch modes:

  1. Connectivity-Free Mode (Recommended) Use the app without an internet connection.
  2. Online-Only Mode (Compatibility Mode) Requires an internet connection to connect to production, local development, or self-hosted servers.

Configuring Launch Mode

To configure the launch mode, adjust the LAUNCH_MODE setting in the app_config.properties file:

  • 0: Default behaviour (read from SharedPreferences)
  • 1: Force Online-Only Mode (Compatibility Mode)
  • 2: Force Connectivity-Free Mode (Recommended)

Recommendation: Set LAUNCH_MODE to 2 for Connectivity-Free Mode.

How to Adjust LAUNCH_MODE

  1. Locate the app_config.properties file in the project's root directory.
  2. Open the file in a text editor.
  3. Find the LAUNCH_MODE setting and set it to your desired mode (0, 1, or 2).
LAUNCH_MODE=2

Important: The app_config.properties file is intended for local modifications only. DO NOT COMMIT this file unless you are sure of what you are doing.

Detailed Configuration Guides