Merge pull request #4053 from k9mail/settings_export_fragment

Add settings export to settings screen
This commit is contained in:
cketti 2019-05-21 21:03:30 +02:00 committed by GitHub
commit 1e8b66e29b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 888 additions and 2 deletions

View file

@ -0,0 +1,12 @@
package com.fsck.k9.helper
import android.os.SystemClock
/**
* Executes the given [block] and returns elapsed realtime in milliseconds.
*/
inline fun measureRealtimeMillis(block: () -> Unit): Long {
val start = SystemClock.elapsedRealtime()
block()
return SystemClock.elapsedRealtime() - start
}

View file

@ -28,6 +28,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-extensions:${versions.androidxLifecycleExtensions}"
implementation "androidx.navigation:navigation-fragment-ktx:${versions.androidxNavigation}"
implementation "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}"
implementation "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}"
implementation "de.cketti.library.changelog:ckchangelog:1.2.1"
implementation "com.github.bumptech.glide:glide:3.6.1"
implementation "com.splitwise:tokenautocomplete:2.0.7"
@ -38,6 +39,7 @@ dependencies {
implementation 'com.mikepenz:materialdrawer:6.1.1'
implementation 'com.mikepenz:fontawesome-typeface:5.3.1.1@aar'
implementation 'com.github.ByteHamster:SearchPreference:v1.1.4'
implementation 'com.mikepenz:fastadapter:3.3.1'
implementation "commons-io:commons-io:${versions.commonsIo}"
implementation "androidx.core:core-ktx:${versions.coreKtx}"

View file

@ -0,0 +1,16 @@
package com.fsck.k9.ui.helper
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
abstract class CoroutineScopeViewModel : ViewModel(), CoroutineScope {
private val job = Job()
override val coroutineContext = Dispatchers.Main + job
override fun onCleared() {
job.cancel()
}
}

View file

@ -4,6 +4,7 @@ import com.fsck.k9.helper.NamedThreadFactory
import com.fsck.k9.ui.account.AccountsLiveData
import com.fsck.k9.ui.settings.account.AccountSettingsDataStoreFactory
import com.fsck.k9.ui.settings.account.AccountSettingsViewModel
import com.fsck.k9.ui.settings.export.SettingsExportViewModel
import com.fsck.k9.ui.settings.general.GeneralSettingsDataStore
import org.koin.android.architecture.ext.viewModel
import org.koin.dsl.module.applicationContext
@ -20,4 +21,6 @@ val settingsUiModule = applicationContext {
viewModel { AccountSettingsViewModel(get(), get()) }
bean { AccountSettingsDataStoreFactory(get(), get(), get("SaveSettingsExecutorService")) }
viewModel { SettingsExportViewModel(get(), get()) }
}

View file

@ -93,6 +93,17 @@ class SettingsListFragment : Fragment() {
}
accountSection.setHeader(SettingsDividerItem(getString(R.string.accounts_title)))
settingsAdapter.add(accountSection)
val backupSection = Section().apply {
val exportSettingsActionItem = SettingsActionItem(
getString(R.string.settings_export_title),
R.id.action_settingsListScreen_to_settingsExportScreen,
R.attr.iconSettingsExport
)
add(exportSettingsActionItem)
}
backupSection.setHeader(SettingsDividerItem(getString(R.string.settings_list_backup_category)))
settingsAdapter.add(backupSection)
}
private fun handleItemClick(item: Item<*>) {

View file

@ -0,0 +1,39 @@
package com.fsck.k9.ui.settings.export
import android.view.View
import android.widget.CheckBox
import androidx.recyclerview.widget.RecyclerView
import com.fsck.k9.ui.R
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.items.AbstractItem
import com.mikepenz.fastadapter.listeners.ClickEventHook
import kotlinx.android.extensions.LayoutContainer
abstract class CheckBoxItem(private val id: Long) : AbstractItem<CheckBoxItem, CheckBoxViewHolder>() {
override fun getIdentifier(): Long = id
override fun getViewHolder(view: View): CheckBoxViewHolder = CheckBoxViewHolder(view)
override fun bindView(viewHolder: CheckBoxViewHolder, payloads: List<Any>) {
super.bindView(viewHolder, payloads)
viewHolder.checkBox.isChecked = isSelected
viewHolder.itemView.isEnabled = isEnabled
viewHolder.checkBox.isEnabled = isEnabled
}
}
class CheckBoxViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), LayoutContainer {
val checkBox: CheckBox = itemView.findViewById(R.id.checkBox)
override val containerView = itemView
}
class CheckBoxClickEvent(val action: (position: Int, isSelected: Boolean) -> Unit) : ClickEventHook<CheckBoxItem>() {
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
return if (viewHolder is CheckBoxViewHolder) viewHolder.checkBox else null
}
override fun onClick(view: View, position: Int, fastAdapter: FastAdapter<CheckBoxItem>, item: CheckBoxItem) {
action(position, !item.isSelected)
}
}

