A single-screen Android app that tests whether a device actually renders Android 16 Live Updates (promoted ongoing notifications) — and, on Samsung, whether the notification reaches the Now Bar (即時通知).
It exists because canPostPromotedNotifications() returning true does not
mean anything appears on screen. The framework guarantees the OS will accept and
promote your notification; it says nothing about whether the OEM's SystemUI
draws a chip. IslandCheck reads back what the OS actually did, so the difference
is visible instead of guessed at.
A sideloaded, non-privileged, platform-unsigned app can appear in Samsung's Now Bar. The recipe is the opposite of what the reverse-engineering guides suggest:
- Post an ordinary AOSP promoted notification —
setOngoing(true),setRequestPromotedOngoing(true), a non-empty content title, a promotable style, not colorized. - Add exactly one Samsung extra:
android.ongoingActivityNoti.automation = true(plus...automationPackage = <your package>). - Send nothing else. On One UI 8.5 the decorative extras actively break it.
- Background the app — an entry stays in
Pendingwhile its own app is foreground.
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setOngoing(true)
.setRequestPromotedOngoing(true)
.setContentTitle("…")
.setStyle(NotificationCompat.ProgressStyle()…)
// Gate on the RUNTIME capability, not on SDK_INT and not on manufacturer.
// SDK 36 is NOT sufficient: One UI 8.0 is plain Android 16 and reports
// canPostPromotedNotifications() == false, while One UI 8.5 (Android 16 QPR2)
// reports true. Only the runtime call distinguishes them.
val canPromote = Build.VERSION.SDK_INT >= 36 &&
NotificationManagerCompat.from(context).canPostPromotedNotifications()
if (canPromote && Build.MANUFACTURER.equals("samsung", ignoreCase = true)) {
builder.addExtras(Bundle().apply {
putBoolean("android.ongoingActivityNoti.automation", true)
putString("android.ongoingActivityNoti.automationPackage", context.packageName)
})
}Delete the if block and you still have a correct Android 16 app. The OEM
block is additive, never load-bearing.
| Vendor | OS | Android | Device | Plain AOSP alone | + Samsung automation |
|---|---|---|---|---|---|
| AOSP | stock | 17 / SDK 37 | emulator | works — status-bar chip | n/a |
| OPPO | ColorOS 16.0.10 | 16 / SDK 36 | Find X9 | works — no vendor code needed | n/a |
| OPPO | ColorOS 16.0.5 | 16 / SDK 36 | Reno 11 (CPH2599) | works once the user turns on a per-app switch — ships off | n/a |
| Honor | MagicOS 10.0.0.193 | 16 / SDK 36 | X6d 5G (NLA-NX1) | works — no vendor code, no switch | n/a |
| vivo | OriginOS 6 (16.1.13.4) | 16 / SDK 36 | V60 Lite (V2529) | works — no vendor code, no switch | n/a |
| Xiaomi | HyperOS 2 | 15 / SDK 35 | POCO C85 (25078PC3EG) | no island — app reports "requires Android 16+" | n/a |
| Xiaomi | HyperOS 3.0 (OS3.0.302.0) | 16 / SDK 36 | POCO C85 (25078PC3EG) | works — no vendor code needed | n/a |
| Samsung | One UI 7.0 | 15 / SDK 35 | Galaxy S25 (SM-S931N) | no chip | fails — not in any list |
| Samsung | One UI 8.0 | 16 / SDK 36 | Galaxy Z Flip 6 | no chip | fails — capability false |
| Samsung | One UI 8.5 | 16 QPR2 / SDK 36 | Galaxy A26 5G (SM-A266B) | no chip | works — Showing |
| Samsung | One UI 8.5 | 16 QPR2 / SDK 36 | Galaxy S26 Ultra | no chip | works |
| Samsung | One UI 9.0 | 17 / SDK 37 | Galaxy A07 (SM-A075F) | no chip | works — Showing |
(Samsung card lane, style=1: fails on 7.0 and 8.5, works on 9.0. It is never
needed — automation alone works everywhere style=1 does, and more.)
OPPO Find X9 on ColorOS 16.0.10 renders the island from a plain AOSP promoted notification, with zero vendor-specific code — confirmed in production by tigerduck-app-android before 2.1.0. No private extras, no allowlist, no reflection.
Xiaomi POCO C85 on HyperOS 3 (Android 16) does the same. IslandCheck reports SUPPORTED, and the claim holds: the plain AOSP promoted notification really does render as Xiaomi's island. Again, no vendor code.
That is the important result for the whole project: the AOSP path is the
product, and the Samsung block is a workaround for one vendor that predates the
standard. ColorOS and HyperOS both render the standard as-is, so the
if (samsung) block stays the only vendor-specific code needed to make the
island appear. What the island then shows for a countdown is a separate
question, and there ColorOS and HyperOS do need help — see
ColorOS and OriginOS ignore the timer too.
Note the Find X9 also returns false from canPostPromotedNotifications()
while rendering correctly — see the section below. It is the reason that API
must never gate posting.
"No vendor code" is not "no setup", though. ColorOS 16.0.5 renders the same notification only after the user turns on a per-app switch, and that switch ships off. See the next section.
On an OPPO Reno 11 (CPH2599) running ColorOS 16.0.5, a fresh install shows no
island. canPostPromotedNotifications() returns false, the OS does not set
FLAG_PROMOTED_ONGOING, and the post appears as an ordinary status-bar icon.
The cause is a per-app switch that is off by default: the app's notification
settings → Show Live Updates on Live Alerts. The AOSP intent
Settings.ACTION_APP_NOTIFICATION_PROMOTION_SETTINGS opens that screen
directly. Turn the switch on and the same unchanged notification renders as the
island (ⓘ 25%). There is still no vendor code involved.
What the switch changes, read over adb with the switch off and then on:
| Signal | Off (default) | On |
|---|---|---|
POST_PROMOTED_NOTIFICATIONS permission |
granted | granted |
android:post_promoted_notifications app-op |
ignore |
allow |
promoted= in dumpsys notification AppSettings |
true |
true |
canPostPromotedNotifications() |
false |
true |
FLAG_PROMOTED_ONGOING on the posted notification |
absent | set |
| Island | none | renders |
The permission is normal|appop. It is granted at install and never changes;
the switch changes the app-op. On this build the op's default mode is ignore,
so every app starts with the switch off unless ColorOS pre-allows it. Google
Maps and ColorOS Clock were already allow. ChatGPT, Gemini and Facebook were
at the default ignore.
On this build, then, the API is an accurate verdict: false really does mean
nothing will render. What it cannot tell you is that the user can fix it. An app
should:
-
Still always post. With the switch off, the notification is an ordinary ongoing notification and costs nothing.
-
When the API returns
false, offer a button that firesACTION_APP_NOTIFICATION_PROMOTION_SETTINGSwithEXTRA_APP_PACKAGE. That is an AOSP intent, so it is not vendor code. -
To tell "switch off" from "platform can't", read the app-op. There is no public constant, so name the op directly:
val mode = context.getSystemService(AppOpsManager::class.java) .unsafeCheckOpNoThrow("android:post_promoted_notifications", Process.myUid(), context.packageName) // MODE_IGNORED → the user can fix it in settings. // Throws IllegalArgumentException on a build without the op.
IslandCheck shows this as the Promotion app-op row in section 2.
Two further observations:
- This is base Android 16, not QPR2.
ro.build.idisBP2A.250605.015, the June 2025 release, and it still promotes. So base Android 16 can do Live Updates. Whether One UI 8.0'sfalsecomes from a platform flag (see the Samsung section) or from this same app-op has not been re-checked. The new app-op row will show which. - The version property.
ro.build.version.oplusromholds only the major version (V16.0.0). The16.0.5that Settings shows is inro.build.version.oplusrom.display, which IslandCheck now reads first.
Open question: the Find X9 returned false and rendered, while the
Reno 11 renders nothing until the API returns true. The Find X9 result came
from tigerduck-app-android, and nobody recorded its app-op state, so it is not
yet clear whether the two builds really behave differently. Until someone
measures it, tigerduck-app-android treats the Find X9 like the Reno 11: it
believes the API, and when the API says false it points the user at the
switch.
Honor X6d 5G (NLA-NX1) on MagicOS 10.0.0.193 renders the plain AOSP
notification in its island (ⓘ 25%) out of the box.
canPostPromotedNotifications() returns true and the OS sets
FLAG_PROMOTED_ONGOING. Left without shortCriticalText, the island runs a
countdown chronometer the way the AOSP chip does — see
below.
There is no island switch. MagicOS leaves the
android:post_promoted_notifications app-op at default, which defers to the
permission, and the permission is granted at install. No activity handles
ACTION_APP_NOTIFICATION_PROMOTION_SETTINGS, so IslandCheck's settings button
falls back to the app's ordinary notification settings. Turning the app's
notifications off turns the island off with them, as you would expect.
The version is in ro.build.version.magic (MagicOS_10.0.0). The build number
appears only in ro.build.display.id (NLA-N31 10.0.0.193(C363E8R202P1)).
vivo V60 Lite (V2529) on OriginOS 6 (PD2512F_EX_A_16.1.13.4.W20) behaves
the same way. The plain AOSP notification renders in the island (ⓘ 85%),
canPostPromotedNotifications() returns true, and the OS sets
FLAG_PROMOTED_ONGOING. One difference from MagicOS: the island ignores a
countdown chronometer and shows the app name instead — see
below.
The app-op is at default, as on MagicOS. Unlike MagicOS, OriginOS does handle
ACTION_APP_NOTIFICATION_PROMOTION_SETTINGS, but the intent opens the
ordinary per-app notification page (allow, silent, lock screen, pop-up,
badges). That page has no Live Updates or island switch.
The version property: ro.vivo.os.version reads 7.0, which is not the
marketed version. ro.vivo.os.build.display.id holds OriginOS 6, and
IslandCheck now reads it first.
Screenshots over adb: adb exec-out screencap -p returns an all-black
image on this phone, even unlocked. Write the capture on the device
(adb shell screencap -p /sdcard/x.png), then adb pull it.
The same POCO C85 was tested on HyperOS 2 (Android 15) before its upgrade. IslandCheck reports Not supported — requires Android 16+, and that is accurate: nothing IslandCheck posts triggers an island. It is the same structural failure as One UI 7.0. The Live Updates framework does not exist below API 36, so there is nothing for the app to opt into. The same hardware went from unsupported to working through the HyperOS 3 update, not through anything an app can do.
Don't be fooled by the Clock app on HyperOS 2. A running timer, minimised, shows a pill in the status bar that looks like the island. It is not the surface this app tests. Android 15 has no Live Updates framework, so that pill is a Xiaomi-proprietary surface driven by Xiaomi's own Clock app. A third-party AOSP promoted notification cannot reach it. Seeing that pill on a HyperOS 2 device says nothing about whether your app's notification will render.
HyperOS does not use the AOSP status-bar chip. It draws Live Updates in its own island and fills it by its own rules. These rules come from decompiling HyperOS's island code on the POCO C85 (HyperOS 3.0.302) while building tigerduck-app-android. They have not been re-derived with IslandCheck.
- It discards any Xiaomi-specific island parameters the app sends and builds the layout itself from the standard notification fields.
- The right-hand side shows the first non-empty of:
shortCriticalText→contentTitle→subText→contentText. - It never reads the countdown timer (
when+ chronometer).
The AOSP chip, as on Pixel and Samsung, tries shortCriticalText first and then
falls back to the timer. The timer is the only thing that ticks without a
re-post. So the two diverge exactly where a countdown needs them to agree:
| Notification sets | AOSP / Samsung chip | HyperOS island |
|---|---|---|
shortCriticalText |
that text | that text |
no shortCriticalText, countdown chronometer |
live countdown | contentTitle — timer ignored |
The trap: the usual countdown recipe leaves shortCriticalText empty on
purpose, so the chip falls through to the self-ticking chronometer. On HyperOS
the island falls through to the title instead. In tigerduck-app-android the
island showed the notification's title where the countdown should have been.
The fix, on HyperOS 3+ (and on ColorOS and OriginOS, see the next
section): set shortCriticalText to the minutes left, rounded up ("47m",
"43 分鐘"). HyperOS never redraws it by itself, so re-post the notification
each time the minute changes. Leave the AOSP chip, Samsung and MagicOS on the
chronometer, which ticks for free.
IslandCheck always sets shortCriticalText ("42%"), so it never hits the
fallback.
Detecting the version: read ro.mi.os.version.code. It is 3 on HyperOS
3.0 (ro.mi.os.version.name = OS3.0). Treat anything below 3 as having no
island, which matches the HyperOS 2 result above. On HyperOS 3+,
canPostPromotedNotifications() is trustworthy. The decompile shows HyperOS
uses that same check to decide which notifications reach the island, so on
HyperOS the API is a real verdict. On ColorOS it is not (see below).
Measured on 2026-10-07 with tigerduck-app-android's in-class Live Update
preview, posted on Android 16 and then backgrounded: a countdown chronometer
(when in the future, showChronometer, chronometerCountDown), the title
Preview Course, and no shortCriticalText. Every phone set
FLAG_PROMOTED_ONGOING. What the island drew differed:
| Device | Chronometer, no shortCriticalText |
shortCriticalText set |
|---|---|---|
| OPPO Reno 11 / ColorOS 16.0.5, switch on | contentTitle (Preview Course) |
that text (34m) |
| vivo V60 Lite / OriginOS 6 | the app name (TigerDuck) |
that text (34m) |
| Honor X6d 5G / MagicOS 10 | live countdown (32:26) |
that text (IslandCheck's ⓘ 25%) |
So the HyperOS fix applies to ColorOS and OriginOS as well: set
shortCriticalText to the minutes left and re-post on each minute change.
MagicOS behaves like the AOSP chip and keeps the chronometer.
OriginOS does not fall back the way HyperOS does. It showed the app name, not
the title, so it does not seem to walk contentTitle → subText →
contentText. Only the two cases in the table were tried. IslandCheck's own
runs on these phones never showed the gap, because it always sets
shortCriticalText.
The Find X9 (ColorOS 16.0.10) was not re-tested. tigerduck-app-android applies the static text to every ColorOS phone, and to OnePlus and realme, which ship the same ROM.
The QPR level matters more than the API level. One UI 8.5 is based on
Android 16 QPR2; One UI 8.0 is plain Android 16. On a Galaxy Z Flip 6 running
One UI 8.0 the app reports NOT SUPPORTED — canPostPromotedNotifications()
returns false despite SDK_INT == 36, because the platform gates Live Updates
behind an internal flag below QPR2. Nothing app-side can change that.
So any Android 16 Galaxy that never receives One UI 8.5 — the Galaxy S22 series,
Z Fold 4, Z Flip 4, and anything still on 8.0 — will never show a chip. Never
branch on SDK_INT alone — but do not gate posting on
canPostPromotedNotifications() either (see below).
Note also that the permission being granted is a different signal from the
capability being available: on One UI 8.0 POST_PROMOTED_NOTIFICATIONS is
granted and the capability is still false. The app reports both separately so
the two are not confused for a revoked permission. It also reports the
android:post_promoted_notifications app-op, which is what a settings switch
actually changes. On ColorOS 16.0.5 that op is ignore while the permission
still reads granted.
It is a useful diagnostic and a bad gate:
| Device | API says | Actually renders? |
|---|---|---|
| OPPO Find X9 / ColorOS 16.0.10 | false |
yes — false negative |
| OPPO Reno 11 / ColorOS 16.0.5, switch off (default) | false |
no — true negative, but the user can fix it |
| OPPO Reno 11 / ColorOS 16.0.5, switch on | true |
yes |
| Samsung One UI 8.0 / Android 16 | false |
no — true negative |
| Samsung One UI 8.5, One UI 9.0 | true |
yes |
| Xiaomi POCO C85 / HyperOS 3 | true |
yes — the island gates on this same check |
| Honor X6d 5G / MagicOS 10 | true |
yes |
| vivo V60 Lite / OriginOS 6 | true |
yes |
The API cannot tell those false cases apart, so:
Always post the notification. If the OEM won't promote it, it degrades to an ordinary ongoing notification and costs nothing. Gating on this API instead silently removes the feature on devices that would have rendered it — which is exactly what happened on the Find X9.
From decompiled One UI SystemUI (jadx on /system/system_ext/priv-app/SystemUI/SystemUI.apk):
// NotificationEntry
isAutomation() = extras.getBoolean("android.ongoingActivityNoti.automation");
mIsRon = notification.hasPromotableCharacteristics(); // API 36+
isPromotedState() {
if ((SettingsHelper.isDevelopRonTestAllowed() // Settings.Secure
// enable_notification_nowbar_test = 1
|| isAutomation()
|| !AllowedOngoingActivityListManager.isAllowListUsing) && mIsRon) return true;
return mIsPromoted;
}
isOngoingActivity() {
if (extras.getInt("android.ongoingActivityNoti.style", 0) <= 0) return mIsRon; // RON lane
mIsRon = FALSE; // private-card lane
return true;
}OngoingActivityDataHelper only adds an entry to the Showing list when
isPromotedState() is true.
The trap: on One UI 8.5, style >= 1 takes the private-card lane and sets
mIsRon = false, which cancels the automation bypass — the bypass requires
mIsRon. Every guide leads with style = 1, which is precisely what blocks the
working path. (One UI 9.0 relaxed this: both lanes reach Showing there.)
Samsung also ships a developer switch for the same bypass —
Settings.Secure enable_notification_nowbar_test = 1 — which is useful for
testing but needs adb, so the automation extra is the shippable route.
Three independent channels, not one API:
| App | Channel | Evidence |
|---|---|---|
| YouTube | MediaSession (public API); SystemUI renders the card itself | YouTube's APK contains 0 ongoingActivityNoti strings and POST_NOTIFICATIONS: granted=false, yet still shows |
| Google Maps | Samsung private extras | Maps' APK contains 3 ongoingActivityNoti strings |
| Google app stocks/weather | SmartSpace | com.samsung.android.smartsuggestions/.extservice.services.CustomSmartspaceService, MANAGE_SMARTSPACE granted |
FACE_WIDGET / SERVICEBOX_REMOTEVIEWS (both signature|privileged) gate
Samsung's RemoteViews custom card surface — they are not what gates
ordinary Showing-list entry.
Requires Android Studio's JDK (AGP rejects newer JDKs) and an Android 16+ device or emulator.
./gradlew :app:assembleDebug
./debug/install.sh --samsung --home # install + post via the automation lane
./debug/nowbar-probe.sh # read back what the OS did
gradle.propertiespinsorg.gradle.java.hometo Android Studio's bundled JDK. That path is machine-specific — change or remove it on another machine.compileSdkis 37 rather than the 36 the app targets, because core-ktx 1.19.0 and compose-bom 2026.09.00 publishminCompileSdk 37.targetSdkstays 36.
| Script | Purpose |
|---|---|
install.sh |
Build → install → launch. --plain / --samsung / --card pick the lane; --home backgrounds the app |
nowbar-probe.sh |
Reads notification flags, Samsung Showing/Hidden/Pending, AOSP surfaces, then prints a verdict. --watch N to follow |
build-apk.sh |
Stages a traceable APK in debug/apk/ for Firebase Test Lab |
rtl.sh |
Drives a Samsung Remote Test Lab device over RDB, reconnecting when the tunnel drops |
_lib.sh |
Shared helpers — device picker, install retry, timeouts |
The test modes are driven by intent extras, so a run is reproducible without tapping screen coordinates:
adb shell am start -n com.test.island.dynamic.android/.MainActivity --ez auto_samsung trueandroid.ongoingActivityNoti.automationis undocumented and appears to be intended for Samsung's own automation tooling. It survived One UI 8.5 → 9.0, but it can disappear in any update.- Setting Bundle extras is not a hidden-API call, so it is not subject to
Android's hidden-API blocklist. Reflecting on
android.app.Notification$OngoingActivityStylewould be — this app uses that class only as a presence probe. AllowedOngoingActivityListManagerreadsallowedList,isAllowListUsingandblockedRONAppListfromINotificationManager, i.e. framework-supplied. A package onblockedRONAppListis rejected beforemIsRonis even assigned, so Samsung retains a kill switch.isNowBarVisibleis not a per-app signal — it readstrueat a clean baseline with nothing posted. Use Showing-list membership.- Findings are from specific devices on specific builds. Re-verify after OS updates; the app is the tool for doing that.
This is a diagnostic harness built by reading publicly readable device state
(dumpsys, getprop) and decompiling on-device system APKs for
interoperability. It ships no Samsung code.