Add support for OAuth flow after settings import

This commit is contained in:
cketti 2022-06-07 15:39:11 +02:00
parent 94c61a7999
commit 9ae7d27e79
9 changed files with 142 additions and 30 deletions

View file

@ -77,17 +77,19 @@ public class SettingsImporter {
public final AccountDescription original; public final AccountDescription original;
public final AccountDescription imported; public final AccountDescription imported;
public final boolean overwritten; public final boolean overwritten;
public final boolean authorizationNeeded;
public final boolean incomingPasswordNeeded; public final boolean incomingPasswordNeeded;
public final boolean outgoingPasswordNeeded; public final boolean outgoingPasswordNeeded;
public final String incomingServerName; public final String incomingServerName;
public final String outgoingServerName; public final String outgoingServerName;
private AccountDescriptionPair(AccountDescription original, AccountDescription imported, private AccountDescriptionPair(AccountDescription original, AccountDescription imported,
boolean overwritten, boolean incomingPasswordNeeded, boolean outgoingPasswordNeeded, boolean overwritten, boolean authorizationNeeded, boolean incomingPasswordNeeded,
String incomingServerName, String outgoingServerName) { boolean outgoingPasswordNeeded, String incomingServerName, String outgoingServerName) {
this.original = original; this.original = original;
this.imported = imported; this.imported = imported;
this.overwritten = overwritten; this.overwritten = overwritten;
this.authorizationNeeded = authorizationNeeded;
this.incomingPasswordNeeded = incomingPasswordNeeded; this.incomingPasswordNeeded = incomingPasswordNeeded;
this.outgoingPasswordNeeded = outgoingPasswordNeeded; this.outgoingPasswordNeeded = outgoingPasswordNeeded;
this.incomingServerName = incomingServerName; this.incomingServerName = incomingServerName;
@ -372,8 +374,11 @@ public class SettingsImporter {
String incomingServerName = incoming.host; String incomingServerName = incoming.host;
boolean incomingPasswordNeeded = AuthType.EXTERNAL != incoming.authenticationType && boolean incomingPasswordNeeded = AuthType.EXTERNAL != incoming.authenticationType &&
AuthType.XOAUTH2 != incoming.authenticationType &&
(incoming.password == null || incoming.password.isEmpty()); (incoming.password == null || incoming.password.isEmpty());
boolean authorizationNeeded = incoming.authenticationType == AuthType.XOAUTH2;
String incomingServerType = ServerTypeConverter.toServerSettingsType(account.incoming.type); String incomingServerType = ServerTypeConverter.toServerSettingsType(account.incoming.type);
if (account.outgoing == null && !incomingServerType.equals(Protocols.WEBDAV)) { if (account.outgoing == null && !incomingServerType.equals(Protocols.WEBDAV)) {
// All account types except WebDAV need to provide outgoing server settings // All account types except WebDAV need to provide outgoing server settings
@ -395,15 +400,18 @@ public class SettingsImporter {
*/ */
String outgoingServerType = ServerTypeConverter.toServerSettingsType(outgoing.type); String outgoingServerType = ServerTypeConverter.toServerSettingsType(outgoing.type);
outgoingPasswordNeeded = AuthType.EXTERNAL != outgoing.authenticationType && outgoingPasswordNeeded = AuthType.EXTERNAL != outgoing.authenticationType &&
AuthType.XOAUTH2 != outgoing.authenticationType &&
!outgoingServerType.equals(Protocols.WEBDAV) && !outgoingServerType.equals(Protocols.WEBDAV) &&
outgoing.username != null && outgoing.username != null &&
!outgoing.username.isEmpty() && !outgoing.username.isEmpty() &&
(outgoing.password == null || outgoing.password.isEmpty()); (outgoing.password == null || outgoing.password.isEmpty());
authorizationNeeded |= outgoing.authenticationType == AuthType.XOAUTH2;
outgoingServerName = outgoing.host; outgoingServerName = outgoing.host;
} }
boolean createAccountDisabled = incomingPasswordNeeded || outgoingPasswordNeeded; boolean createAccountDisabled = incomingPasswordNeeded || outgoingPasswordNeeded || authorizationNeeded;
if (createAccountDisabled) { if (createAccountDisabled) {
editor.putBoolean(accountKeyPrefix + "enabled", false); editor.putBoolean(accountKeyPrefix + "enabled", false);
} }
@ -465,7 +473,7 @@ public class SettingsImporter {
putString(editor, accountKeyPrefix + "messagesNotificationChannelVersion", messageNotificationChannelVersion); putString(editor, accountKeyPrefix + "messagesNotificationChannelVersion", messageNotificationChannelVersion);
AccountDescription imported = new AccountDescription(accountName, uuid); AccountDescription imported = new AccountDescription(accountName, uuid);
return new AccountDescriptionPair(original, imported, mergeImportedAccount, return new AccountDescriptionPair(original, imported, mergeImportedAccount, authorizationNeeded,
incomingPasswordNeeded, outgoingPasswordNeeded, incomingServerName, outgoingServerName); incomingPasswordNeeded, outgoingPasswordNeeded, incomingServerName, outgoingServerName);
} }
@ -1061,11 +1069,12 @@ public class SettingsImporter {
String type = ServerTypeConverter.toServerSettingsType(importedServer.type); String type = ServerTypeConverter.toServerSettingsType(importedServer.type);
int port = convertPort(importedServer.port); int port = convertPort(importedServer.port);
ConnectionSecurity connectionSecurity = convertConnectionSecurity(importedServer.connectionSecurity); ConnectionSecurity connectionSecurity = convertConnectionSecurity(importedServer.connectionSecurity);
String password = importedServer.authenticationType == AuthType.XOAUTH2 ? "" : importedServer.password;
Map<String, String> extra = importedServer.extras != null ? Map<String, String> extra = importedServer.extras != null ?
unmodifiableMap(importedServer.extras.settings) : emptyMap(); unmodifiableMap(importedServer.extras.settings) : emptyMap();
return new ServerSettings(type, importedServer.host, port, connectionSecurity, return new ServerSettings(type, importedServer.host, port, connectionSecurity,
importedServer.authenticationType, importedServer.username, importedServer.password, importedServer.authenticationType, importedServer.username, password,
importedServer.clientCertificateAlias, extra); importedServer.clientCertificateAlias, extra);
} }

View file

@ -18,7 +18,16 @@ class AccountActivator(
val account = preferences.getAccount(accountUuid) ?: error("Account $accountUuid not found") val account = preferences.getAccount(accountUuid) ?: error("Account $accountUuid not found")
setAccountPasswords(account, incomingServerPassword, outgoingServerPassword) setAccountPasswords(account, incomingServerPassword, outgoingServerPassword)
enableAccount(account)
}
fun enableAccount(accountUuid: String) {
val account = preferences.getAccount(accountUuid) ?: error("Account $accountUuid not found")
enableAccount(account)
}
private fun enableAccount(account: Account) {
// Start services if necessary // Start services if necessary
Core.setServicesEnabled(context) Core.setServicesEnabled(context)

View file

@ -14,6 +14,7 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.fsck.k9.activity.setup.OAuthFlowActivity
import com.fsck.k9.ui.R import com.fsck.k9.ui.R
import com.fsck.k9.ui.observeNotNull import com.fsck.k9.ui.observeNotNull
import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.FastAdapter
@ -103,6 +104,12 @@ class SettingsImportFragment : Fragment() {
StatusText.IMPORT_SUCCESS_PASSWORD_REQUIRED -> { StatusText.IMPORT_SUCCESS_PASSWORD_REQUIRED -> {
statusText.text = getString(R.string.settings_import_password_required) statusText.text = getString(R.string.settings_import_password_required)
} }
StatusText.IMPORT_SUCCESS_AUTHORIZATION_REQUIRED -> {
statusText.text = getString(R.string.settings_import_authorization_required)
}
StatusText.IMPORT_SUCCESS_PASSWORD_AND_AUTHORIZATION_REQUIRED -> {
statusText.text = getString(R.string.settings_import_authorization_and_password_required)
}
StatusText.IMPORT_READ_FAILURE -> { StatusText.IMPORT_READ_FAILURE -> {
statusText.text = getString(R.string.settings_import_read_failure) statusText.text = getString(R.string.settings_import_read_failure)
} }
@ -142,6 +149,7 @@ class SettingsImportFragment : Fragment() {
is Action.Close -> closeImportScreen(action) is Action.Close -> closeImportScreen(action)
is Action.PickDocument -> pickDocument() is Action.PickDocument -> pickDocument()
is Action.PasswordPrompt -> showPasswordPrompt(action) is Action.PasswordPrompt -> showPasswordPrompt(action)
is Action.StartAuthorization -> startAuthorization(action)
} }
} }
@ -160,6 +168,15 @@ class SettingsImportFragment : Fragment() {
startActivityForResult(createDocumentIntent, REQUEST_PICK_DOCUMENT) startActivityForResult(createDocumentIntent, REQUEST_PICK_DOCUMENT)
} }
private fun startAuthorization(action: Action.StartAuthorization) {
val intent = OAuthFlowActivity.buildLaunchIntent(
context = requireContext(),
accountUuid = action.accountUuid
)
startActivityForResult(intent, REQUEST_AUTHORIZATION)
}
private fun showPasswordPrompt(action: Action.PasswordPrompt) { private fun showPasswordPrompt(action: Action.PasswordPrompt) {
val dialogFragment = PasswordPromptDialogFragment.create( val dialogFragment = PasswordPromptDialogFragment.create(
action.accountUuid, action.accountUuid,
@ -183,6 +200,7 @@ class SettingsImportFragment : Fragment() {
when (requestCode) { when (requestCode) {
REQUEST_PICK_DOCUMENT -> handlePickDocumentResult(resultCode, data) REQUEST_PICK_DOCUMENT -> handlePickDocumentResult(resultCode, data)
REQUEST_PASSWORD_PROMPT -> handlePasswordPromptResult(resultCode, data) REQUEST_PASSWORD_PROMPT -> handlePasswordPromptResult(resultCode, data)
REQUEST_AUTHORIZATION -> handleAuthorizationResult(resultCode)
} }
} }
@ -203,9 +221,16 @@ class SettingsImportFragment : Fragment() {
} }
} }
private fun handleAuthorizationResult(resultCode: Int) {
if (resultCode == Activity.RESULT_OK) {
viewModel.onReturnAfterAuthorization()
}
}
companion object { companion object {
private const val REQUEST_PICK_DOCUMENT = Activity.RESULT_FIRST_USER private const val REQUEST_PICK_DOCUMENT = Activity.RESULT_FIRST_USER
private const val REQUEST_PASSWORD_PROMPT = Activity.RESULT_FIRST_USER + 1 private const val REQUEST_PASSWORD_PROMPT = Activity.RESULT_FIRST_USER + 1
private const val REQUEST_AUTHORIZATION = Activity.RESULT_FIRST_USER + 2
} }
} }

View file

@ -32,8 +32,9 @@ abstract class ImportListItem<VH : ImportCheckBoxViewHolder>(
val imageLevel = when (importStatus) { val imageLevel = when (importStatus) {
ImportStatus.IMPORT_SUCCESS -> 0 ImportStatus.IMPORT_SUCCESS -> 0
ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED -> 1 ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED -> 1
ImportStatus.NOT_SELECTED -> 2 ImportStatus.IMPORT_SUCCESS_AUTHORIZATION_REQUIRED -> 2
ImportStatus.IMPORT_FAILURE -> 3 ImportStatus.NOT_SELECTED -> 3
ImportStatus.IMPORT_FAILURE -> 4
else -> error("Unexpected import status: $importStatus") else -> error("Unexpected import status: $importStatus")
} }
holder.statusIcon.setImageLevel(imageLevel) holder.statusIcon.setImageLevel(imageLevel)
@ -41,6 +42,7 @@ abstract class ImportListItem<VH : ImportCheckBoxViewHolder>(
val contentDescriptionStringResId = when (importStatus) { val contentDescriptionStringResId = when (importStatus) {
ImportStatus.IMPORT_SUCCESS -> R.string.settings_import_status_success ImportStatus.IMPORT_SUCCESS -> R.string.settings_import_status_success
ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED -> R.string.settings_import_status_password_required ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED -> R.string.settings_import_status_password_required
ImportStatus.IMPORT_SUCCESS_AUTHORIZATION_REQUIRED -> R.string.settings_import_status_log_in_required
ImportStatus.NOT_SELECTED -> R.string.settings_import_status_not_imported ImportStatus.NOT_SELECTED -> R.string.settings_import_status_not_imported
ImportStatus.IMPORT_FAILURE -> R.string.settings_import_status_error ImportStatus.IMPORT_FAILURE -> R.string.settings_import_status_error
else -> error("Unexpected import status: $importStatus") else -> error("Unexpected import status: $importStatus")

View file

@ -67,13 +67,13 @@ class SettingsImportUiModel {
statusText = StatusText.IMPORT_SUCCESS statusText = StatusText.IMPORT_SUCCESS
} }
private fun showPasswordRequiredText() { private fun showActionRequiredText(actionText: StatusText) {
importButton = ButtonState.GONE importButton = ButtonState.GONE
closeButton = ButtonState.ENABLED closeButton = ButtonState.ENABLED
closeButtonLabel = CloseButtonLabel.LATER closeButtonLabel = CloseButtonLabel.LATER
isImportProgressVisible = false isImportProgressVisible = false
isSettingsListEnabled = true isSettingsListEnabled = true
statusText = StatusText.IMPORT_SUCCESS_PASSWORD_REQUIRED statusText = actionText
} }
fun showReadFailureText() { fun showReadFailureText() {
@ -120,7 +120,7 @@ class SettingsImportUiModel {
fun setSettingsListState(position: Int, status: ImportStatus) { fun setSettingsListState(position: Int, status: ImportStatus) {
settingsList[position].importStatus = status settingsList[position].importStatus = status
settingsList[position].enabled = status == ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED settingsList[position].enabled = status.isActionRequired
} }
private fun updateImportButtonFromSelection() { private fun updateImportButtonFromSelection() {
@ -141,17 +141,26 @@ class SettingsImportUiModel {
return return
} }
val passwordsMissing = settingsList.any { it.importStatus == ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED } val passwordsMissing = settingsList.any {
if (passwordsMissing) { it.importStatus == ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED
showPasswordRequiredText() }
return val authorizationRequired = settingsList.any {
it.importStatus == ImportStatus.IMPORT_SUCCESS_AUTHORIZATION_REQUIRED
} }
val partialImportError = settingsList.any { it.importStatus == ImportStatus.IMPORT_FAILURE } if (passwordsMissing && authorizationRequired) {
if (partialImportError) { showActionRequiredText(StatusText.IMPORT_SUCCESS_PASSWORD_AND_AUTHORIZATION_REQUIRED)
showPartialImportErrorText() } else if (passwordsMissing) {
showActionRequiredText(StatusText.IMPORT_SUCCESS_PASSWORD_REQUIRED)
} else if (authorizationRequired) {
showActionRequiredText(StatusText.IMPORT_SUCCESS_AUTHORIZATION_REQUIRED)
} else { } else {
showSuccessText() val partialImportError = settingsList.any { it.importStatus == ImportStatus.IMPORT_FAILURE }
if (partialImportError) {
showPartialImportErrorText()
} else {
showSuccessText()
}
} }
} }
} }
@ -165,15 +174,13 @@ sealed class SettingsListItem {
class Account(val accountIndex: Int, var displayName: String) : SettingsListItem() class Account(val accountIndex: Int, var displayName: String) : SettingsListItem()
} }
enum class ImportStatus { enum class ImportStatus(val isSuccess: Boolean, val isActionRequired: Boolean) {
NOT_AVAILABLE, NOT_AVAILABLE(isSuccess = false, isActionRequired = false),
NOT_SELECTED, NOT_SELECTED(isSuccess = false, isActionRequired = false),
IMPORT_SUCCESS, IMPORT_SUCCESS(isSuccess = true, isActionRequired = false),
IMPORT_SUCCESS_PASSWORD_REQUIRED, IMPORT_SUCCESS_PASSWORD_REQUIRED(isSuccess = true, isActionRequired = true),
IMPORT_FAILURE; IMPORT_SUCCESS_AUTHORIZATION_REQUIRED(isSuccess = true, isActionRequired = true),
IMPORT_FAILURE(isSuccess = false, isActionRequired = false)
val isSuccess: Boolean
get() = this == IMPORT_SUCCESS || this == IMPORT_SUCCESS_PASSWORD_REQUIRED
} }
enum class ButtonState { enum class ButtonState {
@ -188,6 +195,8 @@ enum class StatusText {
IMPORTING_PROGRESS, IMPORTING_PROGRESS,
IMPORT_SUCCESS, IMPORT_SUCCESS,
IMPORT_SUCCESS_PASSWORD_REQUIRED, IMPORT_SUCCESS_PASSWORD_REQUIRED,
IMPORT_SUCCESS_AUTHORIZATION_REQUIRED,
IMPORT_SUCCESS_PASSWORD_AND_AUTHORIZATION_REQUIRED,
IMPORT_READ_FAILURE, IMPORT_READ_FAILURE,
IMPORT_PARTIAL_FAILURE, IMPORT_PARTIAL_FAILURE,
IMPORT_FAILURE IMPORT_FAILURE

View file

@ -36,6 +36,7 @@ class SettingsImportViewModel(
private var accountsMap: MutableMap<AccountNumber, AccountUuid> = mutableMapOf() private var accountsMap: MutableMap<AccountNumber, AccountUuid> = mutableMapOf()
private val accountStateMap: MutableMap<AccountNumber, AccountState> = mutableMapOf() private val accountStateMap: MutableMap<AccountNumber, AccountState> = mutableMapOf()
private var contentUri: Uri? = null private var contentUri: Uri? = null
private var currentlyAuthorizingAccountUuid: String? = null
private val containsGeneralSettings: Boolean private val containsGeneralSettings: Boolean
get() = uiModel.settingsList.any { it is SettingsListItem.GeneralSettings } get() = uiModel.settingsList.any { it is SettingsListItem.GeneralSettings }
@ -69,6 +70,7 @@ class SettingsImportViewModel(
fun initializeFromSavedState(savedInstanceState: Bundle) { fun initializeFromSavedState(savedInstanceState: Bundle) {
contentUri = savedInstanceState.getParcelable(STATE_CONTENT_URI) contentUri = savedInstanceState.getParcelable(STATE_CONTENT_URI)
currentlyAuthorizingAccountUuid = savedInstanceState.getString(STATE_CURRENTLY_AUTHORIZING_ACCOUNT_UUID)
updateUiModel { updateUiModel {
isSettingsListVisible = savedInstanceState.getBoolean(STATE_SETTINGS_LIST_VISIBLE) isSettingsListVisible = savedInstanceState.getBoolean(STATE_SETTINGS_LIST_VISIBLE)
@ -145,6 +147,7 @@ class SettingsImportViewModel(
outState.putBoolean(STATE_LOADING_PROGRESS_VISIBLE, isLoadingProgressVisible) outState.putBoolean(STATE_LOADING_PROGRESS_VISIBLE, isLoadingProgressVisible)
outState.putBoolean(STATE_IMPORT_PROGRESS_VISIBLE, isImportProgressVisible) outState.putBoolean(STATE_IMPORT_PROGRESS_VISIBLE, isImportProgressVisible)
outState.putEnum(STATE_STATUS_TEXT, statusText) outState.putEnum(STATE_STATUS_TEXT, statusText)
outState.putString(STATE_CURRENTLY_AUTHORIZING_ACCOUNT_UUID, currentlyAuthorizingAccountUuid)
if (hasDocumentBeenRead) { if (hasDocumentBeenRead) {
val containsGeneralSettings = this@SettingsImportViewModel.containsGeneralSettings val containsGeneralSettings = this@SettingsImportViewModel.containsGeneralSettings
@ -200,6 +203,9 @@ class SettingsImportViewModel(
ImportStatus.NOT_AVAILABLE -> updateUiModel { ImportStatus.NOT_AVAILABLE -> updateUiModel {
toggleSettingsListItemSelection(position) toggleSettingsListItemSelection(position)
} }
ImportStatus.IMPORT_SUCCESS_AUTHORIZATION_REQUIRED -> {
startAuthorization(settingsListItem as SettingsListItem.Account)
}
ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED -> { ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED -> {
showPasswordPromptDialog(settingsListItem as SettingsListItem.Account) showPasswordPromptDialog(settingsListItem as SettingsListItem.Account)
} }
@ -222,6 +228,21 @@ class SettingsImportViewModel(
} }
} }
fun onReturnAfterAuthorization() {
currentlyAuthorizingAccountUuid?.let { accountUuid ->
currentlyAuthorizingAccountUuid = null
updateUiModel {
val index = getListIndexOfAccount(accountUuid)
setSettingsListState(index, ImportStatus.IMPORT_SUCCESS)
updateCloseButtonAndImportStatusText()
}
GlobalScope.launch(Dispatchers.IO) {
accountActivator.enableAccount(accountUuid)
}
}
}
private fun getListIndexOfAccount(accountUuid: String): Int { private fun getListIndexOfAccount(accountUuid: String): Int {
return uiModel.settingsList.indexOfFirst { return uiModel.settingsList.indexOfFirst {
it is SettingsListItem.Account && accountsMap[it.accountIndex] == accountUuid it is SettingsListItem.Account && accountsMap[it.accountIndex] == accountUuid
@ -333,7 +354,9 @@ class SettingsImportViewModel(
accountsMap[accountIndex] = accountPair.imported.uuid accountsMap[accountIndex] = accountPair.imported.uuid
listItem.displayName = accountPair.imported.name listItem.displayName = accountPair.imported.name
if (accountPair.incomingPasswordNeeded || accountPair.outgoingPasswordNeeded) { if (accountPair.authorizationNeeded) {
setSettingsListState(index, ImportStatus.IMPORT_SUCCESS_AUTHORIZATION_REQUIRED)
} else if (accountPair.incomingPasswordNeeded || accountPair.outgoingPasswordNeeded) {
accountStateMap[accountIndex] = AccountState( accountStateMap[accountIndex] = AccountState(
accountPair.incomingServerName, accountPair.incomingServerName,
accountPair.outgoingServerName, accountPair.outgoingServerName,
@ -364,6 +387,14 @@ class SettingsImportViewModel(
} }
} }
private fun startAuthorization(settingsListItem: SettingsListItem.Account) {
val accountIndex = settingsListItem.accountIndex
val accountUuid = accountsMap[accountIndex]!!
currentlyAuthorizingAccountUuid = accountUuid
sendActionEvent(Action.StartAuthorization(accountUuid))
}
private fun showPasswordPromptDialog(settingsListItem: SettingsListItem.Account) { private fun showPasswordPromptDialog(settingsListItem: SettingsListItem.Account) {
val accountIndex = settingsListItem.accountIndex val accountIndex = settingsListItem.accountIndex
@ -431,12 +462,14 @@ class SettingsImportViewModel(
private const val STATE_CONTENT_URI = "contentUri" private const val STATE_CONTENT_URI = "contentUri"
private const val STATE_GENERAL_SETTINGS_IMPORT_STATUS = "generalSettingsImportStatus" private const val STATE_GENERAL_SETTINGS_IMPORT_STATUS = "generalSettingsImportStatus"
private const val STATE_ACCOUNT_LIST = "accountList" private const val STATE_ACCOUNT_LIST = "accountList"
private const val STATE_CURRENTLY_AUTHORIZING_ACCOUNT_UUID = "currentlyAuthorizingAccountUuid"
} }
} }
sealed class Action { sealed class Action {
class Close(val importSuccess: Boolean) : Action() class Close(val importSuccess: Boolean) : Action()
object PickDocument : Action() object PickDocument : Action()
class StartAuthorization(val accountUuid: String) : Action()
class PasswordPrompt( class PasswordPrompt(
val accountUuid: String, val accountUuid: String,
val accountName: String, val accountName: String,

View file

@ -9,11 +9,15 @@
android:maxLevel="1" android:maxLevel="1"
android:minLevel="1" /> android:minLevel="1" />
<item <item
android:drawable="@drawable/ic_not_imported" android:drawable="@drawable/ic_login"
android:maxLevel="2" android:maxLevel="2"
android:minLevel="2" /> android:minLevel="2" />
<item <item
android:drawable="@drawable/ic_error" android:drawable="@drawable/ic_not_imported"
android:maxLevel="3" android:maxLevel="3"
android:minLevel="3" /> android:minLevel="3" />
<item
android:drawable="@drawable/ic_error"
android:maxLevel="4"
android:minLevel="4" />
</level-list> </level-list>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M10,17V14H3V10H10V7L15,12L10,17M10,2H19A2,2 0 0,1 21,4V20A2,2 0 0,1 19,22H10A2,2 0 0,1 8,20V18H10V20H19V4H10V6H8V4A2,2 0 0,1 10,2Z" />
</vector>

View file

@ -932,12 +932,23 @@ Please submit bug reports, contribute new features and ask questions at
<string name="settings_import_pick_document_button">Select file</string> <string name="settings_import_pick_document_button">Select file</string>
<string name="settings_import_button">Import</string> <string name="settings_import_button">Import</string>
<string name="settings_import_success_generic">Settings successfully imported</string> <string name="settings_import_success_generic">Settings successfully imported</string>
<!-- Displayed after importing accounts when one or multiple accounts require entering a password -->
<string name="settings_import_password_required">Please enter passwords</string> <string name="settings_import_password_required">Please enter passwords</string>
<!-- Displayed after importing accounts when one or multiple accounts require to use the OAuth authorization flow -->
<string name="settings_import_authorization_required">Please sign in</string>
<!-- Displayed after importing accounts when some accounts require entering a password and some to use the OAuth authorization flow -->
<string name="settings_import_authorization_and_password_required">Please sign in and enter passwords</string>
<string name="settings_import_failure">Failed to import settings</string> <string name="settings_import_failure">Failed to import settings</string>
<string name="settings_import_read_failure">Failed to read settings file</string> <string name="settings_import_read_failure">Failed to read settings file</string>
<string name="settings_import_partial_failure">Failed to import some settings</string> <string name="settings_import_partial_failure">Failed to import some settings</string>
<string name="settings_import_status_success">Imported successfully</string> <string name="settings_import_status_success">Imported successfully</string>
<string name="settings_import_status_password_required">Password required</string> <string name="settings_import_status_password_required">Password required</string>
<!-- Content description for the icon that is displayed next to an imported account that requires sign-in -->
<string name="settings_import_status_log_in_required">Sign-in required</string>
<string name="settings_import_status_not_imported">Not imported</string> <string name="settings_import_status_not_imported">Not imported</string>
<string name="settings_import_status_error">Import failure</string> <string name="settings_import_status_error">Import failure</string>
<string name="settings_import_later_button">Later</string> <string name="settings_import_later_button">Later</string>