Skip to content

feat: Profiles switch safety - #21815

Open
criticalAY wants to merge 6 commits into
ankidroid:mainfrom
criticalAY:profiles/switch-safety
Open

criticalAY wants to merge 6 commits into
ankidroid:mainfrom
criticalAY:profiles/switch-safety

Conversation

@criticalAY

@criticalAY criticalAY commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Note

Assisted-by: Assisted-by: Claude Opus 5

Purpose / Description

Switching profiles has to restart the app, because the backend, WebView and SharedPreferences all hold on to the old profile's paths. Nothing did that restart yet, and nothing stopped a switch from killing a sync or backup halfway through. This adds the restart and the checks that guard it. Nothing calls them yet; wiring them to the profile list is the next PR.

Fixes

Approach

See commits

How Has This Been Tested?

Unit tests cover the restart

Checklist

Please, go through these checks before submitting the PR.

  • You have a descriptive commit message with a short title (first line, max 50 chars).
  • You have commented your code, particularly in hard-to-understand areas
  • You have performed a self-review of your own code
  • UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • UI Changes: You have tested your change using the Google Accessibility Scanner

Licenses

Library Description License
Process Phoenix A special activity which facilitates restarting your application process The Apache Software License, Version 2.0

Maintainers:

  • Add the Licenses Adds a resource under an open source license See wiki: Licences label
  • Update the licenses wiki when merging

@david-allison david-allison left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are excellent. Blocker on ProcessPhoenix (or vendoring; probably best to take on the dependency).

Note the 'Licenses' section of the PR template

Comment thread AnkiDroid/src/main/java/com/ichi2/anki/multiprofile/AppRestart.kt Outdated
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/multiprofile/ProfileManager.kt Outdated
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/worker/SyncMediaWorker.kt
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/worker/SyncWorker.kt Outdated
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/BackendBackups.kt
@david-allison david-allison added Needs Author Reply Waiting for a reply from the original author and removed Needs Review labels Sep 11, 2026
@criticalAY criticalAY added the Licenses Adds a resource under an open source license See wiki: Licences label Sep 11, 2026
@criticalAY
criticalAY force-pushed the profiles/switch-safety branch from 98c6ee8 to 403cd3c Compare September 11, 2026 22:29
@criticalAY criticalAY added Needs Review and removed Needs Author Reply Waiting for a reply from the original author labels Sep 11, 2026
@david-allison
david-allison self-requested a review September 11, 2026 23:10
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/multiprofile/AppRestart.kt Outdated
@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Sep 12, 2026
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt
@criticalAY
criticalAY force-pushed the profiles/switch-safety branch from 403cd3c to 641f129 Compare September 12, 2026 18:30
@criticalAY criticalAY removed the Needs Author Reply Waiting for a reply from the original author label Sep 12, 2026
@criticalAY
criticalAY force-pushed the profiles/switch-safety branch from 641f129 to d0cc9e5 Compare September 12, 2026 19:02

@david-allison david-allison left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, cheers!

@david-allison david-allison added Needs Second Approval Has one approval, one more approval to merge and removed Needs Review labels Sep 13, 2026
ProcessPhoenix will do the restart. It relaunches the app from a
separate process once this one has been killed, so the relaunch is
never handed to a process that is still dying. It is Apache 2.0 and has
no dependencies of its own.
PhoenixActivity runs in its own :phoenix process, and Android creates
the Application in every process of the app. AnkiDroidApp would start
the backend and the rest of the app there, in a process that only lives
long enough to kill the old one and launch the new one. It now returns
straight away there.

Assisted-by: Claude Opus 5 (multiple iterations over original code)
A profile switch restarts the process, so it has to wait for work that
outlives the collection queue, and keep new work from starting. A
backup keeps writing after the backend releases the collection, and
syncs run in workers.

Each now holds a lock while it runs, for the restart to take. A worker
that finds its lock taken drops its work rather than rescheduling it,
since the work carries the AnkiWeb key of the profile being left.

The backend cannot be asked to wait for a backup from elsewhere: it
hands its running backup to the first caller that asks, and
createBackup asks as soon as the backup starts.

Assisted-by: Claude Opus 5 (multiple iterations over original code)
A profile switch cannot be applied in place: the backend, WebView and
SharedPreferences all cache paths from the profile that was active when
the process started, so the process has to die and come back.

The restart takes the sync lock first, waiting for a sync that is
already running, since that sync queues a media sync before it lets go
of its lock. Sync work is then cancelled, since it carries this
profile's AnkiWeb key, which also stops a running media sync rather
than waiting it out. The media sync and backup locks come next. All
three are kept until ProcessPhoenix kills the process, so nothing can
start in the meantime, and released if the handover fails.

The collection is then closed: the backend holds it open, and killing
the process mid-write risks corrupting it. ensureClosed runs on the
collection queue, so it also waits for an operation that is already
running.

Assisted-by: Claude Opus 5 (multiple iterations over original code)
A switch is followed by a process restart, and SharedPreferences.apply()
writes to disk asynchronously. Android does not flush pending apply()
writes when the process is killed, so the new profile id could be lost
and the app would come back on the old profile.

Commit the switch write. Startup keeps the asynchronous write, since
nothing kills the process after it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Has Conflicts Licenses Adds a resource under an open source license See wiki: Licences Needs Second Approval Has one approval, one more approval to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants