Show backup status of select system apps
This commit is contained in:
parent
a1b68df923
commit
81cd67217b
9 changed files with 166 additions and 62 deletions
|
@ -14,6 +14,7 @@ import com.stevesoltys.seedvault.metadata.metadataModule
|
|||
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
|
||||
import com.stevesoltys.seedvault.restore.RestoreViewModel
|
||||
import com.stevesoltys.seedvault.restore.install.installModule
|
||||
import com.stevesoltys.seedvault.settings.AppListRetriever
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.settings.SettingsViewModel
|
||||
import com.stevesoltys.seedvault.transport.backup.backupModule
|
||||
|
@ -40,6 +41,7 @@ class App : Application() {
|
|||
single { BackupNotificationManager(this@App) }
|
||||
single { Clock() }
|
||||
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
||||
factory { AppListRetriever(this@App, get(), get(), get()) }
|
||||
|
||||
viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get()) }
|
||||
viewModel { RecoveryCodeViewModel(this@App, get()) }
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
package com.stevesoltys.seedvault.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.metadata.PackageState
|
||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
|
||||
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_WAS_STOPPED
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState.NOT_YET_BACKED_UP
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
|
||||
import com.stevesoltys.seedvault.ui.notification.getAppName
|
||||
import java.util.Locale
|
||||
|
||||
private const val TAG = "AppListRetriever"
|
||||
|
||||
private const val PACKAGE_NAME_SMS = "com.android.providers.telephony"
|
||||
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_CONTACTS = "org.calyxos.backup.contacts"
|
||||
|
||||
internal class AppListRetriever(
|
||||
private val context: Context,
|
||||
private val packageService: PackageService,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val metadataManager: MetadataManager
|
||||
) {
|
||||
|
||||
private val pm: PackageManager = context.packageManager
|
||||
|
||||
@WorkerThread
|
||||
fun getAppList(): List<AppStatus> {
|
||||
return getSpecialApps() + getUserApps()
|
||||
}
|
||||
|
||||
private fun getSpecialApps(): List<AppStatus> {
|
||||
val specialPackages = listOf(
|
||||
Pair(PACKAGE_NAME_SMS, R.string.backup_sms),
|
||||
Pair(PACKAGE_NAME_SETTINGS, R.string.backup_settings),
|
||||
Pair(PACKAGE_NAME_CALL_LOG, R.string.backup_call_log),
|
||||
Pair(PACKAGE_NAME_CONTACTS, R.string.backup_contacts)
|
||||
)
|
||||
return specialPackages.map { (packageName, stringId) ->
|
||||
val metadata = metadataManager.getPackageMetadata(packageName)
|
||||
AppStatus(
|
||||
packageName = packageName,
|
||||
enabled = settingsManager.isBackupEnabled(packageName),
|
||||
icon = getIcon(packageName),
|
||||
name = context.getString(stringId),
|
||||
time = metadata?.time ?: 0,
|
||||
status = metadata?.state.toAppBackupState(),
|
||||
isSpecial = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUserApps(): List<AppStatus> {
|
||||
val locale = Locale.getDefault()
|
||||
return packageService.userApps.map {
|
||||
val metadata = metadataManager.getPackageMetadata(it.packageName)
|
||||
val time = metadata?.time ?: 0
|
||||
val status = metadata?.state.toAppBackupState()
|
||||
if (status == NOT_YET_BACKED_UP) {
|
||||
Log.w(TAG, "No metadata available for: ${it.packageName}")
|
||||
}
|
||||
if (metadata?.hasApk() == false) {
|
||||
Log.w(TAG, "No APK stored for: ${it.packageName}")
|
||||
}
|
||||
AppStatus(
|
||||
packageName = it.packageName,
|
||||
enabled = settingsManager.isBackupEnabled(it.packageName),
|
||||
icon = getIcon(it.packageName),
|
||||
name = getAppName(context, it.packageName).toString(),
|
||||
time = time,
|
||||
status = status
|
||||
)
|
||||
}.sortedBy { it.name.toLowerCase(locale) }
|
||||
}
|
||||
|
||||
private fun getIcon(packageName: String): Drawable = when (packageName) {
|
||||
MAGIC_PACKAGE_MANAGER -> context.getDrawable(R.drawable.ic_launcher_default)!!
|
||||
PACKAGE_NAME_SMS -> context.getDrawable(R.drawable.ic_message)!!
|
||||
PACKAGE_NAME_SETTINGS -> context.getDrawable(R.drawable.ic_settings)!!
|
||||
PACKAGE_NAME_CALL_LOG -> context.getDrawable(R.drawable.ic_call)!!
|
||||
PACKAGE_NAME_CONTACTS -> context.getDrawable(R.drawable.ic_contacts)!!
|
||||
else -> getIconFromPackageManager(packageName)
|
||||
}
|
||||
|
||||
private fun getIconFromPackageManager(packageName: String): Drawable = try {
|
||||
pm.getApplicationIcon(packageName)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
context.getDrawable(R.drawable.ic_launcher_default)!!
|
||||
}
|
||||
|
||||
private fun PackageState?.toAppBackupState(): AppBackupState = when (this) {
|
||||
null -> NOT_YET_BACKED_UP
|
||||
PackageState.NO_DATA -> FAILED_NO_DATA
|
||||
PackageState.WAS_STOPPED -> FAILED_WAS_STOPPED
|
||||
PackageState.NOT_ALLOWED -> FAILED_NOT_ALLOWED
|
||||
PackageState.QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED
|
||||
PackageState.UNKNOWN_ERROR -> FAILED
|
||||
PackageState.APK_AND_DATA -> SUCCEEDED
|
||||
}
|
||||
|
||||
}
|
|
@ -10,6 +10,7 @@ import android.view.View.GONE
|
|||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView.ScaleType
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.DiffUtil.DiffResult
|
||||
|
@ -59,6 +60,7 @@ internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListe
|
|||
inner class AppStatusViewHolder(v: View) : AppViewHolder(v) {
|
||||
fun bind(item: AppStatus) {
|
||||
appName.text = item.name
|
||||
appIcon.scaleType = if (item.isSpecial) ScaleType.CENTER else ScaleType.FIT_CENTER
|
||||
appIcon.setImageDrawable(item.icon)
|
||||
v.background = clickableBackground
|
||||
if (editMode) {
|
||||
|
@ -110,7 +112,8 @@ data class AppStatus(
|
|||
val icon: Drawable,
|
||||
val name: String,
|
||||
val time: Long,
|
||||
val status: AppBackupState
|
||||
val status: AppBackupState,
|
||||
val isSpecial: Boolean = false
|
||||
)
|
||||
|
||||
internal class AppStatusDiff(
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package com.stevesoltys.seedvault.settings
|
||||
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.database.ContentObserver
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
|
@ -9,55 +8,34 @@ import android.net.NetworkCapabilities
|
|||
import android.net.NetworkRequest
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import android.widget.Toast.LENGTH_LONG
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.core.content.ContextCompat.getDrawable
|
||||
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.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.PackageState.WAS_STOPPED
|
||||
import com.stevesoltys.seedvault.permitDiskReads
|
||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||
import com.stevesoltys.seedvault.transport.requestBackup
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
|
||||
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_WAS_STOPPED
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState.NOT_YET_BACKED_UP
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
|
||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.ui.notification.getAppName
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
private const val USER_FULL_DATA_BACKUP_AWARE = "user_full_data_backup_aware"
|
||||
|
||||
private val TAG = SettingsViewModel::class.java.simpleName
|
||||
|
||||
internal class SettingsViewModel(
|
||||
app: Application,
|
||||
settingsManager: SettingsManager,
|
||||
keyManager: KeyManager,
|
||||
private val notificationManager: BackupNotificationManager,
|
||||
private val metadataManager: MetadataManager,
|
||||
private val packageService: PackageService
|
||||
private val appListRetriever: AppListRetriever
|
||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager) {
|
||||
|
||||
private val contentResolver = app.contentResolver
|
||||
|
@ -154,44 +132,7 @@ internal class SettingsViewModel(
|
|||
}
|
||||
|
||||
private fun getAppStatusResult(): LiveData<AppStatusResult> = liveData(Dispatchers.Default) {
|
||||
val pm = app.packageManager
|
||||
val locale = Locale.getDefault()
|
||||
val list = packageService.userApps.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}")
|
||||
NOT_YET_BACKED_UP
|
||||
}
|
||||
NO_DATA -> FAILED_NO_DATA
|
||||
WAS_STOPPED -> FAILED_WAS_STOPPED
|
||||
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,
|
||||
enabled = settingsManager.isBackupEnabled(it.packageName),
|
||||
icon = icon,
|
||||
name = getAppName(app, it.packageName).toString(),
|
||||
time = time,
|
||||
status = status
|
||||
)
|
||||
}.sortedBy { it.name.toLowerCase(locale) }
|
||||
val list = appListRetriever.getAppList()
|
||||
val oldList = mAppStatusList.value?.appStatusList ?: emptyList()
|
||||
val diff = calculateDiff(AppStatusDiff(oldList, list))
|
||||
emit(AppStatusResult(list, diff))
|
||||
|
|
10
app/src/main/res/drawable/ic_call.xml
Normal file
10
app/src/main/res/drawable/ic_call.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="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20.01,15.38c-1.23,0 -2.42,-0.2 -3.53,-0.56 -0.35,-0.12 -0.74,-0.03 -1.01,0.24l-1.57,1.97c-2.83,-1.35 -5.48,-3.9 -6.89,-6.83l1.95,-1.66c0.27,-0.28 0.35,-0.67 0.24,-1.02 -0.37,-1.11 -0.56,-2.3 -0.56,-3.53 0,-0.54 -0.45,-0.99 -0.99,-0.99H4.19C3.65,3 3,3.24 3,3.99 3,13.28 10.73,21 20.01,21c0.71,0 0.99,-0.63 0.99,-1.18v-3.45c0,-0.54 -0.45,-0.99 -0.99,-0.99z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_contacts.xml
Normal file
10
app/src/main/res/drawable/ic_contacts.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="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,0L4,0v2h16L20,0zM4,24h16v-2L4,22v2zM20,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM12,6.75c1.24,0 2.25,1.01 2.25,2.25s-1.01,2.25 -2.25,2.25S9.75,10.24 9.75,9 10.76,6.75 12,6.75zM17,17L7,17v-1.5c0,-1.67 3.33,-2.5 5,-2.5s5,0.83 5,2.5L17,17z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_message.xml
Normal file
10
app/src/main/res/drawable/ic_message.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="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_settings.xml
Normal file
10
app/src/main/res/drawable/ic_settings.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="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" />
|
||||
</vector>
|
|
@ -91,6 +91,10 @@
|
|||
|
||||
<!-- App Backup and Restore State -->
|
||||
|
||||
<string name="backup_sms">SMS text messages</string>
|
||||
<string name="backup_settings">Device settings</string>
|
||||
<string name="backup_call_log">Call history</string>
|
||||
<string name="backup_contacts">Local contacts</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 -->
|
||||
<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>
|
||||
|
|
Loading…
Add table
Reference in a new issue