Android · Engineering Within The Rules
How Google's Rules
How Google's Rules
Shaped Radar
Android gives Radar the low-level radio access that makes an offline mesh possible — but it does so inside a cage of Google Play policies, version-by-version permission clampdowns, and hard radio limits. This is a precise account of the constraints we hit, and exactly how each one changed the way Radar is built.
Part 1 · Google Play Policy Walls
Permissions and behaviours that Android technically supports, but that Google Play restricts to specific app categories — several of which we hit head-on and had to design around.
Full-Screen Intent — Incoming Calls
Ringing call screen over the lock screen
Permission removed for Play compliance
-
Radar's native voice/video calling originally used
USE_FULL_SCREEN_INTENTwithsetFullScreenIntent()so an incoming call would launch the full call screen directly over the lock screen — exactly like the system phone app. -
Google restricts
USE_FULL_SCREEN_INTENTto apps whose core purpose is phone calls or alarms. Since Radar is a communication/mesh app — not a dialler — we are not eligible. On Android 14+ the permission is also revoked by default for non-calling apps. -
How Radar adapted: incoming calls are now surfaced as a high-priority
CallStyleheads-up notification with Accept / Decline buttons. Tapping the banner opens the call screen. We lose the automatic lock-screen takeover, but stay compliant.
What Radar needed
A ringing call screen that wakes the device and shows over the lock screen for
a true phone-call experience.
The constraint
USE_FULL_SCREEN_INTENT is reserved for calling/alarm apps only.
A non-calling app declaring it risks Play rejection and silent revocation on
Android 14+.
Activity Recognition — Movement Detection
Walking / driving / cycling transport mode
Removed under Health-apps policy
-
Radar adjusts its behaviour by transport mode (on foot vs in a vehicle). The natural
API for this is
ACTIVITY_RECOGNITION, which reads the device's hardware activity classifier. - Google Play treats activity-recognition / fitness signals as health data. Apps requesting it are pushed toward the Health-apps policy and Health Connect requirements. Radar is a communication app, not a fitness or health app, so declaring the permission invited policy scrutiny we could not satisfy.
- How Radar adapted: transport mode is now derived purely from GPS-derived speed plus map context — no health-class permission required.
Background ("All the time") Location
Proximity radar & presence beaconing while closed
Restricted permission · manual review
-
Radar's core value is knowing who is near you and broadcasting your own presence —
which requires location access even when the app is in the background.
ACCESS_BACKGROUND_LOCATIONmakes this possible. Android API 29+ - Google Play classifies background location as a sensitive, restricted permission. It cannot simply be shipped: it requires a written justification, a recorded prominent disclosure + runtime consent flow, and a manual policy review (often including a demo video) before the build is approved for production.
- On Android 10+ the permission can no longer be requested in the same prompt as foreground location — the user must be sent to system settings and grant "Allow all the time" as a separate, deliberate step, which Radar guides them through.
SMS / MMS Access
Unified inbox of SMS alongside mesh messages
Restricted SMS/Call-Log policy
-
Radar can surface SMS and MMS next to mesh messages in one inbox. That needs
READ_SMS,RECEIVE_SMS,SEND_SMS,READ_MMSandRECEIVE_MMS. - SMS and Call Log are a restricted permission group under Play policy. Each requires a Permissions Declaration Form, a qualifying core use-case, prominent disclosure, and policy review. Most use-cases are rejected outright — only a narrow set (default handler, backup, etc.) qualify.
- Writing to the SMS store requires being the default SMS app. Since Android 4.4, only the user-selected default SMS application may write to the SMS content provider — so any "mark as read"/compose flow is gated behind the user actively choosing Radar as their default messaging app.
-
Even receiving SMS has rules:
SMS_RECEIVEDis a protected broadcast (exempt from the Android 8.0+ implicit-broadcast ban) and its priority is non-abortable since KitKat — so the manifest receiver had to be declared exactly as the platform dictates.
April 2026 — The End of "Mirror Mode"
Reading incoming SMS without replacing your messaging app
Secondary-handler path closed by Google
-
How Radar used to do it. Originally Radar ran as a lightweight
secondary SMS handler — a "mirror". It listened for the
SMS_RECEIVEDbroadcast and read the inbox withREAD_SMS/RECEIVE_SMS, so your texts could appear alongside mesh messages without Radar ever taking over as your default messaging app. Google Messages (or whatever you use) stayed in charge; Radar just observed. - What changed in April 2026. Google's updated SMS & Call Log Permissions policy stopped accepting the "read-only mirror" use-case. Under the new enforcement the review team cross-checks the Permissions Declaration Form against the actual manifest: if an app requests the restricted SMS permissions, it must genuinely be able to become the device's default SMS handler — passively mirroring another app's messages is no longer an approved reason to hold those permissions. Radar's build was rejected with "Issue with default handler capability — your app does not satisfy the requirements for default handler capability."
-
Why "just mirror it" is now impossible. The old path relied on a
non-abortable
SMS_RECEIVEDlistener that coexisted with the real SMS app. Google now treats that coexistence as the abuse vector it wants to close: only one app may legitimately touch SMS, and it must be the one the user has deliberately chosen as their default. There is no longer any compliant middle ground between "not touching SMS at all" and "being the full default app." - How Radar adapted — and why it now needs MORE permissions. To keep the feature at all, Radar had to become a real, fully-fledged default SMS app that the user can opt into. Being merely able to read messages was the easy part; Android requires a genuine default app to own the entire SMS lifecycle, which pulls in a whole extra tier of permissions and components we never needed as a mirror.
Old "mirror" — minimum footprint
A single
SMS_RECEIVED receiver + READ_SMS /
RECEIVE_SMS. Radar observed; your existing app stayed in charge and kept
persisting messages.
New "default handler" — the extra burden
To satisfy Google, Radar must now declare all four default-handler
components and take on responsibilities a mirror never had:
• A
• A
• An
• A
Plus Radar must now write every received message to the system SMS provider itself — because the default app, not the OS, is responsible for persisting texts.
• A
SMS_DELIVER receiver guarded by
BROADCAST_SMS (the exclusive delivery path for the default app).
• A
WAP_PUSH_DELIVER receiver guarded by
BROADCAST_WAP_PUSH for MMS.
• An
ACTION_SENDTO compose activity (sms:/mms:).
• A
RESPOND_VIA_MESSAGE service holding
SEND_RESPOND_VIA_MESSAGE (quick-reply from the dialler).
Plus Radar must now write every received message to the system SMS provider itself — because the default app, not the OS, is responsible for persisting texts.
⚖️ The irony: a policy meant to reduce how much SMS access apps
hold ended up forcing Radar to request substantially more. The privacy-lighter
"just mirror my texts" option is gone; the only compliant way to show SMS in Radar is for
the user to make Radar their default messaging app outright — and every SMS code path is
now gated behind a live
RoleManager.isRoleHeld(ROLE_SMS) check, so the moment
the user switches their default away, Radar stops reading, receiving and sending SMS
completely.
Battery-Optimisation Exemption (Doze)
Reliable background delivery while idle
Restricted to eligible categories
-
For a mesh node to relay messages and receive SMS while the screen is off, it must
survive Doze. Radar asks for a Doze whitelist exemption via
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS. - Google Play only permits this permission for specific app categories — and explicitly disallows it where a foreground service or other API would do. Radar qualifies as a messaging / peer-discovery app, but the declaration sits under policy review and must be justified.
-
On strict OEM builds (e.g. OxygenOS) the system dialog throws a
SecurityExceptionunless the permission is declared — so it must be in the manifest even though Play scrutinises it.
Foreground-Service Types (Android 14+)
The always-on background process
Declared type + Play justification
-
Radar's persistent background work runs in a single foreground service. On Android 14+
(API 34) every foreground service must declare a type, and Radar
declares
location|connectedDevice. -
Omitting the right type is a hard crash, not a warning. A service that
does BLE scanning/advertising or GATT without
connectedDevicethrowsMissingForegroundServiceTypeExceptionon launch — confirmed on strict OEM builds. -
Each declared type also carries its own runtime permission requirement
(
FOREGROUND_SERVICE_LOCATION,FOREGROUND_SERVICE_CONNECTED_DEVICE) and a Play Console justification for why the service runs at all. -
Knock-on effect: because exact alarms (
SCHEDULE_EXACT_ALARM) are also a restricted permission on Android 12+, Radar deliberately avoids them and restarts its service via WorkManager expedited jobs instead — no special permission needed.
Part 2 · Android Platform & Radio Limits
Hard technical constraints baked into Android's Bluetooth, Wi-Fi and process model. These aren't policy choices — they're the physics of the APIs, and each one dictated a specific piece of Radar's architecture.
BLE Advertising — 31 Bytes & No Live Format Change
Presence beacons carrying location + identity
Hard radio limit
- A legacy BLE advertisement packet is only 31 bytes. After flags and the manufacturer-ID header, Radar has just 24 usable bytes of payload — 25 bytes fails outright with "Data too large". Everything a beacon carries (a 128-bit UUID, latitude, longitude, age, gender) had to be hand-packed at the bit level into those 24 bytes.
-
You cannot change a beacon's format on the fly. Android's
BluetoothLeAdvertiserwon't let you mutate the advertisement structure while it's running — switching format means stopping and restarting the advertiser, which drops the beacon, risks interrupting in-flight peer handshakes, and is throttled by the OS if done too often. - How Radar adapted: a single fixed beacon format plus heavy throttling — payload updates are skipped unless the device has moved ≥5 m or several seconds have passed, and live updates reuse the same format so the advertiser never has to restart. Anything that won't fit in 24 bytes is exchanged later over GATT.
Wi-Fi Aware Must Be Bootstrapped Over BLE GATT
High-bandwidth offline data path between peers
Two-radio handshake forced
- Wi-Fi Aware (NAN) gives Radar a fast, router-less data link — but it cannot find a peer on its own in the way Radar needs. Discovery is publish/subscribe on opaque service names, and a data path can't be reliably negotiated until both sides have already agreed on identity and roles.
-
So the handshake rides on Bluetooth first. Peers are discovered via the
24-byte BLE presence beacon; a BLE GATT connection is then opened, and
identity, role (publisher vs subscriber, decided by UUID comparison) and the
NAN_READYsignal are all exchanged over GATT. Only once that completes is the Wi-Fi Aware data path brought up. - This is a genuine two-radio dance: BLE advertise → BLE scan → GATT connect → handshake → then Wi-Fi Aware. The Android APIs don't offer a single combined discovery+connect primitive, so Radar had to build and sequence the entire state machine itself.
- Side effect: BLE advertising has to be carefully paused/coordinated during the GATT handshake so restarting the beacon doesn't tear down a connection that is mid-negotiation.
What Radar needed
"Find nearby peer, open a fast Wi-Fi link, send data" — in one step.
The constraint
Wi-Fi Aware discovery can't carry the handshake, so identity & readiness must be
brokered over BLE GATT off the back of a BLE advert before NAN can connect.
BLE GATT Payload & MTU Ceiling
Delivering messages when only Bluetooth is available
~512-byte MTU cap
- When Wi-Fi Aware isn't available, messages fall back to BLE GATT. But a GATT characteristic write is capped by the negotiated MTU (max ~512 bytes), leaving roughly 480 bytes of usable payload after overhead.
- Anything larger must be compressed and/or fragmented and reassembled by hand. Radar squeezes encrypted message envelopes under that ceiling and chunks larger content across multiple writes — there is no "just send a big buffer" option on BLE.
No Background Activity Starts (Android 10+)
Showing an incoming call / message from the service
startActivity() blocked
-
When a call or message arrives, the work is happening inside Radar's background service
— but on Android 10+ a background process cannot call
startActivity(). The launch is silently dropped. -
This even applies to
CATEGORY_CALLnotifications and broadcast receivers: aBroadcastReceivertrying to start an activity is blocked too. -
How Radar adapted: the UI is never launched directly from the
background. Instead Radar posts a notification whose
PendingIntentcarries the foreground-start right — the system itself opens the call/chat screen when the user taps it.
The OS (and OEMs) Keep Killing the Service
Keeping the mesh alive when the app is closed
Aggressive process death
-
A mesh node has to keep running after the user swipes the app away. By default Android
kills the service on task removal, so Radar sets
android:stopWithTask="false"to survive "Close All". - On Android 14+ the user can swipe away the foreground-service notification itself, and many OEM skins (OxygenOS, MIUI, etc.) run aggressive task-killers that ignore these hints entirely.
-
Starting a foreground service from the background is itself restricted — doing it at
the wrong moment throws
ForegroundServiceStartNotAllowedException. -
How Radar adapted: a
ServiceWatchdogmonitors the sub-services and restarts any that die; a boot receiver re-launches after reboot; and recovery is scheduled through WorkManager expedited jobs (rather than exact alarms) so the radios resume within seconds without needing a restricted permission.
Bluetooth Permissions Split (Android 12+)
Scanning, connecting and advertising
Permission model rewrite
-
On Android 12 (API 31) the old
BLUETOOTH/BLUETOOTH_ADMINpermissions were replaced by runtime permissionsBLUETOOTH_SCAN,BLUETOOTH_CONNECTandBLUETOOTH_ADVERTISE— each prompted at runtime. -
The manifest has to carry both the legacy permissions (capped with
maxSdkVersion="30"so they don't show up for modern users) and the new ones, with runtime guards branching on the API level throughout the BLE code. - Before Android 12, BLE scanning also legally required location permission, conflating "find nearby Bluetooth" with "track my location" in the user's eyes — a recurring source of permission friction.
Scoped Storage & the Photo Picker
Sharing and saving photos / attachments
No more free file access
-
From Android 10, scoped storage ended broad file-system access. Radar
can no longer just read/write arbitrary paths; saving media goes through
MediaStore, and mismatched parameters throwIllegalArgumentExceptionon Q+. -
Photo selection uses the system Photo Picker with
READ_MEDIA_IMAGESandREAD_MEDIA_VISUAL_USER_SELECTED(API 34), so the user can grant access to only the photos they pick — broad storage permissions are capped at older API levels (maxSdkVersion). -
The blunt alternative,
MANAGE_EXTERNAL_STORAGE("All files access"), is a restricted Play permission granted to almost no app category — so it was never an option.
Package Visibility (Android 11+)
Offering installed apps as call/share fallbacks
Apps hidden by default
-
On Android 11 (API 30), apps can no longer see what else is installed.
queryIntentActivities()andgetLaunchIntentForPackage()return empty/null for anything not explicitly declared. -
So that Radar can offer to hand off a call to WhatsApp / Telegram / Signal / Meet (when
a native call isn't possible), every such app must be listed in a
<queries>block in the manifest — visibility has to be requested package by package.
💡 And it isn't even on every Android phone.
Wi-Fi Aware (NAN) and UWB are optional hardware features — many mid-range and older
devices simply don't have them. Radar marks them
required="false" and every
transport gracefully degrades: Wi-Fi Aware → Wi-Fi Direct → BLE GATT → LAN → web relay,
depending on what the specific handset actually supports. The "build once, run everywhere"
promise has a lot of runtime branching behind it.
None of this is a complaint about Android — it remains the only platform
that lets an app like Radar exist at all. Google's policies exist to protect users'
privacy, battery and security, and most of the constraints above are reasonable defaults
for the 99% of apps that don't need raw radio access.
But Radar lives in that last 1%. Building an off-grid mesh on a modern, locked-down mobile OS means working with every one of these rules: packing beacons to the byte, brokering Wi-Fi over Bluetooth, surfacing calls through notifications, and re-architecting features when a policy wall appears. The result is an app shaped as much by Google's rulebook as by its own ambitions.
→ Why Radar can't run on iOS at all · → The radio tech under the hood · → How we handle your permissions
But Radar lives in that last 1%. Building an off-grid mesh on a modern, locked-down mobile OS means working with every one of these rules: packing beacons to the byte, brokering Wi-Fi over Bluetooth, surfacing calls through notifications, and re-architecting features when a policy wall appears. The result is an app shaped as much by Google's rulebook as by its own ambitions.
→ Why Radar can't run on iOS at all · → The radio tech under the hood · → How we handle your permissions