Neo-Launcher Documentation

Java android & launcher repository by @NeoApplications. Desktop release binaries & installers with 2.2k stars.

2,151 stars Java Java 13 files · ~11,663 tokens #android#launcher
RAW Doc

Technical Documentation: NeoApplications/Neo-Launcher

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (README.md)

Neo Launcher



[!IMPORTANT]
This project is not abandoned. It's just that we (Saul and Antonios) both faced couple of barriers on private life level and technically (on AOSP's side). But the A16-based version is in progress.

Community :speech_balloon:

You can join either our Telegram or Matrix groups to make suggestions, ask questions, receive news, install test builds, or just chat.

Features :abacus:

**Some of the main features**

  • Rich themes support.
  • The Dash.
  • Custom search engines.
  • Gestures support.
  • A unique custom-widget.
  • Categories or folders in the app drawer.
  • Icon shapes and packs support.
  • Customize app icons.
  • Desktop grid preference.
  • Hide apps.
  • Vertical Apps Lists.
  • Icons' scaling.
  • Back up and restore preferences.

There's also a developer's document, where the maintainer and collaborators communicate
their TODOs and plans.

Screenshots :framed_picture:

Contributions

If you have knowledge in Java or Kotlin you can work on additional features or bugs, and open pull requests. Make sure the code is formatted before make a pull request.

You can also contribute to this project by opening issues for bugs or new features.

Support

Translations :left_speech_bubble: [](https://hosted.weblate.org/engage/neolauncher/?utm_source=widget)

Contribute your translations to Neo Launcher on Hosted Weblate. Adding new languages is always accepted and supported.

FAQ

Is QuickSwitch available?

No, it's not, and there is no plans to add it.

Special Thanks :heart:

Thanks to The LawnchairLauncher team for the great work on Lawnchair.

Thanks to Rafael Venâncio for creating the F-Droid repo.

To Donno for the new app icon base.

Thanks to Helena Zhang and Toby Fried for the great Phosphor icons, which we use since 0.9.0.

Contributors :handshake:

Copylefted Libre License :scroll:

Our work is licensed under the GPLv3+.
The original AOSP Launcher3 code is licensed under the Apache License, Version 2.0.

Copyright © 2025 Saul Henriquez, Antonios Hazim and contributors.


Wmshell Src Com Android Wm Shell Docs README (`wmshell/src/com/android/wm/shell/docs/README.md`)

Window Manager Shell Readme

The following docs present more detail about the implementation of the WMShell library (in no
particular order):

  1. What is the Shell
  2. Integration with SystemUI & Launcher
  3. Usage of Dagger
  4. Threading model in the Shell
  5. Making changes in the Shell
  6. Extending the Shell for Products/OEMs
  7. Debugging in the Shell
  8. Testing in the Shell

Todo

  • Per-feature docs
  • Feature flagging
  • Best practices & patterns

Wmshell Src Com Android Wm Shell Docs Overview (`wmshell/src/com/android/wm/shell/docs/overview.md`)

What is the WindowManager Shell


Motivation

The primary motivation for the WindowManager Shell (WMShell) library is to effectively scale
WindowManager by making it easy™ and safe to create windowing features to fit the needs of
various Android products and form factors.

To achieve this, WindowManager separates the policy of managing windows (WMCore) from the
presentation of surfaces (WMShell) and provides a minimal interface boundary for the two to
communicate.

Who is using the library?

Currently, the WMShell library is used to drive the windowing experience on handheld
(phones & tablets), TV, Auto, Arc++, and Wear to varying degrees.

Where does the code live

The core WMShell library code is currently located in
the frameworks/base/libs/WindowManager/Shell
directory and is included as a part dependency of the host SystemUI apk.

How do I build the Shell library

The library can be built directly by running:

shell
m WindowManager-Shell

But this is mainly useful for inspecting the contents of the library or verifying it builds. The
various targets can be found in the Shell
library's Android.bp
file.

Normally, you would build it as a part of the host SystemUI, for example via commandline:

shell
# Phone SystemUI variant
m SystemUI
adevice update

Or preferably, if you are making WMShell/SysUI only changes (no other framework changes), then
building via go/sysui-studio allows for very quick iteration (one click
build and push of SysUI in < 30s).

If you are making framework changes and are using aidegen to set up your platform IDE, make sure
to include the appropriate directories to build, for example:

shell
# frameworks/base will include base/libs/WindowManager/Shell and base/packages/SystemUI
aidegen frameworks/base \
    vendor/<oem>/packages/SystemUI \
    ...

Libs Systemui Utils Kairos Docs Flow To Kairos Cheatsheet (`libs_systemui/utils/kairos/docs/flow-to-kairos-cheatsheet.md`)

From Flows to Kairos

Key differences

  • Kairos evaluates all events (TFlow emissions + observers) in a transaction.

  • Kairos splits Flow APIs into two distinct types: TFlow and TState

    • TFlow is roughly equivalent to SharedFlow w/ a replay cache that
      exists for the duration of the current Kairos transaction and shared with
      SharingStarted.WhileSubscribed()

    • TState is roughly equivalent to StateFlow shared with
      SharingStarted.Eagerly, but the current value can only be queried within
      a Kairos transaction, and the value is only updated at the end of the
      transaction

  • Kairos further divides Flow APIs based on how they internally use state:

    • FrpTransactionScope: APIs that internally query some state need to be
      performed within an Kairos transaction

      • this scope is available from the other scopes, and from most lambdas
        passed to other Kairos APIs
    • FrpStateScope: APIs that internally accumulate state in reaction to
      events need to be performed within an FRP State scope (akin to a
      CoroutineScope)

      • this scope is a side-effect-free subset of FrpBuildScope, and so can be
        used wherever you have an FrpBuildScope
    • FrpBuildScope: APIs that perform external side-effects (Flow.collect)
      need to be performed within an FRP Build scope (akin to a CoroutineScope)

      • this scope is available from FrpNetwork.activateSpec { … }
    • All other APIs can be used anywhere

emptyFlow()

Use emptyTFlow

kotlin
// this TFlow emits nothing
val noEvents: TFlow<Int> = emptyTFlow

map { … }

Use TFlow.map / TState.map

kotlin
val anInt: TState<Int> = …
val squared: TState<Int> = anInt.map { it * it }
val messages: TFlow<String> = …
val messageLengths: TFlow<Int> = messages.map { it.size }

filter { … } / mapNotNull { … }

I have a TFlow

Use TFlow.filter / TFlow.mapNotNull

kotlin
val messages: TFlow<String> = …
val nonEmpty: TFlow<String> = messages.filter { it.isNotEmpty() }

I have a TState

Convert the TState to TFlow using TState.stateChanges, then use
TFlow.filter / TFlow.mapNotNull

If you need to convert back to TState, use TFlow.hold(initialValue) on the
result.

kotlin
tState.stateChanges.filter { … }.hold(initialValue)

Note that TFlow.hold is only available within an FrpStateScope in order to
track the lifetime of the state accumulation.

combine(...) { … }

I have TStates

Use combine(TStates)

kotlin
val someInt: TState<Int> = …
val someString: TState<String> = …
val model: TState<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) }