View file

@ -0,0 +1,158 @@
package com.fsck.k9.ui.settings.export
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import com.fsck.k9.ui.R
import com.fsck.k9.ui.observeNotNull
import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
import kotlinx.android.synthetic.main.fragment_settings_export.*
import org.koin.android.architecture.ext.viewModel
class SettingsExportFragment : Fragment() {
private val viewModel: SettingsExportViewModel by viewModel()
private lateinit var settingsExportAdapter: FastItemAdapter<CheckBoxItem>
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_settings_export, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (savedInstanceState != null) {
viewModel.initializeFromSavedState(savedInstanceState)
}
initializeSettingsExportList(view)
exportButton.setOnClickListener { viewModel.onExportButtonClicked() }
shareButton.setOnClickListener { viewModel.onShareButtonClicked() }
viewModel.getUiModel().observeNotNull(this) { updateUi(it) }
viewModel.getActionEvents().observeNotNull(this) { handleActionEvents(it) }
}
private fun initializeSettingsExportList(view: View) {
settingsExportAdapter = FastItemAdapter<CheckBoxItem>().apply {
setHasStableIds(true)
withOnClickListener { _, _, item: CheckBoxItem, position ->
viewModel.onSettingsListItemSelected(position, !item.isSelected)
true
}
withEventHook(CheckBoxClickEvent { position, isSelected ->
viewModel.onSettingsListItemSelected(position, isSelected)
})
}
val recyclerView = view.findViewById<RecyclerView>(R.id.settingsExportList)
recyclerView.adapter = settingsExportAdapter
}
private fun updateUi(model: SettingsExportUiModel) {
when (model.exportButton) {
ButtonState.DISABLED -> {
exportButton.visibility = View.VISIBLE
exportButton.isEnabled = false
}
ButtonState.ENABLED -> {
exportButton.visibility = View.VISIBLE
exportButton.isEnabled = true
}
ButtonState.INVISIBLE -> exportButton.visibility = View.INVISIBLE
ButtonState.GONE -> exportButton.visibility = View.GONE
}
shareButton.visibility = if (model.isShareButtonVisible) View.VISIBLE else View.GONE
progressBar.visibility = if (model.isProgressVisible) View.VISIBLE else View.GONE
when (model.statusText) {
StatusText.HIDDEN -> statusText.visibility = View.GONE
StatusText.EXPORT_SUCCESS -> {
statusText.visibility = View.VISIBLE
statusText.text = getString(R.string.settings_export_success_generic)
}
StatusText.PROGRESS -> {
statusText.visibility = View.VISIBLE
statusText.text = getString(R.string.settings_export_progress_text)
}
StatusText.EXPORT_FAILURE -> {
statusText.visibility = View.VISIBLE
statusText.text = getString(R.string.settings_export_failure)
}
}
setSettingsList(model.settingsList, model.isSettingsListEnabled)
}
//TODO: Update list instead of replacing it completely
private fun setSettingsList(items: List<SettingsListItem>, enable: Boolean) {
val checkBoxItems = items.map { item ->
val checkBoxItem = when (item) {
is SettingsListItem.GeneralSettings -> GeneralSettingsItem()
is SettingsListItem.Account -> AccountItem(item)
}
checkBoxItem
.withSetSelected(item.selected)
.withEnabled(enable)
}
settingsExportAdapter.set(checkBoxItems)
settingsExportList.isEnabled = enable
}
private fun handleActionEvents(action: Action) {
when (action) {
is Action.PickDocument -> pickDocument(action.fileNameSuggestion, action.mimeType)
is Action.ShareDocument -> shareDocument(action.contentUri, action.mimeType)
}
}
private fun pickDocument(fileNameSuggestion: String, mimeType: String) {
val createDocumentIntent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = mimeType
putExtra(Intent.EXTRA_TITLE, fileNameSuggestion)
addCategory(Intent.CATEGORY_OPENABLE)
}
startActivityForResult(createDocumentIntent, RESULT_PICK_DOCUMENT)
}
private fun shareDocument(contentUri: Uri, mimeType: String) {
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = mimeType
putExtra(Intent.EXTRA_STREAM, contentUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(shareIntent, null))
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
viewModel.saveInstanceState(outState)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == RESULT_PICK_DOCUMENT) {
val contentUri = data?.data
if (resultCode == Activity.RESULT_OK && contentUri != null) {
viewModel.onDocumentPicked(contentUri)
} else {
viewModel.onDocumentPickCanceled()
}
}
}
companion object {
private const val RESULT_PICK_DOCUMENT = Activity.RESULT_FIRST_USER
}
}

