Move code to get available backups from RestorePlugin to BackupPlugin
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
index 95836c5..0176688 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
@@ -10,6 +10,7 @@
 import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderKVRestorePlugin
 import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderRestorePlugin
 import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
+import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
 import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH
 import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH_NEXTCLOUD
 import com.stevesoltys.seedvault.plugins.saf.deleteContents
@@ -94,9 +95,9 @@
     @Test
     fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
         // no backups available initially
-        assertEquals(0, restorePlugin.getAvailableBackups()?.toList()?.size)
+        assertEquals(0, backupPlugin.getAvailableBackups()?.toList()?.size)
         val uri = settingsManager.getStorage()?.getDocumentFile(context)?.uri ?: error("no storage")
-        assertFalse(restorePlugin.hasBackup(uri))
+        assertFalse(backupPlugin.hasBackup(uri))
 
         // prepare returned tokens requested when initializing device
         every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
@@ -106,23 +107,26 @@
         backupPlugin.initializeDevice()
 
         // write metadata (needed for backup to be recognized)
-        backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray())
+        backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA)
+            .writeAndClose(getRandomByteArray())
 
         // one backup available now
-        assertEquals(1, restorePlugin.getAvailableBackups()?.toList()?.size)
-        assertTrue(restorePlugin.hasBackup(uri))
+        assertEquals(1, backupPlugin.getAvailableBackups()?.toList()?.size)
+        assertTrue(backupPlugin.hasBackup(uri))
 
         // initializing again (with another restore set) does add a restore set
         backupPlugin.startNewRestoreSet(token + 1)
         backupPlugin.initializeDevice()
-        backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray())
-        assertEquals(2, restorePlugin.getAvailableBackups()?.toList()?.size)
-        assertTrue(restorePlugin.hasBackup(uri))
+        backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA)
+            .writeAndClose(getRandomByteArray())
+        assertEquals(2, backupPlugin.getAvailableBackups()?.toList()?.size)
+        assertTrue(backupPlugin.hasBackup(uri))
 
         // initializing again (without new restore set) doesn't change number of restore sets
         backupPlugin.initializeDevice()
-        backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray())
-        assertEquals(2, restorePlugin.getAvailableBackups()?.toList()?.size)
+        backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA)
+            .writeAndClose(getRandomByteArray())
+        assertEquals(2, backupPlugin.getAvailableBackups()?.toList()?.size)
 
         // ensure that the new backup dirs exist
         assertTrue(storage.currentKvBackupDir!!.exists())
@@ -138,29 +142,27 @@
 
         // write metadata
         val metadata = getRandomByteArray()
-        backupPlugin.getMetadataOutputStream().writeAndClose(metadata)
+        backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA).writeAndClose(metadata)
 
         // get available backups, expect only one with our token and no error
-        var availableBackups = restorePlugin.getAvailableBackups()?.toList()
+        var availableBackups = backupPlugin.getAvailableBackups()?.toList()
         check(availableBackups != null)
         assertEquals(1, availableBackups.size)
         assertEquals(token, availableBackups[0].token)
-        assertFalse(availableBackups[0].error)
 
         // read metadata matches what was written earlier
-        assertReadEquals(metadata, availableBackups[0].inputStream)
+        assertReadEquals(metadata, availableBackups[0].inputStreamRetriever())
 
         // initializing again (without changing storage) keeps restore set with same token
         backupPlugin.initializeDevice()
-        backupPlugin.getMetadataOutputStream().writeAndClose(metadata)
-        availableBackups = restorePlugin.getAvailableBackups()?.toList()
+        backupPlugin.getOutputStream(token, FILE_BACKUP_METADATA).writeAndClose(metadata)
+        availableBackups = backupPlugin.getAvailableBackups()?.toList()
         check(availableBackups != null)
         assertEquals(1, availableBackups.size)
         assertEquals(token, availableBackups[0].token)
-        assertFalse(availableBackups[0].error)
 
         // metadata hasn't changed