I have TFlows

Convert the TFlows to TStates using TFlow.hold(initialValue), then use
combine(TStates)

If you want the behavior of Flow.combine where nothing is emitted until each
TFlow has emitted at least once, you can use filter:

kotlin
// null used as an example, can use a different sentinel if needed
combine(tFlowA.hold(null), tFlowB.hold(null)) { a, b ->
        a?.let { b?.let { … } }
    }
    .filterNotNull()

Note that TFlow.hold is only available within an FrpStateScope in order to
track the lifetime of the state accumulation.

Explanation

Flow.combine always tracks the last-emitted value of each Flow it's
combining. This is a form of state-accumulation; internally, it collects from
each Flow, tracks the latest-emitted value, and when anything changes, it
re-runs the lambda to combine the latest values.

An effect of this is that Flow.combine doesn't emit until each combined Flow
has emitted at least once. This often bites developers. As a workaround,
developers generally append .onStart { emit(initialValue) } to the Flows
that don't immediately emit.

Kairos avoids this gotcha by forcing usage of TState for combine, thus
ensuring that there is always a current value to be combined for each input.

collect { … }

Use observe { … }

kotlin
val job: Job = tFlow.observe { println("observed: $it") }

Note that observe is only available within an FrpBuildScope in order to
track the lifetime of the observer. FrpBuildScope can only come from a
top-level FrpNetwork.transaction { … }, or a sub-scope created by using a
-Latest operator.

