From 791f68300d4a9e2a16f4df1b32f0e31b464ef16d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 10 Dec 2019 11:50:13 -0300 Subject: [PATCH 1/5] Allow to setup NextCloud account during restore This is especially useful when restore is only allowed during SetupWizard and the backup was stored on a NextCloud account. --- .../ui/storage/RestoreStorageViewModel.kt | 13 +- .../ui/storage/StorageRootAdapter.kt | 4 +- .../ui/storage/StorageRootFetcher.kt | 54 +- .../ui/storage/StorageRootsFragment.kt | 2 - app/src/main/res/drawable/nextcloud.xml | 24 + .../res/drawable/nextcloud_background.xml | 1222 +++++++++++++++++ .../res/drawable/nextcloud_foreground.xml | 32 + app/src/main/res/values/strings.xml | 3 + 8 files changed, 1345 insertions(+), 9 deletions(-) create mode 100644 app/src/main/res/drawable/nextcloud.xml create mode 100644 app/src/main/res/drawable/nextcloud_background.xml create mode 100644 app/src/main/res/drawable/nextcloud_foreground.xml 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 a33ceb7a..5183ad74 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 @@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.ui.storage import android.app.Application import android.net.Uri import android.util.Log +import androidx.annotation.WorkerThread import androidx.documentfile.provider.DocumentFile import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.transport.backup.plugins.DIRECTORY_ROOT @@ -14,19 +15,19 @@ internal class RestoreStorageViewModel(private val app: Application) : StorageVi override val isRestoreOperation = true - override fun onLocationSet(uri: Uri) { + override fun onLocationSet(uri: Uri) = Thread { if (hasBackup(uri)) { saveStorage(uri) - mLocationChecked.setEvent(LocationResult()) + mLocationChecked.postEvent(LocationResult()) } else { Log.w(TAG, "Location was rejected: $uri") // notify the UI that the location was invalid val errorMsg = app.getString(R.string.restore_invalid_location_message, DIRECTORY_ROOT) - mLocationChecked.setEvent(LocationResult(errorMsg)) + mLocationChecked.postEvent(LocationResult(errorMsg)) } - } + }.start() /** * Searches if there's really a backup available in the given location. @@ -37,7 +38,11 @@ internal class RestoreStorageViewModel(private val app: Application) : StorageVi * * TODO maybe move this to the RestoreCoordinator once we can inject it */ + @WorkerThread private fun hasBackup(folderUri: Uri): Boolean { + // FIXME This currently fails for NextCloud's DocumentsProvider, + // if called right after setting up an account. + // It requires three attempts to finally find existing backups. val parent = DocumentFile.fromTreeUri(app, folderUri) ?: throw AssertionError() val rootDir = parent.findFile(DIRECTORY_ROOT) ?: return false val backupSets = DocumentsProviderRestorePlugin.getBackups(rootDir) diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootAdapter.kt index 50e80e8a..c9714ba7 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootAdapter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootAdapter.kt @@ -70,7 +70,9 @@ internal class StorageRootAdapter( else -> summaryView.visibility = GONE } v.setOnClickListener { - if (!isRestore && item.isInternal()) { + if (item.overrideClickListener != null) { + item.overrideClickListener.invoke() + } else if (!isRestore && item.isInternal()) { showWarningDialog(v.context, item) } else { listener.onClick(item) diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootFetcher.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootFetcher.kt index 1a74f4df..6617f4cf 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootFetcher.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootFetcher.kt @@ -3,6 +3,8 @@ package com.stevesoltys.seedvault.ui.storage import android.Manifest.permission.MANAGE_DOCUMENTS import android.content.Context import android.content.Intent +import android.content.Intent.ACTION_VIEW +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.PackageManager.GET_META_DATA import android.content.pm.ProviderInfo import android.database.ContentObserver @@ -24,6 +26,10 @@ const val ROOT_ID_DEVICE = "primary" const val ROOT_ID_HOME = "home" const val AUTHORITY_DOWNLOADS = "com.android.providers.downloads.documents" +const val AUTHORITY_NEXTCLOUD = "org.nextcloud.documents" + +private const val NEXTCLOUD_PACKAGE = "com.nextcloud.client" +private const val NEXTCLOUD_ACTIVITY = "com.owncloud.android.authentication.AuthenticatorActivity" data class StorageRoot( internal val authority: String, @@ -34,7 +40,8 @@ data class StorageRoot( internal val summary: String?, internal val availableBytes: Long?, internal val isUsb: Boolean, - internal val enabled: Boolean = true) { + internal val enabled: Boolean = true, + internal val overrideClickListener: (() -> Unit)? = null) { internal val uri: Uri by lazy { DocumentsContract.buildTreeDocumentUri(authority, documentId) @@ -86,7 +93,8 @@ internal class StorageRootFetcher(private val context: Context, private val isRe roots.addAll(getRoots(providerInfo)) } } - if (isAuthoritySupported(AUTHORITY_STORAGE)) checkOrAddUsbRoot(roots) + checkOrAddUsbRoot(roots) + checkOrAddNextCloudRoot(roots) return roots } @@ -136,7 +144,10 @@ internal class StorageRootFetcher(private val context: Context, private val isRe } private fun checkOrAddUsbRoot(roots: ArrayList) { + if (!isAuthoritySupported(AUTHORITY_STORAGE)) return + for (root in roots) { + // return if we already have a USB storage root if (root.authority == AUTHORITY_STORAGE && root.isUsb) return } val root = StorageRoot( @@ -153,6 +164,44 @@ internal class StorageRootFetcher(private val context: Context, private val isRe roots.add(root) } + private fun checkOrAddNextCloudRoot(roots: ArrayList) { + if (!isRestore) return + + for (root in roots) { + // return if we already have a NextCloud storage root + if (root.authority == AUTHORITY_NEXTCLOUD) return + } + val intent = Intent().apply { + addFlags(FLAG_ACTIVITY_NEW_TASK) + setClassName(NEXTCLOUD_PACKAGE, NEXTCLOUD_ACTIVITY) + // setting a nc:// Uri prevents FirstRunActivity to show + data = Uri.parse("nc://login/server:") + putExtra("onlyAdd", true) + } + val isInstalled = packageManager.resolveActivity(intent, 0) != null + val root = StorageRoot( + authority = AUTHORITY_NEXTCLOUD, + rootId = "fake", + documentId = "fake", + icon = getIcon(context, AUTHORITY_NEXTCLOUD, "fake", 0), + title = context.getString(R.string.storage_fake_nextcloud_title), + summary = context.getString(if (isInstalled) R.string.storage_fake_nextcloud_summary_installed else R.string.storage_fake_nextcloud_summary), + availableBytes = null, + isUsb = false, + enabled = true, + overrideClickListener = { + if (isInstalled) context.startActivity(intent) + else { + val uri = Uri.parse("market://details?id=$NEXTCLOUD_PACKAGE") + val i = Intent(ACTION_VIEW, uri) + i.addFlags(FLAG_ACTIVITY_NEW_TASK) + context.startActivity(i) + } + } + ) + roots.add(root) + } + private fun ProviderInfo.isSupported(): Boolean { return if (!exported) { Log.w(TAG, "Provider is not exported") @@ -202,6 +251,7 @@ internal class StorageRootFetcher(private val context: Context, private val isRe return getPackageIcon(context, authority, icon) ?: when { authority == AUTHORITY_STORAGE && rootId == ROOT_ID_DEVICE -> context.getDrawable(R.drawable.ic_phone_android) authority == AUTHORITY_STORAGE && rootId != ROOT_ID_HOME -> context.getDrawable(R.drawable.ic_usb) + authority == AUTHORITY_NEXTCLOUD -> context.getDrawable(R.drawable.nextcloud) else -> null } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootsFragment.kt index 72f540f3..076d2d4a 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootsFragment.kt @@ -17,8 +17,6 @@ import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE import com.stevesoltys.seedvault.ui.REQUEST_CODE_OPEN_DOCUMENT_TREE import kotlinx.android.synthetic.main.fragment_storage_root.* -private val TAG = StorageRootsFragment::class.java.simpleName - internal class StorageRootsFragment : Fragment(), StorageRootClickedListener { companion object { diff --git a/app/src/main/res/drawable/nextcloud.xml b/app/src/main/res/drawable/nextcloud.xml new file mode 100644 index 00000000..e0491076 --- /dev/null +++ b/app/src/main/res/drawable/nextcloud.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/app/src/main/res/drawable/nextcloud_background.xml b/app/src/main/res/drawable/nextcloud_background.xml new file mode 100644 index 00000000..a9025ed7 --- /dev/null +++ b/app/src/main/res/drawable/nextcloud_background.xml @@ -0,0 +1,1222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/nextcloud_foreground.xml b/app/src/main/res/drawable/nextcloud_foreground.xml new file mode 100644 index 00000000..1fb1aca4 --- /dev/null +++ b/app/src/main/res/drawable/nextcloud_foreground.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b965ff78..9e207a7e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,6 +27,9 @@ USB Flash Drive Needs to be plugged in %1$s free + Nextcloud + Click to install + Click to set up account Initializing backup location… Looking for backups… An error occurred while accessing the backup location. From bbc8bdfaa5cd47dff7881243e0ad4fe45a535eb5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 10 Dec 2019 13:34:57 -0300 Subject: [PATCH 2/5] Allow the user to leave the recovery code input if called from setup wizard --- .../ui/recoverycode/RecoveryCodeInputFragment.kt | 7 ++++++- .../res/layout/fragment_recovery_code_input.xml | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeInputFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeInputFragment.kt index 549c1129..1adf407f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeInputFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeInputFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.View.OnFocusChangeListener +import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.AutoCompleteTextView @@ -32,7 +33,11 @@ class RecoveryCodeInputFragment : Fragment() { super.onActivityCreated(savedInstanceState) viewModel = ViewModelProviders.of(requireActivity()).get(RecoveryCodeViewModel::class.java) - if (viewModel.isRestore) introText.setText(R.string.recovery_code_input_intro) + if (viewModel.isRestore) { + introText.setText(R.string.recovery_code_input_intro) + backView.visibility = VISIBLE + backView.setOnClickListener { requireActivity().finishAfterTransition() } + } val adapter = getAdapter() diff --git a/app/src/main/res/layout/fragment_recovery_code_input.xml b/app/src/main/res/layout/fragment_recovery_code_input.xml index 494a9166..7996bcb8 100644 --- a/app/src/main/res/layout/fragment_recovery_code_input.xml +++ b/app/src/main/res/layout/fragment_recovery_code_input.xml @@ -70,6 +70,22 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> + + \ No newline at end of file From 440491425a8af04d50cb4924bfe96a0145a19f71 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 12 Dec 2019 16:27:57 -0300 Subject: [PATCH 3/5] Work around DocumentFile bug happening with cloud-based DocumentsProviders These might return outdated or now content when queried, then check their cloud storage and report back with up-to-date content. We now detect this (when looking for backups on newly setup storage) and wait until the content has been loaded before acting on the response. This is affecting and was tested with NextCloud. --- .../seedvault/transport/PluginManager.kt | 2 +- .../backup/plugins/DocumentsStorage.kt | 74 +++++++++++++++++++ .../transport/restore/RestoreCoordinator.kt | 4 +- .../transport/restore/RestorePlugin.kt | 3 +- .../plugins/DocumentsProviderRestorePlugin.kt | 27 ++++--- .../ui/storage/RestoreStorageViewModel.kt | 8 +- .../transport/CoordinatorIntegrationTest.kt | 2 +- .../restore/RestoreCoordinatorTest.kt | 4 +- 8 files changed, 104 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/PluginManager.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/PluginManager.kt index 7be8a57e..9f8b3563 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/PluginManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/PluginManager.kt @@ -49,6 +49,6 @@ class PluginManager(context: Context) { private val kvRestore = KVRestore(restorePlugin.kvRestorePlugin, outputFactory, headerReader, crypto) private val fullRestore = FullRestore(restorePlugin.fullRestorePlugin, outputFactory, headerReader, crypto) - internal val restoreCoordinator = RestoreCoordinator(settingsManager, restorePlugin, kvRestore, fullRestore, metadataReader) + internal val restoreCoordinator = RestoreCoordinator(context, settingsManager, restorePlugin, kvRestore, fullRestore, metadataReader) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/plugins/DocumentsStorage.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/plugins/DocumentsStorage.kt index 41d9d548..d401c179 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/plugins/DocumentsStorage.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/plugins/DocumentsStorage.kt @@ -1,14 +1,21 @@ package com.stevesoltys.seedvault.transport.backup.plugins +import android.annotation.SuppressLint import android.content.Context import android.content.pm.PackageInfo +import android.database.ContentObserver +import android.net.Uri +import android.provider.DocumentsContract.* +import android.provider.DocumentsContract.Document.* import android.util.Log import androidx.documentfile.provider.DocumentFile import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.Storage +import libcore.io.IoUtils.closeQuietly import java.io.IOException import java.io.InputStream import java.io.OutputStream +import java.util.concurrent.TimeUnit.MINUTES const val DIRECTORY_ROOT = ".AndroidBackup" const val DIRECTORY_FULL_BACKUP = "full" @@ -126,3 +133,70 @@ fun DocumentFile.deleteContents() { fun DocumentFile.assertRightFile(packageInfo: PackageInfo) { if (name != packageInfo.packageName) throw AssertionError() } + +/** + * Works like [DocumentFile.listFiles] except that it waits until the DocumentProvider has a result. + * This prevents getting an empty list even though there are children to be listed. + */ +@Throws(IOException::class) +fun DocumentFile.listFilesBlocking(context: Context): ArrayList { + val resolver = context.contentResolver + val childrenUri = buildChildDocumentsUriUsingTree(uri, getDocumentId(uri)) + val projection = arrayOf(COLUMN_DOCUMENT_ID, COLUMN_MIME_TYPE) + val result = ArrayList() + + @SuppressLint("Recycle") // gets closed in with(), only earlier exit when null + var cursor = resolver.query(childrenUri, projection, null, null, null) + ?: throw IOException() + val loading = cursor.extras.getBoolean(EXTRA_LOADING, false) + if (loading) { + Log.d(TAG, "Wait for children to get loaded...") + var loaded = false + cursor.registerContentObserver(object : ContentObserver(null) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + Log.d(TAG, "Children loaded. Continue...") + loaded = true + } + }) + val timeout = MINUTES.toMillis(2) + var time = 0 + while (!loaded && time < timeout) { + Thread.sleep(50) + time += 50 + } + if (time >= timeout) Log.w(TAG, "Timed out while waiting for children to load") + closeQuietly(cursor) + // do a new query after content was loaded + @SuppressLint("Recycle") // gets closed after with block + cursor = resolver.query(childrenUri, projection, null, null, null) + ?: throw IOException() + } + with(cursor) { + while (moveToNext()) { + val documentId = getString(0) + val isDirectory = getString(1) == MIME_TYPE_DIR + val file = if (isDirectory) { + val treeUri = buildTreeDocumentUri(uri.authority, documentId) + DocumentFile.fromTreeUri(context, treeUri)!! + } else { + val documentUri = buildDocumentUriUsingTree(uri, documentId) + DocumentFile.fromSingleUri(context, documentUri)!! + } + result.add(file) + } + } + return result +} + +fun DocumentFile.findFileBlocking(context: Context, displayName: String): DocumentFile? { + val files = try { + listFilesBlocking(context) + } catch (e: IOException) { + Log.e(TAG, "Error finding file blocking", e) + return null + } + for (doc in files) { + if (displayName == doc.name) return doc + } + return null +} 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 3812e588..e9fa2f14 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 @@ -5,6 +5,7 @@ import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.RestoreDescription import android.app.backup.RestoreDescription.* import android.app.backup.RestoreSet +import android.content.Context import android.content.pm.PackageInfo import android.os.ParcelFileDescriptor import android.util.Log @@ -22,6 +23,7 @@ private class RestoreCoordinatorState( private val TAG = RestoreCoordinator::class.java.simpleName internal class RestoreCoordinator( + private val context: Context, private val settingsManager: SettingsManager, private val plugin: RestorePlugin, private val kv: KVRestore, @@ -37,7 +39,7 @@ internal class RestoreCoordinator( * or null if an error occurred (the attempt should be rescheduled). **/ fun getAvailableRestoreSets(): Array? { - val availableBackups = plugin.getAvailableBackups() ?: return null + val availableBackups = plugin.getAvailableBackups(context) ?: return null val restoreSets = ArrayList() for (encryptedMetadata in availableBackups) { if (encryptedMetadata.error) continue 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 f0e494eb..4a643438 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,5 +1,6 @@ package com.stevesoltys.seedvault.transport.restore +import android.content.Context import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata interface RestorePlugin { @@ -14,6 +15,6 @@ interface RestorePlugin { * @return metadata for the set of restore images available, * or null if an error occurred (the attempt should be rescheduled). **/ - fun getAvailableBackups(): Sequence? + fun getAvailableBackups(context: Context): Sequence? } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/plugins/DocumentsProviderRestorePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/plugins/DocumentsProviderRestorePlugin.kt index 473066d0..ee451ecc 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/plugins/DocumentsProviderRestorePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/plugins/DocumentsProviderRestorePlugin.kt @@ -1,11 +1,11 @@ package com.stevesoltys.seedvault.transport.restore.plugins +import android.content.Context import android.util.Log +import androidx.annotation.WorkerThread import androidx.documentfile.provider.DocumentFile import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata -import com.stevesoltys.seedvault.transport.backup.plugins.DocumentsStorage -import com.stevesoltys.seedvault.transport.backup.plugins.FILE_BACKUP_METADATA -import com.stevesoltys.seedvault.transport.backup.plugins.FILE_NO_MEDIA +import com.stevesoltys.seedvault.transport.backup.plugins.* import com.stevesoltys.seedvault.transport.restore.FullRestorePlugin import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin import com.stevesoltys.seedvault.transport.restore.RestorePlugin @@ -23,9 +23,9 @@ class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : Re DocumentsProviderFullRestorePlugin(storage) } - override fun getAvailableBackups(): Sequence? { + override fun getAvailableBackups(context: Context): Sequence? { val rootDir = storage.rootBackupDir ?: return null - val backupSets = getBackups(rootDir) + val backupSets = getBackups(context, rootDir) val iterator = backupSets.iterator() return generateSequence { if (!iterator.hasNext()) return@generateSequence null // end sequence @@ -41,9 +41,17 @@ class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : Re } companion object { - fun getBackups(rootDir: DocumentFile): List { + @WorkerThread + fun getBackups(context: Context, rootDir: DocumentFile): List { val backupSets = ArrayList() - for (set in rootDir.listFiles()) { + val files = try { + // block until the DocumentsProvider has results + rootDir.listFilesBlocking(context) + } catch (e: IOException) { + Log.e(TAG, "Error loading backups from storage", e) + return backupSets + } + for (set in files) { if (!set.isDirectory || set.name == null) { if (set.name != FILE_NO_MEDIA) { Log.w(TAG, "Found invalid backup set folder: ${set.name}") @@ -53,10 +61,11 @@ class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : Re val token = try { set.name!!.toLong() } catch (e: NumberFormatException) { - Log.w(TAG, "Found invalid backup set folder: ${set.name}", e) + Log.w(TAG, "Found invalid backup set folder: ${set.name}") continue } - val metadata = set.findFile(FILE_BACKUP_METADATA) + // block until children of set are available + val metadata = set.findFileBlocking(context, FILE_BACKUP_METADATA) if (metadata == null) { Log.w(TAG, "Missing metadata file in backup set folder: ${set.name}") } else { 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 5183ad74..944e94b7 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,6 +7,7 @@ import androidx.annotation.WorkerThread import androidx.documentfile.provider.DocumentFile import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.transport.backup.plugins.DIRECTORY_ROOT +import com.stevesoltys.seedvault.transport.backup.plugins.findFileBlocking import com.stevesoltys.seedvault.transport.restore.plugins.DocumentsProviderRestorePlugin private val TAG = RestoreStorageViewModel::class.java.simpleName @@ -40,12 +41,9 @@ internal class RestoreStorageViewModel(private val app: Application) : StorageVi */ @WorkerThread private fun hasBackup(folderUri: Uri): Boolean { - // FIXME This currently fails for NextCloud's DocumentsProvider, - // if called right after setting up an account. - // It requires three attempts to finally find existing backups. val parent = DocumentFile.fromTreeUri(app, folderUri) ?: throw AssertionError() - val rootDir = parent.findFile(DIRECTORY_ROOT) ?: return false - val backupSets = DocumentsProviderRestorePlugin.getBackups(rootDir) + val rootDir = parent.findFileBlocking(app, DIRECTORY_ROOT) ?: return false + val backupSets = DocumentsProviderRestorePlugin.getBackups(app, rootDir) return backupSets.isNotEmpty() } 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 89904481..12331375 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -50,7 +50,7 @@ internal class CoordinatorIntegrationTest : TransportTest() { private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl) private val fullRestorePlugin = mockk() private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl) - private val restore = RestoreCoordinator(settingsManager, restorePlugin, kvRestore, fullRestore, metadataReader) + private val restore = RestoreCoordinator(context, settingsManager, restorePlugin, kvRestore, fullRestore, metadataReader) private val backupDataInput = mockk() private val fileDescriptor = mockk(relaxed = true) 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 b857d785..46fd3ff0 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 @@ -27,7 +27,7 @@ internal class RestoreCoordinatorTest : TransportTest() { private val full = mockk() private val metadataReader = mockk() - private val restore = RestoreCoordinator(settingsManager, plugin, kv, full, metadataReader) + private val restore = RestoreCoordinator(context, settingsManager, plugin, kv, full, metadataReader) private val token = Random.nextLong() private val inputStream = mockk() @@ -43,7 +43,7 @@ internal class RestoreCoordinatorTest : TransportTest() { androidVersion = Random.nextInt(), deviceName = getRandomString()) - every { plugin.getAvailableBackups() } returns sequenceOf(encryptedMetadata, encryptedMetadata) + every { plugin.getAvailableBackups(context) } returns sequenceOf(encryptedMetadata, encryptedMetadata) every { metadataReader.readMetadata(inputStream, token) } returns metadata every { inputStream.close() } just Runs From d30cb309caaf732aace07e11230c5781d0b90fd4 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 12 Dec 2019 16:57:43 -0300 Subject: [PATCH 4/5] Be more forgiving when checking available restore sets --- .../seedvault/transport/restore/RestoreCoordinator.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 e9fa2f14..75ea3a24 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 @@ -49,13 +49,13 @@ internal class RestoreCoordinator( val set = RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token) restoreSets.add(set) } catch (e: IOException) { - Log.e(TAG, "Error while getting restore sets", e) - return null + Log.e(TAG, "Error while getting restore set ${encryptedMetadata.token}", e) + continue } catch (e: SecurityException) { - Log.e(TAG, "Error while getting restore sets", e) + Log.e(TAG, "Error while getting restore set ${encryptedMetadata.token}", e) return null } catch (e: DecryptionFailedException) { - Log.e(TAG, "Error while decrypting restore set", e) + Log.e(TAG, "Error while decrypting restore set ${encryptedMetadata.token}", e) continue } catch (e: UnsupportedVersionException) { Log.w(TAG, "Backup with unsupported version read", e) From 137e8033a784b382b39387503b1441864f439288 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 16 Dec 2019 09:10:03 -0300 Subject: [PATCH 5/5] Update time of backup progress notifications --- .../com/stevesoltys/seedvault/BackupNotificationManager.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/stevesoltys/seedvault/BackupNotificationManager.kt b/app/src/main/java/com/stevesoltys/seedvault/BackupNotificationManager.kt index a19b8267..f82a8fa9 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/BackupNotificationManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/BackupNotificationManager.kt @@ -10,6 +10,7 @@ import android.content.Context import android.content.Intent import androidx.core.app.NotificationCompat.* import com.stevesoltys.seedvault.settings.SettingsActivity +import java.util.* private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver" private const val CHANNEL_ID_ERROR = "NotificationError" @@ -47,6 +48,7 @@ class BackupNotificationManager(private val context: Context) { val notification = observerBuilder.apply { setContentTitle(context.getString(R.string.notification_title)) setContentText(app) + setWhen(Date().time) setProgress(expected, transferred, false) priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW }.build() @@ -62,6 +64,7 @@ class BackupNotificationManager(private val context: Context) { val notification = observerBuilder.apply { setContentTitle(title) setContentText(app) + setWhen(Date().time) priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW }.build() nm.notify(NOTIFICATION_ID_OBSERVER, notification) @@ -79,6 +82,7 @@ class BackupNotificationManager(private val context: Context) { val notification = errorBuilder.apply { setContentTitle(context.getString(R.string.notification_error_title)) setContentText(context.getString(R.string.notification_error_text)) + setWhen(Date().time) setOnlyAlertOnce(true) setAutoCancel(true) mActions = arrayListOf(action)