-        assertReadEquals(metadata, availableBackups[0].inputStream)
+        assertReadEquals(metadata, availableBackups[0].inputStreamRetriever())
     }
 
     @Test
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
index 025b82b..03f1568 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
@@ -6,7 +6,6 @@
 import com.stevesoltys.seedvault.header.VERSION
 import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
 import org.calyxos.backup.storage.crypto.StreamCrypto.toByteArray
-import java.io.InputStream
 import java.nio.ByteBuffer
 
 typealias PackageMetadataMap = HashMap<String, PackageMetadata>
@@ -100,20 +99,6 @@
 
 internal class DecryptionFailedException(cause: Throwable) : Exception(cause)
 
-class EncryptedBackupMetadata private constructor(
-    val token: Long,
-    val inputStream: InputStream?,
-    val error: Boolean
-) {
-
-    constructor(token: Long, inputStream: InputStream) : this(token, inputStream, false)
-
-    /**
-     * Indicates that there was an error retrieving the encrypted backup metadata.
-     */
-    constructor(token: Long) : this(token, null, true)
-}
-
 internal fun getAD(version: Byte, token: Long) = ByteBuffer.allocate(2 + 8)
     .put(version)
     .put(TYPE_METADATA)
diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderRestorePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderRestorePlugin.kt
index ff88a0d..b701aa5 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderRestorePlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderRestorePlugin.kt
@@ -1,11 +1,7 @@
 package com.stevesoltys.seedvault.plugins.saf
 
 import android.content.Context
-import android.net.Uri
-import android.util.Log
 import androidx.annotation.WorkerThread
-import androidx.documentfile.provider.DocumentFile
-import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata
 import com.stevesoltys.seedvault.transport.restore.FullRestorePlugin
 import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin
 import com.stevesoltys.seedvault.transport.restore.RestorePlugin
@@ -13,8 +9,6 @@
 import java.io.IOException
 import java.io.InputStream
 
-private val TAG = DocumentsProviderRestorePlugin::class.java.simpleName
-
 @WorkerThread
 @Suppress("BlockingMethodInNonBlockingContext") // all methods do I/O
 internal class DocumentsProviderRestorePlugin(
@@ -25,31 +19,6 @@
 ) : RestorePlugin {
 
     @Throws(IOException::class)
-    override suspend fun hasBackup(uri: Uri): Boolean {
-        val parent = DocumentFile.fromTreeUri(context, uri) ?: throw AssertionError()
-        val rootDir = parent.findFileBlocking(context, DIRECTORY_ROOT) ?: return false
-        val backupSets = getBackups(context, rootDir)
-        return backupSets.isNotEmpty()
-    }
-
-    override suspend fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? {
-        val rootDir = storage.rootBackupDir ?: return null
-        val backupSets = getBackups(context, rootDir)
-        val iterator = backupSets.iterator()
-        return generateSequence {
-            if (!iterator.hasNext()) return@generateSequence null // end sequence
-            val backupSet = iterator.next()
-            try {
-                val stream = storage.getInputStream(backupSet.metadataFile)
-                EncryptedBackupMetadata(backupSet.token, stream)
-            } catch (e: IOException) {
-                Log.e(TAG, "Error getting InputStream for backup metadata.", e)
-                EncryptedBackupMetadata(backupSet.token)
-            }
-        }
-    }
-
-    @Throws(IOException::class)
     override suspend fun getApkInputStream(
         token: Long,
         packageName: String,
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupPlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupPlugin.kt
index 893cc55..31ba5f3 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupPlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupPlugin.kt
@@ -58,7 +58,7 @@
      * Returns an [OutputStream] for writing backup metadata.
      */
     @Throws(IOException::class)
-    @Deprecated("use getOutputStream() instead")
+    @Deprecated("use getOutputStream(token, FILE_BACKUP_METADATA) instead")
     suspend fun getMetadataOutputStream(token: Long): OutputStream
 
     /**
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt
index 500be24..0221d37 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt
@@ -19,8 +19,8 @@
 import com.stevesoltys.seedvault.metadata.MetadataManager
 import com.stevesoltys.seedvault.metadata.MetadataReader
 import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.transport.backup.BackupPlugin
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
-import libcore.io.IoUtils.closeQuietly
 import java.io.IOException
 
 private data class RestoreCoordinatorState(
@@ -43,7 +43,7 @@
     private val settingsManager: SettingsManager,
     private val metadataManager: MetadataManager,
     private val notificationManager: BackupNotificationManager,
-    private val plugin: RestorePlugin,
+    private val plugin: BackupPlugin,
     private val kv: KVRestore,
     private val full: FullRestore,
     private val metadataReader: MetadataReader
@@ -57,15 +57,10 @@
         val availableBackups = plugin.getAvailableBackups() ?: return null
         val metadataMap = HashMap<Long, BackupMetadata>()
         for (encryptedMetadata in availableBackups) {
-            if (encryptedMetadata.error) continue
-            check(encryptedMetadata.inputStream != null) {
-                "No error when getting encrypted metadata, but stream is still missing."
-            }
             try {
-                val metadata = metadataReader.readMetadata(
-                    encryptedMetadata.inputStream,
-                    encryptedMetadata.token
-                )
+                val metadata = encryptedMetadata.inputStreamRetriever().use { inputStream ->
+                    metadataReader.readMetadata(inputStream, encryptedMetadata.token)
+                }
                 metadataMap[encryptedMetadata.token] = metadata
             } catch (e: IOException) {
                 Log.e(TAG, "Error while getting restore set ${encryptedMetadata.token}", e)
@@ -79,8 +74,6 @@
             } catch (e: UnsupportedVersionException) {
                 Log.w(TAG, "Backup with unsupported version read", e)
                 continue
-            } finally {
-                closeQuietly(encryptedMetadata.inputStream)
             }
         }
         Log.i(TAG, "Got available metadata for tokens: ${metadataMap.keys}")
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestorePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestorePlugin.kt
index ceae6ca..a0e23b2 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestorePlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestorePlugin.kt
@@ -1,8 +1,5 @@
 package com.stevesoltys.seedvault.transport.restore
 
-import android.net.Uri
-import androidx.annotation.WorkerThread
-import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata
 import java.io.IOException
 import java.io.InputStream
 
@@ -13,24 +10,6 @@
     val fullRestorePlugin: FullRestorePlugin
 
     /**
-     * Get the set of all backups currently available for restore.
-     *
-     * @return metadata for the set of restore images available,
-     * or null if an error occurred (the attempt should be rescheduled).
-     **/
-    suspend fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>?
-
-    /**
-     * Searches if there's really a backup available in the given location.
-     * Returns true if at least one was found and false otherwise.
-     *
-     * FIXME: Passing a Uri is maybe too plugin-specific?
-     */
-    @WorkerThread
-    @Throws(IOException::class)
-    suspend fun hasBackup(uri: Uri): Boolean
-
-    /**
      * Returns an [InputStream] for the given token, for reading an APK that is to be restored.
      */
     @Throws(IOException::class)
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt
index e5b3c15..330567a 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt
@@ -7,7 +7,7 @@
 import com.stevesoltys.seedvault.R
 import com.stevesoltys.seedvault.plugins.saf.DIRECTORY_ROOT
 import com.stevesoltys.seedvault.settings.SettingsManager
-import com.stevesoltys.seedvault.transport.restore.RestorePlugin
+import com.stevesoltys.seedvault.transport.backup.BackupPlugin
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import java.io.IOException
@@ -16,7 +16,7 @@
 
 internal class RestoreStorageViewModel(
     private val app: Application,
-    private val restorePlugin: RestorePlugin,
+    private val backupPlugin: BackupPlugin,
     settingsManager: SettingsManager
 ) : StorageViewModel(app, settingsManager) {
 
@@ -25,7 +25,7 @@
     override fun onLocationSet(uri: Uri) {
         viewModelScope.launch(Dispatchers.IO) {
             val hasBackup = try {
-                restorePlugin.hasBackup(uri)
+                backupPlugin.hasBackup(uri)
             } catch (e: IOException) {
                 Log.e(TAG, "Error reading URI: $uri", e)
                 false
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
index 67d3d03..234e8d9 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
@@ -104,7 +104,7 @@
         settingsManager,
         metadataManager,
         notificationManager,
-        restorePlugin,
+        backupPlugin,
         kvRestore,
         fullRestore,
         metadataReader
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt
index 7bf5314..ab5c061 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt
@@ -11,11 +11,12 @@
 import com.stevesoltys.seedvault.coAssertThrows
 import com.stevesoltys.seedvault.getRandomString
 import com.stevesoltys.seedvault.header.VERSION
-import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata
 import com.stevesoltys.seedvault.metadata.MetadataReader
 import com.stevesoltys.seedvault.metadata.PackageMetadata
 import com.stevesoltys.seedvault.settings.Storage
 import com.stevesoltys.seedvault.transport.TransportTest
+import com.stevesoltys.seedvault.transport.backup.BackupPlugin
+import com.stevesoltys.seedvault.transport.backup.EncryptedMetadata
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import io.mockk.Runs
 import io.mockk.coEvery
@@ -37,7 +38,7 @@
 internal class RestoreCoordinatorTest : TransportTest() {
 
     private val notificationManager: BackupNotificationManager = mockk()
-    private val plugin = mockk<RestorePlugin>()
+    private val plugin = mockk<BackupPlugin>()
     private val kv = mockk<KVRestore>()
     private val full = mockk<FullRestore>()
     private val metadataReader = mockk<MetadataReader>()
@@ -67,11 +68,11 @@
 
     @Test
     fun `getAvailableRestoreSets() builds set from plugin response`() = runBlocking {
-        val encryptedMetadata = EncryptedBackupMetadata(token, inputStream)
+        val encryptedMetadata = EncryptedMetadata(token) { inputStream }
 
         coEvery { plugin.getAvailableBackups() } returns sequenceOf(
             encryptedMetadata,
-            EncryptedBackupMetadata(token + 1, inputStream)
+            EncryptedMetadata(token + 1) { inputStream }
         )
         every { metadataReader.readMetadata(inputStream, token) } returns metadata
         every { metadataReader.readMetadata(inputStream, token + 1) } returns metadata
@@ -99,8 +100,8 @@
     @Test
     fun `startRestore() fetches metadata if missing`() = runBlocking {
         coEvery { plugin.getAvailableBackups() } returns sequenceOf(
-            EncryptedBackupMetadata(token, inputStream),
-            EncryptedBackupMetadata(token + 1, inputStream)
+            EncryptedMetadata(token) { inputStream },
+            EncryptedMetadata(token + 1) { inputStream }
         )
         every { metadataReader.readMetadata(inputStream, token) } returns metadata
         every { metadataReader.readMetadata(inputStream, token + 1) } returns metadata
@@ -112,7 +113,7 @@
     @Test
     fun `startRestore() errors if metadata is not matching token`() = runBlocking {
         coEvery { plugin.getAvailableBackups() } returns sequenceOf(
-            EncryptedBackupMetadata(token + 42, inputStream)
+            EncryptedMetadata(token + 42) { inputStream }
         )
         every { metadataReader.readMetadata(inputStream, token + 42) } returns metadata
         every { inputStream.close() } just Runs
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt
index 43469e2..38a9b2d 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt
@@ -15,6 +15,7 @@
 import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
 import com.stevesoltys.seedvault.toByteArrayFromHex
 import com.stevesoltys.seedvault.transport.TransportTest
+import com.stevesoltys.seedvault.transport.backup.BackupPlugin
 import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
 import io.mockk.coEvery
 import io.mockk.every
@@ -46,7 +47,7 @@
     private val metadataReader = MetadataReaderImpl(cryptoImpl)
     private val notificationManager = mockk<BackupNotificationManager>()
 
-    private val restorePlugin = mockk<RestorePlugin>()
+    private val backupPlugin = mockk<BackupPlugin>()
     private val kvRestorePlugin = mockk<KVRestorePlugin>()
     private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
     private val fullRestorePlugin = mockk<FullRestorePlugin>()
@@ -57,7 +58,7 @@
         settingsManager,
         metadataManager,
         notificationManager,
-        restorePlugin,
+        backupPlugin,
         kvRestore,
         fullRestore,
         metadataReader