Skip to content

Commit

Permalink
feat: Implement hot reloading for app list
Browse files Browse the repository at this point in the history
Change-Id: Ie95cfafad646933a8d9b9ab418ee54f0964b9161
  • Loading branch information
XayahSuSuSu committed Jun 29, 2024
1 parent abc9d39 commit 9b8c535
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.UserHandle
import com.xayah.core.data.R
import com.xayah.core.data.util.srcDir
import com.xayah.core.database.dao.PackageDao
Expand Down Expand Up @@ -222,6 +223,91 @@ class PackageRepository @Inject constructor(
else -> sortByAlphabetNew(sortType)
}

private suspend fun handlePackage(
pm: PackageManager,
info: android.content.pm.PackageInfo,
checkKeystore: Boolean, userId: Int,
userHandle: UserHandle?,
hasPassedOneDay: Boolean
): PackageEntity {
val permissions = PermissionUtil.getPermission(packageManager = pm, packageInfo = info)
val uid = info.applicationInfo.uid
val hasKeystore = if (checkKeystore) PackageUtil.hasKeystore(context.readCustomSUFile().first(), uid) else false
val ssaid = rootService.getPackageSsaidAsUser(packageName = info.packageName, uid = uid, userId = userId)
val iconPath = pathUtil.getPackageIconPath(info.packageName)
val iconExists = rootService.exists(iconPath)
if (iconExists.not() || (iconExists && hasPassedOneDay)) {
runCatching {
val icon = info.applicationInfo.loadIcon(pm)
BaseUtil.writeIcon(icon = icon, dst = iconPath)
}.withLog()
}
val packageInfo = PackageInfo(
label = info.applicationInfo.loadLabel(pm).toString(),
versionName = info.versionName ?: "",
versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
info.longVersionCode
} else {
info.versionCode.toLong()
},
flags = info.applicationInfo.flags,
firstInstallTime = info.firstInstallTime,
)
val extraInfo = PackageExtraInfo(
uid = uid,
labels = listOf(),
hasKeystore = hasKeystore,
permissions = permissions,
ssaid = ssaid,
blocked = false,
activated = false,
existed = true,
)
val indexInfo = PackageIndexInfo(
opType = OpType.BACKUP,
packageName = info.packageName,
userId = userId,
compressionType = context.readCompressionType().first(),
preserveId = DefaultPreserveId,
cloud = "",
backupDir = ""
)
val packageEntity =
getPackage(packageName = info.packageName, opType = OpType.BACKUP, userId = userId, preserveId = DefaultPreserveId, cloud = "", backupDir = "")
?: PackageEntity(
id = 0,
indexInfo = indexInfo,
packageInfo = packageInfo,
extraInfo = extraInfo,
dataStates = PackageDataStates(),
storageStats = PackageStorageStats(),
dataStats = PackageDataStats(),
displayStats = PackageDataStats(),
)
// Update if exists.
packageEntity.apply {
this.packageInfo = packageInfo
this.extraInfo.uid = uid
this.extraInfo.hasKeystore = hasKeystore
this.extraInfo.permissions = permissions
this.extraInfo.ssaid = ssaid
this.extraInfo.existed = true
}
if (userHandle != null) {
rootService.queryStatsForPackage(info, userHandle).also { stats ->
if (stats != null) {
packageEntity.apply {
this.storageStats.appBytes = stats.appBytes
this.storageStats.cacheBytes = stats.cacheBytes
this.storageStats.dataBytes = stats.dataBytes
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) this.storageStats.externalCacheBytes = stats.externalCacheBytes
}
}
}
}
return packageEntity
}

@SuppressLint("StringFormatInvalid")
suspend fun refresh(refreshState: MutableStateFlow<RefreshState>) = run {
packageDao.clearExisted(opType = OpType.BACKUP)
Expand All @@ -248,81 +334,7 @@ class PackageRepository @Inject constructor(
installedPackages.forEachIndexed { index, info ->
val isSystemApp = (info.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0
if (loadSystemApps || isSystemApp.not()) {
val permissions = PermissionUtil.getPermission(packageManager = pm, packageInfo = info)
val uid = info.applicationInfo.uid
val hasKeystore = if (checkKeystore) PackageUtil.hasKeystore(context.readCustomSUFile().first(), uid) else false
val ssaid = rootService.getPackageSsaidAsUser(packageName = info.packageName, uid = uid, userId = userId)
val iconPath = pathUtil.getPackageIconPath(info.packageName)
val iconExists = rootService.exists(iconPath)
if (iconExists.not() || (iconExists && hasPassedOneDay)) {
runCatching {
val icon = info.applicationInfo.loadIcon(pm)
BaseUtil.writeIcon(icon = icon, dst = iconPath)
}.withLog()
}
val packageInfo = PackageInfo(
label = info.applicationInfo.loadLabel(pm).toString(),
versionName = info.versionName ?: "",
versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
info.longVersionCode
} else {
info.versionCode.toLong()
},
flags = info.applicationInfo.flags,
firstInstallTime = info.firstInstallTime,
)
val extraInfo = PackageExtraInfo(
uid = uid,
labels = listOf(),
hasKeystore = hasKeystore,
permissions = permissions,
ssaid = ssaid,
blocked = false,
activated = false,
existed = true,
)
val indexInfo = PackageIndexInfo(
opType = OpType.BACKUP,
packageName = info.packageName,
userId = userId,
compressionType = context.readCompressionType().first(),
preserveId = DefaultPreserveId,
cloud = "",
backupDir = ""
)
val packageEntity =
getPackage(packageName = info.packageName, opType = OpType.BACKUP, userId = userId, preserveId = DefaultPreserveId, cloud = "", backupDir = "")
?: PackageEntity(
id = 0,
indexInfo = indexInfo,
packageInfo = packageInfo,
extraInfo = extraInfo,
dataStates = PackageDataStates(),
storageStats = PackageStorageStats(),
dataStats = PackageDataStats(),
displayStats = PackageDataStats(),
)
// Update if exists.
packageEntity.apply {
this.packageInfo = packageInfo
this.extraInfo.uid = uid
this.extraInfo.hasKeystore = hasKeystore
this.extraInfo.permissions = permissions
this.extraInfo.ssaid = ssaid
this.extraInfo.existed = true
}
if (userHandle != null) {
rootService.queryStatsForPackage(info, userHandle).also { stats ->
if (stats != null) {
packageEntity.apply {
this.storageStats.appBytes = stats.appBytes
this.storageStats.cacheBytes = stats.cacheBytes
this.storageStats.dataBytes = stats.dataBytes
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) this.storageStats.externalCacheBytes = stats.externalCacheBytes
}
}
}
}
val packageEntity = handlePackage(pm, info, checkKeystore, userId, userHandle, hasPassedOneDay)
upsert(packageEntity)

refreshState.emit(refreshState.value.copy(pkg = info.packageName))
Expand All @@ -335,6 +347,50 @@ class PackageRepository @Inject constructor(
refreshState.emit(RefreshState(progress = 1f, pkg = ""))
}

@SuppressLint("StringFormatInvalid")
suspend fun fastRefresh() = run {
val checkKeystore = context.readCheckKeystore().first()
val loadSystemApps = context.readLoadSystemApps().first()
val pm = context.packageManager
val userInfoList = rootService.getUsers()
for (userInfo in userInfoList) {
val userId = userInfo.id
val userHandle = rootService.getUserHandle(userId)
val installedPackages = getInstalledPackages(userId).filter {
val isSystemApp = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0
loadSystemApps || isSystemApp.not()
}.map { it.packageName!! }.toSet()
val storedPackages = packageDao.queryPackageNamesByUserId(OpType.BACKUP, userId).toSet()
val missingPackages = installedPackages.subtract(storedPackages)
val outdatedPackages = storedPackages.subtract(installedPackages)
log { "Missing packages: $missingPackages" }
log { "Outdated packages: $outdatedPackages" }

BaseUtil.mkdirs(context.iconDir())
val iconUpdateTime = context.readIconUpdateTime().first()
val now = DateUtil.getTimestamp()
val hasPassedOneDay = DateUtil.getNumberOfDaysPassed(iconUpdateTime, now) >= 1
if (hasPassedOneDay) context.saveIconUpdateTime(now)
// For missing packages, we query info.
missingPackages.forEach {
// Update packages' info.
val info = rootService.getPackageInfoAsUser(it, PackageManager.GET_PERMISSIONS, userId)
if (info != null) {
val isSystemApp = (info.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0
if (loadSystemApps || isSystemApp.not()) {
val packageEntity = handlePackage(pm, info, checkKeystore, userId, userHandle, hasPassedOneDay)
upsert(packageEntity)
}
}
}

// For those uninstalled packages, we hide them.
outdatedPackages.forEach {
packageDao.setExisted(OpType.BACKUP, it, userId, false)
}
}
}

suspend fun loadPackagesFromLocal() {
val path = backupAppsDir
val paths = rootService.walkFileTree(path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ interface PackageDao {
)
suspend fun query(packageName: String, opType: OpType, userId: Int, cloud: String, backupDir: String): List<PackageEntity>

@Query(
"SELECT indexInfo_packageName FROM PackageEntity WHERE" +
" indexInfo_opType = :opType AND indexInfo_userId = :userId" +
" AND extraInfo_blocked = 0 AND extraInfo_existed = 1"
)
suspend fun queryPackageNamesByUserId(opType: OpType, userId: Int): List<String>

@Query(
"SELECT * FROM PackageEntity WHERE" +
" indexInfo_packageName = :packageName AND indexInfo_opType = :opType AND indexInfo_userId = :userId AND indexInfo_preserveId = :preserveId AND indexInfo_compressionType = :ct" +
Expand Down Expand Up @@ -108,6 +115,13 @@ interface PackageDao {
)
suspend fun setBlocked(id: Long, blocked: Boolean)

@Query(
"UPDATE PackageEntity" +
" SET extraInfo_existed = :existed" +
" WHERE indexInfo_opType = :opType AND indexInfo_packageName = :packageName AND indexInfo_userId = :userId"
)
suspend fun setExisted(opType: OpType, packageName: String, userId: Int, existed: Boolean)

@Query(
"UPDATE PackageEntity SET extraInfo_blocked = 0"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import com.xayah.core.ui.component.LocalSlotScope
import com.xayah.core.ui.component.MultipleSelectionFilterChip
import com.xayah.core.ui.component.PackageItem
import com.xayah.core.ui.component.SearchBar
import com.xayah.core.ui.component.SetOnResume
import com.xayah.core.ui.component.SortChip
import com.xayah.core.ui.component.confirm
import com.xayah.core.ui.component.paddingHorizontal
Expand Down Expand Up @@ -106,8 +107,13 @@ fun PagePackagesBackupList() {
viewModel.emitIntentOnIO(IndexUiIntent.GetUserIds)
}

SetOnResume {
viewModel.emitIntentOnIO(IndexUiIntent.OnFastRefresh)
}

ListScaffold(
scrollBehavior = scrollBehavior,
progress = if (uiState.isLoading) -1F else null,
title = StringResourceToken.fromStringArgs(
StringResourceToken.fromStringId(R.string.select_apps),
StringResourceToken.fromString(if (packagesSelectedState != 0 && isRefreshing.not()) " (${packagesSelectedState}/${packagesState.size})" else ""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ data class IndexUiState(
val selectAll: Boolean,
val filterMode: Boolean,
val uuid: UUID,
val isLoading: Boolean,
) : UiState

sealed class IndexUiIntent : UiIntent {
data object OnRefresh : IndexUiIntent()
data object OnFastRefresh : IndexUiIntent()
data object GetUserIds : IndexUiIntent()
data class SetUserIdIndexList(val list: List<Int>) : IndexUiIntent()
data class FilterByFlag(val index: Int) : IndexUiIntent()
Expand All @@ -60,7 +62,15 @@ class IndexViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val packageRepo: PackageRepository,
private val rootService: RemoteRootService,
) : BaseViewModel<IndexUiState, IndexUiIntent, IndexUiEffect>(IndexUiState(isRefreshing = false, selectAll = false, filterMode = true, uuid = UUID.randomUUID())) {
) : BaseViewModel<IndexUiState, IndexUiIntent, IndexUiEffect>(
IndexUiState(
isRefreshing = false,
selectAll = false,
filterMode = true,
uuid = UUID.randomUUID(),
isLoading = false
)
) {
init {
rootService.onFailure = {
val msg = it.message
Expand All @@ -79,6 +89,12 @@ class IndexViewModel @Inject constructor(
emitState(state.copy(isRefreshing = false, uuid = UUID.randomUUID()))
}

is IndexUiIntent.OnFastRefresh -> {
emitState(state.copy(isLoading = true))
packageRepo.fastRefresh()
emitState(state.copy(isLoading = false))
}

is IndexUiIntent.GetUserIds -> {
context.saveUserIdList(rootService.getUsers().map { it.id })
}
Expand Down

0 comments on commit 9b8c535

Please sign in to comment.