Skip to content

Commit 7e2d1e0

Browse files
committed
feat: allow profile deletion
Introduce deleteProfile() for removing a single user profile. Deletion removes both the registry entry (SharedPreferences) and the on-disk data directory. The only guard is: the currently active profile cannot be deleted, all other profiles including default are deletable.
1 parent 423346d commit 7e2d1e0

File tree

2 files changed

+92
-0
lines changed

2 files changed

+92
-0
lines changed

AnkiDroid/src/main/java/com/ichi2/anki/multiprofile/ProfileManager.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,32 @@ class ProfileManager private constructor(
229229
// TODO: Implement process restart logic (e.g. ProcessPhoenix)
230230
}
231231

232+
/**
233+
* Permanently deletes a profile, removing both its registry
234+
* entry and its on-disk data directory.
235+
*
236+
* @param profileId The [ProfileId] of the profile to delete.
237+
* @throws IllegalArgumentException if [profileId] is the
238+
* currently active profile.
239+
*/
240+
fun deleteProfile(profileId: ProfileId) {
241+
require(profileRegistry.getLastActiveProfileId() != profileId) {
242+
"Cannot delete the currently active profile (${profileId.value}). Switch first."
243+
}
244+
245+
profileRegistry.removeProfile(profileId)
246+
247+
val profileDir = resolveProfileDirectory(profileId)
248+
if (profileDir.file.exists()) {
249+
val deleted = profileDir.file.deleteRecursively()
250+
if (!deleted) {
251+
Timber.w("Failed to fully delete profile directory: ${profileDir.file.absolutePath}")
252+
}
253+
}
254+
255+
Timber.i("Deleted profile: ${profileId.value}")
256+
}
257+
232258
/**
233259
* Holds the meta-data for a profile.
234260
* Converted to JSON for storage to allow future extensibility (e.g. avatars, themes).
@@ -327,6 +353,18 @@ class ProfileManager private constructor(
327353
return result
328354
}
329355

356+
/**
357+
* Removes a profile entry from the registry.
358+
*
359+
* Does **not** delete the profile's data directory on disk.
360+
* Callers are responsible for cleaning up file-system resources.
361+
*
362+
* @param id The [ProfileId] of the profile to remove.
363+
*/
364+
fun removeProfile(id: ProfileId) {
365+
globalPrefs.edit { remove(id.value) }
366+
}
367+
330368
fun contains(id: ProfileId): Boolean = globalPrefs.contains(id.value)
331369
}
332370

AnkiDroid/src/test/java/com/ichi2/anki/multiprofile/ProfileManagerTest.kt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import io.mockk.unmockkAll
3636
import io.mockk.verify
3737
import org.junit.After
3838
import org.junit.Assert.assertEquals
39+
import org.junit.Assert.assertFalse
3940
import org.junit.Assert.assertTrue
4041
import org.junit.Before
4142
import org.junit.Test
@@ -200,4 +201,57 @@ class ProfileManagerTest {
200201
assertEquals(1, allProfiles.size)
201202
assertTrue(allProfiles.containsKey(ProfileId.DEFAULT))
202203
}
204+
205+
@Test
206+
fun `deleteProfile removes profile from registry`() {
207+
val manager = ProfileManager.create(context)
208+
val profileId = manager.createNewProfile("Temporary")
209+
210+
manager.deleteProfile(profileId)
211+
212+
val allProfiles = manager.getAllProfiles()
213+
assertFalse(allProfiles.containsKey(profileId))
214+
}
215+
216+
@Test
217+
fun `deleteProfile does not remove other profiles`() {
218+
val manager = ProfileManager.create(context)
219+
val keep = manager.createNewProfile("Keep")
220+
val remove = manager.createNewProfile("Remove")
221+
222+
manager.deleteProfile(remove)
223+
224+
val allProfiles = manager.getAllProfiles()
225+
assertEquals(2, allProfiles.size)
226+
assertTrue(allProfiles.containsKey(ProfileId.DEFAULT))
227+
assertTrue(allProfiles.containsKey(keep))
228+
}
229+
230+
@Test
231+
fun `deleteProfile allows deleting default when not active`() {
232+
val manager = ProfileManager.create(context)
233+
val other = manager.createNewProfile("Other")
234+
235+
prefs.edit(commit = true) {
236+
putString(KEY_LAST_ACTIVE_PROFILE_ID, other.value)
237+
}
238+
239+
val freshManager = ProfileManager.create(context)
240+
freshManager.deleteProfile(ProfileId.DEFAULT)
241+
242+
assertFalse(freshManager.getAllProfiles().containsKey(ProfileId.DEFAULT))
243+
}
244+
245+
@Test(expected = IllegalArgumentException::class)
246+
fun `deleteProfile throws when deleting active profile`() {
247+
val manager = ProfileManager.create(context)
248+
val profileId = manager.createNewProfile("Active")
249+
250+
prefs.edit(commit = true) {
251+
putString(KEY_LAST_ACTIVE_PROFILE_ID, profileId.value)
252+
}
253+
254+
val freshManager = ProfileManager.create(context)
255+
freshManager.deleteProfile(profileId)
256+
}
203257
}

0 commit comments

Comments
 (0)