Merge pull request #166 from grote/app-groups
Group app status list into three sections
This commit is contained in:
commit
4d1bd9270d
11 changed files with 164 additions and 78 deletions
|
@ -14,7 +14,7 @@ class PackageServiceTest : KoinComponent {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testNotAllowedPackages() {
|
fun testNotAllowedPackages() {
|
||||||
val packages = packageService.notAllowedPackages
|
val packages = packageService.notBackedUpPackages
|
||||||
Log.e("TEST", "Packages: $packages")
|
Log.e("TEST", "Packages: $packages")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.stevesoltys.seedvault.settings
|
package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
|
import android.annotation.StringRes
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
@ -13,6 +14,7 @@ import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState
|
import com.stevesoltys.seedvault.ui.AppBackupState
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
|
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
|
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
|
||||||
|
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_INSTALLED
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NO_DATA
|
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NO_DATA
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_QUOTA_EXCEEDED
|
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_QUOTA_EXCEEDED
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_WAS_STOPPED
|
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_WAS_STOPPED
|
||||||
|
@ -28,6 +30,20 @@ private const val PACKAGE_NAME_SETTINGS = "com.android.providers.settings"
|
||||||
private const val PACKAGE_NAME_CALL_LOG = "com.android.calllogbackup"
|
private const val PACKAGE_NAME_CALL_LOG = "com.android.calllogbackup"
|
||||||
private const val PACKAGE_NAME_CONTACTS = "org.calyxos.backup.contacts"
|
private const val PACKAGE_NAME_CONTACTS = "org.calyxos.backup.contacts"
|
||||||
|
|
||||||
|
sealed class AppListItem
|
||||||
|
|
||||||
|
data class AppStatus(
|
||||||
|
val packageName: String,
|
||||||
|
var enabled: Boolean,
|
||||||
|
val icon: Drawable,
|
||||||
|
val name: String,
|
||||||
|
val time: Long,
|
||||||
|
val status: AppBackupState,
|
||||||
|
val isSpecial: Boolean = false
|
||||||
|
) : AppListItem()
|
||||||
|
|
||||||
|
class AppSectionTitle(@StringRes val titleRes: Int) : AppListItem()
|
||||||
|
|
||||||
internal class AppListRetriever(
|
internal class AppListRetriever(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val packageService: PackageService,
|
private val packageService: PackageService,
|
||||||
|
@ -38,11 +54,13 @@ internal class AppListRetriever(
|
||||||
private val pm: PackageManager = context.packageManager
|
private val pm: PackageManager = context.packageManager
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun getAppList(): List<AppStatus> {
|
fun getAppList(): List<AppListItem> {
|
||||||
return getSpecialApps() + getUserApps()
|
return listOf(AppSectionTitle(R.string.backup_section_system)) + getSpecialApps() +
|
||||||
|
listOf(AppSectionTitle(R.string.backup_section_user)) + getUserApps() +
|
||||||
|
listOf(AppSectionTitle(R.string.backup_section_not_allowed)) + getNotAllowedApps()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSpecialApps(): List<AppStatus> {
|
private fun getSpecialApps(): List<AppListItem> {
|
||||||
val specialPackages = listOf(
|
val specialPackages = listOf(
|
||||||
Pair(PACKAGE_NAME_SMS, R.string.backup_sms),
|
Pair(PACKAGE_NAME_SMS, R.string.backup_sms),
|
||||||
Pair(PACKAGE_NAME_SETTINGS, R.string.backup_settings),
|
Pair(PACKAGE_NAME_SETTINGS, R.string.backup_settings),
|
||||||
|
@ -51,13 +69,18 @@ internal class AppListRetriever(
|
||||||
)
|
)
|
||||||
return specialPackages.map { (packageName, stringId) ->
|
return specialPackages.map { (packageName, stringId) ->
|
||||||
val metadata = metadataManager.getPackageMetadata(packageName)
|
val metadata = metadataManager.getPackageMetadata(packageName)
|
||||||
|
val status = if (packageName == PACKAGE_NAME_CONTACTS && metadata?.state == null) {
|
||||||
|
// handle local contacts backup specially as it might not be installed
|
||||||
|
if (packageService.getVersionName(packageName) == null) FAILED_NOT_INSTALLED
|
||||||
|
else NOT_YET_BACKED_UP
|
||||||
|
} else metadata?.state.toAppBackupState()
|
||||||
AppStatus(
|
AppStatus(
|
||||||
packageName = packageName,
|
packageName = packageName,
|
||||||
enabled = settingsManager.isBackupEnabled(packageName),
|
enabled = settingsManager.isBackupEnabled(packageName),
|
||||||
icon = getIcon(packageName),
|
icon = getIcon(packageName),
|
||||||
name = context.getString(stringId),
|
name = context.getString(stringId),
|
||||||
time = metadata?.time ?: 0,
|
time = metadata?.time ?: 0,
|
||||||
status = metadata?.state.toAppBackupState(),
|
status = status,
|
||||||
isSpecial = true
|
isSpecial = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -86,6 +109,20 @@ internal class AppListRetriever(
|
||||||
}.sortedBy { it.name.toLowerCase(locale) }
|
}.sortedBy { it.name.toLowerCase(locale) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getNotAllowedApps(): List<AppStatus> {
|
||||||
|
val locale = Locale.getDefault()
|
||||||
|
return packageService.userNotAllowedApps.map {
|
||||||
|
AppStatus(
|
||||||
|
packageName = it.packageName,
|
||||||
|
enabled = settingsManager.isBackupEnabled(it.packageName),
|
||||||
|
icon = getIcon(it.packageName),
|
||||||
|
name = getAppName(context, it.packageName).toString(),
|
||||||
|
time = 0,
|
||||||
|
status = FAILED_NOT_ALLOWED
|
||||||
|
)
|
||||||
|
}.sortedBy { it.name.toLowerCase(locale) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun getIcon(packageName: String): Drawable = when (packageName) {
|
private fun getIcon(packageName: String): Drawable = when (packageName) {
|
||||||
MAGIC_PACKAGE_MANAGER -> context.getDrawable(R.drawable.ic_launcher_default)!!
|
MAGIC_PACKAGE_MANAGER -> context.getDrawable(R.drawable.ic_launcher_default)!!
|
||||||
PACKAGE_NAME_SMS -> context.getDrawable(R.drawable.ic_message)!!
|
PACKAGE_NAME_SMS -> context.getDrawable(R.drawable.ic_message)!!
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.stevesoltys.seedvault.settings
|
package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -11,34 +10,53 @@ 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.ScaleType
|
import android.widget.ImageView.ScaleType
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.DiffUtil.DiffResult
|
import androidx.recyclerview.widget.DiffUtil.DiffResult
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
|
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.settings.AppStatusAdapter.AppStatusViewHolder
|
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState
|
|
||||||
import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
|
import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
|
||||||
import com.stevesoltys.seedvault.ui.AppViewHolder
|
import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||||
import com.stevesoltys.seedvault.ui.toRelativeTime
|
import com.stevesoltys.seedvault.ui.toRelativeTime
|
||||||
|
|
||||||
internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListener) :
|
internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListener) :
|
||||||
Adapter<AppStatusViewHolder>() {
|
Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
private val items = ArrayList<AppStatus>()
|
private val items = ArrayList<AppListItem>()
|
||||||
private var editMode = false
|
private var editMode = false
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppStatusViewHolder {
|
override fun getItemViewType(position: Int): Int = when (items[position]) {
|
||||||
|
is AppStatus -> 0
|
||||||
|
is AppSectionTitle -> 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
return when (viewType) {
|
||||||
|
0 -> {
|
||||||
val v = LayoutInflater.from(parent.context)
|
val v = LayoutInflater.from(parent.context)
|
||||||
.inflate(R.layout.list_item_app_status, parent, false)
|
.inflate(R.layout.list_item_app_status, parent, false)
|
||||||
return AppStatusViewHolder(v)
|
AppStatusViewHolder(v)
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
val v = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.list_item_app_section_title, parent, false)
|
||||||
|
AppSectionTitleViewHolder(v)
|
||||||
|
}
|
||||||
|
else -> throw AssertionError("unknown view type")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = items.size
|
override fun getItemCount() = items.size
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: AppStatusViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
holder.bind(items[position])
|
when (holder) {
|
||||||
|
is AppStatusViewHolder -> holder.bind(items[position] as AppStatus)
|
||||||
|
is AppSectionTitleViewHolder -> holder.bind(items[position] as AppSectionTitle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setEditMode(enabled: Boolean) {
|
fun setEditMode(enabled: Boolean) {
|
||||||
|
@ -46,17 +64,24 @@ internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListe
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(newItems: List<AppStatus>, diff: DiffResult) {
|
fun update(newItems: List<AppListItem>, diff: DiffResult) {
|
||||||
items.clear()
|
items.clear()
|
||||||
items.addAll(newItems)
|
items.addAll(newItems)
|
||||||
diff.dispatchUpdatesTo(this)
|
diff.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onItemChanged(item: AppStatus) {
|
fun onItemChanged(item: AppStatus) {
|
||||||
val pos = items.indexOfFirst { it.packageName == item.packageName }
|
val pos = items.indexOfFirst { it is AppStatus && it.packageName == item.packageName }
|
||||||
if (pos != NO_POSITION) notifyItemChanged(pos, item)
|
if (pos != NO_POSITION) notifyItemChanged(pos, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AppSectionTitleViewHolder(v: View) : RecyclerView.ViewHolder(v) {
|
||||||
|
private val titleView: TextView = v as TextView
|
||||||
|
fun bind(item: AppSectionTitle) {
|
||||||
|
titleView.setText(item.titleRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inner class AppStatusViewHolder(v: View) : AppViewHolder(v) {
|
inner class AppStatusViewHolder(v: View) : AppViewHolder(v) {
|
||||||
fun bind(item: AppStatus) {
|
fun bind(item: AppStatus) {
|
||||||
appName.text = item.name
|
appName.text = item.name
|
||||||
|
@ -83,7 +108,13 @@ internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListe
|
||||||
startActivity(context, intent, null)
|
startActivity(context, intent, null)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
if (item.status == FAILED_NOT_ALLOWED) {
|
||||||
|
appStatus.visibility = INVISIBLE
|
||||||
|
progressBar.visibility = INVISIBLE
|
||||||
|
appInfo.visibility = GONE
|
||||||
|
} else {
|
||||||
setState(item.status, false)
|
setState(item.status, false)
|
||||||
|
}
|
||||||
if (item.status == SUCCEEDED) {
|
if (item.status == SUCCEEDED) {
|
||||||
appInfo.text = item.time.toRelativeTime(context)
|
appInfo.text = item.time.toRelativeTime(context)
|
||||||
appInfo.visibility = VISIBLE
|
appInfo.visibility = VISIBLE
|
||||||
|
@ -106,34 +137,31 @@ internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListe
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AppStatus(
|
|
||||||
val packageName: String,
|
|
||||||
var enabled: Boolean,
|
|
||||||
val icon: Drawable,
|
|
||||||
val name: String,
|
|
||||||
val time: Long,
|
|
||||||
val status: AppBackupState,
|
|
||||||
val isSpecial: Boolean = false
|
|
||||||
)
|
|
||||||
|
|
||||||
internal class AppStatusDiff(
|
internal class AppStatusDiff(
|
||||||
private val oldItems: List<AppStatus>,
|
private val oldItems: List<AppListItem>,
|
||||||
private val newItems: List<AppStatus>
|
private val newItems: List<AppListItem>
|
||||||
) : DiffUtil.Callback() {
|
) : DiffUtil.Callback() {
|
||||||
|
|
||||||
override fun getOldListSize() = oldItems.size
|
override fun getOldListSize() = oldItems.size
|
||||||
override fun getNewListSize() = newItems.size
|
override fun getNewListSize() = newItems.size
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
return oldItems[oldItemPosition].packageName == newItems[newItemPosition].packageName
|
val old = oldItems[oldItemPosition]
|
||||||
|
val new = newItems[newItemPosition]
|
||||||
|
if (old is AppSectionTitle && new is AppSectionTitle) return old.titleRes == new.titleRes
|
||||||
|
if (old is AppStatus && new is AppStatus) return old.packageName == new.packageName
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
return oldItems[oldItemPosition] == newItems[newItemPosition]
|
val old = oldItems[oldItemPosition]
|
||||||
|
val new = newItems[newItemPosition]
|
||||||
|
if (old is AppSectionTitle && new is AppSectionTitle) return old.titleRes == new.titleRes
|
||||||
|
return old == new
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class AppStatusResult(
|
internal class AppStatusResult(
|
||||||
val appStatusList: List<AppStatus>,
|
val appStatusList: List<AppListItem>,
|
||||||
val diff: DiffResult
|
val diff: DiffResult
|
||||||
)
|
)
|
||||||
|
|
|
@ -259,7 +259,7 @@ internal class BackupCoordinator(
|
||||||
val result = kv.performBackup(packageInfo, data, flags)
|
val result = kv.performBackup(packageInfo, data, flags)
|
||||||
if (result == TRANSPORT_OK && packageName == MAGIC_PACKAGE_MANAGER) {
|
if (result == TRANSPORT_OK && packageName == MAGIC_PACKAGE_MANAGER) {
|
||||||
// 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()
|
backUpApksOfNotBackedUpPackages()
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -388,13 +388,13 @@ internal class BackupCoordinator(
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = PRIVATE)
|
@VisibleForTesting(otherwise = PRIVATE)
|
||||||
internal suspend fun backUpNotAllowedPackages() {
|
internal suspend fun backUpApksOfNotBackedUpPackages() {
|
||||||
Log.d(TAG, "Checking if APKs of opt-out apps need backup...")
|
Log.d(TAG, "Checking if APKs of opt-out apps need backup...")
|
||||||
val notAllowedPackages = packageService.notAllowedPackages
|
val notBackedUpPackages = packageService.notBackedUpPackages
|
||||||
notAllowedPackages.forEachIndexed { i, packageInfo ->
|
notBackedUpPackages.forEachIndexed { i, packageInfo ->
|
||||||
val packageName = packageInfo.packageName
|
val packageName = packageInfo.packageName
|
||||||
try {
|
try {
|
||||||
nm.onOptOutAppBackup(packageName, i + 1, notAllowedPackages.size)
|
nm.onOptOutAppBackup(packageName, i + 1, notBackedUpPackages.size)
|
||||||
val packageState =
|
val packageState =
|
||||||
if (packageInfo.isStopped()) WAS_STOPPED else NOT_ALLOWED
|
if (packageInfo.isStopped()) WAS_STOPPED else NOT_ALLOWED
|
||||||
val wasBackedUp = backUpApk(packageInfo, packageState)
|
val wasBackedUp = backUpApk(packageInfo, packageState)
|
||||||
|
|
|
@ -66,7 +66,7 @@ internal class PackageService(
|
||||||
return packageArray.toTypedArray()
|
return packageArray.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
val notAllowedPackages: List<PackageInfo>
|
val notBackedUpPackages: List<PackageInfo>
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
get() {
|
get() {
|
||||||
// We need the GET_SIGNING_CERTIFICATES flag here,
|
// We need the GET_SIGNING_CERTIFICATES flag here,
|
||||||
|
@ -88,13 +88,22 @@ internal class PackageService(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of non-system apps (without instrumentation test apps).
|
* A list of non-system apps
|
||||||
|
* (without instrumentation test apps and without apps that don't allow backup).
|
||||||
*/
|
*/
|
||||||
val userApps: List<PackageInfo>
|
val userApps: List<PackageInfo>
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
get() {
|
get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo ->
|
||||||
return packageManager.getInstalledPackages(GET_INSTRUMENTATION)
|
packageInfo.isUserVisible(context) && packageInfo.allowsBackup()
|
||||||
.filter { it.isUserVisible(context) }
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of apps that does not allow backup.
|
||||||
|
*/
|
||||||
|
val userNotAllowedApps: List<PackageInfo>
|
||||||
|
@WorkerThread
|
||||||
|
get() = packageManager.getInstalledPackages(0).filter { packageInfo ->
|
||||||
|
!packageInfo.allowsBackup() && !packageInfo.isSystemApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
val expectedAppTotals: ExpectedAppTotals
|
val expectedAppTotals: ExpectedAppTotals
|
||||||
|
@ -148,6 +157,11 @@ internal fun PackageInfo.isSystemApp(): Boolean {
|
||||||
return applicationInfo.flags and FLAG_SYSTEM != 0
|
return applicationInfo.flags and FLAG_SYSTEM != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun PackageInfo.allowsBackup(): Boolean {
|
||||||
|
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
||||||
|
return applicationInfo.flags and FLAG_ALLOW_BACKUP != 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this is a system app that hasn't been updated.
|
* Returns true if this is a system app that hasn't been updated.
|
||||||
* We don't back up those APKs.
|
* We don't back up those APKs.
|
||||||
|
|
|
@ -23,8 +23,8 @@ enum class AppBackupState {
|
||||||
FAILED -> notShownString
|
FAILED -> notShownString
|
||||||
FAILED_NO_DATA -> context.getString(R.string.backup_app_no_data)
|
FAILED_NO_DATA -> context.getString(R.string.backup_app_no_data)
|
||||||
FAILED_WAS_STOPPED -> context.getString(R.string.backup_app_was_stopped)
|
FAILED_WAS_STOPPED -> context.getString(R.string.backup_app_was_stopped)
|
||||||
FAILED_NOT_ALLOWED -> context.getString(R.string.backup_app_not_allowed)
|
FAILED_NOT_ALLOWED -> notShownString
|
||||||
FAILED_NOT_INSTALLED -> notShownString
|
FAILED_NOT_INSTALLED -> context.getString(R.string.restore_app_not_installed)
|
||||||
FAILED_QUOTA_EXCEEDED -> context.getString(R.string.backup_app_quota_exceeded)
|
FAILED_QUOTA_EXCEEDED -> context.getString(R.string.backup_app_quota_exceeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
8
app/src/main/res/layout/list_item_app_section_title.xml
Normal file
8
app/src/main/res/layout/list_item_app_section_title.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:textColor="?android:textColorPrimary"
|
||||||
|
tools:text="@string/backup_section_not_allowed" />
|
|
@ -12,8 +12,8 @@
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/appIcon"
|
android:id="@+id/appIcon"
|
||||||
android:layout_width="48dp"
|
android:layout_width="42dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="42dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
@ -28,6 +28,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
|
android:textColor="?android:textColorPrimary"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/appInfo"
|
app:layout_constraintBottom_toTopOf="@+id/appInfo"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/switchView"
|
app:layout_constraintEnd_toStartOf="@+id/switchView"
|
||||||
app:layout_constraintStart_toEndOf="@+id/appIcon"
|
app:layout_constraintStart_toEndOf="@+id/appIcon"
|
||||||
|
@ -38,13 +39,13 @@
|
||||||
android:id="@+id/appInfo"
|
android:id="@+id/appInfo"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="4dp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="@+id/appName"
|
app:layout_constraintEnd_toEndOf="@+id/appName"
|
||||||
app:layout_constraintStart_toStartOf="@+id/appName"
|
app:layout_constraintStart_toStartOf="@+id/appName"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/appName"
|
app:layout_constraintTop_toBottomOf="@+id/appName"
|
||||||
tools:text="Some additional information about why the app could not be installed or its data not restored."
|
tools:text="@string/backup_app_not_yet_backed_up"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
@ -60,8 +61,8 @@
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
style="?android:attr/progressBarStyle"
|
style="?android:attr/progressBarStyle"
|
||||||
android:layout_width="24dp"
|
android:layout_width="20dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="20dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
@ -70,6 +71,7 @@
|
||||||
android:id="@+id/switchView"
|
android:id="@+id/switchView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="false"
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<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_title">Backup status</string>
|
||||||
<string name="settings_backup_status_summary">Last backup: %1$s</string>
|
<string name="settings_backup_status_summary">Last backup: %1$s</string>
|
||||||
<string name="settings_backup_exclude_apps">Exclude apps</string>
|
<string name="settings_backup_exclude_apps">Exclude apps</string>
|
||||||
<string name="settings_backup_now">Backup now</string>
|
<string name="settings_backup_now">Backup now</string>
|
||||||
|
@ -91,21 +91,23 @@
|
||||||
|
|
||||||
<!-- App Backup and Restore State -->
|
<!-- App Backup and Restore State -->
|
||||||
|
|
||||||
|
<string name="backup_section_system">System Apps</string>
|
||||||
<string name="backup_sms">SMS text messages</string>
|
<string name="backup_sms">SMS text messages</string>
|
||||||
<string name="backup_settings">Device settings</string>
|
<string name="backup_settings">Device settings</string>
|
||||||
<string name="backup_call_log">Call history</string>
|
<string name="backup_call_log">Call history</string>
|
||||||
<string name="backup_contacts">Local contacts</string>
|
<string name="backup_contacts">Local contacts</string>
|
||||||
|
<string name="backup_section_user">Installed Apps</string>
|
||||||
<!-- This text gets shown for apps that the OS did not try to backup for whatever reason e.g. no backup was run yet -->
|
<!-- This text gets shown for apps that the OS did not try to backup for whatever reason e.g. no backup was run yet -->
|
||||||
<string name="backup_app_not_yet_backed_up">Waiting to back up…</string>
|
<string name="backup_app_not_yet_backed_up">Waiting to back up…</string>
|
||||||
<string name="restore_app_not_yet_backed_up">Was not yet backed up</string>
|
<string name="restore_app_not_yet_backed_up">Was not yet backed up</string>
|
||||||
<string name="backup_app_was_stopped">Not backed up as it wasn\'t used recently</string>
|
<string name="backup_app_was_stopped">Not backed up as it wasn\'t used recently</string>
|
||||||
<string name="restore_app_was_stopped">Was not backed up as it hadn\'t been used recently</string>
|
<string name="restore_app_was_stopped">Was not backed up as it hadn\'t been used recently</string>
|
||||||
<string name="backup_app_no_data">App reported no data for backup</string>
|
<string name="backup_app_no_data">App reported no data for backup</string>
|
||||||
<string name="backup_app_not_allowed">App doesn\'t allow backup</string>
|
|
||||||
<string name="restore_app_not_allowed">App didn\'t allow backup</string>
|
<string name="restore_app_not_allowed">App didn\'t allow backup</string>
|
||||||
<string name="backup_app_quota_exceeded">Backup quota exceeded</string>
|
<string name="backup_app_quota_exceeded">Backup quota exceeded</string>
|
||||||
<string name="restore_app_quota_exceeded">Backup quota was exceeded</string>
|
<string name="restore_app_quota_exceeded">Backup quota was exceeded</string>
|
||||||
<string name="restore_app_not_installed">App not installed</string>
|
<string name="restore_app_not_installed">App not installed</string>
|
||||||
|
<string name="backup_section_not_allowed">Apps that do not allow data backup</string>
|
||||||
|
|
||||||
<!-- Restore -->
|
<!-- Restore -->
|
||||||
<string name="restore_title">Restore from backup</string>
|
<string name="restore_title">Restore from backup</string>
|
||||||
|
|
|
@ -7,6 +7,14 @@
|
||||||
app:persistent="false"
|
app:persistent="false"
|
||||||
app:title="@string/settings_backup" />
|
app:title="@string/settings_backup" />
|
||||||
|
|
||||||
|
<androidx.preference.Preference
|
||||||
|
app:allowDividerAbove="true"
|
||||||
|
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.Preference
|
<androidx.preference.Preference
|
||||||
app:dependency="backup"
|
app:dependency="backup"
|
||||||
app:icon="@drawable/ic_storage"
|
app:icon="@drawable/ic_storage"
|
||||||
|
@ -21,17 +29,6 @@
|
||||||
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"
|
||||||
|
@ -39,8 +36,6 @@
|
||||||
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"
|
||||||
|
|
|
@ -357,7 +357,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
// do actual @pm@ backup
|
// do actual @pm@ backup
|
||||||
coEvery { kv.performBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
|
coEvery { kv.performBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
|
||||||
// now check if we have opt-out apps that we need to back up APKs for
|
// now check if we have opt-out apps that we need to back up APKs for
|
||||||
every { packageService.notAllowedPackages } returns notAllowedPackages
|
every { packageService.notBackedUpPackages } returns notAllowedPackages
|
||||||
// update notification
|
// update notification
|
||||||
every {
|
every {
|
||||||
notificationManager.onOptOutAppBackup(
|
notificationManager.onOptOutAppBackup(
|
||||||
|
@ -411,7 +411,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
fun `APK backup of not allowed apps updates state even without new APK`() = runBlocking {
|
fun `APK backup of not allowed apps updates state even without new APK`() = runBlocking {
|
||||||
val oldPackageMetadata: PackageMetadata = mockk()
|
val oldPackageMetadata: PackageMetadata = mockk()
|
||||||
|
|
||||||
every { packageService.notAllowedPackages } returns listOf(packageInfo)
|
every { packageService.notBackedUpPackages } returns listOf(packageInfo)
|
||||||
every {
|
every {
|
||||||
notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1)
|
notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1)
|
||||||
} just Runs
|
} just Runs
|
||||||
|
@ -431,7 +431,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
} just Runs
|
} just Runs
|
||||||
every { metadataOutputStream.close() } just Runs
|
every { metadataOutputStream.close() } just Runs
|
||||||
|
|
||||||
backup.backUpNotAllowedPackages()
|
backup.backUpApksOfNotBackedUpPackages()
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
metadataManager.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream)
|
metadataManager.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream)
|
||||||
|
@ -441,7 +441,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `APK backup of not allowed apps updates state even without old state`() = runBlocking {
|
fun `APK backup of not allowed apps updates state even without old state`() = runBlocking {
|
||||||
every { packageService.notAllowedPackages } returns listOf(packageInfo)
|
every { packageService.notBackedUpPackages } returns listOf(packageInfo)
|
||||||
every {
|
every {
|
||||||
notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1)
|
notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1)
|
||||||
} just Runs
|
} just Runs
|
||||||
|
@ -459,7 +459,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
} just Runs
|
} just Runs
|
||||||
every { metadataOutputStream.close() } just Runs
|
every { metadataOutputStream.close() } just Runs
|
||||||
|
|
||||||
backup.backUpNotAllowedPackages()
|
backup.backUpApksOfNotBackedUpPackages()
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
metadataManager.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream)
|
metadataManager.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream)
|
||||||
|
|
Loading…
Reference in a new issue