Add support for OAuth flow after settings import
This commit is contained in:
parent
94c61a7999
commit
9ae7d27e79
9 changed files with 142 additions and 30 deletions
|
@ -77,17 +77,19 @@ public class SettingsImporter {
|
|||
public final AccountDescription original;
|
||||
public final AccountDescription imported;
|
||||
public final boolean overwritten;
|
||||
public final boolean authorizationNeeded;
|
||||
public final boolean incomingPasswordNeeded;
|
||||
public final boolean outgoingPasswordNeeded;
|
||||
public final String incomingServerName;
|
||||
public final String outgoingServerName;
|
||||
|
||||
private AccountDescriptionPair(AccountDescription original, AccountDescription imported,
|
||||
boolean overwritten, boolean incomingPasswordNeeded, boolean outgoingPasswordNeeded,
|
||||
String incomingServerName, String outgoingServerName) {
|
||||
boolean overwritten, boolean authorizationNeeded, boolean incomingPasswordNeeded,
|
||||
boolean outgoingPasswordNeeded, String incomingServerName, String outgoingServerName) {
|
||||
this.original = original;
|
||||
this.imported = imported;
|
||||
this.overwritten = overwritten;
|
||||
this.authorizationNeeded = authorizationNeeded;
|
||||
this.incomingPasswordNeeded = incomingPasswordNeeded;
|
||||
this.outgoingPasswordNeeded = outgoingPasswordNeeded;
|
||||
this.incomingServerName = incomingServerName;
|
||||
|
@ -372,8 +374,11 @@ public class SettingsImporter {
|
|||
|
||||
String incomingServerName = incoming.host;
|
||||
boolean incomingPasswordNeeded = AuthType.EXTERNAL != incoming.authenticationType &&
|
||||
AuthType.XOAUTH2 != incoming.authenticationType &&
|
||||
(incoming.password == null || incoming.password.isEmpty());
|
||||
|
||||
boolean authorizationNeeded = incoming.authenticationType == AuthType.XOAUTH2;
|
||||
|
||||
String incomingServerType = ServerTypeConverter.toServerSettingsType(account.incoming.type);
|
||||
if (account.outgoing == null && !incomingServerType.equals(Protocols.WEBDAV)) {
|
||||
// All account types except WebDAV need to provide outgoing server settings
|
||||
|
@ -395,15 +400,18 @@ public class SettingsImporter {
|
|||
*/
|
||||
String outgoingServerType = ServerTypeConverter.toServerSettingsType(outgoing.type);
|
||||
outgoingPasswordNeeded = AuthType.EXTERNAL != outgoing.authenticationType &&
|
||||
AuthType.XOAUTH2 != outgoing.authenticationType &&
|
||||
!outgoingServerType.equals(Protocols.WEBDAV) &&
|
||||
outgoing.username != null &&
|
||||
!outgoing.username.isEmpty() &&
|
||||
(outgoing.password == null || outgoing.password.isEmpty());
|
||||
|
||||
authorizationNeeded |= outgoing.authenticationType == AuthType.XOAUTH2;
|
||||
|
||||
outgoingServerName = outgoing.host;
|
||||
}
|
||||
|
||||
boolean createAccountDisabled = incomingPasswordNeeded || outgoingPasswordNeeded;
|
||||
boolean createAccountDisabled = incomingPasswordNeeded || outgoingPasswordNeeded || authorizationNeeded;
|
||||
if (createAccountDisabled) {
|
||||
editor.putBoolean(accountKeyPrefix + "enabled", false);
|
||||
}
|
||||
|
@ -465,7 +473,7 @@ public class SettingsImporter {
|
|||
putString(editor, accountKeyPrefix + "messagesNotificationChannelVersion", messageNotificationChannelVersion);
|
||||
|
||||
AccountDescription imported = new AccountDescription(accountName, uuid);
|
||||
return new AccountDescriptionPair(original, imported, mergeImportedAccount,
|
||||
return new AccountDescriptionPair(original, imported, mergeImportedAccount, authorizationNeeded,
|
||||
incomingPasswordNeeded, outgoingPasswordNeeded, incomingServerName, outgoingServerName);
|
||||
}
|
||||
|
||||
|
@ -1061,11 +1069,12 @@ public class SettingsImporter {
|
|||
String type = ServerTypeConverter.toServerSettingsType(importedServer.type);
|
||||
int port = convertPort(importedServer.port);
|
||||
ConnectionSecurity connectionSecurity = convertConnectionSecurity(importedServer.connectionSecurity);
|
||||
String password = importedServer.authenticationType == AuthType.XOAUTH2 ? "" : importedServer.password;
|
||||
Map<String, String> extra = importedServer.extras != null ?
|
||||
unmodifiableMap(importedServer.extras.settings) : emptyMap();
|
||||
|
||||
return new ServerSettings(type, importedServer.host, port, connectionSecurity,
|
||||
importedServer.authenticationType, importedServer.username, importedServer.password,
|
||||
importedServer.authenticationType, importedServer.username, password,
|
||||
importedServer.clientCertificateAlias, extra);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,16 @@ class AccountActivator(
|
|||
val account = preferences.getAccount(accountUuid) ?: error("Account $accountUuid not found")
|
||||
|
||||
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
|
||||
Core.setServicesEnabled(context)
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import androidx.core.view.isVisible
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.fsck.k9.activity.setup.OAuthFlowActivity
|
||||
import com.fsck.k9.ui.R
|
||||
import com.fsck.k9.ui.observeNotNull
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
|
@ -103,6 +104,12 @@ class SettingsImportFragment : Fragment() {
|
|||
StatusText.IMPORT_SUCCESS_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.text = getString(R.string.settings_import_read_failure)
|
||||
}
|
||||
|
@ -142,6 +149,7 @@ class SettingsImportFragment : Fragment() {
|
|||
is Action.Close -> closeImportScreen(action)
|
||||
is Action.PickDocument -> pickDocument()
|
||||
is Action.PasswordPrompt -> showPasswordPrompt(action)
|
||||
is Action.StartAuthorization -> startAuthorization(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,6 +168,15 @@ class SettingsImportFragment : Fragment() {
|
|||
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) {
|
||||
val dialogFragment = PasswordPromptDialogFragment.create(
|
||||
action.accountUuid,
|
||||
|
@ -183,6 +200,7 @@ class SettingsImportFragment : Fragment() {
|
|||
when (requestCode) {
|
||||
REQUEST_PICK_DOCUMENT -> handlePickDocumentResult(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 {
|
||||
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_AUTHORIZATION = Activity.RESULT_FIRST_USER + 2
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,8 +32,9 @@ abstract class ImportListItem<VH : ImportCheckBoxViewHolder>(
|
|||
val imageLevel = when (importStatus) {
|
||||
ImportStatus.IMPORT_SUCCESS -> 0
|
||||
ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED -> 1
|
||||
ImportStatus.NOT_SELECTED -> 2
|
||||
ImportStatus.IMPORT_FAILURE -> 3
|
||||
ImportStatus.IMPORT_SUCCESS_AUTHORIZATION_REQUIRED -> 2
|
||||
ImportStatus.NOT_SELECTED -> 3
|
||||
ImportStatus.IMPORT_FAILURE -> 4
|
||||
else -> error("Unexpected import status: $importStatus")
|
||||
}
|
||||
holder.statusIcon.setImageLevel(imageLevel)
|
||||
|
@ -41,6 +42,7 @@ abstract class ImportListItem<VH : ImportCheckBoxViewHolder>(
|
|||
val contentDescriptionStringResId = when (importStatus) {
|
||||
ImportStatus.IMPORT_SUCCESS -> R.string.settings_import_status_success
|
||||
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.IMPORT_FAILURE -> R.string.settings_import_status_error
|
||||
else -> error("Unexpected import status: $importStatus")
|
||||
|
|
|
@ -67,13 +67,13 @@ class SettingsImportUiModel {
|
|||
statusText = StatusText.IMPORT_SUCCESS
|
||||
}
|
||||
|
||||
private fun showPasswordRequiredText() {
|
||||
private fun showActionRequiredText(actionText: StatusText) {
|
||||
importButton = ButtonState.GONE
|
||||
closeButton = ButtonState.ENABLED
|
||||
closeButtonLabel = CloseButtonLabel.LATER
|
||||
isImportProgressVisible = false
|
||||
isSettingsListEnabled = true
|
||||
statusText = StatusText.IMPORT_SUCCESS_PASSWORD_REQUIRED
|
||||
statusText = actionText
|
||||
}
|
||||
|
||||
fun showReadFailureText() {
|
||||
|
@ -120,7 +120,7 @@ class SettingsImportUiModel {
|
|||
|
||||
fun setSettingsListState(position: Int, status: ImportStatus) {
|
||||
settingsList[position].importStatus = status
|
||||
settingsList[position].enabled = status == ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED
|
||||
settingsList[position].enabled = status.isActionRequired
|
||||
}
|
||||
|
||||
private fun updateImportButtonFromSelection() {
|
||||
|
@ -141,17 +141,26 @@ class SettingsImportUiModel {
|
|||
return
|
||||
}
|
||||
|
||||
val passwordsMissing = settingsList.any { it.importStatus == ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED }
|
||||
if (passwordsMissing) {
|
||||
showPasswordRequiredText()
|
||||
return
|
||||
val passwordsMissing = settingsList.any {
|
||||
it.importStatus == ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED
|
||||
}
|
||||
val authorizationRequired = settingsList.any {
|
||||
it.importStatus == ImportStatus.IMPORT_SUCCESS_AUTHORIZATION_REQUIRED
|
||||
}
|
||||
|
||||
val partialImportError = settingsList.any { it.importStatus == ImportStatus.IMPORT_FAILURE }
|
||||
if (partialImportError) {
|
||||
showPartialImportErrorText()
|
||||
if (passwordsMissing && authorizationRequired) {
|
||||
showActionRequiredText(StatusText.IMPORT_SUCCESS_PASSWORD_AND_AUTHORIZATION_REQUIRED)
|
||||
} else if (passwordsMissing) {
|
||||
showActionRequiredText(StatusText.IMPORT_SUCCESS_PASSWORD_REQUIRED)
|
||||
} else if (authorizationRequired) {
|
||||
showActionRequiredText(StatusText.IMPORT_SUCCESS_AUTHORIZATION_REQUIRED)
|
||||
} 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()
|
||||
}
|
||||
|
||||
enum class ImportStatus {
|
||||
NOT_AVAILABLE,
|
||||
NOT_SELECTED,
|
||||
IMPORT_SUCCESS,
|
||||
IMPORT_SUCCESS_PASSWORD_REQUIRED,
|
||||
IMPORT_FAILURE;
|
||||
|
||||
val isSuccess: Boolean
|
||||
get() = this == IMPORT_SUCCESS || this == IMPORT_SUCCESS_PASSWORD_REQUIRED
|
||||
enum class ImportStatus(val isSuccess: Boolean, val isActionRequired: Boolean) {
|
||||
NOT_AVAILABLE(isSuccess = false, isActionRequired = false),
|
||||
NOT_SELECTED(isSuccess = false, isActionRequired = false),
|
||||
IMPORT_SUCCESS(isSuccess = true, isActionRequired = false),
|
||||
IMPORT_SUCCESS_PASSWORD_REQUIRED(isSuccess = true, isActionRequired = true),
|
||||
IMPORT_SUCCESS_AUTHORIZATION_REQUIRED(isSuccess = true, isActionRequired = true),
|
||||
IMPORT_FAILURE(isSuccess = false, isActionRequired = false)
|
||||
}
|
||||
|
||||
enum class ButtonState {
|
||||
|
@ -188,6 +195,8 @@ enum class StatusText {
|
|||
IMPORTING_PROGRESS,
|
||||
IMPORT_SUCCESS,
|
||||
IMPORT_SUCCESS_PASSWORD_REQUIRED,
|
||||
IMPORT_SUCCESS_AUTHORIZATION_REQUIRED,
|
||||
IMPORT_SUCCESS_PASSWORD_AND_AUTHORIZATION_REQUIRED,
|
||||
IMPORT_READ_FAILURE,
|
||||
IMPORT_PARTIAL_FAILURE,
|
||||
IMPORT_FAILURE
|
||||
|
|
|
@ -36,6 +36,7 @@ class SettingsImportViewModel(
|
|||
private var accountsMap: MutableMap<AccountNumber, AccountUuid> = mutableMapOf()
|
||||
private val accountStateMap: MutableMap<AccountNumber, AccountState> = mutableMapOf()
|
||||
private var contentUri: Uri? = null
|
||||
private var currentlyAuthorizingAccountUuid: String? = null
|
||||
|
||||
private val containsGeneralSettings: Boolean
|
||||
get() = uiModel.settingsList.any { it is SettingsListItem.GeneralSettings }
|
||||
|
@ -69,6 +70,7 @@ class SettingsImportViewModel(
|
|||
|
||||
fun initializeFromSavedState(savedInstanceState: Bundle) {
|
||||
contentUri = savedInstanceState.getParcelable(STATE_CONTENT_URI)
|
||||
currentlyAuthorizingAccountUuid = savedInstanceState.getString(STATE_CURRENTLY_AUTHORIZING_ACCOUNT_UUID)
|
||||
|
||||
updateUiModel {
|
||||
isSettingsListVisible = savedInstanceState.getBoolean(STATE_SETTINGS_LIST_VISIBLE)
|
||||
|
@ -145,6 +147,7 @@ class SettingsImportViewModel(
|
|||
outState.putBoolean(STATE_LOADING_PROGRESS_VISIBLE, isLoadingProgressVisible)
|
||||
outState.putBoolean(STATE_IMPORT_PROGRESS_VISIBLE, isImportProgressVisible)
|
||||
outState.putEnum(STATE_STATUS_TEXT, statusText)
|
||||
outState.putString(STATE_CURRENTLY_AUTHORIZING_ACCOUNT_UUID, currentlyAuthorizingAccountUuid)
|
||||
|
||||
if (hasDocumentBeenRead) {
|
||||
val containsGeneralSettings = this@SettingsImportViewModel.containsGeneralSettings
|
||||
|
@ -200,6 +203,9 @@ class SettingsImportViewModel(
|
|||
ImportStatus.NOT_AVAILABLE -> updateUiModel {
|
||||
toggleSettingsListItemSelection(position)
|
||||
}
|
||||
ImportStatus.IMPORT_SUCCESS_AUTHORIZATION_REQUIRED -> {
|
||||
startAuthorization(settingsListItem as SettingsListItem.Account)
|
||||
}
|
||||
ImportStatus.IMPORT_SUCCESS_PASSWORD_REQUIRED -> {
|
||||
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 {
|
||||
return uiModel.settingsList.indexOfFirst {
|
||||
it is SettingsListItem.Account && accountsMap[it.accountIndex] == accountUuid
|
||||
|
@ -333,7 +354,9 @@ class SettingsImportViewModel(
|
|||
accountsMap[accountIndex] = accountPair.imported.uuid
|
||||
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(
|
||||
accountPair.incomingServerName,
|
||||
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) {
|
||||
val accountIndex = settingsListItem.accountIndex
|
||||
|
||||
|
@ -431,12 +462,14 @@ class SettingsImportViewModel(
|
|||
private const val STATE_CONTENT_URI = "contentUri"
|
||||
private const val STATE_GENERAL_SETTINGS_IMPORT_STATUS = "generalSettingsImportStatus"
|
||||
private const val STATE_ACCOUNT_LIST = "accountList"
|
||||
private const val STATE_CURRENTLY_AUTHORIZING_ACCOUNT_UUID = "currentlyAuthorizingAccountUuid"
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Action {
|
||||
class Close(val importSuccess: Boolean) : Action()
|
||||
object PickDocument : Action()
|
||||
class StartAuthorization(val accountUuid: String) : Action()
|
||||
class PasswordPrompt(
|
||||
val accountUuid: String,
|
||||
val accountName: String,
|
||||
|
|
|
@ -9,11 +9,15 @@
|
|||
android:maxLevel="1"
|
||||
android:minLevel="1" />
|
||||
<item
|
||||
android:drawable="@drawable/ic_not_imported"
|
||||
android:drawable="@drawable/ic_login"
|
||||
android:maxLevel="2"
|
||||
android:minLevel="2" />
|
||||
<item
|
||||
android:drawable="@drawable/ic_error"
|
||||
android:drawable="@drawable/ic_not_imported"
|
||||
android:maxLevel="3"
|
||||
android:minLevel="3" />
|
||||
<item
|
||||
android:drawable="@drawable/ic_error"
|
||||
android:maxLevel="4"
|
||||
android:minLevel="4" />
|
||||
</level-list>
|
||||
|
|
10
app/ui/legacy/src/main/res/drawable/ic_login.xml
Normal file
10
app/ui/legacy/src/main/res/drawable/ic_login.xml
Normal 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>
|
|
@ -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_button">Import</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>
|
||||
|
||||
<!-- 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_read_failure">Failed to read settings file</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_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_error">Import failure</string>
|
||||
<string name="settings_import_later_button">Later</string>
|
||||
|
|
Loading…
Reference in a new issue