PRD AD-15 and SI-10, plus polish found while checking the M4 rows on the emulator.
Wave alphabet
- New setting "Alphabet on the left edge" for left-handed use. Rows keep clear of the
alphabet on whichever edge it sits; a scrub from the left darkens the list under the
wave, because there the letters cross the icons and names.
- Screen readers see the strip as one slider: it reads the letter the list is at, and the
adjust gesture moves one letter at a time.
- Letters shrink to fit their slot when a large system font would make them overlap.
Settings
- FlexeraDrawerSettings replaces Lawnchair's App drawer screen, whose grid options
(columns, row height, icon and label size, drawer folders, scroll bar, panel colour and
opacity) do nothing in the list drawer or break its white-on-transparent look.
- Settings home and search hint no longer mention column counts or web results.
Search
- The search field keeps its pill while typing; without a drawer panel behind it, the
dark text sat directly on the wallpaper.
Checked on the Android 17 emulator: drawer search, hiding and restoring an app,
notification dots, swipe down for notifications, the left-edge alphabet and its scrub,
and the slider node in the accessibility tree.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The app is distributed privately, outside any store. Every version is published as a
release with the APK attached in https://git.mariosem.es/msemes/Flexera-Launcher-Releases,
and installed copies update themselves from there.
Versioning
- flexera/version.properties holds the version name (now 0.1.0); build.gradle derives the
version code from it. This replaces Lawnchair's 16_00_02_01_00 / "16.Dev.(hash)".
- Debug builds always use the debug key, so the release key signs releases only.
Updater (app.flexera.update.FlexeraUpdater)
- Asks the release repository for the latest release at most once a day, when the home
screen or settings come to the front, and on demand from Settings > About > Updates.
- Downloads the APK into the cache, checks that it is a newer build of this app, and
hands it to Android's installer. Android enforces the signature.
- Posts a notification when an automatic check finds a newer version.
- Off in debug builds, whose app ID differs from the released one.
Network policy (reverses PRD SI-5, at the owner's request for in-app updates)
- The INTERNET permission is back, plus REQUEST_INSTALL_PACKAGES.
- The update check is the only network use. Lawnchair's online features stay off through
FlexeraNetwork.onlineFeaturesAllowed: web suggestions, the announcement feed, and the
crash-report upload button.
- The About screen's privacy text says what the app now does online.
Releasing
- flexera/release.ps1 raises the version, builds the signed APK, checks its signature,
creates the release and uploads the APK.
Also carries the strings for the next commit (alphabet side and drawer settings).
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
PRD M3 (AD-4 to AD-6, AD-17), plus three look changes the owner asked for.
Wave alphabet
- WaveAlphabetView (flexera/src): a strip of letters on the drawer's end edge showing only
letters that have apps. Touching it jumps the list to that letter with a haptic tick; the
letters near the finger bulge away from the edge and reach further the further in the
finger moves; the selected letter is enlarged.
- The strip reserves the edge from the system back gesture and tells the drawer not to
drag closed under a scrub.
- Launcher3's scroll bar is hidden by default and no longer takes touches while hidden.
Swipe up and hold
- NoButtonNavbarToOverviewTouchController reuses its motion-pause detector on an ordinary
swipe up from the home screen: a pause finishes opening the drawer and hands the same
touch to the alphabet. Swipes that start on the navigation bar keep their stock meaning.
Look
- The drawer fades in over the home screen instead of sliding up from the bottom.
- No background panel or drag handle (drawer opacity default 0); the wallpaper keeps
Lawnchair's blur and scrim.
- App names, letter headers and alphabet letters are white with a soft shadow.
Tested on the Android 17 emulator: quick swipe opens the drawer, edge scrub jumps the
list, swipe up and hold opens the drawer and scrubs from mid-screen, Back closes it.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
PRD M2 (AD-1 to AD-3, AD-7, AD-8, HS-7). The drawer keeps Launcher3's adapter, sorting,
live updates, click, long-press menu and drag; only what differs from the grid is new.
- FlexeraDrawer (flexera/src): column count, row styling, and a letter-header adapter item
- flexera/res/layout/all_apps_icon.xml overrides Launcher3's grid cell with a horizontal
row (46dp icon, name beside it, 64dp tall)
- AlphabeticalAppsList adds a header item at the start of each letter group and points
the fast-scroll section at it; Private Space keeps its own framing
- BaseAllAppsAdapter creates and binds the header view type and styles rows
- DeviceProfileOverrides forces one drawer column
- DeviceProfile.getAllAppsIconStartMargin returns the row padding for a one-column
drawer, which keeps the search bar full width
- Suggested apps row above the list is off by default
Tested on the Android 17 emulator: scrolling, tap to launch, long-press menu, drag to the
home screen, and live removal and return of an app all work. The right-edge fast
scroller did not respond to scripted drags and still needs a hand test.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Lawnchair's screen showed its logo, team and community links and loaded contributor
data from the network. FlexeraAbout (flexera/src) shows the app's own icon, name and
version, credits Lawnchair as the project it is built on, states the privacy position,
and keeps the open-source licence list. It makes no network requests.
The About route in PreferenceNavigation now points at it; Lawnchair's About.kt is untouched.
Tested on the Android 17 emulator: the screen renders, the licence list opens, and
no network request or guard message is logged.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
PRD SI-5: nothing leaves the device.
- flexera/AndroidManifest.xml removes android.permission.INTERNET; it is attached to the
debug and release build types so it outranks flavor and library manifests
- FlexeraNetwork (new, flexera/src): isAvailable() plus a crash guard, because OkHttp
rethrows Android's SecurityException on its dispatcher thread and that killed the launcher
- Drawer search skips web suggestions when the permission is missing
- LawnchairApp installs the guard; the bug reporter ignores the same exception
Tested on the Android 17 emulator: typing in the drawer search and opening the About
screen no longer crash; the guard logged and absorbed six refused requests.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
- App ID com.sema.flexeralauncher (debug builds add .debug)
- App name Flexera Launcher and an adaptive icon built from the artwork in flexera/branding
- New overlay folder flexera/res, wired into the lawnWithQuickstepGithub source set,
so branding and defaults override Lawnchair's without editing its files
- Defaults switched off: At a Glance, Lawnchair's announcement feed, nightly updater
- English strings that named Lawnchair now say Flexera Launcher
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
restartLauncher()'s fallback for when Lawnchair isn't the current default home restarts it via PackageManager.getLaunchIntentForPackage(), which builds a CATEGORY_LAUNCHER (standard-typed) intent to the home activity. Some Android 8-10 OEM ROMs react to that launch by trying to re-type the home task and killing the app with RemoteServiceException("Can't change activity type once set: ... activityType=standard") delivered via SCHEDULE_CRASH, which cannot be caught at runtime. Launch the same activity with a home-category intent pinned to our own package instead; the resolved target and flags are unchanged (NEW_TASK was implicit in the launch intent, CLEAR_TOP stays).
Co-authored-by: caizhixing <caizhixing@users.noreply.github.com>
* feat and fix : Add Tilt to swipe pages setting
Replace the multi-level Motion controls with a single Home screen toggle named “Tilt to swipe pages,” positioned below Infinite scrolling. Update the description to explain that tilting the phone moves between pages while rearranging apps or widgets, and make the pan controller use the toggle directly while retaining the existing preference key for compatibility. Fix for "[BUG] Large empty gap between app drawer search bar and app grid when app suggestions are disabled
#7490"
* EDITS : Coderabbit suggested edits for PR #7500
* Coderabbit suggested changes (2) #7500
* Update gradle-daemon-jvm.properties
* Remove unrelated Gradle toolchain changes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add docstrings for pan-to-move and all apps header changes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add a tilt sensitivity control
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LawnchairFloatingSurfaceView.removeViewImmediate() skipped the removal when
isAttachedToWindow was false. onNewIntent is delivered inside
performResumeActivity, while the window can still be re-attaching: the view is
already a child of the drag layer but not attached yet. The removal was
skipped, so the following show() crashed on addView() with
IllegalStateException: The specified child already has a parent.
You must call removeView() on the child's parent first.
FloatingSurfaceView removes the view unconditionally, and removeViewInLayout()
is a no-op when the view has no parent, so match it.
Co-authored-by: caizhixing <caizhixing@users.noreply.github.com>
lawnchair/res/layout/widgets_two_pane_sheet_paged_view.xml is a leftover copy
of the 2021 two-pane layout that shadows the current one in res/. It is
inflated by WidgetsTwoPaneSheet when a work profile is present, but the AOSP
code has moved on:
- WidgetsFullSheet.setupViews() looks up R.id.search_widgets_list_view, which
only exists in the current layout. The old copy has no search results list,
so the lookup returns null and AdapterHolder.setup() crashes on
setOutlineProvider() while the sheet is being inflated.
- It is also missing R.id.suggestions_header and repeats the "Widgets" title
that the outer widgets_two_pane_sheet.xml already provides.
Deleting the overlay lets the AOSP layout in res/ win, matching the
recyclerview variant which was already realigned with AOSP in #7188.
Co-authored-by: caizhixing <caizhixing@users.noreply.github.com>
* fix(startup): install the shipped baseline profile on sideloaded builds
Removing androidx.startup's InitializationProvider (AOSP b/197780098)
also removed ProfileInstallerInitializer, so GitHub and Nightly APKs
never wrote the bundled baseline profile and ran unoptimised until
background dexopt built a profile of its own.
Do the initializer's job from LawnchairApp instead: main process only,
5 s after start, on the background-priority ordered executor. The
provider stays removed, so no other library initializes eagerly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(startup): only install the profile from the main process on API 26-27
Below API 28 the version check let every process through, including
:bugReport. Look up this pid's process name with ActivityManager there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The bug report file name used the locale-dependent
SimpleDateFormat.getDateTimeInstance(), which yields '/' in some
locales (e.g. ja_JP: "2026/09/28 12:34:56"). The resulting path had
non-existent parent directories, so createNewFile() threw inside the
uncaught exception handler and the default handler was never invoked,
leaving the process frozen instead of restarting.
- Use a fixed, locale-independent format (yyyy-MM-dd_HH-mm-ss, Locale.US)
- Always delegate to the default handler via try/finally so the process
is killed even if generating the report fails
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix: don't crash at startup when the restriction bypass lib is missing
Unseal.unseal() loads ChickenHook's libnrb.so. Repackaged APKs and
multi-open containers can fail to load it, in which case the call throws
UnsatisfiedLinkError (an Error) that catch (Exception) does not handle. This
runs from Application.onCreate(), so the process crash-loops forever.
Catch Exception | LinkageError. A failed unseal only disables the hidden API
exemption for that launch; other Errors (OOM, etc.) still propagate.
* fix: don't crash reading allowEnterPip when the restriction bypass is broken
getAllowEnterPip() uses RestrictionBypass below SDK 35. If the native bypass
library failed to load, the bypass class is permanently broken and any further
access throws LinkageError (ExceptionInInitializerError on the first touch,
NoClassDefFoundError afterwards), which catch (ReflectiveOperationException)
does not handle.
Return false (PIP not allowed) instead of crashing the transition.
---------
Co-authored-by: caizhixing <17589399+caizhixing03@user.noreply.gitee.com>
mSettingsCache is only assigned in onListenerConnected(), but
onListenerDisconnected() unregisters from it unconditionally. The framework
calls onListenerDisconnected() from NotificationListenerService.onDestroy(),
so a service that is destroyed before it ever connects dereferences null and
kills the process.
Guard the unregister; the connected path is unchanged.
Co-authored-by: caizhixing <17589399+caizhixing03@user.noreply.gitee.com>
getAnimationData() read itemsInPreview[0] to derive the base icon size. The
preview list can be empty when the folder has no bound child views yet, which
made opening the folder crash with an IndexOutOfBoundsException.
Fall back to the device profile icon size when the list is empty; the normal
path is unchanged.
Co-authored-by: caizhixing <17589399+caizhixing03@user.noreply.gitee.com>
On Android 11 and below, LawnchairActivityCachingLogic#loadIcon resolves
ActivityInfo through PackageManager#getActivityInfo because
LauncherActivityInfo#getActivityInfo is only public since Android 12. When a
package is updated while the launcher refreshes its icon cache, one of its
launcher activities can vanish between LauncherApps handing out the activity
list and this lookup. The resulting NameNotFoundException is uncaught and
kills the model thread:
Fatal Exception: android.content.pm.PackageManager$NameNotFoundException
at android.app.ApplicationPackageManager.getActivityInfo(ApplicationPackageManager.java:471)
at app.lawnchair.LawnchairActivityCachingLogic.loadIcon(LawnchairActivityCachingLogic.kt:60)
at com.android.launcher3.icons.cache.BaseIconCache.addIconToDBAndMemCache(BaseIconCache.kt:235)
at com.android.launcher3.icons.IconCache.updateIconsForPkg(IconCache.java:206)
at com.android.launcher3.model.PackageUpdatedTask.execute(PackageUpdatedTask.java:160)
at com.android.launcher3.LauncherModel.enqueueModelUpdateTask(LauncherModel.kt:412)
Catch the exception and return BaseIconCache#getDefaultIcon instead:
addIconToDBAndMemCache already treats the default icon as "could not be
loaded" and skips the DB/memory cache entry, matching how other icon load
failures are handled. The two other PackageManager#getActivityInfo call
sites (ThemedIconCompat, SearchResultIcon) already catch this exception; the
CachingLogic was the only unguarded one.
Android 12+ is unaffected: it reads the ActivityInfo snapshot from
LauncherActivityInfo directly.
Co-authored-by: caizhixing <17589399+caizhixing03@user.noreply.gitee.com>
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
* fix(widgets): let the resize frame reach the screen edge horizontally
The resize frame is drawn resize_frame_background_padding (24dp) outside
the widget, but the horizontal drag range clamped the frame's outer edge
to the DragLayer. When the grid sits close to the screen edge (low
workspace/widget padding, many columns) the last column could not be
reached.
When the frame already overhung the screen edge (gap + widget padding
< 24dp, which is the default), the old range did not contain 0 (left
start positive, right end negative), so the first move event snapped the
frame up to 24dp inward on either side. With half-cell resize steps that
would be enough to drop a step.
Bound the widget's edge instead of the frame's, and keep 0 inside the
range so a touch without movement never moves the frame. The vertical
range is unchanged.
* fix(widgets): count the last partial resize step when the finger hits the edge
The finger cannot travel past the screen edge, and the handle can be
grabbed up to 24dp outside the widget. With little workspace/widget
padding, the travel left for the last column can then be shorter than
RESIZE_THRESHOLD of a step.
While a left/right handle is dragged and the finger is within
resize_frame_background_padding of that screen edge, a partial step of
more than 1 - RESIZE_THRESHOLD towards the edge counts as reached. That
threshold keeps the step from being undone right after it is taken, and
it only applies in the growth direction.
* refactor(widgets): shorten resize frame LC-Notes
Comment-only change: trim the edge-clamp LC-Notes in AppWidgetResizeFrame to one or two lines. No behaviour change.
createDatabaseHelper renamed restored.db to the IDP's dbFile but opened
the dbFile it was passed, from the DB_FILE pref. The Nova import writes
grid prefs before restoring, and hotseatColumns' reloadGrid listener can
move the IDP to an intermediate grid (e.g. launcher_6_4_5.db) first. The
restored layout then landed in a file that was never opened, the opened
file was created empty, performRestore sanitized nothing, and the next
start reset the "empty" database to the default layout. A second import
worked because the prefs no longer changed.
Rename into the file that is opened instead.
migrateDbName moves launcher.db to dbName when dbName doesn't exist yet.
With the restore now renamed into dbFile, a dbFile of launcher.db (the
5_by_5 grid) would be moved away and deleted before DatabaseHelper opens
it. Migrate first, then rename.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
SettingsCache observes hidden Settings keys — e.g.
hide_privatespace_entry_point, which ModelInitializer registers a
listener for. Since Android S, hidden settings keys are only readable
by system apps; a query from a third-party app throws
SecurityException.
3962597e6e fixed this for the read path by catching SecurityException
in getValue(), but the ContentObserver path was missed: when the system
writes such a key, onChange() calls updateValue() on the main-thread
handler and the launcher crashes.
Skip the cache update instead, mirroring the existing guard in
getValue().
Co-authored-by: caizhixing <17589399+caizhixing03@user.noreply.gitee.com>
On some OEM builds (e.g. Huawei EMUI on API <= 28), WallpaperManager
.setWallpaperOffsets() internally fetches the current wallpaper bitmap,
and WallpaperManagerService enforces READ_EXTERNAL_STORAGE on that
binder call. When the permission is not granted the call throws
SecurityException, which escaped setOffsetSafely() and fatally crashed
the launcher on every workspace page swipe.
Skip and log the offset update on affected devices instead; wallpaper
parallax simply stops updating there.
Co-authored-by: caizhixing <17589399+caizhixing03@user.noreply.gitee.com>
getMultiWindowDisplaySize() calls WindowManager.getCurrentWindowMetrics(),
added in API 30, without a version check. Lawnchair runs down to
minSdk 26, so building the multi-window device profile
(Launcher#initDeviceProfile and the fallback RecentsActivity) throws
NoSuchMethodError on Android 8-10 devices in split-screen.
Derive the bounds from the default display size below R.
Co-authored-by: caizhixing <17589399+caizhixing03@user.noreply.gitee.com>
VibratorWrapper.vibrate(primitiveId, ...) calls
Vibrator.areAllPrimitivesSupported() and builds a
VibrationEffect.Composition, both added in API 30, without a version
check. Lawnchair runs down to minSdk 26, so dismissing a task in
Recents — TaskViewTouchControllerDeprecated passes PRIMITIVE_TICK, a
non-negative id — throws NoSuchMethodError and crashes the launcher
on Android 8-10 devices.
Fall back to the plain fallback effect below R, mirroring the existing
ATLEAST_S guard in vibrateForTaskbarUnstash() and the ATLEAST_Q branch
that already builds EFFECT_CLICK.
Co-authored-by: caizhixing <17589399+caizhixing03@user.noreply.gitee.com>
config_icon_mask is a framework resource that ROM overlays can replace.
When its value is not valid path data (seen in the wild with a line break
inside a coordinate), PathParser throws during Launcher onCreate and the
launcher enters a crash loop. Parse the mask defensively and fall back
to the platform default mask, mirroring the existing guard around
GenericPathShape in the same file.
Co-authored-by: caizhixing <17589399+caizhixing03@user.noreply.gitee.com>