sample(flow) { … }

I want to sample a TState

Use TState.sample() to get the current value of a TState. This can be
invoked anywhere you have access to an FrpTransactionScope.

kotlin
// the lambda passed to map receives an FrpTransactionScope, so it can invoke
// sample
tFlow.map { tState.sample() }

Explanation

To keep all state-reads consistent, the current value of a TState can only be
queried within a Kairos transaction, modeled with FrpTransactionScope. Note
that both FrpStateScope and FrpBuildScope extend FrpTransactionScope.

I want to sample a TFlow

Convert to a TState by using TFlow.hold(initialValue), then use sample.

Note that hold is only available within an FrpStateScope in order to track
the lifetime of the state accumulation.

stateIn(scope, sharingStarted, initialValue)

Use TFlow.hold(initialValue). There is no need to supply a sharingStarted
argument; all states are accumulated eagerly.

kotlin
val ints: TFlow<Int> = …
val lastSeenInt: TState<Int> = ints.hold(initialValue = 0)

Note that hold is only available within an FrpStateScope in order to track
the lifetime of the state accumulation (akin to the scope parameter of
Flow.stateIn). FrpStateScope can only come from a top-level
FrpNetwork.transaction { … }, or a sub-scope created by using a -Latest
operator. Also note that FrpBuildScope extends FrpStateScope.

distinctUntilChanged()

Use distinctUntilChanged like normal. This is only available for TFlow;
TStates are already distinctUntilChanged.

merge(...)

I have TFlows

Use merge(TFlows) { … }. The lambda argument is used to disambiguate multiple
simultaneous emissions within the same transaction.

Explanation

Under Kairos's rules, a TFlow may only emit up to once per transaction. This
means that if we are merging two or more TFlows that are emitting at the same
time (within the same transaction), the resulting merged TFlow must emit a
single value. The lambda argument allows the developer to decide what to do in
this case.

I have TStates

If combine doesn't satisfy your needs, you can use TState.stateChanges to
convert to a TFlow, and then merge.

conflatedCallbackFlow { … }

Use tFlow { … }.

As a shortcut, if you already have a conflatedCallbackFlow { … }, you can
convert it to a TFlow via Flow.toTFlow().

Note that tFlow is only available within an FrpBuildScope in order to track
the lifetime of the input registration.

first()

I have a TState

Use TState.sample.

I have a TFlow

Use TFlow.nextOnly, which works exactly like Flow.first but instead of
suspending it returns a TFlow that emits once.

The naming is intentionally different because first implies that it is the
first-ever value emitted from the Flow (which makes sense for cold Flows),
whereas nextOnly indicates that only the next value relative to the current
transaction (the one nextOnly is being invoked in) will be emitted.

Note that nextOnly is only available within an FrpStateScope in order to
track the lifetime of the state accumulation.

flatMapLatest { … }

If you want to use -Latest to cancel old side-effects, similar to what the Flow
-Latest operators offer for coroutines, see mapLatest.

I have a TState…

…and want to switch TStates

Use TState.flatMap

kotlin
val flattened = tState.flatMap { a -> getTState(a) }

…and want to switch TFlows

Use TState<TFlow<T>>.switch()

kotlin
val tFlow = tState.map { a -> getTFlow(a) }.switch()

I have a TFlow…

…and want to switch TFlows

Use hold to convert to a TState<TFlow<T>>, then use switch to switch to
the latest TFlow.

kotlin
val tFlow = tFlowOfFlows.hold(emptyTFlow).switch()

…and want to switch TStates

Use hold to convert to a TState<TState<T>>, then use flatMap to switch to
the latest TState.

kotlin
val tState = tFlowOfStates.hold(tStateOf(initialValue)).flatMap { it }

mapLatest { … } / collectLatest { … }

FrpStateScope and FrpBuildScope both provide -Latest operators that
automatically cancel old work when new values are emitted.

kotlin
val currentModel: TState<SomeModel> = …
val mapped: TState<...> = currentModel.mapLatestBuild { model ->
    effect { "new model in the house: $model" }
    model.someState.observe { "someState: $it" }
    val someData: TState<SomeInfo> =
        getBroadcasts(model.uri)
            .map { extractInfo(it) }
            .hold(initialInfo)
    …
}

flowOf(...)

I want a TState

Use tStateOf(initialValue).

I want a TFlow

Use now.map { initialValue }

Note that now is only available within an FrpTransactionScope.

Explanation

TFlows are not cold, and so there isn't a notion of "emit this value once
there is a collector" like there is for Flow. The closest analog would be
TState, since the initial value is retained indefinitely until there is an
observer. However, it is often useful to immediately emit a value within the
current transaction, usually when using a flatMap or switch. In these cases,
using now explicitly models that the emission will occur within the current
transaction.

kotlin
fun <T> FrpTransactionScope.tFlowOf(value: T): TFlow<T> = now.map { value }

MutableStateFlow / MutableSharedFlow

Use MutableTState(frpNetwork, initialValue) and MutableTFlow(frpNetwork).


Libs Systemui Utils Kairos Docs Semantics (`libs_systemui/utils/kairos/docs/semantics.md`)

FRP Semantics

Kairos's pure API is based off of the following denotational semantics
(wikipedia).

The semantics model Kairos types as time-varying values; by making Time a
first-class value, we can define a referentially-transparent API that allows us
to reason about the behavior of the pure Kairos combinators. This is
implementation-agnostic; we can compare the behavior of any implementation with
expected behavior denoted by these semantics to identify bugs.

The semantics are written in pseudo-Kotlin; places where we are deviating from
real Kotlin are noted with comments.

text
/* Detailed source-code truncated for AI context efficiency. */

Wmshell Src Com Android Wm Shell Docs Changes (`wmshell/src/com/android/wm/shell/docs/changes.md`)

Making changes in the Shell


Code reviews

In addition to the individual reviewers who are most familiar with the changes you are making,
please also add [email protected] to keep other WM folks
in the loop.

Adding new code

Internal Shell utility classes

If the new component is used only within the WMShell library, then there are no special
considerations, go ahead and add it (in the com.android.wm.shell.common package for example)
and make sure the appropriate unit tests are added.

Internal Shell components

If the new component is to be used by other components/features within the Shell library, then
you can create an appropriate package for this component to add your new code. The current
pattern is to have a single <Component name>Controller that handles the initialization of the
component.

As mentioned in the Dagger usage docs, you need to determine whether it should go into:

  • WMShellBaseModule for components that other base & product components will depend on
  • or WMShellModule, TvWmShellModule, etc. for product specific components that no base
    components depend on

SysUI accessible components

In addition to doing the above, you will also need to provide an interface for calling to SysUI
from the Shell and vice versa. The current pattern is to have a parallel Optional<Component name>
interface that the <Component name>Controller implements and handles on the main Shell thread
(see SysUI/Shell threading).

In addition, because components accessible to SysUI injection are explicitly listed, you'll have to
add an appropriate method in WMComponent to get the interface and update the Builder in
SysUIComponent to take the interface so it can be injected in SysUI code. The binding between
the two is done in SystemUIFactory#init() which will need to be updated as well.

Specifically, to support calling into a controller from an external process (like Launcher):

  • Create an implementation of the external interface within the controller
  • Have all incoming calls post to the main shell thread (inject @ShellMainThread Executor into the
    controller if needed)
  • Note that callbacks into SysUI should take an associated executor to call back on

Launcher accessible components

Because Launcher is not a part of SystemUI and is a separate process, exposing controllers to
Launcher requires a new AIDL interface to be created and implemented by the controller. The
implementation of the stub interface in the controller otherwise behaves similar to the interface
to SysUI where it posts the work to the main Shell thread.

Specifically, to support calling into a controller from an external process (like Launcher):

  • Create an implementation of the interface binder's Stub class within the controller, have it
    extend ExternalInterfaceBinder and implement invalidate() to ensure it doesn't hold long
    references to the outer controller
  • Make the controller implement RemoteCallable<T>, and have all incoming calls use one of
    the executeRemoteCallWithTaskPermission() calls to verify the caller's identity and ensure the
    call happens on the main shell thread and not the binder thread
  • Inject ShellController and add the instance of the implementation as external interface
  • In Launcher, update TouchInteractionService to pass the interface to SystemUIProxy, and then
    call the SystemUIProxy method as needed in that code

Component initialization

