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_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())
|
||||
|
|
|
@ -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<SelectableAppItem>,
|
||||
val allSelected: Boolean,
|
||||
)
|
||||
|
||||
internal class RestoreViewModel(
|
||||
app: Application,
|
||||
settingsManager: SettingsManager,
|
||||
|
@ -106,8 +112,13 @@ internal class RestoreViewModel(
|
|||
private val mChosenRestorableBackup = MutableLiveData<RestorableBackup>()
|
||||
internal val chosenRestorableBackup: LiveData<RestorableBackup> get() = mChosenRestorableBackup
|
||||
|
||||
private val mSelectedApps = MutableLiveData<SelectedAppsState>()
|
||||
internal val selectedApps: LiveData<SelectedAppsState> get() = mSelectedApps
|
||||
|
||||
internal val installResult: LiveData<InstallResult> =
|
||||
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
|
||||
}
|
||||
|
|
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_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_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_app_status_installing">Re-installing</string>
|
||||
<string name="restore_app_status_installed">Re-installed</string>
|
||||
|
|
Loading…
Reference in a new issue