Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize notifications. Kotlin DSL syle, and Notification Image support #95

Merged
merged 4 commits into from
Dec 11, 2024
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
Expand Up @@ -3,6 +3,8 @@ package com.mmk.kmpnotifier.notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Log
import androidx.core.app.NotificationCompat
Expand All @@ -11,6 +13,13 @@ import com.mmk.kmpnotifier.Constants.ACTION_NOTIFICATION_CLICK
import com.mmk.kmpnotifier.extensions.notificationManager
import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfiguration
import com.mmk.kmpnotifier.permission.PermissionUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import java.net.URL
import kotlin.coroutines.cancellation.CancellationException
import kotlin.random.Random


Expand All @@ -21,13 +30,30 @@ internal class AndroidNotifier(
private val permissionUtil: PermissionUtil,
) : Notifier {

private val scope by lazy { MainScope() }

override fun notify(title: String, body: String, payloadData: Map<String, String>): Int {
val notificationID = Random.nextInt(0, Int.MAX_VALUE)
notify(notificationID, title, body, payloadData)
notify {
this.id = notificationID
this.title = title
this.body = body
this.payloadData = payloadData
}
return notificationID
}

override fun notify(id: Int, title: String, body: String, payloadData: Map<String, String>) {
notify {
this.id = id
this.title = title
this.body = body
this.payloadData = payloadData
}
}

override fun notify(block: NotifierBuilder.() -> Unit) {
val builder = NotifierBuilder().apply(block)
permissionUtil.hasNotificationPermission {
if (it.not())
Log.w(
Expand All @@ -36,24 +62,36 @@ internal class AndroidNotifier(
)
}
val notificationManager = context.notificationManager ?: return
val pendingIntent = getPendingIntent(payloadData)
val pendingIntent = getPendingIntent(builder.payloadData)
notificationChannelFactory.createChannels()
val notification = NotificationCompat.Builder(
context,
androidNotificationConfiguration.notificationChannelData.id
).apply {
setChannelId(androidNotificationConfiguration.notificationChannelData.id)
setContentTitle(title)
setContentText(body)
setSmallIcon(androidNotificationConfiguration.notificationIconResId)
setAutoCancel(true)
setContentIntent(pendingIntent)
androidNotificationConfiguration.notificationIconColorResId?.let {
color = ContextCompat.getColor(context, it)
}
}.build()
scope.launch {
val imageBitmap = builder.image?.asBitmap()
val notification = NotificationCompat.Builder(
context,
androidNotificationConfiguration.notificationChannelData.id
).apply {
setChannelId(androidNotificationConfiguration.notificationChannelData.id)
setContentTitle(builder.title)
setContentText(builder.body)
imageBitmap?.let {
setLargeIcon(it)
setStyle(
NotificationCompat.BigPictureStyle()
.bigPicture(it)
.bigLargeIcon(null as Bitmap?)
)
}
setSmallIcon(androidNotificationConfiguration.notificationIconResId)
setAutoCancel(true)
setContentIntent(pendingIntent)
androidNotificationConfiguration.notificationIconColorResId?.let {
color = ContextCompat.getColor(context, it)
}
}.build()
notificationManager.notify(builder.id, notification)
}


notificationManager.notify(id, notification)
}

override fun remove(id: Int) {
Expand All @@ -67,8 +105,6 @@ internal class AndroidNotifier(
}

private fun getPendingIntent(payloadData: Map<String, String>): PendingIntent? {


val intent = getLauncherActivityIntent()?.apply {
putExtra(ACTION_NOTIFICATION_CLICK, ACTION_NOTIFICATION_CLICK)
payloadData.forEach { putExtra(it.key, it.value) }
Expand All @@ -88,4 +124,32 @@ internal class AndroidNotifier(
return packageManager.getLaunchIntentForPackage(context.applicationContext.packageName)
}

private suspend fun NotificationImage?.asBitmap(): Bitmap? {
return withContext(Dispatchers.IO) {
try {
when (this@asBitmap) {
null -> null
is NotificationImage.Url -> {
URL(url).openStream().buffered().use { inputStream ->
BitmapFactory.decodeStream(inputStream)
}
}

is NotificationImage.File -> {
BitmapFactory.decodeFile(path)
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e(
"AndroidNotifier",
"Error while processing notification image. Ensure correct path or internet connection.",
e
)
null
}
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.mmk.kmpnotifier.notification

public sealed class NotificationImage {
/**
* Url of image. Make sure you gave internet permission
*/
public data class Url(val url: String) : NotificationImage()

/**
* File path. Make sure app can read this file
*/
public data class File(val path: String) : NotificationImage()

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.mmk.kmpnotifier.notification

import org.koin.core.scope.Scope

/**
* Class that represent local notification
*/
Expand Down Expand Up @@ -38,6 +36,12 @@ public interface Notifier {
payloadData: Map<String, String> = emptyMap()
)

/**
* Sends local notification to device,
* with notification builder that allows you to set notification params
*/
public fun notify(block: NotifierBuilder.() -> Unit)


/**
* Remove notification by id
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.mmk.kmpnotifier.notification

import kotlin.random.Random

public class NotifierBuilder {

public var id: Int = Random.nextInt(0, Int.MAX_VALUE)
public var title: String = ""
public var body: String = ""

public var payloadData: Map<String, String> = emptyMap()

public var image: NotificationImage? = null

public fun payload(block: MutableMap<String, String>.() -> Unit) {
payloadData = mutableMapOf<String, String>().apply(block)
}

}
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
package com.mmk.kmpnotifier.notification

import com.mmk.kmpnotifier.extensions.onApplicationDidReceiveRemoteNotification
import com.mmk.kmpnotifier.extensions.onNotificationClicked
import com.mmk.kmpnotifier.extensions.onUserNotification
import com.mmk.kmpnotifier.extensions.shouldShowNotification
import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfiguration
import com.mmk.kmpnotifier.permission.IosPermissionUtil
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import platform.Foundation.NSData
import platform.Foundation.NSTemporaryDirectory
import platform.Foundation.NSURL
import platform.Foundation.dataWithContentsOfURL
import platform.Foundation.writeToURL
import platform.UserNotifications.UNMutableNotificationContent
import platform.UserNotifications.UNNotification
import platform.UserNotifications.UNNotificationContent
import platform.UserNotifications.UNNotificationAttachment
import platform.UserNotifications.UNNotificationPresentationOptions
import platform.UserNotifications.UNNotificationRequest
import platform.UserNotifications.UNNotificationResponse
Expand All @@ -17,6 +27,7 @@ import platform.UserNotifications.UNTimeIntervalNotificationTrigger
import platform.UserNotifications.UNUserNotificationCenter
import platform.UserNotifications.UNUserNotificationCenterDelegateProtocol
import platform.darwin.NSObject
import kotlin.coroutines.cancellation.CancellationException
import kotlin.random.Random

internal class IosNotifier(
Expand All @@ -25,32 +36,67 @@ internal class IosNotifier(
private val iosNotificationConfiguration: NotificationPlatformConfiguration.Ios
) : Notifier {

private val scope by lazy { MainScope() }


override fun notify(title: String, body: String, payloadData: Map<String, String>): Int {
val notificationID = Random.nextInt(0, Int.MAX_VALUE)
notify(notificationID, title, body, payloadData)
notify {
this.id = notificationID
this.title = title
this.body = body
this.payloadData = payloadData
}
return notificationID
}

override fun notify(id: Int, title: String, body: String, payloadData: Map<String, String>) {
permissionUtil.askNotificationPermission {
val notificationContent = UNMutableNotificationContent().apply {
setTitle(title)
setBody(body)
setSound()
setUserInfo(userInfo + payloadData)
}
val trigger = UNTimeIntervalNotificationTrigger.triggerWithTimeInterval(1.0, false)
val notificationRequest = UNNotificationRequest.requestWithIdentifier(
identifier = id.toString(),
content = notificationContent,
trigger = trigger
)
notify {
this.id = id
this.title = title
this.body = body
this.payloadData = payloadData
}
}

notificationCenter.addNotificationRequest(notificationRequest) { error ->
error?.let { println("Error showing notification: $error") }
override fun notify(block: NotifierBuilder.() -> Unit) {
val builder = NotifierBuilder().apply(block)
permissionUtil.askNotificationPermission {
scope.launch {
val notificationContent = UNMutableNotificationContent().apply {
setTitle(builder.title)
setBody(builder.body)
setSound()
setUserInfo(userInfo + builder.payloadData)
// Add image if available
builder.image?.let { notificationImage ->
val attachment = notificationImage.toNotificationAttachment()
attachment?.let {
setAttachments(listOf(it))
}
}
}
val trigger = UNTimeIntervalNotificationTrigger.triggerWithTimeInterval(1.0, false)
val notificationRequest = UNNotificationRequest.requestWithIdentifier(
identifier = builder.id.toString(),
content = notificationContent,
trigger = trigger
)
notificationCenter.addNotificationRequest(notificationRequest) { error ->
error?.let { println("Error showing notification: $error") }
}
}
}

}


override fun remove(id: Int) {
notificationCenter.removeDeliveredNotificationsWithIdentifiers(listOf(id.toString()))
}

override fun removeAll() {
notificationCenter.removeAllDeliveredNotifications()
}

private fun UNMutableNotificationContent.setSound() {
Expand All @@ -62,14 +108,46 @@ internal class IosNotifier(
setSound(notificationSound)
}

override fun remove(id: Int) {
notificationCenter.removeDeliveredNotificationsWithIdentifiers(listOf(id.toString()))
}
@OptIn(ExperimentalForeignApi::class)
private suspend fun NotificationImage.toNotificationAttachment(): UNNotificationAttachment? {
return withContext(Dispatchers.IO) {
try {
when (this@toNotificationAttachment) {
is NotificationImage.Url -> {
val nsUrl = NSURL.URLWithString(url) ?: return@withContext null
val data = NSData.dataWithContentsOfURL(nsUrl)
val tempDirectory = NSTemporaryDirectory()
val tempFilePath =
tempDirectory + "/notification_image_${Random.nextInt()}.jpg"
val tempFileUrl = NSURL.fileURLWithPath(tempFilePath)
data?.writeToURL(tempFileUrl, true)
UNNotificationAttachment.attachmentWithIdentifier(
"notification_image",
tempFileUrl,
null,
null
)
}

override fun removeAll() {
notificationCenter.removeAllDeliveredNotifications()
is NotificationImage.File -> {
val fileUrl = NSURL.fileURLWithPath(path)
UNNotificationAttachment.attachmentWithIdentifier(
"notification_image",
fileUrl,
null,
null
)
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
println("Error creating notification attachment: $e")
null
}
}
}


internal class NotificationDelegate : UNUserNotificationCenterDelegateProtocol, NSObject() {
override fun userNotificationCenter(
center: UNUserNotificationCenter,
Expand All @@ -96,3 +174,5 @@ internal class IosNotifier(
}
}



Loading