To initialize the component:

  • On the Shell side, you potentially need to do two things to initialize the component:
    • Inject ShellInit into your component and add an init callback
    • Ensure that your component is a part of the dagger dependency graph, either by:
      • Making this component a dependency of an existing component already exposed to SystemUI
      • Explicitly add this component to the WMShellBaseModule @ShellCreateTrigger provider or
        the @ShellCreateTriggerOverride provider for your product module to expose it explicitly
        if it is a completely independent component
  • On the SysUI side, update WMShell to setup any bindings for the component that depend on
    SysUI code

To verify that your component is being initialized at startup, you can enable the WM_SHELL_INIT
protolog group and restart the SysUI process:

shell
adb shell wm logging enable-text WM_SHELL_INIT
adb shell kill `pid com.android.systemui`
adb logcat *:S WindowManagerShell

General Do's & Dont's

Do:

  • Add unit tests for all new components
  • Keep controllers simple and break them down as needed
  • Any SysUI callbacks should also take an associated executor to run the callback on

Don't:

  • Don't do initialization in the constructor, only do initialization in the init callbacks.
    Otherwise it complicates the building of the dependency graph.
  • Don't create dependencies from base-module components on specific features (the base module
    is intended for use with all products)
    • Try adding a mechanism to register and listen for changes from the base module component
      instead
  • Don't add blocking synchronous calls in the SysUI interface between Shell & SysUI
    • Try adding a push-mechanism to share data, or an async callback to request data

Exposing shared code for use in Launcher

Launcher doesn't currently build against the Shell library, but needs to have access to some shared
AIDL interfaces and constants. Currently, all AIDL files, and classes under the
com.android.wm.shell.util package are automatically built into the SystemUISharedLib that
Launcher uses.

If the new code doesn't fall into those categories, they can be added explicitly in the Shell's
Android.bp file under the
wm_shell_util-sources filegroup.


Wmshell Src Com Android Wm Shell Docs Dagger (`wmshell/src/com/android/wm/shell/docs/dagger.md`)

Usage of Dagger in the Shell library


Dependencies

Dagger is not required to use the Shell library, but it has a lot of obvious benefits:

  • Not having to worry about how to instantiate all the dependencies of a class, especially as
    dependencies evolve (ie. product controller depends on base controller)
  • Can create boundaries within the same app to encourage better code modularity

As such, the Shell also tries to provide some reasonable out-of-the-box modules for use with Dagger.

Modules

All the Dagger related code in the Shell can be found in the com.android.wm.shell.dagger package,
this is intentional as it keeps the "magic" in a single location. The explicit nature of how
components in the shell are provided is as a result a bit more verbose, but it makes it easy for
developers to jump into a few select files and understand how different components are provided
(especially as products override components).

The module dependency tree looks a bit like:

Ideally features could be abstracted out into their own modules and included as needed by each
product.

Overriding base components

In some rare cases, there are base components that can change behavior depending on which
product it runs on. If there are hooks that can be added to the component, that is the
preferable approach.

The alternative is to use
the @DynamicOverride
annotation to allow the product module to provide an implementation that the base module can
reference. This is most useful if the existence of the entire component is controlled by the
product and the override implementation is optional (there is a default implementation). More
details can be found in the class's javadoc.


Wmshell Src Com Android Wm Shell Docs Debugging (`wmshell/src/com/android/wm/shell/docs/debugging.md`)

Debugging in the Shell


Logging & ProtoLogs

The interactions in the Shell can be pretty complicated, so having good logging is crucial to
debugging problems that arise (especially in dogfood). The Shell uses the same efficient Protolog
mechanism as WM Core, which can be enabled at runtime on debug devices.

TLDR  Don’t use Logs or Slogs except for error cases, Protologs are much more flexible,
easy to add and easy to use

Adding a new ProtoLog

Update ShellProtoLogGroup to include a new log group (ie. NEW_FEATURE) for the content you want to
log. ProtoLog log calls mirror Log.v/d/e(), and take a format message and arguments:

java
ProtoLog.v(NEW_FEATURE, "Test log w/ params: %d %s", 1, “a”)

This code itself will not compile by itself, but the protologtool will preprocess the file when
building to check the log state (is enabled) before printing the print format style log.

Notes

  • ProtoLogs are only fully supported from soong builds (ie. via make/mp). In SysUI-studio it falls
    back to log via Logcat
  • Non-text ProtoLogs are not currently supported with the Shell library (you can't view them with
    traces in Winscope)

Kotlin

