diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index a443b3bc..9ac333e7 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -46,7 +46,7 @@ open class App : Application() { factory { AppListRetriever(this@App, get(), get(), get()) } viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) } - viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get()) } + viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get()) } viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt index 2b4e398f..845b6270 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt @@ -126,6 +126,8 @@ internal class RestoreViewModel( @Throws(RemoteException::class) private fun getOrStartSession(): IRestoreSession { + // TODO consider not using the BackupManager for this, but our own API directly + // this is less error-prone (hanging sessions) and can provide more data val session = this.session ?: backupManager.beginRestoreSessionForUser(UserHandle.myUserId(), null, TRANSPORT_ID) ?: throw RemoteException("beginRestoreSessionForUser returned null") 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 0057d9c1..2cc8719d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -54,12 +54,19 @@ class SettingsFragment : PreferenceFragmentCompat() { backup = findPreference("backup")!! backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> val enabled = newValue as Boolean + // don't enable if we don't have the main key + if (enabled && !viewModel.hasMainKey()) { + showCodeRegenerationNeededDialog() + backup.isChecked = false + return@OnPreferenceChangeListener false + } + // main key is present, so enable or disable normally try { backupManager.isBackupEnabled = enabled if (enabled) viewModel.enableCallLogBackup() return@OnPreferenceChangeListener true } catch (e: RemoteException) { - e.printStackTrace() + Log.e(TAG, "Error setting backup enabled to $enabled", e) backup.isChecked = !enabled return@OnPreferenceChangeListener false } @@ -222,12 +229,8 @@ class SettingsFragment : PreferenceFragmentCompat() { .setTitle(R.string.settings_backup_storage_dialog_title) .setMessage(R.string.settings_backup_storage_dialog_message) .setPositiveButton(R.string.settings_backup_storage_dialog_ok) { dialog, _ -> - if (viewModel.hasMainKey()) { - viewModel.enableStorageBackup() - backupStorage.isChecked = true - } else { - showCodeVerificationNeededDialog() - } + viewModel.enableStorageBackup() + backupStorage.isChecked = true dialog.dismiss() } .setNegativeButton(R.string.settings_backup_apk_dialog_cancel) { dialog, _ -> @@ -236,12 +239,12 @@ class SettingsFragment : PreferenceFragmentCompat() { .show() } - private fun showCodeVerificationNeededDialog() { + private fun showCodeRegenerationNeededDialog() { AlertDialog.Builder(requireContext()) .setIcon(R.drawable.ic_vpn_key) - .setTitle(R.string.settings_backup_storage_code_dialog_title) - .setMessage(R.string.settings_backup_storage_code_dialog_message) - .setPositiveButton(R.string.settings_backup_storage_code_dialog_ok) { dialog, _ -> + .setTitle(R.string.settings_backup_new_code_dialog_title) + .setMessage(R.string.settings_backup_new_code_dialog_message) + .setPositiveButton(R.string.settings_backup_new_code_code_dialog_ok) { dialog, _ -> val callback = (requireActivity() as OnPreferenceStartFragmentCallback) callback.onPreferenceStartFragment(this, backupRecoveryCode) dialog.dismiss() diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt index 7ab65f46..143b47a2 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt @@ -10,6 +10,7 @@ import android.os.RemoteException import android.util.Log import androidx.annotation.WorkerThread import com.stevesoltys.seedvault.BackupMonitor +import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver @@ -27,6 +28,8 @@ class ConfigurableBackupTransportService : Service(), KoinComponent { private var transport: ConfigurableBackupTransport? = null + private val keyManager: KeyManager by inject() + private val backupManager: IBackupManager by inject() private val notificationManager: BackupNotificationManager by inject() override fun onCreate() { @@ -35,7 +38,13 @@ class ConfigurableBackupTransportService : Service(), KoinComponent { Log.d(TAG, "Service created.") } - override fun onBind(intent: Intent): IBinder { + override fun onBind(intent: Intent): IBinder? { + // refuse to work until we have the main key + val noMainKey = keyManager.hasBackupKey() && !keyManager.hasMainKey() + if (noMainKey && backupManager.currentTransport == TRANSPORT_ID) { + notificationManager.onNoMainKeyError() + backupManager.isBackupEnabled = false + } val transport = this.transport ?: throw IllegalStateException("no transport in onBind()") return transport.binder.apply { Log.d(TAG, "Transport bound.") diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt index a38147f4..ea8606a8 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt @@ -7,6 +7,7 @@ import android.app.NotificationManager.IMPORTANCE_DEFAULT import android.app.NotificationManager.IMPORTANCE_HIGH import android.app.NotificationManager.IMPORTANCE_LOW import android.app.PendingIntent +import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.Context import android.content.Intent @@ -33,6 +34,7 @@ private const val NOTIFICATION_ID_OBSERVER = 1 private const val NOTIFICATION_ID_ERROR = 2 private const val NOTIFICATION_ID_RESTORE_ERROR = 3 private const val NOTIFICATION_ID_BACKGROUND = 4 +private const val NOTIFICATION_ID_NO_MAIN_KEY_ERROR = 5 private val TAG = BackupNotificationManager::class.java.simpleName @@ -269,4 +271,28 @@ internal class BackupNotificationManager(private val context: Context) { nm.cancel(NOTIFICATION_ID_RESTORE_ERROR) } + @SuppressLint("RestrictedApi") + fun onNoMainKeyError() { + val intent = Intent(context, SettingsActivity::class.java) + val pendingIntent = PendingIntent.getActivity(context, 0, intent, FLAG_IMMUTABLE) + val actionText = context.getString(R.string.notification_error_action) + val action = Action(0, actionText, pendingIntent) + val notification = Builder(context, CHANNEL_ID_ERROR).apply { + setSmallIcon(R.drawable.ic_cloud_error) + setContentTitle(context.getString(R.string.notification_error_no_main_key_title)) + setContentText(context.getString(R.string.notification_error_no_main_key_text)) + setWhen(System.currentTimeMillis()) + setOnlyAlertOnce(true) + setAutoCancel(false) + setOngoing(true) + setContentIntent(pendingIntent) + mActions = arrayListOf(action) + }.build() + nm.notify(NOTIFICATION_ID_NO_MAIN_KEY_ERROR, notification) + } + + fun onNoMainKeyErrorFixed() { + nm.cancel(NOTIFICATION_ID_NO_MAIN_KEY_ERROR) + } + } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt index d851ea9e..ff649ee5 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt @@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.transport.TRANSPORT_ID import com.stevesoltys.seedvault.transport.backup.BackupCoordinator import com.stevesoltys.seedvault.ui.LiveEvent import com.stevesoltys.seedvault.ui.MutableLiveEvent +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -33,6 +34,7 @@ internal class RecoveryCodeViewModel( private val keyManager: KeyManager, private val backupManager: IBackupManager, private val backupCoordinator: BackupCoordinator, + private val notificationManager: BackupNotificationManager, private val storageBackup: StorageBackup ) : AndroidViewModel(app) { @@ -77,6 +79,7 @@ internal class RecoveryCodeViewModel( // store main key at this opportunity if it is still missing if (verified && !keyManager.hasMainKey()) keyManager.storeMainKey(seed) mExistingCodeChecked.setEvent(verified) + if (verified) notificationManager.onNoMainKeyErrorFixed() } /** @@ -88,6 +91,7 @@ internal class RecoveryCodeViewModel( keyManager.storeBackupKey(seed) keyManager.storeMainKey(seed) mRecoveryCodeSaved.setEvent(true) + notificationManager.onNoMainKeyErrorFixed() } /** @@ -109,7 +113,7 @@ internal class RecoveryCodeViewModel( backupCoordinator.startNewRestoreSet() // initialize the new location - backupManager.initializeTransportsForUser( + if (backupManager.isBackupEnabled) backupManager.initializeTransportsForUser( UserHandle.myUserId(), arrayOf(TRANSPORT_ID), null diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt index aa8a0bb6..91a68b7e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt @@ -41,13 +41,15 @@ internal class BackupStorageViewModel( // will also generate a new backup token for the new restore set backupCoordinator.startNewRestoreSet() - // initialize the new location - backupManager.initializeTransportsForUser( + // initialize the new location (if backups are enabled) + if (backupManager.isBackupEnabled) backupManager.initializeTransportsForUser( UserHandle.myUserId(), arrayOf(TRANSPORT_ID), // if storage is on USB and this is not SetupWizard, do a backup right away InitializationObserver(isUsb && !isSetupWizard) - ) + ) else { + InitializationObserver(false).backupFinished(0) + } } catch (e: IOException) { Log.e(TAG, "Error starting new RestoreSet", e) onInitializationError() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eaa3c7d4..1b2fdafe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,9 +38,9 @@ Experimental feature Backing up files is still experimental and might not work. Do not rely on it for important data. Enable anyway - Recovery code verification required - To enable storage backup, you need to first verify your recovery code or generate a new one. - Verify code + New recovery code required + To continue using app backups, you need to generate a new recovery code.\n\nWe are sorry for the inconvenience. + New code Expert settings Unlimited app quota @@ -120,6 +120,9 @@ Plug in your %1$s before installing the app to restore its data from backup. Uninstall app + Backups disabled + Generate a new recovery code to complete upgrade and continue to use backups. + System apps