Root Cause Analysis
The Bug Chain
The issue is caused by the refreshLaunchers() method in MainActivity mistakenly treating a hidden app as an uninstalled app, which triggers a destructive database cleanup that cascade-deletes the folder and all its contents.
Here is the exact execution chain:
Step 1 — User hides the app
When the user selects "Hide" from the app drawer context menu, MainActivity.hideIcon() (MainActivity.kt:882–891) executes:
private fun hideIcon(item: HomeScreenGridItem) {
ensureBackgroundThread {
val hiddenIcon = HiddenIcon(null, item.packageName, item.activityName, item.title, null)
hiddenIconsDB.insert(hiddenIcon)
runOnUiThread {
binding.allAppsFragment.root.onIconHidden(item)
}
}
}
This method only:
- Inserts a
HiddenIcon record into the hidden_icons database table.
- Removes the app from the drawer UI (
AllAppsFragment.onIconHidden()).
It does NOT:
- Remove the app's icon from any home screen folder.
- Clean up or update
home_screen_grid_items for the hidden app.
- Update
IconCache.launchers to reflect the hiding.
Step 2 — refreshLaunchers() treats the hidden app as uninstalled
On onResume(), refreshLaunchers() (MainActivity.kt:596–619) runs:
private fun refreshLaunchers() {
val launchers = getAllAppLaunchers() // Filters out hidden apps!
// ...
IconCache.launchers.map { it.packageName }.forEach { packageName ->
if (!launchers.map { it.packageName }.contains(packageName)) {
launchersDB.deleteApp(packageName)
homeScreenGridItemsDB.deleteByPackageName(packageName) // DESTRUCTIVE!
}
}
IconCache.launchers = launchers
// ...
}
getAllAppLaunchers() (MainActivity.kt:1082–1131) queries installed apps and filters out hidden ones:
if (hiddenIcons.contains("$packageName/$activityName")) {
continue // Hidden app excluded from result
}
The comparison loop at line 601–605 then finds the hidden app's package in IconCache.launchers (old cache, still containing the hidden app) but NOT in the new filtered launchers list. It concludes the app has been uninstalled and calls homeScreenGridItemsDB.deleteByPackageName(packageName).
Step 3 — deleteByPackageName cascade-deletes the folder
HomeScreenGridItemsDao.deleteByPackageName() (HomeScreenGridItemsDao.kt:56–60):
@Transaction
fun deleteByPackageName(packageName: String) {
deleteItemByPackageName(packageName) // (1)
deleteItemsByParentPackageName(packageName) // (2)
}
Query (1) — deleteItemByPackageName:
DELETE FROM home_screen_grid_items WHERE package_name = :packageName
This deletes ALL grid items (icons, shortcuts, AND the folder itself) whose package_name matches.
Critical detail: When a folder is created by dragging App A onto App B, the folder entity inherits App B's packageName (see HomeScreenGrid.kt:734: potentialParent.copy(type = ITEM_TYPE_FOLDER, ...)). So if the hidden app's packageName matches the folder's packageName, the folder itself is deleted.
Query (2) — deleteItemsByParentPackageName:
DELETE FROM home_screen_grid_items
WHERE parent_id IN (
SELECT id FROM home_screen_grid_items WHERE package_name = :packageName
)
This deletes ALL children of any item (folder) whose packageName matches. Since the folder's packageName may match the hidden app, all remaining items in the folder are also deleted.
Step 4 — fetchGridItems() redraws without the folder
After the cleanup, fetchGridItems() reloads from the database. The folder and its children are gone from the DB, so they no longer appear on the home screen.
Summary
User hides app → hideIcon() only adds to hidden_icons DB
→ onResume() → refreshLaunchers()
→ getAllAppLaunchers() excludes hidden app
→ Loop compares old cache vs new list → "missing" package detected
→ deleteByPackageName() runs → deletes folder + all children from DB
→ fetchGridItems() → folder is gone from home screen
The fundamental mistake is that refreshLaunchers() cannot distinguish between:
- An app that was uninstalled (should clean up home screen items)
- An app that was hidden (should NOT touch home screen items)
Suggested Fix
The fix should address the core issue: refreshLaunchers() should not treat hidden apps as uninstalled apps. There are multiple approaches:
Approach 1: Exclude hidden packages from the cleanup comparison (Recommended)
In refreshLaunchers(), add hidden packages to the known packages set so they are not treated as uninstalled:
private fun refreshLaunchers() {
val launchers = getAllAppLaunchers()
binding.allAppsFragment.root.gotLaunchers(launchers)
binding.widgetsFragment.root.getAppWidgets()
val hiddenPackages = hiddenIconsDB.getHiddenIcons().map { it.packageName }.toSet()
val knownPackages = launchers.map { it.packageName }.toSet() + hiddenPackages
IconCache.launchers.map { it.packageName }.forEach { packageName ->
if (!knownPackages.contains(packageName)) {
launchersDB.deleteApp(packageName)
homeScreenGridItemsDB.deleteByPackageName(packageName)
}
}
IconCache.launchers = launchers
// ...
}
Approach 2: Also remove the hidden icon from the folder in hideIcon()
In addition to Approach 1, hideIcon() should also handle removing the app from any home screen folder:
private fun hideIcon(item: HomeScreenGridItem) {
ensureBackgroundThread {
val hiddenIcon = HiddenIcon(null, item.packageName, item.activityName, item.title, null)
hiddenIconsDB.insert(hiddenIcon)
// Remove the hidden app's icon(s) from any home screen folders
val gridItems = homeScreenGridItemsDB.getAllItems()
gridItems.filter {
it.packageName == item.packageName
&& it.activityName == item.activityName
&& it.parentId != null
}.forEach { folderItem ->
homeScreenGridItemsDB.deleteItemById(folderItem.id!!)
homeScreenGridItemsDB.shiftFolderItems(
folderId = folderItem.parentId!!,
shiftFrom = folderItem.left,
shiftBy = -1
)
// Clean up empty folders
val remainingItems = homeScreenGridItemsDB.getFolderItems(folderItem.parentId!!)
if (remainingItems.isEmpty()) {
homeScreenGridItemsDB.deleteItemById(folderItem.parentId!!)
}
}
runOnUiThread {
binding.allAppsFragment.root.onIconHidden(item)
binding.homeScreenGrid.root.fetchGridItems()
}
}
}
Approach 3: Fix deleteByPackageName to be folder-aware
Modify deleteByPackageName so it does not delete folder entities or their children indiscriminately. Instead, it should only delete individual items and properly handle folder cleanup (remove from folder, shift indices, delete folder if empty).
Recommendation: Approach 1 is the minimal and safest fix. It directly addresses the root cause without refactoring the folder management logic. Approach 2 can be applied as an enhancement for a more complete UX where hiding an app also removes its icon from folders.
Checklist
Affected app version
1.10.0 (Latest)
Affected Android/Custom ROM version
Android 13
Affected device model
Xiaomi Redmi Note 10 Pro
How did you install the app?
Google Play Store
Steps to reproduce the bug
Expected behavior
Actual behavior
Screenshots/Screen recordings
Additional information
Root Cause Analysis
The Bug Chain
The issue is caused by the
refreshLaunchers()method inMainActivitymistakenly treating a hidden app as an uninstalled app, which triggers a destructive database cleanup that cascade-deletes the folder and all its contents.Here is the exact execution chain:
Step 1 — User hides the app
When the user selects "Hide" from the app drawer context menu,
MainActivity.hideIcon()(MainActivity.kt:882–891) executes:This method only:
HiddenIconrecord into thehidden_iconsdatabase table.AllAppsFragment.onIconHidden()).It does NOT:
home_screen_grid_itemsfor the hidden app.IconCache.launchersto reflect the hiding.Step 2 —
refreshLaunchers()treats the hidden app as uninstalledOn
onResume(),refreshLaunchers()(MainActivity.kt:596–619) runs:getAllAppLaunchers()(MainActivity.kt:1082–1131) queries installed apps and filters out hidden ones:The comparison loop at line 601–605 then finds the hidden app's package in
IconCache.launchers(old cache, still containing the hidden app) but NOT in the new filteredlauncherslist. It concludes the app has been uninstalled and callshomeScreenGridItemsDB.deleteByPackageName(packageName).Step 3 —
deleteByPackageNamecascade-deletes the folderHomeScreenGridItemsDao.deleteByPackageName()(HomeScreenGridItemsDao.kt:56–60):Query (1) —
deleteItemByPackageName:This deletes ALL grid items (icons, shortcuts, AND the folder itself) whose
package_namematches.Query (2) —
deleteItemsByParentPackageName:This deletes ALL children of any item (folder) whose
packageNamematches. Since the folder'spackageNamemay match the hidden app, all remaining items in the folder are also deleted.Step 4 —
fetchGridItems()redraws without the folderAfter the cleanup,
fetchGridItems()reloads from the database. The folder and its children are gone from the DB, so they no longer appear on the home screen.Summary
The fundamental mistake is that
refreshLaunchers()cannot distinguish between:Suggested Fix
The fix should address the core issue:
refreshLaunchers()should not treat hidden apps as uninstalled apps. There are multiple approaches:Approach 1: Exclude hidden packages from the cleanup comparison (Recommended)
In
refreshLaunchers(), add hidden packages to the known packages set so they are not treated as uninstalled:Approach 2: Also remove the hidden icon from the folder in
hideIcon()In addition to Approach 1,
hideIcon()should also handle removing the app from any home screen folder:Approach 3: Fix
deleteByPackageNameto be folder-awareModify
deleteByPackageNameso it does not delete folder entities or their children indiscriminately. Instead, it should only delete individual items and properly handle folder cleanup (remove from folder, shift indices, delete folder if empty).Related Files
MainActivity.kthideIcon()method,refreshLaunchers()method,onResume()lifecycle,getAllAppLaunchers()HomeScreenGridItemsDao.ktdeleteByPackageName()cascade transaction,deleteItemByPackageName,deleteItemsByParentPackageNameHomeScreenGrid.ktfetchGridItems(), folder creation logic (potentialParent.copy),removeItemFromHomeScreen(),HomeScreenFolder.getItems()AllAppsFragment.ktonIconHidden()drawer update,onAppLauncherLongPressed()Activity.kthandleGridItemPopupMenu()— "Hide" option visibility logicHiddenIcon.ktHiddenIcondata modelHomeScreenGridItem.ktparentId,packageName,type,FOLDER_MAX_CAPACITY