Merge pull request #71 from grote/69-app-backup-status
Show list of apps and their backup status
This commit is contained in:
commit
3ae600ea8e
22 changed files with 474 additions and 140 deletions
|
@ -29,6 +29,9 @@ android {
|
||||||
targetCompatibility 1.8
|
targetCompatibility 1.8
|
||||||
sourceCompatibility 1.8
|
sourceCompatibility 1.8
|
||||||
}
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
|
}
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.all {
|
unitTests.all {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
|
|
@ -7,7 +7,6 @@ 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.PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
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 android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
|
@ -19,6 +18,7 @@ import androidx.core.app.NotificationCompat.PRIORITY_LOW
|
||||||
import com.stevesoltys.seedvault.restore.ACTION_RESTORE_ERROR_UNINSTALL
|
import com.stevesoltys.seedvault.restore.ACTION_RESTORE_ERROR_UNINSTALL
|
||||||
import com.stevesoltys.seedvault.restore.EXTRA_PACKAGE_NAME
|
import com.stevesoltys.seedvault.restore.EXTRA_PACKAGE_NAME
|
||||||
import com.stevesoltys.seedvault.restore.REQUEST_CODE_UNINSTALL
|
import com.stevesoltys.seedvault.restore.REQUEST_CODE_UNINSTALL
|
||||||
|
import com.stevesoltys.seedvault.settings.ACTION_APP_STATUS_LIST
|
||||||
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"
|
||||||
|
@ -78,23 +78,6 @@ class BackupNotificationManager(private val context: Context) {
|
||||||
nm.notify(NOTIFICATION_ID_OBSERVER, notification)
|
nm.notify(NOTIFICATION_ID_OBSERVER, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBackupResult(app: CharSequence, status: Int, userInitiated: Boolean) {
|
|
||||||
val title = context.getString(when (status) {
|
|
||||||
0 -> R.string.notification_backup_result_complete
|
|
||||||
TRANSPORT_PACKAGE_REJECTED -> R.string.notification_backup_result_rejected
|
|
||||||
else -> R.string.notification_backup_result_error
|
|
||||||
})
|
|
||||||
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(success: Boolean, notBackedUp: Int?, userInitiated: Boolean) {
|
fun onBackupFinished(success: Boolean, notBackedUp: Int?, userInitiated: Boolean) {
|
||||||
if (!userInitiated) {
|
if (!userInitiated) {
|
||||||
nm.cancel(NOTIFICATION_ID_OBSERVER)
|
nm.cancel(NOTIFICATION_ID_OBSERVER)
|
||||||
|
@ -104,11 +87,19 @@ class BackupNotificationManager(private val context: Context) {
|
||||||
val contentText = if (notBackedUp == null) null else {
|
val contentText = if (notBackedUp == null) null else {
|
||||||
context.getString(R.string.notification_success_num_not_backed_up, notBackedUp)
|
context.getString(R.string.notification_success_num_not_backed_up, notBackedUp)
|
||||||
}
|
}
|
||||||
|
val iconRes = if (success) R.drawable.ic_cloud_done else R.drawable.ic_cloud_error
|
||||||
|
val intent = Intent(context, SettingsActivity::class.java).apply {
|
||||||
|
action = ACTION_APP_STATUS_LIST
|
||||||
|
}
|
||||||
|
val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
val notification = observerBuilder.apply {
|
val notification = observerBuilder.apply {
|
||||||
setContentTitle(context.getString(titleRes))
|
setContentTitle(context.getString(titleRes))
|
||||||
setContentText(contentText)
|
setContentText(contentText)
|
||||||
setOngoing(false)
|
setOngoing(false)
|
||||||
setShowWhen(true)
|
setShowWhen(true)
|
||||||
|
setAutoCancel(true)
|
||||||
|
setSmallIcon(iconRes)
|
||||||
|
setContentIntent(pendingIntent)
|
||||||
setWhen(System.currentTimeMillis())
|
setWhen(System.currentTimeMillis())
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
priority = PRIORITY_LOW
|
priority = PRIORITY_LOW
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.stevesoltys.seedvault.crypto
|
package com.stevesoltys.seedvault.crypto
|
||||||
|
|
||||||
import android.os.Build.VERSION.SDK_INT
|
|
||||||
import android.security.keystore.KeyProperties.BLOCK_MODE_GCM
|
import android.security.keystore.KeyProperties.BLOCK_MODE_GCM
|
||||||
import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE
|
import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE
|
||||||
import android.security.keystore.KeyProperties.PURPOSE_DECRYPT
|
import android.security.keystore.KeyProperties.PURPOSE_DECRYPT
|
||||||
|
@ -48,7 +47,7 @@ internal class KeyManagerImpl : KeyManager {
|
||||||
|
|
||||||
override fun storeBackupKey(seed: ByteArray) {
|
override fun storeBackupKey(seed: ByteArray) {
|
||||||
if (seed.size < KEY_SIZE_BYTES) throw IllegalArgumentException()
|
if (seed.size < KEY_SIZE_BYTES) throw IllegalArgumentException()
|
||||||
// TODO check if using first 256 of 512 bytes produced by PBKDF2WithHmacSHA512 is safe!
|
// TODO check if using first 256 of 512 bits produced by PBKDF2WithHmacSHA512 is safe!
|
||||||
val secretKeySpec = SecretKeySpec(seed, 0, KEY_SIZE_BYTES, "AES")
|
val secretKeySpec = SecretKeySpec(seed, 0, KEY_SIZE_BYTES, "AES")
|
||||||
val ksEntry = SecretKeyEntry(secretKeySpec)
|
val ksEntry = SecretKeyEntry(secretKeySpec)
|
||||||
keyStore.setEntry(KEY_ALIAS, ksEntry, getKeyProtection())
|
keyStore.setEntry(KEY_ALIAS, ksEntry, getKeyProtection())
|
||||||
|
@ -68,7 +67,7 @@ internal class KeyManagerImpl : KeyManager {
|
||||||
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
|
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
|
||||||
.setRandomizedEncryptionRequired(true)
|
.setRandomizedEncryptionRequired(true)
|
||||||
// unlocking is required only for decryption, so when restoring from backup
|
// unlocking is required only for decryption, so when restoring from backup
|
||||||
if (SDK_INT >= 28) builder.setUnlockedDeviceRequired(true)
|
builder.setUnlockedDeviceRequired(true)
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,9 @@ import android.content.pm.PackageInfo
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.distinctUntilChanged
|
||||||
import com.stevesoltys.seedvault.Clock
|
import com.stevesoltys.seedvault.Clock
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||||
|
@ -37,6 +40,7 @@ class MetadataManager(
|
||||||
// If this happens, it is hard to recover from this. Let's hope it never does.
|
// If this happens, it is hard to recover from this. Let's hope it never does.
|
||||||
throw AssertionError("Error reading metadata from cache", e)
|
throw AssertionError("Error reading metadata from cache", e)
|
||||||
}
|
}
|
||||||
|
mLastBackupTime.postValue(field.time)
|
||||||
}
|
}
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
|
@ -155,6 +159,7 @@ class MetadataManager(
|
||||||
metadata = oldMetadata
|
metadata = oldMetadata
|
||||||
throw IOException(e)
|
throw IOException(e)
|
||||||
}
|
}
|
||||||
|
mLastBackupTime.postValue(metadata.time)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -171,7 +176,10 @@ class MetadataManager(
|
||||||
* Note that this might be a blocking I/O call.
|
* Note that this might be a blocking I/O call.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun getLastBackupTime(): Long = metadata.time
|
fun getLastBackupTime(): Long = mLastBackupTime.value ?: metadata.time
|
||||||
|
|
||||||
|
private val mLastBackupTime = MutableLiveData<Long>()
|
||||||
|
internal val lastBackupTime: LiveData<Long> = mLastBackupTime.distinctUntilChanged()
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun getPackageMetadata(packageName: String): PackageMetadata? {
|
fun getPackageMetadata(packageName: String): PackageMetadata? {
|
||||||
|
|
|
@ -5,11 +5,7 @@ import android.view.View
|
||||||
import android.view.View.INVISIBLE
|
import android.view.View.INVISIBLE
|
||||||
import android.view.View.VISIBLE
|
import android.view.View.VISIBLE
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
|
||||||
import androidx.recyclerview.widget.SortedList
|
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
|
||||||
|
@ -18,8 +14,9 @@ import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.FAILED
|
||||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.IN_PROGRESS
|
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.IN_PROGRESS
|
||||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
|
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
|
||||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.SUCCEEDED
|
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.SUCCEEDED
|
||||||
|
import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||||
|
|
||||||
internal class InstallProgressAdapter : Adapter<AppViewHolder>() {
|
internal class InstallProgressAdapter : Adapter<AppInstallViewHolder>() {
|
||||||
|
|
||||||
private val items = SortedList<ApkRestoreResult>(ApkRestoreResult::class.java, object : SortedListAdapterCallback<ApkRestoreResult>(this) {
|
private val items = SortedList<ApkRestoreResult>(ApkRestoreResult::class.java, object : SortedListAdapterCallback<ApkRestoreResult>(this) {
|
||||||
override fun areItemsTheSame(item1: ApkRestoreResult, item2: ApkRestoreResult) = item1.packageName == item2.packageName
|
override fun areItemsTheSame(item1: ApkRestoreResult, item2: ApkRestoreResult) = item1.packageName == item2.packageName
|
||||||
|
@ -27,14 +24,14 @@ internal class InstallProgressAdapter : Adapter<AppViewHolder>() {
|
||||||
override fun compare(item1: ApkRestoreResult, item2: ApkRestoreResult) = item1.compareTo(item2)
|
override fun compare(item1: ApkRestoreResult, item2: ApkRestoreResult) = item1.compareTo(item2)
|
||||||
})
|
})
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppInstallViewHolder {
|
||||||
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false)
|
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false)
|
||||||
return AppViewHolder(v)
|
return AppInstallViewHolder(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = items.size()
|
override fun getItemCount() = items.size()
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: AppInstallViewHolder, position: Int) {
|
||||||
holder.bind(items[position])
|
holder.bind(items[position])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,12 +40,7 @@ internal class InstallProgressAdapter : Adapter<AppViewHolder>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class AppViewHolder(v: View) : ViewHolder(v) {
|
internal class AppInstallViewHolder(v: View) : AppViewHolder(v) {
|
||||||
|
|
||||||
private val appIcon: ImageView = v.findViewById(R.id.appIcon)
|
|
||||||
private val appName: TextView = v.findViewById(R.id.appName)
|
|
||||||
private val appStatus: ImageView = v.findViewById(R.id.appStatus)
|
|
||||||
private val progressBar: ProgressBar = v.findViewById(R.id.progressBar)
|
|
||||||
|
|
||||||
fun bind(item: ApkRestoreResult) {
|
fun bind(item: ApkRestoreResult) {
|
||||||
appIcon.setImageDrawable(item.icon)
|
appIcon.setImageDrawable(item.icon)
|
||||||
|
|
|
@ -3,26 +3,13 @@ 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.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.ProgressBar
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
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.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 com.stevesoltys.seedvault.ui.AppViewHolder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
||||||
|
@ -63,16 +50,7 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class PackageViewHolder(v: View) : ViewHolder(v) {
|
inner class PackageViewHolder(v: View) : AppViewHolder(v) {
|
||||||
|
|
||||||
private val context = v.context
|
|
||||||
private val pm = context.packageManager
|
|
||||||
private val appIcon: ImageView = v.findViewById(R.id.appIcon)
|
|
||||||
private val appName: TextView = v.findViewById(R.id.appName)
|
|
||||||
private val appInfo: TextView = v.findViewById(R.id.appInfo)
|
|
||||||
private val appStatus: ImageView = v.findViewById(R.id.appStatus)
|
|
||||||
private val progressBar: ProgressBar = v.findViewById(R.id.progressBar)
|
|
||||||
|
|
||||||
fun bind(item: AppRestoreResult) {
|
fun bind(item: AppRestoreResult) {
|
||||||
appName.text = item.name
|
appName.text = item.name
|
||||||
if (item.packageName == MAGIC_PACKAGE_MANAGER) {
|
if (item.packageName == MAGIC_PACKAGE_MANAGER) {
|
||||||
|
@ -84,38 +62,8 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
||||||
appIcon.setImageResource(R.drawable.ic_launcher_default)
|
appIcon.setImageResource(R.drawable.ic_launcher_default)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (item.status == IN_PROGRESS) {
|
setStatus(item.status)
|
||||||
appInfo.visibility = GONE
|
|
||||||
appStatus.visibility = INVISIBLE
|
|
||||||
progressBar.visibility = VISIBLE
|
|
||||||
} else {
|
|
||||||
appStatus.visibility = VISIBLE
|
|
||||||
progressBar.visibility = INVISIBLE
|
|
||||||
appInfo.visibility = GONE
|
|
||||||
when (item.status) {
|
|
||||||
SUCCEEDED -> {
|
|
||||||
appStatus.setImageResource(R.drawable.ic_check_green)
|
|
||||||
}
|
}
|
||||||
FAILED -> {
|
|
||||||
appStatus.setImageResource(R.drawable.ic_cancel_red)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
appStatus.setImageResource(R.drawable.ic_error_yellow)
|
|
||||||
appInfo.text = getInfo(item.status)
|
|
||||||
appInfo.visibility = VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getInfo(status: AppRestoreStatus): String = when(status) {
|
|
||||||
FAILED_NO_DATA -> context.getString(R.string.restore_app_no_data)
|
|
||||||
FAILED_NOT_ALLOWED -> context.getString(R.string.restore_app_not_allowed)
|
|
||||||
FAILED_NOT_INSTALLED -> context.getString(R.string.restore_app_not_installed)
|
|
||||||
FAILED_QUOTA_EXCEEDED -> context.getString(R.string.restore_app_quota_exceeded)
|
|
||||||
else -> "Please report a bug after you read this."
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.VISIBLE
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.DiffUtil.DiffResult
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
|
import com.stevesoltys.seedvault.R
|
||||||
|
import com.stevesoltys.seedvault.restore.AppRestoreStatus
|
||||||
|
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
|
||||||
|
import com.stevesoltys.seedvault.settings.AppStatusAdapter.AppStatusViewHolder
|
||||||
|
import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||||
|
import com.stevesoltys.seedvault.ui.toRelativeTime
|
||||||
|
|
||||||
|
internal class AppStatusAdapter : Adapter<AppStatusViewHolder>() {
|
||||||
|
|
||||||
|
private val items = ArrayList<AppStatus>()
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppStatusViewHolder {
|
||||||
|
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false)
|
||||||
|
return AppStatusViewHolder(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = items.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: AppStatusViewHolder, position: Int) {
|
||||||
|
holder.bind(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(newItems: List<AppStatus>, diff: DiffResult) {
|
||||||
|
items.clear()
|
||||||
|
items.addAll(newItems)
|
||||||
|
diff.dispatchUpdatesTo(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class AppStatusViewHolder(v: View) : AppViewHolder(v) {
|
||||||
|
fun bind(item: AppStatus) {
|
||||||
|
appName.text = item.name
|
||||||
|
appIcon.setImageDrawable(item.icon)
|
||||||
|
setStatus(item.status)
|
||||||
|
if (item.status == SUCCEEDED) {
|
||||||
|
appInfo.text = item.time.toRelativeTime(context)
|
||||||
|
appInfo.visibility = VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class AppStatus(
|
||||||
|
val packageName: String,
|
||||||
|
val icon: Drawable,
|
||||||
|
val name: String,
|
||||||
|
val time: Long,
|
||||||
|
val status: AppRestoreStatus)
|
||||||
|
|
||||||
|
internal class AppStatusDiff(
|
||||||
|
private val oldItems: List<AppStatus>,
|
||||||
|
private val newItems: List<AppStatus>) : DiffUtil.Callback() {
|
||||||
|
|
||||||
|
override fun getOldListSize() = oldItems.size
|
||||||
|
override fun getNewListSize() = newItems.size
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
return oldItems[oldItemPosition].packageName == newItems[newItemPosition].packageName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
return oldItems[oldItemPosition] == newItems[newItemPosition]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class AppStatusResult(
|
||||||
|
val appStatusList: List<AppStatus>,
|
||||||
|
val diff: DiffResult
|
||||||
|
)
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.INVISIBLE
|
||||||
|
import android.view.View.VISIBLE
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.stevesoltys.seedvault.R
|
||||||
|
import kotlinx.android.synthetic.main.fragment_app_status.*
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
|
||||||
|
class AppStatusFragment : Fragment() {
|
||||||
|
|
||||||
|
private val viewModel: SettingsViewModel by sharedViewModel()
|
||||||
|
|
||||||
|
private val layoutManager = LinearLayoutManager(context)
|
||||||
|
private val adapter = AppStatusAdapter()
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_app_status, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
|
activity?.setTitle(R.string.settings_backup_status_title)
|
||||||
|
|
||||||
|
list.apply {
|
||||||
|
layoutManager = this@AppStatusFragment.layoutManager
|
||||||
|
adapter = this@AppStatusFragment.adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar.visibility = VISIBLE
|
||||||
|
viewModel.appStatusList.observe(this, Observer { result ->
|
||||||
|
adapter.update(result.appStatusList, result.diff)
|
||||||
|
progressBar.visibility = INVISIBLE
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,6 +2,9 @@ package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback
|
||||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
|
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
|
||||||
|
@ -9,7 +12,9 @@ import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class SettingsActivity : RequireProvisioningActivity() {
|
internal const val ACTION_APP_STATUS_LIST = "com.stevesoltys.seedvault.APP_STATUS_LIST"
|
||||||
|
|
||||||
|
class SettingsActivity : RequireProvisioningActivity(), OnPreferenceStartFragmentCallback {
|
||||||
|
|
||||||
private val viewModel: SettingsViewModel by viewModel()
|
private val viewModel: SettingsViewModel by viewModel()
|
||||||
private val notificationManager: BackupNotificationManager by inject()
|
private val notificationManager: BackupNotificationManager by inject()
|
||||||
|
@ -23,7 +28,11 @@ class SettingsActivity : RequireProvisioningActivity() {
|
||||||
|
|
||||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
if (savedInstanceState == null) showFragment(SettingsFragment())
|
if (intent?.action == ACTION_APP_STATUS_LIST) {
|
||||||
|
showFragment(AppStatusFragment())
|
||||||
|
} else if (savedInstanceState == null) {
|
||||||
|
showFragment(SettingsFragment())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
|
@ -41,4 +50,14 @@ class SettingsActivity : RequireProvisioningActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean {
|
||||||
|
val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, pref.fragment)
|
||||||
|
fragment.setTargetFragment(caller, 0)
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.fragment, fragment)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,6 @@ import android.os.Bundle
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
|
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
|
||||||
import android.text.format.DateUtils.MINUTE_IN_MILLIS
|
|
||||||
import android.text.format.DateUtils.getRelativeTimeSpanString
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
@ -28,6 +26,7 @@ import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.UsbMonitor
|
import com.stevesoltys.seedvault.UsbMonitor
|
||||||
import com.stevesoltys.seedvault.isMassStorage
|
import com.stevesoltys.seedvault.isMassStorage
|
||||||
import com.stevesoltys.seedvault.restore.RestoreActivity
|
import com.stevesoltys.seedvault.restore.RestoreActivity
|
||||||
|
import com.stevesoltys.seedvault.ui.toRelativeTime
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
|
||||||
|
@ -43,6 +42,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
private lateinit var autoRestore: TwoStatePreference
|
private lateinit var autoRestore: TwoStatePreference
|
||||||
private lateinit var apkBackup: TwoStatePreference
|
private lateinit var apkBackup: TwoStatePreference
|
||||||
private lateinit var backupLocation: Preference
|
private lateinit var backupLocation: Preference
|
||||||
|
private lateinit var backupStatus: Preference
|
||||||
|
|
||||||
private var menuBackupNow: MenuItem? = null
|
private var menuBackupNow: MenuItem? = null
|
||||||
private var menuRestore: MenuItem? = null
|
private var menuRestore: MenuItem? = null
|
||||||
|
@ -65,7 +65,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
setPreferencesFromResource(R.xml.settings, rootKey)
|
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
|
||||||
backup = findPreference<TwoStatePreference>("backup")!!
|
backup = findPreference("backup")!!
|
||||||
backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
||||||
val enabled = newValue as Boolean
|
val enabled = newValue as Boolean
|
||||||
try {
|
try {
|
||||||
|
@ -78,13 +78,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
backupLocation = findPreference<Preference>("backup_location")!!
|
backupLocation = findPreference("backup_location")!!
|
||||||
backupLocation.setOnPreferenceClickListener {
|
backupLocation.setOnPreferenceClickListener {
|
||||||
viewModel.chooseBackupLocation()
|
viewModel.chooseBackupLocation()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
autoRestore = findPreference<TwoStatePreference>("auto_restore")!!
|
autoRestore = findPreference("auto_restore")!!
|
||||||
autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
||||||
val enabled = newValue as Boolean
|
val enabled = newValue as Boolean
|
||||||
try {
|
try {
|
||||||
|
@ -115,8 +115,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
.show()
|
.show()
|
||||||
return@OnPreferenceChangeListener false
|
return@OnPreferenceChangeListener false
|
||||||
}
|
}
|
||||||
|
backupStatus = findPreference("backup_status")!!
|
||||||
|
|
||||||
viewModel.lastBackupTime.observe(this, Observer { time -> setBackupLocationSummary(time) })
|
viewModel.lastBackupTime.observe(this, Observer { time -> setAppBackupStatusSummary(time) })
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
@ -126,10 +127,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
activity?.setTitle(R.string.backup)
|
activity?.setTitle(R.string.backup)
|
||||||
|
|
||||||
storage = settingsManager.getStorage()
|
storage = settingsManager.getStorage()
|
||||||
setBackupState()
|
setBackupEnabledState()
|
||||||
|
setBackupLocationSummary()
|
||||||
setAutoRestoreState()
|
setAutoRestoreState()
|
||||||
setMenuItemStates()
|
setMenuItemStates()
|
||||||
viewModel.updateLastBackupTime()
|
|
||||||
|
|
||||||
if (storage?.isUsb == true) context?.registerReceiver(usbReceiver, usbFilter)
|
if (storage?.isUsb == true) context?.registerReceiver(usbReceiver, usbFilter)
|
||||||
}
|
}
|
||||||
|
@ -150,23 +151,23 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
setMenuItemStates()
|
setMenuItemStates()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
item.itemId == R.id.action_backup -> {
|
R.id.action_backup -> {
|
||||||
viewModel.backupNow()
|
viewModel.backupNow()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
item.itemId == R.id.action_restore -> {
|
R.id.action_restore -> {
|
||||||
startActivity(Intent(requireContext(), RestoreActivity::class.java))
|
startActivity(Intent(requireContext(), RestoreActivity::class.java))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
item.itemId == R.id.action_about -> {
|
R.id.action_about -> {
|
||||||
AboutDialogFragment().show(fragmentManager!!, AboutDialogFragment.TAG)
|
AboutDialogFragment().show(fragmentManager!!, AboutDialogFragment.TAG)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setBackupState() {
|
private fun setBackupEnabledState() {
|
||||||
try {
|
try {
|
||||||
backup.isChecked = backupManager.isBackupEnabled
|
backup.isChecked = backupManager.isBackupEnabled
|
||||||
backup.isEnabled = true
|
backup.isEnabled = true
|
||||||
|
@ -189,17 +190,15 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setBackupLocationSummary(lastBackupInMillis: Long) {
|
private fun setBackupLocationSummary() {
|
||||||
// get name of storage location
|
// get name of storage location
|
||||||
val storageName = storage?.name ?: getString(R.string.settings_backup_location_none)
|
backupLocation.summary = storage?.name ?: getString(R.string.settings_backup_location_none)
|
||||||
|
|
||||||
// set time of last backup
|
|
||||||
val lastBackup = if (lastBackupInMillis == 0L) {
|
|
||||||
getString(R.string.settings_backup_last_backup_never)
|
|
||||||
} else {
|
|
||||||
getRelativeTimeSpanString(lastBackupInMillis, System.currentTimeMillis(), MINUTE_IN_MILLIS, 0)
|
|
||||||
}
|
}
|
||||||
backupLocation.summary = getString(R.string.settings_backup_location_summary, storageName, lastBackup)
|
|
||||||
|
private fun setAppBackupStatusSummary(lastBackupInMillis: Long) {
|
||||||
|
// set time of last backup
|
||||||
|
val lastBackup = lastBackupInMillis.toRelativeTime(requireContext())
|
||||||
|
backupStatus.summary = getString(R.string.settings_backup_status_summary, lastBackup)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setMenuItemStates() {
|
private fun setMenuItemStates() {
|
||||||
|
|
|
@ -1,12 +1,37 @@
|
||||||
package com.stevesoltys.seedvault.settings
|
package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.content.ContextCompat.getDrawable
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.Transformations.switchMap
|
||||||
|
import androidx.lifecycle.liveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.recyclerview.widget.DiffUtil.calculateDiff
|
||||||
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
|
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.metadata.MetadataManager
|
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
|
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.metadata.isSystemApp
|
||||||
|
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED
|
||||||
|
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_ALLOWED
|
||||||
|
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NO_DATA
|
||||||
|
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_QUOTA_EXCEEDED
|
||||||
|
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
|
||||||
import com.stevesoltys.seedvault.transport.requestBackup
|
import com.stevesoltys.seedvault.transport.requestBackup
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
private val TAG = SettingsViewModel::class.java.simpleName
|
||||||
|
|
||||||
class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
app: Application,
|
app: Application,
|
||||||
|
@ -17,15 +42,64 @@ class SettingsViewModel(
|
||||||
|
|
||||||
override val isRestoreOperation = false
|
override val isRestoreOperation = false
|
||||||
|
|
||||||
private val _lastBackupTime = MutableLiveData<Long>()
|
internal val lastBackupTime = metadataManager.lastBackupTime
|
||||||
internal val lastBackupTime: LiveData<Long> = _lastBackupTime
|
|
||||||
|
|
||||||
internal fun updateLastBackupTime() {
|
private val mAppStatusList = switchMap(lastBackupTime) { getAppStatusResult() }
|
||||||
Thread { _lastBackupTime.postValue(metadataManager.getLastBackupTime()) }.start()
|
internal val appStatusList: LiveData<AppStatusResult> = mAppStatusList
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
// ensures the lastBackupTime LiveData gets set
|
||||||
|
metadataManager.getLastBackupTime()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun backupNow() {
|
internal fun backupNow() {
|
||||||
Thread { requestBackup(app) }.start()
|
Thread { requestBackup(app) }.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getAppStatusResult(): LiveData<AppStatusResult> = liveData(Dispatchers.Main) {
|
||||||
|
val pm = app.packageManager
|
||||||
|
val locale = Locale.getDefault()
|
||||||
|
val list = pm.getInstalledPackages(0)
|
||||||
|
.filter { !it.isSystemApp() }
|
||||||
|
.map {
|
||||||
|
val icon = if (it.packageName == MAGIC_PACKAGE_MANAGER) {
|
||||||
|
getDrawable(app, R.drawable.ic_launcher_default)!!
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
pm.getApplicationIcon(it.packageName)
|
||||||
|
} catch (e: NameNotFoundException) {
|
||||||
|
getDrawable(app, R.drawable.ic_launcher_default)!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val metadata = metadataManager.getPackageMetadata(it.packageName)
|
||||||
|
val time = metadata?.time ?: 0
|
||||||
|
val status = when (metadata?.state) {
|
||||||
|
null -> {
|
||||||
|
Log.w(TAG, "No metadata available for: ${it.packageName}")
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
NO_DATA -> FAILED_NO_DATA
|
||||||
|
NOT_ALLOWED -> FAILED_NOT_ALLOWED
|
||||||
|
QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED
|
||||||
|
UNKNOWN_ERROR -> FAILED
|
||||||
|
APK_AND_DATA -> SUCCEEDED
|
||||||
|
}
|
||||||
|
if (metadata?.hasApk() == false) {
|
||||||
|
Log.w(TAG, "No APK stored for: ${it.packageName}")
|
||||||
|
}
|
||||||
|
AppStatus(
|
||||||
|
packageName = it.packageName,
|
||||||
|
icon = icon,
|
||||||
|
name = getAppName(app, it.packageName).toString(),
|
||||||
|
time = time,
|
||||||
|
status = status
|
||||||
|
)
|
||||||
|
}.sortedBy { it.name.toLowerCase(locale) }
|
||||||
|
val oldList = mAppStatusList.value?.appStatusList ?: emptyList()
|
||||||
|
val diff = calculateDiff(AppStatusDiff(oldList, list))
|
||||||
|
emit(AppStatusResult(list, diff))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
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.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||||
|
@ -94,7 +95,20 @@ internal class BackupCoordinator(
|
||||||
return targetPackage.packageName != plugin.providerPackageName
|
return targetPackage.packageName != plugin.providerPackageName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask the transport about current quota for backup size of the package.
|
||||||
|
*
|
||||||
|
* @param packageName ID of package to provide the quota.
|
||||||
|
* @param isFullBackup If set, transport should return limit for full data backup,
|
||||||
|
* otherwise for key-value backup.
|
||||||
|
* @return Current limit on backup size in bytes.
|
||||||
|
*/
|
||||||
fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long {
|
fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long {
|
||||||
|
// try to back up APK here as later methods are sometimes not called called
|
||||||
|
val pm = context.packageManager
|
||||||
|
backUpApk(pm.getPackageInfo(packageName, GET_SIGNING_CERTIFICATES))
|
||||||
|
|
||||||
|
// report back quota
|
||||||
Log.i(TAG, "Get backup quota for $packageName. Is full backup: $isFullBackup.")
|
Log.i(TAG, "Get backup quota for $packageName. Is full backup: $isFullBackup.")
|
||||||
val quota = if (isFullBackup) full.getQuota() else kv.getQuota()
|
val quota = if (isFullBackup) full.getQuota() else kv.getQuota()
|
||||||
Log.i(TAG, "Reported quota of $quota bytes.")
|
Log.i(TAG, "Reported quota of $quota bytes.")
|
||||||
|
@ -132,8 +146,7 @@ internal class BackupCoordinator(
|
||||||
// hook in here to back up APKs of apps that are otherwise not allowed for backup
|
// hook in here to back up APKs of apps that are otherwise not allowed for backup
|
||||||
backUpNotAllowedPackages()
|
backUpNotAllowedPackages()
|
||||||
}
|
}
|
||||||
val result = kv.performBackup(packageInfo, data, flags)
|
return kv.performBackup(packageInfo, data, flags)
|
||||||
return backUpApk(result, packageInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------
|
||||||
|
@ -166,8 +179,7 @@ internal class BackupCoordinator(
|
||||||
|
|
||||||
fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int {
|
fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int {
|
||||||
cancelReason = UNKNOWN_ERROR
|
cancelReason = UNKNOWN_ERROR
|
||||||
val result = full.performFullBackup(targetPackage, fileDescriptor, flags)
|
return full.performFullBackup(targetPackage, fileDescriptor, flags)
|
||||||
return backUpApk(result, targetPackage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
|
fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
|
||||||
|
@ -254,27 +266,24 @@ internal class BackupCoordinator(
|
||||||
Log.d(TAG, "Checking if APKs of opt-out apps need backup...")
|
Log.d(TAG, "Checking if APKs of opt-out apps need backup...")
|
||||||
packageService.notAllowedPackages.forEach { optOutPackageInfo ->
|
packageService.notAllowedPackages.forEach { optOutPackageInfo ->
|
||||||
try {
|
try {
|
||||||
backUpApk(0, optOutPackageInfo, NOT_ALLOWED)
|
backUpApk(optOutPackageInfo, NOT_ALLOWED)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error backing up opt-out APK of ${optOutPackageInfo.packageName}", e)
|
Log.e(TAG, "Error backing up opt-out APK of ${optOutPackageInfo.packageName}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun backUpApk(result: Int, packageInfo: PackageInfo, packageState: PackageState = UNKNOWN_ERROR): Int {
|
private fun backUpApk(packageInfo: PackageInfo, packageState: PackageState = UNKNOWN_ERROR) {
|
||||||
val packageName = packageInfo.packageName
|
val packageName = packageInfo.packageName
|
||||||
if (packageName == MAGIC_PACKAGE_MANAGER) return result
|
try {
|
||||||
return try {
|
|
||||||
apkBackup.backupApkIfNecessary(packageInfo, packageState) {
|
apkBackup.backupApkIfNecessary(packageInfo, packageState) {
|
||||||
plugin.getApkOutputStream(packageInfo)
|
plugin.getApkOutputStream(packageInfo)
|
||||||
}?.let { packageMetadata ->
|
}?.let { packageMetadata ->
|
||||||
val outputStream = plugin.getMetadataOutputStream()
|
val outputStream = plugin.getMetadataOutputStream()
|
||||||
metadataManager.onApkBackedUp(packageInfo, packageMetadata, outputStream)
|
metadataManager.onApkBackedUp(packageInfo, packageMetadata, outputStream)
|
||||||
}
|
}
|
||||||
result
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error while writing APK or metadata for $packageName", e)
|
Log.e(TAG, "Error while writing APK or metadata for $packageName", e)
|
||||||
TRANSPORT_PACKAGE_REJECTED
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.stevesoltys.seedvault.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.GONE
|
||||||
|
import android.view.View.INVISIBLE
|
||||||
|
import android.view.View.VISIBLE
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
internal open class AppViewHolder(v: View) : RecyclerView.ViewHolder(v) {
|
||||||
|
|
||||||
|
protected val context: Context = v.context
|
||||||
|
protected val pm: PackageManager = context.packageManager
|
||||||
|
|
||||||
|
protected val appIcon: ImageView = v.findViewById(R.id.appIcon)
|
||||||
|
protected val appName: TextView = v.findViewById(R.id.appName)
|
||||||
|
protected val appInfo: TextView = v.findViewById(R.id.appInfo)
|
||||||
|
protected val appStatus: ImageView = v.findViewById(R.id.appStatus)
|
||||||
|
protected val progressBar: ProgressBar = v.findViewById(R.id.progressBar)
|
||||||
|
|
||||||
|
protected fun setStatus(status: AppRestoreStatus) {
|
||||||
|
if (status == IN_PROGRESS) {
|
||||||
|
appInfo.visibility = GONE
|
||||||
|
appStatus.visibility = INVISIBLE
|
||||||
|
progressBar.visibility = VISIBLE
|
||||||
|
} else {
|
||||||
|
appStatus.visibility = VISIBLE
|
||||||
|
progressBar.visibility = INVISIBLE
|
||||||
|
appInfo.visibility = GONE
|
||||||
|
when (status) {
|
||||||
|
SUCCEEDED -> appStatus.setImageResource(R.drawable.ic_check_green)
|
||||||
|
FAILED -> appStatus.setImageResource(R.drawable.ic_cancel_red)
|
||||||
|
else -> {
|
||||||
|
when (status) {
|
||||||
|
FAILED_NO_DATA -> appStatus.setImageResource(R.drawable.ic_radio_button_unchecked_yellow)
|
||||||
|
FAILED_NOT_ALLOWED -> appStatus.setImageResource(R.drawable.ic_block_yellow)
|
||||||
|
else -> appStatus.setImageResource(R.drawable.ic_error_yellow)
|
||||||
|
}
|
||||||
|
appInfo.text = status.getInfo()
|
||||||
|
appInfo.visibility = VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AppRestoreStatus.getInfo(): String = when (this) {
|
||||||
|
FAILED_NO_DATA -> context.getString(R.string.restore_app_no_data)
|
||||||
|
FAILED_NOT_ALLOWED -> context.getString(R.string.restore_app_not_allowed)
|
||||||
|
FAILED_NOT_INSTALLED -> context.getString(R.string.restore_app_not_installed)
|
||||||
|
FAILED_QUOTA_EXCEEDED -> context.getString(R.string.restore_app_quota_exceeded)
|
||||||
|
else -> "Please report a bug after you read this."
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
15
app/src/main/java/com/stevesoltys/seedvault/ui/UiUtils.kt
Normal file
15
app/src/main/java/com/stevesoltys/seedvault/ui/UiUtils.kt
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package com.stevesoltys.seedvault.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.format.DateUtils.MINUTE_IN_MILLIS
|
||||||
|
import android.text.format.DateUtils.getRelativeTimeSpanString
|
||||||
|
import com.stevesoltys.seedvault.R
|
||||||
|
|
||||||
|
fun Long.toRelativeTime(context: Context): CharSequence {
|
||||||
|
return if (this == 0L) {
|
||||||
|
context.getString(R.string.settings_backup_last_backup_never)
|
||||||
|
} else {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, 0)
|
||||||
|
}
|
||||||
|
}
|
10
app/src/main/res/drawable/ic_apps.xml
Normal file
10
app/src/main/res/drawable/ic_apps.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?android:attr/textColorSecondary"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z" />
|
||||||
|
</vector>
|
9
app/src/main/res/drawable/ic_block_yellow.xml
Normal file
9
app/src/main/res/drawable/ic_block_yellow.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/yellow"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.42 3.58,-8 8,-8 1.85,0 3.55,0.63 4.9,1.69L5.69,16.9C4.63,15.55 4,13.85 4,12zM12,20c-1.85,0 -3.55,-0.63 -4.9,-1.69L18.31,7.1C19.37,8.45 20,10.15 20,12c0,4.42 -3.58,8 -8,8z" />
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_cloud_done.xml
Normal file
10
app/src/main/res/drawable/ic_cloud_done.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?android:attr/textColorSecondary"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM10,17l-3.5,-3.5 1.41,-1.41L10,14.17 15.18,9l1.41,1.41L10,17z" />
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/yellow"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
|
||||||
|
</vector>
|
30
app/src/main/res/layout/fragment_app_status.xml
Normal file
30
app/src/main/res/layout/fragment_app_status.xml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/list"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:scrollbarStyle="outsideOverlay"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
style="?android:attr/progressBarStyleLarge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -14,17 +14,19 @@
|
||||||
<string name="settings_backup_location_none">None</string>
|
<string name="settings_backup_location_none">None</string>
|
||||||
<string name="settings_backup_location_internal">Internal Storage</string>
|
<string name="settings_backup_location_internal">Internal Storage</string>
|
||||||
<string name="settings_backup_last_backup_never">Never</string>
|
<string name="settings_backup_last_backup_never">Never</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_auto_restore_summary_usb">Note: Your %1$s needs to be plugged in for this to work.</string>
|
||||||
|
<string name="settings_category_app_data_backup">App data backup</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>
|
||||||
<string name="settings_backup_apk_dialog_message">Disabled app backup will still back up app data. However, it will not get restored automatically.\n\nYou will need to install all your apps manually while having \"Automatic Restore\" switched on.</string>
|
<string name="settings_backup_apk_dialog_message">Disabled app backup will still back up app data. However, it will not get restored automatically.\n\nYou will need to install all your apps manually while having \"Automatic Restore\" switched on.</string>
|
||||||
<string name="settings_backup_apk_dialog_cancel">Cancel</string>
|
<string name="settings_backup_apk_dialog_cancel">Cancel</string>
|
||||||
<string name="settings_backup_apk_dialog_disable">Disable app backup</string>
|
<string name="settings_backup_apk_dialog_disable">Disable app backup</string>
|
||||||
|
<string name="settings_backup_status_title">App backup status</string>
|
||||||
|
<string name="settings_backup_status_summary">Last Backup: %1$s</string>
|
||||||
<string name="settings_backup_now">Backup now</string>
|
<string name="settings_backup_now">Backup now</string>
|
||||||
|
|
||||||
<!-- Storage -->
|
<!-- Storage -->
|
||||||
|
@ -102,7 +104,7 @@
|
||||||
<string name="restore_restoring">Restoring Backup</string>
|
<string name="restore_restoring">Restoring Backup</string>
|
||||||
<string name="restore_magic_package">System Package Manager</string>
|
<string name="restore_magic_package">System Package Manager</string>
|
||||||
<string name="restore_app_no_data">App reported no data for backup</string>
|
<string name="restore_app_no_data">App reported no data for backup</string>
|
||||||
<string name="restore_app_not_allowed">Data backup was not allowed</string>
|
<string name="restore_app_not_allowed">App doesn\'t allow backup</string>
|
||||||
<string name="restore_app_not_installed">App not installed</string>
|
<string name="restore_app_not_installed">App not installed</string>
|
||||||
<string name="restore_app_quota_exceeded">Backup quota exceeded</string>
|
<string name="restore_app_quota_exceeded">Backup quota exceeded</string>
|
||||||
<string name="restore_finished_success">Restore complete</string>
|
<string name="restore_finished_success">Restore complete</string>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<androidx.preference.PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
<androidx.preference.PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<androidx.preference.SwitchPreferenceCompat
|
<androidx.preference.SwitchPreferenceCompat
|
||||||
app:icon="@drawable/ic_cloud_upload"
|
app:icon="@drawable/ic_cloud_upload"
|
||||||
|
@ -20,6 +21,17 @@
|
||||||
app:summary="@string/settings_auto_restore_summary"
|
app:summary="@string/settings_auto_restore_summary"
|
||||||
app:title="@string/settings_auto_restore_title" />
|
app:title="@string/settings_auto_restore_title" />
|
||||||
|
|
||||||
|
<androidx.preference.PreferenceCategory
|
||||||
|
app:key="category_app_data_backup"
|
||||||
|
app:title="@string/settings_category_app_data_backup">
|
||||||
|
|
||||||
|
<androidx.preference.Preference
|
||||||
|
app:fragment="com.stevesoltys.seedvault.settings.AppStatusFragment"
|
||||||
|
app:icon="@drawable/ic_apps"
|
||||||
|
app:key="backup_status"
|
||||||
|
app:title="@string/settings_backup_status_title"
|
||||||
|
tools:summary="Last backup: Never" />
|
||||||
|
|
||||||
<androidx.preference.SwitchPreferenceCompat
|
<androidx.preference.SwitchPreferenceCompat
|
||||||
app:defaultValue="true"
|
app:defaultValue="true"
|
||||||
app:dependency="backup"
|
app:dependency="backup"
|
||||||
|
@ -27,6 +39,8 @@
|
||||||
app:summary="@string/settings_backup_apk_summary"
|
app:summary="@string/settings_backup_apk_summary"
|
||||||
app:title="@string/settings_backup_apk_title" />
|
app:title="@string/settings_backup_apk_title" />
|
||||||
|
|
||||||
|
</androidx.preference.PreferenceCategory>
|
||||||
|
|
||||||
<androidx.preference.Preference
|
<androidx.preference.Preference
|
||||||
app:allowDividerAbove="true"
|
app:allowDividerAbove="true"
|
||||||
app:allowDividerBelow="false"
|
app:allowDividerBelow="false"
|
||||||
|
|
|
@ -113,6 +113,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
val isFullBackup = Random.nextBoolean()
|
val isFullBackup = Random.nextBoolean()
|
||||||
val quota = Random.nextLong()
|
val quota = Random.nextLong()
|
||||||
|
|
||||||
|
expectApkBackupAndMetadataWrite()
|
||||||
if (isFullBackup) {
|
if (isFullBackup) {
|
||||||
every { full.getQuota() } returns quota
|
every { full.getQuota() } returns quota
|
||||||
} else {
|
} else {
|
||||||
|
@ -264,9 +265,9 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expectApkBackupAndMetadataWrite() {
|
private fun expectApkBackupAndMetadataWrite() {
|
||||||
every { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata
|
every { apkBackup.backupApkIfNecessary(any(), UNKNOWN_ERROR, any()) } returns packageMetadata
|
||||||
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||||
every { metadataManager.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream) } just Runs
|
every { metadataManager.onApkBackedUp(any(), packageMetadata, metadataOutputStream) } just Runs
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue