Add UI to add a JMAP account to the app
This commit is contained in:
parent
0b21a7521d
commit
320cc8b40b
19 changed files with 606 additions and 7 deletions
|
@ -25,4 +25,5 @@ val mainModule = module {
|
|||
single<TrustedSocketFactory> { DefaultTrustedSocketFactory(get(), get()) }
|
||||
single { Clock.INSTANCE }
|
||||
factory { ServerNameSuggester() }
|
||||
factory { EmailAddressValidator() }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'org.jetbrains.kotlin.android.extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'org.jlleitschuh.gradle.ktlint'
|
||||
|
||||
if (rootProject.testCoverage) {
|
||||
|
@ -21,6 +22,11 @@ dependencies {
|
|||
|
||||
implementation "androidx.appcompat:appcompat:${versions.androidxAppCompat}"
|
||||
implementation "com.jakewharton.timber:timber:${versions.timber}"
|
||||
implementation "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}"
|
||||
implementation "com.google.android.material:material:${versions.materialComponents}"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:${versions.androidxNavigation}"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0"
|
||||
|
||||
testImplementation "org.robolectric:robolectric:${versions.robolectric}"
|
||||
testImplementation "junit:junit:${versions.junit}"
|
||||
|
@ -105,6 +111,10 @@ android {
|
|||
jvmTarget = kotlinJvmVersion
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
|
|
|
@ -242,6 +242,10 @@
|
|||
android:name="com.fsck.k9.ui.settings.account.AccountSettingsActivity"
|
||||
android:label="@string/account_settings_title_fmt" />
|
||||
|
||||
<activity
|
||||
android:name="com.fsck.k9.ui.addaccount.AddAccountActivity"
|
||||
android:label="@string/add_account_action" />
|
||||
|
||||
<receiver
|
||||
android:name="com.fsck.k9.service.BootReceiver"
|
||||
android:enabled="true">
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.fsck.k9.preferences.K9StoragePersister
|
|||
import com.fsck.k9.preferences.StoragePersister
|
||||
import com.fsck.k9.resources.resourcesModule
|
||||
import com.fsck.k9.storage.storageModule
|
||||
import com.fsck.k9.ui.addaccount.uiAddAccountModule
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
@ -25,5 +26,6 @@ val appModules = listOf(
|
|||
notificationModule,
|
||||
resourcesModule,
|
||||
backendsModule,
|
||||
storageModule
|
||||
storageModule,
|
||||
uiAddAccountModule
|
||||
)
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.account.AccountCreator
|
||||
import com.fsck.k9.backend.BackendManager
|
||||
import com.fsck.k9.backend.jmap.JmapDiscoveryResult.JmapAccount
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mailstore.LocalStoreProvider
|
||||
|
||||
class JmapAccountCreator(
|
||||
private val preferences: Preferences,
|
||||
private val backendManager: BackendManager,
|
||||
private val accountCreator: AccountCreator,
|
||||
private val localStoreProvider: LocalStoreProvider
|
||||
) {
|
||||
fun createAccount(emailAddress: String, password: String, jmapAccount: JmapAccount) {
|
||||
val serverSettings = createServerSettings(emailAddress, password, jmapAccount)
|
||||
|
||||
val account = preferences.newAccount().apply {
|
||||
email = emailAddress
|
||||
description = jmapAccount.name
|
||||
storeUri = backendManager.createStoreUri(serverSettings)
|
||||
transportUri = backendManager.createTransportUri(serverSettings)
|
||||
|
||||
chipColor = accountCreator.pickColor()
|
||||
deletePolicy = Account.DeletePolicy.ON_DELETE
|
||||
}
|
||||
preferences.saveAccount(account)
|
||||
|
||||
createOutboxFolder(account)
|
||||
fetchFolderList(account)
|
||||
}
|
||||
|
||||
private fun createServerSettings(emailAddress: String, password: String, jmapAccount: JmapAccount): ServerSettings {
|
||||
return ServerSettings(
|
||||
"jmap",
|
||||
null,
|
||||
433,
|
||||
ConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
AuthType.PLAIN,
|
||||
emailAddress,
|
||||
password,
|
||||
null,
|
||||
mapOf("accountId" to jmapAccount.accountId)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createOutboxFolder(account: Account) {
|
||||
val localStore = localStoreProvider.getInstance(account)
|
||||
localStore.createLocalFolder(Account.OUTBOX, Account.OUTBOX_NAME)
|
||||
}
|
||||
|
||||
private fun fetchFolderList(account: Account) {
|
||||
val backend = backendManager.getBackend(account)
|
||||
backend.refreshFolderList()
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.fsck.k9.backends
|
||||
|
||||
import com.fsck.k9.backend.BackendManager
|
||||
import com.fsck.k9.backend.jmap.JmapAccountDiscovery
|
||||
import org.koin.dsl.module
|
||||
|
||||
val backendsModule = module {
|
||||
|
@ -17,4 +18,6 @@ val backendsModule = module {
|
|||
single { Pop3BackendFactory(get(), get()) }
|
||||
single { WebDavBackendFactory(get(), get()) }
|
||||
single { JmapBackendFactory(get()) }
|
||||
factory { JmapAccountDiscovery() }
|
||||
factory { JmapAccountCreator(get(), get(), get(), get()) }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package com.fsck.k9.ui
|
||||
|
||||
import android.view.View
|
||||
import androidx.databinding.BindingAdapter
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
||||
@BindingAdapter("isVisible")
|
||||
fun setVisibility(view: View, value: Boolean) {
|
||||
view.visibility = if (value) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
@BindingAdapter("error")
|
||||
fun setError(view: TextInputLayout, value: Int?) {
|
||||
if (value == null) {
|
||||
view.error = null
|
||||
} else {
|
||||
val errorString = view.context.getString(value)
|
||||
view.error = errorString
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package com.fsck.k9.ui.addaccount
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.setupActionBarWithNavController
|
||||
import com.fsck.k9.activity.K9Activity
|
||||
import com.fsck.k9.jmap.R
|
||||
|
||||
class AddAccountActivity : K9Activity() {
|
||||
private lateinit var navController: NavController
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setLayout(R.layout.activity_add_account)
|
||||
|
||||
initializeActionBar()
|
||||
}
|
||||
|
||||
private fun initializeActionBar() {
|
||||
val appBarConfiguration = AppBarConfiguration(topLevelDestinationIds = setOf(R.id.addJmapAccountScreen))
|
||||
|
||||
navController = findNavController(R.id.nav_host_fragment)
|
||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
return navController.navigateUp() || super.onSupportNavigateUp()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package com.fsck.k9.ui.addaccount
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.fsck.k9.jmap.R
|
||||
import com.fsck.k9.jmap.databinding.FragmentAddAccountBinding
|
||||
import com.fsck.k9.ui.observeNotNull
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class AddAccountFragment : Fragment() {
|
||||
private val viewModel: AddAccountViewModel by viewModel()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel.getActionEvents().observeNotNull(this) { handleActionEvents(it) }
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = FragmentAddAccountBinding.inflate(inflater, container, false)
|
||||
binding.lifecycleOwner = this
|
||||
binding.viewModel = viewModel
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun handleActionEvents(action: Action) {
|
||||
when (action) {
|
||||
is Action.GoToMessageList -> goToMessageList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun goToMessageList() {
|
||||
findNavController().navigate(R.id.action_addJmapAccountScreen_to_messageListScreen)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
package com.fsck.k9.ui.addaccount
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fsck.k9.EmailAddressValidator
|
||||
import com.fsck.k9.backend.jmap.JmapAccountDiscovery
|
||||
import com.fsck.k9.backend.jmap.JmapDiscoveryResult
|
||||
import com.fsck.k9.backend.jmap.JmapDiscoveryResult.JmapAccount
|
||||
import com.fsck.k9.backends.JmapAccountCreator
|
||||
import com.fsck.k9.helper.SingleLiveEvent
|
||||
import com.fsck.k9.helper.measureRealtimeMillisWithResult
|
||||
import com.fsck.k9.jmap.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AddAccountViewModel(
|
||||
private val emailAddressValidator: EmailAddressValidator,
|
||||
private val jmapAccountDiscovery: JmapAccountDiscovery,
|
||||
private val jmapAccountCreator: JmapAccountCreator
|
||||
) : ViewModel() {
|
||||
val emailAddress = MutableLiveData<String>()
|
||||
val emailAddressError = MutableLiveData<Int?>()
|
||||
val password = MutableLiveData<String>()
|
||||
val passwordError = MutableLiveData<Int?>()
|
||||
val setupErrorText: MutableLiveData<Int> = createMutableLiveData(R.string.empty_string)
|
||||
val isInputEnabled: MutableLiveData<Boolean> = createMutableLiveData(true)
|
||||
val isNextButtonEnabled: MutableLiveData<Boolean> = createMutableLiveData(true)
|
||||
val isProgressBarVisible: MutableLiveData<Boolean> = createMutableLiveData(false)
|
||||
private val actionLiveData = SingleLiveEvent<Action>()
|
||||
|
||||
init {
|
||||
Transformations.distinctUntilChanged(emailAddress).observeForever { resetEmailAddressError() }
|
||||
Transformations.distinctUntilChanged(password).observeForever { resetPasswordError() }
|
||||
}
|
||||
|
||||
fun getActionEvents(): LiveData<Action> = actionLiveData
|
||||
|
||||
fun onNextButtonClicked() {
|
||||
discoverServerSettings()
|
||||
}
|
||||
|
||||
private fun discoverServerSettings() {
|
||||
val emailAddress = this.emailAddress.value?.trim() ?: ""
|
||||
val password = this.password.value ?: ""
|
||||
|
||||
if (!emailAddressValidator.isValidAddressOnly(emailAddress)) {
|
||||
displayEmailAddressError(R.string.add_account__email_address_error)
|
||||
return
|
||||
}
|
||||
|
||||
showDiscoveryProgressBar()
|
||||
|
||||
viewModelScope.launch {
|
||||
val (elapsed, discoveryResult) = measureRealtimeMillisWithResult {
|
||||
withContext(Dispatchers.IO) {
|
||||
jmapAccountDiscovery.discover(emailAddress, password)
|
||||
}
|
||||
}
|
||||
|
||||
if (elapsed < MIN_PROGRESS_DURATION) {
|
||||
delay(MIN_PROGRESS_DURATION - elapsed)
|
||||
}
|
||||
|
||||
if (discoveryResult is JmapAccount) {
|
||||
createAccount(emailAddress, password, discoveryResult)
|
||||
} else {
|
||||
displayDiscoveryError(discoveryResult)
|
||||
hideDiscoveryProgressBar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createAccount(emailAddress: String, password: String, jmapAccount: JmapAccount) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
jmapAccountCreator.createAccount(emailAddress, password, jmapAccount)
|
||||
}.join()
|
||||
|
||||
sendActionEvent(Action.GoToMessageList)
|
||||
}
|
||||
|
||||
private fun displayDiscoveryError(discoveryResult: JmapDiscoveryResult) {
|
||||
when (discoveryResult) {
|
||||
is JmapDiscoveryResult.GenericFailure -> {
|
||||
displayError(R.string.add_account__generic_failure)
|
||||
}
|
||||
is JmapDiscoveryResult.NoEmailAccountFoundFailure -> {
|
||||
displayError(R.string.add_account__no_email_account_found)
|
||||
}
|
||||
is JmapDiscoveryResult.AuthenticationFailure -> {
|
||||
displayPasswordError(R.string.add_account__password_error)
|
||||
}
|
||||
is JmapDiscoveryResult.EndpointNotFoundFailure -> {
|
||||
displayError(R.string.add_account__jmap_server_not_found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun displayEmailAddressError(@StringRes error: Int) {
|
||||
emailAddressError.value = error
|
||||
}
|
||||
|
||||
private fun resetEmailAddressError() {
|
||||
emailAddressError.value = null
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun displayPasswordError(@StringRes error: Int) {
|
||||
passwordError.value = error
|
||||
}
|
||||
|
||||
private fun resetPasswordError() {
|
||||
passwordError.value = null
|
||||
}
|
||||
|
||||
private fun showDiscoveryProgressBar() {
|
||||
isInputEnabled.value = false
|
||||
isProgressBarVisible.value = true
|
||||
isNextButtonEnabled.value = false
|
||||
setupErrorText.value = R.string.empty_string
|
||||
}
|
||||
|
||||
private fun hideDiscoveryProgressBar() {
|
||||
isInputEnabled.value = true
|
||||
isProgressBarVisible.value = false
|
||||
isNextButtonEnabled.value = true
|
||||
}
|
||||
|
||||
private fun displayError(@StringRes error: Int) {
|
||||
setupErrorText.value = error
|
||||
}
|
||||
|
||||
private fun sendActionEvent(action: Action) {
|
||||
actionLiveData.value = action
|
||||
}
|
||||
|
||||
private fun <T> createMutableLiveData(initialValue: T): MutableLiveData<T> {
|
||||
return MutableLiveData<T>().apply {
|
||||
value = initialValue
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIN_PROGRESS_DURATION = 500
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Action {
|
||||
object GoToMessageList : Action()
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.fsck.k9.ui.addaccount
|
||||
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val uiAddAccountModule = module {
|
||||
viewModel { AddAccountViewModel(get(), get(), get()) }
|
||||
}
|
20
app/k9mail-jmap/src/main/res/layout/activity_add_account.xml
Normal file
20
app/k9mail-jmap/src/main/res/layout/activity_add_account.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/toolbar" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/navigation_add_account" />
|
||||
|
||||
</LinearLayout>
|
117
app/k9mail-jmap/src/main/res/layout/fragment_add_account.xml
Normal file
117
app/k9mail-jmap/src/main/res/layout/fragment_add_account.xml
Normal file
|
@ -0,0 +1,117 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="com.fsck.k9.ui.addaccount.AddAccountViewModel" />
|
||||
</data>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/emailAddressLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:error="@{viewModel.emailAddressError}"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/emailAddress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="@{viewModel.isInputEnabled}"
|
||||
android:hint="@string/account_setup_basics_email_hint"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textEmailAddress"
|
||||
android:text="@={viewModel.emailAddress}" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/emailPasswordLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:error="@{viewModel.passwordError}"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/emailAddressLayout"
|
||||
app:passwordToggleEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/emailPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="@{viewModel.isInputEnabled}"
|
||||
android:hint="@string/account_setup_basics_password_hint"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textPassword"
|
||||
android:text="@={viewModel.password}" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/setupError"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@{viewModel.setupErrorText}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/emailPasswordLayout" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:isVisible="@{viewModel.isProgressBarVisible()}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/nextButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/setupError" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/nextButton"
|
||||
style="@style/Widget.AppCompat.Button.Colored"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:enabled="@{viewModel.isNextButtonEnabled()}"
|
||||
android:onClick="@{() -> viewModel.onNextButtonClicked()}"
|
||||
android:text="@string/next_action"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/emailPasswordLayout"
|
||||
app:layout_constraintVertical_bias="1.0" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
</layout>
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/navigation_add_account"
|
||||
app:startDestination="@id/addJmapAccountScreen">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/addJmapAccountScreen"
|
||||
android:name="com.fsck.k9.ui.addaccount.AddAccountFragment"
|
||||
android:label="@string/add_account_action"
|
||||
tools:layout="@layout/fragment_add_account">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_addJmapAccountScreen_to_messageListScreen"
|
||||
app:destination="@id/messageListScreen" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<activity
|
||||
android:id="@+id/messageListScreen"
|
||||
android:name="com.fsck.k9.activity.MessageList"
|
||||
tools:layout="@layout/message_list"/>
|
||||
|
||||
</navigation>
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/navigation_onboarding"
|
||||
app:startDestination="@id/welcomeScreen">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/welcomeScreen"
|
||||
android:name="com.fsck.k9.ui.onboarding.WelcomeFragment"
|
||||
android:label="@string/welcome_message_title"
|
||||
tools:layout="@layout/fragment_welcome_message">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_welcomeScreen_to_settingsImportScreen"
|
||||
app:destination="@id/settingsImportScreen" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_welcomeScreen_to_addAccountScreen"
|
||||
app:destination="@id/addAccountScreen" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_welcomeScreen_to_messageListScreen"
|
||||
app:destination="@id/messageListScreen" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/settingsImportScreen"
|
||||
android:name="com.fsck.k9.ui.settings.import.SettingsImportFragment"
|
||||
android:label="@string/settings_import_title"
|
||||
tools:layout="@layout/fragment_settings_import"/>
|
||||
|
||||
<activity
|
||||
android:id="@+id/addAccountScreen"
|
||||
android:name="com.fsck.k9.ui.addaccount.AddAccountActivity"
|
||||
android:label="@string/add_account_action"
|
||||
tools:layout="@layout/activity_add_account"/>
|
||||
|
||||
<activity
|
||||
android:id="@+id/messageListScreen"
|
||||
android:name="com.fsck.k9.activity.MessageList"
|
||||
tools:layout="@layout/message_list"/>
|
||||
|
||||
</navigation>
|
|
@ -1,4 +1,11 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<resources>
|
||||
<string name="app_name">K-9 JMAP</string>
|
||||
<string name="empty_string" translatable="false" />
|
||||
|
||||
<string name="add_account__email_address_error">Please enter a valid email address</string>
|
||||
<string name="add_account__password_error">"Couldn't sign in. Please make sure your password is correct."</string>
|
||||
<string name="add_account__generic_failure">Unknown error while discovering server settings</string>
|
||||
<string name="add_account__no_email_account_found">"Couldn't find an email account on the JMAP server"</string>
|
||||
<string name="add_account__jmap_server_not_found">"Couldn't find the JMAP server from your email address"</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package com.fsck.k9.backend.jmap
|
||||
|
||||
import java.net.UnknownHostException
|
||||
import rs.ltt.jmap.client.JmapClient
|
||||
import rs.ltt.jmap.client.api.EndpointNotFoundException
|
||||
import rs.ltt.jmap.client.api.UnauthorizedException
|
||||
import rs.ltt.jmap.common.entity.capability.MailAccountCapability
|
||||
import timber.log.Timber
|
||||
|
||||
class JmapAccountDiscovery {
|
||||
fun discover(emailAddress: String, password: String): JmapDiscoveryResult {
|
||||
val jmapClient = JmapClient(emailAddress, password)
|
||||
val session = try {
|
||||
jmapClient.session.futureGetOrThrow()
|
||||
} catch (e: EndpointNotFoundException) {
|
||||
return JmapDiscoveryResult.EndpointNotFoundFailure
|
||||
} catch (e: UnknownHostException) {
|
||||
return JmapDiscoveryResult.EndpointNotFoundFailure
|
||||
} catch (e: UnauthorizedException) {
|
||||
return JmapDiscoveryResult.AuthenticationFailure
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Unable to get JMAP session")
|
||||
return JmapDiscoveryResult.GenericFailure(e)
|
||||
}
|
||||
|
||||
val accounts = session.getAccounts(MailAccountCapability::class.java)
|
||||
val accountId = when {
|
||||
accounts.isEmpty() -> return JmapDiscoveryResult.NoEmailAccountFoundFailure
|
||||
accounts.size == 1 -> accounts.keys.first()
|
||||
else -> session.getPrimaryAccount(MailAccountCapability::class.java)
|
||||
}
|
||||
|
||||
val account = accounts[accountId]!!
|
||||
val accountName = account.name ?: emailAddress
|
||||
return JmapDiscoveryResult.JmapAccount(accountId, accountName)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class JmapDiscoveryResult {
|
||||
class GenericFailure(val cause: Throwable) : JmapDiscoveryResult()
|
||||
object EndpointNotFoundFailure : JmapDiscoveryResult()
|
||||
object AuthenticationFailure : JmapDiscoveryResult()
|
||||
object NoEmailAccountFoundFailure : JmapDiscoveryResult()
|
||||
|
||||
data class JmapAccount(val accountId: String, val name: String) : JmapDiscoveryResult()
|
||||
}
|
|
@ -7,13 +7,17 @@ import rs.ltt.jmap.client.MethodResponses
|
|||
import rs.ltt.jmap.common.method.MethodResponse
|
||||
|
||||
internal inline fun <reified T : MethodResponse> ListenableFuture<MethodResponses>.getMainResponseBlocking(): T {
|
||||
return try {
|
||||
get().getMain(T::class.java)
|
||||
} catch (e: ExecutionException) {
|
||||
throw e.cause ?: e
|
||||
}
|
||||
return futureGetOrThrow().getMain(T::class.java)
|
||||
}
|
||||
|
||||
internal inline fun <reified T : MethodResponse> JmapRequest.Call.getMainResponseBlocking(): T {
|
||||
return methodResponses.getMainResponseBlocking()
|
||||
}
|
||||
|
||||
internal inline fun <T> ListenableFuture<T>.futureGetOrThrow(): T {
|
||||
return try {
|
||||
get()
|
||||
} catch (e: ExecutionException) {
|
||||
throw e.cause ?: e
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ buildscript {
|
|||
'kotlin': '1.3.50',
|
||||
'androidxAppCompat': '1.0.2',
|
||||
'androidxRecyclerView': '1.0.0',
|
||||
'androidxLifecycleExtensions': '2.0.0',
|
||||
'androidxLifecycleExtensions': '2.1.0',
|
||||
'androidxAnnotation': '1.0.1',
|
||||
'androidxNavigation': '2.0.0',
|
||||
'androidxConstraintLayout': '1.1.3',
|
||||
|
|
Loading…
Reference in a new issue