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"
|
android:name="android.permission.INSTALL_PACKAGES"
|
||||||
tools:ignore="ProtectedPermissions" />
|
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
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
@ -87,5 +91,13 @@
|
||||||
android:resource="@xml/device_filter" />
|
android:resource="@xml/device_filter" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".restore.RestoreErrorBroadcastReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.stevesoltys.seedvault.action.UNINSTALL" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -63,5 +63,7 @@ class App : Application() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const val MAGIC_PACKAGE_MANAGER = PACKAGE_MANAGER_SENTINEL
|
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"
|
fun isDebugBuild() = Build.TYPE == "userdebug"
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package com.stevesoltys.seedvault
|
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.app.backup.IBackupManagerMonitor
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
|
@ -3,24 +3,37 @@ package com.stevesoltys.seedvault
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.NotificationManager.IMPORTANCE_DEFAULT
|
import android.app.NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
import android.app.NotificationManager.IMPORTANCE_HIGH
|
||||||
import android.app.NotificationManager.IMPORTANCE_LOW
|
import android.app.NotificationManager.IMPORTANCE_LOW
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
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
|
import com.stevesoltys.seedvault.settings.SettingsActivity
|
||||||
|
|
||||||
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
|
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
|
||||||
private const val CHANNEL_ID_ERROR = "NotificationError"
|
private const val CHANNEL_ID_ERROR = "NotificationError"
|
||||||
|
private const val CHANNEL_ID_RESTORE_ERROR = "NotificationRestoreError"
|
||||||
private const val NOTIFICATION_ID_OBSERVER = 1
|
private const val NOTIFICATION_ID_OBSERVER = 1
|
||||||
private const val NOTIFICATION_ID_ERROR = 2
|
private const val NOTIFICATION_ID_ERROR = 2
|
||||||
|
private const val NOTIFICATION_ID_RESTORE_ERROR = 3
|
||||||
|
|
||||||
class BackupNotificationManager(private val context: Context) {
|
class BackupNotificationManager(private val context: Context) {
|
||||||
|
|
||||||
private val nm = context.getSystemService(NotificationManager::class.java)!!.apply {
|
private val nm = context.getSystemService(NotificationManager::class.java)!!.apply {
|
||||||
createNotificationChannel(getObserverChannel())
|
createNotificationChannel(getObserverChannel())
|
||||||
createNotificationChannel(getErrorChannel())
|
createNotificationChannel(getErrorChannel())
|
||||||
|
createNotificationChannel(getRestoreErrorChannel())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getObserverChannel(): NotificationChannel {
|
private fun getObserverChannel(): NotificationChannel {
|
||||||
|
@ -35,6 +48,11 @@ class BackupNotificationManager(private val context: Context) {
|
||||||
return NotificationChannel(CHANNEL_ID_ERROR, title, IMPORTANCE_DEFAULT)
|
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 {
|
private val observerBuilder = Builder(context, CHANNEL_ID_OBSERVER).apply {
|
||||||
setSmallIcon(R.drawable.ic_cloud_upload)
|
setSmallIcon(R.drawable.ic_cloud_upload)
|
||||||
}
|
}
|
||||||
|
@ -43,10 +61,16 @@ class BackupNotificationManager(private val context: Context) {
|
||||||
setSmallIcon(R.drawable.ic_cloud_error)
|
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) {
|
fun onBackupUpdate(app: CharSequence, transferred: Int, expected: Int, userInitiated: Boolean) {
|
||||||
val notification = observerBuilder.apply {
|
val notification = observerBuilder.apply {
|
||||||
setContentTitle(context.getString(R.string.notification_title))
|
setContentTitle(context.getString(R.string.notification_title))
|
||||||
setContentText(app)
|
setContentText(app)
|
||||||
|
setOngoing(true)
|
||||||
|
setShowWhen(false)
|
||||||
setWhen(System.currentTimeMillis())
|
setWhen(System.currentTimeMillis())
|
||||||
setProgress(expected, transferred, false)
|
setProgress(expected, transferred, false)
|
||||||
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
|
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
|
||||||
|
@ -63,14 +87,33 @@ class BackupNotificationManager(private val context: Context) {
|
||||||
val notification = observerBuilder.apply {
|
val notification = observerBuilder.apply {
|
||||||
setContentTitle(title)
|
setContentTitle(title)
|
||||||
setContentText(app)
|
setContentText(app)
|
||||||
|
setOngoing(true)
|
||||||
|
setShowWhen(false)
|
||||||
setWhen(System.currentTimeMillis())
|
setWhen(System.currentTimeMillis())
|
||||||
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
|
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
|
||||||
}.build()
|
}.build()
|
||||||
nm.notify(NOTIFICATION_ID_OBSERVER, notification)
|
nm.notify(NOTIFICATION_ID_OBSERVER, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBackupFinished() {
|
fun onBackupFinished(success: Boolean, notBackedUp: Int?, userInitiated: Boolean) {
|
||||||
nm.cancel(NOTIFICATION_ID_OBSERVER)
|
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() {
|
fun onBackupError() {
|
||||||
|
@ -93,4 +136,33 @@ class BackupNotificationManager(private val context: Context) {
|
||||||
nm.cancel(NOTIFICATION_ID_ERROR)
|
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
|
||||||
import android.util.Log.INFO
|
import android.util.Log.INFO
|
||||||
import android.util.Log.isLoggable
|
import android.util.Log.isLoggable
|
||||||
|
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
import org.koin.core.inject
|
import org.koin.core.inject
|
||||||
|
|
||||||
|
@ -14,9 +15,18 @@ private val TAG = NotificationBackupObserver::class.java.simpleName
|
||||||
|
|
||||||
class NotificationBackupObserver(
|
class NotificationBackupObserver(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
private val expectedPackages: Int,
|
||||||
private val userInitiated: Boolean) : IBackupObserver.Stub(), KoinComponent {
|
private val userInitiated: Boolean) : IBackupObserver.Stub(), KoinComponent {
|
||||||
|
|
||||||
private val nm: BackupNotificationManager by inject()
|
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.
|
* 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.
|
* @param backupProgress Current progress of backup for the package.
|
||||||
*/
|
*/
|
||||||
override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) {
|
override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) {
|
||||||
val transferred = backupProgress.bytesTransferred.toInt()
|
showProgressNotification(currentBackupPackage)
|
||||||
val expected = backupProgress.bytesExpected.toInt()
|
|
||||||
val app = getAppName(currentBackupPackage)
|
|
||||||
nm.onBackupUpdate(app, transferred, expected, userInitiated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,7 +53,8 @@ class NotificationBackupObserver(
|
||||||
if (isLoggable(TAG, INFO)) {
|
if (isLoggable(TAG, INFO)) {
|
||||||
Log.i(TAG, "Completed. Target: $target, status: $status")
|
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) {
|
override fun backupFinished(status: Int) {
|
||||||
if (isLoggable(TAG, INFO)) {
|
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)
|
private fun getAppName(packageId: String): CharSequence = getAppName(context, packageId)
|
||||||
|
|
|
@ -6,7 +6,9 @@ import android.content.Intent
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
import android.hardware.usb.UsbDevice
|
import android.hardware.usb.UsbDevice
|
||||||
import android.hardware.usb.UsbInterface
|
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.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
|
@ -23,6 +25,7 @@ private val TAG = UsbIntentReceiver::class.java.simpleName
|
||||||
|
|
||||||
class UsbIntentReceiver : UsbMonitor() {
|
class UsbIntentReceiver : UsbMonitor() {
|
||||||
|
|
||||||
|
// using KoinComponent would crash robolectric tests :(
|
||||||
private val settingsManager: SettingsManager by lazy { get().koin.get<SettingsManager>() }
|
private val settingsManager: SettingsManager by lazy { get().koin.get<SettingsManager>() }
|
||||||
private val metadataManager: MetadataManager by lazy { get().koin.get<MetadataManager>() }
|
private val metadataManager: MetadataManager by lazy { get().koin.get<MetadataManager>() }
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
package com.stevesoltys.seedvault.crypto
|
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.EOFException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package com.stevesoltys.seedvault.crypto
|
package com.stevesoltys.seedvault.crypto
|
||||||
|
|
||||||
import android.os.Build.VERSION.SDK_INT
|
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 android.security.keystore.KeyProtection
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.KeyStore.SecretKeyEntry
|
import java.security.KeyStore.SecretKeyEntry
|
||||||
|
|
|
@ -178,6 +178,13 @@ class MetadataManager(
|
||||||
return metadata.packageMetadataMap[packageName]?.copy()
|
return metadata.packageMetadataMap[packageName]?.copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getPackagesNumNotBackedUp(): Int {
|
||||||
|
return metadata.packageMetadataMap.filter { (_, packageMetadata) ->
|
||||||
|
!packageMetadata.system && packageMetadata.state != APK_AND_DATA
|
||||||
|
}.count()
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
private fun getMetadataFromCache(): BackupMetadata? {
|
private fun getMetadataFromCache(): BackupMetadata? {
|
||||||
|
|
|
@ -4,7 +4,11 @@ import com.stevesoltys.seedvault.Utf8
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.crypto.Crypto
|
||||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
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.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
|
@ -5,8 +5,14 @@ import android.content.Context
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.DocumentsContract.*
|
import android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID
|
||||||
import android.provider.DocumentsContract.Document.*
|
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 android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
|
|
|
@ -14,7 +14,10 @@ import androidx.recyclerview.widget.SortedList
|
||||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreResult
|
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>() {
|
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.content.pm.PackageManager.NameNotFoundException
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
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.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
|
@ -13,7 +15,13 @@ import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.R
|
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 com.stevesoltys.seedvault.restore.RestoreProgressAdapter.PackageViewHolder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package com.stevesoltys.seedvault.restore
|
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.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
|
|
@ -20,8 +20,18 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.getAppName
|
import com.stevesoltys.seedvault.getAppName
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.*
|
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.*
|
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_APPS
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
|
|
@ -180,6 +180,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
activity?.contentResolver?.let {
|
activity?.contentResolver?.let {
|
||||||
autoRestore.isChecked = Settings.Secure.getInt(it, BACKUP_AUTO_RESTORE, 1) == 1
|
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) {
|
private fun setBackupLocationSummary(lastBackupInMillis: Long) {
|
||||||
|
|
|
@ -15,7 +15,6 @@ import androidx.annotation.WorkerThread
|
||||||
import com.stevesoltys.seedvault.BackupMonitor
|
import com.stevesoltys.seedvault.BackupMonitor
|
||||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||||
import com.stevesoltys.seedvault.NotificationBackupObserver
|
import com.stevesoltys.seedvault.NotificationBackupObserver
|
||||||
import com.stevesoltys.seedvault.R
|
|
||||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||||
import org.koin.core.context.GlobalContext.get
|
import org.koin.core.context.GlobalContext.get
|
||||||
|
|
||||||
|
@ -52,19 +51,16 @@ class ConfigurableBackupTransportService : Service() {
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun requestBackup(context: Context) {
|
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 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 packages = packageService.eligiblePackages
|
||||||
|
|
||||||
|
val observer = NotificationBackupObserver(context, packages.size, true)
|
||||||
val result = try {
|
val result = try {
|
||||||
val backupManager: IBackupManager = get().koin.get()
|
val backupManager: IBackupManager = get().koin.get()
|
||||||
backupManager.requestBackup(packages, observer, BackupMonitor(), flags)
|
backupManager.requestBackup(packages, observer, BackupMonitor(), FLAG_USER_INITIATED)
|
||||||
} catch (e: RemoteException) {
|
} catch (e: RemoteException) {
|
||||||
Log.e(TAG, "Error during backup: ", e)
|
Log.e(TAG, "Error during backup: ", e)
|
||||||
|
val nm: BackupNotificationManager = get().koin.get()
|
||||||
nm.onBackupError()
|
nm.onBackupError()
|
||||||
}
|
}
|
||||||
if (result == BackupManager.SUCCESS) {
|
if (result == BackupManager.SUCCESS) {
|
||||||
|
|
|
@ -8,7 +8,11 @@ import android.util.Log
|
||||||
import android.util.PackageUtils.computeSha256DigestBytes
|
import android.util.PackageUtils.computeSha256DigestBytes
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.encodeBase64
|
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 com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
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.Context
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
@ -10,7 +13,10 @@ import com.stevesoltys.seedvault.Clock
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState
|
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.metadata.isSystemApp
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
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.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
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.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
|
@ -2,10 +2,18 @@ package com.stevesoltys.seedvault.transport.restore
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
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.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.*
|
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.PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package com.stevesoltys.seedvault.transport.restore
|
package com.stevesoltys.seedvault.transport.restore
|
||||||
|
|
||||||
import android.content.Context
|
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.graphics.drawable.Drawable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.encodeBase64
|
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.PackageMetadataMap
|
||||||
import com.stevesoltys.seedvault.metadata.isSystemApp
|
import com.stevesoltys.seedvault.metadata.isSystemApp
|
||||||
import com.stevesoltys.seedvault.transport.backup.getSignatures
|
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.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package com.stevesoltys.seedvault.transport.restore
|
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.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
|
@ -6,6 +6,9 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
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.crypto.Crypto
|
||||||
import com.stevesoltys.seedvault.decodeBase64
|
import com.stevesoltys.seedvault.decodeBase64
|
||||||
import com.stevesoltys.seedvault.header.HeaderReader
|
import com.stevesoltys.seedvault.header.HeaderReader
|
||||||
|
@ -17,7 +20,11 @@ import javax.crypto.AEADBadTagException
|
||||||
|
|
||||||
private class KVRestoreState(
|
private class KVRestoreState(
|
||||||
internal val token: Long,
|
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
|
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.
|
* 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.
|
* 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) {
|
fun initializeState(token: Long, packageInfo: PackageInfo, pmPackageInfo: PackageInfo? = null) {
|
||||||
state = KVRestoreState(token, packageInfo)
|
state = KVRestoreState(token, packageInfo, pmPackageInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -111,8 +120,15 @@ internal class KVRestore(
|
||||||
// Decode the key filenames into keys then sort lexically by key
|
// Decode the key filenames into keys then sort lexically by key
|
||||||
val contents = ArrayList<DecodedKey>()
|
val contents = ArrayList<DecodedKey>()
|
||||||
for (recordKey in records) contents.add(DecodedKey(recordKey))
|
for (recordKey in records) contents.add(DecodedKey(recordKey))
|
||||||
contents.sort()
|
// remove keys that are not needed for single package @pm@ restore
|
||||||
return contents
|
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.BackupTransport.TRANSPORT_OK
|
||||||
import android.app.backup.IBackupManager
|
import android.app.backup.IBackupManager
|
||||||
import android.app.backup.RestoreDescription
|
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.app.backup.RestoreSet
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.collection.LongSparseArray
|
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.header.UnsupportedVersionException
|
||||||
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.DecryptionFailedException
|
import com.stevesoltys.seedvault.metadata.DecryptionFailedException
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataReader
|
import com.stevesoltys.seedvault.metadata.MetadataReader
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import libcore.io.IoUtils.closeQuietly
|
import libcore.io.IoUtils.closeQuietly
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
private class RestoreCoordinatorState(
|
private class RestoreCoordinatorState(
|
||||||
internal val token: Long,
|
internal val token: Long,
|
||||||
internal val packages: Iterator<PackageInfo>,
|
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
|
private val TAG = RestoreCoordinator::class.java.simpleName
|
||||||
|
|
||||||
internal class RestoreCoordinator(
|
internal class RestoreCoordinator(
|
||||||
|
private val context: Context,
|
||||||
|
private val settingsManager: SettingsManager,
|
||||||
private val metadataManager: MetadataManager,
|
private val metadataManager: MetadataManager,
|
||||||
|
private val notificationManager: BackupNotificationManager,
|
||||||
private val plugin: RestorePlugin,
|
private val plugin: RestorePlugin,
|
||||||
private val kv: KVRestore,
|
private val kv: KVRestore,
|
||||||
private val full: FullRestore,
|
private val full: FullRestore,
|
||||||
|
@ -104,7 +119,26 @@ internal class RestoreCoordinator(
|
||||||
fun startRestore(token: Long, packages: Array<out PackageInfo>): Int {
|
fun startRestore(token: Long, packages: Array<out PackageInfo>): Int {
|
||||||
check(state == null) { "Started new restore with existing state" }
|
check(state == null) { "Started new restore with existing state" }
|
||||||
Log.i(TAG, "Start restore with ${packages.map { info -> info.packageName }}")
|
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()
|
failedPackages.clear()
|
||||||
return TRANSPORT_OK
|
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
|
// check key/value data first and if available, don't even check for full data
|
||||||
kv.hasDataForPackage(state.token, packageInfo) -> {
|
kv.hasDataForPackage(state.token, packageInfo) -> {
|
||||||
Log.i(TAG, "Found K/V data for $packageName.")
|
Log.i(TAG, "Found K/V data for $packageName.")
|
||||||
kv.initializeState(state.token, packageInfo)
|
kv.initializeState(state.token, packageInfo, state.pmPackageInfo)
|
||||||
state.currentPackage = packageName
|
state.currentPackage = packageName
|
||||||
TYPE_KEY_VALUE
|
TYPE_KEY_VALUE
|
||||||
}
|
}
|
||||||
|
@ -174,7 +208,7 @@ internal class RestoreCoordinator(
|
||||||
/**
|
/**
|
||||||
* Get the data for the application returned by [nextRestorePackage],
|
* Get the data for the application returned by [nextRestorePackage],
|
||||||
* if that method reported [TYPE_KEY_VALUE] as its delivery type.
|
* 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.
|
* @param data An open, writable file into which the key/value backup data should be stored.
|
||||||
* @return the same error codes as [startRestore].
|
* @return the same error codes as [startRestore].
|
||||||
|
@ -232,4 +266,10 @@ internal class RestoreCoordinator(
|
||||||
|
|
||||||
fun isFailedPackage(packageName: String) = packageName in failedPackages
|
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()) }
|
factory { ApkRestore(androidContext(), get()) }
|
||||||
single { KVRestore(get<RestorePlugin>().kvRestorePlugin, get(), get(), get()) }
|
single { KVRestore(get<RestorePlugin>().kvRestorePlugin, get(), get(), get()) }
|
||||||
single { FullRestore(get<RestorePlugin>().fullRestorePlugin, 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.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
import com.stevesoltys.seedvault.ui.LiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
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.InvalidChecksumException
|
||||||
import io.github.novacrypto.bip39.Validation.InvalidWordCountException
|
import io.github.novacrypto.bip39.Validation.InvalidWordCountException
|
||||||
import io.github.novacrypto.bip39.Validation.UnexpectedWhiteSpaceException
|
import io.github.novacrypto.bip39.Validation.UnexpectedWhiteSpaceException
|
||||||
import io.github.novacrypto.bip39.Validation.WordNotFoundException
|
import io.github.novacrypto.bip39.Validation.WordNotFoundException
|
||||||
|
import io.github.novacrypto.bip39.Words
|
||||||
import io.github.novacrypto.bip39.wordlists.English
|
import io.github.novacrypto.bip39.wordlists.English
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
|
@ -14,7 +14,16 @@ import android.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.provider.DocumentsContract.PROVIDER_INTERFACE
|
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 android.util.Log
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import java.lang.Long.parseLong
|
import java.lang.Long.parseLong
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package com.stevesoltys.seedvault.ui.storage
|
package com.stevesoltys.seedvault.ui.storage
|
||||||
|
|
||||||
import android.content.Intent
|
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.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
<string name="settings_backup_location_summary">%1$s · Last Backup %2$s</string>
|
<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_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_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_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_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>
|
<string name="settings_backup_apk_dialog_title">Really disable app backup?</string>
|
||||||
|
@ -69,16 +70,24 @@
|
||||||
<!-- Notification -->
|
<!-- Notification -->
|
||||||
<string name="notification_channel_title">Backup Notification</string>
|
<string name="notification_channel_title">Backup Notification</string>
|
||||||
<string name="notification_title">Backup running</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_complete">Backup complete</string>
|
||||||
<string name="notification_backup_result_rejected">Not backed up</string>
|
<string name="notification_backup_result_rejected">Not backed up</string>
|
||||||
<string name="notification_backup_result_error">Backup failed</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_channel_title">Error Notification</string>
|
||||||
<string name="notification_error_title">Backup Error</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_text">A device backup failed to run.</string>
|
||||||
<string name="notification_error_action">Fix</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 -->
|
<!-- Restore -->
|
||||||
<string name="restore_title">Restore from Backup</string>
|
<string name="restore_title">Restore from Backup</string>
|
||||||
<string name="restore_choose_restore_set">Choose a backup to restore</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.assertContains
|
||||||
import com.stevesoltys.seedvault.getRandomByteArray
|
import com.stevesoltys.seedvault.getRandomByteArray
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
import com.stevesoltys.seedvault.getRandomString
|
||||||
import com.stevesoltys.seedvault.header.*
|
import com.stevesoltys.seedvault.header.HeaderReader
|
||||||
import io.mockk.*
|
import com.stevesoltys.seedvault.header.HeaderWriter
|
||||||
import org.junit.jupiter.api.Assertions.*
|
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.Test
|
||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD
|
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 javax.crypto.Cipher
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,9 @@ package com.stevesoltys.seedvault.header
|
||||||
import com.stevesoltys.seedvault.Utf8
|
import com.stevesoltys.seedvault.Utf8
|
||||||
import com.stevesoltys.seedvault.assertContains
|
import com.stevesoltys.seedvault.assertContains
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
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.Test
|
||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
|
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.getRandomByteArray
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
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.Test
|
||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
|
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.Clock
|
||||||
import com.stevesoltys.seedvault.getRandomByteArray
|
import com.stevesoltys.seedvault.getRandomByteArray
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
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.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
|
@ -21,7 +25,11 @@ import org.junit.Assert.fail
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.koin.core.context.stopKoin
|
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
|
import kotlin.random.Random
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
|
|
@ -8,7 +8,11 @@ import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
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.Test
|
||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
|
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.crypto.Crypto
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
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 io.mockk.mockk
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Test
|
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.MetadataReaderImpl
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||||
import com.stevesoltys.seedvault.transport.backup.*
|
import com.stevesoltys.seedvault.transport.backup.ApkBackup
|
||||||
import com.stevesoltys.seedvault.transport.restore.*
|
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
||||||
import io.mockk.*
|
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
||||||
import org.junit.jupiter.api.Assertions.*
|
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 org.junit.jupiter.api.Test
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
@ -53,7 +74,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
||||||
private val fullRestorePlugin = mockk<FullRestorePlugin>()
|
private val fullRestorePlugin = mockk<FullRestorePlugin>()
|
||||||
private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
||||||
private val restore = RestoreCoordinator(metadataManager, restorePlugin, kvRestore, fullRestore, metadataReader)
|
private val restore = RestoreCoordinator(context, settingsManager, metadataManager, notificationManager, restorePlugin, kvRestore, fullRestore, metadataReader)
|
||||||
|
|
||||||
private val backupDataInput = mockk<BackupDataInput>()
|
private val backupDataInput = mockk<BackupDataInput>()
|
||||||
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||||
|
|
|
@ -10,8 +10,16 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
import com.stevesoltys.seedvault.getRandomString
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||||
import io.mockk.*
|
import io.mockk.Runs
|
||||||
import org.junit.jupiter.api.Assertions.*
|
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.Test
|
||||||
import org.junit.jupiter.api.io.TempDir
|
import org.junit.jupiter.api.io.TempDir
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
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.content.pm.PackageInfo
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
@ -9,9 +12,16 @@ import com.stevesoltys.seedvault.BackupNotificationManager
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
import com.stevesoltys.seedvault.getRandomString
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
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 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.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertThrows
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
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.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
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 org.junit.jupiter.api.Test
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
package com.stevesoltys.seedvault.transport.backup
|
||||||
|
|
||||||
import android.app.backup.BackupDataInput
|
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.Utf8
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
import com.stevesoltys.seedvault.getRandomString
|
||||||
import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE
|
import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE
|
||||||
|
@ -10,7 +14,9 @@ import io.mockk.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
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 org.junit.jupiter.api.Test
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
|
@ -2,14 +2,19 @@ package com.stevesoltys.seedvault.transport.restore
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.ApplicationInfo
|
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.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
import com.stevesoltys.seedvault.getRandomString
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
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.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
@ -24,7 +29,6 @@ import org.junit.jupiter.api.io.TempDir
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.logging.Logger.getLogger
|
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package com.stevesoltys.seedvault.transport.restore
|
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.getRandomByteArray
|
||||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.header.VERSION
|
||||||
|
@ -9,7 +12,11 @@ import io.mockk.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
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 org.junit.jupiter.api.Test
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
|
|
|
@ -8,7 +8,11 @@ import com.stevesoltys.seedvault.getRandomByteArray
|
||||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.header.VERSION
|
||||||
import com.stevesoltys.seedvault.header.VersionHeader
|
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.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertThrows
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
|
@ -1,20 +1,32 @@
|
||||||
package com.stevesoltys.seedvault.transport.restore
|
package com.stevesoltys.seedvault.transport.restore
|
||||||
|
|
||||||
|
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||||
import android.app.backup.RestoreDescription
|
import android.app.backup.RestoreDescription
|
||||||
import android.app.backup.RestoreDescription.*
|
import android.app.backup.RestoreDescription.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.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
import com.stevesoltys.seedvault.getRandomString
|
||||||
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata
|
import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataReader
|
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 com.stevesoltys.seedvault.transport.TransportTest
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
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 org.junit.jupiter.api.Test
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
@ -22,18 +34,27 @@ import kotlin.random.Random
|
||||||
|
|
||||||
internal class RestoreCoordinatorTest : TransportTest() {
|
internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
|
|
||||||
|
private val notificationManager: BackupNotificationManager = mockk()
|
||||||
private val plugin = mockk<RestorePlugin>()
|
private val plugin = mockk<RestorePlugin>()
|
||||||
private val kv = mockk<KVRestore>()
|
private val kv = mockk<KVRestore>()
|
||||||
private val full = mockk<FullRestore>()
|
private val full = mockk<FullRestore>()
|
||||||
private val metadataReader = mockk<MetadataReader>()
|
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 token = Random.nextLong()
|
||||||
private val inputStream = mockk<InputStream>()
|
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 packageInfo2 = PackageInfo().apply { packageName = "org.example2" }
|
||||||
private val packageInfoArray = arrayOf(packageInfo)
|
private val packageInfoArray = arrayOf(packageInfo)
|
||||||
private val packageInfoArray2 = arrayOf(packageInfo, packageInfo2)
|
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
|
@Test
|
||||||
fun `getAvailableRestoreSets() builds set from plugin response`() {
|
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
|
@Test
|
||||||
fun `nextRestorePackage() throws without startRestore()`() {
|
fun `nextRestorePackage() throws without startRestore()`() {
|
||||||
assertThrows(IllegalStateException::class.javaObjectType) {
|
assertThrows(IllegalStateException::class.javaObjectType) {
|
||||||
|
|
|
@ -2,9 +2,9 @@ package com.stevesoltys.seedvault.transport.restore
|
||||||
|
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import com.stevesoltys.seedvault.getRandomByteArray
|
import com.stevesoltys.seedvault.getRandomByteArray
|
||||||
import com.stevesoltys.seedvault.transport.TransportTest
|
|
||||||
import com.stevesoltys.seedvault.header.HeaderReader
|
import com.stevesoltys.seedvault.header.HeaderReader
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.header.VERSION
|
||||||
|
import com.stevesoltys.seedvault.transport.TransportTest
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
Loading…
Add table
Reference in a new issue