View file

@ -0,0 +1,32 @@
package com.fsck.k9.ui.settings.export
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
class GeneralSettingsItem : CheckBoxItem(GENERAL_SETTINGS_ID) {
override fun getType(): Int = R.id.settings_export_list_general_item
override fun getLayoutRes(): Int = R.layout.settings_export_general_list_item
}
class AccountItem(account: SettingsListItem.Account) : CheckBoxItem(account.accountNumber + ACCOUNT_ITEMS_ID_OFFSET) {
private val displayName = account.displayName
private val email = account.email
override fun getType(): Int = R.id.settings_export_list_account_item
override fun getLayoutRes(): Int = R.layout.settings_export_account_list_item
override fun bindView(viewHolder: CheckBoxViewHolder, payloads: List<Any>) {
super.bindView(viewHolder, payloads)
viewHolder.accountDisplayName.text = displayName
viewHolder.accountEmail.text = email
}
}

View file

@ -0,0 +1,99 @@
package com.fsck.k9.ui.settings.export
import com.fsck.k9.Account as K9Account
class SettingsExportUiModel {
var settingsList: List<SettingsListItem> = emptyList()
var isSettingsListEnabled = true
var exportButton: ButtonState = ButtonState.DISABLED
var isShareButtonVisible = false
var isProgressVisible = false
var statusText = StatusText.HIDDEN
fun enableExportButton() {
exportButton = ButtonState.ENABLED
isShareButtonVisible = false
isProgressVisible = false
isSettingsListEnabled = true
}
fun disableExportButton() {
exportButton = ButtonState.DISABLED
isShareButtonVisible = false
isProgressVisible = false
}
fun showProgress() {
isProgressVisible = true
exportButton = ButtonState.INVISIBLE
isShareButtonVisible = false
statusText = StatusText.PROGRESS
isSettingsListEnabled = false
}
fun showSuccessText() {
exportButton = ButtonState.GONE
isProgressVisible = false
isShareButtonVisible = true
isSettingsListEnabled = true
statusText = StatusText.EXPORT_SUCCESS
}
fun showFailureText() {
exportButton = ButtonState.GONE
isShareButtonVisible = false
isProgressVisible = false
isSettingsListEnabled = true
statusText = StatusText.EXPORT_FAILURE
}
fun initializeSettingsList(list: List<SettingsListItem>) {
settingsList = list
updateExportButtonFromSelection()
}
fun setSettingsListItemSelection(position: Int, select: Boolean) {
settingsList[position].selected = select
statusText = StatusText.HIDDEN
isShareButtonVisible = false
updateExportButtonFromSelection()
}
private fun updateExportButtonFromSelection() {
if (isProgressVisible || isShareButtonVisible) return
val atLeastOnceSelected = settingsList.any { it.selected }
if (atLeastOnceSelected) {
enableExportButton()
} else {
disableExportButton()
}
}
}
sealed class SettingsListItem {
var selected: Boolean = true
object GeneralSettings : SettingsListItem()
data class Account(
val accountNumber: Int,
val displayName: String,
val email: String
) : SettingsListItem()
}
enum class ButtonState {
DISABLED,
ENABLED,
INVISIBLE,
GONE
}
enum class StatusText {
HIDDEN,
PROGRESS,
EXPORT_SUCCESS,
EXPORT_FAILURE
}

