Show Backup Location screen before letting user choose backup folder

This screen can also be reached by tapping the previously inactive backup location setting.
This commit is contained in:
Torsten Grote 2019-07-08 16:02:53 +02:00
parent 3e64c3686f
commit 4c79d41963
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
9 changed files with 163 additions and 51 deletions

View file

@ -0,0 +1,59 @@
package com.stevesoltys.backup.settings
import android.app.Activity.RESULT_OK
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.Intent.*
import android.os.Bundle
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.lifecycle.ViewModelProviders
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.stevesoltys.backup.R
private val TAG = BackupLocationFragment::class.java.name
class BackupLocationFragment : PreferenceFragmentCompat() {
private lateinit var viewModel: SettingsViewModel
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.backup_location, rootKey)
requireActivity().setTitle(R.string.settings_backup_location_title)
viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java)
val externalStorage = Preference(requireContext()).apply {
setIcon(R.drawable.ic_storage)
setTitle(R.string.settings_backup_external_storage)
setOnPreferenceClickListener {
showChooseFolderActivity()
true
}
}
preferenceScreen.addPreference(externalStorage)
}
private fun showChooseFolderActivity() {
val openTreeIntent = Intent(ACTION_OPEN_DOCUMENT_TREE)
openTreeIntent.addFlags(FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION)
try {
val documentChooser = createChooser(openTreeIntent, null)
startActivityForResult(documentChooser, REQUEST_CODE_OPEN_DOCUMENT_TREE)
} catch (ex: ActivityNotFoundException) {
Toast.makeText(requireContext(), "Please install a file manager.", LENGTH_LONG).show()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) {
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) {
viewModel.handleChooseFolderResult(result)
} else {
super.onActivityResult(requestCode, resultCode, result)
}
}
}

View file

@ -1,6 +1,7 @@
package com.stevesoltys.backup.settings package com.stevesoltys.backup.settings
import android.app.Application import android.app.Application
import android.util.ByteStringUtils.toHexString
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import com.stevesoltys.backup.LiveEvent import com.stevesoltys.backup.LiveEvent
import com.stevesoltys.backup.MutableLiveEvent import com.stevesoltys.backup.MutableLiveEvent
@ -33,7 +34,8 @@ class RecoveryCodeViewModel(application: Application) : AndroidViewModel(applica
internal val confirmButtonClicked: LiveEvent<Boolean> = mConfirmButtonClicked internal val confirmButtonClicked: LiveEvent<Boolean> = mConfirmButtonClicked
internal fun onConfirmButtonClicked() = mConfirmButtonClicked.setEvent(true) internal fun onConfirmButtonClicked() = mConfirmButtonClicked.setEvent(true)
internal val recoveryCodeSaved = MutableLiveEvent<Boolean>() private val mRecoveryCodeSaved = MutableLiveEvent<Boolean>()
internal val recoveryCodeSaved: LiveEvent<Boolean> = mRecoveryCodeSaved
@Throws(WordNotFoundException::class, InvalidChecksumException::class) @Throws(WordNotFoundException::class, InvalidChecksumException::class)
fun validateAndContinue(input: List<CharSequence>) { fun validateAndContinue(input: List<CharSequence>) {
@ -47,7 +49,11 @@ class RecoveryCodeViewModel(application: Application) : AndroidViewModel(applica
val mnemonic = input.joinToString(" ") val mnemonic = input.joinToString(" ")
val seed = SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(mnemonic, "") val seed = SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(mnemonic, "")
KeyManager.storeBackupKey(seed) KeyManager.storeBackupKey(seed)
recoveryCodeSaved.setEvent(true)
// TODO remove once encryption/decryption uses key from KeyStore
setBackupPassword(getApplication(), toHexString(seed))
mRecoveryCodeSaved.setEvent(true)
} }
} }

View file

