From 22b3ace3aae06b0062aa24f9d2d62ce7b06dcea0 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 2 Aug 2024 09:54:50 -0300 Subject: [PATCH 1/2] Ask on first run if user wants restore --- .../seedvault/settings/FirstRunFragment.kt | 37 +++++++++++++++++++ .../seedvault/settings/SettingsActivity.kt | 31 ++++------------ .../seedvault/settings/SettingsFragment.kt | 20 ++++++++++ .../seedvault/settings/SettingsManager.kt | 8 ++++ .../seedvault/settings/SettingsViewModel.kt | 1 + .../ui/RequireProvisioningActivity.kt | 8 ++-- app/src/main/res/values/strings.xml | 2 + 7 files changed, 79 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/seedvault/settings/FirstRunFragment.kt diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/FirstRunFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/FirstRunFragment.kt new file mode 100644 index 00000000..78090dd6 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/FirstRunFragment.kt @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.settings + +import android.app.Dialog +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.stevesoltys.seedvault.R +import com.stevesoltys.seedvault.restore.RestoreActivity + +class FirstRunFragment : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + isCancelable = false // is what really works, specifying it for the dialog only doesn't + return MaterialAlertDialogBuilder(requireContext()) + .setMessage(R.string.first_start_text) + .setPositiveButton(R.string.setup_button) { dialog, _ -> + parentFragmentManager.beginTransaction() + .replace(R.id.fragment, SettingsFragment(), null) + .commit() + dialog.dismiss() + } + .setNeutralButton(R.string.restore_backup_button) { dialog, _ -> + val i = Intent(requireContext(), RestoreActivity::class.java) + startActivity(i) + dialog.dismiss() + requireActivity().finish() + } + .setCancelable(false) + .create() + } +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt index 6697e3f4..976cb364 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt @@ -6,17 +6,14 @@ package com.stevesoltys.seedvault.settings import android.os.Bundle -import androidx.annotation.CallSuper import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.ui.RequireProvisioningActivity import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel -import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.recoverycode.ARG_FOR_NEW_CODE import com.stevesoltys.seedvault.ui.storage.StorageCheckFragment -import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel internal const val ACTION_APP_STATUS_LIST = "com.stevesoltys.seedvault.APP_STATUS_LIST" @@ -25,7 +22,6 @@ private const val PREF_BACKUP_RECOVERY_CODE = "backup_recovery_code" class SettingsActivity : RequireProvisioningActivity(), OnPreferenceStartFragmentCallback { private val viewModel: SettingsViewModel by viewModel() - private val notificationManager: BackupNotificationManager by inject() override fun getViewModel(): RequireProvisioningViewModel = viewModel @@ -37,8 +33,13 @@ class SettingsActivity : RequireProvisioningActivity(), OnPreferenceStartFragmen setSupportActionBar(requireViewById(R.id.toolbar)) supportActionBar!!.setDisplayHomeAsUpEnabled(true) - // always start with settings fragment as a base (when fresh start) - if (savedInstanceState == null) showFragment(SettingsFragment()) + if (savedInstanceState == null && viewModel.isFirstStart) { + // let user choose whether to restore on first start + FirstRunFragment().show(supportFragmentManager, null) + } else if (savedInstanceState == null) { + // always start with settings fragment as a base (when fresh start) + showFragment(SettingsFragment()) + } // add app status fragment on the stack, if started via intent if (intent?.action == ACTION_APP_STATUS_LIST) { showFragment(AppStatusFragment(), true) @@ -58,24 +59,6 @@ class SettingsActivity : RequireProvisioningActivity(), OnPreferenceStartFragmen } } - @CallSuper - override fun onResume() { - super.onResume() - // Activity results from the parent will get delivered before and might tell us to finish. - // Don't start any new activities when that happens. - // Note: onStart() can get called *before* results get delivered, so we use onResume() here - if (isFinishing) return - - // check that backup is provisioned - if (!viewModel.recoveryCodeIsSet()) { - showRecoveryCodeActivity() - } else if (!viewModel.validLocationIsSet()) { - showStorageActivity() - // remove potential error notifications - notificationManager.onBackupErrorSeen() - } - } - override fun onPreferenceStartFragment( caller: PreferenceFragmentCompat, pref: Preference, diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index e4096d20..df2d9bfc 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -30,6 +30,7 @@ import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.plugins.StorageProperties import com.stevesoltys.seedvault.restore.RestoreActivity +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.toRelativeTime import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.sharedViewModel @@ -42,6 +43,7 @@ class SettingsFragment : PreferenceFragmentCompat() { private val viewModel: SettingsViewModel by sharedViewModel() private val storagePluginManager: StoragePluginManager by inject() private val backupManager: IBackupManager by inject() + private val notificationManager: BackupNotificationManager by inject() private lateinit var backup: TwoStatePreference private lateinit var autoRestore: TwoStatePreference @@ -184,6 +186,24 @@ class SettingsFragment : PreferenceFragmentCompat() { setAppBackupSchedulingSummary(viewModel.appBackupWorkInfo.value) } + override fun onResume() { + super.onResume() + // Activity results from the parent will get delivered before and might tell us to finish. + // Don't start any new activities when that happens. + // Note: onStart() can get called *before* results get delivered, so we use onResume() here + if (requireActivity().isFinishing) return + + // check that backup is provisioned + val activity = requireActivity() as SettingsActivity + if (!viewModel.recoveryCodeIsSet()) { + activity.showRecoveryCodeActivity() + } else if (!viewModel.validLocationIsSet()) { + activity.showStorageActivity() + // remove potential error notifications + notificationManager.onBackupErrorSeen() + } + } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) inflater.inflate(R.menu.settings_menu, menu) diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index b8029623..f62ce688 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -247,6 +247,14 @@ class SettingsManager(private val context: Context) { .putBoolean(PREF_KEY_D2D_BACKUPS, enabled) .apply() } + + /** + * This assumes that if there's no storage plugin set, it is the first start. + * We enforce a storage plugin and don't allow unsetting one, + * so this should be a safe assumption. + */ + val isFirstStart get() = prefs.getString(PREF_KEY_STORAGE_PLUGIN, null) == null + } data class FlashDrive( diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index c69ec7a0..f361f80e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -85,6 +85,7 @@ internal class SettingsViewModel( private val workManager = WorkManager.getInstance(app) override val isRestoreOperation = false + val isFirstStart get() = settingsManager.isFirstStart val isBackupRunning: StateFlow private val mBackupPossible = MutableLiveData(false) diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningActivity.kt index ee9b8216..b02e874d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningActivity.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningActivity.kt @@ -56,12 +56,12 @@ abstract class RequireProvisioningActivity : BackupActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - getViewModel().chooseBackupLocation.observeEvent(this, { show -> + getViewModel().chooseBackupLocation.observeEvent(this) { show -> if (show) showStorageActivity() - }) + } } - protected fun showStorageActivity() { + fun showStorageActivity() { val intent = Intent(this, StorageActivity::class.java).apply { putExtra(INTENT_EXTRA_IS_RESTORE, getViewModel().isRestoreOperation) putExtra(INTENT_EXTRA_IS_SETUP_WIZARD, isSetupWizard) @@ -69,7 +69,7 @@ abstract class RequireProvisioningActivity : BackupActivity() { requestLocation.launch(intent) } - protected fun showRecoveryCodeActivity() { + fun showRecoveryCodeActivity() { val intent = Intent(this, RecoveryCodeActivity::class.java).apply { putExtra(INTENT_EXTRA_IS_RESTORE, getViewModel().isRestoreOperation) putExtra(INTENT_EXTRA_IS_SETUP_WIZARD, isSetupWizard) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e2d5bf3..04de1dcf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,7 +9,9 @@ Seedvault Backup Backup status and settings + Do you want to restore your device from an existing backup or start a new backup? Restore backup + Start new App backup From 7f934c820971c70f6af7d8929cd633e8dd58904a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 15 Aug 2024 18:02:30 -0300 Subject: [PATCH 2/2] Fix back navigation when FirstRunFragment is shown --- .../com/stevesoltys/seedvault/settings/FirstRunFragment.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/FirstRunFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/FirstRunFragment.kt index 78090dd6..ce3a3483 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/FirstRunFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/FirstRunFragment.kt @@ -6,6 +6,7 @@ package com.stevesoltys.seedvault.settings import android.app.Dialog +import android.content.DialogInterface import android.content.Intent import android.os.Bundle import androidx.fragment.app.DialogFragment @@ -16,7 +17,6 @@ import com.stevesoltys.seedvault.restore.RestoreActivity class FirstRunFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - isCancelable = false // is what really works, specifying it for the dialog only doesn't return MaterialAlertDialogBuilder(requireContext()) .setMessage(R.string.first_start_text) .setPositiveButton(R.string.setup_button) { dialog, _ -> @@ -31,7 +31,10 @@ class FirstRunFragment : DialogFragment() { dialog.dismiss() requireActivity().finish() } - .setCancelable(false) .create() } + + override fun onCancel(dialog: DialogInterface) { + requireActivity().finish() + } }