From 4aecbf6c3bb2a52a85e452cfdefbf0eec5036ecb Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:21:22 +0100 Subject: [PATCH 01/12] Update UserNotifications.swift --- .../SkipUI/UIKit/UserNotifications.swift | 266 ++++++++++++++---- 1 file changed, 205 insertions(+), 61 deletions(-) diff --git a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift index 87435ee7..106a4088 100644 --- a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift +++ b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift @@ -4,6 +4,7 @@ import Foundation #if SKIP import android.Manifest +import android.app.AlarmManager import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent @@ -95,17 +96,25 @@ public final class UNUserNotificationCenter { 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 } + #if SKIP guard let activity = UIApplication.shared.androidActivity else { return } - let intent = Intent(activity, type(of: activity).java) + + let intent = Intent("skip.notification.receiver") + intent.setPackage(activity.getPackageName()) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + + intent.putExtra("title", request.content.title) + intent.putExtra("body", request.content.body) + let extras = android.os.Bundle() for (key, value) in request.content.userInfo { if let s = value as? String { @@ -121,83 +130,201 @@ public final class UNUserNotificationCenter { } } intent.putExtras(extras) - - // SKIP INSERT: val pendingFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - let pendingIntent = PendingIntent.getActivity(activity, 0, intent, pendingFlags) - - let channelID = "tools.skip.firebase.messaging" // Match AndroidManifest.xml - let notificationBuilder = NotificationCompat.Builder(activity, channelID) - .setContentTitle(request.content.title) - .setContentText(request.content.body) - .setAutoCancel(true) - .setContentIntent(pendingIntent) - let application = activity.application - if let imageAttachment = request.content.attachments.first(where: { $0.type == "public.image" }) { - notificationBuilder.setSmallIcon(IconCompat.createWithContentUri(imageAttachment.url.absoluteString)) - } else { - let packageName = application.getPackageName() + + let pendingFlags = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + let pendingIntent = PendingIntent.getBroadcast(activity, request.identifier.hashValue, intent, pendingFlags) + + if let nextDate = + (request.trigger as? UNCalendarNotificationTrigger)?.nextTriggerDate() ?? + (request.trigger as? UNTimeIntervalNotificationTrigger)?.nextTriggerDate() { + let triggerMillis = nextDate.currentTimeMillis + let alarmManager = activity.getSystemService(Context.ALARM_SERVICE) as! AlarmManager + let canScheduleExactAlarm = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || alarmManager.canScheduleExactAlarms() + if canScheduleExactAlarm { + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerMillis, pendingIntent) + } else { + alarmManager.set(AlarmManager.RTC_WAKEUP, triggerMillis, pendingIntent) + } + + let preferences = activity.getSharedPreferences("alarms", Context.MODE_PRIVATE) + let editor = preferences.edit() + let ids = java.util.HashSet(preferences.getStringSet("ids", java.util.HashSet()) ?? java.util.HashSet()) + ids.add(request.identifier) + editor.putStringSet("ids", ids) + editor.putLong("date_" + request.identifier, triggerMillis) + editor.apply() + } else { + let channelID = "tools.skip.firebase.messaging" + let notificationBuilder = NotificationCompat.Builder(activity, channelID) + .setContentTitle(request.content.title) + .setContentText(request.content.body) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + // Notification icon: must be a resource with transparent background and white logo // eg: to be used as a default icon must be added in the AndroidManifest.xml with the following code: // - + let application = activity.application + let packageName = application.getPackageName() let iconNotificationIdentifier = "ic_notification" - let resourceFolder = "drawable" - - var resId = application.resources.getIdentifier(iconNotificationIdentifier, resourceFolder, packageName) - + // Check if the resource is found, otherwise fallback to use the default app icon (eg. ic_launcher) + var resId = application.resources.getIdentifier(iconNotificationIdentifier, "drawable", packageName) if resId == 0 { resId = application.resources.getIdentifier("ic_launcher", "mipmap", packageName) } - + notificationBuilder.setSmallIcon(IconCompat.createWithResource(application, resId)) - } - let manager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as! NotificationManager - let appName = application.packageManager.getApplicationLabel(application.applicationInfo) - let channel = NotificationChannel(channelID, appName, NotificationManager.IMPORTANCE_DEFAULT) - manager.createNotificationChannel(channel) - manager.notify(Random.nextInt(), notificationBuilder.build()) + let manager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as! NotificationManager + let appName = application.packageManager.getApplicationLabel(application.applicationInfo) + + if Build.VERSION.SDK_INT >= Build.VERSION_CODES.O { + let channel = NotificationChannel(channelID, appName, NotificationManager.IMPORTANCE_DEFAULT) + manager.createNotificationChannel(channel) + } + + manager.notify(request.identifier.hashValue, notificationBuilder.build()) + } #endif } - - @available(*, unavailable) - public func getPendingNotificationRequests() async -> [Any /* UNNotificationRequest */] { + + public func pendingNotificationRequests() async -> [UNNotificationRequest] { + #if SKIP + return getPendingNotificationRequests() + #else fatalError() + #endif } - - @available(*, unavailable) - public func removePendingNotificationRequests(withIdentifiers: [String]) { + + public func removePendingNotificationRequests(withIdentifiers identifiers: [String]) { + #if SKIP + guard let activity = UIApplication.shared.androidActivity else { return } + let alarmManager = activity.getSystemService(Context.ALARM_SERVICE) as! AlarmManager + let preferences = activity.getSharedPreferences("alarms", Context.MODE_PRIVATE) + let editor = preferences.edit() + + let ids = java.util.HashSet(preferences.getStringSet("ids", java.util.HashSet()) ?? java.util.HashSet()) + + for identifier in identifiers { + let intent = Intent("skip.notification.receiver") + intent.setPackage(activity.getPackageName()) + + let pendingFlags = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + let pendingIntent = PendingIntent.getBroadcast(activity, identifier.hashValue, intent, pendingFlags) + + if pendingIntent != nil { + alarmManager.cancel(pendingIntent) + pendingIntent.cancel() + } + + ids.remove(identifier) + editor.remove("date_" + identifier) + } + + editor.putStringSet("ids", ids) + editor.apply() + #endif } - - @available(*, unavailable) + public func removeAllPendingNotificationRequests() { + #if SKIP + let pendingNotifications = getPendingNotificationRequests() + let identifiers = pendingNotifications.map { $0.identifier } + removePendingNotificationRequests(withIdentifiers: identifiers) + #else + fatalError() + #endif } - - @available(*, unavailable) - public func getDeliveredNotifications() async -> [Any /* UNNotification */] { + + public func deliveredNotifications() async -> [UNNotification] { + #if SKIP + return getDeliveredNotifications() + #else fatalError() + #endif } - - @available(*, unavailable) - public func removeDeliveredNotifications(withIdentifiers: [String]) { + + public func removeDeliveredNotifications(withIdentifiers identifiers: [String]) { + #if SKIP + guard let activity = UIApplication.shared.androidActivity else { return } + + let notificationManager = activity.getSystemService(android.content.Context.NOTIFICATION_SERVICE) as! android.app.NotificationManager + + let preferences = activity.getSharedPreferences("alarms", android.content.Context.MODE_PRIVATE) + let editor = preferences.edit() + let ids = java.util.HashSet(preferences.getStringSet("ids", java.util.HashSet()) ?? java.util.HashSet()) + + for identifier in identifiers { + notificationManager.cancel(identifier.hashValue) + ids.remove(identifier) + editor.remove("date_" + identifier) + } + + editor.putStringSet("ids", ids) + editor.apply() + #else + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers) + #endif } - - @available(*, unavailable) + public func removeAllDeliveredNotifications() { + #if SKIP + let deliveredNotifications = getDeliveredNotifications() + let identifiers = deliveredNotifications.map { $0.request.identifier } + removeDeliveredNotifications(withIdentifiers: identifiers) + #else + fatalError() + #endif } - + @available(*, unavailable) public func setNotificationCategories(_ categories: Set) { } - + @available(*, unavailable) public func getNotificationCategories() async -> Set { fatalError() } + + #if SKIP + private func getAllNotificationRequests() -> [(id: String, timestamp: Long)] { + guard let activity = UIApplication.shared.androidActivity else { return [] } + let preferences = activity.getSharedPreferences("alarms", Context.MODE_PRIVATE) + let ids = preferences.getStringSet("ids", nil) ?? java.util.HashSet() + + var all: [(id: String, timestamp: Long)] = [] + let iterator = ids.iterator() + while iterator.hasNext() { + let id = iterator.next() as! String + let time = preferences.getLong("date_" + id, 0) + all.append((id: id, timestamp: time)) + } + return all + } + + private func getPendingNotificationRequests() -> [UNNotificationRequest] { + let now = Date().currentTimeMillis + return getAllNotificationRequests() + .filter { $0.timestamp > now } + .map { + UNNotificationRequest(identifier: $0.id, content: UNMutableNotificationContent(), trigger: nil) + } + } + + private func getDeliveredNotifications() -> [UNNotification] { + let now = Date().currentTimeMillis + return getAllNotificationRequests() + .filter { $0.timestamp <= now } + .map { + let request = UNNotificationRequest(identifier: $0.id, content: UNMutableNotificationContent(), trigger: nil) + return UNNotification(request: request, date: Date(timeIntervalSince1970: Double($0.timestamp) / 1000.0)) + } + } + #endif } // SKIP @bridge @@ -313,21 +440,21 @@ public struct UNNotificationPresentationOptions : OptionSet { // SKIP @bridge public class UNNotificationContent { // SKIP @bridge - public internal(set) var title: String + public var title: String // SKIP @bridge - public internal(set) var subtitle: String + public var subtitle: String // SKIP @bridge - public internal(set) var body: String - public internal(set) var badge: NSNumber? + public var body: String + public var badge: NSNumber? // SKIP @bridge public var bridgedBadge: Int? { return badge?.intValue } // SKIP @bridge - public internal(set) var sound: UNNotificationSound? + public var sound: UNNotificationSound? // SKIP @bridge - public internal(set) var launchImageName: String - public internal(set) var userInfo: [AnyHashable: Any] + public var launchImageName: String + public var userInfo: [AnyHashable: Any] // SKIP @bridge public var bridgedUserInfo: [AnyHashable: Any] { return userInfo.filter { entry in @@ -336,19 +463,19 @@ public class UNNotificationContent { } } // SKIP @bridge - public internal(set) var attachments: [UNNotificationAttachment] + public var attachments: [UNNotificationAttachment] // SKIP @bridge - public internal(set) var categoryIdentifier: String + public var categoryIdentifier: String // SKIP @bridge - public internal(set) var threadIdentifier: String + public var threadIdentifier: String // SKIP @bridge - public internal(set) var targetContentIdentifier: String? + public var targetContentIdentifier: String? // SKIP @bridge - public internal(set) var summaryArgument: String + public var summaryArgument: String // SKIP @bridge - public internal(set) var summaryArgumentCount: Int + public var summaryArgumentCount: Int // SKIP @bridge - public internal(set) var filterCriteria: String? + public var filterCriteria: String? public init(title: String = "", subtitle: String = "", body: String = "", badge: NSNumber? = nil, sound: UNNotificationSound? = UNNotificationSound.default, launchImageName: String = "", userInfo: [AnyHashable: Any] = [:], attachments: [UNNotificationAttachment] = [], categoryIdentifier: String = "", threadIdentifier: String = "", targetContentIdentifier: String? = nil, summaryArgument: String = "", summaryArgumentCount: Int = 0, filterCriteria: String? = nil) { self.title = title @@ -524,15 +651,32 @@ public final class UNTimeIntervalNotificationTrigger: UNNotificationTrigger { self.timeInterval = timeInterval super.init(repeats: repeats) } + + public func nextTriggerDate() -> Date? { + let now = Date() + return now.addingTimeInterval(self.timeInterval) + } } public final class UNCalendarNotificationTrigger: UNNotificationTrigger { public let dateComponents: DateComponents - public init(dateComponents: DateComponents, repeats: Bool) { + public init(dateMatching dateComponents: DateComponents, repeats: Bool) { self.dateComponents = dateComponents super.init(repeats: repeats) } + + public func nextTriggerDate() -> Date? { + let calendar = Calendar.current + let now = Date() + return calendar.nextDate( + after: now, + matching: self.dateComponents, + matchingPolicy: .nextTime, + repeatedTimePolicy: .first, + direction: .forward + ) + } } public final class UNLocationNotificationTrigger: UNNotificationTrigger { From 8a51883967e05c52577a1bc19fe6046af8e73d32 Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:34:09 +0100 Subject: [PATCH 02/12] Update UserNotifications.swift --- Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift index 106a4088..188459f7 100644 --- a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift +++ b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift @@ -669,13 +669,22 @@ public final class UNCalendarNotificationTrigger: UNNotificationTrigger { public func nextTriggerDate() -> Date? { let calendar = Calendar.current let now = Date() - return calendar.nextDate( + + guard let nextDate = calendar.nextDate( after: now, matching: self.dateComponents, matchingPolicy: .nextTime, repeatedTimePolicy: .first, direction: .forward - ) + ) else { + return nil + } + + if !self.repeats && nextDate <= now { + return nil + } + + return nextDate } } From e9c5f85decb0985f7d0a219fa2cbdacf8d3db650 Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:09:16 +0100 Subject: [PATCH 03/12] Updated Readme --- Package.swift | 4 ++++ README.md | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 5428ed7a..31859366 100644 --- a/Package.swift +++ b/Package.swift @@ -9,6 +9,10 @@ let package = Package( ], dependencies: [ .package(url: "https://source.skip.tools/skip.git", from: "1.6.21"), + + // TODO: Remove temporary reference to skip foundation after PR has been merged (see: https://github.com/skiptools/skip-foundation/pull/93) + .package(url: "https://github.com/fhasse95/skip-foundation.git", branch: "Added-Calendar-Enumeration-Support"), + .package(url: "https://source.skip.tools/skip-model.git", from: "1.6.2"), ], targets: [ diff --git a/README.md b/README.md index f74fba35..26477410 100644 --- a/README.md +++ b/README.md @@ -2466,13 +2466,13 @@ Support levels:
UNNotificationTrigger
    -
  • Ignored on Android
  • +
  • Only `nextTriggerDate` is evaluated but not the `repeat` value
