From 14d4a926f9276f08df7ca8b2c48664c07c9bbf96 Mon Sep 17 00:00:00 2001 From: Mirzamehdi Karimov <32781662+mirzemehdi@users.noreply.github.com> Date: Sun, 7 Jul 2024 19:53:56 +0200 Subject: [PATCH] Implementing Desktop support for local notification (#44) * Initializing Desktop target * Initial local notifier implementation * Implementation of joptionspane for not supported tray notification --- gradle/libs.versions.toml | 1 - kmpnotifier/build.gradle.kts | 7 ++- .../di/LibDependencyInitializer.kt | 3 +- .../com/mmk/kmpnotifier/di/PlatformModule.kt | 1 + .../NotificationPlatformConfiguration.kt | 7 +++ .../mmk/kmpnotifier/di/PlatformModule.jvm.kt | 25 ++++++++ .../extensions/DesktopPlatformExt.kt | 27 ++++++++ .../firebase/FirebaseDesktopPushNotifier.kt | 22 +++++++ .../notification/DesktopNotifierFactory.kt | 15 +++++ .../notification/impl/JOptionPaneNotifier.kt | 34 ++++++++++ .../notification/impl/TrayNotifier.kt | 58 ++++++++++++++++++ .../permission/DesktopPermissionUtil.kt | 13 ++++ sample/build.gradle.kts | 19 +++++- sample/resources/common/ic_notification.png | Bin 0 -> 618 bytes .../drawable/ic_notification.png | Bin 0 -> 618 bytes .../kotlin/com/mmk/kmpnotifier/sample/App.kt | 1 + .../kotlin/com/mmk/kmpnotifier/sample/Main.kt | 17 +++++ .../kmpnotifier/sample/Platform.desktop.kt | 16 +++++ 18 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/di/PlatformModule.jvm.kt create mode 100644 kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/extensions/DesktopPlatformExt.kt create mode 100644 kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/firebase/FirebaseDesktopPushNotifier.kt create mode 100644 kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/notification/DesktopNotifierFactory.kt create mode 100644 kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/notification/impl/JOptionPaneNotifier.kt create mode 100644 kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/notification/impl/TrayNotifier.kt create mode 100644 kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/permission/DesktopPermissionUtil.kt create mode 100644 sample/resources/common/ic_notification.png create mode 100644 sample/src/commonMain/composeResources/drawable/ic_notification.png create mode 100644 sample/src/desktopMain/kotlin/com/mmk/kmpnotifier/sample/Main.kt create mode 100644 sample/src/desktopMain/kotlin/com/mmk/kmpnotifier/sample/Platform.desktop.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8eaf580..f21d1f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,7 +47,6 @@ koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" ,version.ref="firebase-messaging"} - [plugins] jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/kmpnotifier/build.gradle.kts b/kmpnotifier/build.gradle.kts index c594d31..639f8be 100644 --- a/kmpnotifier/build.gradle.kts +++ b/kmpnotifier/build.gradle.kts @@ -16,7 +16,7 @@ kotlin { } } - + jvm() iosX64() iosArm64() iosSimulatorArm64() @@ -47,6 +47,11 @@ kotlin { implementation(libs.koin.core) implementation(libs.kotlinx.coroutine) } + + commonTest.dependencies { + implementation(libs.kotlin.test) + } + } } diff --git a/kmpnotifier/src/commonMain/kotlin/com/mmk/kmpnotifier/di/LibDependencyInitializer.kt b/kmpnotifier/src/commonMain/kotlin/com/mmk/kmpnotifier/di/LibDependencyInitializer.kt index 03b0a11..b01358f 100644 --- a/kmpnotifier/src/commonMain/kotlin/com/mmk/kmpnotifier/di/LibDependencyInitializer.kt +++ b/kmpnotifier/src/commonMain/kotlin/com/mmk/kmpnotifier/di/LibDependencyInitializer.kt @@ -42,13 +42,14 @@ private fun Koin.onLibraryInitialized() { get() //This will make sure that that when lib is initialized, init method is called when (platform) { - Platform.Android -> Unit //In Android platform permission should be asked in activity + Platform.Android, Platform.Desktop -> Unit //In Android platform permission should be asked in activity Platform.Ios -> { val askNotificationPermissionOnStart = (configuration as? NotificationPlatformConfiguration.Ios)?.askNotificationPermissionOnStart ?: true if (askNotificationPermissionOnStart) permissionUtil.askNotificationPermission() } + } } diff --git a/kmpnotifier/src/commonMain/kotlin/com/mmk/kmpnotifier/di/PlatformModule.kt b/kmpnotifier/src/commonMain/kotlin/com/mmk/kmpnotifier/di/PlatformModule.kt index 5d8d5a6..97e225d 100644 --- a/kmpnotifier/src/commonMain/kotlin/com/mmk/kmpnotifier/di/PlatformModule.kt +++ b/kmpnotifier/src/commonMain/kotlin/com/mmk/kmpnotifier/di/PlatformModule.kt @@ -6,5 +6,6 @@ import org.koin.core.module.Module internal sealed interface Platform { data object Android : Platform data object Ios : Platform + data object Desktop : Platform } internal expect val platformModule: Module \ No newline at end of file diff --git a/kmpnotifier/src/commonMain/kotlin/com/mmk/kmpnotifier/notification/configuration/NotificationPlatformConfiguration.kt b/kmpnotifier/src/commonMain/kotlin/com/mmk/kmpnotifier/notification/configuration/NotificationPlatformConfiguration.kt index 6a9261a..2c1363a 100644 --- a/kmpnotifier/src/commonMain/kotlin/com/mmk/kmpnotifier/notification/configuration/NotificationPlatformConfiguration.kt +++ b/kmpnotifier/src/commonMain/kotlin/com/mmk/kmpnotifier/notification/configuration/NotificationPlatformConfiguration.kt @@ -1,5 +1,6 @@ package com.mmk.kmpnotifier.notification.configuration + /** * You can configure some customization for notifications depending on the platform */ @@ -55,4 +56,10 @@ public sealed interface NotificationPlatformConfiguration { public val showPushNotification: Boolean = true, public val askNotificationPermissionOnStart: Boolean = true ) : NotificationPlatformConfiguration + + + public data class Desktop( + public val showPushNotification: Boolean = true, + public val notificationIconPath: String? = null + ) : NotificationPlatformConfiguration } \ No newline at end of file diff --git a/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/di/PlatformModule.jvm.kt b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/di/PlatformModule.jvm.kt new file mode 100644 index 0000000..aae4cc1 --- /dev/null +++ b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/di/PlatformModule.jvm.kt @@ -0,0 +1,25 @@ +package com.mmk.kmpnotifier.di + +import com.mmk.kmpnotifier.firebase.FirebaseDesktopPushNotifier +import com.mmk.kmpnotifier.notification.DesktopNotifierFactory +import com.mmk.kmpnotifier.notification.Notifier +import com.mmk.kmpnotifier.notification.PushNotifier +import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfiguration +import com.mmk.kmpnotifier.permission.DesktopPermissionUtil +import com.mmk.kmpnotifier.permission.PermissionUtil +import org.koin.core.module.Module +import org.koin.core.module.dsl.factoryOf +import org.koin.dsl.bind +import org.koin.dsl.module + +internal actual val platformModule: Module = module { + factory { Platform.Desktop } bind Platform::class + + factory { + val configuration = + get() as NotificationPlatformConfiguration.Desktop + DesktopNotifierFactory.getNotifier(configuration = configuration) + } bind Notifier::class + factoryOf(::DesktopPermissionUtil) bind PermissionUtil::class + factoryOf(::FirebaseDesktopPushNotifier) bind PushNotifier::class +} \ No newline at end of file diff --git a/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/extensions/DesktopPlatformExt.kt b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/extensions/DesktopPlatformExt.kt new file mode 100644 index 0000000..c4d483c --- /dev/null +++ b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/extensions/DesktopPlatformExt.kt @@ -0,0 +1,27 @@ +package com.mmk.kmpnotifier.extensions + +import java.io.File + +internal sealed interface DesktopPlatform { + data object Linux : DesktopPlatform + data object Windows : DesktopPlatform + data object MacOs : DesktopPlatform +} + +internal fun getDesktopPlatformType(): DesktopPlatform? { + val name = System.getProperty("os.name") + return when { + name?.contains("Linux") == true -> DesktopPlatform.Linux + name?.contains("Win") == true -> DesktopPlatform.Windows + name?.contains("Mac") == true -> DesktopPlatform.MacOs + else -> null + } +} + +public fun composeDesktopResourcesPath(): String? { + return runCatching { + val resourcesDirectory = File(System.getProperty("compose.application.resources.dir")) + return resourcesDirectory.canonicalPath + }.getOrNull() + +} \ No newline at end of file diff --git a/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/firebase/FirebaseDesktopPushNotifier.kt b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/firebase/FirebaseDesktopPushNotifier.kt new file mode 100644 index 0000000..6b5d2ec --- /dev/null +++ b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/firebase/FirebaseDesktopPushNotifier.kt @@ -0,0 +1,22 @@ +package com.mmk.kmpnotifier.firebase + +import com.mmk.kmpnotifier.notification.PushNotifier + +internal class FirebaseDesktopPushNotifier:PushNotifier { + override suspend fun getToken(): String? { + println("Get firebase toekn") + return null + } + + override suspend fun deleteMyToken() { + println("Delete firebase toekn") + } + + override suspend fun subscribeToTopic(topic: String) { + println("Subscribe firebase topic") + } + + override suspend fun unSubscribeFromTopic(topic: String) { + println("Unsubscribe firebase topic") + } +} \ No newline at end of file diff --git a/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/notification/DesktopNotifierFactory.kt b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/notification/DesktopNotifierFactory.kt new file mode 100644 index 0000000..32c881c --- /dev/null +++ b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/notification/DesktopNotifierFactory.kt @@ -0,0 +1,15 @@ +package com.mmk.kmpnotifier.notification + +import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfiguration +import com.mmk.kmpnotifier.notification.impl.JOptionPaneNotifier +import com.mmk.kmpnotifier.notification.impl.TrayNotifier + +internal object DesktopNotifierFactory { + fun getNotifier(configuration: NotificationPlatformConfiguration.Desktop): Notifier { + return when { + TrayNotifier.isSupported -> TrayNotifier(configuration = configuration) + //TODO for now return JOptionPaneNotifier for not supported platforms + else -> JOptionPaneNotifier(configuration = configuration) + } + } +} \ No newline at end of file diff --git a/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/notification/impl/JOptionPaneNotifier.kt b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/notification/impl/JOptionPaneNotifier.kt new file mode 100644 index 0000000..6340759 --- /dev/null +++ b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/notification/impl/JOptionPaneNotifier.kt @@ -0,0 +1,34 @@ +package com.mmk.kmpnotifier.notification.impl + +import com.mmk.kmpnotifier.notification.Notifier +import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfiguration +import javax.swing.ImageIcon +import javax.swing.JOptionPane + +internal class JOptionPaneNotifier(private val configuration: NotificationPlatformConfiguration.Desktop) : + Notifier { + + override fun notify(title: String, body: String, payloadData: Map): Int { + val id = -1 + notify(id = id, title = title, body = body, payloadData) + return id + } + + override fun notify(id: Int, title: String, body: String, payloadData: Map) { + JOptionPane.showMessageDialog( + null, + body, + title, + JOptionPane.INFORMATION_MESSAGE, + ImageIcon(configuration.notificationIconPath) + ) + } + + override fun remove(id: Int) { + println("No remove functionality") + } + + override fun removeAll() { + println("No removeAll functionality") + } +} \ No newline at end of file diff --git a/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/notification/impl/TrayNotifier.kt b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/notification/impl/TrayNotifier.kt new file mode 100644 index 0000000..ed4fe07 --- /dev/null +++ b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/notification/impl/TrayNotifier.kt @@ -0,0 +1,58 @@ +package com.mmk.kmpnotifier.notification.impl + +import com.mmk.kmpnotifier.notification.Notifier +import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfiguration +import java.awt.SystemTray +import java.awt.Toolkit +import java.awt.TrayIcon +import kotlin.random.Random + +internal class TrayNotifier(private val configuration: NotificationPlatformConfiguration.Desktop) : + Notifier { + + private val trayIcons: MutableMap = mutableMapOf() + + companion object { + val isSupported by lazy { + SystemTray.isSupported().also { + if (it.not()) System.err.println( + "Tray is not supported on the current platform. " + ) + } + } + } + + override fun notify(title: String, body: String, payloadData: Map): Int { + if (isSupported.not()) return -1 + val notificationID = Random.nextInt(0, Int.MAX_VALUE) + notify(notificationID, title, body, payloadData) + return notificationID + } + + override fun notify( + id: Int, + title: String, + body: String, + payloadData: Map + ) { + if (isSupported.not()) return + val icon = Toolkit.getDefaultToolkit().getImage(configuration.notificationIconPath) + val trayIcon = TrayIcon(icon).apply { + isImageAutoSize = true + } + SystemTray.getSystemTray().add(trayIcon) + .also { trayIcons[id] = trayIcon } + trayIcon.displayMessage(title, body, TrayIcon.MessageType.INFO) + } + + override fun remove(id: Int) { + val systemTray = SystemTray.getSystemTray() + val trayIcon = trayIcons.getOrDefault(id, null) + trayIcon?.let { systemTray.remove(it) } + } + + override fun removeAll() { + val systemTray = SystemTray.getSystemTray() + systemTray.trayIcons.forEach { systemTray.remove(it) } + } +} \ No newline at end of file diff --git a/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/permission/DesktopPermissionUtil.kt b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/permission/DesktopPermissionUtil.kt new file mode 100644 index 0000000..5c5aa21 --- /dev/null +++ b/kmpnotifier/src/jvmMain/kotlin/com/mmk/kmpnotifier/permission/DesktopPermissionUtil.kt @@ -0,0 +1,13 @@ +package com.mmk.kmpnotifier.permission + +internal class DesktopPermissionUtil:PermissionUtil { + override fun hasNotificationPermission(onPermissionResult: (Boolean) -> Unit) { + println("Desktop has permission result") + onPermissionResult(true) + } + + override fun askNotificationPermission(onPermissionGranted: () -> Unit) { + println("Desktop ask permission") + onPermissionGranted() + } +} \ No newline at end of file diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 5cbb6fe..1ae25be 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.compose.ExperimentalComposeLibrary +import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { alias(libs.plugins.kotlinMultiplatform) @@ -17,6 +18,7 @@ kotlin { } } } + jvm("desktop") listOf( iosX64(), iosArm64(), @@ -29,6 +31,7 @@ kotlin { } } sourceSets { + val desktopMain by getting androidMain.dependencies { implementation(libs.compose.ui) @@ -39,10 +42,13 @@ kotlin { implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material) - @OptIn(ExperimentalComposeLibrary::class) implementation(compose.components.resources) api(project(":kmpnotifier")) } + desktopMain.dependencies { + implementation(compose.desktop.currentOs) + implementation(compose.desktop.common) + } } } @@ -82,4 +88,15 @@ android { debugImplementation(libs.compose.ui.tooling) } } +compose.desktop { + application { + mainClass = "com.mmk.kmpnotifier.sample.MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "KMPNotifier" + packageVersion = "1.0.0" + appResourcesRootDir.set(project.layout.projectDirectory.dir("resources")) + } + } +} diff --git a/sample/resources/common/ic_notification.png b/sample/resources/common/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..4a0728ae844ed022291562e02908bb080b87ecc3 GIT binary patch literal 618 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sQ2{<7uI>dsKqf6f@2ah8KyzhF zg8YIRLhqh$|De+F{7+I1_aY{t?}gH=rZ$JRuAb(k>oKuo#{~uk#zmekjv*e$?}lD1 zYgXV9WnmFq#d_t|v;XzQ3umtm-teaIw0E_N;?9TnmaO`>`fl{v+q-p3%bQtDZkDt% zE@@xBa+|>chMt~YhGNe^Jp<0ai&r`EPioyB`g7*-Pp+Yz94-F*zvb2~Vy?Q`xJu%X zp87wjhegguK8I;{bbg$Cf^EwSd%bsG4^}ltF3Ua^ACPnA-3Doa%V(D|OE}c-$$7C* z-SfY;@4Ow-*JiT#iCmdpva~BjD`{tt?<3PYQ+BT4lF3*UI9+OoKidP({VImu6H^~g z+#$Bp*;xMyg9+!jW0hIOL_CM`u3^pg{=S5PH*|WVcD^hsyCn4&pCB! z&D5+(iD^eZFFoFxZo1Mtsf|84;tDnm{r-UW|j}`S( literal 0 HcmV?d00001 diff --git a/sample/src/commonMain/composeResources/drawable/ic_notification.png b/sample/src/commonMain/composeResources/drawable/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..4a0728ae844ed022291562e02908bb080b87ecc3 GIT binary patch literal 618 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sQ2{<7uI>dsKqf6f@2ah8KyzhF zg8YIRLhqh$|De+F{7+I1_aY{t?}gH=rZ$JRuAb(k>oKuo#{~uk#zmekjv*e$?}lD1 zYgXV9WnmFq#d_t|v;XzQ3umtm-teaIw0E_N;?9TnmaO`>`fl{v+q-p3%bQtDZkDt% zE@@xBa+|>chMt~YhGNe^Jp<0ai&r`EPioyB`g7*-Pp+Yz94-F*zvb2~Vy?Q`xJu%X zp87wjhegguK8I;{bbg$Cf^EwSd%bsG4^}ltF3Ua^ACPnA-3Doa%V(D|OE}c-$$7C* z-SfY;@4Ow-*JiT#iCmdpva~BjD`{tt?<3PYQ+BT4lF3*UI9+OoKidP({VImu6H^~g z+#$Bp*;xMyg9+!jW0hIOL_CM`u3^pg{=S5PH*|WVcD^hsyCn4&pCB! z&D5+(iD^eZFFoFxZo1Mtsf|84;tDnm{r-UW|j}`S( literal 0 HcmV?d00001 diff --git a/sample/src/commonMain/kotlin/com/mmk/kmpnotifier/sample/App.kt b/sample/src/commonMain/kotlin/com/mmk/kmpnotifier/sample/App.kt index 8ab6ab3..c85a110 100644 --- a/sample/src/commonMain/kotlin/com/mmk/kmpnotifier/sample/App.kt +++ b/sample/src/commonMain/kotlin/com/mmk/kmpnotifier/sample/App.kt @@ -23,6 +23,7 @@ import com.mmk.kmpnotifier.notification.NotifierManager fun App() { var myPushNotificationToken by remember { mutableStateOf("") } LaunchedEffect(true) { + println("LaunchedEffectApp is called") NotifierManager.addListener(object : NotifierManager.Listener { override fun onNewToken(token: String) { diff --git a/sample/src/desktopMain/kotlin/com/mmk/kmpnotifier/sample/Main.kt b/sample/src/desktopMain/kotlin/com/mmk/kmpnotifier/sample/Main.kt new file mode 100644 index 0000000..2634374 --- /dev/null +++ b/sample/src/desktopMain/kotlin/com/mmk/kmpnotifier/sample/Main.kt @@ -0,0 +1,17 @@ +package com.mmk.kmpnotifier.sample + +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application + + +fun main() = application { + AppInitializer.onApplicationStart() + Window( + onCloseRequest = ::exitApplication, + title = "KMPNotifier Desktop", + ) { + println("Desktop app is started") + App() + + } +} \ No newline at end of file diff --git a/sample/src/desktopMain/kotlin/com/mmk/kmpnotifier/sample/Platform.desktop.kt b/sample/src/desktopMain/kotlin/com/mmk/kmpnotifier/sample/Platform.desktop.kt new file mode 100644 index 0000000..8944288 --- /dev/null +++ b/sample/src/desktopMain/kotlin/com/mmk/kmpnotifier/sample/Platform.desktop.kt @@ -0,0 +1,16 @@ +package com.mmk.kmpnotifier.sample + +import com.mmk.kmpnotifier.extensions.composeDesktopResourcesPath +import com.mmk.kmpnotifier.notification.NotifierManager +import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfiguration +import java.io.File + +actual fun onApplicationStartPlatformSpecific() { + println("Desktop app is initialized") + NotifierManager.initialize( + NotificationPlatformConfiguration.Desktop( + showPushNotification = true, + notificationIconPath = composeDesktopResourcesPath() + File.separator + "ic_notification.png" + ) + ) +} \ No newline at end of file