Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app-common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ dependencies {

implementation(libs.androidx.work.runtime)
implementation(libs.androidx.lifecycle.process)
implementation(libs.kotlinx.collections.immutable)

testImplementation(projects.feature.account.fake)
testImplementation(projects.core.testing)
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
package net.thunderbird.app.common.account

import android.content.res.Resources
import app.k9mail.core.ui.legacy.theme2.common.R
import net.thunderbird.core.android.account.LegacyAccountDtoManager
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.first
import net.thunderbird.feature.account.profile.AccountProfileRepository

internal class AccountColorPicker(
private val accountManager: LegacyAccountDtoManager,
private val resources: Resources,
private val repository: AccountProfileRepository,
private val accountColors: ImmutableList<Int>,
) {
fun pickColor(): Int {
val accounts = accountManager.getAccounts()
val usedAccountColors = accounts.map { it.chipColor }.toSet()
val accountColors = resources.getIntArray(R.array.account_colors).toList()
suspend fun pickColor(): Int {
val profiles = repository.getAll().first()
val usedCounts = profiles.groupingBy { it.color }.eachCount()

val availableColors = accountColors - usedAccountColors
if (availableColors.isEmpty()) {
return accountColors.random()
val minCount = accountColors.minOf { usedCounts[it] ?: 0 }
val candidates = accountColors.filter {
(usedCounts[it] ?: 0) == minCount
}

val defaultAccountColors = resources.getIntArray(R.array.default_account_colors)
return availableColors.shuffled().minByOrNull { color ->
val index = defaultAccountColors.indexOf(color)
if (index != -1) index else defaultAccountColors.size
} ?: error("availableColors must not be empty")
return if (candidates.isNotEmpty()) {
candidates.shuffled().first()
} else {
accountColors.shuffled().first()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package net.thunderbird.app.common.account

import app.k9mail.feature.account.setup.AccountSetupExternalContract
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import net.thunderbird.app.common.account.data.DefaultAccountProfileLocalDataSource
import net.thunderbird.app.common.account.data.DefaultLegacyAccountManager
import net.thunderbird.core.android.account.AccountDefaultsProvider
Expand All @@ -13,8 +15,11 @@ import net.thunderbird.feature.account.core.featureAccountCoreModule
import net.thunderbird.feature.account.storage.legacy.featureAccountStorageLegacyModule
import net.thunderbird.feature.mail.account.api.AccountManager
import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.binds
import org.koin.dsl.module
import app.k9mail.core.ui.legacy.theme2.common.R as ThemeCommonR

internal val appCommonAccountModule = module {
includes(
Expand Down Expand Up @@ -43,10 +48,16 @@ internal val appCommonAccountModule = module {
)
}

factory<ImmutableList<Int>>(named("AccountColors")) {
androidContext().resources.getIntArray(
ThemeCommonR.array.account_colors,
).toList().toImmutableList()
}

factory {
AccountColorPicker(
accountManager = get(),
resources = get(),
repository = get(),
accountColors = get(named("AccountColors")),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ internal class DefaultAccountProfileLocalDataSource(
private val dataMapper: AccountProfileDataMapper,
) : AccountProfileLocalDataSource {

override fun getAll(): Flow<List<AccountProfile>> {
return accountManager.getAll()
.map { accounts ->
accounts.map { dto ->
dataMapper.toDomain(dto.profile)
}
}
}

override fun getById(accountId: AccountId): Flow<AccountProfile?> {
return accountManager.getById(accountId)
.map { account ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package net.thunderbird.app.common.account

import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isOneOf
import kotlin.test.Test
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.thunderbird.app.common.account.data.FakeAccountProfileRepository
import net.thunderbird.feature.account.AccountIdFactory
import net.thunderbird.feature.account.profile.AccountAvatar
import net.thunderbird.feature.account.profile.AccountProfile

class AccountColorPickerTest {

@Test
fun `should pick random color when none used`() = runTest {
// Arrange
val profiles: MutableStateFlow<List<AccountProfile>> = MutableStateFlow(emptyList())
val testSubject = AccountColorPicker(
repository = FakeAccountProfileRepository(profiles),
accountColors = ACCOUNT_COLORS,
)

// Act
val result = testSubject.pickColor()

// Assert
assertThat(result).isOneOf(COLOR_RED, COLOR_GREEN, COLOR_BLUE)
}

@Test
fun `should pick one of the available colors when some are used`() = runTest {
// Arrange
val profiles: MutableStateFlow<List<AccountProfile>> = MutableStateFlow(
listOf(
ACCOUNT_PROFILE_GREEN_1,
),
)
val testSubject = AccountColorPicker(
repository = FakeAccountProfileRepository(profiles),
accountColors = ACCOUNT_COLORS,
)

// Act
val result = testSubject.pickColor()

// Assert
assertThat(result).isOneOf(COLOR_RED, COLOR_BLUE)
}

@Test
fun `should pick last available color when others are used`() = runTest {
// Arrange
val profiles: MutableStateFlow<List<AccountProfile>> = MutableStateFlow(
listOf(
ACCOUNT_PROFILE_RED_1,
ACCOUNT_PROFILE_GREEN_1,
),
)
val testSubject = AccountColorPicker(
repository = FakeAccountProfileRepository(profiles),
accountColors = ACCOUNT_COLORS,
)

// Act
val result = testSubject.pickColor()

// Assert
assertThat(result).isEqualTo(COLOR_BLUE)
}

@Test
fun `should pick random color when all colors are used equally`() = runTest {
// Arrange
val profiles: MutableStateFlow<List<AccountProfile>> = MutableStateFlow(
listOf(
ACCOUNT_PROFILE_RED_1,
ACCOUNT_PROFILE_GREEN_1,
ACCOUNT_PROFILE_BLUE_1,
),
)
val testSubject = AccountColorPicker(
repository = FakeAccountProfileRepository(profiles),
accountColors = ACCOUNT_COLORS,
)

// Act
val result = testSubject.pickColor()

// Assert
assertThat(result).isOneOf(COLOR_RED, COLOR_GREEN, COLOR_BLUE)
}

@Test
fun `should pick from least used colors when colors are used multiple times`() = runTest {
// Arrange
val profiles: MutableStateFlow<List<AccountProfile>> = MutableStateFlow(
listOf(
ACCOUNT_PROFILE_RED_1,
ACCOUNT_PROFILE_RED_2,
ACCOUNT_PROFILE_GREEN_1,
ACCOUNT_PROFILE_GREEN_2,
ACCOUNT_PROFILE_BLUE_1,
),
)
val testSubject = AccountColorPicker(
repository = FakeAccountProfileRepository(profiles),
accountColors = ACCOUNT_COLORS,
)

// Act
val result = testSubject.pickColor()

// Assert
assertThat(result).isEqualTo(COLOR_BLUE)
}

private companion object {
const val COLOR_RED = 0xFF0000
const val COLOR_GREEN = 0x00FF00
const val COLOR_BLUE = 0x0000FF

val ACCOUNT_COLORS = persistentListOf(
COLOR_RED,
COLOR_GREEN,
COLOR_BLUE,
)

val ACCOUNT_PROFILE_RED_1 = AccountProfile(
id = AccountIdFactory.create(),
name = "Account Red 1",
color = COLOR_RED,
avatar = AccountAvatar.Icon(name = "icon1"),
)
val ACCOUNT_PROFILE_RED_2 = AccountProfile(
id = AccountIdFactory.create(),
name = "Account Red 2",
color = COLOR_RED,
avatar = AccountAvatar.Icon(name = "icon4"),
)

val ACCOUNT_PROFILE_GREEN_1 = AccountProfile(
id = AccountIdFactory.create(),
name = "Account Green 1",
color = COLOR_GREEN,
avatar = AccountAvatar.Icon(name = "icon2"),
)

val ACCOUNT_PROFILE_GREEN_2 = AccountProfile(
id = AccountIdFactory.create(),
name = "Account Green 2",
color = COLOR_GREEN,
avatar = AccountAvatar.Icon(name = "icon5"),
)

val ACCOUNT_PROFILE_BLUE_1 = AccountProfile(
id = AccountIdFactory.create(),
name = "Account Blue 1",
color = COLOR_BLUE,
avatar = AccountAvatar.Icon(name = "icon3"),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,45 +24,77 @@ import org.junit.Test

class DefaultAccountProfileLocalDataSourceTest {

@Test
fun `getAll should return all account profiles`() = runTest {
// Arrange
val accountId1 = AccountIdFactory.create()
val legacyAccount1 = createLegacyAccount(accountId1)
val accountProfile1 = createAccountProfile(accountId1)

val accountId2 = AccountIdFactory.create()
val legacyAccount2 = createLegacyAccount(accountId2)
val accountProfile2 = createAccountProfile(accountId2)

val testSubject = createTestSubject(listOf(legacyAccount1, legacyAccount2))

// Act / Assert
testSubject.getAll().test {
val profiles = awaitItem()
assertThat(profiles).isEqualTo(listOf(accountProfile1, accountProfile2))
}
}

@Test
fun `getAll should return empty list when no accounts found`() = runTest {
// Arrange
val testSubject = createTestSubject(emptyList())

// Act / Assert
testSubject.getAll().test {
val result = awaitItem()
assertThat(result).isEqualTo(emptyList())
}
}

@Test
fun `getById should return account profile`() = runTest {
// arrange
// Arrange
val accountId = AccountIdFactory.create()
val legacyAccount = createLegacyAccount(accountId)
val accountProfile = createAccountProfile(accountId)
val testSubject = createTestSubject(legacyAccount)
val testSubject = createTestSubject(listOf(legacyAccount))

// act & assert
// Act / Assert
testSubject.getById(accountId).test {
assertThat(awaitItem()).isEqualTo(accountProfile)
}
}

@Test
fun `getById should return null when account is not found`() = runTest {
// arrange
// Arrange
val accountId = AccountIdFactory.create()
val testSubject = createTestSubject(null)
val testSubject = createTestSubject(emptyList())

// act & assert
// Act / Assert
testSubject.getById(accountId).test {
assertThat(awaitItem()).isEqualTo(null)
}
}

@Test
fun `update should save account profile`() = runTest {
// arrange
// Arrange
val accountId = AccountIdFactory.create()
val legacyAccount = createLegacyAccount(accountId)
val accountProfile = createAccountProfile(accountId)

val updatedName = "updatedName"
val updatedAccountProfile = accountProfile.copy(name = updatedName)

val testSubject = createTestSubject(legacyAccount)
val testSubject = createTestSubject(listOf(legacyAccount))

// act & assert
// Act / Assert
testSubject.getById(accountId).test {
assertThat(awaitItem()).isEqualTo(accountProfile)

Expand Down Expand Up @@ -139,15 +171,11 @@ class DefaultAccountProfileLocalDataSourceTest {
}

private fun createTestSubject(
legacyAccount: LegacyAccount?,
accounts: List<LegacyAccount>,
): DefaultAccountProfileLocalDataSource {
return DefaultAccountProfileLocalDataSource(
accountManager = FakeLegacyAccountManager(
initialAccounts = if (legacyAccount != null) {
listOf(legacyAccount)
} else {
emptyList()
},
initialAccounts = accounts,
),
dataMapper = DefaultAccountProfileDataMapper(
avatarMapper = DefaultAccountAvatarDataMapper(),
Expand Down
Loading
Loading