diff --git a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.android.kt b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.android.kt new file mode 100644 index 00000000000..bf54d97db7c --- /dev/null +++ b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.android.kt @@ -0,0 +1,44 @@ +package net.thunderbird.feature.notification.api.ui.action.icon + +import net.thunderbird.feature.notification.api.R +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon + +internal actual val NotificationActionIcons.Reply: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_reply, + ) + +internal actual val NotificationActionIcons.MarkAsRead: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_mark_email_read, + ) + +internal actual val NotificationActionIcons.Delete: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_delete, + ) + +internal actual val NotificationActionIcons.MarkAsSpam: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_report, + ) + +internal actual val NotificationActionIcons.Archive: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_archive, + ) + +internal actual val NotificationActionIcons.UpdateServerSettings: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_settings, + ) + +internal actual val NotificationActionIcons.Retry: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_refresh, + ) + +internal actual val NotificationActionIcons.DisablePushAction: NotificationIcon + get() = NotificationIcon( + systemNotificationIcon = R.drawable.ic_settings, + ) diff --git a/feature/notification/api/src/commonMain/composeResources/values/strings.xml b/feature/notification/api/src/commonMain/composeResources/values/strings.xml index 2320de026e7..07881ac7ee9 100644 --- a/feature/notification/api/src/commonMain/composeResources/values/strings.xml +++ b/feature/notification/api/src/commonMain/composeResources/values/strings.xml @@ -45,4 +45,15 @@ Disable Push + Reply + Mark Read + Mark All Read + Delete + Delete All + Archive + Archive All + Spam + Retry + Update Server Settings + diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/PushServiceNotification.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/PushServiceNotification.kt index d663fc29bd6..60196f54890 100644 --- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/PushServiceNotification.kt +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/content/PushServiceNotification.kt @@ -3,6 +3,8 @@ package net.thunderbird.feature.notification.api.content import net.thunderbird.feature.notification.api.NotificationChannel import net.thunderbird.feature.notification.api.NotificationSeverity import net.thunderbird.feature.notification.api.ui.action.NotificationAction +import net.thunderbird.feature.notification.api.ui.action.icon.DisablePushAction +import net.thunderbird.feature.notification.api.ui.action.icon.NotificationActionIcons import net.thunderbird.feature.notification.api.ui.icon.AlarmPermissionMissing import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon import net.thunderbird.feature.notification.api.ui.icon.NotificationIcons @@ -168,7 +170,9 @@ sealed class PushServiceNotification : AppNotification(), SystemNotification { * @return A set of [NotificationAction] instances. */ private suspend fun buildNotificationActions(): Set = setOf( + NotificationAction.Tap, NotificationAction.CustomAction( - message = getString(resource = Res.string.push_info_disable_push_action), + title = getString(resource = Res.string.push_info_disable_push_action), + icon = NotificationActionIcons.DisablePushAction, ), ) diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/NotificationAction.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/NotificationAction.kt index d1183716826..adb1836fc84 100644 --- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/NotificationAction.kt +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/NotificationAction.kt @@ -1,50 +1,125 @@ package net.thunderbird.feature.notification.api.ui.action +import net.thunderbird.feature.notification.api.content.SystemNotification +import net.thunderbird.feature.notification.api.ui.action.icon.Archive +import net.thunderbird.feature.notification.api.ui.action.icon.Delete +import net.thunderbird.feature.notification.api.ui.action.icon.MarkAsRead +import net.thunderbird.feature.notification.api.ui.action.icon.MarkAsSpam +import net.thunderbird.feature.notification.api.ui.action.icon.NotificationActionIcons +import net.thunderbird.feature.notification.api.ui.action.icon.Reply +import net.thunderbird.feature.notification.api.ui.action.icon.Retry +import net.thunderbird.feature.notification.api.ui.action.icon.UpdateServerSettings +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon +import net.thunderbird.feature.notification.resources.api.Res +import net.thunderbird.feature.notification.resources.api.notification_action_archive +import net.thunderbird.feature.notification.resources.api.notification_action_delete +import net.thunderbird.feature.notification.resources.api.notification_action_mark_as_read +import net.thunderbird.feature.notification.resources.api.notification_action_reply +import net.thunderbird.feature.notification.resources.api.notification_action_retry +import net.thunderbird.feature.notification.resources.api.notification_action_spam +import net.thunderbird.feature.notification.resources.api.notification_action_update_server_settings +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.getString + /** * Represents the various actions that can be performed on a notification. */ -sealed interface NotificationAction { +sealed class NotificationAction { + abstract val icon: NotificationIcon? + protected abstract val titleResource: StringResource? + + open suspend fun resolveTitle(): String? = titleResource?.let { getString(it) } + + /** + * Action to open the notification. This is the default action when a notification is tapped. + * + * This action typically does not have an icon or title displayed on the notification itself, + * as it's implied by tapping the notification content. + * + * All [SystemNotification] will have this action implicitly, even if not specified in the + * [SystemNotification.actions] set. + */ + data object Tap : NotificationAction() { + override val icon: NotificationIcon? = null + override val titleResource: StringResource? = null + } + /** * Action to reply to the email message associated with the notification. */ - data object Reply : NotificationAction + data object Reply : NotificationAction() { + override val icon: NotificationIcon = NotificationActionIcons.Reply + + override val titleResource: StringResource = Res.string.notification_action_reply + } /** * Action to mark the email message associated with the notification as read. */ - data object MarkAsRead : NotificationAction + data object MarkAsRead : NotificationAction() { + override val icon: NotificationIcon = NotificationActionIcons.MarkAsRead + + override val titleResource: StringResource = Res.string.notification_action_mark_as_read + } /** * Action to delete the email message associated with the notification. */ - data object Delete : NotificationAction + data object Delete : NotificationAction() { + override val icon: NotificationIcon = NotificationActionIcons.Delete + + override val titleResource: StringResource = Res.string.notification_action_delete + } /** * Action to mark the email message associated with the notification as spam. */ - data object MarkAsSpam : NotificationAction + data object MarkAsSpam : NotificationAction() { + override val icon: NotificationIcon = NotificationActionIcons.MarkAsSpam + + override val titleResource: StringResource = Res.string.notification_action_spam + } /** * Action to archive the email message associated with the notification. */ - data object Archive : NotificationAction + data object Archive : NotificationAction() { + override val icon: NotificationIcon = NotificationActionIcons.Archive + + override val titleResource: StringResource = Res.string.notification_action_archive + } /** * Action to prompt the user to update server settings, typically when authentication fails. */ - data object UpdateServerSettings : NotificationAction + data object UpdateServerSettings : NotificationAction() { + override val icon: NotificationIcon = NotificationActionIcons.UpdateServerSettings + + override val titleResource: StringResource = Res.string.notification_action_update_server_settings + } /** * Action to retry a failed operation, such as sending a message or fetching new messages. */ - data object Retry : NotificationAction + data object Retry : NotificationAction() { + override val icon: NotificationIcon = NotificationActionIcons.Retry + + override val titleResource: StringResource = Res.string.notification_action_retry + } /** * Represents a custom notification action. * * This can be used for actions that are not predefined and require a specific message. * - * @property message The text to be displayed for this custom action. + * @property title The text to be displayed for this custom action. */ - data class CustomAction(val message: String) : NotificationAction + data class CustomAction( + val title: String, + override val icon: NotificationIcon? = null, + ) : NotificationAction() { + override val titleResource: StringResource get() = error("Custom Action must not supply a title resource") + + override suspend fun resolveTitle(): String = title + } } diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.kt new file mode 100644 index 00000000000..259d37d6bc8 --- /dev/null +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.kt @@ -0,0 +1,14 @@ +package net.thunderbird.feature.notification.api.ui.action.icon + +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon + +internal object NotificationActionIcons + +internal expect val NotificationActionIcons.Reply: NotificationIcon +internal expect val NotificationActionIcons.MarkAsRead: NotificationIcon +internal expect val NotificationActionIcons.Delete: NotificationIcon +internal expect val NotificationActionIcons.MarkAsSpam: NotificationIcon +internal expect val NotificationActionIcons.Archive: NotificationIcon +internal expect val NotificationActionIcons.UpdateServerSettings: NotificationIcon +internal expect val NotificationActionIcons.Retry: NotificationIcon +internal expect val NotificationActionIcons.DisablePushAction: NotificationIcon diff --git a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcon.kt b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcon.kt index 6280242d575..a53d234f841 100644 --- a/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcon.kt +++ b/feature/notification/api/src/commonMain/kotlin/net/thunderbird/feature/notification/api/ui/icon/NotificationIcon.kt @@ -12,8 +12,8 @@ import androidx.compose.ui.graphics.vector.ImageVector * @property inAppNotificationIcon The icon to be used for in-app notifications. */ data class NotificationIcon( - private val systemNotificationIcon: SystemNotificationIcon? = null, - private val inAppNotificationIcon: ImageVector? = null, + val systemNotificationIcon: SystemNotificationIcon? = null, + val inAppNotificationIcon: ImageVector? = null, ) { init { diff --git a/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.jvm.kt b/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.jvm.kt new file mode 100644 index 00000000000..821368370a2 --- /dev/null +++ b/feature/notification/api/src/jvmMain/kotlin/net/thunderbird/feature/notification/api/ui/action/icon/NotificationActionIcons.jvm.kt @@ -0,0 +1,14 @@ +package net.thunderbird.feature.notification.api.ui.action.icon + +import net.thunderbird.feature.notification.api.ui.icon.NotificationIcon + +private const val ERROR_MESSAGE = "Can't send notifications from a jvm library. Use android library or app instead." + +internal actual val NotificationActionIcons.Reply: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationActionIcons.MarkAsRead: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationActionIcons.Delete: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationActionIcons.MarkAsSpam: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationActionIcons.Archive: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationActionIcons.UpdateServerSettings: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationActionIcons.Retry: NotificationIcon get() = error(ERROR_MESSAGE) +internal actual val NotificationActionIcons.DisablePushAction: NotificationIcon get() = error(ERROR_MESSAGE) diff --git a/feature/notification/impl/build.gradle.kts b/feature/notification/impl/build.gradle.kts index 3ef9207f1ac..bac4e026572 100644 --- a/feature/notification/impl/build.gradle.kts +++ b/feature/notification/impl/build.gradle.kts @@ -10,6 +10,15 @@ kotlin { implementation(projects.core.logging.api) implementation(projects.feature.notification.api) } + commonTest.dependencies { + implementation(projects.core.logging.testing) + } + androidUnitTest.dependencies { + implementation(libs.androidx.test.core) + implementation(libs.mockito.core) + implementation(libs.mockito.kotlin) + implementation(libs.robolectric) + } } } diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.android.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.android.kt new file mode 100644 index 00000000000..cef362deedf --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.android.kt @@ -0,0 +1,29 @@ +package net.thunderbird.feature.notification.impl.inject + +import net.thunderbird.feature.notification.api.content.SystemNotification +import net.thunderbird.feature.notification.impl.intent.action.DefaultNotificationActionIntentCreator +import net.thunderbird.feature.notification.impl.intent.action.NotificationActionIntentCreator +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 + +internal actual val platformFeatureNotificationModule: Module = module { + single>>(named()) { + listOf( + DefaultNotificationActionIntentCreator( + logger = get(), + applicationContext = androidApplication(), + ), + ) + } + + single>(named(NotificationActionCreator.TypeQualifier.System)) { + DefaultSystemNotificationActionCreator( + logger = get(), + actionIntentCreators = get(named()), + ) + } +} diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/DefaultNotificationActionIntentCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/DefaultNotificationActionIntentCreator.kt new file mode 100644 index 00000000000..ebabf08ff48 --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/DefaultNotificationActionIntentCreator.kt @@ -0,0 +1,49 @@ +package net.thunderbird.feature.notification.impl.intent.action + +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.ui.action.NotificationAction + +private const val TAG = "DefaultNotificationActionIntentCreator" + +/** + * A default implementation of [NotificationActionIntentCreator] that creates a [PendingIntent] + * to launch the application when a notification action is triggered. + * + * This creator accepts any [NotificationAction] and always attempts to create a launch intent + * for the current application. + * + * @property logger The logger instance for logging debug messages. + * @property applicationContext The application context used to access system services like PackageManager. + */ +internal class DefaultNotificationActionIntentCreator( + private val logger: Logger, + private val applicationContext: Context, +) : NotificationActionIntentCreator { + override fun accept(action: NotificationAction): Boolean = true + + override fun create(action: NotificationAction): PendingIntent? { + logger.debug(TAG) { "create() called with: action = $action" } + val packageManager = applicationContext.packageManager + val launchIntent = requireNotNull( + packageManager.getLaunchIntentForPackage(applicationContext.packageName), + ) { + "Could not retrieve the launch intent from ${applicationContext.packageName}" + } + + return PendingIntentCompat.getActivity( + /* context = */ + applicationContext, + /* requestCode = */ + 1, + /* intent = */ + launchIntent, + /* flags = */ + 0, + /* isMutable = */ + false, + ) + } +} diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/NotificationActionIntentCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/NotificationActionIntentCreator.kt new file mode 100644 index 00000000000..7c0299f2fe0 --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/intent/action/NotificationActionIntentCreator.kt @@ -0,0 +1,32 @@ +package net.thunderbird.feature.notification.impl.intent.action + +import android.app.PendingIntent +import net.thunderbird.feature.notification.api.ui.action.NotificationAction + +/** + * Interface for creating a [PendingIntent] for a given [NotificationAction]. + * + * This interface is used to decouple the creation of [PendingIntent]s from the notification creation logic. + * Implementations of this interface should be registered in the Koin graph using the [TypeQualifier]. + * + * @param TNotificationAction The type of [NotificationAction] this creator can handle. + */ +internal interface NotificationActionIntentCreator { + /** + * 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 + + /** + * 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? + + object TypeQualifier +} diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/AndroidNotificationAction.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/AndroidNotificationAction.kt new file mode 100644 index 00000000000..95d9a2176ac --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/AndroidNotificationAction.kt @@ -0,0 +1,18 @@ +package net.thunderbird.feature.notification.impl.ui.action + +import android.app.PendingIntent +import androidx.annotation.DrawableRes + +/** + * Represents an action that can be performed on an Android notification. + * + * @property icon The drawable resource ID for the action's icon. + * @property title The title of the action. + * @property pendingIntent The [PendingIntent] to be executed when the action is triggered. + */ +data class AndroidNotificationAction( + @DrawableRes + val icon: Int?, + val title: String?, + val pendingIntent: PendingIntent?, +) diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/DefaultSystemNotificationActionCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/DefaultSystemNotificationActionCreator.kt new file mode 100644 index 00000000000..dc7395e13ae --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/DefaultSystemNotificationActionCreator.kt @@ -0,0 +1,29 @@ +package net.thunderbird.feature.notification.impl.ui.action + +import net.thunderbird.core.logging.Logger +import net.thunderbird.feature.notification.api.content.SystemNotification +import net.thunderbird.feature.notification.api.ui.action.NotificationAction +import net.thunderbird.feature.notification.impl.intent.action.NotificationActionIntentCreator + +private const val TAG = "DefaultSystemNotificationActionCreator" + +internal class DefaultSystemNotificationActionCreator( + private val logger: Logger, + private val actionIntentCreators: List>, +) : NotificationActionCreator { + override suspend fun create( + notification: SystemNotification, + action: NotificationAction, + ): AndroidNotificationAction { + logger.debug(TAG) { "create() called with: notification = $notification, action = $action" } + val intent = actionIntentCreators + .first { it.accept(action) } + .create(action) + + return AndroidNotificationAction( + icon = action.icon?.systemNotificationIcon, + title = action.resolveTitle(), + pendingIntent = intent, + ) + } +} diff --git a/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/NotificationActionCreator.kt b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/NotificationActionCreator.kt new file mode 100644 index 00000000000..db6cc1f7b20 --- /dev/null +++ b/feature/notification/impl/src/androidMain/kotlin/net/thunderbird/feature/notification/impl/ui/action/NotificationActionCreator.kt @@ -0,0 +1,25 @@ +package net.thunderbird.feature.notification.impl.ui.action + +import net.thunderbird.feature.notification.api.content.Notification +import net.thunderbird.feature.notification.api.ui.action.NotificationAction + +/** + * Interface responsible for creating Android-specific notification actions ([AndroidNotificationAction]) + * from generic notification actions ([NotificationAction]). + * + * This allows decoupling the core notification logic from the Android platform specifics. + * + * @param TNotification The type of [Notification] this creator can handle. + */ +interface NotificationActionCreator { + /** + * Creates an [AndroidNotificationAction] for the given [notification] and [action]. + * + * @param notification The notification to create the action for. + * @param action The action to create. + * @return The created [AndroidNotificationAction]. + */ + suspend fun create(notification: TNotification, action: NotificationAction): AndroidNotificationAction + + enum class TypeQualifier { System, InApp } +} diff --git a/feature/notification/impl/src/androidUnitTest/kotlin/net/thunderbird/feature/notification/impl/intent/action/DefaultNotificationActionIntentCreatorTest.kt b/feature/notification/impl/src/androidUnitTest/kotlin/net/thunderbird/feature/notification/impl/intent/action/DefaultNotificationActionIntentCreatorTest.kt new file mode 100644 index 00000000000..e45bb2da940 --- /dev/null +++ b/feature/notification/impl/src/androidUnitTest/kotlin/net/thunderbird/feature/notification/impl/intent/action/DefaultNotificationActionIntentCreatorTest.kt @@ -0,0 +1,132 @@ +package net.thunderbird.feature.notification.impl.intent.action + +import android.app.Application +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import androidx.core.app.PendingIntentCompat +import androidx.test.core.app.ApplicationProvider +import assertk.all +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isTrue +import assertk.assertions.prop +import net.thunderbird.core.logging.testing.TestLogger +import net.thunderbird.feature.notification.api.ui.action.NotificationAction +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mockStatic +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DefaultNotificationActionIntentCreatorTest { + private val application: Application = ApplicationProvider.getApplicationContext() + + @Test + fun `accept should return true for any type of notification action`() { + // Arrange + val multipleActions = listOf( + NotificationAction.Tap, + NotificationAction.Reply, + NotificationAction.MarkAsRead, + NotificationAction.Delete, + NotificationAction.MarkAsSpam, + NotificationAction.Archive, + NotificationAction.UpdateServerSettings, + NotificationAction.Retry, + NotificationAction.CustomAction(title = "Custom Action 1"), + NotificationAction.CustomAction(title = "Custom Action 2"), + NotificationAction.CustomAction(title = "Custom Action 3"), + ) + val testSubject = createTestSubject() + + // Act + val accepted = multipleActions.fold(initial = true) { accepted, action -> + accepted and testSubject.accept(action) + } + + // Assert + assertThat(accepted).isTrue() + } + + @Test + fun `create should return PendingIntent for any type of notification action`() { + // Arrange + mockStatic(PendingIntentCompat::class.java).use { pendingIntentCompat -> + // Arrange (cont.) + val expectedIntent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_LAUNCHER) + setPackage(application.packageName) + } + val mockedPackageManager = mock { + on { + getLaunchIntentForPackage(eq(application.packageName)) + } doReturn expectedIntent + } + val context = spy(application) { + on { packageManager } doReturn mockedPackageManager + } + + pendingIntentCompat + .`when` { + PendingIntentCompat.getActivity( + /* context = */ + any(), + /* requestCode = */ + any(), + /* intent = */ + any(), + /* flags = */ + any(), + /* isMutable = */ + eq(false), + ) + } + .thenReturn(mock()) + + val intentCaptor = argumentCaptor() + val testSubject = createTestSubject(context) + + // Act + testSubject.create(NotificationAction.Tap) + + // Assert + pendingIntentCompat.verify { + PendingIntentCompat.getActivity( + /* context = */ + eq(context), + /* requestCode = */ + eq(1), + /* intent = */ + intentCaptor.capture(), + /* flags = */ + eq(0), + /* isMutable = */ + eq(false), + ) + } + assertThat(intentCaptor.firstValue).all { + prop(Intent::getAction).isEqualTo(Intent.ACTION_MAIN) + prop(Intent::getPackage).isEqualTo(application.packageName) + transform { intent -> intent.hasCategory(Intent.CATEGORY_LAUNCHER) } + .isTrue() + } + } + } + + private fun createTestSubject( + context: Context = application, + ): DefaultNotificationActionIntentCreator { + return DefaultNotificationActionIntentCreator( + logger = TestLogger(), + applicationContext = context, + ) + } +} diff --git a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.kt b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.kt index 2e0c65053ba..a98af699488 100644 --- a/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.kt +++ b/feature/notification/impl/src/commonMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.kt @@ -5,9 +5,14 @@ import net.thunderbird.feature.notification.impl.command.NotificationCommandFact import net.thunderbird.feature.notification.impl.receiver.InAppNotificationNotifier import net.thunderbird.feature.notification.impl.receiver.SystemNotificationNotifier import net.thunderbird.feature.notification.impl.sender.DefaultNotificationSender +import org.koin.core.module.Module import org.koin.dsl.module +internal expect val platformFeatureNotificationModule: Module + val featureNotificationModule = module { + includes(platformFeatureNotificationModule) + factory { SystemNotificationNotifier() } factory { InAppNotificationNotifier() } diff --git a/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.jvm.kt b/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.jvm.kt new file mode 100644 index 00000000000..045bda3545f --- /dev/null +++ b/feature/notification/impl/src/jvmMain/kotlin/net/thunderbird/feature/notification/impl/inject/NotificationModule.jvm.kt @@ -0,0 +1,5 @@ +package net.thunderbird.feature.notification.impl.inject + +import org.koin.dsl.module + +internal actual val platformFeatureNotificationModule = module { }