View file

@ -0,0 +1,213 @@
package com.fsck.k9.ui.settings.export
import android.content.Context
import android.net.Uri
import android.os.Bundle
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.fsck.k9.Preferences
import com.fsck.k9.helper.SingleLiveEvent
import com.fsck.k9.helper.measureRealtimeMillis
import com.fsck.k9.preferences.SettingsExporter
import com.fsck.k9.ui.helper.CoroutineScopeViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private typealias AccountUuid = String
private typealias AccountNumber = Int
class SettingsExportViewModel(val context: Context, val preferences: Preferences) : CoroutineScopeViewModel() {
private val uiModelLiveData = MutableLiveData<SettingsExportUiModel>()
private val actionLiveData = SingleLiveEvent<Action>()
private val uiModel = SettingsExportUiModel()
private var accountsMap: Map<AccountNumber, AccountUuid> = emptyMap()
private var savedSelection: SavedListItemSelection? = null
private var contentUri: Uri? = null
private val includeGeneralSettings: Boolean
get() {
return savedSelection?.includeGeneralSettings
?: uiModel.settingsList.first { it is SettingsListItem.GeneralSettings }.selected
}
private val selectedAccounts: Set<AccountUuid>
get() {
return savedSelection?.selectedAccountUuids
?: uiModel.settingsList.asSequence()
.filterIsInstance<SettingsListItem.Account>()
.filter { it.selected }
.map {
accountsMap[it.accountNumber] ?: error("Unknown account number: ${it.accountNumber}")
}
.toSet()
}
fun getActionEvents(): LiveData<Action> = actionLiveData
fun getUiModel(): LiveData<SettingsExportUiModel> {
if (uiModelLiveData.value == null) {
uiModelLiveData.value = uiModel
launch {
val accounts = withContext(Dispatchers.IO) { preferences.accounts }
accountsMap = accounts.map { it.accountNumber to it.uuid }.toMap()
val listItems = savedSelection.let { savedState ->
val generalSettings = SettingsListItem.GeneralSettings.apply {
selected = savedState == null || savedState.includeGeneralSettings
}
val accountListItems = accounts.map { account ->
SettingsListItem.Account(account.accountNumber, account.displayName, account.email).apply {
selected = savedState == null || account.uuid in savedState.selectedAccountUuids
}
}
listOf(generalSettings) + accountListItems
}
updateUiModel {
initializeSettingsList(listItems)
}
}
}
return uiModelLiveData
}
fun initializeFromSavedState(savedInstanceState: Bundle) {
savedSelection = SavedListItemSelection(
includeGeneralSettings = savedInstanceState.getBoolean(STATE_INCLUDE_GENERAL_SETTINGS),
selectedAccountUuids = savedInstanceState.getStringArray(STATE_SELECTED_ACCOUNTS)?.toSet() ?: emptySet()
)
uiModel.apply {
isSettingsListEnabled = savedInstanceState.getBoolean(STATE_SETTINGS_LIST_ENABLED)
exportButton = ButtonState.valueOf(
savedInstanceState.getString(STATE_EXPORT_BUTTON, ButtonState.DISABLED.name)
)
isShareButtonVisible = savedInstanceState.getBoolean(STATE_SHARE_BUTTON_VISIBLE)
isProgressVisible = savedInstanceState.getBoolean(STATE_PROGRESS_VISIBLE)
statusText = StatusText.valueOf(savedInstanceState.getString(STATE_STATUS_TEXT, StatusText.HIDDEN.name))
}
contentUri = savedInstanceState.getParcelable(STATE_CONTENT_URI)
}
fun onExportButtonClicked() {
updateUiModel {
disableExportButton()
}
startExportSettings()
}
fun onShareButtonClicked() {
sendActionEvent(Action.ShareDocument(contentUri!!, SETTINGS_MIME_TYPE))
}
private fun startExportSettings() {
val exportFileName = SettingsExporter.generateDatedExportFileName()
sendActionEvent(Action.PickDocument(exportFileName, SETTINGS_MIME_TYPE))
}
fun onDocumentPicked(contentUri: Uri) {
this.contentUri = contentUri
updateUiModel {
showProgress()
}
val includeGeneralSettings = this.includeGeneralSettings
val selectedAccounts = this.selectedAccounts
launch {
try {
val elapsed = measureRealtimeMillis {
withContext(Dispatchers.IO) {
SettingsExporter.exportToUri(context, includeGeneralSettings, selectedAccounts, contentUri)
}
}
if (elapsed < MIN_PROGRESS_DURATION) {
delay(MIN_PROGRESS_DURATION - elapsed)
}
updateUiModel {
showSuccessText()
}
} catch (e: Exception) {
updateUiModel {
showFailureText()
}
}
}
}
fun onDocumentPickCanceled() {
updateUiModel {
enableExportButton()
}
}
fun saveInstanceState(outState: Bundle) {
outState.putBoolean(STATE_SETTINGS_LIST_ENABLED, uiModel.isSettingsListEnabled)
outState.putString(STATE_EXPORT_BUTTON, uiModel.exportButton.name)
outState.putBoolean(STATE_SHARE_BUTTON_VISIBLE, uiModel.isShareButtonVisible)
outState.putBoolean(STATE_PROGRESS_VISIBLE, uiModel.isProgressVisible)
outState.putString(STATE_STATUS_TEXT, uiModel.statusText.name)
outState.putBoolean(STATE_INCLUDE_GENERAL_SETTINGS, includeGeneralSettings)
outState.putStringArray(STATE_SELECTED_ACCOUNTS, selectedAccounts.toTypedArray())
outState.putParcelable(STATE_CONTENT_URI, contentUri)
}
fun onSettingsListItemSelected(position: Int, isSelected: Boolean) {
savedSelection = null
updateUiModel {
setSettingsListItemSelection(position, isSelected)
}
}
private fun updateUiModel(block: SettingsExportUiModel.() -> Unit) {
uiModel.block()
uiModelLiveData.value = uiModel
}
private fun sendActionEvent(action: Action) {
actionLiveData.value = action
}
companion object {
private const val MIN_PROGRESS_DURATION = 1000L
private const val SETTINGS_MIME_TYPE = "application/octet-stream"
private const val STATE_SETTINGS_LIST_ENABLED = "settingsListEnabled"
private const val STATE_EXPORT_BUTTON = "exportButton"
private const val STATE_SHARE_BUTTON_VISIBLE = "shareButtonVisible"
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_SELECTED_ACCOUNTS = "selectedAccounts"
private const val STATE_CONTENT_URI = "contentUri"
}
}
sealed class Action {
class PickDocument(val fileNameSuggestion: String, val mimeType: String) : Action()
class ShareDocument(val contentUri: Uri, val mimeType: String) : Action()
}
private data class SavedListItemSelection(
val includeGeneralSettings: Boolean,
val selectedAccountUuids: Set<AccountUuid>
)

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="m8,7h3V16h2V7h3L12,3ZM4,19v2h16v-2z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:alpha="0.54"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="m8,7h3V16h2V7h3L12,3ZM4,19v2h16v-2z"/>
</vector>

