Skip to content

Commit 7ff7ab3

Browse files
rafaeltonholowmontwe
authored andcommitted
Merge pull request #9562 from wmontwe/fix-account-avatar-type-migration
Fix database migration missing avatar type
1 parent 5a296ce commit 7ff7ab3

File tree

4 files changed

+253
-1
lines changed

4 files changed

+253
-1
lines changed

legacy/storage/src/main/java/com/fsck/k9/preferences/K9StoragePersister.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import net.thunderbird.core.preference.storage.StorageUpdater;
2424

2525
public class K9StoragePersister implements StoragePersister {
26-
private static final int DB_VERSION = 27;
26+
private static final int DB_VERSION = 28;
2727
private static final String DB_NAME = "preferences_storage";
2828

2929
private final Context context;
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.fsck.k9.preferences.migration
2+
3+
import android.database.sqlite.SQLiteDatabase
4+
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
5+
6+
/**
7+
* Migration to ensure all accounts have an avatar type set.
8+
* This fixes an issue where migration 27 might not have set the avatar type correctly.
9+
*/
10+
@Suppress("TooManyFunctions")
11+
class StorageMigrationTo28(
12+
private val db: SQLiteDatabase,
13+
private val migrationsHelper: StorageMigrationHelper,
14+
) {
15+
fun ensureAvatarSet() {
16+
val accountUuidsValue = migrationsHelper.readValue(db, "accountUuids")
17+
if (accountUuidsValue.isNullOrEmpty()) {
18+
return
19+
}
20+
21+
val accountUuids = accountUuidsValue.split(",")
22+
for (accountUuid in accountUuids) {
23+
ensureAvatarTypeForAccount(accountUuid)
24+
}
25+
}
26+
27+
private fun ensureAvatarTypeForAccount(accountUuid: String) {
28+
var avatarType = readAvatarType(accountUuid)
29+
val avatarMonogram = readAvatarMonogram(accountUuid)
30+
31+
if (avatarType.isEmpty()) {
32+
avatarType = AvatarTypeDto.MONOGRAM.name
33+
insertAvatarType(accountUuid, avatarType)
34+
}
35+
36+
if (avatarType == AvatarTypeDto.MONOGRAM.name && avatarMonogram.isEmpty()) {
37+
val monogram = generateAvatarMonogram(accountUuid)
38+
insertAvatarMonogram(accountUuid, monogram)
39+
}
40+
}
41+
42+
private fun generateAvatarMonogram(accountUuid: String): String {
43+
val name = readName(accountUuid)
44+
val email = readEmail(accountUuid)
45+
return getAvatarMonogram(name, email)
46+
}
47+
48+
private fun getAvatarMonogram(name: String?, email: String?): String {
49+
return if (name != null && name.isNotEmpty()) {
50+
composeAvatarMonogram(name)
51+
} else if (email != null && email.isNotEmpty()) {
52+
composeAvatarMonogram(email)
53+
} else {
54+
AVATAR_MONOGRAM_DEFAULT
55+
}
56+
}
57+
58+
private fun composeAvatarMonogram(name: String): String {
59+
return name.replace(" ", "").take(2).uppercase()
60+
}
61+
62+
private fun readAvatarType(accountUuid: String): String {
63+
return migrationsHelper.readValue(db, "$accountUuid.$AVATAR_TYPE_KEY") ?: ""
64+
}
65+
66+
private fun readAvatarMonogram(accountUuid: String): String {
67+
return migrationsHelper.readValue(db, "$accountUuid.$AVATAR_MONOGRAM_KEY") ?: ""
68+
}
69+
70+
private fun readName(accountUuid: String): String {
71+
return migrationsHelper.readValue(db, "$accountUuid.$NAME_KEY") ?: ""
72+
}
73+
74+
private fun readEmail(accountUuid: String): String {
75+
return migrationsHelper.readValue(db, "$accountUuid.$EMAIL_KEY") ?: ""
76+
}
77+
78+
private fun insertAvatarType(accountUuid: String, avatarType: String) {
79+
migrationsHelper.insertValue(db, "$accountUuid.$AVATAR_TYPE_KEY", avatarType)
80+
}
81+
82+
private fun insertAvatarMonogram(accountUuid: String, monogram: String) {
83+
migrationsHelper.insertValue(db, "$accountUuid.$AVATAR_MONOGRAM_KEY", monogram)
84+
}
85+
86+
private companion object {
87+
const val NAME_KEY = "name.0"
88+
const val EMAIL_KEY = "email.0"
89+
const val AVATAR_TYPE_KEY = "avatarType"
90+
const val AVATAR_MONOGRAM_KEY = "avatarMonogram"
91+
92+
private const val AVATAR_MONOGRAM_DEFAULT = "XX"
93+
}
94+
}

legacy/storage/src/main/java/com/fsck/k9/preferences/migration/StorageMigrations.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ internal object StorageMigrations {
3434
if (oldVersion < 25) StorageMigrationTo25(db, migrationsHelper).convertToAuthTypeNone()
3535
if (oldVersion < 26) StorageMigrationTo26(db, migrationsHelper).fixIdentities()
3636
if (oldVersion < 27) StorageMigrationTo27(db, migrationsHelper).addAvatarMonogram()
37+
if (oldVersion < 28) StorageMigrationTo28(db, migrationsHelper).ensureAvatarSet()
3738
}
3839
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.fsck.k9.preferences.migration
2+
3+
import assertk.assertThat
4+
import assertk.assertions.doesNotContainKey
5+
import assertk.assertions.isEqualTo
6+
import assertk.assertions.key
7+
import com.fsck.k9.preferences.createPreferencesDatabase
8+
import java.util.UUID
9+
import kotlin.test.Test
10+
import net.thunderbird.core.logging.legacy.Log
11+
import net.thunderbird.core.logging.testing.TestLogger
12+
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
13+
import org.junit.After
14+
import org.junit.Before
15+
import org.junit.runner.RunWith
16+
import org.robolectric.RobolectricTestRunner
17+
18+
@RunWith(RobolectricTestRunner::class)
19+
class StorageMigrationTo28Test {
20+
private val database = createPreferencesDatabase()
21+
private val migrationHelper = DefaultStorageMigrationHelper()
22+
private val migration = StorageMigrationTo28(database, migrationHelper)
23+
24+
@Before
25+
fun setUp() {
26+
Log.logger = TestLogger()
27+
}
28+
29+
@After
30+
fun tearDown() {
31+
database.close()
32+
}
33+
34+
@Test
35+
fun `avatar type should be set for accounts with no avatar type`() {
36+
val accountUuid = createAccount(
37+
"avatarType" to null,
38+
"avatarMonogram" to "AB",
39+
)
40+
writeAccountUuids(accountUuid)
41+
42+
migration.ensureAvatarSet()
43+
44+
assertAvatarType(accountUuid, AvatarTypeDto.MONOGRAM)
45+
assertAvatarMonogram(accountUuid, "AB")
46+
}
47+
48+
@Test
49+
fun `avatar type should not be set for accounts with existing avatar type`() {
50+
val accountUuid = createAccount(
51+
"avatarType" to AvatarTypeDto.IMAGE.name,
52+
)
53+
writeAccountUuids(accountUuid)
54+
55+
migration.ensureAvatarSet()
56+
57+
assertAvatarType(accountUuid, AvatarTypeDto.IMAGE)
58+
assertThat(migrationHelper.readAllValues(database))
59+
.doesNotContainKey("$accountUuid.avatarMonogram")
60+
}
61+
62+
@Test
63+
fun `avatar monogram should be set for accounts with MONOGRAM avatar type and no monogram`() {
64+
val accountUuid = createAccount(
65+
"avatarType" to AvatarTypeDto.MONOGRAM.name,
66+
"avatarMonogram" to null,
67+
"name.0" to "John Doe",
68+
"email.0" to "test@example.com",
69+
)
70+
writeAccountUuids(accountUuid)
71+
72+
migration.ensureAvatarSet()
73+
74+
assertAvatarType(accountUuid, AvatarTypeDto.MONOGRAM)
75+
assertAvatarMonogram(accountUuid, "JO")
76+
}
77+
78+
@Test
79+
fun `avatar monogram should be added for accounts with no monogram and name but email`() {
80+
val accountUuid = createAccount(
81+
"avatarType" to AvatarTypeDto.MONOGRAM.name,
82+
"avatarMonogram" to null,
83+
"name.0" to null,
84+
"email.0" to "test@example.com",
85+
)
86+
writeAccountUuids(accountUuid)
87+
88+
migration.ensureAvatarSet()
89+
90+
assertAvatarType(accountUuid, AvatarTypeDto.MONOGRAM)
91+
assertAvatarMonogram(accountUuid, "TE")
92+
}
93+
94+
@Test
95+
fun `avatar monogram should be added for accounts with MONOGRAM avatar type and no monogram, name and email`() {
96+
val accountUuid = createAccount(
97+
"avatarType" to AvatarTypeDto.MONOGRAM.name,
98+
"avatarMonogram" to null,
99+
"name.0" to null,
100+
"email.0" to null,
101+
)
102+
writeAccountUuids(accountUuid)
103+
104+
migration.ensureAvatarSet()
105+
106+
assertAvatarType(accountUuid, AvatarTypeDto.MONOGRAM)
107+
assertAvatarMonogram(accountUuid, "XX")
108+
}
109+
110+
@Test
111+
fun `avatar type and monogram should be set for accounts with missing avatar type and monogram`() {
112+
val accountUuid = createAccount(
113+
"avatarType" to null,
114+
"avatarMonogram" to null,
115+
"name.0" to "John Doe",
116+
"email.0" to "test@example.com",
117+
)
118+
119+
writeAccountUuids(accountUuid)
120+
121+
migration.ensureAvatarSet()
122+
123+
assertAvatarType(accountUuid, AvatarTypeDto.MONOGRAM)
124+
assertAvatarMonogram(accountUuid, "JO")
125+
}
126+
127+
private fun assertAvatarType(
128+
accountUuid: String,
129+
expectedAvatarType: AvatarTypeDto,
130+
) {
131+
assertThat(migrationHelper.readAllValues(database))
132+
.key("$accountUuid.avatarType").isEqualTo(expectedAvatarType.name)
133+
}
134+
135+
private fun assertAvatarMonogram(
136+
accountUuid: String,
137+
expectedMonogram: String,
138+
) {
139+
assertThat(migrationHelper.readAllValues(database))
140+
.key("$accountUuid.avatarMonogram").isEqualTo(expectedMonogram)
141+
}
142+
143+
private fun writeAccountUuids(vararg accounts: String) {
144+
val accountUuids = accounts.joinToString(separator = ",")
145+
migrationHelper.insertValue(database, "accountUuids", accountUuids)
146+
}
147+
148+
private fun createAccount(vararg pairs: Pair<String, String?>): String {
149+
val accountUuid = UUID.randomUUID().toString()
150+
151+
for ((key, value) in pairs) {
152+
migrationHelper.insertValue(database, "$accountUuid.$key", value)
153+
}
154+
155+
return accountUuid
156+
}
157+
}

0 commit comments

Comments
 (0)