@ -1,17 +1,13 @@
package com.stevesoltys.backup.settings package com.stevesoltys.backup.settings
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.Intent.*
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import android.widget.Toast.LENGTH_SHORT
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.stevesoltys.backup.LiveEventHandler
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
private val TAG = SettingsActivity::class.java.name private val TAG = SettingsActivity::class.java.name
@ -19,7 +15,6 @@ private val TAG = SettingsActivity::class.java.name
const val REQUEST_CODE_OPEN_DOCUMENT_TREE = 1 const val REQUEST_CODE_OPEN_DOCUMENT_TREE = 1
const val REQUEST_CODE_RECOVERY_CODE = 2 const val REQUEST_CODE_RECOVERY_CODE = 2
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
private lateinit var viewModel: SettingsViewModel private lateinit var viewModel: SettingsViewModel
@ -30,18 +25,25 @@ class SettingsActivity : AppCompatActivity() {
setContentView(R.layout.activity_settings) setContentView(R.layout.activity_settings)
viewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java) viewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java)
viewModel.onLocationSet.observeEvent(this, LiveEventHandler { wasEmptyBefore ->
if (wasEmptyBefore) showFragment(SettingsFragment())
else supportFragmentManager.popBackStack()
})
viewModel.chooseBackupLocation.observeEvent(this, LiveEventHandler { show ->
if (show) showFragment(BackupLocationFragment(), true)
})
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null) showFragment(SettingsFragment())
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) {
if (resultCode != RESULT_OK) { if (resultCode != RESULT_OK) {
Log.w(TAG, "Error in activity result: $requestCode") Log.w(TAG, "Error in activity result: $requestCode")
finishAfterTransition() finishAfterTransition()
} } else {
super.onActivityResult(requestCode, resultCode, result)
if (requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) {
viewModel.handleChooseFolderResult(result)
} }
} }
@ -53,34 +55,16 @@ class SettingsActivity : AppCompatActivity() {
if (!viewModel.recoveryCodeIsSet()) { if (!viewModel.recoveryCodeIsSet()) {
showRecoveryCodeActivity() showRecoveryCodeActivity()
} else if (!viewModel.locationIsSet()) { } else if (!viewModel.locationIsSet()) {
showChooseFolderActivity() showFragment(BackupLocationFragment())
} }
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean = when {
menuInflater.inflate(R.menu.settings_menu, menu) item.itemId == android.R.id.home -> {
if (resources.getBoolean(R.bool.show_restore_in_settings)) { onBackPressed()
menu.findItem(R.id.action_restore).isVisible = true true
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when {
item.itemId == android.R.id.home -> {
onBackPressed()
true
}
item.itemId == R.id.action_backup -> {
Toast.makeText(this, "Not yet implemented", LENGTH_SHORT).show()
true
}
item.itemId == R.id.action_restore -> {
Toast.makeText(this, "Not yet implemented", LENGTH_SHORT).show()
true
}
else -> super.onOptionsItemSelected(item)
} }
else -> super.onOptionsItemSelected(item)
} }
private fun showRecoveryCodeActivity() { private fun showRecoveryCodeActivity() {
@ -88,17 +72,11 @@ class SettingsActivity : AppCompatActivity() {
startActivityForResult(intent, REQUEST_CODE_RECOVERY_CODE) startActivityForResult(intent, REQUEST_CODE_RECOVERY_CODE)
} }
private fun showChooseFolderActivity() { private fun showFragment(f: Fragment, addToBackStack: Boolean = false) {
val openTreeIntent = Intent(ACTION_OPEN_DOCUMENT_TREE) val fragmentTransaction = supportFragmentManager.beginTransaction()
openTreeIntent.addFlags(FLAG_GRANT_PERSISTABLE_URI_PERMISSION or .replace(R.id.fragment, f)
FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION) if (addToBackStack) fragmentTransaction.addToBackStack(null)
// TODO StringRes fragmentTransaction.commit()
try {
val documentChooser = createChooser(openTreeIntent, "Select the backup location")
startActivityForResult(documentChooser, REQUEST_CODE_OPEN_DOCUMENT_TREE)
} catch (ex: ActivityNotFoundException) {
Toast.makeText(this, "Please install a file manager.", LENGTH_LONG).show()
}
} }
} }

View file

