Skip to content

Commit

Permalink
WIP sort app selection for restore better
Browse files Browse the repository at this point in the history
  • Loading branch information
grote committed May 23, 2024
1 parent 9eda7bb commit 05b9a49
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,67 @@ import android.view.LayoutInflater
import android.view.View
import android.view.View.INVISIBLE
import android.view.ViewGroup
import android.widget.ImageView.ScaleType.CENTER
import android.widget.ImageView.ScaleType.FIT_CENTER
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import androidx.recyclerview.widget.RecyclerView
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
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SYSTEM
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

sealed interface AppSelectionItem

internal class AppSelectionSection(@StringRes val titleRes: Int) : AppSelectionItem

internal data class SelectableAppItem(
val packageName: String,
val metadata: PackageMetadata,
val selected: Boolean,
val hasIcon: Boolean? = null,
) {
) : AppSelectionItem {
val name: String get() = metadata.name?.toString() ?: packageName
}

internal class AppSelectionAdapter(
val scope: CoroutineScope,
val iconLoader: suspend (String, (Bitmap) -> Unit) -> Unit,
val iconLoader: suspend (SelectableAppItem, (Bitmap) -> Unit) -> Unit,
val listener: (SelectableAppItem) -> Unit,
) : Adapter<AppSelectionViewHolder>() {
) : Adapter<RecyclerView.ViewHolder>() {

private val diffCallback = object : ItemCallback<SelectableAppItem>() {
private val diffCallback = object : ItemCallback<AppSelectionItem>() {
override fun areItemsTheSame(
oldItem: SelectableAppItem,
newItem: SelectableAppItem,
): Boolean = oldItem.packageName == newItem.packageName
oldItem: AppSelectionItem,
newItem: AppSelectionItem,
): Boolean {
return if (oldItem is AppSelectionSection && newItem is AppSelectionSection) {
oldItem.titleRes == newItem.titleRes
} else if (oldItem is SelectableAppItem && newItem is SelectableAppItem) {
oldItem.packageName == newItem.packageName
} else {
false
}
}

override fun areContentsTheSame(
old: SelectableAppItem,
new: SelectableAppItem,
old: AppSelectionItem,
new: AppSelectionItem,
): Boolean {
return old.selected == new.selected && old.hasIcon == new.hasIcon
return if (old is AppSelectionSection && new is AppSelectionSection) {
true
} else if (old is SelectableAppItem && new is SelectableAppItem) {
old.selected == new.selected && old.hasIcon == new.hasIcon
} else {
false
}
}
}
private val differ = AsyncListDiffer(this, diffCallback)
Expand All @@ -55,27 +78,67 @@ internal class AppSelectionAdapter(

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 getItemViewType(position: Int): Int = when (differ.currentList[position]) {
is SelectableAppItem -> 0
is AppSelectionSection -> 1
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
0 -> {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item_app_status, parent, false)
SelectableAppViewHolder(v)
}

1 -> {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item_app_section_title, parent, false)
AppSelectionSectionViewHolder(v)
}

else -> throw AssertionError("unknown view type")
}
}

override fun getItemCount() = differ.currentList.size

override fun onBindViewHolder(holder: AppSelectionViewHolder, position: Int) {
holder.bind(differ.currentList[position])
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is SelectableAppViewHolder -> {
holder.bind(differ.currentList[position] as SelectableAppItem)
}

is AppSelectionSectionViewHolder -> {
holder.bind(differ.currentList[position] as AppSelectionSection)
}
}
}

fun submitList(items: List<AppSelectionItem>) {
val itemsWithSections = items.toMutableList().apply {
val i = indexOfLast {
it as SelectableAppItem
it.packageName == PACKAGE_NAME_SYSTEM
}
add(i + 1, AppSelectionSection(R.string.backup_section_user))
add(0, AppSelectionSection(R.string.backup_section_system))
}
differ.submitList(itemsWithSections)
}

fun submitList(items: List<SelectableAppItem>) {
differ.submitList(items)
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is SelectableAppViewHolder) holder.iconJob?.cancel()
}

