super-productivity/android
Johannes Millan 7483ba241b
feat(local-backup): harden mobile backups against eviction (#7901) (#7924)
* fix(local-backup): don't overwrite a good backup with empty state (#7901)

After a WebView IndexedDB eviction the live NgRx store can boot empty.
The 5-min local-backup timer would then clobber the last good copy in
durable storage (Android SQLite KeyValStore, iOS file, Electron file)
with nothing. Gate _backup() with hasMeaningfulStateData() so empty/
degraded state can never overwrite a good backup, mirroring the existing
snapshot/compaction empty-overwrite guard.

* feat(local-backup): keep a second backup generation on mobile (#7901)

A single overwritten row/file is fragile: one bad or corrupt write cycle
can erase the only copy. Add a two-generation ring on the single-slot
mobile platforms (Android KeyValStore key, iOS file) — promote the
current backup to a 'prev' slot before overwriting, and restore from the
newest usable slot, falling back to prev when the primary is corrupt or
empty. Electron already keeps rotated, timestamped backups, so it is left
unchanged.

Adds backup-ring.util (isUsableBackupStr / selectBestBackupStr) with full
unit coverage; the iOS write is split behind _writeIOSFile so the promote
ordering is testable without the Capacitor Filesystem proxy.

* 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.

* fix(local-backup): harden restore paths from multi-agent review (#7901)

- loadBackupIOS: return '' instead of throwing when no usable backup
  exists. askForFileStoreBackupIfAvailable runs from the fire-and-forget
  _initBackups() at startup, so a throw became an unhandled rejection; ''
  degrades to the existing import-error snack and mirrors loadBackupAndroid.
- Consolidate the Android newline escape into loadBackupAndroid (the single
  escape site) and drop the now-redundant re-escape + log line at the call
  site, removing a full-string pass on the restore path.

* feat(local-backup): restore fuller generation, show contents (#7901)

Two restore-path improvements (issue #7901 item 4):

- selectBestBackupStr now prefers the ring generation carrying MORE
  data when both slots are usable (tie -> primary). After an eviction
  the live store can boot near-empty and a 5-min backup may write that
  degraded state to the primary; the full copy survives in prev, and
  restore must surface the full one rather than the newer-but-smaller.

- The mobile restore prompt loads the backup first and names its task
  and project counts (new RESTORE_FILE_BACKUP_MOBILE string), so a user
  never blindly discards the only copy of their data. Falls back to the
  generic prompt for an unparseable blob.

Note: a 'defer backups until the restore decision resolves' guard was
considered but is already structurally guaranteed -- init() (the only
thing starting the 5-min timer) runs after the synchronous restore
confirm in _initBackups, so the timer cannot fire before the decision.

* fix(local-backup): restore the newest backup, not the larger one (#7901)

Multi-agent review of d1e03e550a flagged the restore-time "prefer the
fuller generation" heuristic as unsafe: a legitimate bulk-archive or
delete makes the newer generation smaller, producing the SAME ring shape
as a post-eviction degraded primary. Preferring the larger slot would
then silently restore the older generation and resurrect removed tasks --
a worse, more common failure than the narrow eviction gap it closed.

- Revert selectBestBackupStr to newest-usable-wins (drop backupWeight and
  the size comparison). The prev slot stays a fallback for an empty/corrupt
  newest slot only. The informed prompt (below) is what protects the
  eviction case now: the user sees a substantial backup and accepts it.
- Fix the prompt counts (summarizeBackupStr): include archived tasks so a
  heavily-archived backup doesn't read as empty, and exclude the
  always-present INBOX project so it matches hasMeaningfulStateData.
- Reword RESTORE_FILE_BACKUP_MOBILE: drop the self-contradictory "NO DATA
  ... but a backup with N tasks" framing, and use label-style counts
  ("tasks: N") so it reads correctly for a count of 1 (no ICU plurals).

Deferred follow-ups from the review: a write-time "suspicious shrink"
guard (needs threshold design) and a backup date in the prompt (needs an
Android bridge change to surface KEY_CREATED_AT; iOS has stat mtime).
2026-06-01 21:40:10 +02:00
..
.idea chore(android): untrack .idea/gradle.xml 2026-04-17 18:13:02 +02:00
app feat(local-backup): harden mobile backups against eviction (#7901) (#7924) 2026-06-01 21:40:10 +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