A focus-oriented launcher for a Pixel 6. One button swaps the phone to a quieter home screen — a few apps, no notifications — and one gesture brings the normal launcher back.
One locked requirement, everything else open.
From the phone's normal launcher, hit a button and it swaps to a smaller, lighter, focus-oriented launcher: just minimal apps and notifications. And a clear way back to the classic launcher.
Everything below that line is a proposal. The important framing: this is a tool for one person on one phone. Both products I studied are commercial and carry a lot of weight that exists only because they must serve strangers — onboarding wizards, subscription tiers, six configurable profiles, analytics, Play Store policy compliance. None of that is load-bearing here, and cutting it is most of what makes this project small enough to actually finish.
Fairphone open-sourced Moments. That turned guesswork into evidence.
Shipped on the Fairphone (Gen. 6), Android 15. A lime-green physical switch on the side of the phone flips into a minimal mode. Five presets — Essentials (Camera, Browser, Maps, Messages, Phone), Deep Focus, Recharge, Journey, Quality Time — each capped at five apps, up to six saved. Notifications and calls are blocked by default. Fairphone's own line: "clicking in and out of apps doesn't take you back to your Android home screen."
The source is public — fairphone/fairphone-moments, Kotlin + Compose, EUPL-1.2, internal codename SpringLauncher. Reading it was the single most useful hour of this investigation:
| What I found in the source | Why it matters here |
|---|---|
Android.bp declares privileged: true, presigned: true.
The manifest asks for WRITE_SECURE_SETTINGS, INTERACT_ACROSS_USERS_FULL,
MODIFY_DAY_NIGHT_MODE, NETWORK_AIRPLANE_MODE, POWER_SAVER,
each tagged tools:ignore="ProtectedPermissions". |
Moments is a privileged system app on an OS Fairphone controls. This is the headline finding. Most of its magic is not clever app code, it is platform ownership — and it is not available to us. See §03. |
The switch handler does exactly two things:
startActivity(NEW_TASK|CLEAR_TASK|NO_ANIMATION) to enter, and
instance?.finish() to leave. |
The "launcher swap" is an activity lifecycle toggle. It only reads as a swap because the surrounding framework is theirs. Copying the technique without the privileges gets you nothing. |
Notifications are handled with AutomaticZenRule + ZenPolicy
(Do Not Disturb), via ACCESS_NOTIFICATION_POLICY. The
NotificationInterceptorService and CallInterceptorReceiver exist in the
tree but are commented out of the manifest: "Disabling these components for
now until they are properly implemented." |
A funded team with system privileges tried notification interception and shipped DND instead. That is a strong, free lesson: start with DND, treat reading notifications as a later, optional step. |
| A profile stores: name, icon, two gradient colours, app list, allowed contacts, repeat-call, wallpaper, dark mode, blue-light filter, sound, battery saver, brightness. | A good checklist of what a "mode" can contain. Most entries need privileged permissions we don't have — useful for knowing what to cut early rather than discover late. |
Firebase Analytics on every switch event; a demo mode for retail units; a Play Store
rejection worked around by dropping MANAGE_EXTERNAL_STORAGE; users reporting
Play Protect flagging the app. |
Pure commercial-product overhead. All of it disappears when the audience is one person and distribution is a sideloaded APK. |
A third-party Android launcher (com.qqlabs.minimalistlauncher), and a
philosophically different animal. It is not a mode you enter — it is a launcher you
live in, permanently replacing your home screen. Its features cluster around
restriction and measurement:
The wider genre (Before Launcher, Olauncher, Niagara) converges on the same grammar: swipe right for a searchable text app list, swipe left for filtered notifications, a small pinned favourites row, edge-scrub alphabet navigation.
Fairphone gives you a mode you switch into and out of. Minimalist Phone gives you a permanent restriction plus willpower tooling. The brief here is explicitly the first shape. So: Moments' model, Minimalist Phone's typography, neither's business model.
On a stock Pixel, taking over the home screen is one tap. Giving it back is not symmetric.
An Android launcher is just an activity with this in its manifest:
<intent-filter><action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.HOME"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>Which of the installed candidates actually receives the HOME intent is decided by the
role android.app.role.HOME. From AOSP's roles.xml, that role is
exclusive="true", exclusivity="user", and — critically — it carries a
requestTitle and requestDescription. A role with those is
requestable: RoleManager.createRequestRoleIntent(ROLE_HOME) puts up a
system dialog, the user taps once, and your app is the home app.
There is no public API for an app to give up a role it holds. Taking the home screen is one tap; handing it back has no equivalent call. Every design below is shaped by that asymmetry, and it is the main reason this can't simply mirror how Moments works.
Fairphone sidesteps it entirely by being part of the OS. We can't. So the realistic options are:
| Option | How the round trip works | Verdict |
|---|---|---|
| A — Focus holds HOME permanently, forwards to Pixel Launcher when off | Zero taps. Focus is always the home app; when inactive it immediately launches
NexusLauncherActivity so you see your normal launcher. |
Rejected Pixel Launcher is bound to Quickstep as the role holder. Driving it while it doesn't hold the role invites flicker, broken home/recents animations, and a configuration Google never tests. Too fragile for a daily driver. |
| B — Focus takes HOME on entry, releases it on exit | In: one system dialog, one tap. Out: the system Home-app picker via
Settings.ACTION_HOME_SETTINGS. |
Recommended Works with the platform instead of against it. No privileged permissions, no root, no custom ROM. Costs a tap each way. |
| C — Focus is the only launcher; no classic mode at all | Build a full-featured launcher and a minimal one in the same app. | Rejected Contradicts the brief, and means writing a real launcher — grids, folders, pages, widgets. Ten times the project. |
The HOME role's <required-components> is "an activity with MAIN + HOME", and
AOSP's roles.xml carries a comment pointing at
HomeRoleBehavior.getFallbackHolder(). That suggests a possibility worth one evening:
if Focus disables its own HOME activity with setComponentEnabledSetting, it stops
satisfying the required component, and the role may fall back automatically to Pixel Launcher.
If it works, exiting focus mode becomes zero-tap and fully automatic. If it doesn't, the Settings picker is the shipped path and nothing else in the design changes. I have not verified this on a device — it is flagged as a spike, not assumed.
Because this is sideloaded rather than shipped through Play, a one-time
adb shell pm grant … WRITE_SECURE_SETTINGS is legitimately available. That unlocks
device-wide effects like greyscale that Fairphone needed system-app status for. It stays strictly
opt-in — nothing core depends on it.
Three surfaces in, one deliberate gesture out.
The brief says "hit a button". On a Pixel that button can live in three places, and I'd build all three because each is a handful of lines:
TileService) — the primary. Reachable from
inside any app, one swipe and a tap, survives changing launchers, and it is the same control
that turns focus off. This is the closest thing stock Android has to a dedicated switch.The exit control sits permanently at the bottom of the focus home screen. It is a press-and-hold for ~1 second, not a tap — enough that it never fires by accident in a pocket, not so much that it becomes a puzzle. The Quick Settings tile toggles back out too.
A deliberate non-feature: no lockout. No "you must wait 25 minutes", no penalty screen, no streak to break. Minimalist Phone's session-limit machinery is aimed at people fighting their own phone; this is a mode you chose to enter, and being unable to leave your own phone during a real interruption is a genuine failure mode, not a feature.
The five screens that make up the whole interaction.
01Default launcher Pixel Launcher, untouched. Focus adds one icon and takes nothing away — if the project is abandoned, deleting the app leaves no trace.
02The trigger A Quick Settings tile — reachable from inside any app, not just the home screen. The same tile turns focus back off.
03The transition A short blurred confirmation, borrowed from Moments. It exists to make the mode change unmistakable — the worst launcher bug is not knowing which mode you're in.
04Focus home Clock, five app names, one count of what's waiting, one way out. No drawer, no search, no widgets, no swipe to anything. Text instead of icons — nothing here is designed to catch your eye.
04bLock screen Not a replaced lock screen — Android has no API for that. This is a wallpaper rendered from the same Compose code as screen 04, laid out so the system's own clock fills the top. Glance at the phone and you know you're in focus mode without unlocking it. Deliberately no button shapes: a control that can't be tapped only teaches your thumb to try.
05The way back Press and hold fills the pill over ~1s, then the classic launcher returns. Deliberate enough not to misfire, never a lockout.
06Configuration The entire settings surface. One list, a cap, nothing else — the cap is the feature. Reached by long-pressing the clock.
Small enough to finish, in that order.
| Feature | Notes |
|---|---|
| Focus home screen | Clock, allowed-app list, exit control. Text-first, monochrome, no icons. |
| Enter / exit | Quick Settings tile + Quick Tap + home shortcut in; hold-to-exit out. The locked-in requirement. |
| HOME role handling | Take on entry, release on exit — plus the fallback spike from §03. |
| Do Not Disturb | One AutomaticZenRule tied to focus state. Following Fairphone's shipped choice, not their abandoned one. |
| Allowed-app configuration | One screen, one list, a cap of 5. |
| Transition confirmation | The blurred overlay. Cheap, and it removes all ambiguity about which mode you're in. |
| Notification count line Q4 | "3 waiting" — a count only, no senders, no previews, not tappable. Counts allowed apps only. Needs NotificationListenerService. |
| Device-wide greyscale Q5 | On entering focus, off on leaving. One-time adb grant; degrades cleanly to colour without it. See the restore-safety note below. |
| Starred contacts break through new | ZenPolicy.allowCalls(PEOPLE_TYPE_STARRED) plus repeat callers. Uses the favourites already curated in Contacts — no second list, and no READ_CONTACTS. A focus mode you can't be reached in is one that stays switched off. |
| Lock screen + always-on new | Lock and home wallpaper rendered from the same Compose code as the focus screen; always-on display on; lock-screen notifications hidden. All reverted on exit. See ADR 6. |
| later | Session timer — focus for 25/50 minutes, then auto-exit. Additive, not a lockout. |
| later | A second profile — only once one profile has proved itself. Fairphone ships six; one person needs one. |
| no | App blocking, session limits, delay screens — the Minimalist Phone half. Wrong model: focus is a place you go, not a warden. |
| no | Screen-time dashboards, streaks, usage nags — measuring the problem is not solving it, and Android Digital Wellbeing already exists. |
| no | Analytics, accounts, cloud sync — one user, one device. |
| no | Play Store distribution — sideloading dodges the exact policy surface that cost Fairphone a rejection and a Play Protect flag. |
| no | General launcher features — folders, pages, icon packs, widgets, search, app drawer. Every one is an escape hatch back to distraction. |
| no | Onboarding wizard — the author installed it. A README is the onboarding. |
| no | Other devices, other Android versions, work profiles, iOS — target is one Pixel 6. |
Neither product is being cloned. Here is the ledger.
| From | Taken | Deliberately different |
|---|---|---|
| Fairphone Moments | The core model — a mode you switch into and out of, not a permanent state. A small hard cap of apps per mode (five is a good number and they landed on it too). The blurred transition confirmation. DND via AutomaticZenRule rather than notification interception — their own source
shows them retreating to exactly this. |
No privileged system app; we work inside what a sideloaded APK can do, which forces the
role-handoff design in §03. One profile, not six. Presets for strangers; one person knows their own list. No analytics, no Firebase, no Play Services — their README warns the app is degraded without them, which is a dependency worth not having. A Pixel-native trigger (Quick Tap + QS tile) instead of hardware we don't have. |
| Minimalist Phone | The visual language: text instead of icons, monochrome, generous spacing, nothing that
competes for attention. The idea of an explicit allowlist rather than a blocklist — decide what's in, not what's out. |
No blocking, no session limits, no delay screens, no streaks, no screen-time reporting.
Its model is a permanent restriction plus willpower tooling; the brief here is a reversible
mode. No swipe-right app drawer. In their design it's a convenience; in a focus mode it's an escape hatch back to everything, which defeats the point. No subscription, obviously — but also none of the structure a subscription forces (tiers, upsells, retention hooks). |
Kotlin, Compose, one Gradle module, no dependencies worth arguing about.
Kotlin with Jetpack Compose. This wasn't specified in the brief, so it is being
decided and recorded rather than assumed — the full reasoning is in
ADR 2. In short: Kotlin is the
only first-class Android language and every API this app touches is documented Kotlin-first;
Compose suits a handful of bespoke text screens and its @Preview system means the
mockups above and the real screens can converge in one place. Fairphone built the same kind of app
in the same stack, which is a useful sanity check.
Rejected: Java (more ceremony, no Compose ergonomics); Flutter/React Native (a cross-platform layer is pure cost for one Android device, and every interesting API here would need a hand-written platform channel); KMP (nothing to share with).
FocusModeController is the only thing that decides anything.| Permission / grant | Why, and how it's obtained |
|---|---|
ACCESS_NOTIFICATION_POLICY | Create and toggle the DND zen rule. User grants once in Settings. |
android.app.role.HOME | The role itself. One system dialog per entry, or once if the spike works out. |
| (none needed) | Listing installed apps uses LauncherApps.getActivityList(), which needs no permission —
QUERY_ALL_PACKAGES, which Fairphone declares, is not required for this. |
BIND_NOTIFICATION_LISTENER_SERVICE | The "3 waiting" count (Q4). User grants Notification Access once in Settings. DND suppresses alerting, not posting, so the listener still sees what arrives while focus is on. |
SET_WALLPAPER | Lock and home wallpaper during focus mode. A normal permission — no prompt, no grant. |
READ_MEDIA_IMAGES | Reading the current wallpaper so the original can be restored. Granted once over adb, same as WRITE_SECURE_SETTINGS. Without it, focus mode leaves the wallpaper alone rather than changing something it can't undo. |
WRITE_SECURE_SETTINGS | Greyscale, always-on display, and hiding lock-screen notifications. Granted once over adb, never requested at runtime:adb -d shell pm grant <pkg> android.permission.WRITE_SECURE_SETTINGSThen Settings.Secure: accessibility_display_daltonizer{,_enabled},
doze_always_on, lock_screen_show_notifications — all verified writable on the device. |
Focus mode now changes four pieces of global device state: greyscale, the always-on display, lock-screen notifications, and the wallpaper. None of them are app state. If Focus is killed, crashes, or is force-stopped mid-session, the phone is left grey, burning battery on an always-on display, hiding its lock-screen notifications, and wearing the wrong wallpaper — with no obvious way back.
So restoration is a real component, not a line in onDestroy: persist the intended
state, and reconcile it on app start and on BOOT_COMPLETED. This is the single
largest piece of engineering the device-state effects buy, and it is worth naming before it is
discovered.
While Focus holds the HOME role, Pixel Launcher's gesture-navigation polish and At a Glance are gone — the swipe-up-to-home animation gets generic. This is unavoidable for any third-party launcher on a Pixel, it only applies while focus mode is active, and quantifying how much it grates is one of the things the first spike is for.
Dagger v1.0.0-beta.9. A working skeleton now, the real pipeline when there's code to build.
The skeleton is in the repo and runs today — dagger check passes
focus:check against the pinned engine, and dagger call focus tree proves
the workspace mounts. That is the whole point of it: prove the mechanism, don't build a pipeline
for an app that doesn't exist yet.
Splitting android-sdk from gradle earns its keep: the SDK container is
slow to build and highly cacheable, the Gradle runner is generic enough to reuse unchanged, and
focus stays thin. Two things that will need care when they land — non-interactive
Android SDK licence acceptance (belongs in android-sdk, cached once) and release
signing (keystore as a Dagger secret, never a file in the repo; debug builds need neither).
Full detail in docs/build-tooling.md and
ADR 3.
All eight answered on 16 August, then two more added after the spike ran.
Q4 and Q5 pulled the notification count line and device-wide greyscale out of "maybe later" and into v1. Everything else confirmed the recommendation, which means the shape of the app doesn't change — it just gets two more things in the first release.
Fairphone caps at five and it feels right — enough for a phone to still be a phone, few enough that the list never needs scrolling or scanning.
The honest tension: a personal phone occasionally needs the banking app right now. But every minimal launcher that adds a full app list ends up being used as a normal launcher.
Fairphone has none — it's the physical switch or nothing. Minimalist Phone has blocking schedules, and they're part of what makes it feel like a warden.
Three levels: (a) DND only — nothing gets through, nothing is shown; (b) DND plus a
count line, "3 waiting", no senders or previews; (c) a filtered list rendered in the launcher,
which needs NotificationListenerService and is where Fairphone's own attempt stalled.
Probably the single highest-impact effect per line of code — colour is most of what makes app
icons pull at you. Needs a one-time adb shell pm grant … WRITE_SECURE_SETTINGS,
which is available precisely because this is sideloaded onto your own device.
adb command at install time; the app
degrades cleanly to colour if the grant is missing, so it never becomes a hard dependency. The
keys are confirmed working in shipped apps — see §08, along with the
restore-on-crash safety net this makes necessary.Options: instant tap, hold ~1s, or a confirmation dialog. Fairphone's physical switch is instant and unmissable; a tap on a touchscreen is neither.
Fairphone ships five presets and allows six saved. That is a product serving strangers who don't know what they want yet.
Unavoidable for any third-party launcher, and only while focus mode is active — but it's the kind of thing that's fine in theory and irritating in practice.
Raised by Yves: "if my wife is calling me it can be for a very good reason." The concern is exactly right — a focus mode you can't be reached in is one you never leave switched on, and you'll keep checking the phone anyway.
ZenPolicy.allowCalls(PEOPLE_TYPE_STARRED)
plus allowRepeatCallers(true). Three things make it cheap: it reuses the favourites
already in Contacts so there's no second list to maintain, it needs no
READ_CONTACTS because the platform evaluates starred status itself, and repeat
callers cover the emergency from someone you never thought to star. This is how Android's own
Bedtime mode behaves.The goal: glance at the phone, see focus mode, don't even try to unlock — a device that always behaves the same way, where the lock is a commodity rather than a gate.
Validate the idea before paying for the plumbing.
Does the platform cooperate? Yes.
ROLE_HOME, live with it for a dayAll of it verified on the device. The automatic return works, so exit is zero-tap. The gesture tax is real but minor. Full results in ADR 4.
The locked-in requirement, well.
Then use it for a month before writing another line.
Driven by use, not by plan.
Every item here is a guess until v1 has been lived with. Ordering to be decided then — and "none of them" is a perfectly good outcome.
Android.bp, SwitchStateChangeViewModel, ZenNotificationManagerPermissionController/res/xml/roles.xml — the android.app.role.HOME definition