Ensure that we have the main key for v1 crypto

We ask the user to generate a new key, because actively asking for the old one is training bad security habits, but technically verifying the old key will also work.
diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt
index a443b3b..9ac333e 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 @@
         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 2b4e398..845b627 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 @@
 
     @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 0057d9c..2cc8719 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 @@
         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 @@
             .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 @@
             .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 7ab65f4..143b47a 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.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 @@
 
     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 @@
         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 a38147f..ea8606a 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_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_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 @@
         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 d851ea9..ff649ee 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.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 @@
     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 @@
         // 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 @@
         keyManager.storeBackupKey(seed)
         keyManager.storeMainKey(seed)
         mRecoveryCodeSaved.setEvent(true)
+        notificationManager.onNoMainKeyErrorFixed()
     }
 
     /**
@@ -109,7 +113,7 @@
                 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 aa8a0bb..91a68b7 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 @@
                 // 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 eaa3c7d..1b2fdaf 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -38,9 +38,9 @@
     <string name="settings_backup_storage_dialog_title">Experimental feature</string>
     <string name="settings_backup_storage_dialog_message">Backing up files is still experimental and might not work. Do not rely on it for important data.</string>
     <string name="settings_backup_storage_dialog_ok">Enable anyway</string>
-    <string name="settings_backup_storage_code_dialog_title">Recovery code verification required</string>
-    <string name="settings_backup_storage_code_dialog_message">To enable storage backup, you need to first verify your recovery code or generate a new one.</string>
-    <string name="settings_backup_storage_code_dialog_ok">Verify code</string>
+    <string name="settings_backup_new_code_dialog_title">New recovery code required</string>
+    <string name="settings_backup_new_code_dialog_message">To continue using app backups, you need to generate a new recovery code.\n\nWe are sorry for the inconvenience.</string>
+    <string name="settings_backup_new_code_code_dialog_ok">New code</string>
 
     <string name="settings_expert_title">Expert settings</string>
     <string name="settings_expert_quota_title">Unlimited app quota</string>
@@ -120,6 +120,9 @@
     <string name="notification_restore_error_text">Plug in your %1$s before installing the app to restore its data from backup.</string>
     <string name="notification_restore_error_action">Uninstall app</string>
 
+    <string name="notification_error_no_main_key_title">Backups disabled</string>
+    <string name="notification_error_no_main_key_text">Generate a new recovery code to complete upgrade and continue to use backups.</string>
+
     <!-- App Backup and Restore State -->
 
     <string name="backup_section_system">System apps</string>