Repo created
This commit is contained in:
parent
75dc487a7a
commit
39c29d175b
6317 changed files with 388324 additions and 2 deletions
45
legacy/common/build.gradle.kts
Normal file
45
legacy/common/build.gradle.kts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.android)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.legacy.ui.legacy)
|
||||
implementation(projects.legacy.core)
|
||||
implementation(projects.legacy.storage)
|
||||
implementation(projects.legacy.cryptoOpenpgp)
|
||||
implementation(projects.backend.imap)
|
||||
implementation(projects.backend.pop3)
|
||||
|
||||
implementation(projects.core.featureflag)
|
||||
implementation(projects.core.logging.api)
|
||||
implementation(projects.feature.launcher)
|
||||
|
||||
implementation(projects.feature.account.setup)
|
||||
implementation(projects.feature.account.edit)
|
||||
implementation(projects.feature.navigation.drawer.api)
|
||||
implementation(projects.feature.settings.import)
|
||||
|
||||
implementation(projects.feature.widget.unread)
|
||||
implementation(projects.feature.widget.messageList)
|
||||
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.preferencex)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.appauth)
|
||||
|
||||
implementation(libs.glide)
|
||||
annotationProcessor(libs.glide.compiler)
|
||||
|
||||
if (project.hasProperty("k9mail.enableLeakCanary") && project.property("k9mail.enableLeakCanary") == "true") {
|
||||
debugImplementation(libs.leakcanary.android)
|
||||
}
|
||||
|
||||
testImplementation(projects.core.logging.testing)
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(projects.feature.account.fake)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.fsck.k9.common"
|
||||
}
|
||||
342
legacy/common/src/main/AndroidManifest.xml
Normal file
342
legacy/common/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false"
|
||||
/>
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:smallScreens="true"
|
||||
/>
|
||||
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" android:maxSdkVersion="33" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
||||
tools:ignore="ProtectedPermissions"
|
||||
/>
|
||||
|
||||
<application
|
||||
android:allowTaskReparenting="false"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsRtl="true"
|
||||
tools:ignore="UnusedAttribute"
|
||||
>
|
||||
|
||||
<!-- TODO: Remove once minSdkVersion has been changed to 24+ -->
|
||||
<meta-data
|
||||
android:name="com.lge.support.SPLIT_WINDOW"
|
||||
android:value="true"
|
||||
/>
|
||||
<uses-library
|
||||
android:name="com.sec.android.app.multiwindow"
|
||||
android:required="false"
|
||||
/>
|
||||
|
||||
<meta-data
|
||||
android:name="com.sec.android.support.multiwindow"
|
||||
android:value="true"
|
||||
/>
|
||||
<meta-data
|
||||
android:name="com.samsung.android.sdk.multiwindow.penwindow.enable"
|
||||
android:value="true"
|
||||
/>
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.setup.AccountSetupComposition"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/account_settings_composition_title"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.choosefolder.ChooseFolderActivity"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/choose_folder_title"
|
||||
android:noHistory="true"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.ChooseIdentity"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/choose_identity_title"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.ManageIdentities"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/manage_identities_title"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.EditIdentity"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/edit_identity_title"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.endtoend.AutocryptKeyTransferActivity"
|
||||
android:configChanges="locale"
|
||||
android:label="@string/ac_transfer_title"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.MessageList"
|
||||
android:launchMode="singleTop"
|
||||
android:exported="true"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.APP_EMAIL" />
|
||||
|
||||
<!-- TODO: Remove once minSdkVersion has been changed to 24+ -->
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||
<category android:name="android.intent.category.PENWINDOW_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data
|
||||
android:host="messages"
|
||||
android:scheme="k9mail"
|
||||
/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!--
|
||||
This component is disabled by default. It will be enabled programmatically after an account has been set up.
|
||||
-->
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.MessageCompose"
|
||||
android:configChanges="locale"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
<data android:scheme="mailto" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
<data android:scheme="mailto" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<data android:mimeType="*/*" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<data android:mimeType="*/*" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="mailto" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="org.autocrypt.PEER_ACTION" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.Search"
|
||||
android:label="@string/search_action"
|
||||
android:exported="false"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.activity.UpgradeDatabases"
|
||||
android:label="@string/upgrade_databases_title"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.managefolders.ManageFoldersActivity"
|
||||
android:label="@string/folders_action"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.settings.SettingsActivity"
|
||||
android:label="@string/prefs_title"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.settings.general.GeneralSettingsActivity"
|
||||
android:label="@string/general_settings_title"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.settings.account.AccountSettingsActivity"
|
||||
android:label="@string/account_settings_title_fmt"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.messagesource.MessageSourceActivity"
|
||||
android:label="@string/show_headers_action"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.changelog.RecentChangesActivity"
|
||||
android:label="@string/changelog_recent_changes_title"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.push.PushInfoActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:label="@string/push_info_title"
|
||||
android:taskAffinity="${applicationId}.push_info"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="app.k9mail.action.PUSH_INFO" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- This component is disabled by default. It will be enabled programmatically if necessary. -->
|
||||
<receiver
|
||||
android:name="com.fsck.k9.controller.push.BootCompleteReceiver"
|
||||
android:exported="false"
|
||||
android:enabled="false"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service android:name="com.fsck.k9.notification.NotificationActionService" />
|
||||
|
||||
<service
|
||||
android:name="com.fsck.k9.service.DatabaseUpgradeService"
|
||||
android:exported="false"
|
||||
/>
|
||||
|
||||
<service
|
||||
android:name="com.fsck.k9.controller.push.PushService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync|specialUse"
|
||||
>
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="This service is used to maintain a continuous connection to an IMAP server to be able to provide instant notifications to the user when a new email arrives. Firebase Cloud Messaging is not suitable for this task, neither are mechanisms like AndroidX WorkManager. Other foreground service types aren't a good fit for this use case."
|
||||
/>
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="com.fsck.k9.provider.AttachmentProvider"
|
||||
android:authorities="${applicationId}.attachmentprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true"
|
||||
>
|
||||
|
||||
<meta-data
|
||||
android:name="de.cketti.safecontentresolver.ALLOW_INTERNAL_ACCESS"
|
||||
android:value="true"
|
||||
/>
|
||||
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="com.fsck.k9.provider.RawMessageProvider"
|
||||
android:authorities="${applicationId}.rawmessageprovider"
|
||||
android:exported="false"
|
||||
>
|
||||
|
||||
<meta-data
|
||||
android:name="de.cketti.safecontentresolver.ALLOW_INTERNAL_ACCESS"
|
||||
android:value="true"
|
||||
/>
|
||||
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="com.fsck.k9.provider.DecryptedFileProvider"
|
||||
android:authorities="${applicationId}.decryptedfileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true"
|
||||
>
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/decrypted_file_provider_paths"
|
||||
/>
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="com.fsck.k9.provider.AttachmentTempFileProvider"
|
||||
android:authorities="${applicationId}.tempfileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true"
|
||||
>
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/temp_file_provider_paths"
|
||||
/>
|
||||
|
||||
<meta-data
|
||||
android:name="de.cketti.safecontentresolver.ALLOW_INTERNAL_ACCESS"
|
||||
android:value="true"
|
||||
/>
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
android:name="net.openid.appauth.RedirectUriReceiverActivity"
|
||||
android:exported="true"
|
||||
>
|
||||
|
||||
<!-- The library's default intent filter with `appAuthRedirectScheme` replaced by `applicationId` -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="${applicationId}" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Microsoft uses a special redirect URI format for Android apps -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:scheme="msauth"
|
||||
android:host="${applicationId}"
|
||||
/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import app.k9mail.feature.widget.message.list.messageListWidgetModule
|
||||
import app.k9mail.feature.widget.unread.UnreadWidgetUpdateListener
|
||||
import app.k9mail.feature.widget.unread.unreadWidgetModule
|
||||
import com.fsck.k9.account.newAccountModule
|
||||
import com.fsck.k9.backends.backendsModule
|
||||
import com.fsck.k9.controller.ControllerExtension
|
||||
import com.fsck.k9.crypto.EncryptionExtractor
|
||||
import com.fsck.k9.crypto.openpgp.OpenPgpEncryptionExtractor
|
||||
import com.fsck.k9.notification.notificationModule
|
||||
import com.fsck.k9.preferences.K9StoragePersister
|
||||
import com.fsck.k9.resources.resourcesModule
|
||||
import com.fsck.k9.storage.storageModule
|
||||
import net.thunderbird.core.featureflag.FeatureFlagProvider
|
||||
import net.thunderbird.core.featureflag.InMemoryFeatureFlagProvider
|
||||
import net.thunderbird.core.preference.storage.StoragePersister
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val legacyCommonAppModule = module {
|
||||
single<MessagingListenerProvider> {
|
||||
DefaultMessagingListenerProvider(
|
||||
listeners = listOf(
|
||||
get<UnreadWidgetUpdateListener>(),
|
||||
),
|
||||
)
|
||||
}
|
||||
single(named("controllerExtensions")) { emptyList<ControllerExtension>() }
|
||||
single<EncryptionExtractor> { OpenPgpEncryptionExtractor.newInstance() }
|
||||
single<StoragePersister> {
|
||||
K9StoragePersister(get(), get())
|
||||
}
|
||||
single<FeatureFlagProvider> {
|
||||
InMemoryFeatureFlagProvider(
|
||||
featureFlagFactory = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val legacyCommonAppModules = listOf(
|
||||
legacyCommonAppModule,
|
||||
messageListWidgetModule,
|
||||
unreadWidgetModule,
|
||||
notificationModule,
|
||||
resourcesModule,
|
||||
backendsModule,
|
||||
storageModule,
|
||||
newAccountModule,
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.fsck.k9
|
||||
|
||||
import app.k9mail.legacy.message.controller.MessagingListener
|
||||
|
||||
interface MessagingListenerProvider {
|
||||
val listeners: List<MessagingListener>
|
||||
}
|
||||
|
||||
class DefaultMessagingListenerProvider(
|
||||
override val listeners: List<MessagingListener>,
|
||||
) : MessagingListenerProvider
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.fsck.k9.account
|
||||
|
||||
import android.content.Context
|
||||
import app.k9mail.feature.settings.import.SettingsImportExternalContract
|
||||
import com.fsck.k9.Core
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.controller.MessagingController
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
|
||||
/**
|
||||
* Activate account after server password(s) have been provided on settings import.
|
||||
*/
|
||||
class AccountActivator(
|
||||
private val context: Context,
|
||||
private val preferences: Preferences,
|
||||
private val messagingController: MessagingController,
|
||||
) : SettingsImportExternalContract.AccountActivator {
|
||||
override fun enableAccount(accountUuid: String, incomingServerPassword: String?, outgoingServerPassword: String?) {
|
||||
val account = preferences.getAccount(accountUuid) ?: error("Account $accountUuid not found")
|
||||
|
||||
setAccountPasswords(account, incomingServerPassword, outgoingServerPassword)
|
||||
enableAccount(account)
|
||||
}
|
||||
|
||||
override fun enableAccount(accountUuid: String) {
|
||||
val account = preferences.getAccount(accountUuid) ?: error("Account $accountUuid not found")
|
||||
|
||||
enableAccount(account)
|
||||
}
|
||||
|
||||
private fun enableAccount(account: LegacyAccount) {
|
||||
// Start services if necessary
|
||||
Core.setServicesEnabled(context)
|
||||
|
||||
// Get list of folders from remote server
|
||||
messagingController.refreshFolderList(account)
|
||||
}
|
||||
|
||||
private fun setAccountPasswords(
|
||||
account: LegacyAccount,
|
||||
incomingServerPassword: String?,
|
||||
outgoingServerPassword: String?,
|
||||
) {
|
||||
if (incomingServerPassword != null) {
|
||||
account.incomingServerSettings = account.incomingServerSettings.newPassword(incomingServerPassword)
|
||||
}
|
||||
|
||||
if (outgoingServerPassword != null) {
|
||||
account.outgoingServerSettings = account.outgoingServerSettings.newPassword(outgoingServerPassword)
|
||||
}
|
||||
|
||||
preferences.saveAccount(account)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.fsck.k9.account
|
||||
|
||||
import app.k9mail.feature.account.common.AccountCommonExternalContract
|
||||
import app.k9mail.feature.account.edit.AccountEditExternalContract
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract
|
||||
import app.k9mail.feature.settings.import.SettingsImportExternalContract
|
||||
import org.koin.dsl.module
|
||||
|
||||
val newAccountModule = module {
|
||||
factory<AccountSetupExternalContract.AccountOwnerNameProvider> {
|
||||
AccountOwnerNameProvider(
|
||||
preferences = get(),
|
||||
)
|
||||
}
|
||||
|
||||
factory<AccountCommonExternalContract.AccountStateLoader> {
|
||||
AccountStateLoader(
|
||||
accountManager = get(),
|
||||
)
|
||||
}
|
||||
|
||||
factory<AccountEditExternalContract.AccountServerSettingsUpdater> {
|
||||
AccountServerSettingsUpdater(
|
||||
accountManager = get(),
|
||||
)
|
||||
}
|
||||
|
||||
factory<SettingsImportExternalContract.AccountActivator> {
|
||||
AccountActivator(
|
||||
context = get(),
|
||||
preferences = get(),
|
||||
messagingController = get(),
|
||||
)
|
||||
}
|
||||
|
||||
factory<DeletePolicyProvider> { DefaultDeletePolicyProvider() }
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.fsck.k9.account
|
||||
|
||||
import app.k9mail.feature.account.setup.AccountSetupExternalContract
|
||||
import com.fsck.k9.Preferences
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AccountOwnerNameProvider(
|
||||
private val preferences: Preferences,
|
||||
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) : AccountSetupExternalContract.AccountOwnerNameProvider {
|
||||
override suspend fun getOwnerName(): String? {
|
||||
return withContext(coroutineDispatcher) {
|
||||
preferences.defaultAccount?.senderName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.fsck.k9.account
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.AuthorizationState
|
||||
import app.k9mail.feature.account.edit.AccountEditExternalContract
|
||||
import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterFailure
|
||||
import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterResult
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreSettings
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreSettings.autoDetectNamespace
|
||||
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 kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.thunderbird.core.android.account.AccountManager
|
||||
import net.thunderbird.core.common.mail.Protocols
|
||||
import net.thunderbird.core.logging.legacy.Log
|
||||
|
||||
class AccountServerSettingsUpdater(
|
||||
private val accountManager: AccountManager,
|
||||
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) : AccountEditExternalContract.AccountServerSettingsUpdater {
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override suspend fun updateServerSettings(
|
||||
accountUuid: String,
|
||||
isIncoming: Boolean,
|
||||
serverSettings: ServerSettings,
|
||||
authorizationState: AuthorizationState?,
|
||||
): AccountUpdaterResult {
|
||||
return try {
|
||||
withContext(coroutineDispatcher) {
|
||||
updateSettings(accountUuid, isIncoming, serverSettings, authorizationState)
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
Log.e(error, "Error while updating account server settings with UUID %s", accountUuid)
|
||||
|
||||
AccountUpdaterResult.Failure(AccountUpdaterFailure.UnknownError(error))
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSettings(
|
||||
accountUuid: String,
|
||||
isIncoming: Boolean,
|
||||
serverSettings: ServerSettings,
|
||||
authorizationState: AuthorizationState?,
|
||||
): AccountUpdaterResult {
|
||||
val account = accountManager.getAccount(accountUuid = accountUuid) ?: return AccountUpdaterResult.Failure(
|
||||
AccountUpdaterFailure.AccountNotFound(accountUuid),
|
||||
)
|
||||
|
||||
if (isIncoming) {
|
||||
if (serverSettings.type == Protocols.IMAP) {
|
||||
account.useCompression = serverSettings.isUseCompression
|
||||
account.isSendClientInfoEnabled = serverSettings.isSendClientInfo
|
||||
account.incomingServerSettings = serverSettings.copy(
|
||||
extra = ImapStoreSettings.createExtra(
|
||||
autoDetectNamespace = serverSettings.autoDetectNamespace,
|
||||
pathPrefix = serverSettings.pathPrefix,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
account.incomingServerSettings = serverSettings
|
||||
}
|
||||
} else {
|
||||
account.outgoingServerSettings = serverSettings
|
||||
}
|
||||
|
||||
account.oAuthState = authorizationState?.value
|
||||
|
||||
accountManager.saveAccount(account)
|
||||
|
||||
return AccountUpdaterResult.Success(accountUuid)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.fsck.k9.account
|
||||
|
||||
import app.k9mail.feature.account.common.AccountCommonExternalContract
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountState
|
||||
import app.k9mail.feature.account.common.domain.entity.AuthorizationState
|
||||
import com.fsck.k9.backends.toImapServerSettings
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.thunderbird.core.android.account.AccountManager
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
import net.thunderbird.core.common.mail.Protocols
|
||||
import net.thunderbird.core.logging.legacy.Log
|
||||
|
||||
class AccountStateLoader(
|
||||
private val accountManager: AccountManager,
|
||||
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) : AccountCommonExternalContract.AccountStateLoader {
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override suspend fun loadAccountState(accountUuid: String): AccountState? {
|
||||
return try {
|
||||
withContext(coroutineDispatcher) {
|
||||
load(accountUuid)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(e, "Error while loading account")
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun load(accountUuid: String): AccountState? {
|
||||
return accountManager.getAccount(accountUuid)?.let { mapToAccountState(it) }
|
||||
}
|
||||
|
||||
private fun mapToAccountState(account: LegacyAccount): AccountState {
|
||||
return AccountState(
|
||||
uuid = account.uuid,
|
||||
emailAddress = account.email,
|
||||
incomingServerSettings = account.incomingServerSettingsExtra,
|
||||
outgoingServerSettings = account.outgoingServerSettings,
|
||||
authorizationState = AuthorizationState(account.oAuthState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val LegacyAccount.incomingServerSettingsExtra: ServerSettings
|
||||
get() = when (incomingServerSettings.type) {
|
||||
Protocols.IMAP -> toImapServerSettings()
|
||||
else -> incomingServerSettings
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.fsck.k9.account
|
||||
|
||||
import net.thunderbird.core.android.account.DeletePolicy
|
||||
import net.thunderbird.core.common.mail.Protocols
|
||||
|
||||
class DefaultDeletePolicyProvider : DeletePolicyProvider {
|
||||
override fun getDeletePolicy(accountType: String): DeletePolicy {
|
||||
return when (accountType) {
|
||||
Protocols.IMAP -> DeletePolicy.ON_DELETE
|
||||
Protocols.POP3 -> DeletePolicy.NEVER
|
||||
"demo" -> DeletePolicy.ON_DELETE
|
||||
else -> throw AssertionError("Unhandled case: $accountType")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.fsck.k9.account
|
||||
|
||||
import net.thunderbird.core.android.account.DeletePolicy
|
||||
import net.thunderbird.core.common.mail.Protocols
|
||||
|
||||
/**
|
||||
* Decides which [DeletePolicy] an account uses by default.
|
||||
*/
|
||||
interface DeletePolicyProvider {
|
||||
/**
|
||||
* Returns the [DeletePolicy] an account of type [accountType] should use by default.
|
||||
*
|
||||
* @param accountType The protocol identifier of the incoming server of an account. See [Protocols].
|
||||
*/
|
||||
fun getDeletePolicy(accountType: String): DeletePolicy
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import com.fsck.k9.mail.oauth.AuthStateStorage
|
||||
import net.thunderbird.core.android.account.AccountManager
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
|
||||
class AccountAuthStateStorage(
|
||||
private val accountManager: AccountManager,
|
||||
private val account: LegacyAccount,
|
||||
) : AuthStateStorage {
|
||||
override fun getAuthorizationState(): String? {
|
||||
return account.oAuthState
|
||||
}
|
||||
|
||||
override fun updateAuthorizationState(authorizationState: String?) {
|
||||
account.oAuthState = authorizationState
|
||||
accountManager.saveAccount(account)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.SystemClock
|
||||
import androidx.core.app.AlarmManagerCompat
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.fsck.k9.backend.imap.SystemAlarmManager
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import net.thunderbird.core.logging.legacy.Log
|
||||
|
||||
private const val ALARM_ACTION = "com.fsck.k9.backends.ALARM"
|
||||
private const val REQUEST_CODE = 1
|
||||
|
||||
private typealias Callback = () -> Unit
|
||||
|
||||
class AndroidAlarmManager(
|
||||
private val context: Context,
|
||||
private val alarmManager: AlarmManager,
|
||||
backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) : SystemAlarmManager {
|
||||
private val coroutineScope = CoroutineScope(backgroundDispatcher)
|
||||
|
||||
private val pendingIntent: PendingIntent = run {
|
||||
val intent = Intent(ALARM_ACTION).apply {
|
||||
setPackage(context.packageName)
|
||||
}
|
||||
|
||||
PendingIntentCompat.getBroadcast(context, REQUEST_CODE, intent, 0, false)!!
|
||||
}
|
||||
|
||||
private val callback = AtomicReference<Callback?>(null)
|
||||
|
||||
init {
|
||||
val intentFilter = IntentFilter(ALARM_ACTION)
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val callback = callback.getAndSet(null)
|
||||
if (callback == null) {
|
||||
Log.w("Alarm triggered but 'callback' was null")
|
||||
} else {
|
||||
coroutineScope.launch {
|
||||
callback.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
intentFilter,
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||
)
|
||||
}
|
||||
|
||||
override fun setAlarm(triggerTime: Long, callback: Callback) {
|
||||
this.callback.set(callback)
|
||||
|
||||
AlarmManagerCompat.setExactAndAllowWhileIdle(
|
||||
alarmManager,
|
||||
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||
triggerTime,
|
||||
pendingIntent,
|
||||
)
|
||||
}
|
||||
|
||||
override fun cancelAlarm() {
|
||||
callback.set(null)
|
||||
alarmManager.cancel(pendingIntent)
|
||||
}
|
||||
|
||||
override fun now(): Long = SystemClock.elapsedRealtime()
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.backend.BackendFactory
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.backend.imap.ImapBackend
|
||||
import com.fsck.k9.backend.imap.ImapPushConfigProvider
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.power.PowerManager
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
import com.fsck.k9.mail.store.imap.IdleRefreshManager
|
||||
import com.fsck.k9.mail.store.imap.ImapClientInfo
|
||||
import com.fsck.k9.mail.store.imap.ImapStore
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreConfig
|
||||
import com.fsck.k9.mail.transport.smtp.SmtpTransport
|
||||
import com.fsck.k9.mailstore.K9BackendStorageFactory
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import net.thunderbird.core.android.account.AccountManager
|
||||
import net.thunderbird.core.android.account.Expunge
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
class ImapBackendFactory(
|
||||
private val accountManager: AccountManager,
|
||||
private val powerManager: PowerManager,
|
||||
private val idleRefreshManager: IdleRefreshManager,
|
||||
private val backendStorageFactory: K9BackendStorageFactory,
|
||||
private val trustedSocketFactory: TrustedSocketFactory,
|
||||
private val context: Context,
|
||||
private val clientInfoAppName: String,
|
||||
private val clientInfoAppVersion: String,
|
||||
) : BackendFactory {
|
||||
override fun createBackend(account: LegacyAccount): Backend {
|
||||
val accountName = account.displayName
|
||||
val backendStorage = backendStorageFactory.createBackendStorage(account)
|
||||
val imapStore = createImapStore(account)
|
||||
val pushConfigProvider = createPushConfigProvider(account)
|
||||
val smtpTransport = createSmtpTransport(account)
|
||||
|
||||
return ImapBackend(
|
||||
accountName,
|
||||
backendStorage,
|
||||
imapStore,
|
||||
powerManager,
|
||||
idleRefreshManager,
|
||||
pushConfigProvider,
|
||||
smtpTransport,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createImapStore(account: LegacyAccount): ImapStore {
|
||||
val serverSettings = account.toImapServerSettings()
|
||||
|
||||
val oAuth2TokenProvider = if (serverSettings.authenticationType == AuthType.XOAUTH2) {
|
||||
createOAuth2TokenProvider(account)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val config = createImapStoreConfig(account)
|
||||
return ImapStore.create(
|
||||
serverSettings,
|
||||
config,
|
||||
trustedSocketFactory,
|
||||
oAuth2TokenProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createImapStoreConfig(account: LegacyAccount): ImapStoreConfig {
|
||||
return object : ImapStoreConfig {
|
||||
override val logLabel
|
||||
get() = account.uuid
|
||||
|
||||
override fun isSubscribedFoldersOnly() = account.isSubscribedFoldersOnly
|
||||
|
||||
override fun isExpungeImmediately() = account.expungePolicy == Expunge.EXPUNGE_IMMEDIATELY
|
||||
|
||||
override fun clientInfo() = ImapClientInfo(appName = clientInfoAppName, appVersion = clientInfoAppVersion)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSmtpTransport(account: LegacyAccount): SmtpTransport {
|
||||
val serverSettings = account.outgoingServerSettings
|
||||
val oauth2TokenProvider = if (serverSettings.authenticationType == AuthType.XOAUTH2) {
|
||||
createOAuth2TokenProvider(account)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return SmtpTransport(serverSettings, trustedSocketFactory, oauth2TokenProvider)
|
||||
}
|
||||
|
||||
private fun createOAuth2TokenProvider(account: LegacyAccount): RealOAuth2TokenProvider {
|
||||
val authStateStorage = AccountAuthStateStorage(accountManager, account)
|
||||
return RealOAuth2TokenProvider(context, authStateStorage)
|
||||
}
|
||||
|
||||
private fun createPushConfigProvider(account: LegacyAccount) = object : ImapPushConfigProvider {
|
||||
override val maxPushFoldersFlow: Flow<Int>
|
||||
get() = accountManager.getAccountFlow(account.uuid)
|
||||
.filterNotNull()
|
||||
.map { it.maxPushFolders }
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val idleRefreshMinutesFlow: Flow<Int>
|
||||
get() = accountManager.getAccountFlow(account.uuid)
|
||||
.filterNotNull()
|
||||
.map { it.idleRefreshMinutes }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreSettings
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreSettings.autoDetectNamespace
|
||||
import com.fsck.k9.mail.store.imap.ImapStoreSettings.pathPrefix
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
|
||||
fun LegacyAccount.toImapServerSettings(): ServerSettings {
|
||||
val serverSettings = incomingServerSettings
|
||||
return serverSettings.copy(
|
||||
extra = ImapStoreSettings.createExtra(
|
||||
autoDetectNamespace = serverSettings.autoDetectNamespace,
|
||||
pathPrefix = serverSettings.pathPrefix,
|
||||
useCompression = useCompression,
|
||||
sendClientInfo = isSendClientInfoEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import com.fsck.k9.backend.BackendManager
|
||||
import com.fsck.k9.backend.imap.BackendIdleRefreshManager
|
||||
import com.fsck.k9.backend.imap.SystemAlarmManager
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory
|
||||
import com.fsck.k9.mail.store.imap.IdleRefreshManager
|
||||
import net.thunderbird.backend.api.BackendFactory
|
||||
import net.thunderbird.backend.api.folder.RemoteFolderCreator
|
||||
import net.thunderbird.backend.imap.ImapRemoteFolderCreatorFactory
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import com.fsck.k9.backend.BackendFactory as LegacyBackendFactory
|
||||
|
||||
val backendsModule = module {
|
||||
single {
|
||||
val developmentBackends = get<Map<String, LegacyBackendFactory>>(named("developmentBackends"))
|
||||
BackendManager(
|
||||
mapOf(
|
||||
"imap" to get<ImapBackendFactory>(),
|
||||
"pop3" to get<Pop3BackendFactory>(),
|
||||
) + developmentBackends,
|
||||
)
|
||||
}
|
||||
single {
|
||||
ImapBackendFactory(
|
||||
accountManager = get(),
|
||||
powerManager = get(),
|
||||
idleRefreshManager = get(),
|
||||
backendStorageFactory = get(),
|
||||
trustedSocketFactory = get(),
|
||||
context = get(),
|
||||
clientInfoAppName = get(named("ClientInfoAppName")),
|
||||
clientInfoAppVersion = get(named("ClientInfoAppVersion")),
|
||||
)
|
||||
}
|
||||
|
||||
single<BackendFactory<*>>(named("imap")) {
|
||||
get<ImapBackendFactory>()
|
||||
}
|
||||
|
||||
single<RemoteFolderCreator.Factory>(named("imap")) {
|
||||
ImapRemoteFolderCreatorFactory(
|
||||
logger = get(),
|
||||
backendFactory = get(named("imap")),
|
||||
)
|
||||
}
|
||||
|
||||
single<SystemAlarmManager> { AndroidAlarmManager(context = get(), alarmManager = get()) }
|
||||
single<IdleRefreshManager> { BackendIdleRefreshManager(alarmManager = get()) }
|
||||
single { Pop3BackendFactory(get(), get()) }
|
||||
single<OAuth2TokenProviderFactory> { RealOAuth2TokenProviderFactory(context = get()) }
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import com.fsck.k9.backend.BackendFactory
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.backend.pop3.Pop3Backend
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
import com.fsck.k9.mail.store.pop3.Pop3Store
|
||||
import com.fsck.k9.mail.transport.smtp.SmtpTransport
|
||||
import com.fsck.k9.mailstore.K9BackendStorageFactory
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
|
||||
class Pop3BackendFactory(
|
||||
private val backendStorageFactory: K9BackendStorageFactory,
|
||||
private val trustedSocketFactory: TrustedSocketFactory,
|
||||
) : BackendFactory {
|
||||
override fun createBackend(account: LegacyAccount): Backend {
|
||||
val accountName = account.displayName
|
||||
val backendStorage = backendStorageFactory.createBackendStorage(account)
|
||||
val pop3Store = createPop3Store(account)
|
||||
val smtpTransport = createSmtpTransport(account)
|
||||
return Pop3Backend(accountName, backendStorage, pop3Store, smtpTransport)
|
||||
}
|
||||
|
||||
private fun createPop3Store(account: LegacyAccount): Pop3Store {
|
||||
val serverSettings = account.incomingServerSettings
|
||||
return Pop3Store(serverSettings, trustedSocketFactory)
|
||||
}
|
||||
|
||||
private fun createSmtpTransport(account: LegacyAccount): SmtpTransport {
|
||||
val serverSettings = account.outgoingServerSettings
|
||||
val oauth2TokenProvider: OAuth2TokenProvider? = null
|
||||
return SmtpTransport(serverSettings, trustedSocketFactory, oauth2TokenProvider)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.mail.AuthenticationFailedException
|
||||
import com.fsck.k9.mail.oauth.AuthStateStorage
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import net.openid.appauth.AuthState
|
||||
import net.openid.appauth.AuthorizationException
|
||||
import net.openid.appauth.AuthorizationException.AuthorizationRequestErrors
|
||||
import net.openid.appauth.AuthorizationException.GeneralErrors
|
||||
import net.openid.appauth.AuthorizationService
|
||||
import net.thunderbird.core.logging.legacy.Log
|
||||
|
||||
class RealOAuth2TokenProvider(
|
||||
context: Context,
|
||||
private val authStateStorage: AuthStateStorage,
|
||||
) : OAuth2TokenProvider {
|
||||
private val authService = AuthorizationService(context)
|
||||
private var requestFreshToken = false
|
||||
|
||||
override val primaryEmail: String?
|
||||
get() {
|
||||
return parseAuthState()
|
||||
.parsedIdToken
|
||||
?.additionalClaims
|
||||
?.get("email")
|
||||
?.toString()
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override fun getToken(timeoutMillis: Long): String {
|
||||
val latch = CountDownLatch(1)
|
||||
var token: String? = null
|
||||
var exception: AuthorizationException? = null
|
||||
|
||||
val authState = parseAuthState()
|
||||
if (requestFreshToken) {
|
||||
authState.needsTokenRefresh = true
|
||||
}
|
||||
|
||||
val oldAccessToken = authState.accessToken
|
||||
|
||||
try {
|
||||
authState.performActionWithFreshTokens(
|
||||
authService,
|
||||
) { accessToken: String?, _, authException: AuthorizationException? ->
|
||||
token = accessToken
|
||||
exception = authException
|
||||
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
latch.await(timeoutMillis, TimeUnit.MILLISECONDS)
|
||||
} catch (e: Exception) {
|
||||
Log.w(e, "Failed to fetch an access token. Clearing authorization state.")
|
||||
|
||||
authStateStorage.updateAuthorizationState(authorizationState = null)
|
||||
|
||||
throw AuthenticationFailedException(
|
||||
message = "Failed to fetch an access token",
|
||||
throwable = e,
|
||||
)
|
||||
}
|
||||
|
||||
val authException = exception
|
||||
if (authException == GeneralErrors.NETWORK_ERROR ||
|
||||
authException == GeneralErrors.SERVER_ERROR ||
|
||||
authException == AuthorizationRequestErrors.SERVER_ERROR ||
|
||||
authException == AuthorizationRequestErrors.TEMPORARILY_UNAVAILABLE
|
||||
) {
|
||||
throw IOException("Error while fetching an access token", authException)
|
||||
} else if (authException != null) {
|
||||
authStateStorage.updateAuthorizationState(authorizationState = null)
|
||||
|
||||
throw AuthenticationFailedException(
|
||||
message = "Failed to fetch an access token",
|
||||
throwable = authException,
|
||||
messageFromServer = authException.error,
|
||||
)
|
||||
} else if (token != oldAccessToken) {
|
||||
requestFreshToken = false
|
||||
authStateStorage.updateAuthorizationState(authorizationState = authState.jsonSerializeString())
|
||||
}
|
||||
|
||||
return token ?: throw AuthenticationFailedException("Failed to fetch an access token")
|
||||
}
|
||||
|
||||
override fun invalidateToken() {
|
||||
requestFreshToken = true
|
||||
}
|
||||
|
||||
private fun parseAuthState(): AuthState {
|
||||
return authStateStorage
|
||||
.getAuthorizationState()
|
||||
?.let { AuthState.jsonDeserialize(it) }
|
||||
?: throw AuthenticationFailedException("Login required")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.mail.oauth.AuthStateStorage
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProviderFactory
|
||||
|
||||
class RealOAuth2TokenProviderFactory(
|
||||
private val context: Context,
|
||||
) : OAuth2TokenProviderFactory {
|
||||
override fun create(authStateStorage: AuthStateStorage): OAuth2TokenProvider {
|
||||
return RealOAuth2TokenProvider(context, authStateStorage)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.fsck.k9.glide;
|
||||
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
|
||||
@GlideModule
|
||||
public class K9AppGlideModule extends AppGlideModule {
|
||||
}
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
package com.fsck.k9.notification
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import app.k9mail.feature.launcher.FeatureLauncherActivity
|
||||
import app.k9mail.feature.launcher.FeatureLauncherTarget
|
||||
import app.k9mail.legacy.mailstore.MessageStoreManager
|
||||
import app.k9mail.legacy.message.controller.MessageReference
|
||||
import com.fsck.k9.K9
|
||||
import com.fsck.k9.activity.MessageList
|
||||
import com.fsck.k9.activity.compose.MessageActions
|
||||
import com.fsck.k9.ui.messagelist.DefaultFolderProvider
|
||||
import com.fsck.k9.ui.notification.DeleteConfirmationActivity
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
import net.thunderbird.core.preference.GeneralSettingsManager
|
||||
import net.thunderbird.feature.search.legacy.LocalMessageSearch
|
||||
|
||||
/**
|
||||
* This class contains methods to create the [PendingIntent]s for the actions of our notifications.
|
||||
*
|
||||
* **Note:**
|
||||
* We need to take special care to ensure the `PendingIntent`s are unique as defined in the documentation of
|
||||
* [PendingIntent]. Otherwise selecting a notification action might perform the action on the wrong message.
|
||||
*
|
||||
* We add unique values to `Intent.data` so we end up with unique `PendingIntent`s.
|
||||
*
|
||||
* In the past we've used the notification ID as `requestCode` argument when creating a `PendingIntent`. But since we're
|
||||
* reusing notification IDs, it's safer to make sure the `Intent` itself is unique.
|
||||
*/
|
||||
internal class K9NotificationActionCreator(
|
||||
private val context: Context,
|
||||
private val defaultFolderProvider: DefaultFolderProvider,
|
||||
private val messageStoreManager: MessageStoreManager,
|
||||
private val generalSettingsManager: GeneralSettingsManager,
|
||||
) : NotificationActionCreator {
|
||||
|
||||
override fun createViewMessagePendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val openInUnifiedInbox =
|
||||
generalSettingsManager.getConfig().display.inboxSettings.isShowUnifiedInbox &&
|
||||
isIncludedInUnifiedInbox(messageReference)
|
||||
val intent = createMessageViewIntent(messageReference, openInUnifiedInbox)
|
||||
|
||||
return PendingIntentCompat.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createViewFolderPendingIntent(account: LegacyAccount, folderId: Long): PendingIntent {
|
||||
val intent = createMessageListIntent(account, folderId)
|
||||
return PendingIntentCompat.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createViewMessagesPendingIntent(
|
||||
account: LegacyAccount,
|
||||
messageReferences: List<MessageReference>,
|
||||
): PendingIntent {
|
||||
val folderIds = extractFolderIds(messageReferences)
|
||||
|
||||
val intent = if (generalSettingsManager.getConfig()
|
||||
.display
|
||||
.inboxSettings
|
||||
.isShowUnifiedInbox &&
|
||||
areAllIncludedInUnifiedInbox(account, folderIds)
|
||||
) {
|
||||
createUnifiedInboxIntent(account)
|
||||
} else if (folderIds.size == 1) {
|
||||
createMessageListIntent(account, folderIds.first())
|
||||
} else {
|
||||
createNewMessagesIntent(account)
|
||||
}
|
||||
return PendingIntentCompat.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createViewFolderListPendingIntent(account: LegacyAccount): PendingIntent {
|
||||
val intent = createMessageListIntent(account)
|
||||
return PendingIntentCompat.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createDismissAllMessagesPendingIntent(account: LegacyAccount): PendingIntent {
|
||||
val intent = NotificationActionService.createDismissAllMessagesIntent(context, account).apply {
|
||||
data = Uri.parse("data:,dismissAll/${account.uuid}/${System.currentTimeMillis()}")
|
||||
}
|
||||
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createDismissMessagePendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = NotificationActionService.createDismissMessageIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,dismiss/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createReplyPendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = MessageActions.getActionReplyIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,reply/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntentCompat.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createMarkMessageAsReadPendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = NotificationActionService.createMarkMessageAsReadIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,markAsRead/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createMarkAllAsReadPendingIntent(
|
||||
account: LegacyAccount,
|
||||
messageReferences: List<MessageReference>,
|
||||
): PendingIntent {
|
||||
val accountUuid = account.uuid
|
||||
val intent =
|
||||
NotificationActionService.createMarkAllAsReadIntent(context, accountUuid, messageReferences).apply {
|
||||
data = Uri.parse("data:,markAllAsRead/$accountUuid/${System.currentTimeMillis()}")
|
||||
}
|
||||
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun getEditIncomingServerSettingsIntent(account: LegacyAccount): PendingIntent {
|
||||
val intent = FeatureLauncherActivity.getIntent(
|
||||
context = context,
|
||||
target = FeatureLauncherTarget.AccountEditIncomingSettings(account.uuid),
|
||||
)
|
||||
return PendingIntentCompat.getActivity(context, account.accountNumber, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun getEditOutgoingServerSettingsIntent(account: LegacyAccount): PendingIntent {
|
||||
val intent = FeatureLauncherActivity.getIntent(
|
||||
context = context,
|
||||
target = FeatureLauncherTarget.AccountEditOutgoingSettings(account.uuid),
|
||||
)
|
||||
return PendingIntentCompat.getActivity(context, account.accountNumber, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createDeleteMessagePendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
return if (K9.isConfirmDeleteFromNotification) {
|
||||
createDeleteConfirmationPendingIntent(messageReference)
|
||||
} else {
|
||||
createDeleteServicePendingIntent(messageReference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDeleteServicePendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = NotificationActionService.createDeleteMessageIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,delete/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
private fun createDeleteConfirmationPendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = DeleteConfirmationActivity.getIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,deleteConfirmation/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntentCompat.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createDeleteAllPendingIntent(
|
||||
account: LegacyAccount,
|
||||
messageReferences: List<MessageReference>,
|
||||
): PendingIntent {
|
||||
return if (K9.isConfirmDeleteFromNotification) {
|
||||
getDeleteAllConfirmationPendingIntent(messageReferences)
|
||||
} else {
|
||||
getDeleteAllServicePendingIntent(account, messageReferences)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeleteAllConfirmationPendingIntent(messageReferences: List<MessageReference>): PendingIntent {
|
||||
val intent = DeleteConfirmationActivity.getIntent(context, messageReferences).apply {
|
||||
data = Uri.parse("data:,deleteAllConfirmation/${System.currentTimeMillis()}")
|
||||
}
|
||||
return PendingIntentCompat.getActivity(context, 0, intent, FLAG_CANCEL_CURRENT, false)!!
|
||||
}
|
||||
|
||||
private fun getDeleteAllServicePendingIntent(
|
||||
account: LegacyAccount,
|
||||
messageReferences: List<MessageReference>,
|
||||
): PendingIntent {
|
||||
val accountUuid = account.uuid
|
||||
val intent =
|
||||
NotificationActionService.createDeleteAllMessagesIntent(context, accountUuid, messageReferences).apply {
|
||||
data = Uri.parse("data:,deleteAll/$accountUuid/${System.currentTimeMillis()}")
|
||||
}
|
||||
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createArchiveMessagePendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = NotificationActionService.createArchiveMessageIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,archive/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createArchiveAllPendingIntent(
|
||||
account: LegacyAccount,
|
||||
messageReferences: List<MessageReference>,
|
||||
): PendingIntent {
|
||||
val intent = NotificationActionService.createArchiveAllIntent(context, account, messageReferences).apply {
|
||||
data = Uri.parse("data:,archiveAll/${account.uuid}/${System.currentTimeMillis()}")
|
||||
}
|
||||
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
override fun createMarkMessageAsSpamPendingIntent(messageReference: MessageReference): PendingIntent {
|
||||
val intent = NotificationActionService.createMarkMessageAsSpamIntent(context, messageReference).apply {
|
||||
data = Uri.parse("data:,spam/${messageReference.toIdentityString()}")
|
||||
}
|
||||
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
|
||||
}
|
||||
|
||||
private fun createMessageListIntent(account: LegacyAccount): Intent {
|
||||
val folderId = defaultFolderProvider.getDefaultFolder(account)
|
||||
val search = LocalMessageSearch().apply {
|
||||
addAllowedFolder(folderId)
|
||||
addAccountUuid(account.uuid)
|
||||
}
|
||||
|
||||
return MessageList.intentDisplaySearch(
|
||||
context = context,
|
||||
search = search,
|
||||
noThreading = false,
|
||||
newTask = true,
|
||||
clearTop = true,
|
||||
).apply {
|
||||
data = Uri.parse("data:,messageList/${account.uuid}/$folderId")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMessageListIntent(account: LegacyAccount, folderId: Long): Intent {
|
||||
val search = LocalMessageSearch().apply {
|
||||
addAllowedFolder(folderId)
|
||||
addAccountUuid(account.uuid)
|
||||
}
|
||||
|
||||
return MessageList.intentDisplaySearch(
|
||||
context = context,
|
||||
search = search,
|
||||
noThreading = false,
|
||||
newTask = true,
|
||||
clearTop = true,
|
||||
).apply {
|
||||
data = Uri.parse("data:,messageList/${account.uuid}/$folderId")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMessageViewIntent(messageReference: MessageReference, openInUnifiedInbox: Boolean): Intent {
|
||||
return MessageList.actionDisplayMessageIntent(context, messageReference, openInUnifiedInbox).apply {
|
||||
data = Uri.parse("data:,messageView/${messageReference.toIdentityString()}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createUnifiedInboxIntent(account: LegacyAccount): Intent {
|
||||
return MessageList.createUnifiedInboxIntent(context, account).apply {
|
||||
data = Uri.parse("data:,unifiedInbox/${account.uuid}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNewMessagesIntent(account: LegacyAccount): Intent {
|
||||
return MessageList.createNewMessagesIntent(context, account).apply {
|
||||
data = Uri.parse("data:,newMessages/${account.uuid}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractFolderIds(messageReferences: List<MessageReference>): Set<Long> {
|
||||
return messageReferences.asSequence().map { it.folderId }.toSet()
|
||||
}
|
||||
|
||||
private fun areAllIncludedInUnifiedInbox(account: LegacyAccount, folderIds: Collection<Long>): Boolean {
|
||||
val messageStore = messageStoreManager.getMessageStore(account)
|
||||
return messageStore.areAllIncludedInUnifiedInbox(folderIds)
|
||||
}
|
||||
|
||||
private fun isIncludedInUnifiedInbox(messageReference: MessageReference): Boolean {
|
||||
val messageStore = messageStoreManager.getMessageStore(messageReference.accountUuid)
|
||||
return messageStore.areAllIncludedInUnifiedInbox(listOf(messageReference.folderId))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package com.fsck.k9.notification
|
||||
|
||||
import android.content.Context
|
||||
import app.k9mail.core.ui.legacy.designsystem.atom.icon.Icons
|
||||
import com.fsck.k9.ui.R
|
||||
|
||||
class K9NotificationResourceProvider(private val context: Context) : NotificationResourceProvider {
|
||||
override val iconWarning: Int = Icons.Outlined.Warning
|
||||
override val iconMarkAsRead: Int = Icons.Outlined.MarkEmailRead
|
||||
override val iconDelete: Int = Icons.Outlined.Delete
|
||||
override val iconReply: Int = Icons.Outlined.Reply
|
||||
override val iconNewMail: Int = Icons.Outlined.MarkEmailUnread
|
||||
override val iconSendingMail: Int = Icons.Outlined.Sync
|
||||
override val iconCheckingMail: Int = Icons.Outlined.Sync
|
||||
override val iconBackgroundWorkNotification: Int = Icons.Outlined.Bolt
|
||||
override val wearIconMarkAsRead: Int = Icons.Outlined.MarkEmailRead
|
||||
override val wearIconDelete: Int = Icons.Outlined.Delete
|
||||
override val wearIconArchive: Int = Icons.Outlined.Archive
|
||||
override val wearIconReplyAll: Int = Icons.Outlined.Reply
|
||||
override val wearIconMarkAsSpam: Int = Icons.Outlined.Report
|
||||
|
||||
override val pushChannelName: String
|
||||
get() = context.getString(R.string.notification_channel_push_title)
|
||||
override val pushChannelDescription: String
|
||||
get() = context.getString(R.string.notification_channel_push_description)
|
||||
override val messagesChannelName: String
|
||||
get() = context.getString(R.string.notification_channel_messages_title)
|
||||
override val messagesChannelDescription: String
|
||||
get() = context.getString(R.string.notification_channel_messages_description)
|
||||
override val miscellaneousChannelName: String
|
||||
get() = context.getString(R.string.notification_channel_miscellaneous_title)
|
||||
override val miscellaneousChannelDescription: String
|
||||
get() = context.getString(R.string.notification_channel_miscellaneous_description)
|
||||
|
||||
override fun authenticationErrorTitle(): String =
|
||||
context.getString(R.string.notification_authentication_error_title)
|
||||
|
||||
override fun authenticationErrorBody(accountName: String): String =
|
||||
context.getString(R.string.notification_authentication_error_text, accountName)
|
||||
|
||||
override fun notifyErrorTitle(): String = context.getString(R.string.notification_notify_error_title)
|
||||
|
||||
override fun notifyErrorText(): String = context.getString(R.string.notification_notify_error_text)
|
||||
|
||||
override fun certificateErrorTitle(): String = context.getString(R.string.notification_certificate_error_public)
|
||||
|
||||
override fun certificateErrorTitle(accountName: String): String =
|
||||
context.getString(R.string.notification_certificate_error_title, accountName)
|
||||
|
||||
override fun certificateErrorBody(): String = context.getString(R.string.notification_certificate_error_text)
|
||||
|
||||
override fun newMessagesTitle(newMessagesCount: Int): String {
|
||||
return context.resources.getQuantityString(
|
||||
R.plurals.notification_new_messages_title,
|
||||
newMessagesCount,
|
||||
newMessagesCount,
|
||||
)
|
||||
}
|
||||
|
||||
override fun additionalMessages(overflowMessagesCount: Int, accountName: String): String =
|
||||
context.getString(R.string.notification_additional_messages, overflowMessagesCount, accountName)
|
||||
|
||||
override fun previewEncrypted(): String = context.getString(R.string.preview_encrypted)
|
||||
|
||||
override fun noSubject(): String = context.getString(R.string.general_no_subject)
|
||||
|
||||
override fun recipientDisplayName(recipientDisplayName: String): String =
|
||||
context.getString(R.string.message_to_fmt, recipientDisplayName)
|
||||
|
||||
override fun noSender(): String = context.getString(R.string.general_no_sender)
|
||||
|
||||
override fun sendFailedTitle(): String = context.getString(R.string.send_failure_subject)
|
||||
|
||||
override fun sendingMailTitle(): String = context.getString(R.string.notification_bg_send_title)
|
||||
|
||||
override fun sendingMailBody(accountName: String): String =
|
||||
context.getString(R.string.notification_bg_send_ticker, accountName)
|
||||
|
||||
override fun checkingMailTicker(accountName: String, folderName: String): String =
|
||||
context.getString(R.string.notification_bg_sync_ticker, accountName, folderName)
|
||||
|
||||
override fun checkingMailTitle(): String = context.getString(R.string.notification_bg_sync_title)
|
||||
|
||||
override fun checkingMailSeparator(): String = context.getString(R.string.notification_bg_title_separator)
|
||||
|
||||
override fun actionMarkAsRead(): String = context.getString(R.string.notification_action_mark_as_read)
|
||||
|
||||
override fun actionMarkAllAsRead(): String = context.getString(R.string.notification_action_mark_all_as_read)
|
||||
|
||||
override fun actionDelete(): String = context.getString(R.string.notification_action_delete)
|
||||
|
||||
override fun actionDeleteAll(): String = context.getString(R.string.notification_action_delete_all)
|
||||
|
||||
override fun actionReply(): String = context.getString(R.string.notification_action_reply)
|
||||
|
||||
override fun actionArchive(): String = context.getString(R.string.notification_action_archive)
|
||||
|
||||
override fun actionArchiveAll(): String = context.getString(R.string.notification_action_archive_all)
|
||||
|
||||
override fun actionMarkAsSpam(): String = context.getString(R.string.notification_action_spam)
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.fsck.k9.notification
|
||||
|
||||
import app.k9mail.core.android.common.contact.ContactRepository
|
||||
import app.k9mail.legacy.di.DI
|
||||
import com.fsck.k9.K9
|
||||
import com.fsck.k9.QuietTimeChecker
|
||||
import com.fsck.k9.mail.Flag
|
||||
import com.fsck.k9.mail.K9MailLib
|
||||
import com.fsck.k9.mail.Message
|
||||
import com.fsck.k9.mailstore.LocalFolder
|
||||
import com.fsck.k9.mailstore.LocalMessage
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.ExperimentalTime
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
import net.thunderbird.core.common.mail.toEmailAddressOrNull
|
||||
import net.thunderbird.core.logging.legacy.Log
|
||||
import net.thunderbird.core.preference.GeneralSettingsManager
|
||||
import net.thunderbird.core.preference.notification.NotificationPreference
|
||||
|
||||
class K9NotificationStrategy(
|
||||
private val contactRepository: ContactRepository,
|
||||
private val generalSettingsManager: GeneralSettingsManager,
|
||||
) : NotificationStrategy {
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override fun shouldNotifyForMessage(
|
||||
account: LegacyAccount,
|
||||
localFolder: LocalFolder,
|
||||
message: LocalMessage,
|
||||
isOldMessage: Boolean,
|
||||
): Boolean {
|
||||
if (!K9.isNotificationDuringQuietTimeEnabled && generalSettingsManager.getConfig().notification.isQuietTime) {
|
||||
Log.v("No notification: Quiet time is active")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!account.isNotifyNewMail) {
|
||||
Log.v("No notification: Notifications are disabled")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!localFolder.isVisible) {
|
||||
Log.v("No notification: Message is in folder not being displayed")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!localFolder.isNotificationsEnabled) {
|
||||
Log.v("No notification: Notifications are not enabled for this folder")
|
||||
return false
|
||||
}
|
||||
|
||||
if (isOldMessage) {
|
||||
Log.v("No notification: Message is old")
|
||||
return false
|
||||
}
|
||||
|
||||
if (message.isSet(Flag.SEEN)) {
|
||||
Log.v("No notification: Message is marked as read")
|
||||
return false
|
||||
}
|
||||
|
||||
if (account.isIgnoreChatMessages && message.isChatMessage) {
|
||||
Log.v("No notification: Notifications for chat messages are disabled")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!account.isNotifySelfNewMail && account.isAnIdentity(message.from)) {
|
||||
Log.v("No notification: Notifications for messages from yourself are disabled")
|
||||
return false
|
||||
}
|
||||
|
||||
if (account.isNotifyContactsMailOnly &&
|
||||
!contactRepository.hasAnyContactFor(message.from.asList().mapNotNull { it.address.toEmailAddressOrNull() })
|
||||
) {
|
||||
Log.v("No notification: Message is not from a known contact")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private val Message.isChatMessage: Boolean
|
||||
get() = getHeader(K9MailLib.CHAT_HEADER).isNotEmpty()
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
private val NotificationPreference.isQuietTime: Boolean
|
||||
get() {
|
||||
val clock = DI.get<Clock>()
|
||||
val quietTimeChecker = QuietTimeChecker(
|
||||
clock = clock,
|
||||
quietTimeStart = quietTimeStarts,
|
||||
quietTimeEnd = quietTimeEnds,
|
||||
)
|
||||
return quietTimeChecker.isQuietTime
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.fsck.k9.notification
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
val notificationModule = module {
|
||||
single<NotificationActionCreator> {
|
||||
K9NotificationActionCreator(
|
||||
context = get(),
|
||||
defaultFolderProvider = get(),
|
||||
messageStoreManager = get(),
|
||||
generalSettingsManager = get(),
|
||||
)
|
||||
}
|
||||
single<NotificationResourceProvider> { K9NotificationResourceProvider(get()) }
|
||||
single<NotificationStrategy> { K9NotificationStrategy(get(), generalSettingsManager = get()) }
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.fsck.k9.resources
|
||||
|
||||
import android.content.Context
|
||||
import com.fsck.k9.autocrypt.AutocryptStringProvider
|
||||
import com.fsck.k9.ui.R
|
||||
|
||||
class K9AutocryptStringProvider(private val context: Context) : AutocryptStringProvider {
|
||||
override fun transferMessageSubject(): String = context.getString(R.string.ac_transfer_msg_subject)
|
||||
override fun transferMessageBody(): String = context.getString(R.string.ac_transfer_msg_body)
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.fsck.k9.resources
|
||||
|
||||
import android.content.Context
|
||||
import app.k9mail.core.ui.legacy.designsystem.atom.icon.Icons
|
||||
import com.fsck.k9.CoreResourceProvider
|
||||
import com.fsck.k9.notification.PushNotificationState
|
||||
import com.fsck.k9.ui.R
|
||||
|
||||
class K9CoreResourceProvider(
|
||||
private val context: Context,
|
||||
) : CoreResourceProvider {
|
||||
override fun defaultIdentityDescription(): String = context.getString(R.string.default_identity_description)
|
||||
|
||||
override fun contactDisplayNamePrefix(): String = context.getString(R.string.message_to_label)
|
||||
override fun contactUnknownSender(): String = context.getString(R.string.unknown_sender)
|
||||
override fun contactUnknownRecipient(): String = context.getString(R.string.unknown_recipient)
|
||||
|
||||
override fun messageHeaderFrom(): String = context.getString(R.string.message_compose_quote_header_from)
|
||||
override fun messageHeaderTo(): String = context.getString(R.string.message_compose_quote_header_to)
|
||||
override fun messageHeaderCc(): String = context.getString(R.string.message_compose_quote_header_cc)
|
||||
override fun messageHeaderDate(): String = context.getString(R.string.message_compose_quote_header_send_date)
|
||||
override fun messageHeaderSubject(): String = context.getString(R.string.message_compose_quote_header_subject)
|
||||
override fun messageHeaderSeparator(): String = context.getString(R.string.message_compose_quote_header_separator)
|
||||
|
||||
override fun noSubject(): String = context.getString(R.string.general_no_subject)
|
||||
|
||||
override fun userAgent(): String = context.getString(R.string.message_header_mua)
|
||||
|
||||
override fun replyHeader(sender: String): String =
|
||||
context.getString(R.string.message_compose_reply_header_fmt, sender)
|
||||
|
||||
override fun replyHeader(sender: String, sentDate: String): String =
|
||||
context.getString(R.string.message_compose_reply_header_fmt_with_date, sentDate, sender)
|
||||
|
||||
override fun searchUnifiedInboxTitle(): String = context.getString(R.string.integrated_inbox_title)
|
||||
override fun searchUnifiedInboxDetail(): String = context.getString(R.string.integrated_inbox_detail)
|
||||
|
||||
override val iconPushNotification: Int = Icons.Outlined.Notifications
|
||||
|
||||
override fun pushNotificationText(notificationState: PushNotificationState): String {
|
||||
val resId = when (notificationState) {
|
||||
PushNotificationState.INITIALIZING -> R.string.push_notification_state_initializing
|
||||
PushNotificationState.LISTENING -> R.string.push_notification_state_listening
|
||||
PushNotificationState.WAIT_BACKGROUND_SYNC -> R.string.push_notification_state_wait_background_sync
|
||||
PushNotificationState.WAIT_NETWORK -> R.string.push_notification_state_wait_network
|
||||
PushNotificationState.ALARM_PERMISSION_MISSING -> R.string.push_notification_state_alarm_permission_missing
|
||||
}
|
||||
return context.getString(resId)
|
||||
}
|
||||
|
||||
override fun pushNotificationInfoText(): String = context.getString(R.string.push_notification_info)
|
||||
|
||||
override fun pushNotificationGrantAlarmPermissionText(): String =
|
||||
context.getString(R.string.push_notification_grant_alarm_permission)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.fsck.k9.resources
|
||||
|
||||
import com.fsck.k9.CoreResourceProvider
|
||||
import com.fsck.k9.autocrypt.AutocryptStringProvider
|
||||
import org.koin.dsl.module
|
||||
|
||||
val resourcesModule = module {
|
||||
single<CoreResourceProvider> {
|
||||
K9CoreResourceProvider(
|
||||
context = get(),
|
||||
)
|
||||
}
|
||||
single<AutocryptStringProvider> {
|
||||
K9AutocryptStringProvider(
|
||||
context = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
13
legacy/common/src/main/res/values/manifest_values.xml
Normal file
13
legacy/common/src/main/res/values/manifest_values.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!--
|
||||
We'd like to disable this component by default. However, due to a bug in Android versions prior to 12, users then
|
||||
wouldn't be able to use the home screen widget.
|
||||
See https://android.googlesource.com/platform/frameworks/base/+/85be035336af8d83eb24980026418207c85991cb%5E%21/#F0
|
||||
|
||||
Previously, we've set this value to false on Android 12+ devices. However, a few people have reported widgets
|
||||
disappearing after updating to an app version containing that change. So for now we're back to having widgets
|
||||
enabled by default on all Android versions. We'll try again for Android versions released in or after 2025.
|
||||
-->
|
||||
<bool name="home_screen_widgets_enabled">true</bool>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package com.fsck.k9.account
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.AuthorizationState
|
||||
import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterFailure
|
||||
import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterResult
|
||||
import assertk.all
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isInstanceOf
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.prop
|
||||
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.FakeAccountData.ACCOUNT_ID_RAW
|
||||
import net.thunderbird.core.logging.legacy.Log
|
||||
import net.thunderbird.core.logging.testing.TestLogger
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import net.thunderbird.core.android.account.LegacyAccount as K9Account
|
||||
|
||||
class AccountServerSettingsUpdaterTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Log.logger = TestLogger()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateServerSettings() SHOULD return account not found exception WHEN none present with uuid`() = runTest {
|
||||
val accountManager = FakeAccountManager(accounts = mutableMapOf())
|
||||
val testSubject = AccountServerSettingsUpdater(accountManager)
|
||||
|
||||
val result = testSubject.updateServerSettings(
|
||||
accountUuid = ACCOUNT_ID_RAW,
|
||||
isIncoming = true,
|
||||
serverSettings = INCOMING_SERVER_SETTINGS,
|
||||
authorizationState = AUTHORIZATION_STATE,
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
AccountUpdaterResult.Failure(
|
||||
error = AccountUpdaterFailure.AccountNotFound(ACCOUNT_ID_RAW),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateServerSettings() SHOULD return success with updated incoming settings WHEN is incoming`() = runTest {
|
||||
val accountManager = FakeAccountManager(
|
||||
accounts = mutableMapOf(ACCOUNT_ID_RAW to createAccount(ACCOUNT_ID_RAW)),
|
||||
)
|
||||
val updatedIncomingServerSettings = INCOMING_SERVER_SETTINGS.copy(port = 123)
|
||||
val updatedAuthorizationState = AuthorizationState("new")
|
||||
val testSubject = AccountServerSettingsUpdater(accountManager)
|
||||
|
||||
val result = testSubject.updateServerSettings(
|
||||
accountUuid = ACCOUNT_ID_RAW,
|
||||
isIncoming = true,
|
||||
serverSettings = updatedIncomingServerSettings,
|
||||
authorizationState = updatedAuthorizationState,
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(AccountUpdaterResult.Success(ACCOUNT_ID_RAW))
|
||||
|
||||
val k9Account = accountManager.getAccount(ACCOUNT_ID_RAW)
|
||||
assertThat(k9Account).isNotNull().all {
|
||||
prop(K9Account::incomingServerSettings).isEqualTo(updatedIncomingServerSettings)
|
||||
prop(K9Account::outgoingServerSettings).isEqualTo(OUTGOING_SERVER_SETTINGS)
|
||||
prop(K9Account::oAuthState).isEqualTo(updatedAuthorizationState.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateServerSettings() SHOULD return success with updated outgoing settings WHEN is not incoming`() = runTest {
|
||||
val accountManager = FakeAccountManager(
|
||||
accounts = mutableMapOf(ACCOUNT_ID_RAW to createAccount(ACCOUNT_ID_RAW)),
|
||||
)
|
||||
val updatedOutgoingServerSettings = OUTGOING_SERVER_SETTINGS.copy(port = 123)
|
||||
val updatedAuthorizationState = AuthorizationState("new")
|
||||
val testSubject = AccountServerSettingsUpdater(accountManager)
|
||||
|
||||
val result = testSubject.updateServerSettings(
|
||||
accountUuid = ACCOUNT_ID_RAW,
|
||||
isIncoming = false,
|
||||
serverSettings = updatedOutgoingServerSettings,
|
||||
authorizationState = updatedAuthorizationState,
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(AccountUpdaterResult.Success(ACCOUNT_ID_RAW))
|
||||
|
||||
val k9Account = accountManager.getAccount(ACCOUNT_ID_RAW)
|
||||
assertThat(k9Account).isNotNull().all {
|
||||
prop(K9Account::incomingServerSettings).isEqualTo(INCOMING_SERVER_SETTINGS)
|
||||
prop(K9Account::outgoingServerSettings).isEqualTo(updatedOutgoingServerSettings)
|
||||
prop(K9Account::oAuthState).isEqualTo(updatedAuthorizationState.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateServerSettings() SHOULD return unknown error when exception thrown`() = runTest {
|
||||
val accountManager = FakeAccountManager(
|
||||
accounts = mutableMapOf(ACCOUNT_ID_RAW to createAccount(ACCOUNT_ID_RAW)),
|
||||
isFailureOnSave = true,
|
||||
)
|
||||
val testSubject = AccountServerSettingsUpdater(accountManager)
|
||||
|
||||
val result = testSubject.updateServerSettings(
|
||||
accountUuid = ACCOUNT_ID_RAW,
|
||||
isIncoming = true,
|
||||
serverSettings = INCOMING_SERVER_SETTINGS,
|
||||
authorizationState = AUTHORIZATION_STATE,
|
||||
)
|
||||
|
||||
assertThat(result).isInstanceOf<AccountUpdaterResult.Failure>()
|
||||
.prop(AccountUpdaterResult.Failure::error).isInstanceOf<AccountUpdaterFailure.UnknownError>()
|
||||
.prop(AccountUpdaterFailure.UnknownError::error).isInstanceOf<Exception>()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val INCOMING_SERVER_SETTINGS = ServerSettings(
|
||||
type = "pop3",
|
||||
host = "pop.example.org",
|
||||
port = 465,
|
||||
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
authenticationType = AuthType.PLAIN,
|
||||
username = "username",
|
||||
password = "password",
|
||||
clientCertificateAlias = null,
|
||||
extra = emptyMap(),
|
||||
)
|
||||
|
||||
val OUTGOING_SERVER_SETTINGS = ServerSettings(
|
||||
type = "smtp",
|
||||
host = "smtp.example.org",
|
||||
port = 587,
|
||||
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
authenticationType = AuthType.PLAIN,
|
||||
username = "username",
|
||||
password = "password",
|
||||
clientCertificateAlias = null,
|
||||
extra = emptyMap(),
|
||||
)
|
||||
|
||||
val AUTHORIZATION_STATE = AuthorizationState("auth state")
|
||||
|
||||
fun createAccount(accountUuid: String): K9Account {
|
||||
return K9Account(
|
||||
uuid = accountUuid,
|
||||
).apply {
|
||||
incomingServerSettings = INCOMING_SERVER_SETTINGS
|
||||
outgoingServerSettings = OUTGOING_SERVER_SETTINGS
|
||||
oAuthState = AUTHORIZATION_STATE.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.fsck.k9.account
|
||||
|
||||
import app.k9mail.feature.account.common.domain.entity.AccountState
|
||||
import app.k9mail.feature.account.common.domain.entity.AuthorizationState
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNull
|
||||
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.FakeAccountData.ACCOUNT_ID_RAW
|
||||
import net.thunderbird.core.android.account.Identity
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
import org.junit.Test
|
||||
|
||||
class AccountStateLoaderTest {
|
||||
|
||||
@Test
|
||||
fun `loadAccountState() SHOULD return null when accountManager returns null`() = runTest {
|
||||
val accountManager = FakeAccountManager()
|
||||
val testSubject = AccountStateLoader(accountManager)
|
||||
|
||||
val result = testSubject.loadAccountState(ACCOUNT_ID_RAW)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadAccountState() SHOULD return account when present in accountManager`() = runTest {
|
||||
val accounts = mutableMapOf(
|
||||
ACCOUNT_ID_RAW to LegacyAccount(uuid = ACCOUNT_ID_RAW).apply {
|
||||
identities = mutableListOf(Identity())
|
||||
email = "emailAddress"
|
||||
incomingServerSettings = INCOMING_SERVER_SETTINGS
|
||||
outgoingServerSettings = OUTGOING_SERVER_SETTINGS
|
||||
oAuthState = "oAuthState"
|
||||
},
|
||||
)
|
||||
val accountManager = FakeAccountManager(accounts = accounts)
|
||||
val testSubject = AccountStateLoader(accountManager)
|
||||
|
||||
val result = testSubject.loadAccountState(ACCOUNT_ID_RAW)
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
AccountState(
|
||||
uuid = ACCOUNT_ID_RAW,
|
||||
emailAddress = "emailAddress",
|
||||
incomingServerSettings = INCOMING_SERVER_SETTINGS,
|
||||
outgoingServerSettings = OUTGOING_SERVER_SETTINGS,
|
||||
authorizationState = AuthorizationState("oAuthState"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val INCOMING_SERVER_SETTINGS = ServerSettings(
|
||||
type = "pop3",
|
||||
host = "pop.example.org",
|
||||
port = 465,
|
||||
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
authenticationType = AuthType.PLAIN,
|
||||
username = "username",
|
||||
password = "password",
|
||||
clientCertificateAlias = null,
|
||||
extra = emptyMap(),
|
||||
)
|
||||
|
||||
val OUTGOING_SERVER_SETTINGS = ServerSettings(
|
||||
type = "smtp",
|
||||
host = "smtp.example.org",
|
||||
port = 587,
|
||||
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
authenticationType = AuthType.PLAIN,
|
||||
username = "username",
|
||||
password = "password",
|
||||
clientCertificateAlias = null,
|
||||
extra = emptyMap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.fsck.k9.account
|
||||
|
||||
import assertk.assertFailure
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isInstanceOf
|
||||
import net.thunderbird.core.android.account.DeletePolicy
|
||||
import net.thunderbird.core.common.mail.Protocols
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultDeletePolicyProviderTest {
|
||||
private val deletePolicyProvider = DefaultDeletePolicyProvider()
|
||||
|
||||
@Test
|
||||
fun `getDeletePolicy with IMAP should return ON_DELETE`() {
|
||||
val result = deletePolicyProvider.getDeletePolicy(Protocols.IMAP)
|
||||
|
||||
assertThat(result).isEqualTo(DeletePolicy.ON_DELETE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDeletePolicy with POP3 should return NEVER`() {
|
||||
val result = deletePolicyProvider.getDeletePolicy(Protocols.POP3)
|
||||
|
||||
assertThat(result).isEqualTo(DeletePolicy.NEVER)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDeletePolicy with demo should return ON_DELETE`() {
|
||||
val result = deletePolicyProvider.getDeletePolicy("demo")
|
||||
|
||||
assertThat(result).isEqualTo(DeletePolicy.ON_DELETE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDeletePolicy with SMTP should fail`() {
|
||||
assertFailure {
|
||||
deletePolicyProvider.getDeletePolicy(Protocols.SMTP)
|
||||
}.isInstanceOf<AssertionError>()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.fsck.k9.account
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import net.thunderbird.core.android.account.AccountManager
|
||||
import net.thunderbird.core.android.account.AccountRemovedListener
|
||||
import net.thunderbird.core.android.account.AccountsChangeListener
|
||||
import net.thunderbird.core.android.account.LegacyAccount
|
||||
|
||||
class FakeAccountManager(
|
||||
private val accounts: MutableMap<String, LegacyAccount> = mutableMapOf(),
|
||||
private val isFailureOnSave: Boolean = false,
|
||||
) : AccountManager {
|
||||
|
||||
override fun getAccounts(): List<LegacyAccount> = accounts.values.toList()
|
||||
|
||||
override fun getAccountsFlow(): Flow<List<LegacyAccount>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getAccount(accountUuid: String): LegacyAccount? = accounts[accountUuid]
|
||||
|
||||
override fun getAccountFlow(accountUuid: String): Flow<LegacyAccount> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addAccountRemovedListener(listener: AccountRemovedListener) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun moveAccount(account: LegacyAccount, newPosition: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun removeOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionThrown")
|
||||
override fun saveAccount(account: LegacyAccount) {
|
||||
if (isFailureOnSave) {
|
||||
throw Exception("FakeAccountManager.saveAccount() failed")
|
||||
}
|
||||
accounts[account.uuid] = account
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue