diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index 17db2917..ce1f0477 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -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.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()) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt new file mode 100644 index 00000000..00783c3c --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt @@ -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 { + return getSpecialApps() + getUserApps() + } + + private fun getSpecialApps(): List { + 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 { + 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 + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt index a6e80fbc..2f6336ce 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt @@ -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( diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index 773b7f95..aadf7127 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -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 = 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)) diff --git a/app/src/main/res/drawable/ic_call.xml b/app/src/main/res/drawable/ic_call.xml new file mode 100644 index 00000000..d954d1ca --- /dev/null +++ b/app/src/main/res/drawable/ic_call.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_contacts.xml b/app/src/main/res/drawable/ic_contacts.xml new file mode 100644 index 00000000..bf94c73f --- /dev/null +++ b/app/src/main/res/drawable/ic_contacts.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_message.xml b/app/src/main/res/drawable/ic_message.xml new file mode 100644 index 00000000..a0bd37ac --- /dev/null +++ b/app/src/main/res/drawable/ic_message.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 00000000..a7c7678d --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c45c7a1c..6a9a234a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -91,6 +91,10 @@ + SMS text messages + Device settings + Call history + Local contacts Waiting to back up… Was not yet backed up