Skip to content

Commit

Permalink
Implementing Desktop support for local notification (#44)
Browse files Browse the repository at this point in the history
* Initializing Desktop target

* Initial local notifier implementation

* Implementation of joptionspane for not supported tray notification
  • Loading branch information
mirzemehdi authored Jul 7, 2024
1 parent 6807e2d commit 14d4a92
Show file tree
Hide file tree
Showing 18 changed files with 262 additions and 4 deletions.
1 change: 0 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
7 changes: 6 additions & 1 deletion kmpnotifier/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ kotlin {
}
}


jvm()
iosX64()
iosArm64()
iosSimulatorArm64()
Expand Down Expand Up @@ -47,6 +47,11 @@ kotlin {
implementation(libs.koin.core)
implementation(libs.kotlinx.coroutine)
}

commonTest.dependencies {
implementation(libs.kotlin.test)
}

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ private fun Koin.onLibraryInitialized() {
get<PushNotifier>() //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()
}

}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mmk.kmpnotifier.notification.configuration


/**
* You can configure some customization for notifications depending on the platform
*/
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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<NotificationPlatformConfiguration>() as NotificationPlatformConfiguration.Desktop
DesktopNotifierFactory.getNotifier(configuration = configuration)
} bind Notifier::class
factoryOf(::DesktopPermissionUtil) bind PermissionUtil::class
factoryOf(::FirebaseDesktopPushNotifier) bind PushNotifier::class
}
Original file line number Diff line number Diff line change
@@ -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()

}
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String>): 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<String, String>) {
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")
}
}
Original file line number Diff line number Diff line change
@@ -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<Int, TrayIcon> = 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<String, String>): 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<String, String>
) {
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) }
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
19 changes: 18 additions & 1 deletion sample/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.compose.desktop.application.dsl.TargetFormat

plugins {
alias(libs.plugins.kotlinMultiplatform)
Expand All @@ -17,6 +18,7 @@ kotlin {
}
}
}
jvm("desktop")
listOf(
iosX64(),
iosArm64(),
Expand All @@ -29,6 +31,7 @@ kotlin {
}
}
sourceSets {
val desktopMain by getting

androidMain.dependencies {
implementation(libs.compose.ui)
Expand All @@ -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)
}
}
}

Expand Down Expand Up @@ -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"))
}
}
}

Binary file added sample/resources/common/ic_notification.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions sample/src/desktopMain/kotlin/com/mmk/kmpnotifier/sample/Main.kt
Original file line number Diff line number Diff line change
@@ -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()

}
}
Original file line number Diff line number Diff line change
@@ -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"
)
)
}

0 comments on commit 14d4a92

Please sign in to comment.