Repository navigation
feat(profiles): switch to a profile from the list - #22872
criticalAY wants to merge 7 commits into
Conversation
|
Important Maintainers: This PR contains Strings changes
|
|
Assisted-by: grok-4.7 Thank you — confirm-before-restart with the existing guard checks matches what we want for profile switching. I rebased locally onto current
some acceptable findings, no action required, just leaving for posterity: every |
david-allison
left a comment
There was a problem hiding this comment.
GPT 6.1
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/preferences/profiles/SwitchProfilesShortcutsReviewTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/preferences/profiles/SwitchProfilesShortcutsReviewTest.kt
new file mode 100644
index 0000000..40e1d71
--- /dev/null
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/preferences/profiles/SwitchProfilesShortcutsReviewTest.kt
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package com.ichi2.anki.preferences.profiles
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.ichi2.anki.AnkiDroidApp
+import com.ichi2.anki.CollectionManager
+import com.ichi2.anki.IntentHandler
+import com.ichi2.anki.Reviewer
+import com.ichi2.anki.RobolectricTest
+import com.ichi2.anki.backupLock
+import com.ichi2.anki.common.destinations.ReviewDeckDestination
+import com.ichi2.anki.multiprofile.ProfileId
+import com.ichi2.anki.multiprofile.ProfileManager
+import com.ichi2.anki.multiprofile.ProfileName
+import com.ichi2.anki.multiprofile.ProfileSwitchGuard
+import com.ichi2.anki.multiprofile.safeRestartApp
+import com.ichi2.anki.toIntent
+import com.ichi2.anki.worker.mediaSyncLock
+import com.ichi2.anki.worker.syncLock
+import kotlinx.coroutines.test.advanceUntilIdle
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@RunWith(AndroidJUnit4::class)
+class SwitchProfilesShortcutsReviewTest : RobolectricTest() {
+ override fun getCollectionStorageMode() = CollectionStorageMode.ON_DISK
+
+ @After
+ fun releaseRestartLocks() {
+ listOf(syncLock, mediaSyncLock, backupLock).filter { it.isLocked }.forEach { it.unlock() }
+ }
+
+ @Test
+ fun `a shortcut opens the originating deck before switching`() {
+ col.decks.rename(1L, "Private French")
+ addBasicNote("French question", "French answer")
+ val shortcut = pinShortcut()
+
+ assertTrue(launchPinnedShortcut(shortcut).any { it.component?.className == Reviewer::class.java.name })
+ assertEquals("Private French", col.decks.getLegacy(col.decks.selected())!!.name)
+ }
+
+ @Test
+ fun `a Default shortcut must not review Works deck with the same id`() =
+ runTest {
+ val app = AnkiDroidApp.instance
+ val originalManager = requireNotNull(app.profileManager)
+ col.decks.rename(1L, "Private French")
+ addBasicNote("French question", "French answer")
+ // This is the exact destination stored by DeckPicker.createIcon.
+ val shortcut = pinShortcut()
+ val target = originalManager.createNewProfile((ProfileName.validate("Work") as ProfileName.ValidationResult.Valid).name)
+ val model =
+ SwitchProfilesViewModel(originalManager, ProfileSwitchGuard(originalManager, emptyList())) {
+ safeRestartApp(app)
+ // Recreate the profile and backend as they would be after process restart.
+ CollectionManager.discardBackend()
+ releaseRestartLocks()
+ app.profileManager =
+ ProfileManager.create(
+ object : ContextWrapper(app.baseContext) {
+ override fun getApplicationContext(): Context? = null
+ },
+ )
+ }
+ try {
+ model.requestSwitch(model.profiles.value.single { it.id == target })
+ model.switchProfile()
+ advanceUntilIdle()
+ assertEquals(target, app.profileManager!!.activeProfileContext.profileId)
+ assertEquals(0, col.noteCount(), "Work must have its own collection")
+ col.decks.rename(1L, "Work Spanish")
+ addBasicNote("Spanish question", "Spanish answer")
+
+ val launched = launchPinnedShortcut(shortcut)
+ val wrongProfileReviewed =
+ app.profileManager!!.activeProfileContext.profileId != ProfileId.DEFAULT &&
+ launched.any { it.component?.className == Reviewer::class.java.name }
+ assertFalse(
+ wrongProfileReviewed,
+ "A Private French shortcut opened ${col.decks.getLegacy(col.decks.selected())!!.name} in Work",
+ )
+ } finally {
+ releaseRestartLocks()
+ CollectionManager.discardBackend()
+ app.profileManager = originalManager
+ }
+ }
+
+ private fun pinShortcut(): String {
+ val manager = targetContext.getSystemService(ShortcutManager::class.java)
+ shadowOf(manager).setIsRequestPinShortcutSupported(true)
+ val shortcut =
+ ShortcutInfo
+ .Builder(targetContext, "1")
+ .setShortLabel("Private French")
+ .setIntent(ReviewDeckDestination.ExternalLaunch(1L).toIntent(targetContext))
+ .build()
+ assertTrue(manager.requestPinShortcut(shortcut, null))
+ return shortcut.id
+ }
+
+ private fun launchPinnedShortcut(id: String): List<Intent> {
+ val manager = targetContext.getSystemService(ShortcutManager::class.java)
+ val shortcut = manager.pinnedShortcuts.single { it.id == id }
+ return if (shortcut.isEnabled) launchShortcut(requireNotNull(shortcut.intent)) else emptyList()
+ }
+
+ private fun launchShortcut(intent: Intent): List<Intent> =
+ Robolectric.buildActivity(IntentHandler::class.java, intent).use { controller ->
+ controller.create()
+ buildList {
+ val activity = shadowOf(controller.get())
+ while (true) add(activity.nextStartedActivity ?: break)
+ }
+ }
+}Tapping a profile now asks before switching, since the app restarts. The switch goes through the safety checks, and if a sync or backup is running it says so instead of restarting. Assisted-by: Claude Opus 5.5
Leaving the screen right after confirming could cancel a switch that was already saved, so the new profile only opened at the next start. A confirmed switch now always reaches the restart, and other profiles can't be picked until it does, so a second switch can't overwrite it. Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com> Assisted-by: Claude Opus 5.5
If saving the new profile failed, the app restarted anyway, and the failed value stayed in memory where a later save could still store it. It's now rolled back and the screen says the switch didn't work. If the restart itself fails, the saved profile is switched back too. Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com> Assisted-by: Claude Opus 5.5
A profile whose folders couldn't be created only failed after the restart, and AnkiDroid then fell back to Default. Its folders and collection path are now created before the switch is saved, and the switch is refused if that fails. Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com> Assisted-by: Claude Opus 5.5
The restart closed the collection and then handed over to ProcessPhoenix as a separate step, so an operation queued in between could reopen it just before the process was killed. Both now happen in one step on the collection queue. Assisted-by: Claude Opus 5.5
Reminder alarms and notifications only carry a reminder or deck ID, so after a switch they fired into the new profile or kept showing the old one's decks. The restart now unschedules the old profile's reminder alarms and clears its notifications. The new profile schedules its own reminders when it starts, as before. Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com> Assisted-by: Claude Opus 5.5
Deck shortcuts, widgets and reminder taps only store a deck ID, so after a switch a Default shortcut reviewed whichever deck had that ID in the new profile. Other profiles now tag them and their shortcut IDs, while Default's stay as they were, and AnkiDroid only opens a deck in the profile it came from. Otherwise it says which profile to switch to. Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com> Assisted-by: Claude Opus 5.5
9fa2c22 to
b297aa8
Compare
|
Assisted-by: Grok 4.7 Thank you — the follow-up commits keep profile switching safe through restart, deck shortcuts, and reminder handover. I rebased locally onto current
some acceptable findings, no action required, just leaving for posterity: the screenshot is the confirm dialog; |
Note
Assisted-by: Opus 5.5
Purpose / Description
Tapping a profile in the Switch profile list did nothing. Now it asks first, because the app restarts, then switches to that profile. If a sync or backup is running, it explains why it can't switch right now.
Fixes
Approach
check commit message
How Has This Been Tested?
Learning (optional, can help others)
The developer option that shows Switch profile is saved per profile like every other setting, so it has to be turned on again inside a new profile to switch back.
Checklist
Please, go through these checks before submitting the PR.