skiptools / skiptools/skip-ui

Local notifications silently do nothing on Android: `add()` returns success with no delegate, and `NotificationWorker` throws on a missing icon resource

Open
#514 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Swift
Stars
330
Forks
76
Avg merge
3h 6m
Merged PRs (30d)
1

Description

Environment: Skip 1.9.7, skip-ui 1.59.2, skip-fuse-ui 1.18.1, Swift 6.3.0, Android 15 (API 35) emulator. Skip Fuse.

Summary

On Android, scheduling a local notification through
UNUserNotificationCenter.current().add(_:) completes without throwing and
delivers nothing. There is no error, no log, and no notification record — the
app has no way to know it failed.

Two independent causes, both of which must be fixed by the app, and neither of
which is discoverable from the API:

1. add() silently no-ops unless a delegate is set.
SkipUI/UIKit/UserNotifications.swift begins:

public func add(_ request: UNNotificationRequest) async throws {
    guard let delegate else { return }
    let notification = UNNotification(request: request, date: Date.now)
    let options = await delegate.userNotificationCenter(self, willPresent: notification)
    guard options.contains(.banner) || options.contains(.alert) else { return }
    ...

This diverges from iOS in two ways. On iOS add() does not require a delegate
at all, and willPresent is consulted only when a notification fires while
the app is foregrounded
— it has no bearing on whether a notification is
scheduled or delivered while backgrounded. Here it is consulted at schedule
time and silently discards the request.

The practical effect: an app that follows Apple's documentation, sets no
delegate, and schedules a notification gets a successful add() and no
notification, forever, with nothing to debug.

2. NotificationWorker throws when neither icon resource exists.
Once a delegate is set, the work is enqueued and then fails:

E WM-WorkerWrapper: Work [ tags={ skip.ui.NotificationWorker, ... } ] failed because it threw an exception/error
E WM-WorkerWrapper: java.lang.IllegalArgumentException: Drawable resource ID must not be 0
E WM-WorkerWrapper:     at androidx.core.graphics.drawable.IconCompat.createWithResource(IconCompat.java:237)
E WM-WorkerWrapper:     at skip.ui.NotificationWorker.doWork(UserNotifications.kt:1153)

The worker resolves its small icon as:

var resId = context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName())
if resId == 0 {
    resId = context.getResources().getIdentifier("ic_launcher", "mipmap", context.getPackageName())
}
builder.setSmallIcon(IconCompat.createWithResource(context, resId))

A stock skip create app ships neither drawable/ic_notification nor
mipmap/ic_launcher — it has no Android/app/src/main/res directory at all,
since icons are generated from the Darwin asset catalog. So both lookups return
0 and IconCompat.createWithResource throws. This failure happens on a
WorkManager background thread, so again the app sees nothing.

Related: #203 covered the same icon lookup, for the case where a resource
exists but is in the wrong format. This is the adjacent case — where neither
resource exists at all — which throws instead of rendering incorrectly.

Reproduction
  1. skip create a Fuse app.
  2. Add <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    to the manifest and grant it at runtime.
  3. Schedule a notification:
let content = UNMutableNotificationContent()
content.title = "Test"
content.body = "Hello"
let request = UNNotificationRequest(
    identifier: "test",
    content: content,
    trigger: UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
)
try await UNUserNotificationCenter.current().add(request)
  1. Nothing is delivered. adb shell dumpsys notification --noredact shows no
    record for the package and no registered channel. add() did not throw.
Suggested fixes
  • Do not require a delegate to schedule. Treat a missing delegate as the
    iOS default (deliver normally), and consult willPresent at delivery time
    for foreground presentation, not at schedule time. If the current behaviour
    is intentional, add() should throw rather than return successfully.
  • Fall back to a built-in icon (android.R.drawable.ic_dialog_info or the
    application icon from context.applicationInfo.icon) when neither lookup
    resolves, rather than passing 0 to IconCompat. Alternatively, have
    skip create generate a default ic_notification drawable.
  • Surface worker failures. A NotificationWorker exception is currently
    invisible to the app; logging it through Skip's logger would have turned a
    multi-hour investigation into a one-line diagnosis.

The silent-success path is the worst part of this. Either failure alone would
be quick to find if it reported anything at all.

Workaround (what we shipped)

No #if SKIP escape hatch required — both fixes are ordinary app code:

  1. Set a delegate that opts into presentation:
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
        return [.banner, .sound]
    }
}
// retain it — UNUserNotificationCenter.delegate is weak
UNUserNotificationCenter.current().delegate = notificationDelegate
  1. Add Android/app/src/main/res/drawable/ic_notification.xml (a white
    silhouette on a transparent background).

With both in place, delivery works on Android and the same source is unchanged
on iOS.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with SkipUI/UIKit/UserNotifications.swift and the NotificationWorker implementation in UserNotifications.kt, then reproduce the issue using the supplied skip create app and adb commands. Trace scheduling without a delegate and worker behavior when both icon lookups return zero. Done means both cases report or handle failure appropriately and notifications can be scheduled and delivered without app-provided icon resources.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, kotlin, swift
Domain
mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.