Repo created
This commit is contained in:
parent
75dc487a7a
commit
39c29d175b
6317 changed files with 388324 additions and 2 deletions
7
app-common/README.md
Normal file
7
app-common/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# App Common
|
||||
|
||||
# App Common
|
||||
|
||||
This is the central integration point for shared code among the K-9 Mail and Thunderbird for Android applications. Its purpose is to collect and organize the individual feature modules that contain the actual functionality, as well as the "glue code" and configurations that tie them together.
|
||||
|
||||
By keeping the shared code focused on these boundaries, we can ensure that it remains lean and avoids unnecessary dependencies. This approach allows us to maintain a clean and modular architecture, making it easier to maintain and update the codebase.
|
||||
48
app-common/build.gradle.kts
Normal file
48
app-common/build.gradle.kts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.thunderbird.app.common"
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.legacy.common)
|
||||
api(projects.legacy.ui.legacy)
|
||||
|
||||
api(projects.feature.account.core)
|
||||
api(projects.feature.launcher)
|
||||
api(projects.feature.navigation.drawer.api)
|
||||
|
||||
implementation(projects.legacy.core)
|
||||
implementation(projects.core.android.account)
|
||||
|
||||
implementation(projects.core.logging.api)
|
||||
implementation(projects.core.logging.implComposite)
|
||||
implementation(projects.core.logging.implConsole)
|
||||
implementation(projects.core.logging.implLegacy)
|
||||
implementation(projects.core.logging.implFile)
|
||||
|
||||
implementation(projects.core.featureflag)
|
||||
implementation(projects.core.ui.legacy.theme2.common)
|
||||
|
||||
implementation(projects.feature.account.avatar.api)
|
||||
implementation(projects.feature.account.avatar.impl)
|
||||
implementation(projects.feature.account.setup)
|
||||
implementation(projects.feature.mail.account.api)
|
||||
implementation(projects.feature.migration.provider)
|
||||
implementation(projects.feature.notification.api)
|
||||
implementation(projects.feature.notification.impl)
|
||||
implementation(projects.feature.widget.messageList)
|
||||
|
||||
implementation(projects.mail.protocols.imap)
|
||||
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
|
||||
testImplementation(projects.feature.account.fake)
|
||||
}
|
||||
22
app-common/src/main/AndroidManifest.xml
Normal file
22
app-common/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:installLocation="auto"
|
||||
>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:hasFragileUserData="false"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="true"
|
||||
/>
|
||||
|
||||
<queries>
|
||||
<!-- Allow access to external text processing actions so they can be displayed in the text selection toolbar -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package net.thunderbird.app.common
|
||||
|
||||
import com.fsck.k9.legacyCommonAppModules
|
||||
import com.fsck.k9.legacyCoreModules
|
||||
import com.fsck.k9.legacyUiModules
|
||||
import net.thunderbird.app.common.account.appCommonAccountModule
|
||||
import net.thunderbird.app.common.core.appCommonCoreModule
|
||||
import net.thunderbird.app.common.feature.appCommonFeatureModule
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
|
||||
val appCommonModule: Module = module {
|
||||
includes(legacyCommonAppModules)
|
||||
includes(legacyCoreModules)
|
||||
includes(legacyUiModules)
|
||||
|
||||
includes(
|
||||
appCommonAccountModule,
|
||||
appCommonCoreModule,
|
||||
appCommonFeatureModule,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
package net.thunderbird.app.common
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import app.k9mail.feature.widget.message.list.MessageListWidgetManager
|
||||
import app.k9mail.legacy.di.DI
|
||||
import com.fsck.k9.Core
|
||||
import com.fsck.k9.K9
|
||||
import com.fsck.k9.MessagingListenerProvider
|
||||
import com.fsck.k9.controller.MessagingController
|
||||
import com.fsck.k9.job.WorkManagerConfigurationProvider
|
||||
import com.fsck.k9.notification.NotificationChannelManager
|
||||
import com.fsck.k9.ui.base.AppLanguageManager
|
||||
import com.fsck.k9.ui.base.extensions.currentLocale
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import net.thunderbird.app.common.feature.LoggerLifecycleObserver
|
||||
import net.thunderbird.core.common.exception.ExceptionHandler
|
||||
import net.thunderbird.core.logging.Logger
|
||||
import net.thunderbird.core.logging.file.FileLogSink
|
||||
import net.thunderbird.core.logging.legacy.Log
|
||||
import net.thunderbird.core.ui.theme.manager.ThemeManager
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.qualifier.named
|
||||
import androidx.work.Configuration as WorkManagerConfiguration
|
||||
|
||||
abstract class BaseApplication : Application(), WorkManagerConfiguration.Provider {
|
||||
|
||||
private val messagingController: MessagingController by inject()
|
||||
private val messagingListenerProvider: MessagingListenerProvider by inject()
|
||||
private val themeManager: ThemeManager by inject()
|
||||
private val appLanguageManager: AppLanguageManager by inject()
|
||||
private val notificationChannelManager: NotificationChannelManager by inject()
|
||||
private val messageListWidgetManager: MessageListWidgetManager by inject()
|
||||
private val workManagerConfigurationProvider: WorkManagerConfigurationProvider by inject()
|
||||
private val logger: Logger by inject()
|
||||
private val syncDebugFileLogSink: FileLogSink by inject(named("syncDebug"))
|
||||
|
||||
private val appCoroutineScope: CoroutineScope = MainScope()
|
||||
private var appLanguageManagerInitialized = false
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
Core.earlyInit()
|
||||
|
||||
// Start Koin early so it is ready by the time content providers are initialized.
|
||||
DI.start(this, listOf(provideAppModule()))
|
||||
Log.logger = logger
|
||||
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
K9.init(this)
|
||||
Core.init(this)
|
||||
initializeAppLanguage()
|
||||
updateNotificationChannelsOnAppLanguageChanges()
|
||||
themeManager.init()
|
||||
messageListWidgetManager.init()
|
||||
|
||||
messagingListenerProvider.listeners.forEach { listener ->
|
||||
messagingController.addListener(listener)
|
||||
}
|
||||
val originalHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(originalHandler))
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(LoggerLifecycleObserver(syncDebugFileLogSink))
|
||||
}
|
||||
|
||||
abstract fun provideAppModule(): Module
|
||||
|
||||
private fun initializeAppLanguage() {
|
||||
appLanguageManager.init()
|
||||
applyOverrideLocaleToConfiguration()
|
||||
appLanguageManagerInitialized = true
|
||||
listenForAppLanguageChanges()
|
||||
}
|
||||
|
||||
private fun applyOverrideLocaleToConfiguration() {
|
||||
appLanguageManager.getOverrideLocale()?.let { overrideLocale ->
|
||||
updateConfigurationWithLocale(superResources.configuration, overrideLocale)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenForAppLanguageChanges() {
|
||||
appLanguageManager.overrideLocale
|
||||
.drop(1) // We already applied the initial value
|
||||
.onEach { overrideLocale ->
|
||||
val locale = overrideLocale ?: Locale.getDefault()
|
||||
updateConfigurationWithLocale(superResources.configuration, locale)
|
||||
}
|
||||
.launchIn(appCoroutineScope)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfiguration: Configuration) {
|
||||
applyOverrideLocaleToConfiguration()
|
||||
super.onConfigurationChanged(superResources.configuration)
|
||||
}
|
||||
|
||||
private fun updateConfigurationWithLocale(configuration: Configuration, locale: Locale) {
|
||||
Log.d("Updating application configuration with locale '$locale'")
|
||||
|
||||
val newConfiguration = Configuration(configuration).apply {
|
||||
currentLocale = locale
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
superResources.updateConfiguration(newConfiguration, superResources.displayMetrics)
|
||||
}
|
||||
|
||||
private val superResources: Resources
|
||||
get() = super.getResources()
|
||||
|
||||
// Creating a WebView instance triggers something that will cause the configuration of the Application's Resources
|
||||
// instance to be reset to the default, i.e. not containing our locale override. Unfortunately, we're not notified
|
||||
// about this event. So we're checking each time someone asks for the Resources instance whether we need to change
|
||||
// the configuration again. Luckily, right now (Android 11), the platform is calling this method right after
|
||||
// resetting the configuration.
|
||||
override fun getResources(): Resources {
|
||||
val resources = super.getResources()
|
||||
|
||||
if (appLanguageManagerInitialized) {
|
||||
appLanguageManager.getOverrideLocale()?.let { overrideLocale ->
|
||||
if (resources.configuration.currentLocale != overrideLocale) {
|
||||
Log.w("Resources configuration was reset. Re-applying locale override.")
|
||||
appLanguageManager.applyOverrideLocale()
|
||||
applyOverrideLocaleToConfiguration()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
private fun updateNotificationChannelsOnAppLanguageChanges() {
|
||||
appLanguageManager.appLocale
|
||||
.distinctUntilChanged()
|
||||
.onEach { notificationChannelManager.updateChannels() }
|
||||
.launchIn(appCoroutineScope)
|
||||
}
|
||||
|
||||
override val workManagerConfiguration: WorkManagerConfiguration
|
||||
get() = workManagerConfigurationProvider.getConfiguration()
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
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.AccountManager
|
||||
|
||||
internal class AccountColorPicker(
|
||||
private val accountManager: AccountManager,
|
||||
private val resources: Resources,
|
||||
) {
|
||||
fun pickColor(): Int {
|
||||
val accounts = accountManager.getAccounts()
|
||||
val usedAccountColors = accounts.map { it.chipColor }.toSet()
|
||||
val accountColors = resources.getIntArray(R.array.account_colors).toList()
|
||||
|
||||
val availableColors = accountColors - usedAccountColors
|
||||
if (availableColors.isEmpty()) {
|
||||
return accountColors.random()
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
package net.thunderbird.app.common.account
|
||||
|
||||
import android.content.Context
|
||||
import app.k9mail.feature.account.common.domain.entity.Account
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderOption
|
||||
import app.k9mail.feature.account.common.domain.entity.SpecialFolderSettings
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult
|
||||
import com.fsck.k9.Core
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.account.DeletePolicyProvider
|
||||
import com.fsck.k9.controller.MessagingController
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreSettings.autoDetectNamespace
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreSettings.createExtra
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreSettings.isSendClientInfo
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreSettings.isUseCompression
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreSettings.pathPrefix
|
||||
import com.fsck.k9.mailstore.SpecialLocalFoldersCreator
|
||||
import com.fsck.k9.preferences.UnifiedInboxConfigurator
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
import net.thunderbird.core.common.mail.Protocols
|
||||
import net.thunderbird.core.logging.legacy.Log
|
||||
import net.thunderbird.feature.account.avatar.AvatarMonogramCreator
|
||||
import net.thunderbird.feature.account.storage.profile.AvatarDto
|
||||
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
|
||||
import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
|
||||
|
||||
// TODO Move to feature/account/setup
|
||||
@Suppress("LongParameterList")
|
||||
internal class AccountCreator(
|
||||
private val accountColorPicker: AccountColorPicker,
|
||||
private val localFoldersCreator: SpecialLocalFoldersCreator,
|
||||
private val preferences: Preferences,
|
||||
private val context: Context,
|
||||
private val messagingController: MessagingController,
|
||||
private val deletePolicyProvider: DeletePolicyProvider,
|
||||
private val avatarMonogramCreator: AvatarMonogramCreator,
|
||||
private val unifiedInboxConfigurator: UnifiedInboxConfigurator,
|
||||
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) : AccountSetupExternalContract.AccountCreator {
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override suspend fun createAccount(account: Account): AccountCreatorResult {
|
||||
return try {
|
||||
withContext(coroutineDispatcher) { AccountCreatorResult.Success(create(account)) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(e, "Error while creating new account")
|
||||
|
||||
AccountCreatorResult.Error(e.message ?: "Unknown create account error")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun create(account: Account): String {
|
||||
val newAccount = preferences.newAccount(account.uuid)
|
||||
|
||||
newAccount.email = account.emailAddress
|
||||
|
||||
newAccount.avatar = AvatarDto(
|
||||
avatarType = AvatarTypeDto.MONOGRAM,
|
||||
avatarMonogram = avatarMonogramCreator.create(account.options.accountName, account.emailAddress),
|
||||
avatarImageUri = null,
|
||||
avatarIconName = null,
|
||||
)
|
||||
|
||||
newAccount.setIncomingServerSettings(account.incomingServerSettings)
|
||||
newAccount.outgoingServerSettings = account.outgoingServerSettings
|
||||
|
||||
newAccount.oAuthState = account.authorizationState
|
||||
|
||||
newAccount.name = account.options.accountName
|
||||
newAccount.senderName = account.options.displayName
|
||||
if (account.options.emailSignature != null) {
|
||||
newAccount.signatureUse = true
|
||||
newAccount.signature = account.options.emailSignature
|
||||
}
|
||||
newAccount.isNotifyNewMail = account.options.showNotification
|
||||
newAccount.automaticCheckIntervalMinutes = account.options.checkFrequencyInMinutes
|
||||
newAccount.displayCount = account.options.messageDisplayCount
|
||||
|
||||
newAccount.deletePolicy = deletePolicyProvider.getDeletePolicy(newAccount.incomingServerSettings.type)
|
||||
newAccount.chipColor = accountColorPicker.pickColor()
|
||||
|
||||
localFoldersCreator.createSpecialLocalFolders(newAccount)
|
||||
|
||||
account.specialFolderSettings?.let { specialFolderSettings ->
|
||||
newAccount.setSpecialFolders(specialFolderSettings)
|
||||
}
|
||||
|
||||
newAccount.markSetupFinished()
|
||||
|
||||
preferences.saveAccount(newAccount)
|
||||
|
||||
unifiedInboxConfigurator.configureUnifiedInbox()
|
||||
|
||||
Core.setServicesEnabled(context)
|
||||
|
||||
messagingController.refreshFolderListBlocking(newAccount)
|
||||
|
||||
if (account.options.checkFrequencyInMinutes == -1) {
|
||||
messagingController.checkMail(newAccount, false, true, false, null)
|
||||
}
|
||||
|
||||
return newAccount.uuid
|
||||
}
|
||||
|
||||
/**
|
||||
* Set special folders by name.
|
||||
*
|
||||
* Since the folder list hasn't been synced yet, we don't have database IDs for the folders. So we use the same
|
||||
* mechanism that is used when importing settings. See [com.fsck.k9.mailstore.SpecialFolderUpdater] for details.
|
||||
*/
|
||||
private fun LegacyAccount.setSpecialFolders(specialFolders: SpecialFolderSettings) {
|
||||
importedArchiveFolder = specialFolders.archiveSpecialFolderOption.toFolderServerId()
|
||||
archiveFolderSelection = specialFolders.archiveSpecialFolderOption.toFolderSelection()
|
||||
|
||||
importedDraftsFolder = specialFolders.draftsSpecialFolderOption.toFolderServerId()
|
||||
draftsFolderSelection = specialFolders.draftsSpecialFolderOption.toFolderSelection()
|
||||
|
||||
importedSentFolder = specialFolders.sentSpecialFolderOption.toFolderServerId()
|
||||
sentFolderSelection = specialFolders.sentSpecialFolderOption.toFolderSelection()
|
||||
|
||||
importedSpamFolder = specialFolders.spamSpecialFolderOption.toFolderServerId()
|
||||
spamFolderSelection = specialFolders.spamSpecialFolderOption.toFolderSelection()
|
||||
|
||||
importedTrashFolder = specialFolders.trashSpecialFolderOption.toFolderServerId()
|
||||
trashFolderSelection = specialFolders.trashSpecialFolderOption.toFolderSelection()
|
||||
}
|
||||
|
||||
private fun SpecialFolderOption.toFolderServerId(): String? {
|
||||
return when (this) {
|
||||
is SpecialFolderOption.None -> null
|
||||
is SpecialFolderOption.Regular -> remoteFolder.serverId.serverId
|
||||
is SpecialFolderOption.Special -> remoteFolder.serverId.serverId
|
||||
}
|
||||
}
|
||||
|
||||
private fun SpecialFolderOption.toFolderSelection(): SpecialFolderSelection {
|
||||
return when (this) {
|
||||
is SpecialFolderOption.None -> {
|
||||
if (isAutomatic) SpecialFolderSelection.AUTOMATIC else SpecialFolderSelection.MANUAL
|
||||
}
|
||||
is SpecialFolderOption.Regular -> {
|
||||
SpecialFolderSelection.MANUAL
|
||||
}
|
||||
is SpecialFolderOption.Special -> {
|
||||
if (isAutomatic) SpecialFolderSelection.AUTOMATIC else SpecialFolderSelection.MANUAL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LegacyAccount.setIncomingServerSettings(serverSettings: ServerSettings) {
|
||||
if (serverSettings.type == Protocols.IMAP) {
|
||||
useCompression = serverSettings.isUseCompression
|
||||
isSendClientInfoEnabled = serverSettings.isSendClientInfo
|
||||
incomingServerSettings = serverSettings.copy(
|
||||
extra = createExtra(
|
||||
autoDetectNamespace = serverSettings.autoDetectNamespace,
|
||||
pathPrefix = serverSettings.pathPrefix,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
incomingServerSettings = serverSettings
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package net.thunderbird.app.common.account
|
||||
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract
|
||||
import net.thunderbird.app.common.account.data.DefaultAccountProfileLocalDataSource
|
||||
import net.thunderbird.app.common.account.data.DefaultLegacyAccountWrapperManager
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider
|
||||
import net.thunderbird.core.android.account.LegacyAccountWrapperManager
|
||||
import net.thunderbird.feature.account.avatar.AvatarMonogramCreator
|
||||
import net.thunderbird.feature.account.avatar.DefaultAvatarMonogramCreator
|
||||
import net.thunderbird.feature.account.core.AccountCoreExternalContract.AccountProfileLocalDataSource
|
||||
import net.thunderbird.feature.account.core.featureAccountCoreModule
|
||||
import net.thunderbird.feature.account.storage.legacy.featureAccountStorageLegacyModule
|
||||
import org.koin.android.ext.koin.androidApplication
|
||||
import org.koin.dsl.module
|
||||
|
||||
internal val appCommonAccountModule = module {
|
||||
includes(
|
||||
featureAccountCoreModule,
|
||||
featureAccountStorageLegacyModule,
|
||||
)
|
||||
|
||||
single<LegacyAccountWrapperManager> {
|
||||
DefaultLegacyAccountWrapperManager(
|
||||
accountManager = get(),
|
||||
accountDataMapper = get(),
|
||||
)
|
||||
}
|
||||
|
||||
single<AccountProfileLocalDataSource> {
|
||||
DefaultAccountProfileLocalDataSource(
|
||||
accountManager = get(),
|
||||
dataMapper = get(),
|
||||
)
|
||||
}
|
||||
|
||||
single<AccountDefaultsProvider> {
|
||||
DefaultAccountDefaultsProvider(
|
||||
resourceProvider = get(),
|
||||
featureFlagProvider = get(),
|
||||
)
|
||||
}
|
||||
|
||||
factory {
|
||||
AccountColorPicker(
|
||||
accountManager = get(),
|
||||
resources = get(),
|
||||
)
|
||||
}
|
||||
|
||||
factory<AvatarMonogramCreator> {
|
||||
DefaultAvatarMonogramCreator()
|
||||
}
|
||||
|
||||
factory<AccountSetupExternalContract.AccountCreator> {
|
||||
AccountCreator(
|
||||
accountColorPicker = get(),
|
||||
localFoldersCreator = get(),
|
||||
preferences = get(),
|
||||
context = androidApplication(),
|
||||
deletePolicyProvider = get(),
|
||||
messagingController = get(),
|
||||
avatarMonogramCreator = get(),
|
||||
unifiedInboxConfigurator = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package net.thunderbird.app.common.account
|
||||
|
||||
import com.fsck.k9.CoreResourceProvider
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_FORMAT
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_FORMAT_AUTO
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_READ_RECEIPT
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_QUOTED_TEXT_SHOWN
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_QUOTE_PREFIX
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_QUOTE_STYLE
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_REMOTE_SEARCH_NUM_RESULTS
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_REPLY_AFTER_QUOTE
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_RINGTONE_URI
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_SORT_ASCENDING
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_SORT_TYPE
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_STRIP_SIGNATURE
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_SYNC_INTERVAL
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_VISIBLE_LIMIT
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.NO_OPENPGP_KEY
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.UNASSIGNED_ACCOUNT_NUMBER
|
||||
import net.thunderbird.core.android.account.Expunge
|
||||
import net.thunderbird.core.android.account.FolderMode
|
||||
import net.thunderbird.core.android.account.Identity
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
import net.thunderbird.core.android.account.ShowPictures
|
||||
import net.thunderbird.core.featureflag.FeatureFlagProvider
|
||||
import net.thunderbird.core.featureflag.toFeatureFlagKey
|
||||
import net.thunderbird.core.preference.storage.Storage
|
||||
import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
|
||||
import net.thunderbird.feature.notification.NotificationLight
|
||||
import net.thunderbird.feature.notification.NotificationSettings
|
||||
import net.thunderbird.feature.notification.NotificationVibration
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal class DefaultAccountDefaultsProvider(
|
||||
private val resourceProvider: CoreResourceProvider,
|
||||
private val featureFlagProvider: FeatureFlagProvider,
|
||||
) : AccountDefaultsProvider {
|
||||
|
||||
override fun applyDefaults(account: LegacyAccount) = with(account) {
|
||||
applyLegacyDefaults()
|
||||
}
|
||||
|
||||
override fun applyOverwrites(account: LegacyAccount, storage: Storage) = with(account) {
|
||||
if (storage.contains("${account.uuid}.notifyNewMail")) {
|
||||
isNotifyNewMail = storage.getBoolean("${account.uuid}.notifyNewMail", false)
|
||||
isNotifySelfNewMail = storage.getBoolean("${account.uuid}.notifySelfNewMail", true)
|
||||
} else {
|
||||
isNotifyNewMail = featureFlagProvider.provide(
|
||||
"email_notification_default".toFeatureFlagKey(),
|
||||
).whenEnabledOrNot(
|
||||
onEnabled = { true },
|
||||
onDisabledOrUnavailable = { false },
|
||||
)
|
||||
|
||||
isNotifySelfNewMail = featureFlagProvider.provide(
|
||||
"email_notification_default".toFeatureFlagKey(),
|
||||
).whenEnabledOrNot(
|
||||
onEnabled = { true },
|
||||
onDisabledOrUnavailable = { false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun LegacyAccount.applyLegacyDefaults() {
|
||||
automaticCheckIntervalMinutes = DEFAULT_SYNC_INTERVAL
|
||||
idleRefreshMinutes = 24
|
||||
displayCount = DEFAULT_VISIBLE_LIMIT
|
||||
accountNumber = UNASSIGNED_ACCOUNT_NUMBER
|
||||
isNotifyNewMail = true
|
||||
folderNotifyNewMailMode = FolderMode.ALL
|
||||
isNotifySync = false
|
||||
isNotifySelfNewMail = true
|
||||
isNotifyContactsMailOnly = false
|
||||
isIgnoreChatMessages = false
|
||||
messagesNotificationChannelVersion = 0
|
||||
folderDisplayMode = FolderMode.NOT_SECOND_CLASS
|
||||
folderSyncMode = FolderMode.FIRST_CLASS
|
||||
folderPushMode = FolderMode.NONE
|
||||
sortType = DEFAULT_SORT_TYPE
|
||||
setSortAscending(DEFAULT_SORT_TYPE, DEFAULT_SORT_ASCENDING)
|
||||
showPictures = ShowPictures.NEVER
|
||||
isSignatureBeforeQuotedText = false
|
||||
expungePolicy = Expunge.EXPUNGE_IMMEDIATELY
|
||||
importedAutoExpandFolder = null
|
||||
legacyInboxFolder = null
|
||||
maxPushFolders = 10
|
||||
isSubscribedFoldersOnly = false
|
||||
maximumPolledMessageAge = -1
|
||||
maximumAutoDownloadMessageSize = DEFAULT_MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE
|
||||
messageFormat = DEFAULT_MESSAGE_FORMAT
|
||||
isMessageFormatAuto = DEFAULT_MESSAGE_FORMAT_AUTO
|
||||
isMessageReadReceipt = DEFAULT_MESSAGE_READ_RECEIPT
|
||||
quoteStyle = DEFAULT_QUOTE_STYLE
|
||||
quotePrefix = DEFAULT_QUOTE_PREFIX
|
||||
isDefaultQuotedTextShown = DEFAULT_QUOTED_TEXT_SHOWN
|
||||
isReplyAfterQuote = DEFAULT_REPLY_AFTER_QUOTE
|
||||
isStripSignature = DEFAULT_STRIP_SIGNATURE
|
||||
isSyncRemoteDeletions = true
|
||||
openPgpKey = NO_OPENPGP_KEY
|
||||
isRemoteSearchFullText = false
|
||||
remoteSearchNumResults = DEFAULT_REMOTE_SEARCH_NUM_RESULTS
|
||||
isUploadSentMessages = true
|
||||
isMarkMessageAsReadOnView = true
|
||||
isMarkMessageAsReadOnDelete = true
|
||||
isAlwaysShowCcBcc = false
|
||||
lastSyncTime = 0L
|
||||
lastFolderListRefreshTime = 0L
|
||||
|
||||
setArchiveFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setDraftsFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setSentFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setSpamFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
setTrashFolderId(null, SpecialFolderSelection.AUTOMATIC)
|
||||
|
||||
identities = ArrayList<Identity>()
|
||||
|
||||
val identity = Identity(
|
||||
signatureUse = false,
|
||||
signature = null,
|
||||
description = resourceProvider.defaultIdentityDescription(),
|
||||
)
|
||||
identities.add(identity)
|
||||
|
||||
updateNotificationSettings {
|
||||
NotificationSettings(
|
||||
isRingEnabled = true,
|
||||
ringtone = DEFAULT_RINGTONE_URI,
|
||||
light = NotificationLight.Disabled,
|
||||
vibration = NotificationVibration.DEFAULT,
|
||||
)
|
||||
}
|
||||
|
||||
resetChangeMarkers()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package net.thunderbird.app.common.account.data
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import net.thunderbird.core.android.account.LegacyAccountWrapperManager
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
import net.thunderbird.feature.account.core.AccountCoreExternalContract.AccountProfileLocalDataSource
|
||||
import net.thunderbird.feature.account.profile.AccountProfile
|
||||
import net.thunderbird.feature.account.storage.mapper.AccountProfileDataMapper
|
||||
|
||||
internal class DefaultAccountProfileLocalDataSource(
|
||||
private val accountManager: LegacyAccountWrapperManager,
|
||||
private val dataMapper: AccountProfileDataMapper,
|
||||
) : AccountProfileLocalDataSource {
|
||||
|
||||
override fun getById(accountId: AccountId): Flow<AccountProfile?> {
|
||||
return accountManager.getById(accountId)
|
||||
.map { account ->
|
||||
account?.let { dto ->
|
||||
dataMapper.toDomain(dto.profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(accountProfile: AccountProfile) {
|
||||
val currentAccount = accountManager.getById(accountProfile.id)
|
||||
.firstOrNull() ?: return
|
||||
|
||||
val accountProfile = dataMapper.toDto(accountProfile)
|
||||
|
||||
val updatedAccount = currentAccount.copy(
|
||||
profile = accountProfile,
|
||||
)
|
||||
|
||||
accountManager.update(updatedAccount)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package net.thunderbird.app.common.account.data
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import net.thunderbird.core.android.account.AccountManager
|
||||
import net.thunderbird.core.android.account.LegacyAccountWrapper
|
||||
import net.thunderbird.core.android.account.LegacyAccountWrapperManager
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
import net.thunderbird.feature.account.storage.legacy.mapper.DefaultLegacyAccountWrapperDataMapper
|
||||
|
||||
internal class DefaultLegacyAccountWrapperManager(
|
||||
private val accountManager: AccountManager,
|
||||
private val accountDataMapper: DefaultLegacyAccountWrapperDataMapper,
|
||||
) : LegacyAccountWrapperManager {
|
||||
|
||||
override fun getAll(): Flow<List<LegacyAccountWrapper>> {
|
||||
return accountManager.getAccountsFlow()
|
||||
.map { list ->
|
||||
list.map { account ->
|
||||
accountDataMapper.toDomain(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getById(id: AccountId): Flow<LegacyAccountWrapper?> {
|
||||
return accountManager.getAccountFlow(id.asRaw()).map { account ->
|
||||
account?.let {
|
||||
accountDataMapper.toDomain(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(account: LegacyAccountWrapper) {
|
||||
accountManager.saveAccount(
|
||||
accountDataMapper.toDto(account),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package net.thunderbird.app.common.core
|
||||
|
||||
import android.content.Context
|
||||
import kotlin.time.ExperimentalTime
|
||||
import net.thunderbird.app.common.core.logging.DefaultLogLevelManager
|
||||
import net.thunderbird.core.common.inject.getList
|
||||
import net.thunderbird.core.common.inject.singleListOf
|
||||
import net.thunderbird.core.logging.DefaultLogger
|
||||
import net.thunderbird.core.logging.LogLevel
|
||||
import net.thunderbird.core.logging.LogLevelManager
|
||||
import net.thunderbird.core.logging.LogLevelProvider
|
||||
import net.thunderbird.core.logging.LogSink
|
||||
import net.thunderbird.core.logging.Logger
|
||||
import net.thunderbird.core.logging.composite.CompositeLogSink
|
||||
import net.thunderbird.core.logging.console.ConsoleLogSink
|
||||
import net.thunderbird.core.logging.file.AndroidFileSystemManager
|
||||
import net.thunderbird.core.logging.file.FileLogSink
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val appCommonCoreModule: Module = module {
|
||||
single<LogLevelManager> {
|
||||
DefaultLogLevelManager()
|
||||
}.bind<LogLevelProvider>()
|
||||
|
||||
singleListOf<LogSink>(
|
||||
{ ConsoleLogSink(level = LogLevel.VERBOSE) },
|
||||
)
|
||||
|
||||
single<CompositeLogSink> {
|
||||
CompositeLogSink(
|
||||
logLevelProvider = get(),
|
||||
sinks = getList(),
|
||||
)
|
||||
}
|
||||
|
||||
single<Logger> {
|
||||
@OptIn(ExperimentalTime::class)
|
||||
DefaultLogger(
|
||||
sink = get<CompositeLogSink>(),
|
||||
)
|
||||
}
|
||||
|
||||
single<CompositeLogSink>(named(SYNC_DEBUG_LOG)) {
|
||||
CompositeLogSink(
|
||||
logLevelProvider = get(),
|
||||
sinks = getList(),
|
||||
)
|
||||
}
|
||||
|
||||
single<FileLogSink>(named(SYNC_DEBUG_LOG)) {
|
||||
FileLogSink(
|
||||
level = LogLevel.DEBUG,
|
||||
fileName = "thunderbird-sync-debug",
|
||||
fileLocation = get<Context>().filesDir.path,
|
||||
fileSystemManager = AndroidFileSystemManager(get<Context>().contentResolver),
|
||||
)
|
||||
}
|
||||
|
||||
single<Logger>(named(SYNC_DEBUG_LOG)) {
|
||||
@OptIn(ExperimentalTime::class)
|
||||
DefaultLogger(
|
||||
sink = get<CompositeLogSink>(named(SYNC_DEBUG_LOG)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal const val SYNC_DEBUG_LOG = "syncDebug"
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package net.thunderbird.app.common.core.logging
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import net.thunderbird.app.common.BuildConfig
|
||||
import net.thunderbird.core.logging.LogLevel
|
||||
import net.thunderbird.core.logging.LogLevelManager
|
||||
|
||||
class DefaultLogLevelManager : LogLevelManager {
|
||||
private val defaultLevel = if (BuildConfig.DEBUG) LogLevel.VERBOSE else LogLevel.INFO
|
||||
private val logLevel = MutableStateFlow(defaultLevel)
|
||||
|
||||
override fun override(level: LogLevel) {
|
||||
logLevel.update { level }
|
||||
}
|
||||
|
||||
override fun restoreDefault() {
|
||||
override(defaultLevel)
|
||||
}
|
||||
|
||||
override fun current(): LogLevel = logLevel.value
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package net.thunderbird.app.common.feature
|
||||
|
||||
import android.content.Context
|
||||
import app.k9mail.feature.launcher.FeatureLauncherExternalContract
|
||||
import com.fsck.k9.activity.MessageList
|
||||
|
||||
internal class AccountSetupFinishedLauncher(
|
||||
private val context: Context,
|
||||
) : FeatureLauncherExternalContract.AccountSetupFinishedLauncher {
|
||||
override fun launch(accountUuid: String?) {
|
||||
if (accountUuid != null) {
|
||||
MessageList.launch(context, accountUuid)
|
||||
} else {
|
||||
MessageList.launch(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package net.thunderbird.app.common.feature
|
||||
|
||||
import app.k9mail.feature.launcher.FeatureLauncherExternalContract
|
||||
import app.k9mail.feature.launcher.di.featureLauncherModule
|
||||
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract
|
||||
import net.thunderbird.feature.notification.impl.inject.featureNotificationModule
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
internal val appCommonFeatureModule = module {
|
||||
includes(featureLauncherModule)
|
||||
includes(featureNotificationModule)
|
||||
|
||||
factory<FeatureLauncherExternalContract.AccountSetupFinishedLauncher> {
|
||||
AccountSetupFinishedLauncher(
|
||||
context = androidContext(),
|
||||
)
|
||||
}
|
||||
|
||||
single<NavigationDrawerExternalContract.DrawerConfigLoader> {
|
||||
NavigationDrawerConfigLoader(get())
|
||||
}
|
||||
|
||||
single<NavigationDrawerExternalContract.DrawerConfigWriter> {
|
||||
NavigationDrawerConfigWriter(get())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package net.thunderbird.app.common.feature
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.thunderbird.core.logging.file.FileLogSink
|
||||
|
||||
class LoggerLifecycleObserver(val fileLogSink: FileLogSink?) : DefaultLifecycleObserver {
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
super.onStop(owner)
|
||||
fileLogSink?.let {
|
||||
owner.lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
it.flushAndCloseBuffer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package net.thunderbird.app.common.feature
|
||||
|
||||
import com.fsck.k9.preferences.DrawerConfigManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract
|
||||
|
||||
internal class NavigationDrawerConfigLoader(private val drawerConfigManager: DrawerConfigManager) :
|
||||
NavigationDrawerExternalContract.DrawerConfigLoader {
|
||||
override fun loadDrawerConfigFlow(): Flow<NavigationDrawerExternalContract.DrawerConfig> {
|
||||
return drawerConfigManager.getConfigFlow()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package net.thunderbird.app.common.feature
|
||||
|
||||
import com.fsck.k9.preferences.DrawerConfigManager
|
||||
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract
|
||||
import net.thunderbird.feature.navigation.drawer.api.NavigationDrawerExternalContract.DrawerConfig
|
||||
|
||||
internal class NavigationDrawerConfigWriter(
|
||||
private val drawerConfigManager: DrawerConfigManager,
|
||||
) : NavigationDrawerExternalContract.DrawerConfigWriter {
|
||||
override fun writeDrawerConfig(drawerConfig: DrawerConfig) {
|
||||
drawerConfigManager.save(drawerConfig)
|
||||
}
|
||||
}
|
||||
14
app-common/src/main/res/xml/network_security_config.xml
Normal file
14
app-common/src/main/res/xml/network_security_config.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="InsecureBaseConfiguration,AcceptsUserCertificates"
|
||||
>
|
||||
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
|
||||
</network-security-config>
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
package net.thunderbird.app.common.account
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import com.fsck.k9.CoreResourceProvider
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_FORMAT
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_FORMAT_AUTO
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_READ_RECEIPT
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_QUOTED_TEXT_SHOWN
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_QUOTE_PREFIX
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_QUOTE_STYLE
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_REMOTE_SEARCH_NUM_RESULTS
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_REPLY_AFTER_QUOTE
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_RINGTONE_URI
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_SORT_ASCENDING
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_SORT_TYPE
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_STRIP_SIGNATURE
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_SYNC_INTERVAL
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.DEFAULT_VISIBLE_LIMIT
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.NO_OPENPGP_KEY
|
||||
import net.thunderbird.core.android.account.AccountDefaultsProvider.Companion.UNASSIGNED_ACCOUNT_NUMBER
|
||||
import net.thunderbird.core.android.account.Expunge
|
||||
import net.thunderbird.core.android.account.FolderMode
|
||||
import net.thunderbird.core.android.account.Identity
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
import net.thunderbird.core.android.account.ShowPictures
|
||||
import net.thunderbird.core.featureflag.FeatureFlagResult
|
||||
import net.thunderbird.core.preference.storage.Storage
|
||||
import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
|
||||
import net.thunderbird.feature.notification.NotificationLight
|
||||
import net.thunderbird.feature.notification.NotificationSettings
|
||||
import net.thunderbird.feature.notification.NotificationVibration
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
|
||||
class DefaultAccountDefaultsProviderTest {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Test
|
||||
fun `applyDefaults should return default values`() {
|
||||
// arrange
|
||||
val resourceProvider = mock<CoreResourceProvider> {
|
||||
on { defaultIdentityDescription() } doReturn "Default Identity"
|
||||
}
|
||||
val account = LegacyAccount(
|
||||
uuid = "cf728064-077d-4369-a0c7-7c2b21693d9b",
|
||||
isSensitiveDebugLoggingEnabled = { false },
|
||||
)
|
||||
val identities = listOf(
|
||||
Identity(
|
||||
signatureUse = false,
|
||||
signature = null,
|
||||
description = resourceProvider.defaultIdentityDescription(),
|
||||
),
|
||||
)
|
||||
val notificationSettings = NotificationSettings(
|
||||
isRingEnabled = true,
|
||||
ringtone = DEFAULT_RINGTONE_URI,
|
||||
light = NotificationLight.Disabled,
|
||||
vibration = NotificationVibration.DEFAULT,
|
||||
)
|
||||
val testSubject = DefaultAccountDefaultsProvider(
|
||||
resourceProvider = resourceProvider,
|
||||
featureFlagProvider = {
|
||||
FeatureFlagResult.Disabled
|
||||
},
|
||||
)
|
||||
|
||||
// act
|
||||
testSubject.applyDefaults(account)
|
||||
|
||||
// assert
|
||||
assertThat(account.automaticCheckIntervalMinutes).isEqualTo(DEFAULT_SYNC_INTERVAL)
|
||||
assertThat(account.idleRefreshMinutes).isEqualTo(24)
|
||||
assertThat(account.displayCount).isEqualTo(DEFAULT_VISIBLE_LIMIT)
|
||||
assertThat(account.accountNumber).isEqualTo(UNASSIGNED_ACCOUNT_NUMBER)
|
||||
assertThat(account.isNotifyNewMail).isTrue()
|
||||
assertThat(account.folderNotifyNewMailMode).isEqualTo(FolderMode.ALL)
|
||||
assertThat(account.isNotifySync).isFalse()
|
||||
assertThat(account.isNotifySelfNewMail).isTrue()
|
||||
assertThat(account.isNotifyContactsMailOnly).isFalse()
|
||||
assertThat(account.isIgnoreChatMessages).isFalse()
|
||||
assertThat(account.messagesNotificationChannelVersion).isEqualTo(0)
|
||||
assertThat(account.folderDisplayMode).isEqualTo(FolderMode.NOT_SECOND_CLASS)
|
||||
assertThat(account.folderSyncMode).isEqualTo(FolderMode.FIRST_CLASS)
|
||||
assertThat(account.folderPushMode).isEqualTo(FolderMode.NONE)
|
||||
assertThat(account.sortType).isEqualTo(DEFAULT_SORT_TYPE)
|
||||
assertThat(account.isSortAscending(DEFAULT_SORT_TYPE)).isEqualTo(DEFAULT_SORT_ASCENDING)
|
||||
assertThat(account.showPictures).isEqualTo(ShowPictures.NEVER)
|
||||
assertThat(account.isSignatureBeforeQuotedText).isFalse()
|
||||
assertThat(account.expungePolicy).isEqualTo(Expunge.EXPUNGE_IMMEDIATELY)
|
||||
assertThat(account.importedAutoExpandFolder).isNull()
|
||||
assertThat(account.legacyInboxFolder).isNull()
|
||||
assertThat(account.maxPushFolders).isEqualTo(10)
|
||||
assertThat(account.isSubscribedFoldersOnly).isFalse()
|
||||
assertThat(account.maximumPolledMessageAge).isEqualTo(-1)
|
||||
assertThat(account.maximumAutoDownloadMessageSize).isEqualTo(DEFAULT_MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE)
|
||||
assertThat(account.messageFormat).isEqualTo(DEFAULT_MESSAGE_FORMAT)
|
||||
assertThat(account.isMessageFormatAuto).isEqualTo(DEFAULT_MESSAGE_FORMAT_AUTO)
|
||||
assertThat(account.isMessageReadReceipt).isEqualTo(DEFAULT_MESSAGE_READ_RECEIPT)
|
||||
assertThat(account.quoteStyle).isEqualTo(DEFAULT_QUOTE_STYLE)
|
||||
assertThat(account.quotePrefix).isEqualTo(DEFAULT_QUOTE_PREFIX)
|
||||
assertThat(account.isDefaultQuotedTextShown).isEqualTo(DEFAULT_QUOTED_TEXT_SHOWN)
|
||||
assertThat(account.isReplyAfterQuote).isEqualTo(DEFAULT_REPLY_AFTER_QUOTE)
|
||||
assertThat(account.isStripSignature).isEqualTo(DEFAULT_STRIP_SIGNATURE)
|
||||
assertThat(account.isSyncRemoteDeletions).isTrue()
|
||||
assertThat(account.openPgpKey).isEqualTo(NO_OPENPGP_KEY)
|
||||
assertThat(account.isRemoteSearchFullText).isFalse()
|
||||
assertThat(account.remoteSearchNumResults).isEqualTo(DEFAULT_REMOTE_SEARCH_NUM_RESULTS)
|
||||
assertThat(account.isUploadSentMessages).isTrue()
|
||||
assertThat(account.isMarkMessageAsReadOnView).isTrue()
|
||||
assertThat(account.isMarkMessageAsReadOnDelete).isTrue()
|
||||
assertThat(account.isAlwaysShowCcBcc).isFalse()
|
||||
assertThat(account.lastSyncTime).isEqualTo(0L)
|
||||
assertThat(account.lastFolderListRefreshTime).isEqualTo(0L)
|
||||
|
||||
assertThat(account.archiveFolderId).isNull()
|
||||
assertThat(account.archiveFolderSelection).isEqualTo(SpecialFolderSelection.AUTOMATIC)
|
||||
assertThat(account.draftsFolderId).isNull()
|
||||
assertThat(account.draftsFolderSelection).isEqualTo(SpecialFolderSelection.AUTOMATIC)
|
||||
assertThat(account.sentFolderId).isNull()
|
||||
assertThat(account.sentFolderSelection).isEqualTo(SpecialFolderSelection.AUTOMATIC)
|
||||
assertThat(account.spamFolderId).isNull()
|
||||
assertThat(account.spamFolderSelection).isEqualTo(SpecialFolderSelection.AUTOMATIC)
|
||||
assertThat(account.trashFolderId).isNull()
|
||||
assertThat(account.trashFolderSelection).isEqualTo(SpecialFolderSelection.AUTOMATIC)
|
||||
assertThat(account.archiveFolderId).isNull()
|
||||
assertThat(account.archiveFolderSelection).isEqualTo(SpecialFolderSelection.AUTOMATIC)
|
||||
|
||||
assertThat(account.identities).isEqualTo(identities)
|
||||
assertThat(account.notificationSettings).isEqualTo(notificationSettings)
|
||||
|
||||
assertThat(account.isChangedVisibleLimits).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `applyOverwrites should return patched account when disabled`() {
|
||||
// arrange
|
||||
val resourceProvider = mock<CoreResourceProvider> {
|
||||
on { defaultIdentityDescription() } doReturn "Default Identity"
|
||||
}
|
||||
val account = LegacyAccount(
|
||||
uuid = "cf728064-077d-4369-a0c7-7c2b21693d9b",
|
||||
isSensitiveDebugLoggingEnabled = { false },
|
||||
)
|
||||
val storage = mock<Storage> {
|
||||
on { contains("${account.uuid}.notifyNewMail") } doReturn false
|
||||
on { getBoolean("${account.uuid}.notifyNewMail", false) } doReturn false
|
||||
on { getBoolean("${account.uuid}.notifySelfNewMail", false) } doReturn false
|
||||
}
|
||||
val testSubject = DefaultAccountDefaultsProvider(
|
||||
resourceProvider = resourceProvider,
|
||||
featureFlagProvider = {
|
||||
FeatureFlagResult.Disabled
|
||||
},
|
||||
)
|
||||
|
||||
// act
|
||||
testSubject.applyOverwrites(account, storage)
|
||||
|
||||
// assert
|
||||
assertThat(account.isNotifyNewMail).isFalse()
|
||||
assertThat(account.isNotifySelfNewMail).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `applyOverwrites should return patched account when enabled`() {
|
||||
// arrange
|
||||
val resourceProvider = mock<CoreResourceProvider> {
|
||||
on { defaultIdentityDescription() } doReturn "Default Identity"
|
||||
}
|
||||
val account = LegacyAccount(
|
||||
uuid = "cf728064-077d-4369-a0c7-7c2b21693d9b",
|
||||
isSensitiveDebugLoggingEnabled = { false },
|
||||
)
|
||||
val storage = mock<Storage> {
|
||||
on { contains("${account.uuid}.notifyNewMail") } doReturn false
|
||||
on { getBoolean("${account.uuid}.notifyNewMail", false) } doReturn false
|
||||
on { getBoolean("${account.uuid}.notifySelfNewMail", false) } doReturn false
|
||||
}
|
||||
val testSubject = DefaultAccountDefaultsProvider(
|
||||
resourceProvider = resourceProvider,
|
||||
featureFlagProvider = {
|
||||
FeatureFlagResult.Enabled
|
||||
},
|
||||
)
|
||||
|
||||
// act
|
||||
testSubject.applyOverwrites(account, storage)
|
||||
|
||||
// assert
|
||||
assertThat(account.isNotifyNewMail).isTrue()
|
||||
assertThat(account.isNotifySelfNewMail).isTrue()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `applyOverwrites updates account notification values from storage when storage contains isNotifyNewMail value`() {
|
||||
// arrange
|
||||
val resourceProvider = mock<CoreResourceProvider> {
|
||||
on { defaultIdentityDescription() } doReturn "Default Identity"
|
||||
}
|
||||
val account = LegacyAccount(
|
||||
uuid = "cf728064-077d-4369-a0c7-7c2b21693d9b",
|
||||
isSensitiveDebugLoggingEnabled = { false },
|
||||
)
|
||||
val storage = mock<Storage> {
|
||||
on { contains("${account.uuid}.notifyNewMail") } doReturn true
|
||||
on { getBoolean("${account.uuid}.notifyNewMail", false) } doReturn false
|
||||
on { getBoolean("${account.uuid}.notifySelfNewMail", false) } doReturn false
|
||||
}
|
||||
val testSubject = DefaultAccountDefaultsProvider(
|
||||
resourceProvider = resourceProvider,
|
||||
featureFlagProvider = {
|
||||
FeatureFlagResult.Enabled
|
||||
},
|
||||
)
|
||||
|
||||
// act
|
||||
testSubject.applyOverwrites(account, storage)
|
||||
|
||||
// assert
|
||||
assertThat(account.isNotifyNewMail).isFalse()
|
||||
assertThat(account.isNotifySelfNewMail).isFalse()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `applyOverwrites updates account notification values from featureFlag values when storage does not contain isNotifyNewMail value`() {
|
||||
// arrange
|
||||
val resourceProvider = mock<CoreResourceProvider> {
|
||||
on { defaultIdentityDescription() } doReturn "Default Identity"
|
||||
}
|
||||
val account = LegacyAccount(
|
||||
uuid = "cf728064-077d-4369-a0c7-7c2b21693d9b",
|
||||
isSensitiveDebugLoggingEnabled = { false },
|
||||
)
|
||||
val storage = mock<Storage> {
|
||||
on { contains("${account.uuid}.notifyNewMail") } doReturn false
|
||||
on { getBoolean("${account.uuid}.notifyNewMail", false) } doReturn false
|
||||
on { getBoolean("${account.uuid}.notifySelfNewMail", false) } doReturn false
|
||||
}
|
||||
val testSubject = DefaultAccountDefaultsProvider(
|
||||
resourceProvider = resourceProvider,
|
||||
featureFlagProvider = {
|
||||
FeatureFlagResult.Enabled
|
||||
},
|
||||
)
|
||||
|
||||
// act
|
||||
testSubject.applyOverwrites(account, storage)
|
||||
|
||||
// assert
|
||||
assertThat(account.isNotifyNewMail).isTrue()
|
||||
assertThat(account.isNotifySelfNewMail).isTrue()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package net.thunderbird.app.common.account.data
|
||||
|
||||
import app.cash.turbine.test
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.thunderbird.account.fake.FakeAccountProfileData.PROFILE_COLOR
|
||||
import net.thunderbird.account.fake.FakeAccountProfileData.PROFILE_NAME
|
||||
import net.thunderbird.core.android.account.Identity
|
||||
import net.thunderbird.core.android.account.LegacyAccountWrapper
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
import net.thunderbird.feature.account.AccountIdFactory
|
||||
import net.thunderbird.feature.account.profile.AccountAvatar
|
||||
import net.thunderbird.feature.account.profile.AccountProfile
|
||||
import net.thunderbird.feature.account.storage.legacy.mapper.DefaultAccountAvatarDataMapper
|
||||
import net.thunderbird.feature.account.storage.legacy.mapper.DefaultAccountProfileDataMapper
|
||||
import net.thunderbird.feature.account.storage.profile.AvatarDto
|
||||
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
|
||||
import net.thunderbird.feature.account.storage.profile.ProfileDto
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultAccountProfileLocalDataSourceTest {
|
||||
|
||||
@Test
|
||||
fun `getById should return account profile`() = runTest {
|
||||
// arrange
|
||||
val accountId = AccountIdFactory.create()
|
||||
val legacyAccount = createLegacyAccount(accountId)
|
||||
val accountProfile = createAccountProfile(accountId)
|
||||
val testSubject = createTestSubject(legacyAccount)
|
||||
|
||||
// act & assert
|
||||
testSubject.getById(accountId).test {
|
||||
assertThat(awaitItem()).isEqualTo(accountProfile)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getById should return null when account is not found`() = runTest {
|
||||
// arrange
|
||||
val accountId = AccountIdFactory.create()
|
||||
val testSubject = createTestSubject(null)
|
||||
|
||||
// act & assert
|
||||
testSubject.getById(accountId).test {
|
||||
assertThat(awaitItem()).isEqualTo(null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `update should save account profile`() = runTest {
|
||||
// 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)
|
||||
|
||||
// act & assert
|
||||
testSubject.getById(accountId).test {
|
||||
assertThat(awaitItem()).isEqualTo(accountProfile)
|
||||
|
||||
testSubject.update(updatedAccountProfile)
|
||||
|
||||
assertThat(awaitItem()).isEqualTo(updatedAccountProfile)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object Companion {
|
||||
fun createLegacyAccount(
|
||||
id: AccountId,
|
||||
displayName: String = PROFILE_NAME,
|
||||
color: Int = PROFILE_COLOR,
|
||||
): LegacyAccountWrapper {
|
||||
return LegacyAccountWrapper(
|
||||
isSensitiveDebugLoggingEnabled = { true },
|
||||
id = id,
|
||||
name = displayName,
|
||||
email = "demo@example.com",
|
||||
profile = ProfileDto(
|
||||
id = id,
|
||||
name = displayName,
|
||||
color = color,
|
||||
avatar = AvatarDto(
|
||||
avatarType = AvatarTypeDto.ICON,
|
||||
avatarMonogram = null,
|
||||
avatarImageUri = null,
|
||||
avatarIconName = "star",
|
||||
),
|
||||
),
|
||||
identities = listOf(
|
||||
Identity(
|
||||
signatureUse = false,
|
||||
description = "Demo User",
|
||||
),
|
||||
),
|
||||
incomingServerSettings = ServerSettings(
|
||||
type = "imap",
|
||||
host = "imap.example.com",
|
||||
port = 993,
|
||||
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
authenticationType = AuthType.PLAIN,
|
||||
username = "test",
|
||||
password = "password",
|
||||
clientCertificateAlias = null,
|
||||
),
|
||||
outgoingServerSettings = ServerSettings(
|
||||
type = "smtp",
|
||||
host = "smtp.example.com",
|
||||
port = 465,
|
||||
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
authenticationType = AuthType.PLAIN,
|
||||
username = "test",
|
||||
password = "password",
|
||||
clientCertificateAlias = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createAccountProfile(
|
||||
accountId: AccountId,
|
||||
name: String = PROFILE_NAME,
|
||||
color: Int = PROFILE_COLOR,
|
||||
): AccountProfile {
|
||||
return AccountProfile(
|
||||
id = accountId,
|
||||
name = name,
|
||||
color = color,
|
||||
avatar = AccountAvatar.Icon(
|
||||
name = "star",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTestSubject(
|
||||
legacyAccount: LegacyAccountWrapper?,
|
||||
): DefaultAccountProfileLocalDataSource {
|
||||
return DefaultAccountProfileLocalDataSource(
|
||||
accountManager = FakeLegacyAccountWrapperManager(
|
||||
initialAccounts = if (legacyAccount != null) {
|
||||
listOf(legacyAccount)
|
||||
} else {
|
||||
emptyList()
|
||||
},
|
||||
),
|
||||
dataMapper = DefaultAccountProfileDataMapper(
|
||||
avatarMapper = DefaultAccountAvatarDataMapper(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package net.thunderbird.app.common.account.data
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import net.thunderbird.core.android.account.LegacyAccountWrapper
|
||||
import net.thunderbird.core.android.account.LegacyAccountWrapperManager
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
|
||||
internal class FakeLegacyAccountWrapperManager(
|
||||
initialAccounts: List<LegacyAccountWrapper> = emptyList(),
|
||||
) : LegacyAccountWrapperManager {
|
||||
|
||||
private val accountsState = MutableStateFlow(
|
||||
initialAccounts,
|
||||
)
|
||||
private val accounts: StateFlow<List<LegacyAccountWrapper>> = accountsState
|
||||
|
||||
override fun getAll(): Flow<List<LegacyAccountWrapper>> = accounts
|
||||
|
||||
override fun getById(id: AccountId): Flow<LegacyAccountWrapper?> = accounts
|
||||
.map { list ->
|
||||
list.find { it.id == id }
|
||||
}
|
||||
|
||||
override suspend fun update(account: LegacyAccountWrapper) {
|
||||
accountsState.update { currentList ->
|
||||
currentList.toMutableList().apply {
|
||||
removeIf { it.uuid == account.uuid }
|
||||
add(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue