From 77278a11e009e5eea91d40c0fa26bf75c59f856c Mon Sep 17 00:00:00 2001 From: TMC Date: Thu, 12 Nov 2026 20:21:59 +0100 Subject: [PATCH] Hide app from the app list's long-press menu; stop the restart on the first preference write Hide app: a new entry in the menu a long press on an app opens in the app list. It adds the app to Lawnchair's hidden list (hidden_apps), which the list and its search already leave out and which Settings > App drawer > Hidden apps already shows, so that page is where an app is brought back. A message with Undo follows. The entry is offered in the app list only (not for home screen icons, which hiding does not touch) and not while the home screen is locked. Preference collectors: five collectors in PreferenceManager2 (icon shape, dock mode, dock on/off, dock search provider, labels in the dock) had drop(1) before distinctUntilChanged. The first emission after the dropped one then always passed, so the first write of any preference after the launcher started reloaded the model, rebuilt the grid three times and recreated the launcher. Hiding an app is such a write, made while the launcher is on screen: the list went blank for a moment and the Undo message was lost with the old activity. The operators are swapped, so only a changed value passes. Checked on the emulator: both menu layouts (labelled rows and the icon row), hiding right after a fresh start (no relaunch in the log, message stays), Undo, and unticking the apps under Hidden apps. Co-Authored-By: Claude Fable 5.2 --- flexera/res/drawable/ic_flexera_hide.xml | 30 +++++++ flexera/res/values/flexera_strings.xml | 3 + flexera/src/app/flexera/drawer/HideApp.kt | 78 +++++++++++++++++++ .../src/app/lawnchair/LawnchairLauncher.kt | 3 +- .../preferences2/PreferenceManager2.kt | 16 ++-- 5 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 flexera/res/drawable/ic_flexera_hide.xml create mode 100644 flexera/src/app/flexera/drawer/HideApp.kt diff --git a/flexera/res/drawable/ic_flexera_hide.xml b/flexera/res/drawable/ic_flexera_hide.xml new file mode 100644 index 0000000000..414ab1ea06 --- /dev/null +++ b/flexera/res/drawable/ic_flexera_hide.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/flexera/res/values/flexera_strings.xml b/flexera/res/values/flexera_strings.xml index 5a23abfc49..7d1e245415 100644 --- a/flexera/res/values/flexera_strings.xml +++ b/flexera/res/values/flexera_strings.xml @@ -93,6 +93,9 @@ Slide up or down along the alphabet\'s edge of the home screen to open the app list at a letter. Swipe up and hold does the same from anywhere. + Hide app + + %1$s is hidden Blur behind open folders Blurs the home screen while a folder is open Quick launch diff --git a/flexera/src/app/flexera/drawer/HideApp.kt b/flexera/src/app/flexera/drawer/HideApp.kt new file mode 100644 index 0000000000..6de740a78a --- /dev/null +++ b/flexera/src/app/flexera/drawer/HideApp.kt @@ -0,0 +1,78 @@ +package app.flexera.drawer + +import android.view.View +import androidx.lifecycle.lifecycleScope +import app.lawnchair.LawnchairLauncher +import app.lawnchair.preferences2.PreferenceManager2 +import app.lawnchair.preferences2.firstCached +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS +import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION +import com.android.launcher3.R +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.popup.SystemShortcut +import com.android.launcher3.util.ComponentKey +import com.android.launcher3.views.Snackbar +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +/** + * "Hide app" in the menu a long press on an app opens in the app list. + * + * It puts the app on the hidden list Lawnchair already has: the one the app list and its search + * leave out, kept under Settings > App drawer > Hidden apps, where an app is brought back. Before + * this, hiding meant going through that settings page or through Customize. A message with Undo + * follows, because the app is gone from the list the moment it is hidden. + */ +object HideApp { + + /** + * Offered for apps in the app list only: hiding does nothing to an icon on the home screen, + * so offering it there would promise something else. Not offered while the home screen is + * locked against changes, like Customize and Uninstall. + */ + @JvmField + val SHORTCUT = SystemShortcut.Factory { launcher: LawnchairLauncher, itemInfo: ItemInfo, originalView: View -> + val component = itemInfo.targetComponent + when { + itemInfo.container != CONTAINER_ALL_APPS -> null + itemInfo.itemType != ITEM_TYPE_APPLICATION || component == null -> null + PreferenceManager2.getInstance(launcher).lockHomeScreen.firstCached() -> null + else -> Shortcut(launcher, itemInfo, originalView, ComponentKey(component, itemInfo.user)) + } + } + + private class Shortcut( + launcher: LawnchairLauncher, + itemInfo: ItemInfo, + originalView: View, + private val app: ComponentKey, + ) : SystemShortcut( + R.drawable.ic_flexera_hide, + R.string.flexera_hide_app, + launcher, + itemInfo, + originalView, + ) { + override fun onClick(view: View) { + val launcher = mTarget + val name = mItemInfo.title?.toString().orEmpty() + dismissTaskMenuView() + setHidden(launcher, app, hidden = true) + Snackbar.show( + launcher, + launcher.getString(R.string.flexera_app_hidden, name), + R.string.undo, + null, + ) { setHidden(launcher, app, hidden = false) } + } + } + + private fun setHidden(launcher: LawnchairLauncher, app: ComponentKey, hidden: Boolean) { + val hiddenApps = PreferenceManager2.getInstance(launcher).hiddenApps + val key = app.toString() + launcher.lifecycleScope.launch { + val now = hiddenApps.get().first() + hiddenApps.set(if (hidden) now + key else now - key) + } + } +} diff --git a/lawnchair/src/app/lawnchair/LawnchairLauncher.kt b/lawnchair/src/app/lawnchair/LawnchairLauncher.kt index 77264a141c..05a5acc0ca 100644 --- a/lawnchair/src/app/lawnchair/LawnchairLauncher.kt +++ b/lawnchair/src/app/lawnchair/LawnchairLauncher.kt @@ -283,7 +283,8 @@ class LawnchairLauncher : QuickstepLauncher() { super.getSupportedShortcuts(container), Stream.concat( Stream.of(LawnchairShortcut.UNINSTALL, LawnchairShortcut.CUSTOMIZE, LawnchairShortcut.OPEN_IN_STORE), - Stream.of(LawnchairShortcut.PAUSE_APPS), + // Flexera Launcher: "Hide app", which its factory offers in the app list only. + Stream.of(LawnchairShortcut.PAUSE_APPS, app.flexera.drawer.HideApp.SHORTCUT), ), ) diff --git a/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt b/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt index a048c7761d..f9ddae8435 100644 --- a/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt +++ b/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt @@ -919,10 +919,16 @@ class PreferenceManager2 @Inject constructor( .onEach { cachedPreferences = it } .launchIn(scope) + // Flexera Launcher: in the five collectors below, distinctUntilChanged comes before + // drop(1). Upstream has them the other way round, which lets the first emission after + // the dropped one through whatever its value: the first write of any preference after + // the launcher started then reloaded the model, rebuilt the grid three times and + // recreated the launcher. Hiding an app from the app list is such a write, made while + // the owner looks at the launcher. initializeIconShape(iconShape.firstCached(this)) iconShape.get() - .drop(1) .distinctUntilChanged() + .drop(1) .onEach { shape -> initializeIconShape(shape) L3ThemeManager.INSTANCE.get(context) @@ -931,14 +937,14 @@ class PreferenceManager2 @Inject constructor( .launchIn(scope) hotseatMode.get() - .drop(1) .distinctUntilChanged() + .drop(1) .onEach { reloadHelper.restart() } .launchIn(scope) isHotseatEnabled.get() - .drop(1) .distinctUntilChanged() + .drop(1) .onEach { reloadHelper.recreate() reloadHelper.reloadGrid() @@ -946,14 +952,14 @@ class PreferenceManager2 @Inject constructor( .launchIn(scope) hotseatQsbProvider.get() - .drop(1) .distinctUntilChanged() + .drop(1) .onEach { reloadHelper.recreate() } .launchIn(scope) enableLabelInDock.get() - .drop(1) .distinctUntilChanged() + .drop(1) .onEach { reloadHelper.reloadGrid() } .launchIn(scope) }