From 16813395c7caeae0b9e42da5f70326f3f934264d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 20 May 2024 13:25:08 -0300 Subject: [PATCH 01/30] Change app excludes from switches to checkboxes apparently this is closer to the material design specs: https://m2.material.io/components/checkboxes#usage --- .../stevesoltys/seedvault/settings/AppStatusAdapter.kt | 10 +++++----- .../java/com/stevesoltys/seedvault/ui/AppViewHolder.kt | 4 ++-- app/src/main/res/layout/list_item_app_status.xml | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) 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 63401588..960fbf88 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt @@ -96,15 +96,15 @@ internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListe v.background = clickableBackground if (editMode) { v.setOnClickListener { - switchView.toggle() - item.enabled = switchView.isChecked + checkBox.toggle() + item.enabled = checkBox.isChecked toggleListener.onAppStatusToggled(item) } appInfo.visibility = GONE appStatus.visibility = INVISIBLE progressBar.visibility = INVISIBLE - switchView.visibility = VISIBLE - switchView.isChecked = item.enabled + checkBox.visibility = VISIBLE + checkBox.isChecked = item.enabled } else { v.setOnClickListener(null) v.setOnLongClickListener { @@ -130,7 +130,7 @@ internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListe } appInfo.visibility = VISIBLE } - switchView.visibility = INVISIBLE + checkBox.visibility = INVISIBLE } // show disabled items differently showEnabled(item.enabled) diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/AppViewHolder.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/AppViewHolder.kt index 19f4f100..e5448d0f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/AppViewHolder.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/AppViewHolder.kt @@ -15,7 +15,7 @@ import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.switchmaterial.SwitchMaterial +import com.google.android.material.checkbox.MaterialCheckBox import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.ui.AppBackupState.FAILED import com.stevesoltys.seedvault.ui.AppBackupState.IN_PROGRESS @@ -32,7 +32,7 @@ internal abstract class AppViewHolder(protected val v: View) : RecyclerView.View protected val appInfo: TextView = v.requireViewById(R.id.appInfo) protected val appStatus: ImageView = v.requireViewById(R.id.appStatus) protected val progressBar: ProgressBar = v.requireViewById(R.id.progressBar) - protected val switchView: SwitchMaterial = v.requireViewById(R.id.switchView) + protected val checkBox: MaterialCheckBox = v.requireViewById(R.id.checkboxView) init { // don't use clickable background by default diff --git a/app/src/main/res/layout/list_item_app_status.xml b/app/src/main/res/layout/list_item_app_status.xml index 8ff13961..29d288b6 100644 --- a/app/src/main/res/layout/list_item_app_status.xml +++ b/app/src/main/res/layout/list_item_app_status.xml @@ -8,9 +8,9 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="40dp" - android:layout_marginEnd="40dp" + android:layout_marginHorizontal="16dp" android:background="?android:selectableItemBackground" + android:paddingHorizontal="24dp" android:paddingTop="8dp" android:paddingBottom="8dp" android:screenReaderFocusable="true"> @@ -35,7 +35,7 @@ android:layout_marginEnd="16dp" android:textColor="?android:textColorPrimary" app:layout_constraintBottom_toTopOf="@+id/appInfo" - app:layout_constraintEnd_toStartOf="@+id/switchView" + app:layout_constraintEnd_toStartOf="@+id/checkboxView" app:layout_constraintStart_toEndOf="@+id/appIcon" app:layout_constraintTop_toTopOf="parent" tools:text="Seedvault Backup" /> @@ -72,8 +72,8 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> - Date: Mon, 20 May 2024 16:11:42 -0300 Subject: [PATCH 02/30] Show other (launchable) system apps in backup status --- .../seedvault/settings/AppListRetriever.kt | 23 ++++++++++++++++--- .../seedvault/settings/AppStatusFragment.kt | 8 +++---- .../seedvault/settings/SettingsViewModel.kt | 3 +++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt index 5da1c7a9..b03a27d0 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt @@ -7,7 +7,11 @@ package com.stevesoltys.seedvault.settings import android.annotation.StringRes import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_MAIN +import android.content.Intent.CATEGORY_LAUNCHER import android.content.pm.PackageManager +import android.content.pm.PackageManager.MATCH_SYSTEM_ONLY import android.graphics.drawable.Drawable import android.util.Log import androidx.annotation.WorkerThread @@ -80,6 +84,10 @@ internal class AppListRetriever( Pair(PACKAGE_NAME_CALL_LOG, R.string.backup_call_log), Pair(PACKAGE_NAME_CONTACTS, R.string.backup_contacts) ) + // filter intent for apps with a launcher activity + val i = Intent(ACTION_MAIN).apply { + addCategory(CATEGORY_LAUNCHER) + } return specialPackages.map { (packageName, stringId) -> val metadata = metadataManager.getPackageMetadata(packageName) val status = if (packageName == PACKAGE_NAME_CONTACTS && metadata?.state == null) { @@ -97,6 +105,18 @@ internal class AppListRetriever( status = status, isSpecial = true ) + } + context.packageManager.queryIntentActivities(i, MATCH_SYSTEM_ONLY).map { + val packageName = it.activityInfo.packageName + val metadata = metadataManager.getPackageMetadata(packageName) + AppStatus( + packageName = packageName, + enabled = settingsManager.isBackupEnabled(packageName), + icon = getIcon(packageName), + name = it.loadLabel(context.packageManager).toString(), + time = metadata?.time ?: 0, + size = metadata?.size, + status = metadata?.state.toAppBackupState(), + ) } } @@ -109,9 +129,6 @@ internal class AppListRetriever( 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), diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusFragment.kt index eb4ac7d5..25c5aad1 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusFragment.kt @@ -61,10 +61,10 @@ class AppStatusFragment : Fragment(), AppStatusToggleListener { } progressBar.visibility = VISIBLE - viewModel.appStatusList.observe(viewLifecycleOwner, { result -> + viewModel.appStatusList.observe(viewLifecycleOwner) { result -> adapter.update(result.appStatusList, result.diff) progressBar.visibility = INVISIBLE - }) + } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -73,10 +73,10 @@ class AppStatusFragment : Fragment(), AppStatusToggleListener { appEditMenuItem = menu.findItem(R.id.edit_app_blacklist) // observe edit mode changes here where we are sure to have the MenuItem - viewModel.appEditMode.observe(viewLifecycleOwner, { enabled -> + viewModel.appEditMode.observe(viewLifecycleOwner) { enabled -> appEditMenuItem.isChecked = enabled adapter.setEditMode(enabled) - }) + } } override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { 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 5be48bfd..a62ff3f4 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -97,6 +97,9 @@ internal class SettingsViewModel( private val mAppStatusList = lastBackupTime.switchMap { // updates app list when lastBackupTime changes + // FIXME: Since we are currently updating that time a lot, + // re-fetching everything on each change hammers the system hard + // which can cause android.os.DeadObjectException getAppStatusResult() } internal val appStatusList: LiveData = mAppStatusList From 905340770caf1c411d238ebd075fd7a799638540 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 20 May 2024 15:16:46 -0300 Subject: [PATCH 03/30] Allow user to choose which apps should get restored --- .../seedvault/restore/AppSelectionAdapter.kt | 100 ++++++++++++++ .../seedvault/restore/AppSelectionFragment.kt | 70 ++++++++++ .../seedvault/restore/RestoreActivity.kt | 6 +- .../seedvault/restore/RestoreViewModel.kt | 74 +++++++++- .../layout/fragment_restore_app_selection.xml | 129 ++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 6 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionAdapter.kt create mode 100644 app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionFragment.kt create mode 100644 app/src/main/res/layout/fragment_restore_app_selection.xml diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionAdapter.kt new file mode 100644 index 00000000..4e95a63a --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionAdapter.kt @@ -0,0 +1,100 @@ +package com.stevesoltys.seedvault.restore + +import android.text.format.DateUtils +import android.text.format.Formatter +import android.view.LayoutInflater +import android.view.View +import android.view.View.INVISIBLE +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil.ItemCallback +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.VISIBLE +import com.stevesoltys.seedvault.R +import com.stevesoltys.seedvault.metadata.PackageMetadata +import com.stevesoltys.seedvault.restore.AppSelectionAdapter.AppSelectionViewHolder +import com.stevesoltys.seedvault.ui.AppViewHolder + +internal data class SelectableAppItem( + val packageName: String, + val metadata: PackageMetadata, + val selected: Boolean, +) { + val name: String get() = packageName +} + +internal class AppSelectionAdapter( + val listener: (SelectableAppItem) -> Unit, +) : Adapter() { + + private val diffCallback = object : ItemCallback() { + override fun areItemsTheSame( + oldItem: SelectableAppItem, + newItem: SelectableAppItem, + ): Boolean = oldItem.packageName == newItem.packageName + + override fun areContentsTheSame( + old: SelectableAppItem, + new: SelectableAppItem, + ): Boolean { + return old.selected == new.selected + } + } + private val differ = AsyncListDiffer(this, diffCallback) + + init { + setHasStableIds(true) + } + + override fun getItemId(position: Int): Long = position.toLong() // items never get added/removed + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppSelectionViewHolder { + val v = LayoutInflater.from(parent.context) + .inflate(R.layout.list_item_app_status, parent, false) + return AppSelectionViewHolder(v) + } + + override fun getItemCount() = differ.currentList.size + + override fun onBindViewHolder(holder: AppSelectionViewHolder, position: Int) { + holder.bind(differ.currentList[position]) + } + + fun submitList(items: List) { + differ.submitList(items) + } + + internal inner class AppSelectionViewHolder(v: View) : AppViewHolder(v) { + fun bind(item: SelectableAppItem) { + v.background = clickableBackground + v.setOnClickListener { + checkBox.toggle() + } + + checkBox.setOnCheckedChangeListener(null) + checkBox.isChecked = item.selected + checkBox.setOnCheckedChangeListener { _, _ -> + listener(item) + } + checkBox.visibility = VISIBLE + progressBar.visibility = INVISIBLE + + appIcon.setImageResource(R.drawable.ic_launcher_default) + appName.text = item.packageName + val time = if (item.metadata.time > 0) DateUtils.getRelativeTimeSpanString( + item.metadata.time, + System.currentTimeMillis(), + DateUtils.HOUR_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ) else v.context.getString(R.string.settings_backup_last_backup_never) + val size = if (item.metadata.size == null) "" else "(" + Formatter.formatShortFileSize( + v.context, + item.metadata.size ?: 0 + ) + ")" + appInfo.text = + v.context.getString(R.string.settings_backup_status_summary, "$time $size") + appInfo.visibility = VISIBLE + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionFragment.kt new file mode 100644 index 00000000..72aa3797 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionFragment.kt @@ -0,0 +1,70 @@ +package com.stevesoltys.seedvault.restore + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.checkbox.MaterialCheckBox +import com.stevesoltys.seedvault.R +import org.koin.androidx.viewmodel.ext.android.sharedViewModel + +class AppSelectionFragment : Fragment() { + + private val viewModel: RestoreViewModel by sharedViewModel() + + private val layoutManager = LinearLayoutManager(context) + private val adapter = AppSelectionAdapter { item -> + viewModel.onAppSelected(item) + } + + private lateinit var backupNameView: TextView + private lateinit var toggleAllTextView: TextView + private lateinit var toggleAllView: MaterialCheckBox + private lateinit var appList: RecyclerView + private lateinit var button: Button + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + val v: View = inflater.inflate(R.layout.fragment_restore_app_selection, container, false) + + backupNameView = v.requireViewById(R.id.backupNameView) + toggleAllTextView = v.requireViewById(R.id.toggleAllTextView) + toggleAllView = v.requireViewById(R.id.toggleAllView) + appList = v.requireViewById(R.id.appList) + button = v.requireViewById(R.id.button) + + return v + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + toggleAllTextView.setOnClickListener { + viewModel.onCheckAllAppsClicked() + } + toggleAllView.setOnClickListener { + viewModel.onCheckAllAppsClicked() + } + + appList.apply { + layoutManager = this@AppSelectionFragment.layoutManager + adapter = this@AppSelectionFragment.adapter + } + button.setOnClickListener { viewModel.onNextClickedAfterSelectingApps() } + + viewModel.chosenRestorableBackup.observe(viewLifecycleOwner) { restorableBackup -> + backupNameView.text = restorableBackup.name + } + viewModel.selectedApps.observe(viewLifecycleOwner) { state -> + adapter.submitList(state.apps) + toggleAllView.isChecked = state.allSelected + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt index 4e8c1819..5b5c30bd 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreActivity.kt @@ -12,6 +12,7 @@ import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED +import com.stevesoltys.seedvault.restore.DisplayFragment.SELECT_APPS import com.stevesoltys.seedvault.restore.install.InstallProgressFragment import com.stevesoltys.seedvault.ui.RequireProvisioningActivity import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel @@ -28,15 +29,16 @@ class RestoreActivity : RequireProvisioningActivity() { setContentView(R.layout.activity_fragment_container) - viewModel.displayFragment.observeEvent(this, { fragment -> + viewModel.displayFragment.observeEvent(this) { fragment -> when (fragment) { + SELECT_APPS -> showFragment(AppSelectionFragment()) RESTORE_APPS -> showFragment(InstallProgressFragment()) RESTORE_BACKUP -> showFragment(RestoreProgressFragment()) RESTORE_FILES -> showFragment(RestoreFilesFragment()) RESTORE_FILES_STARTED -> showFragment(RestoreFilesStartedFragment()) else -> throw AssertionError() } - }) + } if (savedInstanceState == null) { showFragment(RestoreSetFragment()) diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt index 5d8905d9..ea055691 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt @@ -27,6 +27,7 @@ import com.stevesoltys.seedvault.BackupMonitor import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.crypto.KeyManager +import com.stevesoltys.seedvault.metadata.PackageMetadataMap 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 @@ -38,6 +39,7 @@ import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED +import com.stevesoltys.seedvault.restore.DisplayFragment.SELECT_APPS import com.stevesoltys.seedvault.restore.install.ApkRestore import com.stevesoltys.seedvault.restore.install.InstallIntentCreator import com.stevesoltys.seedvault.restore.install.InstallResult @@ -45,7 +47,6 @@ import com.stevesoltys.seedvault.restore.install.isInstalled import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.storage.StorageRestoreService import com.stevesoltys.seedvault.transport.TRANSPORT_ID -import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator import com.stevesoltys.seedvault.ui.AppBackupState import com.stevesoltys.seedvault.ui.AppBackupState.FAILED @@ -60,6 +61,7 @@ import com.stevesoltys.seedvault.ui.LiveEvent import com.stevesoltys.seedvault.ui.MutableLiveEvent import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.notification.getAppName +import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.catch @@ -72,13 +74,17 @@ import org.calyxos.backup.storage.api.StorageBackup import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_TIMESTAMP_START import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID import org.calyxos.backup.storage.ui.restore.SnapshotViewModel -import java.lang.IllegalStateException import java.util.LinkedList private val TAG = RestoreViewModel::class.java.simpleName internal const val PACKAGES_PER_CHUNK = NUM_PACKAGES_PER_TRANSACTION +internal class SelectedAppsState( + val apps: List, + val allSelected: Boolean, +) + internal class RestoreViewModel( app: Application, settingsManager: SettingsManager, @@ -106,8 +112,13 @@ internal class RestoreViewModel( private val mChosenRestorableBackup = MutableLiveData() internal val chosenRestorableBackup: LiveData get() = mChosenRestorableBackup + private val mSelectedApps = MutableLiveData() + internal val selectedApps: LiveData get() = mSelectedApps + internal val installResult: LiveData = mChosenRestorableBackup.switchMap { backup -> + // TODO does this stay stable when re-observing this LiveData? + // TODO pass in app selection done by user getInstallResult(backup) } internal val installIntentCreator by lazy { InstallIntentCreator(app.packageManager) } @@ -164,6 +175,63 @@ internal class RestoreViewModel( override fun onRestorableBackupClicked(restorableBackup: RestorableBackup) { mChosenRestorableBackup.value = restorableBackup + val items = restorableBackup.packageMetadataMap.mapNotNull { (packageName, metadata) -> + if (metadata.time == 0L && !metadata.hasApk()) null + else if (packageName == MAGIC_PACKAGE_MANAGER) null + else SelectableAppItem(packageName, metadata, true) + }.sortedWith { i1, i2 -> + if (i1.metadata.system == i2.metadata.system) i1.name.compareTo(i2.name, true) + else i1.metadata.system.compareTo(i2.metadata.system) + } + mSelectedApps.value = SelectedAppsState(items, true) + mDisplayFragment.setEvent(SELECT_APPS) + } + + fun onCheckAllAppsClicked() { + val apps = selectedApps.value?.apps ?: return + val allSelected = apps.all { it.selected } + if (allSelected) { + // unselect all + val newApps = apps.map { if (it.selected) it.copy(selected = false) else it } + mSelectedApps.value = SelectedAppsState(newApps, false) + } else { + // select all + val newApps = apps.map { if (!it.selected) it.copy(selected = true) else it } + mSelectedApps.value = SelectedAppsState(newApps, true) + } + } + + fun onAppSelected(item: SelectableAppItem) { + val apps = selectedApps.value?.apps?.toMutableList() ?: return + val iterator = apps.listIterator() + var allSelected = true + while (iterator.hasNext()) { + val app = iterator.next() + if (app.packageName == item.packageName) { + iterator.set(item.copy(selected = !item.selected)) + allSelected = allSelected && !item.selected + } else { + allSelected = allSelected && app.selected + } + } + mSelectedApps.value = SelectedAppsState(apps, allSelected) + } + + internal fun onNextClickedAfterSelectingApps() { + val backup = chosenRestorableBackup.value ?: error("No chosen backup") + // map packages names to selection state + val apps = selectedApps.value?.apps?.associate { + Pair(it.packageName, it.selected) + } ?: error("no selected apps") + // filter out unselected packages + val packages = backup.packageMetadataMap.filterKeys { packageName -> + apps[packageName] != true // packages that weren't found, won't get filtered + } as PackageMetadataMap + // replace original chosen backup with unselected packages removed + mChosenRestorableBackup.value = backup.copy( + backupMetadata = backup.backupMetadata.copy(packageMetadataMap = packages), + ) + // tell UI to move to InstallFragment mDisplayFragment.setEvent(RESTORE_APPS) } @@ -475,5 +543,5 @@ internal class RestoreBackupResult(val errorMsg: String? = null) { } internal enum class DisplayFragment { - RESTORE_APPS, RESTORE_BACKUP, RESTORE_FILES, RESTORE_FILES_STARTED + SELECT_APPS, RESTORE_APPS, RESTORE_BACKUP, RESTORE_FILES, RESTORE_FILES_STARTED } diff --git a/app/src/main/res/layout/fragment_restore_app_selection.xml b/app/src/main/res/layout/fragment_restore_app_selection.xml new file mode 100644 index 00000000..36f68bf5 --- /dev/null +++ b/app/src/main/res/layout/fragment_restore_app_selection.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +