Skip to content

Commit

Permalink
Customize notifications. Kotlin DSL syle, and Notification Image supp…
Browse files Browse the repository at this point in the history
…ort (#95)

* Adding Dsl style builder to Notifier notify method

* Notification Image support for android

* Android Notification Image using File path

* Ios notification image support
  • Loading branch information
mirzemehdi committed Dec 11, 2024
1 parent 9d2f50a commit 763a3f9
Show file tree
Hide file tree
Showing 11 changed files with 319 additions and 63 deletions.
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

0 comments on commit 763a3f9

Please sign in to comment.