@ -15,6 +15,11 @@ import com.stevesoltys.backup.R
import android.content.Context.BACKUP_SERVICE import android.content.Context.BACKUP_SERVICE
import android.os.ServiceManager.getService import android.os.ServiceManager.getService
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.Toast
import androidx.lifecycle.ViewModelProviders
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.Preference.OnPreferenceChangeListener import androidx.preference.Preference.OnPreferenceChangeListener
@ -24,14 +29,19 @@ class SettingsFragment : PreferenceFragmentCompat() {
private lateinit var backupManager: IBackupManager private lateinit var backupManager: IBackupManager
private lateinit var viewModel: SettingsViewModel
private lateinit var backup: TwoStatePreference private lateinit var backup: TwoStatePreference
private lateinit var autoRestore: TwoStatePreference private lateinit var autoRestore: TwoStatePreference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey) setPreferencesFromResource(R.xml.settings, rootKey)
setHasOptionsMenu(true)
backupManager = IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) backupManager = IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE))
viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java)
backup = findPreference("backup") as TwoStatePreference backup = findPreference("backup") as TwoStatePreference
backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
val enabled = newValue as Boolean val enabled = newValue as Boolean
@ -45,6 +55,12 @@ class SettingsFragment : PreferenceFragmentCompat() {
} }
} }
val backupLocation = findPreference("backup_location")
backupLocation.setOnPreferenceClickListener {
viewModel.chooseBackupLocation()
true
}
autoRestore = findPreference("auto_restore") as TwoStatePreference autoRestore = findPreference("auto_restore") as TwoStatePreference
autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
val enabled = newValue as Boolean val enabled = newValue as Boolean
@ -62,6 +78,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
// we need to re-set the title when returning to this fragment
requireActivity().setTitle(R.string.app_name)
try { try {
backup.isChecked = backupManager.isBackupEnabled backup.isChecked = backupManager.isBackupEnabled
backup.isEnabled = true backup.isEnabled = true
@ -74,4 +93,24 @@ class SettingsFragment : PreferenceFragmentCompat() {
autoRestore.isChecked = Settings.Secure.getInt(resolver, BACKUP_AUTO_RESTORE, 1) == 1 autoRestore.isChecked = Settings.Secure.getInt(resolver, BACKUP_AUTO_RESTORE, 1) == 1
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.settings_menu, menu)
if (resources.getBoolean(R.bool.show_restore_in_settings)) {
menu.findItem(R.id.action_restore).isVisible = true
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when {
item.itemId == R.id.action_backup -> {
Toast.makeText(requireContext(), "Not yet implemented", Toast.LENGTH_SHORT).show()
true
}
item.itemId == R.id.action_restore -> {
Toast.makeText(requireContext(), "Not yet implemented", Toast.LENGTH_SHORT).show()
true
}
else -> super.onOptionsItemSelected(item)
}
} }

View file

@ -5,12 +5,25 @@ import android.content.Intent
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import com.stevesoltys.backup.LiveEvent
import com.stevesoltys.backup.MutableLiveEvent
import com.stevesoltys.backup.security.KeyManager import com.stevesoltys.backup.security.KeyManager
class SettingsViewModel(application: Application) : AndroidViewModel(application) { class SettingsViewModel(application: Application) : AndroidViewModel(application) {
private val app = application private val app = application
private val mLocationWasSet = MutableLiveEvent<Boolean>()
/**
* Will be set to true if this is the initial location.
* It will be false if an existing location was changed.
*/
internal val onLocationSet: LiveEvent<Boolean> = locationWasSet
private val mChooseBackupLocation = MutableLiveEvent<Boolean>()
internal val chooseBackupLocation: LiveEvent<Boolean> = mChooseBackupLocation
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
fun recoveryCodeIsSet() = KeyManager.hasBackupKey() fun recoveryCodeIsSet() = KeyManager.hasBackupKey()
fun locationIsSet() = getBackupFolderUri(getApplication()) != null fun locationIsSet() = getBackupFolderUri(getApplication()) != null
@ -21,8 +34,14 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
val takeFlags = result.flags and (FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION) val takeFlags = result.flags and (FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION)
app.contentResolver.takePersistableUriPermission(folderUri, takeFlags) app.contentResolver.takePersistableUriPermission(folderUri, takeFlags)
// check if this is initial set-up or a later change
val wasEmptyBefore = getBackupFolderUri(app) == null
// store backup folder location in settings // store backup folder location in settings
setBackupFolderUri(app, folderUri) setBackupFolderUri(app, folderUri)
// notify the UI that the location has been set
mLocationWasSet.setEvent(wasEmptyBefore)
} }
} }

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment" android:id="@+id/fragment"
android:name="com.stevesoltys.backup.settings.SettingsFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />

View file

@ -26,6 +26,8 @@
<!-- Settings --> <!-- Settings -->
<string name="settings_backup">Backup my data</string> <string name="settings_backup">Backup my data</string>
<string name="settings_backup_location">Backup location</string> <string name="settings_backup_location">Backup location</string>
<string name="settings_backup_location_title">Backup Location</string>
<string name="settings_backup_location_info">Choose where to store your backups. More options might get added in the future.</string>
<string name="settings_backup_external_storage">External Storage</string> <string name="settings_backup_external_storage">External Storage</string>
<string name="settings_info">All backups are encrypted on your phone. To restore from backup you will need your 12-word recovery code.</string> <string name="settings_info">All backups are encrypted on your phone. To restore from backup you will need your 12-word recovery code.</string>
<string name="settings_auto_restore_title">Automatic restore</string> <string name="settings_auto_restore_title">Automatic restore</string>

View file

@ -0,0 +1,11 @@
<androidx.preference.PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.preference.Preference
app:allowDividerAbove="true"
app:allowDividerBelow="false"
app:icon="@drawable/ic_info_outline"
app:selectable="false"
app:order="1337"
app:summary="@string/settings_backup_location_info" />
</androidx.preference.PreferenceScreen>

View file

@ -10,7 +10,6 @@
app:dependency="backup" app:dependency="backup"
app:icon="@drawable/ic_storage" app:icon="@drawable/ic_storage"
app:key="backup_location" app:key="backup_location"
app:selectable="false"
app:summary="@string/settings_backup_external_storage" app:summary="@string/settings_backup_external_storage"
app:title="@string/settings_backup_location" /> app:title="@string/settings_backup_location" />