Bloom turns a long-press on a floating button into a Voronoi tessellation of your most-used shortcuts. This page walks through the pieces end to end, from seeding math to the Samsung workaround.
The shortcut bloom is computed in three passes, each building on the last:
Sunflowers pack seeds in a spiral where each new seed is offset by the golden angle (137.5077…°). This is the tightest known packing for points on a disk without lattice repetition — every radial and every angular slice has a different density profile, so the eye never locks onto a grid pattern. Bloom uses the exact same rule:
angle[i] = i × 137.50776405°
distance[i] = scaleFactor × √i
x[i] = centerX + distance[i] × cos(angle[i])
y[i] = centerY + distance[i] × sin(angle[i])
scaleFactor is tuned to 38 dp in the shipping app (down from 52 dp in the first prototype — reducing dead space between seeds).
Raw phyllotaxis positions are a good initial distribution, but some cells sit too close and some too far. A 200-iteration physics pass nudges them into a tighter blob:
force = (center − pos) × 0.025 × decaydecay = 1 − iteration / maxIterations, so the system anneals toward a stable configurationThe final positions are cached. Bloom only recomputes when the user changes the sticker count or layout settings.
Once positions are fixed, a Delaunay triangulation is computed in-memory and inverted into a Voronoi diagram. Each cell polygon is drawn with Canvas.drawPath(). The entire bloom area is clipped to a circular mask with Canvas.clipPath(), so the outer cells appear to "terminate at the bloom edge".
Touch targeting. Hit-testing is not point-in-polygon. Instead, for every tap coordinate, Bloom finds the nearest seed via squared-distance comparison. That is the Voronoi definition, so the result is identical but the lookup is O(N) with no polygon math. No dead gaps between icons.
A naive overlay uses a single WindowManager window with WRAP_CONTENT bounds that resize as the bloom opens. This is what most floating-launcher apps do — and it runs into a Samsung One UI bug.
On One UI 5.x through 7.0, when an accessibility overlay window changes size mid-animation, the compositor inserts a 300 ms fade on the entire window that cannot be suppressed via windowAnimations = 0 or layoutAnimation = null. This causes Bloom's petals to flash in and out during open/close animations. It is a reproducible regression filed against Samsung internally; fixes have not shipped as of April 2026.
Bloom splits the overlay into two coordinated windows:
| Window | Size | Flags | Responsibility |
|---|---|---|---|
| VisualWindow | Full screen, fixed | FLAG_NOT_TOUCHABLE |
Draws Voronoi cells + icons. Size never changes, so One UI never animates it. |
| TouchWindow | Dynamic, matches bloom content | Transparent, no background | Captures touches. Resizes as the bloom opens, but has nothing visual to animate. |
An OnLayoutChangeListener on the content view keeps the TouchWindow's bounds synchronized with the visible Voronoi region. The user sees one unified bloom; under the hood, the animation runs in the visual layer while the touch layer silently follows.
Bloom's accessibility_service_config.xml is configured for minimum privilege:
<accessibility-service
android:accessibilityEventTypes="typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:canPerformGestures="false"
android:canRetrieveWindowContent="false"
android:description="@string/accessibility_service_description"
android:notificationTimeout="100"
android:settingsActivity="app.nlap.bloom.ui.SettingsActivity" />
canRetrieveWindowContent="false" — Bloom cannot read what other apps are showing. This is enforced by Android; the permission isn't granted.canPerformGestures="false" — Bloom never calls dispatchGesture(). It only fires global actions via performGlobalAction().flagRequestFilterKeyEvents, no flagRequestEnhancedWebAccessibility, no typeAllMask — only the single event type it actually needs.This matters for Google Play review. Starting 2026-01-28, Android introduces App Protection Mode that revokes AccessibilityService privileges from apps that declare overly broad flags without a legitimate accessibility use case. Bloom declares only what it uses and documents why.
A sticker is the smallest unit of user-facing functionality. Each sticker is a tuple of (id, icon, label, action). Categories:
Flashlight (CameraManager.setTorchMode), auto-rotate (Settings.System.ACCELEROMETER_ROTATION), DND (NotificationManager.setInterruptionFilter), brightness slider, volume slider, Wi-Fi / Bluetooth (via settings-panel intent on Android 10+).
Flashlight includes a CameraManager.TorchCallback so that external apps toggling the torch (e.g., a widget, or the system quick settings) update Bloom's sticker state. No polling, no drift.
Home, back, recents, last-app — all via performGlobalAction (GLOBAL_ACTION_HOME etc.). Last-app is implemented as two quick consecutive GLOBAL_ACTION_RECENTS fires (Android does not expose a direct API).
Screenshot (GLOBAL_ACTION_TAKE_SCREENSHOT), lock screen (GLOBAL_ACTION_LOCK_SCREEN), notification panel, quick settings panel.
The AI bridge is the only "intelligent" sticker category, and Bloom runs it entirely on-device. Nothing leaves your phone unless you send it to an AI app:
MediaProjection grabs the visible screen → Bitmap → ML Kit TextRecognition → recognized text handed to you via an Android share sheet. Paste into ChatGPT, Claude, Perplexity, Translate, whatever you use.MediaProjection → crop overlay UI → Intent.ACTION_SEND with image/* MIME. Same share sheet, same apps.Bloom never calls an AI API of its own. This keeps the app free to run (no OpenAI bill, no Anthropic bill) and keeps your data on-device. You choose which AI to invoke, per-query, via the share sheet.
User-defined shortcuts: any installed app, any deep link (Intent URI), any URL. Configured in Settings → Sticker editor.
Bloom's floating overlay requires an active PRO pass to launch. The rule is simple:
PRO active = System.currentTimeMillis() < pro_expiry_timestamp
Activate = pro_expiry_timestamp = now + 24 hours
Watching one full rewarded AdMob video activates 24 hours of PRO. No subscription, no in-app purchase, no account. Wall-clock based — Bloom intentionally does not use SystemClock.elapsedRealtime() defense against clock rollback, because the attack payoff (skipping one ad) is not worth the complexity or the false positives when a user legitimately changes time zones.
In the EEA, UK, and Switzerland, Bloom uses Google's User Messaging Platform SDK to collect GDPR consent before any AdMob request fires. AdConsentManager wraps the UMP APIs; the consent form appears on the first app launch and can be re-opened from Settings → Manage privacy preferences. Outside of GDPR jurisdictions, the "Manage privacy preferences" option is hidden entirely.
All ad loaders (banner, rewarded) gate on ConsentInformation.canRequestAds(). If consent hasn't been obtained or was denied, no ad request is made — the user still sees Bloom, just without ads, and rewarded video is replaced by an equivalent local grant (to keep the feature accessible).
| Component | Tech |
|---|---|
| Language | Kotlin |
| UI framework | Android Views (not Compose) |
| Min / target SDK | 35 / 35 (Android 15) |
| Overlay | AccessibilityService + WindowManager |
| OCR | Google ML Kit TextRecognition (on-device) |
| Ads | Google AdMob (banner + rewarded) |
| GDPR | Google User Messaging Platform (UMP) 4.0.0 |
| Crash reporting | Sentry (anonymous) |
| Persistence | SharedPreferences (local only) |
| Build | Gradle 8.11.1, AGP 8.7.3 |
Bloom ships a camera-sticker roadmap for later phases: magnifier, color picker, live translate, business-card scanner, front-camera mirror, QR code, live OCR, ML Kit Document Scanner. CAMERA permission is not declared today — it will be declared the moment the first camera sticker ships, with a Prominent Disclosure screen and an updated Play Console Data Safety declaration.
The gesture-navigation flicker on Android 15 launcher transitions remains an OS-level limitation. Bloom detects it and degrades gracefully; a full fix requires changes Google has not exposed to third-party apps.