Skip to content

Commit 354b0ce

Browse files
refactor(performance): Optimize app list loading and sorting
This commit introduces significant performance and architectural improvements to the app list generation process in `MainViewModel`. Key changes: - **Optimized App Loading:** A lightweight `RawApp` data class is now used for initial app data collection, reducing memory overhead before the final `AppListItem` objects are created. This is particularly effective for devices with many applications. - **Efficient Sorting:** App sorting logic has been revised. Pinned apps are now correctly sorted to the top of the list, followed by an alphabetical sort of all other apps (recent and regular). This is applied consistently in both `buildList` and `getAppsList`. - **Parallel Processing:** App fetching from different user profiles (System, Work, Private) now runs in parallel more efficiently. - **Improved Clarity:** The `getAppsList` function has been restructured and documented for better readability and maintainability.
1 parent 70ea715 commit 354b0ce

File tree

2 files changed

+92
-70
lines changed

2 files changed

+92
-70
lines changed

app/src/main/java/com/github/droidworksstudio/mlauncher/MainViewModel.kt

Lines changed: 92 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -471,8 +471,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
471471
list.add(buildItem(raw))
472472
}
473473

474-
// ✅ Sort by normalized label
475-
list.sortWith(compareBy { normalize(getLabel(it)) })
474+
val pinnedMap = items.associateWith { isPinned(it) } // Map<T, Boolean>
475+
476+
// ✅ Sort pinned first, then by normalized label
477+
list.sortWith(
478+
compareByDescending<R> { pinnedMap[items[list.indexOf(it)]] == true }
479+
.thenBy { normalize(getLabel(it)) }
480+
)
476481

477482
// ✅ Build scroll index (safe, no `continue`)
478483
list.forEachIndexed { index, item ->
@@ -487,8 +492,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
487492
}
488493

489494
/**
490-
* Build app list on IO dispatcher. This function is still suspend and safe to call
491-
* from background, but it ensures all heavy operations happen on Dispatchers.IO.
495+
* Build app list on IO dispatcher. This function is suspend and safe to call
496+
* from a background thread, but it ensures all heavy operations happen on Dispatchers.IO.
497+
*
498+
* Features:
499+
* - Fetches recent apps and user profile apps in parallel.
500+
* - Filters hidden/pinned apps before creating AppListItem objects.
501+
* - Updates profile counters efficiently.
502+
* - Optimized for memory and CPU usage on devices with many apps.
492503
*/
493504
suspend fun getAppsList(
494505
context: Context,
@@ -503,109 +514,121 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
503514
val seenAppKeys = mutableSetOf<String>()
504515
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
505516
val profiles = userManager.userProfiles.toList()
517+
val privateManager = PrivateSpaceManager(context)
506518

507519
fun appKey(pkg: String, cls: String, profileHash: Int) = "$pkg|$cls|$profileHash"
508520
fun isHidden(pkg: String, key: String) =
509521
listOf(pkg, key, "$pkg|${key.hashCode()}").any { it in hiddenAppsSet }
510522

511-
AppLogger.d(
512-
"AppListDebug",
513-
"🔄 getAppsList called with: includeRegular=$includeRegularApps, includeHidden=$includeHiddenApps, includeRecent=$includeRecentApps"
523+
// Lightweight intermediate storage
524+
data class RawApp(
525+
val pkg: String,
526+
val cls: String,
527+
val label: String,
528+
val user: UserHandle,
529+
val profileType: String,
530+
val category: AppCategory
514531
)
515532

516-
val allApps = mutableListOf<AppListItem>()
533+
val rawApps = mutableListOf<RawApp>()
517534

518-
// 🔹 Add recent apps
535+
// 🔹 Recent apps
519536
if (prefs.recentAppsDisplayed && includeRecentApps) {
520537
runCatching {
521-
AppUsageMonitor.createInstance(context).getLastTenAppsUsed(context).forEach { (pkg, name, activity) ->
522-
val key = appKey(pkg, activity, 0)
523-
if (seenAppKeys.add(key)) {
524-
allApps.add(
525-
AppListItem(
526-
activityLabel = name,
527-
activityPackage = pkg,
528-
activityClass = activity,
529-
user = Process.myUserHandle(),
530-
profileType = "SYSTEM",
531-
customLabel = prefs.getAppAlias(pkg).ifEmpty { name },
532-
customTag = prefs.getAppTag(pkg, null),
533-
category = AppCategory.RECENT
538+
AppUsageMonitor.createInstance(context)
539+
.getLastTenAppsUsed(context)
540+
.forEach { (pkg, name, activity) ->
541+
val key = appKey(pkg, activity, 0)
542+
if (seenAppKeys.add(key)) {
543+
rawApps.add(
544+
RawApp(
545+
pkg = pkg,
546+
cls = activity,
547+
label = name,
548+
user = Process.myUserHandle(),
549+
profileType = "SYSTEM",
550+
category = AppCategory.RECENT
551+
)
534552
)
535-
)
553+
}
536554
}
537-
}
538555
}.onFailure { t ->
539556
AppLogger.e("AppListDebug", "Failed to add recent apps: ${t.message}", t)
540557
}
541558
}
542559

543-
// 🔹 Process user profiles in parallel
560+
// 🔹 Profile apps in parallel
561+
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
544562
val deferreds = profiles.map { profile ->
545563
async {
546-
val privateManager = PrivateSpaceManager(context)
547564
if (privateManager.isPrivateSpaceProfile(profile) && privateManager.isPrivateSpaceLocked()) {
548565
AppLogger.d("AppListDebug", "🔒 Skipping locked private profile: $profile")
549-
return@async emptyList<AppListItem>()
550-
}
551-
552-
val profileType = when {
553-
privateManager.isPrivateSpaceProfile(profile) -> "PRIVATE"
554-
profile != Process.myUserHandle() -> "WORK"
555-
else -> "SYSTEM"
556-
}
557-
558-
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
559-
val activities = runCatching { launcherApps.getActivityList(null, profile) }.getOrElse {
560-
AppLogger.e("AppListDebug", "Failed to get activities for $profile: ${it.message}", it)
561-
emptyList()
562-
}
566+
emptyList<RawApp>()
567+
} else {
568+
val profileType = when {
569+
privateManager.isPrivateSpaceProfile(profile) -> "PRIVATE"
570+
profile != Process.myUserHandle() -> "WORK"
571+
else -> "SYSTEM"
572+
}
563573

564-
activities.mapNotNull { info ->
565-
val pkg = info.applicationInfo.packageName
566-
val cls = info.componentName.className
567-
val label = info.label.toString()
568-
if (pkg == BuildConfig.APPLICATION_ID) return@mapNotNull null
569-
570-
val key = appKey(pkg, cls, profile.hashCode())
571-
if (!seenAppKeys.add(key)) return@mapNotNull null
572-
if ((isHidden(pkg, key) && !includeHiddenApps) || (!isHidden(pkg, key) && !includeRegularApps))
573-
return@mapNotNull null
574-
575-
val category = if (pkg in pinnedPackages) AppCategory.PINNED else AppCategory.REGULAR
576-
AppListItem(
577-
activityLabel = label,
578-
activityPackage = pkg,
579-
activityClass = cls,
580-
user = profile,
581-
profileType = profileType,
582-
customLabel = prefs.getAppAlias(pkg),
583-
customTag = prefs.getAppTag(pkg, profile),
584-
category = category
585-
)
574+
runCatching { launcherApps.getActivityList(null, profile) }
575+
.getOrElse {
576+
AppLogger.e("AppListDebug", "Failed to get activities for $profile: ${it.message}", it)
577+
emptyList()
578+
}
579+
.mapNotNull { info ->
580+
val pkg = info.applicationInfo.packageName
581+
val cls = info.componentName.className
582+
if (pkg == BuildConfig.APPLICATION_ID) return@mapNotNull null
583+
val key = appKey(pkg, cls, profile.hashCode())
584+
if (!seenAppKeys.add(key)) return@mapNotNull null
585+
if ((isHidden(pkg, key) && !includeHiddenApps) || (!isHidden(pkg, key) && !includeRegularApps))
586+
return@mapNotNull null
587+
588+
val category = if (pkg in pinnedPackages) AppCategory.PINNED else AppCategory.REGULAR
589+
RawApp(pkg, cls, info.label.toString(), profile, profileType, category)
590+
}
586591
}
587592
}
588593
}
589594

590-
val profileApps = deferreds.flatMap { it.await() }
591-
allApps.addAll(profileApps)
595+
deferreds.forEach { rawApps.addAll(it.await()) }
592596

593597
// 🔹 Update profile counters
594598
listOf("SYSTEM", "PRIVATE", "WORK", "USER").forEach { type ->
595-
val count = allApps.count { it.profileType == type }
596-
prefs.setProfileCounter(type, count)
599+
prefs.setProfileCounter(type, rawApps.count { it.profileType == type })
600+
}
601+
602+
// 🔹 Convert RawApp → AppListItem
603+
val allApps = rawApps.map { raw ->
604+
AppListItem(
605+
activityLabel = raw.label,
606+
activityPackage = raw.pkg,
607+
activityClass = raw.cls,
608+
user = raw.user,
609+
profileType = raw.profileType,
610+
customLabel = prefs.getAppAlias(raw.pkg),
611+
customTag = prefs.getAppTag(raw.pkg, raw.user),
612+
category = raw.category
613+
)
597614
}
615+
// 🔹 Sort pinned apps first, then regular/recent alphabetically
616+
.sortedWith(
617+
compareByDescending<AppListItem> { it.category == AppCategory.PINNED }
618+
.thenBy { normalizeForSort(it.label) }
619+
)
620+
.toMutableList()
598621

599-
// 🔹 Finalize list using buildList()
622+
// 🔹 Build scroll map and finalize
600623
buildList(
601624
items = allApps,
602-
seenKey = mutableSetOf(), // ✅ fresh set (don’t reuse seenAppKeys!)
625+
seenKey = mutableSetOf(),
603626
scrollMapLiveData = _appScrollMap,
604627
includeHidden = includeHiddenApps,
605628
getKey = { "${it.activityPackage}|${it.activityClass}|${it.user.hashCode()}" },
606629
isHidden = { it.activityPackage in hiddenAppsSet },
607630
isPinned = { it.activityPackage in pinnedPackages },
608-
buildItem = { it }, // Already an AppListItem
631+
buildItem = { it },
609632
getLabel = { it.label },
610633
normalize = ::normalizeForSort
611634
)

app/src/main/java/com/github/droidworksstudio/mlauncher/ui/AppDrawerFragment.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,6 @@ class AppDrawerFragment : BaseFragment() {
209209
combinedScrollMaps.value = Pair(viewModel.appScrollMap.value ?: emptyMap(), contactMap)
210210
}
211211

212-
213212
combinedScrollMaps.observe(viewLifecycleOwner) { (appMap, contactMap) ->
214213
binding.azSidebar.onLetterSelected = { section ->
215214
when (binding.menuView.displayedChild) {

0 commit comments

Comments
 (0)