Allow user to choose which apps should get restored
This commit is contained in:
parent
4803288629
commit
905340770c
6 changed files with 376 additions and 5 deletions
|
@ -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<AppSelectionViewHolder>() {
|
||||||
|
|
||||||
|
private val diffCallback = object : ItemCallback<SelectableAppItem>() {
|
||||||
|
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<SelectableAppItem>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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_BACKUP
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED
|
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.restore.install.InstallProgressFragment
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
|
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
||||||
|
@ -28,15 +29,16 @@ class RestoreActivity : RequireProvisioningActivity() {
|
||||||
|
|
||||||
setContentView(R.layout.activity_fragment_container)
|
setContentView(R.layout.activity_fragment_container)
|
||||||
|
|
||||||
viewModel.displayFragment.observeEvent(this, { fragment ->
|
viewModel.displayFragment.observeEvent(this) { fragment ->
|
||||||
when (fragment) {
|
when (fragment) {
|
||||||
|
SELECT_APPS -> showFragment(AppSelectionFragment())
|
||||||
RESTORE_APPS -> showFragment(InstallProgressFragment())
|
RESTORE_APPS -> showFragment(InstallProgressFragment())
|
||||||
RESTORE_BACKUP -> showFragment(RestoreProgressFragment())
|
RESTORE_BACKUP -> showFragment(RestoreProgressFragment())
|
||||||
RESTORE_FILES -> showFragment(RestoreFilesFragment())
|
RESTORE_FILES -> showFragment(RestoreFilesFragment())
|
||||||
RESTORE_FILES_STARTED -> showFragment(RestoreFilesStartedFragment())
|
RESTORE_FILES_STARTED -> showFragment(RestoreFilesStartedFragment())
|
||||||
else -> throw AssertionError()
|
else -> throw AssertionError()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
showFragment(RestoreSetFragment())
|
showFragment(RestoreSetFragment())
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.stevesoltys.seedvault.BackupMonitor
|
||||||
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.crypto.KeyManager
|
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.APK_AND_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
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_BACKUP
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES_STARTED
|
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.ApkRestore
|
||||||
import com.stevesoltys.seedvault.restore.install.InstallIntentCreator
|
import com.stevesoltys.seedvault.restore.install.InstallIntentCreator
|
||||||
import com.stevesoltys.seedvault.restore.install.InstallResult
|
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.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.storage.StorageRestoreService
|
import com.stevesoltys.seedvault.storage.StorageRestoreService
|
||||||
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
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.transport.restore.RestoreCoordinator
|
||||||
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
|
||||||
|
@ -60,6 +61,7 @@ import com.stevesoltys.seedvault.ui.LiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
||||||
import com.stevesoltys.seedvault.ui.notification.getAppName
|
import com.stevesoltys.seedvault.ui.notification.getAppName
|
||||||
|
import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.catch
|
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_TIMESTAMP_START
|
||||||
import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID
|
import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID
|
||||||
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
|
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
|
|
||||||
private val TAG = RestoreViewModel::class.java.simpleName
|
private val TAG = RestoreViewModel::class.java.simpleName
|
||||||
|
|
||||||
internal const val PACKAGES_PER_CHUNK = NUM_PACKAGES_PER_TRANSACTION
|
internal const val PACKAGES_PER_CHUNK = NUM_PACKAGES_PER_TRANSACTION
|
||||||
|
|
||||||
|
internal class SelectedAppsState(
|
||||||
|
val apps: List<SelectableAppItem>,
|
||||||
|
val allSelected: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
internal class RestoreViewModel(
|
internal class RestoreViewModel(
|
||||||
app: Application,
|
app: Application,
|
||||||
settingsManager: SettingsManager,
|
settingsManager: SettingsManager,
|
||||||
|
@ -106,8 +112,13 @@ internal class RestoreViewModel(
|
||||||
private val mChosenRestorableBackup = MutableLiveData<RestorableBackup>()
|
private val mChosenRestorableBackup = MutableLiveData<RestorableBackup>()
|
||||||
internal val chosenRestorableBackup: LiveData<RestorableBackup> get() = mChosenRestorableBackup
|
internal val chosenRestorableBackup: LiveData<RestorableBackup> get() = mChosenRestorableBackup
|
||||||
|
|
||||||
|
private val mSelectedApps = MutableLiveData<SelectedAppsState>()
|
||||||
|
internal val selectedApps: LiveData<SelectedAppsState> get() = mSelectedApps
|
||||||
|
|
||||||
internal val installResult: LiveData<InstallResult> =
|
internal val installResult: LiveData<InstallResult> =
|
||||||
mChosenRestorableBackup.switchMap { backup ->
|
mChosenRestorableBackup.switchMap { backup ->
|
||||||
|
// TODO does this stay stable when re-observing this LiveData?
|
||||||
|
// TODO pass in app selection done by user
|
||||||
getInstallResult(backup)
|
getInstallResult(backup)
|
||||||
}
|
}
|
||||||
internal val installIntentCreator by lazy { InstallIntentCreator(app.packageManager) }
|
internal val installIntentCreator by lazy { InstallIntentCreator(app.packageManager) }
|
||||||
|
@ -164,6 +175,63 @@ internal class RestoreViewModel(
|
||||||
|
|
||||||
override fun onRestorableBackupClicked(restorableBackup: RestorableBackup) {
|
override fun onRestorableBackupClicked(restorableBackup: RestorableBackup) {
|
||||||
mChosenRestorableBackup.value = 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)
|
mDisplayFragment.setEvent(RESTORE_APPS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -475,5 +543,5 @@ internal class RestoreBackupResult(val errorMsg: String? = null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum class DisplayFragment {
|
internal enum class DisplayFragment {
|
||||||
RESTORE_APPS, RESTORE_BACKUP, RESTORE_FILES, RESTORE_FILES_STARTED
|
SELECT_APPS, RESTORE_APPS, RESTORE_BACKUP, RESTORE_FILES, RESTORE_FILES_STARTED
|
||||||
}
|
}
|
||||||
|
|
129
app/src/main/res/layout/fragment_restore_app_selection.xml
Normal file
129
app/src/main/res/layout/fragment_restore_app_selection.xml
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_scrollFlags="scroll|enterAlwaysCollapsed">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView"
|
||||||
|
style="@style/SudHeaderIcon"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_cloud_download"
|
||||||
|
app:tint="?android:colorAccent"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/titleView"
|
||||||
|
style="@style/SudHeaderTitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/restore_select_packages"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/imageView" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/backupNameView"
|
||||||
|
style="@style/SudDescription"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="?android:textColorTertiary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/titleView"
|
||||||
|
tools:text="Pixel 2 XL - Owner of the device" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/toggleAllTextView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="0dp"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingStart="40dp"
|
||||||
|
android:text="@string/restore_select_packages_all"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/toggleAllView"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/backupNameView"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:id="@+id/toggleAllView"
|
||||||
|
style="@style/SudContent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/backupNameView"
|
||||||
|
tools:checked="true" />
|
||||||
|
|
||||||
|
<com.google.android.material.divider.MaterialDivider
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="40dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:dividerColor="?attr/colorControlNormal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/toggleAllView" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/appList"
|
||||||
|
style="@style/SudContent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="0dp"
|
||||||
|
android:layout_marginTop="0dp"
|
||||||
|
android:layout_marginEnd="0dp"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/button"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/backupNameView"
|
||||||
|
tools:listitem="@layout/list_item_app_status" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button"
|
||||||
|
style="@style/SudPrimaryButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:text="@string/restore_backup_button"
|
||||||
|
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/appList" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -209,6 +209,8 @@
|
||||||
<string name="restore_invalid_location_message">We could not find any backups at this location.\n\nPlease choose another location that contains a %s folder.</string>
|
<string name="restore_invalid_location_message">We could not find any backups at this location.\n\nPlease choose another location that contains a %s folder.</string>
|
||||||
<string name="restore_set_error">An error occurred while loading the backups.</string>
|
<string name="restore_set_error">An error occurred while loading the backups.</string>
|
||||||
<string name="restore_set_empty_result">No suitable backups found at given location.\n\nThis is most likely due to a wrong recovery code or a storage error.</string>
|
<string name="restore_set_empty_result">No suitable backups found at given location.\n\nThis is most likely due to a wrong recovery code or a storage error.</string>
|
||||||
|
<string name="restore_select_packages">Select apps to restore</string>
|
||||||
|
<string name="restore_select_packages_all">All of the following apps</string>
|
||||||
<string name="restore_installing_packages">Re-installing apps</string>
|
<string name="restore_installing_packages">Re-installing apps</string>
|
||||||
<string name="restore_app_status_installing">Re-installing</string>
|
<string name="restore_app_status_installing">Re-installing</string>
|
||||||
<string name="restore_app_status_installed">Re-installed</string>
|
<string name="restore_app_status_installed">Re-installed</string>
|
||||||
|
|
Loading…
Reference in a new issue