Protolog tool does not yet have support for Kotlin code (
see b/168581922).
For logging in Kotlin, use
the KtProtoLog
class which has a similar API to the Java ProtoLog class.

Enabling ProtoLog command line logging

Run these commands to enable protologs (in logcat) for WM
Core (list of all core tags):

shell
adb shell wm logging enable-text TAG
adb shell wm logging disable-text TAG

And these commands to enable protologs (in logcat) for WM
Shell (list of all shell tags):

shell
adb shell dumpsys activity service SystemUIService WMShell protolog enable-text TAG
adb shell dumpsys activity service SystemUIService WMShell protolog enable-text TAG

Winscope Tracing

The Winscope tool is extremely useful in determining what is happening on-screen in both
WindowManager and SurfaceFlinger. Follow go/winscope to learn how to
use the tool. This trace will contain all the information about the windows/activities/surfaces on
screen.

WindowManager/SurfaceFlinger hierarchy dump

A quick way to view the WindowManager hierarchy without a winscope trace is via the wm dumps:

shell
adb shell dumpsys activity containers

Likewise, the SurfaceFlinger hierarchy can be dumped for inspection by running:

shell
adb shell dumpsys SurfaceFlinger
# Search output for "Layer Hierarchy"

Tracing global SurfaceControl transaction updates

While Winscope traces are very useful, it sometimes doesn't give you enough information about which
part of the code is initiating the transaction updates. In such cases, it can be helpful to get
stack traces when specific surface transaction calls are made, which is possible by enabling the
following system properties for example:

shell
# Enabling
adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha  # matches the name of the SurfaceControlTransaction method
adb shell setprop persist.wm.debug.sc.tx.log_match_name com.android.systemui # matches the name of the surface
adb reboot
adb logcat -s "SurfaceControlRegistry"

# Disabling logging
adb shell setprop persist.wm.debug.sc.tx.log_match_call \"\"
adb shell setprop persist.wm.debug.sc.tx.log_match_name \"\"
adb reboot

It is not necessary to set both log_match_call and log_match_name, but note logs can be quite
noisy if unfiltered.

Tracing activity starts in the app process