View file

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/settingsExportList"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/bottomBar"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/settings_export_account_list_item" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottomBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/bottomBarBackground"
android:elevation="8dp"
android:minHeight="56dp"
app:layout_constraintBottom_toBottomOf="parent"
tools:ignore="UnusedAttribute">
<Button
android:id="@+id/exportButton"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:enabled="false"
android:text="@string/settings_export_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/shareButton"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:text="@string/settings_export_share_button"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/buttonBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="left"
app:constraint_referenced_ids="exportButton,shareButton" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="32dp"
android:layout_height="32dp"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/exportButton"
app:layout_constraintEnd_toEndOf="@+id/exportButton"
app:layout_constraintStart_toStartOf="@+id/exportButton"
app:layout_constraintTop_toTopOf="@+id/exportButton" />
<TextView
android:id="@+id/statusText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/buttonBarrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Exporting…"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<CheckBox
android:id="@+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/accountDisplayName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:duplicateParentState="true"
android:textAppearance="?android:attr/textAppearanceListItem"
app:layout_constraintBottom_toTopOf="@+id/accountEmail"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Account name" />
<TextView
android:id="@+id/accountEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:duplicateParentState="true"
android:textAppearance="?android:attr/textAppearanceSmall"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/accountDisplayName"
tools:text="user@domain.example" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="72dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:minHeight="?android:attr/listPreferredItemHeightSmall">
<CheckBox
android:id="@+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:duplicateParentState="true"
android:text="@string/general_settings_title"
android:textAppearance="?android:attr/textAppearanceListItem"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="72dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -22,6 +22,9 @@
<action
android:id="@+id/action_settingsListScreen_to_addAccountScreen"
app:destination="@id/addAccountScreen" />
<action
android:id="@+id/action_settingsListScreen_to_settingsExportScreen"
app:destination="@id/settingsExportScreen" />
</fragment>
<activity
@ -41,4 +44,10 @@
android:label="@string/account_setup_basics_title"
tools:layout="@layout/account_setup_basics"/>
<fragment
android:id="@+id/settingsExportScreen"
android:name="com.fsck.k9.ui.settings.export.SettingsExportFragment"
android:label="@string/settings_export_title"
tools:layout="@layout/fragment_settings_export"/>
</navigation>

