diff --git a/features/alarm/src/iosMain/kotlin/com/escodro/alarm/di/PlatformAlarmModule.kt b/features/alarm/src/iosMain/kotlin/com/escodro/alarm/di/PlatformAlarmModule.kt index b6c7014f0..86612368b 100644 --- a/features/alarm/src/iosMain/kotlin/com/escodro/alarm/di/PlatformAlarmModule.kt +++ b/features/alarm/src/iosMain/kotlin/com/escodro/alarm/di/PlatformAlarmModule.kt @@ -2,12 +2,14 @@ package com.escodro.alarm.di import com.escodro.alarm.notification.IosNotificationScheduler import com.escodro.alarm.notification.IosTaskNotification +import com.escodro.alarm.notification.NotificationActionDelegate import com.escodro.alarm.notification.NotificationScheduler import com.escodro.alarm.notification.TaskNotification import com.escodro.alarm.permission.IosAlarmPermission import com.escodro.alarmapi.AlarmPermission import org.koin.core.module.Module import org.koin.core.module.dsl.factoryOf +import org.koin.core.module.dsl.singleOf import org.koin.dsl.bind import org.koin.dsl.module @@ -16,4 +18,6 @@ actual val platformAlarmModule: Module = module { factoryOf(::IosTaskNotification) bind TaskNotification::class factoryOf(::IosAlarmPermission) bind AlarmPermission::class + + singleOf(::NotificationActionDelegate) } diff --git a/features/alarm/src/iosMain/kotlin/com/escodro/alarm/notification/IosNotificationScheduler.kt b/features/alarm/src/iosMain/kotlin/com/escodro/alarm/notification/IosNotificationScheduler.kt index de1289c94..6d60fa283 100644 --- a/features/alarm/src/iosMain/kotlin/com/escodro/alarm/notification/IosNotificationScheduler.kt +++ b/features/alarm/src/iosMain/kotlin/com/escodro/alarm/notification/IosNotificationScheduler.kt @@ -1,6 +1,9 @@ package com.escodro.alarm.notification import com.escodro.alarm.model.Task +import com.escodro.resources.MR +import dev.icerock.moko.resources.desc.Resource +import dev.icerock.moko.resources.desc.StringDesc import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import platform.Foundation.NSCalendar @@ -14,14 +17,42 @@ import platform.Foundation.NSLog import platform.Foundation.dateWithTimeIntervalSince1970 import platform.UserNotifications.UNCalendarNotificationTrigger import platform.UserNotifications.UNMutableNotificationContent +import platform.UserNotifications.UNNotificationAction +import platform.UserNotifications.UNNotificationActionOptionNone +import platform.UserNotifications.UNNotificationCategory import platform.UserNotifications.UNNotificationRequest import platform.UserNotifications.UNUserNotificationCenter internal class IosNotificationScheduler : NotificationScheduler { + private val notificationCenter = UNUserNotificationCenter.currentNotificationCenter() + + init { + registerCategories() + } + + private fun registerCategories() { + val doneAction = UNNotificationAction.actionWithIdentifier( + identifier = ACTION_IDENTIFIER_DONE, + title = StringDesc.Resource(MR.strings.notification_action_completed).localized(), + options = UNNotificationActionOptionNone, + ) + + val category = UNNotificationCategory.categoryWithIdentifier( + identifier = CATEGORY_IDENTIFIER_TASK, + actions = listOf(doneAction), + intentIdentifiers = emptyList(), + options = UNNotificationActionOptionNone, + ) + + notificationCenter.setNotificationCategories(setOf(category)) + } + override fun scheduleTaskNotification(task: Task, timeInMillis: Long) { val content = UNMutableNotificationContent() content.setBody(task.title) + content.setCategoryIdentifier(CATEGORY_IDENTIFIER_TASK) + content.setUserInfo(mapOf(USER_INFO_TASK_ID to task.id)) val nsDate = NSDate.dateWithTimeIntervalSince1970(timeInMillis / 1000.0) val dateComponents = NSCalendar.currentCalendar.components( @@ -36,7 +67,7 @@ internal class IosNotificationScheduler : NotificationScheduler { ) val request = UNNotificationRequest.requestWithIdentifier( - task.id.toString(), + identifier = task.id.toString(), content = content, trigger = trigger, ) @@ -52,7 +83,6 @@ internal class IosNotificationScheduler : NotificationScheduler { override fun cancelTaskNotification(task: Task) { NSLog("Canceling notification with id '${task.title}'") - val notificationCenter = UNUserNotificationCenter.currentNotificationCenter() notificationCenter.removePendingNotificationRequestsWithIdentifiers(listOf(task.id.toString())) } @@ -64,4 +94,22 @@ internal class IosNotificationScheduler : NotificationScheduler { scheduleTaskNotification(task, time) } + + companion object { + + /** + * Identifier for the task category actions. + */ + const val CATEGORY_IDENTIFIER_TASK = "task_actions" + + /** + * Identifier for the done action. + */ + const val ACTION_IDENTIFIER_DONE = "done_action" + + /** + * Key to store the task id in the notification content. + */ + const val USER_INFO_TASK_ID = "task_id" + } } diff --git a/features/alarm/src/iosMain/kotlin/com/escodro/alarm/notification/NotificationActionDelegate.kt b/features/alarm/src/iosMain/kotlin/com/escodro/alarm/notification/NotificationActionDelegate.kt new file mode 100644 index 000000000..b4a491652 --- /dev/null +++ b/features/alarm/src/iosMain/kotlin/com/escodro/alarm/notification/NotificationActionDelegate.kt @@ -0,0 +1,49 @@ +package com.escodro.alarm.notification + +import com.escodro.coroutines.AppCoroutineScope +import com.escodro.domain.usecase.task.CompleteTask +import platform.Foundation.NSLog +import platform.UserNotifications.UNNotificationResponse + +/** + * Class to handle the notification actions in iOS. + * + * @param appCoroutineScope the app-wide coroutine scope + * @param completeTaskUseCase the use case to complete a task + */ +class NotificationActionDelegate( + private val appCoroutineScope: AppCoroutineScope, + private val completeTaskUseCase: CompleteTask, +) { + + /** + * Handles the notification response and actions. + * + * @param response the notification response + * @param onCompletion the action to be executed after the task is completed + */ + fun userNotificationCenter(response: UNNotificationResponse, onCompletion: () -> Unit) { + NSLog("NotificationActionDelegate - userNotificationCenter") + val content = response.notification.request.content + NSLog("NotificationActionDelegate - content: $content") + val taskId: Long = content.userInfo[IosNotificationScheduler.USER_INFO_TASK_ID] as? Long + ?: return + + when (response.actionIdentifier) { + IosNotificationScheduler.ACTION_IDENTIFIER_DONE -> completeTask( + taskId = taskId, + onCompletion = onCompletion, + ) + + else -> NSLog("NotificationActionDelegate - Action not supported") + } + } + + private fun completeTask(taskId: Long, onCompletion: () -> Unit) { + appCoroutineScope.launch { + NSLog("NotificationActionDelegate - completeTask") + completeTaskUseCase(taskId) + onCompletion() + } + } +} diff --git a/ios-app/alkaa/AppDelegate.swift b/ios-app/alkaa/AppDelegate.swift index d83518aae..54a0dc5a7 100644 --- a/ios-app/alkaa/AppDelegate.swift +++ b/ios-app/alkaa/AppDelegate.swift @@ -7,6 +7,7 @@ import Foundation import UIKit +import shared class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { @@ -19,4 +20,17 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele { completionHandler([.banner, .list, .badge, .sound]) } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: + @escaping () -> Void) { + + let delegate = InjectionHelper().notificationActionDelegate + delegate.userNotificationCenter(response: response) { + // Call the completion handler after the action handling is finished + // Otherwise the Coroutine will be cancelled if the app is not running in background + completionHandler() + } + } } diff --git a/shared/src/iosMain/kotlin/com/escodro/shared/di/InjectionHelper.kt b/shared/src/iosMain/kotlin/com/escodro/shared/di/InjectionHelper.kt new file mode 100644 index 000000000..06ea57e62 --- /dev/null +++ b/shared/src/iosMain/kotlin/com/escodro/shared/di/InjectionHelper.kt @@ -0,0 +1,16 @@ +package com.escodro.shared.di + +import com.escodro.alarm.notification.NotificationActionDelegate +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +/** + * Helper class to inject dependencies in the iOS platform. + */ +class InjectionHelper : KoinComponent { + + /** + * Provides the [NotificationActionDelegate] instance. + */ + val notificationActionDelegate: NotificationActionDelegate by inject() +}