From 4c79d41963f11b4618c213beb24f763cfd111b5d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 8 Jul 2019 16:02:53 +0200 Subject: [PATCH] Show Backup Location screen before letting user choose backup folder This screen can also be reached by tapping the previously inactive backup location setting. --- .../backup/settings/BackupLocationFragment.kt | 59 ++++++++++++++++ .../backup/settings/RecoveryCodeViewModel.kt | 10 ++- .../backup/settings/SettingsActivity.kt | 70 +++++++------------ .../backup/settings/SettingsFragment.kt | 39 +++++++++++ .../backup/settings/SettingsViewModel.kt | 19 +++++ app/src/main/res/layout/activity_settings.xml | 3 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/backup_location.xml | 11 +++ app/src/main/res/xml/settings.xml | 1 - 9 files changed, 163 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/backup/settings/BackupLocationFragment.kt create mode 100644 app/src/main/res/xml/backup_location.xml diff --git a/app/src/main/java/com/stevesoltys/backup/settings/BackupLocationFragment.kt b/app/src/main/java/com/stevesoltys/backup/settings/BackupLocationFragment.kt new file mode 100644 index 00000000..140ea439 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/backup/settings/BackupLocationFragment.kt @@ -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) + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt b/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt index b5e1c0a7..a64f4ec8 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/RecoveryCodeViewModel.kt @@ -1,6 +1,7 @@ package com.stevesoltys.backup.settings import android.app.Application +import android.util.ByteStringUtils.toHexString import androidx.lifecycle.AndroidViewModel import com.stevesoltys.backup.LiveEvent import com.stevesoltys.backup.MutableLiveEvent @@ -33,7 +34,8 @@ class RecoveryCodeViewModel(application: Application) : AndroidViewModel(applica internal val confirmButtonClicked: LiveEvent = mConfirmButtonClicked internal fun onConfirmButtonClicked() = mConfirmButtonClicked.setEvent(true) - internal val recoveryCodeSaved = MutableLiveEvent() + private val mRecoveryCodeSaved = MutableLiveEvent() + internal val recoveryCodeSaved: LiveEvent = mRecoveryCodeSaved @Throws(WordNotFoundException::class, InvalidChecksumException::class) fun validateAndContinue(input: List) { @@ -47,7 +49,11 @@ class RecoveryCodeViewModel(application: Application) : AndroidViewModel(applica val mnemonic = input.joinToString(" ") val seed = SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(mnemonic, "") KeyManager.storeBackupKey(seed) - recoveryCodeSaved.setEvent(true) + + // TODO remove once encryption/decryption uses key from KeyStore + setBackupPassword(getApplication(), toHexString(seed)) + + mRecoveryCodeSaved.setEvent(true) } } diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt index 3ffb4f4f..18a46926 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsActivity.kt @@ -1,17 +1,13 @@ package com.stevesoltys.backup.settings -import android.content.ActivityNotFoundException import android.content.Intent -import android.content.Intent.* import android.os.Bundle import android.util.Log -import android.view.Menu 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.fragment.app.Fragment import androidx.lifecycle.ViewModelProviders +import com.stevesoltys.backup.LiveEventHandler import com.stevesoltys.backup.R 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_RECOVERY_CODE = 2 - class SettingsActivity : AppCompatActivity() { private lateinit var viewModel: SettingsViewModel @@ -30,18 +25,25 @@ class SettingsActivity : AppCompatActivity() { setContentView(R.layout.activity_settings) 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) + + if (savedInstanceState == null) showFragment(SettingsFragment()) } override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { if (resultCode != RESULT_OK) { Log.w(TAG, "Error in activity result: $requestCode") finishAfterTransition() - } - - if (requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) { - viewModel.handleChooseFolderResult(result) + } else { + super.onActivityResult(requestCode, resultCode, result) } } @@ -53,34 +55,16 @@ class SettingsActivity : AppCompatActivity() { if (!viewModel.recoveryCodeIsSet()) { showRecoveryCodeActivity() } else if (!viewModel.locationIsSet()) { - showChooseFolderActivity() + showFragment(BackupLocationFragment()) } } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.settings_menu, menu) - if (resources.getBoolean(R.bool.show_restore_in_settings)) { - menu.findItem(R.id.action_restore).isVisible = 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) + override fun onOptionsItemSelected(item: MenuItem): Boolean = when { + item.itemId == android.R.id.home -> { + onBackPressed() + true } + else -> super.onOptionsItemSelected(item) } private fun showRecoveryCodeActivity() { @@ -88,17 +72,11 @@ class SettingsActivity : AppCompatActivity() { startActivityForResult(intent, REQUEST_CODE_RECOVERY_CODE) } - 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) - // TODO StringRes - 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() - } + private fun showFragment(f: Fragment, addToBackStack: Boolean = false) { + val fragmentTransaction = supportFragmentManager.beginTransaction() + .replace(R.id.fragment, f) + if (addToBackStack) fragmentTransaction.addToBackStack(null) + fragmentTransaction.commit() } } diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt index 31e8432f..6e1d6135 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt @@ -15,6 +15,11 @@ import com.stevesoltys.backup.R import android.content.Context.BACKUP_SERVICE import android.os.ServiceManager.getService 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.OnPreferenceChangeListener @@ -24,14 +29,19 @@ class SettingsFragment : PreferenceFragmentCompat() { private lateinit var backupManager: IBackupManager + private lateinit var viewModel: SettingsViewModel + private lateinit var backup: TwoStatePreference private lateinit var autoRestore: TwoStatePreference override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.settings, rootKey) + setHasOptionsMenu(true) backupManager = IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) + viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java) + backup = findPreference("backup") as TwoStatePreference backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> 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.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> val enabled = newValue as Boolean @@ -62,6 +78,9 @@ class SettingsFragment : PreferenceFragmentCompat() { override fun onStart() { super.onStart() + // we need to re-set the title when returning to this fragment + requireActivity().setTitle(R.string.app_name) + try { backup.isChecked = backupManager.isBackupEnabled backup.isEnabled = true @@ -74,4 +93,24 @@ class SettingsFragment : PreferenceFragmentCompat() { 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) + } + } diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt index 26bec197..bb782954 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsViewModel.kt @@ -5,12 +5,25 @@ import android.content.Intent import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION import androidx.lifecycle.AndroidViewModel +import com.stevesoltys.backup.LiveEvent +import com.stevesoltys.backup.MutableLiveEvent import com.stevesoltys.backup.security.KeyManager class SettingsViewModel(application: Application) : AndroidViewModel(application) { private val app = application + private val mLocationWasSet = MutableLiveEvent() + /** + * 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 = locationWasSet + + private val mChooseBackupLocation = MutableLiveEvent() + internal val chooseBackupLocation: LiveEvent = mChooseBackupLocation + internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true) + fun recoveryCodeIsSet() = KeyManager.hasBackupKey() 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) 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 setBackupFolderUri(app, folderUri) + + // notify the UI that the location has been set + mLocationWasSet.setEvent(wasEmptyBefore) } } diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 13b68c73..d64f58e8 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -1,6 +1,5 @@ - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21aaaf16..44b2e553 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,6 +26,8 @@ Backup my data Backup location + Backup Location + Choose where to store your backups. More options might get added in the future. External Storage All backups are encrypted on your phone. To restore from backup you will need your 12-word recovery code. Automatic restore diff --git a/app/src/main/res/xml/backup_location.xml b/app/src/main/res/xml/backup_location.xml new file mode 100644 index 00000000..1c8abbe6 --- /dev/null +++ b/app/src/main/res/xml/backup_location.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index b8a0c952..98f3f972 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -10,7 +10,6 @@ app:dependency="backup" app:icon="@drawable/ic_storage" app:key="backup_location" - app:selectable="false" app:summary="@string/settings_backup_external_storage" app:title="@string/settings_backup_location" />