Merge pull request #67 from grote/51-removable-storage-ux
Improve UX for auto-restore when using removable storage
This commit is contained in:
commit
6afdb7f4f7
48 changed files with 568 additions and 91 deletions
app/src
main
AndroidManifest.xml
java/com/stevesoltys/seedvault
App.ktBackupMonitor.ktBackupNotificationManager.ktNotificationBackupObserver.ktUsbIntentReceiver.kt
crypto
metadata
plugins/saf
restore
InstallProgressAdapter.ktRestoreErrorBroadcastReceiver.ktRestoreProgressAdapter.ktRestoreSetAdapter.ktRestoreViewModel.kt
settings
transport
ui
res/values
test/java/com/stevesoltys/seedvault
crypto
header
metadata
transport
|
@ -29,6 +29,10 @@
|
|||
android:name="android.permission.INSTALL_PACKAGES"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<!-- This is needed when using auto-restore with removable storage
|
||||
to allow the user to uninstall an app when storage was not plugged in during install -->
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="false"
|
||||
|
@ -87,5 +91,13 @@
|
|||
android:resource="@xml/device_filter" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".restore.RestoreErrorBroadcastReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.stevesoltys.seedvault.action.UNINSTALL" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -63,5 +63,7 @@ class App : Application() {
|
|||
}
|
||||
|
||||
const val MAGIC_PACKAGE_MANAGER = PACKAGE_MANAGER_SENTINEL
|
||||
const val ANCESTRAL_RECORD_KEY = "@ancestral_record@"
|
||||
const val GLOBAL_METADATA_KEY = "@meta@"
|
||||
|
||||
fun isDebugBuild() = Build.TYPE == "userdebug"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.stevesoltys.seedvault
|
||||
|
||||
import android.app.backup.BackupManagerMonitor.*
|
||||
import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY
|
||||
import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_ID
|
||||
import android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_NAME
|
||||
import android.app.backup.IBackupManagerMonitor
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
|
|
|
@ -3,24 +3,37 @@ package com.stevesoltys.seedvault
|
|||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.NotificationManager.IMPORTANCE_DEFAULT
|
||||
import android.app.NotificationManager.IMPORTANCE_HIGH
|
||||
import android.app.NotificationManager.IMPORTANCE_LOW
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat.*
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import androidx.core.app.NotificationCompat.Action
|
||||
import androidx.core.app.NotificationCompat.Builder
|
||||
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
|
||||
import androidx.core.app.NotificationCompat.PRIORITY_HIGH
|
||||
import androidx.core.app.NotificationCompat.PRIORITY_LOW
|
||||
import com.stevesoltys.seedvault.restore.ACTION_RESTORE_ERROR_UNINSTALL
|
||||
import com.stevesoltys.seedvault.restore.EXTRA_PACKAGE_NAME
|
||||
import com.stevesoltys.seedvault.restore.REQUEST_CODE_UNINSTALL
|
||||
import com.stevesoltys.seedvault.settings.SettingsActivity
|
||||
|
||||
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
|
||||
private const val CHANNEL_ID_ERROR = "NotificationError"
|
||||
private const val CHANNEL_ID_RESTORE_ERROR = "NotificationRestoreError"
|
||||
private const val NOTIFICATION_ID_OBSERVER = 1
|
||||
private const val NOTIFICATION_ID_ERROR = 2
|
||||
private const val NOTIFICATION_ID_RESTORE_ERROR = 3
|
||||
|
||||
class BackupNotificationManager(private val context: Context) {
|
||||
|
||||
private val nm = context.getSystemService(NotificationManager::class.java)!!.apply {
|
||||
createNotificationChannel(getObserverChannel())
|
||||
createNotificationChannel(getErrorChannel())
|
||||
createNotificationChannel(getRestoreErrorChannel())
|
||||
}
|
||||
|
||||
private fun getObserverChannel(): NotificationChannel {
|
||||
|
@ -35,6 +48,11 @@ class BackupNotificationManager(private val context: Context) {
|
|||
return NotificationChannel(CHANNEL_ID_ERROR, title, IMPORTANCE_DEFAULT)
|
||||
}
|
||||
|
||||
private fun getRestoreErrorChannel(): NotificationChannel {
|
||||
val title = context.getString(R.string.notification_restore_error_channel_title)
|
||||
return NotificationChannel(CHANNEL_ID_RESTORE_ERROR, title, IMPORTANCE_HIGH)
|
||||
}
|
||||
|
||||
private val observerBuilder = Builder(context, CHANNEL_ID_OBSERVER).apply {
|
||||
setSmallIcon(R.drawable.ic_cloud_upload)
|
||||
}
|
||||
|
@ -43,10 +61,16 @@ class BackupNotificationManager(private val context: Context) {
|
|||
setSmallIcon(R.drawable.ic_cloud_error)
|
||||
}
|
||||
|
||||
private val restoreErrorBuilder = Builder(context, CHANNEL_ID_RESTORE_ERROR).apply {
|
||||
setSmallIcon(R.drawable.ic_cloud_error)
|
||||
}
|
||||
|
||||
fun onBackupUpdate(app: CharSequence, transferred: Int, expected: Int, userInitiated: Boolean) {
|
||||
val notification = observerBuilder.apply {
|
||||
setContentTitle(context.getString(R.string.notification_title))
|
||||
setContentText(app)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setWhen(System.currentTimeMillis())
|
||||
setProgress(expected, transferred, false)
|
||||
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
|
||||
|
@ -63,14 +87,33 @@ class BackupNotificationManager(private val context: Context) {
|
|||
val notification = observerBuilder.apply {
|
||||
setContentTitle(title)
|
||||
setContentText(app)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setWhen(System.currentTimeMillis())
|
||||
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
|
||||
}.build()
|
||||
nm.notify(NOTIFICATION_ID_OBSERVER, notification)
|
||||
}
|
||||
|
||||
fun onBackupFinished() {
|
||||
nm.cancel(NOTIFICATION_ID_OBSERVER)
|
||||
fun onBackupFinished(success: Boolean, notBackedUp: Int?, userInitiated: Boolean) {
|
||||
if (!userInitiated) {
|
||||
nm.cancel(NOTIFICATION_ID_OBSERVER)
|
||||
return
|
||||
}
|
||||
val titleRes = if (success) R.string.notification_success_title else R.string.notification_failed_title
|
||||
val contentText = if (notBackedUp == null) null else {
|
||||
context.getString(R.string.notification_success_num_not_backed_up, notBackedUp)
|
||||
}
|
||||
val notification = observerBuilder.apply {
|
||||
setContentTitle(context.getString(titleRes))
|
||||
setContentText(contentText)
|
||||
setOngoing(false)
|
||||
setShowWhen(true)
|
||||
setWhen(System.currentTimeMillis())
|
||||
setProgress(0, 0, false)
|
||||
priority = PRIORITY_LOW
|
||||
}.build()
|
||||
nm.notify(NOTIFICATION_ID_OBSERVER, notification)
|
||||
}
|
||||
|
||||
fun onBackupError() {
|
||||
|
@ -93,4 +136,33 @@ class BackupNotificationManager(private val context: Context) {
|
|||
nm.cancel(NOTIFICATION_ID_ERROR)
|
||||
}
|
||||
|
||||
fun onRemovableStorageNotAvailableForRestore(packageName: String, storageName: String) {
|
||||
val appName = try {
|
||||
val appInfo = context.packageManager.getApplicationInfo(packageName, 0)
|
||||
context.packageManager.getApplicationLabel(appInfo)
|
||||
} catch (e: NameNotFoundException) {
|
||||
packageName
|
||||
}
|
||||
val intent = Intent(ACTION_RESTORE_ERROR_UNINSTALL).apply {
|
||||
setPackage(context.packageName)
|
||||
putExtra(EXTRA_PACKAGE_NAME, packageName)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getBroadcast(context, REQUEST_CODE_UNINSTALL, intent, FLAG_UPDATE_CURRENT)
|
||||
val actionText = context.getString(R.string.notification_restore_error_action)
|
||||
val action = Action(R.drawable.ic_warning, actionText, pendingIntent)
|
||||
val notification = restoreErrorBuilder.apply {
|
||||
setContentTitle(context.getString(R.string.notification_restore_error_title, appName))
|
||||
setContentText(context.getString(R.string.notification_restore_error_text, storageName))
|
||||
setWhen(System.currentTimeMillis())
|
||||
setAutoCancel(true)
|
||||
priority = PRIORITY_HIGH
|
||||
mActions = arrayListOf(action)
|
||||
}.build()
|
||||
nm.notify(NOTIFICATION_ID_RESTORE_ERROR, notification)
|
||||
}
|
||||
|
||||
fun onRestoreErrorSeen() {
|
||||
nm.cancel(NOTIFICATION_ID_RESTORE_ERROR)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.content.pm.PackageManager.NameNotFoundException
|
|||
import android.util.Log
|
||||
import android.util.Log.INFO
|
||||
import android.util.Log.isLoggable
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
|
||||
|
@ -14,9 +15,18 @@ private val TAG = NotificationBackupObserver::class.java.simpleName
|
|||
|
||||
class NotificationBackupObserver(
|
||||
private val context: Context,
|
||||
private val expectedPackages: Int,
|
||||
private val userInitiated: Boolean) : IBackupObserver.Stub(), KoinComponent {
|
||||
|
||||
private val nm: BackupNotificationManager by inject()
|
||||
private val metadataManager: MetadataManager by inject()
|
||||
private var currentPackage: String? = null
|
||||
private var numPackages: Int = 0
|
||||
|
||||
init {
|
||||
// we need to show this manually as [onUpdate] isn't called for first @pm@ package
|
||||
nm.onBackupUpdate(getAppName(MAGIC_PACKAGE_MANAGER), 0, expectedPackages, userInitiated)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method could be called several times for packages with full data backup.
|
||||
|
@ -26,10 +36,7 @@ class NotificationBackupObserver(
|
|||
* @param backupProgress Current progress of backup for the package.
|
||||
*/
|
||||
override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) {
|
||||
val transferred = backupProgress.bytesTransferred.toInt()
|
||||
val expected = backupProgress.bytesExpected.toInt()
|
||||
val app = getAppName(currentBackupPackage)
|
||||
nm.onBackupUpdate(app, transferred, expected, userInitiated)
|
||||
showProgressNotification(currentBackupPackage)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,7 +53,8 @@ class NotificationBackupObserver(
|
|||
if (isLoggable(TAG, INFO)) {
|
||||
Log.i(TAG, "Completed. Target: $target, status: $status")
|
||||
}
|
||||
nm.onBackupResult(getAppName(target), status, userInitiated)
|
||||
// often [onResult] gets called right away without any [onUpdate] call
|
||||
showProgressNotification(target)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,9 +66,23 @@ class NotificationBackupObserver(
|
|||
*/
|
||||
override fun backupFinished(status: Int) {
|
||||
if (isLoggable(TAG, INFO)) {
|
||||
Log.i(TAG, "Backup finished. Status: $status")
|
||||
Log.i(TAG, "Backup finished $numPackages/$expectedPackages. Status: $status")
|
||||
}
|
||||
nm.onBackupFinished()
|
||||
val success = status == 0
|
||||
val notBackedUp = if (success) metadataManager.getPackagesNumNotBackedUp() else null
|
||||
nm.onBackupFinished(success, notBackedUp, userInitiated)
|
||||
}
|
||||
|
||||
private fun showProgressNotification(packageName: String) {
|
||||
if (currentPackage == packageName) return
|
||||
|
||||
if (isLoggable(TAG, INFO)) {
|
||||
Log.i(TAG, "Showing progress notification for $currentPackage $numPackages/$expectedPackages")
|
||||
}
|
||||
currentPackage = packageName
|
||||
val app = getAppName(packageName)
|
||||
numPackages += 1
|
||||
nm.onBackupUpdate(app, numPackages, expectedPackages, userInitiated)
|
||||
}
|
||||
|
||||
private fun getAppName(packageId: String): CharSequence = getAppName(context, packageId)
|
||||
|
|
|
@ -6,7 +6,9 @@ import android.content.Intent
|
|||
import android.database.ContentObserver
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbInterface
|
||||
import android.hardware.usb.UsbManager.*
|
||||
import android.hardware.usb.UsbManager.ACTION_USB_DEVICE_ATTACHED
|
||||
import android.hardware.usb.UsbManager.ACTION_USB_DEVICE_DETACHED
|
||||
import android.hardware.usb.UsbManager.EXTRA_DEVICE
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.provider.DocumentsContract
|
||||
|
@ -23,6 +25,7 @@ private val TAG = UsbIntentReceiver::class.java.simpleName
|
|||
|
||||
class UsbIntentReceiver : UsbMonitor() {
|
||||
|
||||
// using KoinComponent would crash robolectric tests :(
|
||||
private val settingsManager: SettingsManager by lazy { get().koin.get<SettingsManager>() }
|
||||
private val metadataManager: MetadataManager by lazy { get().koin.get<MetadataManager>() }
|
||||
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
package com.stevesoltys.seedvault.crypto
|
||||
|
||||
import com.stevesoltys.seedvault.header.*
|
||||
import com.stevesoltys.seedvault.header.HeaderReader
|
||||
import com.stevesoltys.seedvault.header.HeaderWriter
|
||||
import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH
|
||||
import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
|
||||
import com.stevesoltys.seedvault.header.MAX_VERSION_HEADER_SIZE
|
||||
import com.stevesoltys.seedvault.header.SegmentHeader
|
||||
import com.stevesoltys.seedvault.header.VersionHeader
|
||||
import java.io.EOFException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package com.stevesoltys.seedvault.crypto
|
||||
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.security.keystore.KeyProperties.*
|
||||
import android.security.keystore.KeyProperties.BLOCK_MODE_GCM
|
||||
import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE
|
||||
import android.security.keystore.KeyProperties.PURPOSE_DECRYPT
|
||||
import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT
|
||||
import android.security.keystore.KeyProtection
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStore.SecretKeyEntry
|
||||
|
|
|
@ -178,6 +178,13 @@ class MetadataManager(
|
|||
return metadata.packageMetadataMap[packageName]?.copy()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getPackagesNumNotBackedUp(): Int {
|
||||
return metadata.packageMetadataMap.filter { (_, packageMetadata) ->
|
||||
!packageMetadata.system && packageMetadata.state != APK_AND_DATA
|
||||
}.count()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@VisibleForTesting
|
||||
private fun getMetadataFromCache(): BackupMetadata? {
|
||||
|
|
|
@ -4,7 +4,11 @@ import com.stevesoltys.seedvault.Utf8
|
|||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.*
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
|
|
|
@ -5,8 +5,14 @@ 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.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID
|
||||
import android.provider.DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||
import android.provider.DocumentsContract.Document.MIME_TYPE_DIR
|
||||
import android.provider.DocumentsContract.EXTRA_LOADING
|
||||
import android.provider.DocumentsContract.buildChildDocumentsUriUsingTree
|
||||
import android.provider.DocumentsContract.buildDocumentUriUsingTree
|
||||
import android.provider.DocumentsContract.buildTreeDocumentUri
|
||||
import android.provider.DocumentsContract.getDocumentId
|
||||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
|
|
|
@ -14,7 +14,10 @@ import androidx.recyclerview.widget.SortedList
|
|||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreResult
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.*
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.FAILED
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.IN_PROGRESS
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.SUCCEEDED
|
||||
|
||||
internal class InstallProgressAdapter : Adapter<AppViewHolder>() {
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package com.stevesoltys.seedvault.restore
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import androidx.core.net.toUri
|
||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||
import org.koin.core.context.GlobalContext.get
|
||||
|
||||
internal const val ACTION_RESTORE_ERROR_UNINSTALL = "com.stevesoltys.seedvault.action.UNINSTALL"
|
||||
internal const val EXTRA_PACKAGE_NAME = "com.stevesoltys.seedvault.extra.PACKAGE_NAME"
|
||||
internal const val REQUEST_CODE_UNINSTALL = 4576841
|
||||
|
||||
class RestoreErrorBroadcastReceiver : BroadcastReceiver() {
|
||||
|
||||
// using KoinComponent would crash robolectric tests :(
|
||||
private val notificationManager: BackupNotificationManager by lazy { get().koin.get<BackupNotificationManager>() }
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != ACTION_RESTORE_ERROR_UNINSTALL) return
|
||||
|
||||
notificationManager.onRestoreErrorSeen()
|
||||
|
||||
val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME)!!
|
||||
@Suppress("DEPRECATION") // the alternative doesn't work for us
|
||||
val i = Intent(Intent.ACTION_UNINSTALL_PACKAGE).apply {
|
||||
data = "package:$packageName".toUri()
|
||||
flags = FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
context.startActivity(i)
|
||||
}
|
||||
|
||||
}
|
|
@ -3,7 +3,9 @@ package com.stevesoltys.seedvault.restore
|
|||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.*
|
||||
import android.view.View.GONE
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
|
@ -13,7 +15,13 @@ import androidx.recyclerview.widget.RecyclerView.Adapter
|
|||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.*
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_INSTALLED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NO_DATA
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.IN_PROGRESS
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
|
||||
import com.stevesoltys.seedvault.restore.RestoreProgressAdapter.PackageViewHolder
|
||||
import java.util.*
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.stevesoltys.seedvault.restore
|
||||
|
||||
import android.text.format.DateUtils.*
|
||||
import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE
|
||||
import android.text.format.DateUtils.HOUR_IN_MILLIS
|
||||
import android.text.format.DateUtils.getRelativeTimeSpanString
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
|
|
@ -20,8 +20,18 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
|||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||
import com.stevesoltys.seedvault.getAppName
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.*
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.*
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_INSTALLED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NO_DATA
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.IN_PROGRESS
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
|
||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
|
|
|
@ -180,6 +180,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
activity?.contentResolver?.let {
|
||||
autoRestore.isChecked = Settings.Secure.getInt(it, BACKUP_AUTO_RESTORE, 1) == 1
|
||||
}
|
||||
val storage = this.storage
|
||||
if (storage?.isUsb == true) {
|
||||
autoRestore.summary = getString(R.string.settings_auto_restore_summary) + "\n\n" +
|
||||
getString(R.string.settings_auto_restore_summary_usb, storage.name)
|
||||
} else {
|
||||
autoRestore.setSummary(R.string.settings_auto_restore_summary)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBackupLocationSummary(lastBackupInMillis: Long) {
|
||||
|
|
|
@ -15,7 +15,6 @@ import androidx.annotation.WorkerThread
|
|||
import com.stevesoltys.seedvault.BackupMonitor
|
||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.NotificationBackupObserver
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||
import org.koin.core.context.GlobalContext.get
|
||||
|
||||
|
@ -52,19 +51,16 @@ class ConfigurableBackupTransportService : Service() {
|
|||
|
||||
@WorkerThread
|
||||
fun requestBackup(context: Context) {
|
||||
// show notification
|
||||
val nm: BackupNotificationManager = get().koin.get()
|
||||
nm.onBackupUpdate(context.getString(R.string.notification_backup_starting), 0, 1, true)
|
||||
|
||||
val packageService: PackageService = get().koin.get()
|
||||
val observer = NotificationBackupObserver(context, true)
|
||||
val flags = FLAG_NON_INCREMENTAL_BACKUP or FLAG_USER_INITIATED
|
||||
val packages = packageService.eligiblePackages
|
||||
|
||||
val observer = NotificationBackupObserver(context, packages.size, true)
|
||||
val result = try {
|
||||
val backupManager: IBackupManager = get().koin.get()
|
||||
backupManager.requestBackup(packages, observer, BackupMonitor(), flags)
|
||||
backupManager.requestBackup(packages, observer, BackupMonitor(), FLAG_USER_INITIATED)
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Error during backup: ", e)
|
||||
val nm: BackupNotificationManager = get().koin.get()
|
||||
nm.onBackupError()
|
||||
}
|
||||
if (result == BackupManager.SUCCESS) {
|
||||
|
|
|
@ -8,7 +8,11 @@ import android.util.Log
|
|||
import android.util.PackageUtils.computeSha256DigestBytes
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.encodeBase64
|
||||
import com.stevesoltys.seedvault.metadata.*
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageState
|
||||
import com.stevesoltys.seedvault.metadata.isSystemApp
|
||||
import com.stevesoltys.seedvault.metadata.isUpdatedSystemApp
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package com.stevesoltys.seedvault.transport.backup
|
||||
|
||||
import android.app.backup.BackupTransport.*
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
|
@ -10,7 +13,10 @@ import com.stevesoltys.seedvault.Clock
|
|||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.metadata.PackageState
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.*
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||
import com.stevesoltys.seedvault.metadata.isSystemApp
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import java.io.IOException
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package com.stevesoltys.seedvault.transport.backup
|
||||
|
||||
import android.app.backup.BackupTransport.*
|
||||
import android.app.backup.BackupTransport.FLAG_USER_INITIATED
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package com.stevesoltys.seedvault.transport.backup
|
||||
|
||||
import android.app.backup.BackupTransport.*
|
||||
import android.app.backup.BackupTransport.FLAG_INCREMENTAL
|
||||
import android.app.backup.BackupTransport.FLAG_NON_INCREMENTAL
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
|
|
|
@ -2,10 +2,18 @@ package com.stevesoltys.seedvault.transport.restore
|
|||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.*
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_RECEIVER_FOREGROUND
|
||||
import android.content.IntentFilter
|
||||
import android.content.IntentSender
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageInstaller.*
|
||||
import android.content.pm.PackageInstaller.EXTRA_PACKAGE_NAME
|
||||
import android.content.pm.PackageInstaller.EXTRA_STATUS
|
||||
import android.content.pm.PackageInstaller.EXTRA_STATUS_MESSAGE
|
||||
import android.content.pm.PackageInstaller.STATUS_SUCCESS
|
||||
import android.content.pm.PackageInstaller.SessionParams
|
||||
import android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package com.stevesoltys.seedvault.transport.restore
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager.*
|
||||
import android.content.pm.PackageManager.GET_SIGNATURES
|
||||
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.encodeBase64
|
||||
|
@ -9,7 +11,9 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
|
|||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||
import com.stevesoltys.seedvault.metadata.isSystemApp
|
||||
import com.stevesoltys.seedvault.transport.backup.getSignatures
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.*
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.FAILED
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.IN_PROGRESS
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package com.stevesoltys.seedvault.transport.restore
|
||||
|
||||
import android.app.backup.BackupTransport.*
|
||||
import android.app.backup.BackupTransport.NO_MORE_DATA
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
|
|
|
@ -6,6 +6,9 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
|
|||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.ANCESTRAL_RECORD_KEY
|
||||
import com.stevesoltys.seedvault.GLOBAL_METADATA_KEY
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
import com.stevesoltys.seedvault.decodeBase64
|
||||
import com.stevesoltys.seedvault.header.HeaderReader
|
||||
|
@ -17,7 +20,11 @@ import javax.crypto.AEADBadTagException
|
|||
|
||||
private class KVRestoreState(
|
||||
internal val token: Long,
|
||||
internal val packageInfo: PackageInfo)
|
||||
internal val packageInfo: PackageInfo,
|
||||
/**
|
||||
* Optional [PackageInfo] for single package restore, optimizes restore of @pm@
|
||||
*/
|
||||
internal val pmPackageInfo: PackageInfo?)
|
||||
|
||||
private val TAG = KVRestore::class.java.simpleName
|
||||
|
||||
|
@ -42,9 +49,11 @@ internal class KVRestore(
|
|||
*
|
||||
* It is possible that the system decides to not restore the package.
|
||||
* Then a new state will be initialized right away without calling other methods.
|
||||
*
|
||||
* @param pmPackageInfo single optional [PackageInfo] to optimize restore of @pm@
|
||||
*/
|
||||
fun initializeState(token: Long, packageInfo: PackageInfo) {
|
||||
state = KVRestoreState(token, packageInfo)
|
||||
fun initializeState(token: Long, packageInfo: PackageInfo, pmPackageInfo: PackageInfo? = null) {
|
||||
state = KVRestoreState(token, packageInfo, pmPackageInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,8 +120,15 @@ internal class KVRestore(
|
|||
// Decode the key filenames into keys then sort lexically by key
|
||||
val contents = ArrayList<DecodedKey>()
|
||||
for (recordKey in records) contents.add(DecodedKey(recordKey))
|
||||
contents.sort()
|
||||
return contents
|
||||
// remove keys that are not needed for single package @pm@ restore
|
||||
val pmPackageName = state?.pmPackageInfo?.packageName
|
||||
val sortedKeys = if (packageInfo.packageName == MAGIC_PACKAGE_MANAGER && pmPackageName != null) {
|
||||
val keys = listOf(ANCESTRAL_RECORD_KEY, GLOBAL_METADATA_KEY, pmPackageName)
|
||||
Log.d(TAG, "Single package restore, restrict restore keys to $pmPackageName")
|
||||
contents.filterTo(ArrayList()) { it.key in keys }
|
||||
} else contents
|
||||
sortedKeys.sort()
|
||||
return sortedKeys
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,29 +4,44 @@ import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
|||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import android.app.backup.IBackupManager
|
||||
import android.app.backup.RestoreDescription
|
||||
import android.app.backup.RestoreDescription.*
|
||||
import android.app.backup.RestoreDescription.NO_MORE_PACKAGES
|
||||
import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
|
||||
import android.app.backup.RestoreDescription.TYPE_KEY_VALUE
|
||||
import android.app.backup.RestoreSet
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import androidx.collection.LongSparseArray
|
||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
||||
import com.stevesoltys.seedvault.metadata.DecryptionFailedException
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.metadata.MetadataReader
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import libcore.io.IoUtils.closeQuietly
|
||||
import java.io.IOException
|
||||
|
||||
private class RestoreCoordinatorState(
|
||||
internal val token: Long,
|
||||
internal val packages: Iterator<PackageInfo>,
|
||||
internal var currentPackage: String? = null)
|
||||
/**
|
||||
* Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
|
||||
*/
|
||||
internal val pmPackageInfo: PackageInfo?) {
|
||||
internal var currentPackage: String? = null
|
||||
}
|
||||
|
||||
private val TAG = RestoreCoordinator::class.java.simpleName
|
||||
|
||||
internal class RestoreCoordinator(
|
||||
private val context: Context,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val metadataManager: MetadataManager,
|
||||
private val notificationManager: BackupNotificationManager,
|
||||
private val plugin: RestorePlugin,
|
||||
private val kv: KVRestore,
|
||||
private val full: FullRestore,
|
||||
|
@ -104,7 +119,26 @@ internal class RestoreCoordinator(
|
|||
fun startRestore(token: Long, packages: Array<out PackageInfo>): Int {
|
||||
check(state == null) { "Started new restore with existing state" }
|
||||
Log.i(TAG, "Start restore with ${packages.map { info -> info.packageName }}")
|
||||
state = RestoreCoordinatorState(token, packages.iterator())
|
||||
|
||||
// If there's only one package to restore (Auto Restore feature), add it to the state
|
||||
val pmPackageInfo = if (packages.size == 2 && packages[0].packageName == MAGIC_PACKAGE_MANAGER) {
|
||||
val pmPackageName = packages[1].packageName
|
||||
Log.d(TAG, "Optimize for single package restore of $pmPackageName")
|
||||
// check if the backup is on removable storage that is not plugged in
|
||||
if (isStorageRemovableAndNotAvailable()) {
|
||||
// check if we even have a backup of that app
|
||||
if (metadataManager.getPackageMetadata(pmPackageName) != null) {
|
||||
// remind user to plug in storage device
|
||||
val storageName = settingsManager.getStorage()?.name
|
||||
?: context.getString(R.string.settings_backup_location_none)
|
||||
notificationManager.onRemovableStorageNotAvailableForRestore(pmPackageName, storageName)
|
||||
}
|
||||
return TRANSPORT_ERROR
|
||||
}
|
||||
packages[1]
|
||||
} else null
|
||||
|
||||
state = RestoreCoordinatorState(token, packages.iterator(), pmPackageInfo)
|
||||
failedPackages.clear()
|
||||
return TRANSPORT_OK
|
||||
}
|
||||
|
@ -148,7 +182,7 @@ internal class RestoreCoordinator(
|
|||
// check key/value data first and if available, don't even check for full data
|
||||
kv.hasDataForPackage(state.token, packageInfo) -> {
|
||||
Log.i(TAG, "Found K/V data for $packageName.")
|
||||
kv.initializeState(state.token, packageInfo)
|
||||
kv.initializeState(state.token, packageInfo, state.pmPackageInfo)
|
||||
state.currentPackage = packageName
|
||||
TYPE_KEY_VALUE
|
||||
}
|
||||
|
@ -174,7 +208,7 @@ internal class RestoreCoordinator(
|
|||
/**
|
||||
* Get the data for the application returned by [nextRestorePackage],
|
||||
* if that method reported [TYPE_KEY_VALUE] as its delivery type.
|
||||
* If the package has only TYPE_FULL_STREAM data, then this method will return an error.
|
||||
* If the package has only [TYPE_FULL_STREAM] data, then this method will return an error.
|
||||
*
|
||||
* @param data An open, writable file into which the key/value backup data should be stored.
|
||||
* @return the same error codes as [startRestore].
|
||||
|
@ -232,4 +266,10 @@ internal class RestoreCoordinator(
|
|||
|
||||
fun isFailedPackage(packageName: String) = packageName in failedPackages
|
||||
|
||||
// TODO this is plugin specific, needs to be factored out when supporting different plugins
|
||||
private fun isStorageRemovableAndNotAvailable(): Boolean {
|
||||
val storage = settingsManager.getStorage() ?: return false
|
||||
return storage.isUsb && !storage.getDocumentFile(context).isDirectory
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,5 +8,5 @@ val restoreModule = module {
|
|||
factory { ApkRestore(androidContext(), get()) }
|
||||
single { KVRestore(get<RestorePlugin>().kvRestorePlugin, get(), get(), get()) }
|
||||
single { FullRestore(get<RestorePlugin>().fullRestorePlugin, get(), get(), get()) }
|
||||
single { RestoreCoordinator(get(), get(), get(), get(), get()) }
|
||||
single { RestoreCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
}
|
||||
|
|
|
@ -5,11 +5,15 @@ import com.stevesoltys.seedvault.App
|
|||
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
||||
import io.github.novacrypto.bip39.*
|
||||
import io.github.novacrypto.bip39.JavaxPBKDF2WithHmacSHA512
|
||||
import io.github.novacrypto.bip39.MnemonicGenerator
|
||||
import io.github.novacrypto.bip39.MnemonicValidator
|
||||
import io.github.novacrypto.bip39.SeedCalculator
|
||||
import io.github.novacrypto.bip39.Validation.InvalidChecksumException
|
||||
import io.github.novacrypto.bip39.Validation.InvalidWordCountException
|
||||
import io.github.novacrypto.bip39.Validation.UnexpectedWhiteSpaceException
|
||||
import io.github.novacrypto.bip39.Validation.WordNotFoundException
|
||||
import io.github.novacrypto.bip39.Words
|
||||
import io.github.novacrypto.bip39.wordlists.English
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
|
|
|
@ -14,7 +14,16 @@ import android.net.Uri
|
|||
import android.os.Handler
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.DocumentsContract.PROVIDER_INTERFACE
|
||||
import android.provider.DocumentsContract.Root.*
|
||||
import android.provider.DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
|
||||
import android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID
|
||||
import android.provider.DocumentsContract.Root.COLUMN_FLAGS
|
||||
import android.provider.DocumentsContract.Root.COLUMN_ICON
|
||||
import android.provider.DocumentsContract.Root.COLUMN_ROOT_ID
|
||||
import android.provider.DocumentsContract.Root.COLUMN_SUMMARY
|
||||
import android.provider.DocumentsContract.Root.COLUMN_TITLE
|
||||
import android.provider.DocumentsContract.Root.FLAG_REMOVABLE_USB
|
||||
import android.provider.DocumentsContract.Root.FLAG_SUPPORTS_CREATE
|
||||
import android.provider.DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
|
||||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.R
|
||||
import java.lang.Long.parseLong
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package com.stevesoltys.seedvault.ui.storage
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.Intent.*
|
||||
import android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
<string name="settings_backup_location_summary">%1$s · Last Backup %2$s</string>
|
||||
<string name="settings_info">All backups are encrypted on your phone. To restore from backup you will need your 12-word recovery code.</string>
|
||||
<string name="settings_auto_restore_title">Automatic restore</string>
|
||||
<string name="settings_auto_restore_summary">When reinstalling an app, restore backed up settings and data</string>
|
||||
<string name="settings_auto_restore_summary">When reinstalling an app, restore backed up settings and data.</string>
|
||||
<string name="settings_auto_restore_summary_usb">Note: Your %1$s needs to be plugged in for this to work.</string>
|
||||
<string name="settings_backup_apk_title">App backup</string>
|
||||
<string name="settings_backup_apk_summary">Back up the apps themselves. Otherwise, only app data would get backed up.</string>
|
||||
<string name="settings_backup_apk_dialog_title">Really disable app backup?</string>
|
||||
|
@ -69,16 +70,24 @@
|
|||
<!-- Notification -->
|
||||
<string name="notification_channel_title">Backup Notification</string>
|
||||
<string name="notification_title">Backup running</string>
|
||||
<string name="notification_backup_starting">Starting Backup…</string>
|
||||
<string name="notification_backup_result_complete">Backup complete</string>
|
||||
<string name="notification_backup_result_rejected">Not backed up</string>
|
||||
<string name="notification_backup_result_error">Backup failed</string>
|
||||
|
||||
<string name="notification_success_title">Backup finished</string>
|
||||
<string name="notification_success_num_not_backed_up">%1$d apps could not get backed up</string>
|
||||
<string name="notification_failed_title">Backup failed</string>
|
||||
|
||||
<string name="notification_error_channel_title">Error Notification</string>
|
||||
<string name="notification_error_title">Backup Error</string>
|
||||
<string name="notification_error_text">A device backup failed to run.</string>
|
||||
<string name="notification_error_action">Fix</string>
|
||||
|
||||
<string name="notification_restore_error_channel_title">Auto Restore Flash Drive Error</string>
|
||||
<string name="notification_restore_error_title">Could not restore data for %1$s</string>
|
||||
<string name="notification_restore_error_text">Plug in your %1$s before installing the app to restore its data from backup.</string>
|
||||
<string name="notification_restore_error_action">Uninstall App</string>
|
||||
|
||||
<!-- Restore -->
|
||||
<string name="restore_title">Restore from Backup</string>
|
||||
<string name="restore_choose_restore_set">Choose a backup to restore</string>
|
||||
|
|
|
@ -3,13 +3,33 @@ package com.stevesoltys.seedvault.crypto
|
|||
import com.stevesoltys.seedvault.assertContains
|
||||
import com.stevesoltys.seedvault.getRandomByteArray
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.header.*
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import com.stevesoltys.seedvault.header.HeaderReader
|
||||
import com.stevesoltys.seedvault.header.HeaderWriter
|
||||
import com.stevesoltys.seedvault.header.IV_SIZE
|
||||
import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE
|
||||
import com.stevesoltys.seedvault.header.MAX_PACKAGE_LENGTH_SIZE
|
||||
import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
|
||||
import com.stevesoltys.seedvault.header.MAX_VERSION_HEADER_SIZE
|
||||
import com.stevesoltys.seedvault.header.SegmentHeader
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.header.VersionHeader
|
||||
import io.mockk.CapturingSlot
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Assertions.fail
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD
|
||||
import java.io.*
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.EOFException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import javax.crypto.Cipher
|
||||
import kotlin.random.Random
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ package com.stevesoltys.seedvault.header
|
|||
import com.stevesoltys.seedvault.Utf8
|
||||
import com.stevesoltys.seedvault.assertContains
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
|
||||
|
|
|
@ -2,7 +2,9 @@ package com.stevesoltys.seedvault.header
|
|||
|
||||
import com.stevesoltys.seedvault.getRandomByteArray
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
|
||||
|
|
|
@ -10,7 +10,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||
import com.stevesoltys.seedvault.Clock
|
||||
import com.stevesoltys.seedvault.getRandomByteArray
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.*
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
|
@ -21,7 +25,11 @@ import org.junit.Assert.fail
|
|||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koin.core.context.stopKoin
|
||||
import java.io.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import kotlin.random.Random
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
|
|
@ -8,7 +8,11 @@ import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
|||
import io.mockk.mockk
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Assertions.fail
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
|
||||
|
|
|
@ -2,7 +2,10 @@ package com.stevesoltys.seedvault.metadata
|
|||
|
||||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.*
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
|
|
@ -18,10 +18,31 @@ import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH
|
|||
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||
import com.stevesoltys.seedvault.transport.backup.*
|
||||
import com.stevesoltys.seedvault.transport.restore.*
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import com.stevesoltys.seedvault.transport.backup.ApkBackup
|
||||
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
||||
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
||||
import com.stevesoltys.seedvault.transport.backup.DEFAULT_QUOTA_FULL_BACKUP
|
||||
import com.stevesoltys.seedvault.transport.backup.FullBackup
|
||||
import com.stevesoltys.seedvault.transport.backup.FullBackupPlugin
|
||||
import com.stevesoltys.seedvault.transport.backup.InputFactory
|
||||
import com.stevesoltys.seedvault.transport.backup.KVBackup
|
||||
import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin
|
||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||
import com.stevesoltys.seedvault.transport.restore.FullRestore
|
||||
import com.stevesoltys.seedvault.transport.restore.FullRestorePlugin
|
||||
import com.stevesoltys.seedvault.transport.restore.KVRestore
|
||||
import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin
|
||||
import com.stevesoltys.seedvault.transport.restore.OutputFactory
|
||||
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
||||
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
|
||||
import io.mockk.CapturingSlot
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.fail
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
@ -53,7 +74,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(metadataManager, restorePlugin, kvRestore, fullRestore, metadataReader)
|
||||
private val restore = RestoreCoordinator(context, settingsManager, metadataManager, notificationManager, restorePlugin, kvRestore, fullRestore, metadataReader)
|
||||
|
||||
private val backupDataInput = mockk<BackupDataInput>()
|
||||
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||
|
|
|
@ -10,8 +10,16 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
|||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package com.stevesoltys.seedvault.transport.backup
|
||||
|
||||
import android.app.backup.BackupTransport.*
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
||||
import android.content.pm.PackageInfo
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
|
@ -9,9 +12,16 @@ import com.stevesoltys.seedvault.BackupNotificationManager
|
|||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.*
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||
import com.stevesoltys.seedvault.settings.Storage
|
||||
import io.mockk.*
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Test
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
package com.stevesoltys.seedvault.transport.backup
|
||||
|
||||
import android.app.backup.BackupTransport.*
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
package com.stevesoltys.seedvault.transport.backup
|
||||
|
||||
import android.app.backup.BackupDataInput
|
||||
import android.app.backup.BackupTransport.*
|
||||
import android.app.backup.BackupTransport.FLAG_INCREMENTAL
|
||||
import android.app.backup.BackupTransport.FLAG_NON_INCREMENTAL
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import com.stevesoltys.seedvault.Utf8
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE
|
||||
|
@ -10,7 +14,9 @@ import io.mockk.Runs
|
|||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
|
|
@ -2,14 +2,19 @@ package com.stevesoltys.seedvault.transport.restore
|
|||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ApplicationInfo.*
|
||||
import android.content.pm.ApplicationInfo.FLAG_INSTALLED
|
||||
import android.content.pm.ApplicationInfo.FLAG_SYSTEM
|
||||
import android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.*
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.FAILED
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.IN_PROGRESS
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.SUCCEEDED
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
@ -24,7 +29,6 @@ import org.junit.jupiter.api.io.TempDir
|
|||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.logging.Logger.getLogger
|
||||
import kotlin.random.Random
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package com.stevesoltys.seedvault.transport.restore
|
||||
|
||||
import android.app.backup.BackupTransport.*
|
||||
import android.app.backup.BackupTransport.NO_MORE_DATA
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||
import com.stevesoltys.seedvault.getRandomByteArray
|
||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
|
@ -9,7 +12,11 @@ import io.mockk.Runs
|
|||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.EOFException
|
||||
|
|
|
@ -8,7 +8,11 @@ import com.stevesoltys.seedvault.getRandomByteArray
|
|||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.header.VersionHeader
|
||||
import io.mockk.*
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verifyAll
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Test
|
||||
|
|
|
@ -1,20 +1,32 @@
|
|||
package com.stevesoltys.seedvault.transport.restore
|
||||
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import android.app.backup.RestoreDescription
|
||||
import android.app.backup.RestoreDescription.*
|
||||
import android.app.backup.RestoreDescription.NO_MORE_PACKAGES
|
||||
import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
|
||||
import android.app.backup.RestoreDescription.TYPE_KEY_VALUE
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
||||
import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata
|
||||
import com.stevesoltys.seedvault.metadata.MetadataReader
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.settings.Storage
|
||||
import com.stevesoltys.seedvault.transport.TransportTest
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Assertions.fail
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
@ -22,18 +34,27 @@ import kotlin.random.Random
|
|||
|
||||
internal class RestoreCoordinatorTest : TransportTest() {
|
||||
|
||||
private val notificationManager: BackupNotificationManager = mockk()
|
||||
private val plugin = mockk<RestorePlugin>()
|
||||
private val kv = mockk<KVRestore>()
|
||||
private val full = mockk<FullRestore>()
|
||||
private val metadataReader = mockk<MetadataReader>()
|
||||
|
||||
private val restore = RestoreCoordinator(metadataManager, plugin, kv, full, metadataReader)
|
||||
private val restore = RestoreCoordinator(context, settingsManager, metadataManager, notificationManager, plugin, kv, full, metadataReader)
|
||||
|
||||
private val token = Random.nextLong()
|
||||
private val inputStream = mockk<InputStream>()
|
||||
private val storage: Storage = mockk()
|
||||
private val documentFile: DocumentFile = mockk()
|
||||
private val packageInfo2 = PackageInfo().apply { packageName = "org.example2" }
|
||||
private val packageInfoArray = arrayOf(packageInfo)
|
||||
private val packageInfoArray2 = arrayOf(packageInfo, packageInfo2)
|
||||
private val pmPackageInfoArray = arrayOf(
|
||||
PackageInfo().apply { packageName = "@pm@" },
|
||||
packageInfo
|
||||
)
|
||||
private val packageName = packageInfo.packageName
|
||||
private val storageName = getRandomString()
|
||||
|
||||
@Test
|
||||
fun `getAvailableRestoreSets() builds set from plugin response`() {
|
||||
|
@ -74,6 +95,46 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startRestore() optimized auto-restore with removed storage shows notification`() {
|
||||
every { settingsManager.getStorage() } returns storage
|
||||
every { storage.isUsb } returns true
|
||||
every { storage.getDocumentFile(context) } returns documentFile
|
||||
every { documentFile.isDirectory } returns false
|
||||
every { metadataManager.getPackageMetadata(packageName) } returns PackageMetadata(42L)
|
||||
every { storage.name } returns storageName
|
||||
every { notificationManager.onRemovableStorageNotAvailableForRestore(packageName, storageName) } just Runs
|
||||
|
||||
assertEquals(TRANSPORT_ERROR, restore.startRestore(token, pmPackageInfoArray))
|
||||
|
||||
verify(exactly = 1) { notificationManager.onRemovableStorageNotAvailableForRestore(packageName, storageName) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startRestore() optimized auto-restore with available storage shows no notification`() {
|
||||
every { settingsManager.getStorage() } returns storage
|
||||
every { storage.isUsb } returns true
|
||||
every { storage.getDocumentFile(context) } returns documentFile
|
||||
every { documentFile.isDirectory } returns true
|
||||
|
||||
assertEquals(TRANSPORT_OK, restore.startRestore(token, pmPackageInfoArray))
|
||||
|
||||
verify(exactly = 0) { notificationManager.onRemovableStorageNotAvailableForRestore(packageName, storageName) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startRestore() optimized auto-restore with removed storage but no backup shows no notification`() {
|
||||
every { settingsManager.getStorage() } returns storage
|
||||
every { storage.isUsb } returns true
|
||||
every { storage.getDocumentFile(context) } returns documentFile
|
||||
every { documentFile.isDirectory } returns false
|
||||
every { metadataManager.getPackageMetadata(packageName) } returns null
|
||||
|
||||
assertEquals(TRANSPORT_ERROR, restore.startRestore(token, pmPackageInfoArray))
|
||||
|
||||
verify(exactly = 0) { notificationManager.onRemovableStorageNotAvailableForRestore(packageName, storageName) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nextRestorePackage() throws without startRestore()`() {
|
||||
assertThrows(IllegalStateException::class.javaObjectType) {
|
||||
|
|
|
@ -2,9 +2,9 @@ package com.stevesoltys.seedvault.transport.restore
|
|||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.stevesoltys.seedvault.getRandomByteArray
|
||||
import com.stevesoltys.seedvault.transport.TransportTest
|
||||
import com.stevesoltys.seedvault.header.HeaderReader
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.transport.TransportTest
|
||||
import io.mockk.mockk
|
||||
import java.io.InputStream
|
||||
import kotlin.random.Random
|
||||
|
|
Loading…
Add table
Reference in a new issue