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 <noreply@anthropic.com>
This commit is contained in:
TMCandClaude Fable 5.2 committed 2026-11-12 20:21:59 +01:00
1 parent 81c5c8914a
commit 77278a11e0
5 files changed
+124 -6

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- "Hide app" in an app's long-press menu: an eye with a line through it. Drawn as strokes;
the menu tints it like its other icons. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- The eye's outline, open on the side the line crosses first. -->
<path
android:pathData="M9.9,5.2C10.6,5.07 11.3,5 12,5C16.2,5 19.7,7.6 21.5,12C20.9,13.4 20.1,14.6 19.1,15.6M16.4,17.6C15.1,18.5 13.6,19 12,19C7.8,19 4.3,16.4 2.5,12C3.3,10 4.5,8.4 6,7.2"
android:strokeColor="@android:color/white"
android:strokeWidth="1.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- What the line leaves of the pupil. -->
<path
android:pathData="M14.1,14.1C13.56,14.66 12.82,15 12,15C10.34,15 9,13.66 9,12C9,11.18 9.34,10.44 9.9,9.9"
android:strokeColor="@android:color/white"
android:strokeWidth="1.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- The line. -->
<path
android:pathData="M4,4L20,20"
android:strokeColor="@android:color/white"
android:strokeWidth="1.8"
android:strokeLineCap="round" />
</vector>
+3
View File
@@ -93,6 +93,9 @@
<string name="flexera_scrub_from_home_description">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.</string>
<!-- Quick launch: a swipe on past the first or the last home screen page (app.flexera.home.QuickLaunch) -->
<string name="flexera_hide_app">Hide app</string>
<!-- Shown after an app was hidden from the app list. %1$s is the app's name -->
<string name="flexera_app_hidden">%1$s is hidden</string>
<string name="flexera_folder_blur">Blur behind open folders</string>
<string name="flexera_folder_blur_description">Blurs the home screen while a folder is open</string>
<string name="flexera_quick_launch">Quick launch</string>
+78
View File
@@ -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<LawnchairLauncher>(
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)
}
}
}
@@ -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),
),
)
@@ -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)
}