1
0
Fork 0

Merge pull request from grote/51-removable-storage-ux

Improve UX for auto-restore when using removable storage
This commit is contained in:
Steve Soltys 2020-01-17 18:39:50 -05:00 committed by GitHub
commit 6afdb7f4f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 568 additions and 91 deletions

View file

@ -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>

View file

@ -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"

View file

@ -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

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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>() }

View file

@ -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

View file

@ -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

View file

@ -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? {

View file

@ -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

View file

@ -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

View file

@ -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>() {

View file

@ -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)
}
}

View file

@ -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.*

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}
/**

View file

@ -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
}
}

View file

@ -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()) }
}

View file

@ -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.*

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.*

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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