ReviewOS

craft-native/craft

setBadge does nothing on Android — constructs a NotificationManagerCompat and discards it

#148
Closed glennmichael123 opened this 9 days ago ·
9 days ago

craft.setBadge(count) works on iOS and silently does nothing on Android.

The Kotlin

@JavascriptInterface
fun setBadge(count: Int) {
    // Android doesn't have native badge support, but we can use notification channels
    // This is a simplified implementation - for full support, use ShortcutBadger library
    try {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager = NotificationManagerCompat.from(activity)
            // Badge is typically shown via notification count
            // Full implementation would require creating a notification
        }
    } catch (e: Exception) {
        log("Badge not supported: ${e.message}")
    }
}

@JavascriptInterface
fun clearBadge() {
    setBadge(0)
}

It constructs a NotificationManagerCompat, discards it, and returns. count is never read. clearBadge delegates to it.

Why it is silent rather than merely broken

Every part of the surface reports success:

  • the injected JS exposes it — setBadge: function(count) { CraftAndroid.setBadge(count) }
  • craft.d.ts:506 declares setBadge(count: number): void for both platforms
  • the catch only fires on an exception, and nothing here throws — so not even a log line

And the same call works on iOS: bridge_mobile_display.zig implements setBadge/clearBadge for real, including badge authorization. So a page that sets an unread count is correct on one platform and quietly wrong on the other, with nothing anywhere to indicate which.

Same class as the bugs this repo has been finding

  • the ota* methods — page-callable, no handler behind them
  • registerBackgroundTask — inserts into a Set nothing reads, resolves registered: true, and never calls BGTaskScheduler.register
  • craftShortcut / craftOTAProgress / craftOTAStatus (#127) — subscribed by both bridges, dispatched by neither

The shared shape is an implementation that returns successfully without doing the thing.

What to do about it

Android genuinely has no first-party badge API — the comment is right about that. Launchers implement it through vendor-specific broadcasts, which is what ShortcutBadger wraps, and there is no supported route that works across OEMs.

So the honest options are to implement it via a notification with setNumber, which is what NotificationManagerCompat was reached for and would at least do something on launchers that read notification counts; or to make it refuse, so a page can tell. What it should not do is what it does now.

Meanwhile

Both are recorded in deliberate_deferrals in android_conformance_test.zig with the anti-rot guards the iOS table uses — a row Zig starts serving fails the build, and so does a row naming an action the Kotlin no longer has. Migrating them to Zig now would mean porting a no-op, which is what the iOS table already refuses to do for startVideoRecording (#115) for the same reason: there is no working contract to carry across.

9 days ago

The Android half is the same defect, and slightly more explicit about it.

@JavascriptInterface
fun setBadge(count: Int) {
    // Android doesn't have native badge support, but we can use notification channels
    // This is a simplified implementation - for full support, use ShortcutBadger library
    try {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager = NotificationManagerCompat.from(activity)
            // Badge is typically shown via notification count
            // Full implementation would require creating a notification
        }
    } catch (e: Exception) {
        log("Badge not supported: ${e.message}")
    }
}

count is never read. The NotificationManagerCompat is built and discarded, exactly as the iOS UNUserNotificationCenter is. The method returns normally, so craft.setBadge(3) reports success on both platforms and changes nothing on either.

clearBadge() is setBadge(0), so it inherits the same non-behaviour.

Two things make the Android side worth listing separately from iOS rather than only being "the same bug twice":

  • The comments say so. "Full implementation would require creating a notification" is an accurate description of unfinished work sitting in shipped code with no marker a reader would grep for — no TODO, no throw, no rejection.
  • Android genuinely cannot do this from the framework. There is no badge API; launchers read it from notification counts or from vendor-specific broadcasts, which is what ShortcutBadger wraps. So unlike iOS, where the fix is to call setBadgeCount on the manager it already has, the Android fix is a design decision: post a notification carrying setNumber(count), take the ShortcutBadger dependency, or declare the method unsupported and reject.

Declaring it unsupported is defensible here in a way it is not on iOS. What is not defensible on either platform is resolving successfully.

Zig serves neither setBadge nor clearBadge — migrating a method whose body does nothing would only move the silence.

9 days ago

Implemented and pushed in d677e1a. Android now reports appBadge: false, badge calls throw a clear unsupported-platform error instead of silently succeeding, and the dead native methods/import were removed. Types, docs, test bridge, generator regression, and the Android conformance ratchet were updated. Exact-commit CI, SBOM/vulnerability scan, Binary Size, and Benchmark are green.

Sign in to comment on this issue.