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:
commit
bb9d498ea8
18 changed files with 1476 additions and 31 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
}
|
||||
|
|
|
@ -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<RestoreSet>? {
|
||||
val availableBackups = plugin.getAvailableBackups() ?: return null
|
||||
val availableBackups = plugin.getAvailableBackups(context) ?: return null
|
||||
val restoreSets = ArrayList<RestoreSet>()
|
||||
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)
|
||||
|
|
|
@ -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<EncryptedBackupMetadata>?
|
||||
fun getAvailableBackups(context: Context): Sequence<EncryptedBackupMetadata>?
|
||||
|
||||
}
|
||||
|
|
|
@ -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<EncryptedBackupMetadata>? {
|
||||
override fun getAvailableBackups(context: Context): Sequence<EncryptedBackupMetadata>? {
|
||||
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<BackupSet> {
|
||||
@WorkerThread
|
||||
fun getBackups(context: Context, rootDir: DocumentFile): List<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.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 {
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<StorageRoot>) {
|
||||
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<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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
24
app/src/main/res/drawable/nextcloud.xml
Normal file
24
app/src/main/res/drawable/nextcloud.xml
Normal 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>
|
1222
app/src/main/res/drawable/nextcloud_background.xml
Normal file
1222
app/src/main/res/drawable/nextcloud_background.xml
Normal file
File diff suppressed because it is too large
Load diff
32
app/src/main/res/drawable/nextcloud_foreground.xml
Normal file
32
app/src/main/res/drawable/nextcloud_foreground.xml
Normal 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>
|
|
@ -70,6 +70,22 @@
|
|||
app:layout_constraintEnd_toEndOf="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>
|
||||
|
||||
</ScrollView>
|
|
@ -27,6 +27,9 @@
|
|||
<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_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_restore_title">Looking for backups…</string>
|
||||
<string name="storage_check_fragment_backup_error">An error occurred while accessing the backup location.</string>
|
||||
|
|
|
@ -50,7 +50,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
||||
private val fullRestorePlugin = mockk<FullRestorePlugin>()
|
||||
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 fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||
|
|
|
@ -27,7 +27,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
private val full = mockk<FullRestore>()
|
||||
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 inputStream = mockk<InputStream>()
|
||||
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue