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) 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..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 @@ -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 @@ -47,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) 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/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/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt index a33ceb7a..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 @@ -3,9 +3,11 @@ 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 +import com.stevesoltys.seedvault.transport.backup.plugins.findFileBlocking import com.stevesoltys.seedvault.transport.restore.plugins.DocumentsProviderRestorePlugin private val TAG = RestoreStorageViewModel::class.java.simpleName @@ -14,19 +16,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,10 +39,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 { 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/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/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 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. 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