It's sometimes useful to know when to see a stack trace of when an activity starts in the app code
(ie. if you are repro'ing a bug related to activity starts). You can enable this system property to
get this trace:

shell
# Enabling
adb shell setprop persist.wm.debug.start_activity true
adb reboot
adb logcat -s "Instrumentation"

# Disabling
adb shell setprop persist.wm.debug.start_activity \"\"
adb reboot

Dumps

Because the Shell library is built as a part of SystemUI, dumping the state is currently done as a
part of dumping the SystemUI service. Dumping the Shell specific data can be done by specifying the
WMShell SysUI service:

shell
adb shell dumpsys activity service SystemUIService WMShell

If information should be added to the dump, either:

  • Update WMShell if you are dumping SysUI state
  • Inject ShellCommandHandler into your Shell class, and add a dump callback

Shell commands

It can be useful to add additional shell commands to drive and test specific interactions.

To add a new command for your feature, inject a ShellCommandHandler into your class and add a
shell command handler in your controller.

shell
# List all available commands
adb shell dumpsys activity service SystemUIService WMShell help

# Run a specific command
adb shell dumpsys activity service SystemUIService WMShell <cmd> <args> ...

Debugging in Android Studio

If you are using the go/sysui-studio project, then you can debug Shell
code directly from Android Studio like any other app.


Wmshell Src Com Android Wm Shell Docs Extending (`wmshell/src/com/android/wm/shell/docs/extending.md`)

Extending the Shell for Products/OEMs


General Do's & Dont's

Do:

  •  

Don't

  • Don't override classes provided by WMShellBaseModule, it makes it difficult to make
    simple changes to the Shell library base modules which are shared by all products
    • If possible add mechanisms to modify the base class behavior

Wmshell Src Com Android Wm Shell Docs Sysui (`wmshell/src/com/android/wm/shell/docs/sysui.md`)

Shell & SystemUI


Setup

The SystemUI of various products depend on and build against the WM Shell library. To ensure
that we don't inadvertently build dependencies between the Shell library and one particular
product (ie. handheld SysUI), we deliberately separate the initialization of the WM Shell
component from the SysUI component when set up through Dagger.

TLDR  Initialize everything as needed in the WM component scope and export only well
defined interfaces to SysUI.

Initialization

There are more details in the Dagger docs, but the general overview of the SysUI/Shell
initialization flow is such:

  1. SysUI Global scope is initialize (see GlobalModule and its included modules)
  2. WM Shell scope is initialized, for example
    1. On phones: WMComponent includes WMShellModule which includes WMShellBaseModule
      (common to all SysUI)
    2. On TVs: TvWMComponent includes TvWMShellModule which includes WMShellBaseModule
    3. etc.
  3. SysUI explicitly passes interfaces provided from the WMComponent to SysUIComponent via
    the SysUIComponent#Builder, then builds the SysUI scoped components
  4. WMShell is the SystemUI “service” (in the SysUI scope) that initializes with the app after the
    SystemUI part of the dependency graph has been created. It contains the binding code between the
    interfaces provided by the Shell and the rest of SystemUI.
  5. SysUI can inject the interfaces into its own components

More detail can be found in go/wm-sysui-dagger.

Interfaces from SysUI to Shell components

Within the same process, the WM Shell components can be running on a different thread than the main
SysUI thread (disabled on certain products). This introduces challenges where we have to be
careful about how SysUI calls into the Shell and vice versa.

As a result, we enforce explicit interfaces between SysUI and Shell components, and the
implementations of the interfaces on each side need to post to the right thread before it calls
into other code.

For example, you might have:

  1. (Shell) ShellFeature interface to be used from SysUI
  2. (Shell) ShellFeatureController handles logic, implements ShellFeature interface and posts to
    main Shell thread
  3. SysUI application init injects Optional as an interface to SysUI to call
  4. (SysUI) SysUIFeature depends on ShellFeature interface
  5. (SysUI) SysUIFeature injects Optional, and sets up a callback for the Shell to
    call, and the callback posts to the main SysUI thread

Adding an interface to a Shell component may seem like a lot of boiler plate, but is currently
necessary to maintain proper threading and logic isolation.

Listening for Configuration changes & other SysUI events

Aside from direct calls into Shell controllers for exposed features, the Shell also receives
common event callbacks from SysUI via the ShellController. This includes things like:

  • Configuration changes
  • Keyguard events
  • Shell init
  • Shell dumps & commands

For other events which are specific to the Shell feature, then you can add callback methods on
the Shell feature interface. Any such calls should never be synchronous calls as
they will need to post to the Shell main thread to run.

Shell commands & Dumps

Since the Shell library is a part of the SysUI process, it relies on SysUI to trigger commands
on individual Shell components, or to dump individual shell components.

shell
# Dump everything
adb shell dumpsys activity service SystemUIService WMShell

# Run a specific command
adb shell dumpsys activity service SystemUIService WMShell help
adb shell dumpsys activity service SystemUIService WMShell <cmd> <args> ...

More detail can be found in Debugging in the Shell section.


Wmshell Src Com Android Wm Shell Docs Testing (`wmshell/src/com/android/wm/shell/docs/testing.md`)

Testing


Unit tests

New WM Shell unit tests can be added to the
Shell/tests/unittest directory, and can
be run via command line using atest:

shell
atest WMShellUnitTests

If you use the SysUI Studio project, you can run and debug tests directly in the source files
(click on the little arrows next to the test class or test method).

These unit tests are run as a part of WindowManager presubmit, and the dashboards for these unit
tests tests can be found at go/wm-tests.

This GCL file configures the tests being run on the server.

Flicker tests

Flicker tests are tests that perform actions and make assertions on the state in Window Manager
and SurfaceFlinger traces captured during the run.

New WM Shell Flicker tests can be added to the
Shell/tests/flicker directory, and can be run via command
line using atest:

shell
# Bubbles
atest WMShellFlickerTestsBubbles

# PIP
atest WMShellFlickerTestsPip1
atest WMShellFlickerTestsPip2
atest WMShellFlickerTestsPip3
atest WMShellFlickerTestsPipApps
atest WMShellFlickerTestsPipAppsCSuite

# Splitscreen
atest WMShellFlickerTestsSplitScreenGroup1
atest WMShellFlickerTestsSplitScreenGroup2

# Other
atest WMShellFlickerTestsOther

Note: Currently Flicker tests can only be run from the commandline and not via SysUI Studio

A subset of the flicker tests tests are run as a part of WindowManager presubmit, and the
dashboards for these tests tests can be found at go/wm-tests-flicker.

CTS tests

Some windowing features also have CTS tests to ensure consistent behavior across OEMs. For example:

These can also be run via commandline only using atest, for example:

shell
atest PinnedStackTests

Wmshell Src Com Android Wm Shell Docs Threading (`wmshell/src/com/android/wm/shell/docs/threading.md`)

Threading


Boundaries

text
Thread boundary
                                          |
                                WM Shell  | SystemUI
                                          |
                                          |
FeatureController <-> FeatureInterface <--|--> WMShell <-> SysUI
       |          (^post to shell thread) |            (^post to main thread)
      ...                                 |
       |                                  |
 OtherControllers                         |

Threads

We currently have multiple threads in use in the Shell library depending on the configuration by
the product.

  • SysUI main thread (standard main thread)
  • ShellMainThread (only used if the resource config_enableShellMainThread is set true
    (ie. phones))
    • This falls back to the SysUI main thread otherwise
    • Note:
      • This thread runs with THREAD_PRIORITY_DISPLAY priority since so many windowing-critical
        components depend on it
      • This is also the UI thread for almost all UI created by the Shell
      • The Shell main thread Handler (and the Executor that wraps it) is async, so
        messages/runnables used via this Handler are handled immediately if there is no sync
        messages prior to it in the queue.
  • ShellBackgroundThread (for longer running tasks where we don't want to block the shell main
    thread)
    • This is always another thread even if config_enableShellMainThread is not set true
    • Note:
      • This thread runs with THREAD_PRIORITY_BACKGROUND priority
  • ShellAnimationThread (currently only used for Transitions and Splitscreen, but potentially all
    animations could be offloaded here)
  • ShellSplashScreenThread (only for use with splashscreens)

Dagger setup

The threading-related components are provided by
the WMShellConcurrencyModule,
for example, the Executors and Handlers for the various threads that are used. You can request
an executor of the necessary type by using the appropriate annotation for each of the threads (ie.
@ShellMainThread Executor) when injecting into your Shell component.

To get the SysUI main thread, you can use the @Main annotation.

Best practices

Components

  • Don't do initialization in the Shell component constructors
    • If the host SysUI is not careful, it may construct the WMComponent dependencies on the main
      thread, and this reduces the likelihood that components will intiailize on the wrong thread
      in such cases
  • Be careful of using CountDownLatch and other blocking synchronization mechanisms in Shell code
    • If the Shell main thread is not a separate thread, this will cause a deadlock
  • Callbacks, Observers, Listeners to any non-shell component should post onto main Shell thread
    • This includes Binder calls, SysUI calls, BroadcastReceivers, etc. Basically any API that
      takes a runnable should either be registered with the right Executor/Handler or posted to
      the main Shell thread manually
  • Since everything in the Shell runs on the main Shell thread, you do not need to explicitly
    synchronize your code (unless you are trying to prevent reentrantcy, but that can also be
    done in other ways)

Handlers/Executors

  • You generally never need to create Handlers explicitly, instead inject @ShellMainThread ShellExecutor instead
    • This is a common pattern to defer logic in UI code, but the Handler created wraps the Looper
      that is currently running, which can be wrong (see above for initialization vs construction)
  • That said, sometimes Handlers are necessary because Framework API only takes Handlers or you
    want to dedupe multiple messages
    • In such cases inject @ShellMainThread Handler or use view.getHandler() which should be OK
      assuming that the view root was initialized on the main Shell thread
  • Never use Looper.getMainLooper()
    • It's likely going to be wrong, you can inject @Main ShellExecutor to get the SysUI main
      thread

Testing

  • You can use a TestShellExecutor to control the processing of messages

Wmshell Src Com Android Wm Shell Docs Patterns TEMPLATE (`wmshell/src/com/android/wm/shell/docs/patterns/TEMPLATE.md`)

Pattern (one line description)

What this pattern solves

Give detailed information about the problem that this pattern addresses, include when it
should/should not be used.

How it works

Give a high level overview of how this pattern works technically with sufficient links for the
relevant dependencies for folks to read more.

Code examples

Explain how this pattern is used in code.

File.kt:

kotlin
fun someFunction() {
    // Use this code
}

Add relevant links to other files that implement this pattern, design docs, or other important
info.

Link 1: More info
Link 2: More info


--- METRICS ---