Merge pull request #54 from grote/nextcloud-restore

Support restore from Nextcloud account even if not installed or set up
This commit is contained in:
Steve Soltys 2019-12-16 11:47:46 -05:00 committed by GitHub
commit bb9d498ea8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1476 additions and 31 deletions

View file

@ -10,6 +10,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.app.NotificationCompat.* import androidx.core.app.NotificationCompat.*
import com.stevesoltys.seedvault.settings.SettingsActivity import com.stevesoltys.seedvault.settings.SettingsActivity
import java.util.*
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver" private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
private const val CHANNEL_ID_ERROR = "NotificationError" private const val CHANNEL_ID_ERROR = "NotificationError"
@ -47,6 +48,7 @@ class BackupNotificationManager(private val context: Context) {
val notification = observerBuilder.apply { val notification = observerBuilder.apply {
setContentTitle(context.getString(R.string.notification_title)) setContentTitle(context.getString(R.string.notification_title))
setContentText(app) setContentText(app)
setWhen(Date().time)
setProgress(expected, transferred, false) setProgress(expected, transferred, false)
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
}.build() }.build()
@ -62,6 +64,7 @@ class BackupNotificationManager(private val context: Context) {
val notification = observerBuilder.apply { val notification = observerBuilder.apply {
setContentTitle(title) setContentTitle(title)
setContentText(app) setContentText(app)
setWhen(Date().time)
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
}.build() }.build()
nm.notify(NOTIFICATION_ID_OBSERVER, notification) nm.notify(NOTIFICATION_ID_OBSERVER, notification)
@ -79,6 +82,7 @@ class BackupNotificationManager(private val context: Context) {
val notification = errorBuilder.apply { val notification = errorBuilder.apply {
setContentTitle(context.getString(R.string.notification_error_title)) setContentTitle(context.getString(R.string.notification_error_title))
setContentText(context.getString(R.string.notification_error_text)) setContentText(context.getString(R.string.notification_error_text))
setWhen(Date().time)
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
setAutoCancel(true) setAutoCancel(true)
mActions = arrayListOf(action) mActions = arrayListOf(action)

View file

@ -49,6 +49,6 @@ class PluginManager(context: Context) {
private val kvRestore = KVRestore(restorePlugin.kvRestorePlugin, outputFactory, headerReader, crypto) private val kvRestore = KVRestore(restorePlugin.kvRestorePlugin, outputFactory, headerReader, crypto)
private val fullRestore = FullRestore(restorePlugin.fullRestorePlugin, 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)
} }

View file

@ -1,14 +1,21 @@
package com.stevesoltys.seedvault.transport.backup.plugins package com.stevesoltys.seedvault.transport.backup.plugins
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.pm.PackageInfo 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 android.util.Log
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.settings.Storage import com.stevesoltys.seedvault.settings.Storage
import libcore.io.IoUtils.closeQuietly
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.util.concurrent.TimeUnit.MINUTES
const val DIRECTORY_ROOT = ".AndroidBackup" const val DIRECTORY_ROOT = ".AndroidBackup"
const val DIRECTORY_FULL_BACKUP = "full" const val DIRECTORY_FULL_BACKUP = "full"
@ -126,3 +133,70 @@ fun DocumentFile.deleteContents() {
fun DocumentFile.assertRightFile(packageInfo: PackageInfo) { fun DocumentFile.assertRightFile(packageInfo: PackageInfo) {
if (name != packageInfo.packageName) throw AssertionError() 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<DocumentFile> {
val resolver = context.contentResolver
val childrenUri = buildChildDocumentsUriUsingTree(uri, getDocumentId(uri))
val projection = arrayOf(COLUMN_DOCUMENT_ID, COLUMN_MIME_TYPE)
val result = ArrayList<DocumentFile>()
@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
}

View file

@ -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.RestoreDescription.* import android.app.backup.RestoreDescription.*
import android.app.backup.RestoreSet import android.app.backup.RestoreSet
import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
@ -22,6 +23,7 @@ private class RestoreCoordinatorState(
private val TAG = RestoreCoordinator::class.java.simpleName private val TAG = RestoreCoordinator::class.java.simpleName
internal class RestoreCoordinator( internal class RestoreCoordinator(
private val context: Context,
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val plugin: RestorePlugin, private val plugin: RestorePlugin,
private val kv: KVRestore, private val kv: KVRestore,
@ -37,7 +39,7 @@ internal class RestoreCoordinator(
* or null if an error occurred (the attempt should be rescheduled). * or null if an error occurred (the attempt should be rescheduled).
**/ **/
fun getAvailableRestoreSets(): Array<RestoreSet>? { fun getAvailableRestoreSets(): Array<RestoreSet>? {
val availableBackups = plugin.getAvailableBackups() ?: return null val availableBackups = plugin.getAvailableBackups(context) ?: return null
val restoreSets = ArrayList<RestoreSet>() val restoreSets = ArrayList<RestoreSet>()
for (encryptedMetadata in availableBackups) { for (encryptedMetadata in availableBackups) {
if (encryptedMetadata.error) continue if (encryptedMetadata.error) continue
@ -47,13 +49,13 @@ internal class RestoreCoordinator(
val set = RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token) val set = RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token)
restoreSets.add(set) restoreSets.add(set)
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error while getting restore sets", e) Log.e(TAG, "Error while getting restore set ${encryptedMetadata.token}", e)
return null continue
} catch (e: SecurityException) { } 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 return null
} catch (e: DecryptionFailedException) { } catch (e: DecryptionFailedException) {
Log.e(TAG, "Error while decrypting restore set", e) Log.e(TAG, "Error while decrypting restore set ${encryptedMetadata.token}", e)
continue continue
} catch (e: UnsupportedVersionException) { } catch (e: UnsupportedVersionException) {
Log.w(TAG, "Backup with unsupported version read", e) Log.w(TAG, "Backup with unsupported version read", e)

View file

@ -1,5 +1,6 @@
package com.stevesoltys.seedvault.transport.restore package com.stevesoltys.seedvault.transport.restore
import android.content.Context
import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata
interface RestorePlugin { interface RestorePlugin {
@ -14,6 +15,6 @@ interface RestorePlugin {
* @return metadata for the set of restore images available, * @return metadata for the set of restore images available,
* or null if an error occurred (the attempt should be rescheduled). * or null if an error occurred (the attempt should be rescheduled).
**/ **/
fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? fun getAvailableBackups(context: Context): Sequence<EncryptedBackupMetadata>?
} }

View file

@ -1,11 +1,11 @@
package com.stevesoltys.seedvault.transport.restore.plugins package com.stevesoltys.seedvault.transport.restore.plugins
import android.content.Context
import android.util.Log import android.util.Log
import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata
import com.stevesoltys.seedvault.transport.backup.plugins.DocumentsStorage import com.stevesoltys.seedvault.transport.backup.plugins.*
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.restore.FullRestorePlugin import com.stevesoltys.seedvault.transport.restore.FullRestorePlugin
import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin
import com.stevesoltys.seedvault.transport.restore.RestorePlugin import com.stevesoltys.seedvault.transport.restore.RestorePlugin
@ -23,9 +23,9 @@ class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : Re
DocumentsProviderFullRestorePlugin(storage) DocumentsProviderFullRestorePlugin(storage)
} }
override fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? { override fun getAvailableBackups(context: Context): Sequence<EncryptedBackupMetadata>? {
val rootDir = storage.rootBackupDir ?: return null val rootDir = storage.rootBackupDir ?: return null
val backupSets = getBackups(rootDir) val backupSets = getBackups(context, rootDir)
val iterator = backupSets.iterator() val iterator = backupSets.iterator()
return generateSequence { return generateSequence {
if (!iterator.hasNext()) return@generateSequence null // end sequence if (!iterator.hasNext()) return@generateSequence null // end sequence
@ -41,9 +41,17 @@ class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : Re
} }
companion object { companion object {
fun getBackups(rootDir: DocumentFile): List<BackupSet> { @WorkerThread
fun getBackups(context: Context, rootDir: DocumentFile): List<BackupSet> {
val backupSets = ArrayList<BackupSet>() val backupSets = ArrayList<BackupSet>()
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.isDirectory || set.name == null) {
if (set.name != FILE_NO_MEDIA) { if (set.name != FILE_NO_MEDIA) {
Log.w(TAG, "Found invalid backup set folder: ${set.name}") Log.w(TAG, "Found invalid backup set folder: ${set.name}")
@ -53,10 +61,11 @@ class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : Re
val token = try { val token = try {
set.name!!.toLong() set.name!!.toLong()
} catch (e: NumberFormatException) { } 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 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) { if (metadata == null) {
Log.w(TAG, "Missing metadata file in backup set folder: ${set.name}") Log.w(TAG, "Missing metadata file in backup set folder: ${set.name}")
} else { } else {

View file

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.OnFocusChangeListener import android.view.View.OnFocusChangeListener
import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
@ -32,7 +33,11 @@ class RecoveryCodeInputFragment : Fragment() {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(requireActivity()).get(RecoveryCodeViewModel::class.java) 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() val adapter = getAdapter()

View file

@ -3,9 +3,11 @@ package com.stevesoltys.seedvault.ui.storage
import android.app.Application import android.app.Application
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.transport.backup.plugins.DIRECTORY_ROOT 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 import com.stevesoltys.seedvault.transport.restore.plugins.DocumentsProviderRestorePlugin
private val TAG = RestoreStorageViewModel::class.java.simpleName private val TAG = RestoreStorageViewModel::class.java.simpleName
@ -14,19 +16,19 @@ internal class RestoreStorageViewModel(private val app: Application) : StorageVi
override val isRestoreOperation = true override val isRestoreOperation = true
override fun onLocationSet(uri: Uri) { override fun onLocationSet(uri: Uri) = Thread {
if (hasBackup(uri)) { if (hasBackup(uri)) {
saveStorage(uri) saveStorage(uri)
mLocationChecked.setEvent(LocationResult()) mLocationChecked.postEvent(LocationResult())
} else { } else {
Log.w(TAG, "Location was rejected: $uri") Log.w(TAG, "Location was rejected: $uri")
// notify the UI that the location was invalid // notify the UI that the location was invalid
val errorMsg = app.getString(R.string.restore_invalid_location_message, DIRECTORY_ROOT) 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. * 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 * TODO maybe move this to the RestoreCoordinator once we can inject it
*/ */
@WorkerThread
private fun hasBackup(folderUri: Uri): Boolean { private fun hasBackup(folderUri: Uri): Boolean {
val parent = DocumentFile.fromTreeUri(app, folderUri) ?: throw AssertionError() val parent = DocumentFile.fromTreeUri(app, folderUri) ?: throw AssertionError()
val rootDir = parent.findFile(DIRECTORY_ROOT) ?: return false val rootDir = parent.findFileBlocking(app, DIRECTORY_ROOT) ?: return false
val backupSets = DocumentsProviderRestorePlugin.getBackups(rootDir) val backupSets = DocumentsProviderRestorePlugin.getBackups(app, rootDir)
return backupSets.isNotEmpty() return backupSets.isNotEmpty()
} }

View file

@ -70,7 +70,9 @@ internal class StorageRootAdapter(
else -> summaryView.visibility = GONE else -> summaryView.visibility = GONE
} }
v.setOnClickListener { v.setOnClickListener {
if (!isRestore && item.isInternal()) { if (item.overrideClickListener != null) {
item.overrideClickListener.invoke()
} else if (!isRestore && item.isInternal()) {
showWarningDialog(v.context, item) showWarningDialog(v.context, item)
} else { } else {
listener.onClick(item) listener.onClick(item)

View file

@ -3,6 +3,8 @@ package com.stevesoltys.seedvault.ui.storage
import android.Manifest.permission.MANAGE_DOCUMENTS import android.Manifest.permission.MANAGE_DOCUMENTS
import android.content.Context import android.content.Context
import android.content.Intent 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.PackageManager.GET_META_DATA
import android.content.pm.ProviderInfo import android.content.pm.ProviderInfo
import android.database.ContentObserver import android.database.ContentObserver
@ -24,6 +26,10 @@ const val ROOT_ID_DEVICE = "primary"
const val ROOT_ID_HOME = "home" const val ROOT_ID_HOME = "home"
const val AUTHORITY_DOWNLOADS = "com.android.providers.downloads.documents" 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( data class StorageRoot(
internal val authority: String, internal val authority: String,
@ -34,7 +40,8 @@ data class StorageRoot(
internal val summary: String?, internal val summary: String?,
internal val availableBytes: Long?, internal val availableBytes: Long?,
internal val isUsb: Boolean, 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 { internal val uri: Uri by lazy {
DocumentsContract.buildTreeDocumentUri(authority, documentId) DocumentsContract.buildTreeDocumentUri(authority, documentId)
@ -86,7 +93,8 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
roots.addAll(getRoots(providerInfo)) roots.addAll(getRoots(providerInfo))
} }
} }
if (isAuthoritySupported(AUTHORITY_STORAGE)) checkOrAddUsbRoot(roots) checkOrAddUsbRoot(roots)
checkOrAddNextCloudRoot(roots)
return roots return roots
} }
@ -136,7 +144,10 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
} }
private fun checkOrAddUsbRoot(roots: ArrayList<StorageRoot>) { private fun checkOrAddUsbRoot(roots: ArrayList<StorageRoot>) {
if (!isAuthoritySupported(AUTHORITY_STORAGE)) return
for (root in roots) { for (root in roots) {
// return if we already have a USB storage root
if (root.authority == AUTHORITY_STORAGE && root.isUsb) return if (root.authority == AUTHORITY_STORAGE && root.isUsb) return
} }
val root = StorageRoot( val root = StorageRoot(
@ -153,6 +164,44 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
roots.add(root) roots.add(root)
} }
private fun checkOrAddNextCloudRoot(roots: ArrayList<StorageRoot>) {
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 { private fun ProviderInfo.isSupported(): Boolean {
return if (!exported) { return if (!exported) {
Log.w(TAG, "Provider is not 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 { 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_DEVICE -> context.getDrawable(R.drawable.ic_phone_android)
authority == AUTHORITY_STORAGE && rootId != ROOT_ID_HOME -> context.getDrawable(R.drawable.ic_usb) authority == AUTHORITY_STORAGE && rootId != ROOT_ID_HOME -> context.getDrawable(R.drawable.ic_usb)
authority == AUTHORITY_NEXTCLOUD -> context.getDrawable(R.drawable.nextcloud)
else -> null else -> null
} }
} }

View file

@ -17,8 +17,6 @@ import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
import com.stevesoltys.seedvault.ui.REQUEST_CODE_OPEN_DOCUMENT_TREE import com.stevesoltys.seedvault.ui.REQUEST_CODE_OPEN_DOCUMENT_TREE
import kotlinx.android.synthetic.main.fragment_storage_root.* import kotlinx.android.synthetic.main.fragment_storage_root.*
private val TAG = StorageRootsFragment::class.java.simpleName
internal class StorageRootsFragment : Fragment(), StorageRootClickedListener { internal class StorageRootsFragment : Fragment(), StorageRootClickedListener {
companion object { companion object {

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Nextcloud Android client application
Copyright (C) 2017 Andy Scherzinger
Copyright (C) 2017 Nextcloud.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
License as published by the Free Software Foundation; either
version 3 of the License, or any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU AFFERO GENERAL PUBLIC LICENSE for more details.
You should have received a copy of the GNU Affero General Public
License along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/nextcloud_background"/>
<foreground android:drawable="@drawable/nextcloud_foreground"/>
</adaptive-icon>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
<!--
Nextcloud Android client application
Copyright (C) 2017 Andy Scherzinger
Copyright (C) 2017 Nextcloud.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
License as published by the Free Software Foundation; either
version 3 of the License, or any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU AFFERO GENERAL PUBLIC LICENSE for more details.
You should have received a copy of the GNU Affero General Public
License along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1636.9231"
android:viewportHeight="1636.9231">
<group android:translateX="286.46155"
android:translateY="286.46155">
<path
android:pathData="M532.7,320C439.3,320 360.9,383.9 337,469.9 316.1,423.9 270.1,391.3 216.6,391.3 143.8,391.3 84,451.2 84,524c-0,72.8 59.8,132.6 132.6,132.7 53.5,-0 99.4,-32.6 120.4,-78.6 23.9,86 102.4,149.9 195.7,149.9 92.8,0 170.8,-63.2 195.3,-148.5 21.2,45.1 66.5,77.2 119.4,77.2 72.8,0 132.7,-59.8 132.6,-132.7 -0,-72.8 -59.9,-132.6 -132.6,-132.6 -52.8,0 -98.2,32 -119.4,77.2 -24.4,-85.3 -102.4,-148.5 -195.3,-148.5zM532.7,397.9c70.1,0 126.1,56 126.1,126.1 0,70.1 -56,126.1 -126.1,126.1 -70.1,-0 -126.1,-56 -126.1,-126.1 0,-70.1 56,-126.1 126.1,-126.1zM216.6,469.2c30.7,0 54.8,24.1 54.8,54.8 0,30.7 -24,54.8 -54.8,54.8 -30.7,0 -54.8,-24.1 -54.8,-54.8 0,-30.7 24.1,-54.8 54.8,-54.8zM847.4,469.2c30.7,-0 54.8,24.1 54.8,54.8 0,30.7 -24.1,54.8 -54.8,54.8 -30.7,0 -54.8,-24.1 -54.8,-54.8 0,-30.7 24.1,-54.8 54.8,-54.8z"
android:fillType="nonZero"
android:fillColor="#ffffff"/>
</group>
</vector>

View file

@ -70,6 +70,22 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/backView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/restore_back"
android:textColor="?android:colorAccent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/doneButton"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintVertical_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/wordList"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </ScrollView>

View file

@ -27,6 +27,9 @@
<string name="storage_fake_drive_title">USB Flash Drive</string> <string name="storage_fake_drive_title">USB Flash Drive</string>
<string name="storage_fake_drive_summary">Needs to be plugged in</string> <string name="storage_fake_drive_summary">Needs to be plugged in</string>
<string name="storage_available_bytes"><xliff:g example="1 GB" id="size">%1$s</xliff:g> free</string> <string name="storage_available_bytes"><xliff:g example="1 GB" id="size">%1$s</xliff:g> free</string>
<string name="storage_fake_nextcloud_title">Nextcloud</string>
<string name="storage_fake_nextcloud_summary">Click to install</string>
<string name="storage_fake_nextcloud_summary_installed">Click to set up account</string>
<string name="storage_check_fragment_backup_title">Initializing backup location…</string> <string name="storage_check_fragment_backup_title">Initializing backup location…</string>
<string name="storage_check_fragment_restore_title">Looking for backups…</string> <string name="storage_check_fragment_restore_title">Looking for backups…</string>
<string name="storage_check_fragment_backup_error">An error occurred while accessing the backup location.</string> <string name="storage_check_fragment_backup_error">An error occurred while accessing the backup location.</string>

View file

@ -50,7 +50,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl) private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
private val fullRestorePlugin = mockk<FullRestorePlugin>() private val fullRestorePlugin = mockk<FullRestorePlugin>()
private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl) 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<BackupDataInput>() private val backupDataInput = mockk<BackupDataInput>()
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true) private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)

View file

@ -27,7 +27,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
private val full = mockk<FullRestore>() private val full = mockk<FullRestore>()
private val metadataReader = mockk<MetadataReader>() private val metadataReader = mockk<MetadataReader>()
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 token = Random.nextLong()
private val inputStream = mockk<InputStream>() private val inputStream = mockk<InputStream>()
@ -43,7 +43,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
androidVersion = Random.nextInt(), androidVersion = Random.nextInt(),
deviceName = getRandomString()) 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 { metadataReader.readMetadata(inputStream, token) } returns metadata
every { inputStream.close() } just Runs every { inputStream.close() } just Runs