override fun onViewRecycled(holder: AppSelectionViewHolder) {
holder.iconJob?.cancel()
class AppSelectionSectionViewHolder(v: View) : RecyclerView.ViewHolder(v) {
private val titleView: TextView = v as TextView
fun bind(item: AppSelectionSection) {
titleView.setText(item.titleRes)
}
}

internal inner class AppSelectionViewHolder(v: View) : AppViewHolder(v) {
internal inner class SelectableAppViewHolder(v: View) : AppViewHolder(v) {

var iconJob: Job? = null

Expand All @@ -99,7 +162,9 @@ internal class AppSelectionAdapter(
} else if (item.hasIcon) {
appIcon.alpha = 0.5f
iconJob = scope.launch {
iconLoader(item.packageName) { bitmap ->
iconLoader(item) { bitmap ->
val isSpecial = item.metadata.system && !item.metadata.isLaunchableSystemApp
appIcon.scaleType = if (isSpecial) CENTER else FIT_CENTER
appIcon.setImageBitmap(bitmap)
appIcon.alpha = 1f
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ class AppSelectionFragment : Fragment() {
}
}

private suspend fun loadIcon(packageName: String, callback: (Bitmap) -> Unit) {
viewModel.loadIcon(packageName, callback)
private suspend fun loadIcon(item: SelectableAppItem, callback: (Bitmap) -> Unit) {
viewModel.loadIcon(item, callback)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import android.os.UserHandle
import android.util.Log
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.appcompat.content.res.AppCompatResources.getDrawable
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
Expand All @@ -28,6 +30,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.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
Expand Down Expand Up @@ -60,8 +63,10 @@ import com.stevesoltys.seedvault.ui.AppBackupState.NOT_YET_BACKED_UP
import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
import com.stevesoltys.seedvault.ui.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SYSTEM
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
import com.stevesoltys.seedvault.ui.notification.getAppName
import com.stevesoltys.seedvault.ui.systemData
import com.stevesoltys.seedvault.worker.FILE_BACKUP_ICONS
import com.stevesoltys.seedvault.worker.IconManager
import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION
Expand All @@ -78,6 +83,7 @@ import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_TIMESTA
import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
import java.util.LinkedList
import java.util.Locale

private val TAG = RestoreViewModel::class.java.simpleName

Expand Down Expand Up @@ -183,12 +189,32 @@ internal class RestoreViewModel(
// filter and sort app items for display
val items = restorableBackup.packageMetadataMap.mapNotNull { (packageName, metadata) ->
if (metadata.time == 0L && !metadata.hasApk()) null
else if (packageName == MAGIC_PACKAGE_MANAGER) null
else if (metadata.system && !metadata.isLaunchableSystemApp) 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)
}.sortedBy {
it.name.lowercase(Locale.getDefault())
}.toMutableList()
val systemDataItems = systemData.mapNotNull { (packageName, data) ->
val metadata = restorableBackup.packageMetadataMap[packageName]
?: return@mapNotNull null
if (metadata.time == 0L && !metadata.hasApk()) return@mapNotNull null
val name = app.getString(data.nameRes)
SelectableAppItem(packageName, metadata.copy(name = name), true, hasIcon = true)
}
val systemItem = SelectableAppItem(
packageName = PACKAGE_NAME_SYSTEM,
metadata = PackageMetadata(
time = restorableBackup.packageMetadataMap.values.maxOf {
if (it.system) it.time else -1
},
system = true,
name = app.getString(R.string.backup_system_apps),
),
selected = true,
hasIcon = true,
)
items.add(0, systemItem)
items.addAll(0, systemDataItems)
mSelectedApps.value =
SelectedAppsState(apps = items, allSelected = true, iconsLoaded = false)
// download icons
Expand All @@ -205,7 +231,7 @@ internal class RestoreViewModel(
}
// update state, so it knows that icons have loaded
val updatedItems = items.map { item ->
item.copy(hasIcon = item.packageName in packagesWithIcons)
item.copy(hasIcon = item.hasIcon ?: false || item.packageName in packagesWithIcons)
}
val newState =
SelectedAppsState(updatedItems, allSelected = true, iconsLoaded = true)
Expand All @@ -214,8 +240,18 @@ internal class RestoreViewModel(
mDisplayFragment.setEvent(SELECT_APPS)
}

suspend fun loadIcon(packageName: String, callback: (Bitmap) -> Unit) {
iconManager.loadIcon(packageName, callback)
suspend fun loadIcon(item: SelectableAppItem, callback: (Bitmap) -> Unit) {
if (item.packageName == PACKAGE_NAME_SYSTEM) {
val bitmap = getDrawable(app, R.drawable.ic_app_settings)!!.toBitmap()
callback(bitmap)
} else if (item.metadata.system && !item.metadata.isLaunchableSystemApp &&
item.packageName in systemData.keys
) {
val bitmap = getDrawable(app, systemData[item.packageName]!!.iconRes)!!.toBitmap()
callback(bitmap)
} else {
iconManager.loadIcon(item.packageName, callback)
}
}

fun onCheckAllAppsClicked() {
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/stevesoltys/seedvault/ui/SystemData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal const val PACKAGE_NAME_SMS = "com.android.providers.telephony"
internal const val PACKAGE_NAME_SETTINGS = "com.android.providers.settings"
internal const val PACKAGE_NAME_CALL_LOG = "com.android.calllogbackup"
internal const val PACKAGE_NAME_CONTACTS = "org.calyxos.backup.contacts"
internal const val PACKAGE_NAME_SYSTEM = "@system@"

val systemData = mapOf(
PACKAGE_NAME_SMS to SystemData(R.string.backup_sms, R.drawable.ic_message),
Expand All @@ -23,5 +24,5 @@ val systemData = mapOf(

data class SystemData(
@StringRes val nameRes: Int,
@DrawableRes val iconRes: Int?,
@DrawableRes val iconRes: Int,
)
12 changes: 12 additions & 0 deletions app/src/main/res/drawable/ic_app_settings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<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="M21.81,12.74l-0.82,-0.63v-0.22l0.8,-0.63c0.16,-0.12 0.2,-0.34 0.1,-0.51l-0.85,-1.48c-0.07,-0.13 -0.21,-0.2 -0.35,-0.2 -0.05,0 -0.1,0.01 -0.15,0.03l-0.95,0.38c-0.08,-0.05 -0.11,-0.07 -0.19,-0.11l-0.15,-1.01c-0.03,-0.21 -0.2,-0.36 -0.4,-0.36h-1.71c-0.2,0 -0.37,0.15 -0.4,0.34l-0.14,1.01c-0.03,0.02 -0.07,0.03 -0.1,0.05l-0.09,0.06 -0.95,-0.38c-0.05,-0.02 -0.1,-0.03 -0.15,-0.03 -0.14,0 -0.27,0.07 -0.35,0.2l-0.85,1.48c-0.1,0.17 -0.06,0.39 0.1,0.51l0.8,0.63v0.23l-0.8,0.63c-0.16,0.12 -0.2,0.34 -0.1,0.51l0.85,1.48c0.07,0.13 0.21,0.2 0.35,0.2 0.05,0 0.1,-0.01 0.15,-0.03l0.95,-0.37c0.08,0.05 0.12,0.07 0.2,0.11l0.15,1.01c0.03,0.2 0.2,0.34 0.4,0.34h1.71c0.2,0 0.37,-0.15 0.4,-0.34l0.15,-1.01c0.03,-0.02 0.07,-0.03 0.1,-0.05l0.09,-0.06 0.95,0.38c0.05,0.02 0.1,0.03 0.15,0.03 0.14,0 0.27,-0.07 0.35,-0.2l0.85,-1.48c0.1,-0.17 0.06,-0.39 -0.1,-0.51zM18,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM17,17h2v4c0,1.1 -0.9,2 -2,2H7c-1.1,0 -2,-0.9 -2,-2V3c0,-1.1 0.9,-2 2,-2h10c1.1,0 2,0.9 2,2v4h-2V6H7v12h10v-1z" />

</vector>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
<string name="backup_settings">Device settings</string>
<string name="backup_call_log">Call history</string>
<string name="backup_contacts">Local contacts</string>
<string name="backup_system_apps">System apps</string>
<string name="backup_section_user">Apps</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>
Expand Down

0 comments on commit 05b9a49

Please sign in to comment.