- 🟠 + 🟡
UNUserNotificationCenter @@ -2481,7 +2481,13 @@ Support levels:
  • func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool
  • var delegate: (any UNUserNotificationCenterDelegate)?
  • func add(_ request: UNNotificationRequest) async throws
  • -
  • The `add` function ignores all scheduling and repeat options and simply delivers the notification immediately.
  • +
  • The `add` function ignores all repeat options and and simply delivers the notification either immediately or after the next trigger date is reached.
  • +
  • func pendingNotificationRequests() async -> [UNNotificationRequest]
  • +
  • func removePendingNotificationRequests(withIdentifiers identifiers: [String])
  • +
  • func removeAllPendingNotificationRequests()
  • +
  • func deliveredNotifications() async -> [UNNotification]
  • +
  • func removeDeliveredNotifications(withIdentifiers identifiers: [String])
  • +
  • func removeAllDeliveredNotifications()
  • From 30105f8a37bb67441a1965d04dc190b0115b8ade Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Sun, 15 Feb 2026 00:44:41 +0100 Subject: [PATCH 04/12] Added bridging --- Package.swift | 2 +- .../SkipUI/UIKit/UserNotifications.swift | 42 +++++++++++-------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/Package.swift b/Package.swift index 31859366..bd985442 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,7 @@ let package = Package( .package(url: "https://source.skip.tools/skip.git", from: "1.6.21"), // TODO: Remove temporary reference to skip foundation after PR has been merged (see: https://github.com/skiptools/skip-foundation/pull/93) - .package(url: "https://github.com/fhasse95/skip-foundation.git", branch: "Added-Calendar-Enumeration-Support"), + .package(path: "/Users/fabian/Desktop/Develop/Contributions/skip-foundation"), .package(url: "https://source.skip.tools/skip-model.git", from: "1.6.2"), ], diff --git a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift index 188459f7..b932c40d 100644 --- a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift +++ b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift @@ -191,15 +191,17 @@ public final class UNUserNotificationCenter { } #endif } - - public func pendingNotificationRequests() async -> [UNNotificationRequest] { + + // SKIP @bridge + public func pendingNotificationRequests() async -> [Any /* UNNotificationRequest */] { #if SKIP return getPendingNotificationRequests() #else fatalError() #endif } - + + // SKIP @bridge public func removePendingNotificationRequests(withIdentifiers identifiers: [String]) { #if SKIP guard let activity = UIApplication.shared.androidActivity else { return } @@ -229,25 +231,28 @@ public final class UNUserNotificationCenter { editor.apply() #endif } - + + // SKIP @bridge public func removeAllPendingNotificationRequests() { #if SKIP let pendingNotifications = getPendingNotificationRequests() - let identifiers = pendingNotifications.map { $0.identifier } + let identifiers = pendingNotifications.compactMap { ($0 as? UNNotificationRequest)?.identifier } removePendingNotificationRequests(withIdentifiers: identifiers) #else fatalError() #endif } - - public func deliveredNotifications() async -> [UNNotification] { + + // SKIP @bridge + public func deliveredNotifications() async -> [Any /* UNNotification */] { #if SKIP return getDeliveredNotifications() #else fatalError() #endif } - + + // SKIP @bridge public func removeDeliveredNotifications(withIdentifiers identifiers: [String]) { #if SKIP guard let activity = UIApplication.shared.androidActivity else { return } @@ -267,29 +272,30 @@ public final class UNUserNotificationCenter { editor.putStringSet("ids", ids) editor.apply() #else - UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers) + fatalError() #endif } - + + // SKIP @bridge public func removeAllDeliveredNotifications() { #if SKIP let deliveredNotifications = getDeliveredNotifications() - let identifiers = deliveredNotifications.map { $0.request.identifier } + let identifiers = deliveredNotifications.compactMap { ($0 as? UNNotification)?.request.identifier } removeDeliveredNotifications(withIdentifiers: identifiers) #else fatalError() #endif } - + @available(*, unavailable) public func setNotificationCategories(_ categories: Set) { } - + @available(*, unavailable) public func getNotificationCategories() async -> Set { fatalError() } - + #if SKIP private func getAllNotificationRequests() -> [(id: String, timestamp: Long)] { guard let activity = UIApplication.shared.androidActivity else { return [] } @@ -305,8 +311,8 @@ public final class UNUserNotificationCenter { } return all } - - private func getPendingNotificationRequests() -> [UNNotificationRequest] { + + private func getPendingNotificationRequests() -> [Any /* UNNotificationRequest */] { let now = Date().currentTimeMillis return getAllNotificationRequests() .filter { $0.timestamp > now } @@ -314,8 +320,8 @@ public final class UNUserNotificationCenter { UNNotificationRequest(identifier: $0.id, content: UNMutableNotificationContent(), trigger: nil) } } - - private func getDeliveredNotifications() -> [UNNotification] { + + private func getDeliveredNotifications() -> [Any /* UNNotification */] { let now = Date().currentTimeMillis return getAllNotificationRequests() .filter { $0.timestamp <= now } From 8a8bf54fde7ad5a30ad7250d88cf33dcbecc68be Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:07:55 +0100 Subject: [PATCH 05/12] Added Bridging --- .../SkipUI/UIKit/UserNotifications.swift | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift index b932c40d..7dad54fe 100644 --- a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift +++ b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift @@ -391,7 +391,8 @@ public final class UNNotificationRequest : @unchecked Sendable { // SKIP @bridge public let content: UNNotificationContent public let trigger: UNNotificationTrigger? - + + // SKIP @bridge public init(identifier: String, content: UNNotificationContent, trigger: UNNotificationTrigger?) { self.identifier = identifier self.content = content @@ -642,43 +643,57 @@ public class UNNotificationAttachment { } } +// SKIP @bridge public class UNNotificationTrigger { + + // SKIP @bridge public let repeats: Bool + // SKIP @bridge public init(repeats: Bool) { self.repeats = repeats } } +// SKIP @bridge public final class UNTimeIntervalNotificationTrigger: UNNotificationTrigger { + + // SKIP @bridge public let timeInterval: TimeInterval + // SKIP @bridge public init(timeInterval: TimeInterval, repeats: Bool) { self.timeInterval = timeInterval super.init(repeats: repeats) } + // SKIP @bridge public func nextTriggerDate() -> Date? { let now = Date() return now.addingTimeInterval(self.timeInterval) } } +// SKIP @bridge public final class UNCalendarNotificationTrigger: UNNotificationTrigger { - public let dateComponents: DateComponents - - public init(dateMatching dateComponents: DateComponents, repeats: Bool) { + + // SKIP @bridge + public let dateComponents: Any /* DateComponents */ + + // SKIP @bridge + public init(dateMatching dateComponents: Any /* DateComponents */, repeats: Bool) { self.dateComponents = dateComponents super.init(repeats: repeats) } + // SKIP @bridge public func nextTriggerDate() -> Date? { let calendar = Calendar.current let now = Date() guard let nextDate = calendar.nextDate( after: now, - matching: self.dateComponents, + matching: self.dateComponents as! DateComponents, matchingPolicy: .nextTime, repeatedTimePolicy: .first, direction: .forward From 41142e70ddc34694de7003011a49a75b106e9544 Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Sat, 21 Feb 2026 13:37:04 +0100 Subject: [PATCH 06/12] Replaced AlarmManager with WorkManager --- Sources/SkipUI/Skip/skip.yml | 5 +- .../SkipUI/UIKit/UserNotifications.swift | 217 +++++++++--------- 2 files changed, 114 insertions(+), 108 deletions(-) diff --git a/Sources/SkipUI/Skip/skip.yml b/Sources/SkipUI/Skip/skip.yml index 500796b7..8981e62b 100644 --- a/Sources/SkipUI/Skip/skip.yml +++ b/Sources/SkipUI/Skip/skip.yml @@ -18,6 +18,7 @@ settings: - 'version("androidx-activity", "1.11.0")' - 'version("androidx-lifecycle-process", "2.9.2")' - 'version("androidx-material3-adaptive", "1.1.0")' + - 'version("androidx-work", "2.11.1")' # the version for these libraries is derived from the kotlin-bom declared is skip-model/Sources/SkipModel/Skip/skip.yml - 'library("androidx-core-ktx", "androidx.core", "core-ktx").withoutVersion()' @@ -36,6 +37,8 @@ settings: - 'library("androidx-lifecycle-process", "androidx.lifecycle", "lifecycle-process").versionRef("androidx-lifecycle-process")' - 'library("androidx-compose-material3-adaptive", "androidx.compose.material3.adaptive", "adaptive").versionRef("androidx-material3-adaptive")' + - 'library("androidx-work-runtime", "androidx.work", "work-runtime-ktx").versionRef("androidx-work")' + - 'library("coil-compose", "io.coil-kt.coil3", "coil-compose").versionRef("coil")' - 'library("coil-network-okhttp", "io.coil-kt.coil3", "coil-network-okhttp").versionRef("coil")' - 'library("coil-svg", "io.coil-kt.coil3", "coil-svg").versionRef("coil")' @@ -75,6 +78,7 @@ build: - 'api(libs.androidx.appcompat.resources)' - 'api(libs.androidx.activity.compose)' - 'api(libs.androidx.lifecycle.process)' + - 'api(libs.androidx.work.runtime)' - 'implementation(libs.coil.compose)' - 'implementation(libs.coil.network.okhttp)' - 'implementation(libs.coil.svg)' @@ -86,4 +90,3 @@ build: - 'androidTestImplementation(libs.androidx.compose.ui.test.junit4)' - 'testImplementation(libs.androidx.compose.ui.test.manifest)' - 'androidTestImplementation(libs.androidx.compose.ui.test.manifest)' - diff --git a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift index 7dad54fe..a2d01428 100644 --- a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift +++ b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift @@ -4,7 +4,6 @@ import Foundation #if SKIP import android.Manifest -import android.app.AlarmManager import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent @@ -20,6 +19,13 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner +import androidx.work.WorkManager +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.Data +import androidx.work.Worker +import androidx.work.WorkerParameters +import androidx.work.ListenableWorker +import java.util.concurrent.TimeUnit import kotlin.random.Random #endif @@ -93,101 +99,47 @@ public final class UNUserNotificationCenter { // SKIP @bridge public func add(_ request: UNNotificationRequest) async throws { - guard let delegate else { - return - } + 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 - } + guard options.contains(.banner) || options.contains(.alert) else { return } #if SKIP - guard let activity = UIApplication.shared.androidActivity else { - return - } - - let intent = Intent("skip.notification.receiver") - intent.setPackage(activity.getPackageName()) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - - intent.putExtra("title", request.content.title) - intent.putExtra("body", request.content.body) + guard let activity = UIApplication.shared.androidActivity else { return } - let extras = android.os.Bundle() + // Build the data which should be displayed in the notification. + let dataBuilder = Data.Builder() + .putString("title", request.content.title) + .putString("body", request.content.body) + .putInt("id", request.identifier.hashValue) for (key, value) in request.content.userInfo { - if let s = value as? String { - extras.putString(key.toString(), s) - } else if let b = value as? Bool { - extras.putBoolean(key.toString(), b) - } else if let i = value as? Int { - extras.putInt(key.toString(), i) - } else if let d = value as? Double { - extras.putDouble(key.toString(), d) - } else { - extras.putString(key.toString(), value.toString()) - } + dataBuilder.putString(key.toString(), value.toString()) } - intent.putExtras(extras) - let pendingFlags = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - let pendingIntent = PendingIntent.getBroadcast(activity, request.identifier.hashValue, intent, pendingFlags) + // Get the next trigger date (if any). + let nextDate = (request.trigger as? UNCalendarNotificationTrigger)?.nextTriggerDate() ?? + (request.trigger as? UNTimeIntervalNotificationTrigger)?.nextTriggerDate() + let delayMillis = nextDate != nil ? max(0, nextDate!.currentTimeMillis - System.currentTimeMillis()) : 0 - if let nextDate = - (request.trigger as? UNCalendarNotificationTrigger)?.nextTriggerDate() ?? - (request.trigger as? UNTimeIntervalNotificationTrigger)?.nextTriggerDate() { - - let triggerMillis = nextDate.currentTimeMillis - let alarmManager = activity.getSystemService(Context.ALARM_SERVICE) as! AlarmManager - let canScheduleExactAlarm = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || alarmManager.canScheduleExactAlarms() - if canScheduleExactAlarm { - alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerMillis, pendingIntent) - } else { - alarmManager.set(AlarmManager.RTC_WAKEUP, triggerMillis, pendingIntent) - } - - let preferences = activity.getSharedPreferences("alarms", Context.MODE_PRIVATE) - let editor = preferences.edit() - let ids = java.util.HashSet(preferences.getStringSet("ids", java.util.HashSet()) ?? java.util.HashSet()) + // Add the notification work request to the work manager. + let workData = dataBuilder.build() + let workRequest = OneTimeWorkRequestBuilder() + .setInitialDelay(delayMillis, TimeUnit.MILLISECONDS) + .setInputData(workData) + .addTag(request.identifier) + .build() + WorkManager.getInstance(activity).enqueue(workRequest) + + // Add the notification identifier to the shared preferences to be able to cancel it later. + if let triggerMillis = nextDate?.currentTimeMillis { + let prefs = activity.getSharedPreferences("notifications", Context.MODE_PRIVATE) + let ids = java.util.HashSet(prefs.getStringSet("ids", java.util.HashSet()) ?? java.util.HashSet()) ids.add(request.identifier) - editor.putStringSet("ids", ids) - editor.putLong("date_" + request.identifier, triggerMillis) - editor.apply() - } else { - let channelID = "tools.skip.firebase.messaging" - let notificationBuilder = NotificationCompat.Builder(activity, channelID) - .setContentTitle(request.content.title) - .setContentText(request.content.body) - .setAutoCancel(true) - .setContentIntent(pendingIntent) - - // Notification icon: must be a resource with transparent background and white logo - // eg: to be used as a default icon must be added in the AndroidManifest.xml with the following code: - // - let application = activity.application - let packageName = application.getPackageName() - let iconNotificationIdentifier = "ic_notification" - - // Check if the resource is found, otherwise fallback to use the default app icon (eg. ic_launcher) - var resId = application.resources.getIdentifier(iconNotificationIdentifier, "drawable", packageName) - if resId == 0 { - resId = application.resources.getIdentifier("ic_launcher", "mipmap", packageName) - } - - notificationBuilder.setSmallIcon(IconCompat.createWithResource(application, resId)) - - let manager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as! NotificationManager - let appName = application.packageManager.getApplicationLabel(application.applicationInfo) - - if Build.VERSION.SDK_INT >= Build.VERSION_CODES.O { - let channel = NotificationChannel(channelID, appName, NotificationManager.IMPORTANCE_DEFAULT) - manager.createNotificationChannel(channel) - } - - manager.notify(request.identifier.hashValue, notificationBuilder.build()) + prefs.edit() + .putStringSet("ids", ids) + .putLong("date_" + request.identifier, triggerMillis) + .apply() } #endif } @@ -205,30 +157,31 @@ public final class UNUserNotificationCenter { public func removePendingNotificationRequests(withIdentifiers identifiers: [String]) { #if SKIP guard let activity = UIApplication.shared.androidActivity else { return } - let alarmManager = activity.getSystemService(Context.ALARM_SERVICE) as! AlarmManager - let preferences = activity.getSharedPreferences("alarms", Context.MODE_PRIVATE) - let editor = preferences.edit() + // Get all notification identifiers from the shared preferences. + let preferences = activity.getSharedPreferences("notifications", Context.MODE_PRIVATE) + let editor = preferences.edit() let ids = java.util.HashSet(preferences.getStringSet("ids", java.util.HashSet()) ?? java.util.HashSet()) + // Get the work manager. + let workManager = WorkManager.getInstance(activity) + + // Cancel all pending notifications using the work manager. + let now = System.currentTimeMillis() for identifier in identifiers { - let intent = Intent("skip.notification.receiver") - intent.setPackage(activity.getPackageName()) - - let pendingFlags = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - let pendingIntent = PendingIntent.getBroadcast(activity, identifier.hashValue, intent, pendingFlags) - - if pendingIntent != nil { - alarmManager.cancel(pendingIntent) - pendingIntent.cancel() + let triggerTime = preferences.getLong("date_" + identifier, 0) + if triggerTime > now { + workManager.cancelAllWorkByTag(identifier) + ids.remove(identifier) + editor.remove("date_" + identifier) } - - ids.remove(identifier) - editor.remove("date_" + identifier) } + // Update the notification identifiers in the shared preferences. editor.putStringSet("ids", ids) editor.apply() + #else + fatalError() #endif } @@ -257,18 +210,26 @@ public final class UNUserNotificationCenter { #if SKIP guard let activity = UIApplication.shared.androidActivity else { return } - let notificationManager = activity.getSystemService(android.content.Context.NOTIFICATION_SERVICE) as! android.app.NotificationManager - - let preferences = activity.getSharedPreferences("alarms", android.content.Context.MODE_PRIVATE) + // Get all notification identifiers from the shared preferences. + let preferences = activity.getSharedPreferences("notifications", Context.MODE_PRIVATE) let editor = preferences.edit() let ids = java.util.HashSet(preferences.getStringSet("ids", java.util.HashSet()) ?? java.util.HashSet()) + // Get the notification manager. + let notificationManager = activity.getSystemService(android.content.Context.NOTIFICATION_SERVICE) as! android.app.NotificationManager + + // Cancel all delivered notifications using the notification manager. + let now = System.currentTimeMillis() for identifier in identifiers { - notificationManager.cancel(identifier.hashValue) - ids.remove(identifier) - editor.remove("date_" + identifier) + let triggerTime = preferences.getLong("date_" + identifier, 0) + if triggerTime < now { + notificationManager.cancel(identifier.hashValue) + ids.remove(identifier) + editor.remove("date_" + identifier) + } } + // Update the notification identifiers in the shared preferences. editor.putStringSet("ids", ids) editor.apply() #else @@ -299,7 +260,7 @@ public final class UNUserNotificationCenter { #if SKIP private func getAllNotificationRequests() -> [(id: String, timestamp: Long)] { guard let activity = UIApplication.shared.androidActivity else { return [] } - let preferences = activity.getSharedPreferences("alarms", Context.MODE_PRIVATE) + let preferences = activity.getSharedPreferences("notifications", Context.MODE_PRIVATE) let ids = preferences.getStringSet("ids", nil) ?? java.util.HashSet() var all: [(id: String, timestamp: Long)] = [] @@ -309,6 +270,7 @@ public final class UNUserNotificationCenter { let time = preferences.getLong("date_" + id, 0) all.append((id: id, timestamp: time)) } + return all } @@ -847,3 +809,44 @@ public class UNNotificationSettings : NSObject { } #endif + +#if SKIP + +public class NotificationWorker : Worker { + + public init(context: Context, params: WorkerParameters) { + super.init(context, params) + } + + public override func doWork() -> ListenableWorker.Result { + let title = getInputData().getString("title") ?? "" + let body = getInputData().getString("body") ?? "" + let notificationId = getInputData().getInt("id", 0) + let ctx = getApplicationContext() + + let channelID = "tools.skip.firebase.messaging" + let manager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as! NotificationManager + + if android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O { + let appName = ctx.getApplicationInfo().loadLabel(ctx.getPackageManager()).toString() + let channel = NotificationChannel(channelID, appName, NotificationManager.IMPORTANCE_DEFAULT) + manager.createNotificationChannel(channel) + } + + var resId = ctx.getResources().getIdentifier("ic_notification", "drawable", ctx.getPackageName()) + if resId == 0 { + resId = ctx.getResources().getIdentifier("ic_launcher", "mipmap", ctx.getPackageName()) + } + + let builder = NotificationCompat.Builder(ctx, channelID) + .setContentTitle(title) + .setContentText(body) + .setSmallIcon(resId) + .setAutoCancel(true) + + manager.notify(notificationId, builder.build()) + + return ListenableWorker.Result.success() + } +} +#endif From 98e0e630486a5a849af769c8951816b2e3baef2c Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Sat, 21 Feb 2026 14:18:51 +0100 Subject: [PATCH 07/12] Update UserNotifications.swift --- .../SkipUI/UIKit/UserNotifications.swift | 106 +++++++++++++----- 1 file changed, 77 insertions(+), 29 deletions(-) diff --git a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift index a2d01428..4cfea19c 100644 --- a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift +++ b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift @@ -113,8 +113,24 @@ public final class UNUserNotificationCenter { .putString("title", request.content.title) .putString("body", request.content.body) .putInt("id", request.identifier.hashValue) + + if let imageAttachment = request.content.attachments.first(where: { $0.type == "public.image" }) { + dataBuilder.putString("image_url", imageAttachment.url.absoluteString) + } + for (key, value) in request.content.userInfo { - dataBuilder.putString(key.toString(), value.toString()) + let k = key.toString() + if let s = value as? String { + dataBuilder.putString(k, s) + } else if let b = value as? Bool { + dataBuilder.putBoolean(k, b) + } else if let i = value as? Int { + dataBuilder.putInt(k, i) + } else if let d = value as? Double { + dataBuilder.putDouble(k, d) + } else { + dataBuilder.putString(k, value.toString()) + } } // Get the next trigger date (if any). @@ -216,7 +232,7 @@ public final class UNUserNotificationCenter { let ids = java.util.HashSet(preferences.getStringSet("ids", java.util.HashSet()) ?? java.util.HashSet()) // Get the notification manager. - let notificationManager = activity.getSystemService(android.content.Context.NOTIFICATION_SERVICE) as! android.app.NotificationManager + let notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as! NotificationManager // Cancel all delivered notifications using the notification manager. let now = System.currentTimeMillis() @@ -808,45 +824,77 @@ public class UNNotificationSettings : NSObject { } } -#endif - #if SKIP - public class NotificationWorker : Worker { - public init(context: Context, params: WorkerParameters) { super.init(context, params) } - + public override func doWork() -> ListenableWorker.Result { - let title = getInputData().getString("title") ?? "" - let body = getInputData().getString("body") ?? "" - let notificationId = getInputData().getInt("id", 0) - let ctx = getApplicationContext() - - let channelID = "tools.skip.firebase.messaging" - let manager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as! NotificationManager - - if android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O { - let appName = ctx.getApplicationInfo().loadLabel(ctx.getPackageManager()).toString() - let channel = NotificationChannel(channelID, appName, NotificationManager.IMPORTANCE_DEFAULT) - manager.createNotificationChannel(channel) + // Get the data which should be displayed in the notification. + let inputData = getInputData() + let id = inputData.getInt("id", 0) + let title = inputData.getString("title") ?? "" + let body = inputData.getString("body") ?? "" + let imageAttachmentUrl = inputData.getString("image_url") + + let context = getApplicationContext() + let intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()) + intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP) + + let bundle = android.os.Bundle() + let allData = inputData.keyValueMap + for (key, value) in allData where key != "title" && key != "body" && key != "id" && key != "image_url" { + if let s = value as? String { + bundle.putString(key, s) + } else if let b = value as? Bool { + bundle.putBoolean(key, b) + } else if let i = value as? Int { + bundle.putInt(key, i) + } else if let d = value as? Double { + bundle.putDouble(key, d) + } else { + bundle.putString(key, value.toString()) + } } - - var resId = ctx.getResources().getIdentifier("ic_notification", "drawable", ctx.getPackageName()) - if resId == 0 { - resId = ctx.getResources().getIdentifier("ic_launcher", "mipmap", ctx.getPackageName()) + intent?.putExtras(bundle) + + // Create the notification channel. + let channelID = "tools.skip.firebase.messaging" // Match AndroidManifest.xml + let manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as! NotificationManager + if Build.VERSION.SDK_INT >= Build.VERSION_CODES.O { + let appName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString() + manager.createNotificationChannel(NotificationChannel(channelID, appName, NotificationManager.IMPORTANCE_DEFAULT)) } - - let builder = NotificationCompat.Builder(ctx, channelID) + + // Build the notification. + let pendingIntent = PendingIntent.getActivity(context, id, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT) + let builder = NotificationCompat.Builder(context, channelID) .setContentTitle(title) .setContentText(body) - .setSmallIcon(resId) .setAutoCancel(true) - - manager.notify(notificationId, builder.build()) - + .setContentIntent(pendingIntent) + + // Update the notification icon. + if let url = imageAttachmentUrl { + builder.setSmallIcon(IconCompat.createWithContentUri(url)) + } else { + // Notification icon: must be a resource with transparent background and white logo + // eg: to be used as a default icon must be added in the AndroidManifest.xml with the following code: + // + 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)) + } + + // Display the notification. + manager.notify(id, builder.build()) return ListenableWorker.Result.success() } } #endif +#endif From 6244d684553a463a6e8d70c0fbe9331595065de5 Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Sat, 21 Feb 2026 14:20:11 +0100 Subject: [PATCH 08/12] Update UserNotifications.swift --- Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift index 4cfea19c..4cc66648 100644 --- a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift +++ b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift @@ -119,17 +119,16 @@ public final class UNUserNotificationCenter { } for (key, value) in request.content.userInfo { - let k = key.toString() if let s = value as? String { - dataBuilder.putString(k, s) + dataBuilder.putString(key.toString(), s) } else if let b = value as? Bool { - dataBuilder.putBoolean(k, b) + dataBuilder.putBoolean(key.toString(), b) } else if let i = value as? Int { - dataBuilder.putInt(k, i) + dataBuilder.putInt(key.toString(), i) } else if let d = value as? Double { - dataBuilder.putDouble(k, d) + dataBuilder.putDouble(key.toString(), d) } else { - dataBuilder.putString(k, value.toString()) + dataBuilder.putString(key.toString(), value.toString()) } } From 5cdc4f31876e95c00d90155cecb20d45374fd8f6 Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:32:11 +0100 Subject: [PATCH 09/12] Update UserNotifications.swift --- .../SkipUI/UIKit/UserNotifications.swift | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift index 4cc66648..b87fbca0 100644 --- a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift +++ b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift @@ -25,6 +25,7 @@ import androidx.work.Data import androidx.work.Worker import androidx.work.WorkerParameters import androidx.work.ListenableWorker +import java.util.HashSet import java.util.concurrent.TimeUnit import kotlin.random.Random #endif @@ -137,6 +138,9 @@ public final class UNUserNotificationCenter { (request.trigger as? UNTimeIntervalNotificationTrigger)?.nextTriggerDate() let delayMillis = nextDate != nil ? max(0, nextDate!.currentTimeMillis - System.currentTimeMillis()) : 0 + // Get the work manager. + let workManager = WorkManager.getInstance(activity) + // Add the notification work request to the work manager. let workData = dataBuilder.build() let workRequest = OneTimeWorkRequestBuilder() @@ -144,14 +148,14 @@ public final class UNUserNotificationCenter { .setInputData(workData) .addTag(request.identifier) .build() - WorkManager.getInstance(activity).enqueue(workRequest) + workManager.enqueue(workRequest) // Add the notification identifier to the shared preferences to be able to cancel it later. if let triggerMillis = nextDate?.currentTimeMillis { - let prefs = activity.getSharedPreferences("notifications", Context.MODE_PRIVATE) - let ids = java.util.HashSet(prefs.getStringSet("ids", java.util.HashSet()) ?? java.util.HashSet()) + let preferences = activity.getSharedPreferences("notifications", Context.MODE_PRIVATE) + let ids = HashSet(preferences.getStringSet("ids", HashSet()) ?? HashSet()) ids.add(request.identifier) - prefs.edit() + preferences.edit() .putStringSet("ids", ids) .putLong("date_" + request.identifier, triggerMillis) .apply() @@ -176,7 +180,7 @@ public final class UNUserNotificationCenter { // Get all notification identifiers from the shared preferences. let preferences = activity.getSharedPreferences("notifications", Context.MODE_PRIVATE) let editor = preferences.edit() - let ids = java.util.HashSet(preferences.getStringSet("ids", java.util.HashSet()) ?? java.util.HashSet()) + let ids = HashSet(preferences.getStringSet("ids", HashSet()) ?? HashSet()) // Get the work manager. let workManager = WorkManager.getInstance(activity) @@ -228,7 +232,7 @@ public final class UNUserNotificationCenter { // Get all notification identifiers from the shared preferences. let preferences = activity.getSharedPreferences("notifications", Context.MODE_PRIVATE) let editor = preferences.edit() - let ids = java.util.HashSet(preferences.getStringSet("ids", java.util.HashSet()) ?? java.util.HashSet()) + let ids = HashSet(preferences.getStringSet("ids", HashSet()) ?? HashSet()) // Get the notification manager. let notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as! NotificationManager @@ -860,10 +864,10 @@ public class NotificationWorker : Worker { // Create the notification channel. let channelID = "tools.skip.firebase.messaging" // Match AndroidManifest.xml - let manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as! NotificationManager + let notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as! NotificationManager if Build.VERSION.SDK_INT >= Build.VERSION_CODES.O { let appName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString() - manager.createNotificationChannel(NotificationChannel(channelID, appName, NotificationManager.IMPORTANCE_DEFAULT)) + notificationManager.createNotificationChannel(NotificationChannel(channelID, appName, NotificationManager.IMPORTANCE_DEFAULT)) } // Build the notification. @@ -891,7 +895,7 @@ public class NotificationWorker : Worker { } // Display the notification. - manager.notify(id, builder.build()) + notificationManager.notify(id, builder.build()) return ListenableWorker.Result.success() } } From 231fb7eeb516364ec2e21b8d87efc2fc0375d695 Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:37:56 +0100 Subject: [PATCH 10/12] Update Package.swift --- Package.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Package.swift b/Package.swift index bd985442..5428ed7a 100644 --- a/Package.swift +++ b/Package.swift @@ -9,10 +9,6 @@ let package = Package( ], dependencies: [ .package(url: "https://source.skip.tools/skip.git", from: "1.6.21"), - - // TODO: Remove temporary reference to skip foundation after PR has been merged (see: https://github.com/skiptools/skip-foundation/pull/93) - .package(path: "/Users/fabian/Desktop/Develop/Contributions/skip-foundation"), - .package(url: "https://source.skip.tools/skip-model.git", from: "1.6.2"), ], targets: [ From 29d4fb40f2ef9f13163456d198a118177e7e4631 Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:38:10 +0100 Subject: [PATCH 11/12] Update UserNotifications.swift --- Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift index b87fbca0..66cadc53 100644 --- a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift +++ b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift @@ -19,12 +19,12 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner -import androidx.work.WorkManager -import androidx.work.OneTimeWorkRequestBuilder import androidx.work.Data import androidx.work.Worker +import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.ListenableWorker +import androidx.work.OneTimeWorkRequestBuilder import java.util.HashSet import java.util.concurrent.TimeUnit import kotlin.random.Random From 6479746ac331e16fd0d1d8d7231882a3637ec5d2 Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Sat, 7 Mar 2026 22:44:13 +0100 Subject: [PATCH 12/12] Code Review Comment --- Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift index 66cadc53..84e2bb56 100644 --- a/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift +++ b/Sources/SkipUI/SkipUI/UIKit/UserNotifications.swift @@ -152,7 +152,7 @@ public final class UNUserNotificationCenter { // Add the notification identifier to the shared preferences to be able to cancel it later. if let triggerMillis = nextDate?.currentTimeMillis { - let preferences = activity.getSharedPreferences("notifications", Context.MODE_PRIVATE) + let preferences = activity.getSharedPreferences("__skip_usernotifications", Context.MODE_PRIVATE) let ids = HashSet(preferences.getStringSet("ids", HashSet()) ?? HashSet()) ids.add(request.identifier) preferences.edit() @@ -178,7 +178,7 @@ public final class UNUserNotificationCenter { guard let activity = UIApplication.shared.androidActivity else { return } // Get all notification identifiers from the shared preferences. - let preferences = activity.getSharedPreferences("notifications", Context.MODE_PRIVATE) + let preferences = activity.getSharedPreferences("__skip_usernotifications", Context.MODE_PRIVATE) let editor = preferences.edit() let ids = HashSet(preferences.getStringSet("ids", HashSet()) ?? HashSet()) @@ -230,7 +230,7 @@ public final class UNUserNotificationCenter { guard let activity = UIApplication.shared.androidActivity else { return } // Get all notification identifiers from the shared preferences. - let preferences = activity.getSharedPreferences("notifications", Context.MODE_PRIVATE) + let preferences = activity.getSharedPreferences("__skip_usernotifications", Context.MODE_PRIVATE) let editor = preferences.edit() let ids = HashSet(preferences.getStringSet("ids", HashSet()) ?? HashSet()) @@ -279,7 +279,7 @@ public final class UNUserNotificationCenter { #if SKIP private func getAllNotificationRequests() -> [(id: String, timestamp: Long)] { guard let activity = UIApplication.shared.androidActivity else { return [] } - let preferences = activity.getSharedPreferences("notifications", Context.MODE_PRIVATE) + let preferences = activity.getSharedPreferences("__skip_usernotifications", Context.MODE_PRIVATE) let ids = preferences.getStringSet("ids", nil) ?? java.util.HashSet() var all: [(id: String, timestamp: Long)] = []