diff --git a/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt b/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt index 29d6a5c5570..ff05dc51a01 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt +++ b/app/core/src/main/java/com/fsck/k9/preferences/SettingsExporter.kt @@ -15,11 +15,11 @@ import com.fsck.k9.mailstore.FolderRepositoryManager import com.fsck.k9.preferences.ServerTypeConverter.fromServerSettingsType import com.fsck.k9.preferences.Settings.InvalidSettingValueException import com.fsck.k9.preferences.Settings.SettingsDescription +import org.xmlpull.v1.XmlSerializer +import timber.log.Timber import java.io.OutputStream import java.text.SimpleDateFormat import java.util.Calendar -import org.xmlpull.v1.XmlSerializer -import timber.log.Timber class SettingsExporter( private val contentResolver: ContentResolver, @@ -29,10 +29,10 @@ class SettingsExporter( private val folderRepositoryManager: FolderRepositoryManager ) { @Throws(SettingsImportExportException::class) - fun exportToUri(includeGlobals: Boolean, accountUuids: Set, uri: Uri) { + fun exportToUri(includeGlobals: Boolean, accountUuids: Set, uri: Uri, withPassword: Boolean) { try { contentResolver.openOutputStream(uri)!!.use { outputStream -> - exportPreferences(outputStream, includeGlobals, accountUuids) + exportPreferences(outputStream, includeGlobals, accountUuids, withPassword) } } catch (e: Exception) { throw SettingsImportExportException(e) @@ -40,7 +40,7 @@ class SettingsExporter( } @Throws(SettingsImportExportException::class) - fun exportPreferences(outputStream: OutputStream, includeGlobals: Boolean, accountUuids: Set) { + fun exportPreferences(outputStream: OutputStream, includeGlobals: Boolean, accountUuids: Set, withPassword: Boolean) { try { val serializer = Xml.newSerializer() serializer.setOutput(outputStream, "UTF-8") @@ -68,7 +68,7 @@ class SettingsExporter( serializer.startTag(null, ACCOUNTS_ELEMENT) for (accountUuid in accountUuids) { val account = preferences.getAccount(accountUuid) - writeAccount(serializer, account, prefs) + writeAccount(serializer, account, prefs, withPassword) } serializer.endTag(null, ACCOUNTS_ELEMENT) @@ -90,8 +90,10 @@ class SettingsExporter( try { writeKeyAndPrettyValueFromSetting(serializer, key, setting, valueString) } catch (e: InvalidSettingValueException) { - Timber.w("Global setting \"%s\" has invalid value \"%s\" in preference storage. " + - "This shouldn't happen!", key, valueString) + Timber.w( + "Global setting \"%s\" has invalid value \"%s\" in preference storage. " + + "This shouldn't happen!", key, valueString + ) } } else { Timber.d("Couldn't find key \"%s\" in preference storage. Using default value.", key) @@ -100,7 +102,7 @@ class SettingsExporter( } } - private fun writeAccount(serializer: XmlSerializer, account: Account, prefs: Map) { + private fun writeAccount(serializer: XmlSerializer, account: Account, prefs: Map, withPassword: Boolean) { val identities = mutableSetOf() val accountUuid = account.uuid @@ -130,9 +132,9 @@ class SettingsExporter( writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType.name) } writeElement(serializer, USERNAME_ELEMENT, incoming.username) + if (withPassword) + writeElement(serializer, PASSWORD_ELEMENT, incoming.password) writeElement(serializer, CLIENT_CERTIFICATE_ALIAS_ELEMENT, incoming.clientCertificateAlias) - // XXX For now we don't export the password - // writeElement(serializer, PASSWORD_ELEMENT, incoming.password); var extras = incoming.extra if (!extras.isNullOrEmpty()) { @@ -160,9 +162,9 @@ class SettingsExporter( writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType.name) } writeElement(serializer, USERNAME_ELEMENT, outgoing.username) + if (withPassword) + writeElement(serializer, PASSWORD_ELEMENT, outgoing.password) writeElement(serializer, CLIENT_CERTIFICATE_ALIAS_ELEMENT, outgoing.clientCertificateAlias) - // XXX For now we don't export the password - // writeElement(serializer, PASSWORD_ELEMENT, outgoing.password); extras = outgoing.extra if (!extras.isNullOrEmpty()) { @@ -353,8 +355,9 @@ class SettingsExporter( try { writeKeyAndPrettyValueFromSetting(serializer, identityKey, setting, valueString) } catch (e: InvalidSettingValueException) { - Timber.w("Identity setting \"%s\" has invalid value \"%s\" in preference storage. " + - "This shouldn't happen!", identityKey, valueString + Timber.w( + "Identity setting \"%s\" has invalid value \"%s\" in preference storage. " + + "This shouldn't happen!", identityKey, valueString ) } } @@ -390,8 +393,10 @@ class SettingsExporter( try { writeKeyAndPrettyValueFromSetting(serializer, key, setting, value) } catch (e: InvalidSettingValueException) { - Timber.w("Folder setting \"%s\" has invalid value \"%s\" in preference storage. " + - "This shouldn't happen!", key, value) + Timber.w( + "Folder setting \"%s\" has invalid value \"%s\" in preference storage. " + + "This shouldn't happen!", key, value + ) } } } diff --git a/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java b/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java index a99543daeb1..941985b0524 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java +++ b/app/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.java @@ -1,22 +1,12 @@ package com.fsck.k9.preferences; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - import android.content.Context; import android.content.SharedPreferences; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; +import androidx.annotation.VisibleForTesting; + import com.fsck.k9.Account; import com.fsck.k9.AccountPreferenceSerializer; import com.fsck.k9.Core; @@ -33,9 +23,22 @@ import com.fsck.k9.mailstore.LocalStore; import com.fsck.k9.mailstore.LocalStoreProvider; import com.fsck.k9.preferences.Settings.InvalidSettingValueException; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + import timber.log.Timber; @@ -83,8 +86,8 @@ public static class AccountDescriptionPair { public final String outgoingServerName; private AccountDescriptionPair(AccountDescription original, AccountDescription imported, - boolean overwritten, boolean incomingPasswordNeeded, boolean outgoingPasswordNeeded, - String incomingServerName, String outgoingServerName) { + boolean overwritten, boolean incomingPasswordNeeded, boolean outgoingPasswordNeeded, + String incomingServerName, String outgoingServerName) { this.original = original; this.imported = imported; this.overwritten = overwritten; @@ -101,7 +104,7 @@ public static class ImportResults { public final List erroneousAccounts; private ImportResults(boolean globalSettings, List importedAccounts, - List erroneousAccounts) { + List erroneousAccounts) { this.globalSettings = globalSettings; this.importedAccounts = importedAccounts; this.erroneousAccounts = erroneousAccounts; @@ -113,14 +116,10 @@ private ImportResults(boolean globalSettings, List impor * settings and/or account settings. For all account configurations found, the name of the * account along with the account UUID is returned. * - * @param inputStream - * An {@code InputStream} to read the settings from. - * + * @param inputStream An {@code InputStream} to read the settings from. * @return An {@link ImportContents} instance containing information about the contents of the - * settings file. - * - * @throws SettingsImportExportException - * In case of an error. + * settings file. + * @throws SettingsImportExportException In case of an error. */ public static ImportContents getImportStreamContents(InputStream inputStream) throws SettingsImportExportException { @@ -157,24 +156,17 @@ public static ImportContents getImportStreamContents(InputStream inputStream) * Reads an import {@link InputStream} and imports the global settings and/or account * configurations specified by the arguments. * - * @param context - * A {@link Context} instance. - * @param inputStream - * The {@code InputStream} to read the settings from. - * @param globalSettings - * {@code true} if global settings should be imported from the file. - * @param accountUuids - * A list of UUIDs of the accounts that should be imported. - * @param overwrite - * {@code true} if existing accounts should be overwritten when an account with the - * same UUID is found in the settings file.
- * Note: This can have side-effects we currently don't handle, e.g. - * changing the account type from IMAP to POP3. So don't use this for now! + * @param context A {@link Context} instance. + * @param inputStream The {@code InputStream} to read the settings from. + * @param globalSettings {@code true} if global settings should be imported from the file. + * @param accountUuids A list of UUIDs of the accounts that should be imported. + * @param overwrite {@code true} if existing accounts should be overwritten when an account with the + * same UUID is found in the settings file.
+ * Note: This can have side-effects we currently don't handle, e.g. + * changing the account type from IMAP to POP3. So don't use this for now! * @return An {@link ImportResults} instance containing information about errors and - * successfully imported accounts. - * - * @throws SettingsImportExportException - * In case of an error. + * successfully imported accounts. + * @throws SettingsImportExportException In case of an error. */ public static ImportResults importSettings(Context context, InputStream inputStream, boolean globalSettings, List accountUuids, boolean overwrite) throws SettingsImportExportException { @@ -286,8 +278,7 @@ public static ImportResults importSettings(Context context, InputStream inputStr // create missing OUTBOX folders for (AccountDescriptionPair importedAccount : importedAccounts) { - String accountUuid = importedAccount.imported.uuid; - Account account = preferences.getAccount(accountUuid); + String accountUuid = importedAccount.imported.uuid;Account account = preferences.getAccount(accountUuid); LocalStore localStore = localStoreProvider.getInstance(account); long outboxFolderId = localStore.createLocalFolder(Account.OUTBOX_NAME, FolderType.OUTBOX); @@ -309,7 +300,7 @@ public static ImportResults importSettings(Context context, InputStream inputStr } private static void importGlobalSettings(Storage storage, StorageEditor editor, int contentVersion, - ImportedSettings settings) { + ImportedSettings settings) { // Validate global settings Map validatedSettings = GeneralSettingsDescriptions.validate(contentVersion, settings.settings); @@ -334,7 +325,7 @@ private static void importGlobalSettings(Storage storage, StorageEditor editor, } private static AccountDescriptionPair importAccount(Context context, StorageEditor editor, int contentVersion, - ImportedAccount account, boolean overwrite) throws InvalidSettingValueException { + ImportedAccount account, boolean overwrite) throws InvalidSettingValueException { AccountDescription original = new AccountDescription(account.name, account.uuid); @@ -399,7 +390,7 @@ private static AccountDescriptionPair importAccount(Context context, StorageEdit /* * Mark account as disabled if the settings file contained a username but no password. However, no password - * is required for the outgoing server for WebDAV accounts, because incoming and outgoing servers are + * is required for the outgoing server for WebDAV accounts, because incoming and outgoing servers are * identical for this account type. Nor is a password required if the AuthType is EXTERNAL. */ String outgoingServerType = ServerTypeConverter.toServerSettingsType(outgoing.type); @@ -474,7 +465,7 @@ private static AccountDescriptionPair importAccount(Context context, StorageEdit } private static void importFolder(StorageEditor editor, int contentVersion, String uuid, ImportedFolder folder, - boolean overwrite, Preferences prefs) { + boolean overwrite, Preferences prefs) { // Validate folder settings Map validatedSettings = @@ -507,7 +498,7 @@ private static void importFolder(StorageEditor editor, int contentVersion, Strin } private static void importIdentities(StorageEditor editor, int contentVersion, String uuid, ImportedAccount account, - boolean overwrite, Account existingAccount, Preferences prefs) throws InvalidSettingValueException { + boolean overwrite, Account existingAccount, Preferences prefs) throws InvalidSettingValueException { String accountKeyPrefix = uuid + "."; @@ -637,12 +628,9 @@ private static int findIdentity(ImportedIdentity identity, List identi * Write to an {@link SharedPreferences.Editor} while logging what is written if debug logging * is enabled. * - * @param editor - * The {@code Editor} to write to. - * @param key - * The name of the preference to modify. - * @param value - * The new value for the preference. + * @param editor The {@code Editor} to write to. + * @param key The name of the preference to modify. + * @param value The new value for the preference. */ private static void putString(StorageEditor editor, String key, String value) { if (K9.isDebugLoggingEnabled()) { @@ -657,7 +645,7 @@ private static void putString(StorageEditor editor, String key, String value) { @VisibleForTesting static Imported parseSettings(InputStream inputStream, boolean globalSettings, List accountUuids, - boolean overview) throws SettingsImportExportException { + boolean overview) throws SettingsImportExportException { if (!overview && accountUuids == null) { throw new IllegalArgumentException("Argument 'accountUuids' must not be null."); @@ -710,7 +698,7 @@ private static String getText(XmlPullParser xpp) throws XmlPullParserException, } private static Imported parseRoot(XmlPullParser xpp, boolean globalSettings, List accountUuids, - boolean overview) throws XmlPullParserException, IOException, SettingsImportExportException { + boolean overview) throws XmlPullParserException, IOException, SettingsImportExportException { Imported result = new Imported(); @@ -829,7 +817,7 @@ private static ImportedSettings parseSettings(XmlPullParser xpp, String endTag) } private static Map parseAccounts(XmlPullParser xpp, List accountUuids, - boolean overview) throws XmlPullParserException, IOException { + boolean overview) throws XmlPullParserException, IOException { Map accounts = null; diff --git a/app/core/src/test/java/com/fsck/k9/preferences/SettingsExporterTest.kt b/app/core/src/test/java/com/fsck/k9/preferences/SettingsExporterTest.kt index e9694668a4d..1ee9fb6589d 100644 --- a/app/core/src/test/java/com/fsck/k9/preferences/SettingsExporterTest.kt +++ b/app/core/src/test/java/com/fsck/k9/preferences/SettingsExporterTest.kt @@ -66,7 +66,7 @@ class SettingsExporterTest : K9RobolectricTest() { private fun exportPreferences(globalSettings: Boolean, accounts: Set): Document { return ByteArrayOutputStream().use { outputStream -> - settingsExporter.exportPreferences(outputStream, globalSettings, accounts) + settingsExporter.exportPreferences(outputStream, globalSettings, accounts, false) parseXml(outputStream.toByteArray()) } } diff --git a/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportFragment.kt b/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportFragment.kt index 366a189a7fa..a1d932e4199 100644 --- a/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportFragment.kt +++ b/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportFragment.kt @@ -97,6 +97,7 @@ class SettingsExportFragment : Fragment() { val checkBoxItems = items.map { item -> val checkBoxItem = when (item) { is SettingsListItem.GeneralSettings -> GeneralSettingsItem() + is SettingsListItem.Passwords -> PasswordItem() is SettingsListItem.Account -> AccountItem(item) } diff --git a/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportListItems.kt b/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportListItems.kt index 8d9eb8ebe25..6e569eada9e 100644 --- a/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportListItems.kt +++ b/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportListItems.kt @@ -4,13 +4,20 @@ import com.fsck.k9.ui.R import kotlinx.android.synthetic.main.settings_export_account_list_item.* private const val GENERAL_SETTINGS_ID = 0L -private const val ACCOUNT_ITEMS_ID_OFFSET = 1L +private const val PASSWORD_ID = 1L +private const val ACCOUNT_ITEMS_ID_OFFSET = 2L + class GeneralSettingsItem : CheckBoxItem(GENERAL_SETTINGS_ID) { override val type = R.id.settings_export_list_general_item override val layoutRes = R.layout.settings_export_general_list_item } +class PasswordItem : CheckBoxItem(PASSWORD_ID) { + override val type = R.id.settings_export_list_password_item + override val layoutRes = R.layout.settings_export_password_item +} + class AccountItem(account: SettingsListItem.Account) : CheckBoxItem(account.accountNumber + ACCOUNT_ITEMS_ID_OFFSET) { private val displayName = account.displayName private val email = account.email @@ -18,9 +25,9 @@ class AccountItem(account: SettingsListItem.Account) : CheckBoxItem(account.acco override val type = R.id.settings_export_list_account_item override val layoutRes = R.layout.settings_export_account_list_item - override fun bindView(viewHolder: CheckBoxViewHolder, payloads: MutableList) { - super.bindView(viewHolder, payloads) - viewHolder.accountDisplayName.text = displayName - viewHolder.accountEmail.text = email + override fun bindView(holder: CheckBoxViewHolder, payloads: MutableList) { + super.bindView(holder, payloads) + holder.accountDisplayName.text = displayName + holder.accountEmail.text = email } } diff --git a/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportUiModel.kt b/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportUiModel.kt index 80295f17f91..71f7f1a1b8f 100644 --- a/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportUiModel.kt +++ b/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportUiModel.kt @@ -70,9 +70,10 @@ class SettingsExportUiModel { } sealed class SettingsListItem { - var selected: Boolean = true + var selected: Boolean = true object GeneralSettings : SettingsListItem() + object Passwords : SettingsListItem() data class Account( val accountNumber: Int, val displayName: String, diff --git a/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportViewModel.kt b/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportViewModel.kt index 986e2ca1cb8..2a20c7b9ca7 100644 --- a/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportViewModel.kt +++ b/app/ui/src/main/java/com/fsck/k9/ui/settings/export/SettingsExportViewModel.kt @@ -38,6 +38,12 @@ class SettingsExportViewModel( ?: uiModel.settingsList.first { it is SettingsListItem.GeneralSettings }.selected } + private val includePasswords: Boolean + get() { + return savedSelection?.includePasswords + ?: uiModel.settingsList.first { it is SettingsListItem.Passwords }.selected + } + private val selectedAccounts: Set get() { return savedSelection?.selectedAccountUuids @@ -65,6 +71,9 @@ class SettingsExportViewModel( val generalSettings = SettingsListItem.GeneralSettings.apply { selected = savedState == null || savedState.includeGeneralSettings } + val passwords = SettingsListItem.Passwords.apply { + selected = savedState == null || savedState.includePasswords + } val accountListItems = accounts.map { account -> SettingsListItem.Account(account.accountNumber, account.displayName, account.email).apply { @@ -72,7 +81,7 @@ class SettingsExportViewModel( } } - listOf(generalSettings) + accountListItems + listOf(generalSettings, passwords) + accountListItems } updateUiModel { @@ -87,6 +96,7 @@ class SettingsExportViewModel( fun initializeFromSavedState(savedInstanceState: Bundle) { savedSelection = SavedListItemSelection( includeGeneralSettings = savedInstanceState.getBoolean(STATE_INCLUDE_GENERAL_SETTINGS), + includePasswords = savedInstanceState.getBoolean(STATE_INCLUDE_PASSWORDS), selectedAccountUuids = savedInstanceState.getStringArray(STATE_SELECTED_ACCOUNTS)?.toSet() ?: emptySet() ) @@ -128,13 +138,14 @@ class SettingsExportViewModel( } val includeGeneralSettings = this.includeGeneralSettings + val includePasswords = this.includePasswords val selectedAccounts = this.selectedAccounts viewModelScope.launch { try { val elapsed = measureRealtimeMillis { withContext(Dispatchers.IO) { - settingsExporter.exportToUri(includeGeneralSettings, selectedAccounts, contentUri) + settingsExporter.exportToUri(includeGeneralSettings, selectedAccounts, contentUri, includePasswords) } } @@ -167,6 +178,7 @@ class SettingsExportViewModel( outState.putString(STATE_STATUS_TEXT, uiModel.statusText.name) outState.putBoolean(STATE_INCLUDE_GENERAL_SETTINGS, includeGeneralSettings) + outState.putBoolean(STATE_INCLUDE_PASSWORDS, includePasswords) outState.putStringArray(STATE_SELECTED_ACCOUNTS, selectedAccounts.toTypedArray()) outState.putParcelable(STATE_CONTENT_URI, contentUri) @@ -199,6 +211,7 @@ class SettingsExportViewModel( private const val STATE_PROGRESS_VISIBLE = "progressVisible" private const val STATE_STATUS_TEXT = "statusText" private const val STATE_INCLUDE_GENERAL_SETTINGS = "includeGeneralSettings" + private const val STATE_INCLUDE_PASSWORDS = "includePasswords" private const val STATE_SELECTED_ACCOUNTS = "selectedAccounts" private const val STATE_CONTENT_URI = "contentUri" } @@ -211,5 +224,6 @@ sealed class Action { private data class SavedListItemSelection( val includeGeneralSettings: Boolean, + val includePasswords: Boolean, val selectedAccountUuids: Set ) diff --git a/app/ui/src/main/res/layout/settings_export_password_item.xml b/app/ui/src/main/res/layout/settings_export_password_item.xml new file mode 100644 index 00000000000..5b9c7a91627 --- /dev/null +++ b/app/ui/src/main/res/layout/settings_export_password_item.xml @@ -0,0 +1,44 @@ + + + + + + + + + + diff --git a/app/ui/src/main/res/values/ids.xml b/app/ui/src/main/res/values/ids.xml index daf495e05e4..b12216299c9 100644 --- a/app/ui/src/main/res/values/ids.xml +++ b/app/ui/src/main/res/values/ids.xml @@ -9,6 +9,7 @@ + diff --git a/app/ui/src/main/res/values/strings.xml b/app/ui/src/main/res/values/strings.xml index 7405dfc3b6d..1beab03d8fa 100644 --- a/app/ui/src/main/res/values/strings.xml +++ b/app/ui/src/main/res/values/strings.xml @@ -1206,6 +1206,7 @@ Please submit bug reports, contribute new features and ask questions at You can click here to learn more. General settings + Passwords (plain text) No OpenPGP app installed Install