View file

@ -2,6 +2,7 @@
<resources>
<declare-styleable name="K9Styles">
<attr name="bottomBarBackground" format="reference|color" />
<attr name="iconUnifiedInbox" format="reference" />
<attr name="iconFolder" format="reference" />
<attr name="iconFolderInbox" format="reference" />
@ -63,6 +64,7 @@
<attr name="iconSettingsGeneral" format="reference" />
<attr name="iconSettingsAccount" format="reference" />
<attr name="iconSettingsAccountAdd" format="reference" />
<attr name="iconSettingsExport" format="reference" />
<attr name="textColorPrimaryRecipientDropdown" format="reference" />
<attr name="textColorSecondaryRecipientDropdown" format="reference" />
<attr name="backgroundColorChooseAccountHeader" format="color" />

View file

@ -8,4 +8,7 @@
<item type="id" name="dialog_attachment_progress"/>
<item type="id" name="dialog_account_setup_error"/>
<item type="id" name="settings_export_list_general_item"/>
<item type="id" name="settings_export_list_account_item"/>
</resources>

View file

@ -991,6 +991,15 @@ Please submit bug reports, contribute new features and ask questions at
<string name="messagelist_sent_to_me_sigil">»</string>
<string name="messagelist_sent_cc_me_sigil"></string>
<string name="settings_list_backup_category">Backup</string>
<string name="settings_export_title">Export settings</string>
<string name="settings_export_button">Export</string>
<string name="settings_export_share_button">Share</string>
<string name="settings_export_progress_text">Exporting settings…</string>
<string name="settings_export_success_generic">Settings successfully exported</string>
<string name="settings_export_failure">Failed to export settings</string>
<string name="import_export_action">Settings Import &amp; Export</string>
<string name="settings_export_account">Export account settings</string>
<string name="settings_export_all">Export settings and accounts</string>
@ -1002,7 +1011,6 @@ Please submit bug reports, contribute new features and ask questions at
<string name="settings_exporting">Exporting settings…</string>
<string name="settings_importing">Importing settings…</string>
<string name="settings_import_scanning_file">Scanning file…</string>
<string name="settings_export_success_generic">Settings successfully exported</string>
<string name="settings_import_global_settings_success">Imported global settings</string>
<string name="settings_import_success">Imported <xliff:g id="accounts">%s</xliff:g></string>
<string name="settings_import_account_imported_as">Imported <xliff:g id="original_account_name">%s</xliff:g> as <xliff:g id="account_name_after_import">%s</xliff:g></string>
@ -1010,7 +1018,6 @@ Please submit bug reports, contribute new features and ask questions at
<item quantity="one">1 account</item>
<item quantity="other"><xliff:g id="numAccounts">%s</xliff:g> accounts</item>
</plurals>
<string name="settings_export_failure">Failed to export settings</string>
<string name="settings_import_failure">Failed to import any settings</string>
<string name="settings_export_success_header">Export succeeded</string>
<string name="settings_export_failed_header">Export failed</string>

