Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package net.thunderbird.feature.notification.api

import net.thunderbird.feature.notification.api.content.Notification

/**
* A registry for managing notifications and their corresponding IDs.
*
* It establishes and maintains the correlation between a [Notification] object
* and its unique [NotificationId].
* This can also be used to track which notifications are currently being displayed
* to the user.
*/
interface NotificationRegistry {
/**
* A [Map] off all the current notifications, associated with their IDs,
* being displayed to the user.
*/
val registrar: Map<NotificationId, Notification>

/**
* Retrieves a [Notification] object based on its [notificationId].
*
* @param notificationId The ID of the notification to retrieve.
* @return The [Notification] object associated with the given [notificationId],
* or `null` if no such notification exists.
*/
operator fun get(notificationId: NotificationId): Notification?

/**
* Retrieves the [NotificationId] associated with the given [notification].
*
* @param notification The notification for which to retrieve the ID.
* @return The [NotificationId] if the notification is registered, or `null` otherwise.
*/
operator fun get(notification: Notification): NotificationId?

/**
* Registers a notification and returns its unique ID.
*
* If the provided [notification] is already registered, this function will effectively
* return its known [NotificationId].
*
* @param notification The [Notification] object to register.
* @return The unique [NotificationId] assigned to the registered notification.
*/
suspend fun register(notification: Notification): NotificationId

/**
* Unregisters a [Notification] by its [NotificationId].
*
* @param notificationId The ID of the notification to unregister.
*/
fun unregister(notificationId: NotificationId)

/**
* Unregisters a previously registered notification.
*
* @param notification The [Notification] object to unregister.
*/
fun unregister(notification: Notification)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ package net.thunderbird.feature.notification.api
* For [Temporary] and [Warning], user action might be recommended or optional.
* For [Information], no user action is usually needed.
*/
enum class NotificationSeverity {
enum class NotificationSeverity(val dismissable: Boolean) {
/**
* Completely blocks the user from performing essential tasks or accessing core functionality.
*
Expand All @@ -24,7 +24,7 @@ enum class NotificationSeverity {
* - Retry
* - Provide other credentials
*/
Fatal,
Fatal(dismissable = false),

/**
* Prevents the user from completing specific core actions or causes significant disruption to functionality.
Expand All @@ -36,7 +36,7 @@ enum class NotificationSeverity {
* - **Notification Actions:**
* - Retry
*/
Critical,
Critical(dismissable = false),

/**
* Causes a temporary disruption or delay to functionality, which may resolve on its own.
Expand All @@ -48,7 +48,7 @@ enum class NotificationSeverity {
* - **Notification Message:** You are offline, the message will be sent later.
* - **Notification Actions:** N/A
*/
Temporary,
Temporary(dismissable = true),

/**
* Alerts the user to a potential issue or limitation that may affect functionality if not addressed.
Expand All @@ -61,7 +61,7 @@ enum class NotificationSeverity {
* - **Notification Actions:**
* - Manage Storage
*/
Warning,
Warning(dismissable = true),

/**
* Provides status or context without impacting functionality or requiring action.
Expand All @@ -72,5 +72,5 @@ enum class NotificationSeverity {
* - **Notification Message:** Last time email synchronization succeeded
* - **Notification Actions:** N/A
*/
Information,
Information(dismissable = true),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.thunderbird.feature.notification.api.command

class NotificationCommandException @JvmOverloads constructor(
override val message: String?,
override val cause: Throwable? = null,
) : Exception(message, cause)
2 changes: 2 additions & 0 deletions feature/notification/impl/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
plugins {
id(ThunderbirdPlugins.Library.kmpCompose)
alias(libs.plugins.dev.mokkery)
}

kotlin {
sourceSets {
commonMain.dependencies {
implementation(projects.core.common)
implementation(projects.core.featureflag)
implementation(projects.core.outcome)
implementation(projects.core.logging.api)
implementation(projects.feature.notification.api)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

</manifest>
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
package net.thunderbird.feature.notification.impl.inject

import net.thunderbird.feature.notification.api.content.SystemNotification
import net.thunderbird.feature.notification.api.receiver.NotificationNotifier
import net.thunderbird.feature.notification.impl.intent.action.AlarmPermissionMissingNotificationTapActionIntentCreator
import net.thunderbird.feature.notification.impl.intent.action.DefaultNotificationActionIntentCreator
import net.thunderbird.feature.notification.impl.intent.action.NotificationActionIntentCreator
import net.thunderbird.feature.notification.impl.receiver.AndroidSystemNotificationNotifier
import net.thunderbird.feature.notification.impl.receiver.SystemNotificationNotifier
import net.thunderbird.feature.notification.impl.ui.action.DefaultSystemNotificationActionCreator
import net.thunderbird.feature.notification.impl.ui.action.NotificationActionCreator
import org.koin.android.ext.koin.androidApplication
import org.koin.core.module.Module
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.koin.dsl.onClose

internal actual val platformFeatureNotificationModule: Module = module {
single<List<NotificationActionIntentCreator<*>>>(named<NotificationActionIntentCreator.TypeQualifier>()) {
single<List<NotificationActionIntentCreator<*, *>>>(named<NotificationActionIntentCreator.TypeQualifier>()) {
listOf(
AlarmPermissionMissingNotificationTapActionIntentCreator(
context = androidApplication(),
logger = get(),
),
// The Default implementation must always be the last.
DefaultNotificationActionIntentCreator(
logger = get(),
applicationContext = androidApplication(),
Expand All @@ -26,4 +36,14 @@ internal actual val platformFeatureNotificationModule: Module = module {
actionIntentCreators = get(named<NotificationActionIntentCreator.TypeQualifier>()),
)
}

single<NotificationNotifier<SystemNotification>>(named<SystemNotificationNotifier>()) {
AndroidSystemNotificationNotifier(
logger = get(),
applicationContext = androidApplication(),
notificationActionCreator = get(named(NotificationActionCreator.TypeQualifier.System)),
)
}.onClose { notifier ->
notifier?.dispose()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package net.thunderbird.feature.notification.impl.intent.action

import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.core.app.PendingIntentCompat
import androidx.core.net.toUri
import net.thunderbird.core.logging.Logger
import net.thunderbird.feature.notification.api.content.Notification
import net.thunderbird.feature.notification.api.content.PushServiceNotification
import net.thunderbird.feature.notification.api.ui.action.NotificationAction

private const val TAG = "AlarmPermissionMissingNotificationIntentCreator"

class AlarmPermissionMissingNotificationTapActionIntentCreator(
private val context: Context,
private val logger: Logger,
) : NotificationActionIntentCreator<PushServiceNotification.AlarmPermissionMissing, NotificationAction.Tap> {
override fun accept(notification: Notification, action: NotificationAction): Boolean =
Build.VERSION.SDK_INT > Build.VERSION_CODES.S &&
notification is PushServiceNotification.AlarmPermissionMissing

@RequiresApi(Build.VERSION_CODES.S)
override fun create(
notification: PushServiceNotification.AlarmPermissionMissing,
action: NotificationAction.Tap,
): PendingIntent {
logger.debug(TAG) { "create() called with: notification = $notification" }
val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
data = "package:${context.packageName}".toUri()
}

return requireNotNull(
PendingIntentCompat.getActivity(
/* context = */
context,
/* requestCode = */
1,
/* intent = */
intent,
/* flags = */
0,
/* isMutable = */
false,
),
) {
"Could not create PendingIntent for AlarmPermissionMissing Notification."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.PendingIntent
import android.content.Context
import androidx.core.app.PendingIntentCompat
import net.thunderbird.core.logging.Logger
import net.thunderbird.feature.notification.api.content.Notification
import net.thunderbird.feature.notification.api.ui.action.NotificationAction

private const val TAG = "DefaultNotificationActionIntentCreator"
Expand All @@ -21,11 +22,11 @@ private const val TAG = "DefaultNotificationActionIntentCreator"
internal class DefaultNotificationActionIntentCreator(
private val logger: Logger,
private val applicationContext: Context,
) : NotificationActionIntentCreator<NotificationAction> {
override fun accept(action: NotificationAction): Boolean = true
) : NotificationActionIntentCreator<Notification, NotificationAction> {
override fun accept(notification: Notification, action: NotificationAction): Boolean = true

override fun create(action: NotificationAction): PendingIntent? {
logger.debug(TAG) { "create() called with: action = $action" }
override fun create(notification: Notification, action: NotificationAction): PendingIntent? {
logger.debug(TAG) { "create() called with: notification = $notification, action = $action" }
val packageManager = applicationContext.packageManager
val launchIntent = requireNotNull(
packageManager.getLaunchIntentForPackage(applicationContext.packageName),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.thunderbird.feature.notification.impl.intent.action

import android.app.PendingIntent
import net.thunderbird.feature.notification.api.content.Notification
import net.thunderbird.feature.notification.api.ui.action.NotificationAction

/**
Expand All @@ -11,22 +12,25 @@ import net.thunderbird.feature.notification.api.ui.action.NotificationAction
*
* @param TNotificationAction The type of [NotificationAction] this creator can handle.
*/
internal interface NotificationActionIntentCreator<in TNotificationAction : NotificationAction> {
internal interface NotificationActionIntentCreator<
in TNotification : Notification,
in TNotificationAction : NotificationAction,
> {
/**
* Determines whether this [NotificationActionIntentCreator] can create an intent for the given [action].
*
* @param action The [NotificationAction] to check.
* @return `true` if this creator can handle the [action], `false` otherwise.
*/
fun accept(action: NotificationAction): Boolean
fun accept(notification: Notification, action: NotificationAction): Boolean

/**
* Creates a [PendingIntent] for the given notification action.
*
* @param action The notification action to create an intent for.
* @return The created [PendingIntent], or `null` if the action is not supported or an error occurs.
*/
fun create(action: TNotificationAction): PendingIntent?
fun create(notification: TNotification, action: TNotificationAction): PendingIntent?

object TypeQualifier
}
Loading