View file

@ -14,6 +14,7 @@
<item name="android:windowLightStatusBar" tools:targetApi="23">true</item>
<item name="colorPrimary">@color/md_grey_100</item>
<item name="colorPrimaryDark">@color/md_grey_100</item>
<item name="bottomBarBackground">@color/md_grey_50</item>
<item name="material_drawer_background">@color/material_drawer_background</item>
<item name="material_drawer_primary_text">@color/material_drawer_primary_text</item>
@ -89,6 +90,7 @@
<item name="iconSettingsGeneral">@drawable/ic_cog_light</item>
<item name="iconSettingsAccount">@drawable/ic_account_light</item>
<item name="iconSettingsAccountAdd">@drawable/ic_account_plus_light</item>
<item name="iconSettingsExport">@drawable/ic_export_light</item>
<item name="textColorPrimaryRecipientDropdown">@android:color/primary_text_light</item>
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_light</item>
<item name="messageListSelectedBackgroundColor">#8038B8E2</item>
@ -130,6 +132,7 @@
<item name="android:windowLightStatusBar" tools:targetApi="23">false</item>
<item name="colorPrimary">@color/md_grey_900</item>
<item name="colorPrimaryDark">@color/md_grey_900</item>
<item name="bottomBarBackground">@color/md_grey_900</item>
<item name="material_drawer_background">@color/material_drawer_dark_background</item>
<item name="material_drawer_primary_text">@color/material_drawer_dark_primary_text</item>
@ -205,6 +208,7 @@
<item name="iconSettingsGeneral">@drawable/ic_cog_dark</item>
<item name="iconSettingsAccount">@drawable/ic_account_dark</item>
<item name="iconSettingsAccountAdd">@drawable/ic_account_plus_dark</item>
<item name="iconSettingsExport">@drawable/ic_export_dark</item>
<item name="textColorPrimaryRecipientDropdown">@android:color/primary_text_dark</item>
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_dark</item>
<item name="messageListSelectedBackgroundColor">#8038B8E2</item>

View file

@ -13,6 +13,7 @@ buildscript {
'androidxLifecycleExtensions': '2.0.0',
'androidxAnnotation': '1.0.1',
'androidxNavigation': '2.0.0',
'androidxConstraintLayout': '1.1.3',
'coreKtx': '1.0.1',
'preferencesFix': '1.0.0',
'okio': '1.14.0',

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 24 24"
version="1.1"
id="svg6"
sodipodi:docname="ic_export.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1859"
inkscape:window-height="1145"
id="namedview8"
showgrid="false"
inkscape:zoom="27.812866"
inkscape:cx="18.87956"
inkscape:cy="10.262807"
inkscape:window-x="61"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<path
d="M0 0h24v24H0z"
fill="none"
id="path4" />
<path
id="path822"
d="m 8,6.9999999 h 3 V 16 h 2 V 6.9999999 h 3 L 12,3.0000001 Z M 4,19 v 2 h 16 v -2 z"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB