diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/BreezSdkLiquidConnector.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/BreezSdkLiquidConnector.kt index b1e97ed..402d253 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/BreezSdkLiquidConnector.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/BreezSdkLiquidConnector.kt @@ -14,17 +14,21 @@ class BreezSdkLiquidConnector { internal fun connectSDK( connectRequest: ConnectRequest, sdkListener: EventListener, - logger: ServiceLogger + logger: ServiceLogger, ): BindingLiquidSdk { synchronized(this) { if (liquidSDK == null) { logger.log( - TAG, "Connecting to Breez Liquid SDK", "DEBUG" + TAG, + "Connecting to Breez Liquid SDK", + "DEBUG", ) liquidSDK = connect(connectRequest) logger.log(TAG, "Connected to Breez Liquid SDK", "DEBUG") liquidSDK!!.addEventListener(sdkListener) - } else logger.log(TAG, "Already connected to Breez Liquid SDK", "DEBUG") + } else { + logger.log(TAG, "Already connected to Breez Liquid SDK", "DEBUG") + } return liquidSDK!! } diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/Constants.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/Constants.kt index b125dc3..6dc52e6 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/Constants.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/Constants.kt @@ -5,12 +5,13 @@ object Constants { const val SHUTDOWN_DELAY_MS = 60 * 1000L // Notification Channels - const val NOTIFICATION_CHANNEL_SWAP_UPDATED = "SWAP_UPDATED" + const val NOTIFICATION_CHANNEL_DISMISSIBLE = "DISMISSIBLE" const val NOTIFICATION_CHANNEL_FOREGROUND_SERVICE = "FOREGROUND_SERVICE" - const val NOTIFICATION_CHANNEL_LNURL_PAY = "LNURL_PAY" + const val NOTIFICATION_CHANNEL_REPLACEABLE = "REPLACEABLE" // Notification Ids const val NOTIFICATION_ID_FOREGROUND_SERVICE = 100 + const val NOTIFICATION_ID_REPLACEABLE = 1001 // Intent Extras const val EXTRA_REMOTE_MESSAGE = "remote_message" @@ -18,6 +19,7 @@ object Constants { // Message Data @Suppress("unused") const val MESSAGE_DATA_TYPE = "notification_type" + @Suppress("unused") const val MESSAGE_DATA_PAYLOAD = "notification_payload" @@ -26,6 +28,14 @@ object Constants { const val MESSAGE_TYPE_SWAP_UPDATED = "swap_updated" // Resource Identifiers + const val DISMISSIBLE_NOTIFICATION_CHANNEL_DESCRIPTION = + "dismissible_notification_channel_description" + const val DISMISSIBLE_NOTIFICATION_CHANNEL_NAME = + "dismissible_notification_channel_name" + const val DISMISSIBLE_WORKGROUP_ID = "dismissible" + const val DISMISSIBLE_WORKGROUP_DESCRIPTION = + "dismissible_work_group_description" + const val DISMISSIBLE_WORKGROUP_NAME = "dismissible_work_group_name" const val FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION = "foreground_service_notification_channel_description" const val FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME = @@ -38,15 +48,8 @@ object Constants { "lnurl_pay_invoice_notification_title" const val LNURL_PAY_METADATA_PLAIN_TEXT = "lnurl_pay_metadata_plain_text" - const val LNURL_PAY_NOTIFICATION_CHANNEL_DESCRIPTION = - "lnurl_pay_notification_channel_description" - const val LNURL_PAY_NOTIFICATION_CHANNEL_NAME = - "lnurl_pay_notification_channel_name" const val LNURL_PAY_NOTIFICATION_FAILURE_TITLE = "lnurl_pay_notification_failure_title" - const val LNURL_PAY_WORKGROUP_ID = "lnurl_pay" - const val LNURL_PAY_WORKGROUP_DESCRIPTION = "lnurl_pay_work_group_description" - const val LNURL_PAY_WORKGROUP_NAME = "lnurl_pay_work_group_name" const val NOTIFICATION_COLOR = "default_notification_color" const val NOTIFICATION_ICON = "ic_stat_ic_notification" const val PAYMENT_RECEIVED_NOTIFICATION_TEXT = @@ -61,22 +64,27 @@ object Constants { "payment_waiting_fee_acceptance_notification_title" const val PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT = "payment_waiting_fee_acceptance_text" + const val REPLACEABLE_NOTIFICATION_CHANNEL_DESCRIPTION = + "replaceable_notification_channel_description" + const val REPLACEABLE_NOTIFICATION_CHANNEL_NAME = + "replaceable_notification_channel_name" + const val REPLACEABLE_WORKGROUP_ID = "replaceable" + const val REPLACEABLE_WORKGROUP_DESCRIPTION = "replaceable_work_group_description" + const val REPLACEABLE_WORKGROUP_NAME = "replaceable_work_group_name" const val SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT = "swap_confirmed_notification_failure_text" const val SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE = "swap_confirmed_notification_failure_title" const val SWAP_CONFIRMED_NOTIFICATION_TITLE = "swap_confirmed_notification_title" - const val SWAP_UPDATED_NOTIFICATION_CHANNEL_DESCRIPTION = - "swap_updated_notification_channel_description" - const val SWAP_UPDATED_NOTIFICATION_CHANNEL_NAME = - "swap_updated_notification_channel_name" - const val SWAP_UPDATED_WORKGROUP_ID = "swap_updated" - const val SWAP_UPDATED_WORKGROUP_DESCRIPTION = - "swap_updated_work_group_description" - const val SWAP_UPDATED_WORKGROUP_NAME = "swap_updated_work_group_name" // Resource Identifier Defaults + const val DEFAULT_DISMISSIBLE_NOTIFICATION_CHANNEL_DESCRIPTION = + "Channel for dismissible notifications when the application is in the background" + const val DEFAULT_DISMISSIBLE_NOTIFICATION_CHANNEL_NAME = "Dismissable Notifications" + const val DEFAULT_DISMISSIBLE_WORKGROUP_DESCRIPTION = + "Required to handle dismissible notifications when the application is in the background" + const val DEFAULT_DISMISSIBLE_WORKGROUP_NAME = "Dismissable Notifications" const val DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION = "Shown when the application is in the background" const val DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME = @@ -89,14 +97,8 @@ object Constants { "Fetching Invoice" const val DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT = "Pay with LNURL" - const val DEFAULT_LNURL_PAY_NOTIFICATION_CHANNEL_DESCRIPTION = - "Notifications for receiving payments when the application is in the background" - const val DEFAULT_LNURL_PAY_NOTIFICATION_CHANNEL_NAME = "Receiving Payments" const val DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE = "Receive Payment Failed" - const val DEFAULT_LNURL_PAY_WORKGROUP_DESCRIPTION = - "Required to handle LNURL pay requests when the application is in the background" - const val DEFAULT_LNURL_PAY_WORKGROUP_NAME = "LNURL Payments" const val DEFAULT_NOTIFICATION_COLOR = "#0089F9" const val DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TEXT = "Received %d sats" @@ -110,15 +112,15 @@ object Constants { "Payment requires fee acceptance" const val DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT = "Tap to review updated fees" + const val DEFAULT_REPLACEABLE_WORKGROUP_DESCRIPTION = + "Required to handle replaceable notifications when the application is in the background" + const val DEFAULT_REPLACEABLE_WORKGROUP_NAME = "Replaceable Notifications" + const val DEFAULT_REPLACEABLE_NOTIFICATION_CHANNEL_DESCRIPTION = + "Channel for replaceable notifications when the application is in the background" + const val DEFAULT_REPLACEABLE_NOTIFICATION_CHANNEL_NAME = + "Replaceable Notifications" const val DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT = "Tap to complete payment" const val DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE = "Payment Pending" - const val DEFAULT_SWAP_UPDATED_NOTIFICATION_CHANNEL_DESCRIPTION = - "Notifications for swap updates when the application is in the background" - const val DEFAULT_SWAP_UPDATED_NOTIFICATION_CHANNEL_NAME = - "Swap Updates" - const val DEFAULT_SWAP_UPDATED_WORKGROUP_DESCRIPTION = - "Required to handle swap updates when the application is in the background" - const val DEFAULT_SWAP_UPDATED_WORKGROUP_NAME = "Swap Updates" } diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ForegroundService.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ForegroundService.kt index dcb59a1..5291084 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ForegroundService.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ForegroundService.kt @@ -11,9 +11,9 @@ import breez_sdk_liquid.EventListener import breez_sdk_liquid.Logger import breez_sdk_liquid.SdkEvent import breez_sdk_liquid_notification.BreezSdkLiquidConnector.Companion.connectSDK -import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_SWAP_UPDATED import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_LNURL_PAY_INFO import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_LNURL_PAY_INVOICE +import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_SWAP_UPDATED import breez_sdk_liquid_notification.Constants.NOTIFICATION_ID_FOREGROUND_SERVICE import breez_sdk_liquid_notification.Constants.SERVICE_TIMEOUT_MS import breez_sdk_liquid_notification.Constants.SHUTDOWN_DELAY_MS @@ -22,19 +22,23 @@ import breez_sdk_liquid_notification.job.Job import breez_sdk_liquid_notification.job.LnurlPayInfoJob import breez_sdk_liquid_notification.job.LnurlPayInvoiceJob import breez_sdk_liquid_notification.job.SwapUpdatedJob -import kotlin.io.path.Path import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlin.io.path.Path interface SdkForegroundService { fun onFinished(job: Job) } -abstract class ForegroundService : SdkForegroundService, EventListener, Service() { +abstract class ForegroundService : + Service(), + SdkForegroundService, + EventListener { private var liquidSDK: BindingLiquidSdk? = null + @Suppress("MemberVisibilityCanBePrivate") val serviceScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) protected var logger: ServiceLogger = ServiceLogger() @@ -48,9 +52,7 @@ abstract class ForegroundService : SdkForegroundService, EventListener, Service( // SERVICE LIFECYCLE // // =========================================================== // - override fun onBind(intent: Intent): IBinder? { - return null - } + override fun onBind(intent: Intent): IBinder? = null /** Called by a Job to signal that it is complete. */ override fun onFinished(job: Job) { @@ -63,20 +65,22 @@ abstract class ForegroundService : SdkForegroundService, EventListener, Service( /** Stop the service */ private val serviceTimeoutHandler = Handler(Looper.getMainLooper()) - private val serviceTimeoutRunnable: Runnable = Runnable { - logger.log(TAG, "Reached service timeout...", "DEBUG") - synchronized(this) { - jobs.forEach { job -> job.onShutdown() } + private val serviceTimeoutRunnable: Runnable = + Runnable { + logger.log(TAG, "Reached service timeout...", "DEBUG") + synchronized(this) { + jobs.forEach { job -> job.onShutdown() } + } + + shutdown() } - shutdown() - } - private val shutdownHandler = Handler(Looper.getMainLooper()) - private val shutdownRunnable: Runnable = Runnable { - logger.log(TAG, "Reached scheduled shutdown...", "DEBUG") - shutdown() - } + private val shutdownRunnable: Runnable = + Runnable { + logger.log(TAG, "Reached scheduled shutdown...", "DEBUG") + shutdown() + } private fun resetDelayedCallbacks() { serviceTimeoutHandler.removeCallbacksAndMessages(null) @@ -98,7 +102,11 @@ abstract class ForegroundService : SdkForegroundService, EventListener, Service( } /** Called when an intent is called for this service. */ - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + override fun onStartCommand( + intent: Intent?, + flags: Int, + startId: Int, + ): Int { super.onStartCommand(intent, flags, startId) resetDelayedCallbacks() @@ -136,43 +144,51 @@ abstract class ForegroundService : SdkForegroundService, EventListener, Service( /** Get the job to be executed from the Message data in the Intent. * This can be overridden to handle custom jobs. */ - open fun getJobFromIntent(intent: Intent?): Job? { - return Message.createFromIntent(intent)?.let { message -> + open fun getJobFromIntent(intent: Intent?): Job? = + Message.createFromIntent(intent)?.let { message -> message.payload?.let { payload -> when (message.type) { - MESSAGE_TYPE_SWAP_UPDATED -> SwapUpdatedJob( - applicationContext, - this, - payload, - logger - ) + MESSAGE_TYPE_SWAP_UPDATED -> + SwapUpdatedJob( + applicationContext, + this, + payload, + logger, + ) - MESSAGE_TYPE_LNURL_PAY_INFO -> LnurlPayInfoJob( - applicationContext, - this, - payload, - logger - ) + MESSAGE_TYPE_LNURL_PAY_INFO -> + LnurlPayInfoJob( + applicationContext, + this, + payload, + logger, + ) - MESSAGE_TYPE_LNURL_PAY_INVOICE -> LnurlPayInvoiceJob( - applicationContext, - this, - payload, - logger - ) + MESSAGE_TYPE_LNURL_PAY_INVOICE -> + LnurlPayInvoiceJob( + applicationContext, + this, + payload, + logger, + ) else -> null } } } - } - private fun launchSdkConnection(connectRequest: ConnectRequest, job: Job) { + private fun launchSdkConnection( + connectRequest: ConnectRequest, + job: Job, + ) { val sdkListener = this - serviceScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e -> - logger.log(TAG, "Breez Liquid SDK connection failed $e", "ERROR") - delayedShutdown() - }) { + serviceScope.launch( + Dispatchers.IO + + CoroutineExceptionHandler { _, e -> + logger.log(TAG, "Breez Liquid SDK connection failed $e", "ERROR") + delayedShutdown() + }, + ) { liquidSDK ?: run { liquidSDK = connectSDK(connectRequest, sdkListener, logger) } diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/Message.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/Message.kt index fc609e6..4f54cac 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/Message.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/Message.kt @@ -6,14 +6,18 @@ import android.os.Parcel import android.os.Parcelable import breez_sdk_liquid_notification.Constants.EXTRA_REMOTE_MESSAGE -data class Message(val type: String?, val payload: String?) : Parcelable { +data class Message( + val type: String?, + val payload: String?, +) : Parcelable { constructor(parcel: Parcel) : this(parcel.readString(), parcel.readString()) - override fun describeContents(): Int { - return 0 - } + override fun describeContents(): Int = 0 - override fun writeToParcel(parcel: Parcel, flags: Int) { + override fun writeToParcel( + parcel: Parcel, + flags: Int, + ) { parcel.writeString(type) parcel.writeString(payload) } @@ -22,19 +26,19 @@ data class Message(val type: String?, val payload: String?) : Parcelable { @Suppress("DEPRECATION") fun createFromIntent(intent: Intent?): Message? { return intent?.let { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) it.getParcelableExtra( - EXTRA_REMOTE_MESSAGE, - Message::class.java - ) else it.getParcelableExtra(EXTRA_REMOTE_MESSAGE) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + it.getParcelableExtra( + EXTRA_REMOTE_MESSAGE, + Message::class.java, + ) + } else { + it.getParcelableExtra(EXTRA_REMOTE_MESSAGE) + } } } - override fun createFromParcel(parcel: Parcel): Message { - return Message(parcel) - } + override fun createFromParcel(parcel: Parcel): Message = Message(parcel) - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } + override fun newArray(size: Int): Array = arrayOfNulls(size) } } diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/MessagingService.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/MessagingService.kt index 76e481a..f4ec08c 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/MessagingService.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/MessagingService.kt @@ -21,14 +21,21 @@ interface MessagingService { /** Check if the foreground service is needed depending on the * message type and foreground state of the application. */ - fun startServiceIfNeeded(context: Context, message: Message) { + fun startServiceIfNeeded( + context: Context, + message: Message, + ) { val notificationManager = getNotificationManager(context) - val isServiceNeeded = when (message.type) { - MESSAGE_TYPE_SWAP_UPDATED -> !isAppForeground(context) - else -> true + val isServiceNeeded = + when (message.type) { + MESSAGE_TYPE_SWAP_UPDATED -> !isAppForeground(context) + else -> true + } + if (notificationManager != null && isServiceNeeded) { + startForegroundService(message) + } else { + Log.w(TAG, "Ignoring message ${message.type}: ${message.payload}") } - if (notificationManager != null && isServiceNeeded) startForegroundService(message) - else Log.w(TAG, "Ignoring message ${message.type}: ${message.payload}") } /** Basic implementation to check if the application is in the foreground */ diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/NotificationHelper.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/NotificationHelper.kt index f4987f8..973e375 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/NotificationHelper.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/NotificationHelper.kt @@ -17,37 +17,38 @@ import androidx.annotation.RequiresApi import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import breez_sdk_liquid_notification.Constants.DEFAULT_DISMISSIBLE_NOTIFICATION_CHANNEL_DESCRIPTION +import breez_sdk_liquid_notification.Constants.DEFAULT_DISMISSIBLE_NOTIFICATION_CHANNEL_NAME +import breez_sdk_liquid_notification.Constants.DEFAULT_DISMISSIBLE_WORKGROUP_DESCRIPTION +import breez_sdk_liquid_notification.Constants.DEFAULT_DISMISSIBLE_WORKGROUP_NAME import breez_sdk_liquid_notification.Constants.DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION import breez_sdk_liquid_notification.Constants.DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME import breez_sdk_liquid_notification.Constants.DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_TITLE -import breez_sdk_liquid_notification.Constants.DEFAULT_LNURL_PAY_NOTIFICATION_CHANNEL_DESCRIPTION -import breez_sdk_liquid_notification.Constants.DEFAULT_LNURL_PAY_NOTIFICATION_CHANNEL_NAME -import breez_sdk_liquid_notification.Constants.DEFAULT_LNURL_PAY_WORKGROUP_DESCRIPTION -import breez_sdk_liquid_notification.Constants.DEFAULT_LNURL_PAY_WORKGROUP_NAME import breez_sdk_liquid_notification.Constants.DEFAULT_NOTIFICATION_COLOR -import breez_sdk_liquid_notification.Constants.DEFAULT_SWAP_UPDATED_NOTIFICATION_CHANNEL_DESCRIPTION -import breez_sdk_liquid_notification.Constants.DEFAULT_SWAP_UPDATED_NOTIFICATION_CHANNEL_NAME -import breez_sdk_liquid_notification.Constants.DEFAULT_SWAP_UPDATED_WORKGROUP_DESCRIPTION -import breez_sdk_liquid_notification.Constants.DEFAULT_SWAP_UPDATED_WORKGROUP_NAME +import breez_sdk_liquid_notification.Constants.DEFAULT_REPLACEABLE_NOTIFICATION_CHANNEL_DESCRIPTION +import breez_sdk_liquid_notification.Constants.DEFAULT_REPLACEABLE_NOTIFICATION_CHANNEL_NAME +import breez_sdk_liquid_notification.Constants.DEFAULT_REPLACEABLE_WORKGROUP_DESCRIPTION +import breez_sdk_liquid_notification.Constants.DEFAULT_REPLACEABLE_WORKGROUP_NAME +import breez_sdk_liquid_notification.Constants.DISMISSIBLE_NOTIFICATION_CHANNEL_DESCRIPTION +import breez_sdk_liquid_notification.Constants.DISMISSIBLE_NOTIFICATION_CHANNEL_NAME +import breez_sdk_liquid_notification.Constants.DISMISSIBLE_WORKGROUP_DESCRIPTION +import breez_sdk_liquid_notification.Constants.DISMISSIBLE_WORKGROUP_ID +import breez_sdk_liquid_notification.Constants.DISMISSIBLE_WORKGROUP_NAME import breez_sdk_liquid_notification.Constants.FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION import breez_sdk_liquid_notification.Constants.FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME import breez_sdk_liquid_notification.Constants.FOREGROUND_SERVICE_NOTIFICATION_TITLE -import breez_sdk_liquid_notification.Constants.LNURL_PAY_NOTIFICATION_CHANNEL_DESCRIPTION -import breez_sdk_liquid_notification.Constants.LNURL_PAY_NOTIFICATION_CHANNEL_NAME -import breez_sdk_liquid_notification.Constants.LNURL_PAY_WORKGROUP_DESCRIPTION -import breez_sdk_liquid_notification.Constants.LNURL_PAY_WORKGROUP_ID -import breez_sdk_liquid_notification.Constants.LNURL_PAY_WORKGROUP_NAME -import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_SWAP_UPDATED +import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_DISMISSIBLE import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_FOREGROUND_SERVICE -import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_LNURL_PAY +import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_REPLACEABLE import breez_sdk_liquid_notification.Constants.NOTIFICATION_COLOR import breez_sdk_liquid_notification.Constants.NOTIFICATION_ICON import breez_sdk_liquid_notification.Constants.NOTIFICATION_ID_FOREGROUND_SERVICE -import breez_sdk_liquid_notification.Constants.SWAP_UPDATED_NOTIFICATION_CHANNEL_DESCRIPTION -import breez_sdk_liquid_notification.Constants.SWAP_UPDATED_NOTIFICATION_CHANNEL_NAME -import breez_sdk_liquid_notification.Constants.SWAP_UPDATED_WORKGROUP_DESCRIPTION -import breez_sdk_liquid_notification.Constants.SWAP_UPDATED_WORKGROUP_ID -import breez_sdk_liquid_notification.Constants.SWAP_UPDATED_WORKGROUP_NAME +import breez_sdk_liquid_notification.Constants.NOTIFICATION_ID_REPLACEABLE +import breez_sdk_liquid_notification.Constants.REPLACEABLE_NOTIFICATION_CHANNEL_DESCRIPTION +import breez_sdk_liquid_notification.Constants.REPLACEABLE_NOTIFICATION_CHANNEL_NAME +import breez_sdk_liquid_notification.Constants.REPLACEABLE_WORKGROUP_DESCRIPTION +import breez_sdk_liquid_notification.Constants.REPLACEABLE_WORKGROUP_ID +import breez_sdk_liquid_notification.Constants.REPLACEABLE_WORKGROUP_NAME import breez_sdk_liquid_notification.ResourceHelper.Companion.getColor import breez_sdk_liquid_notification.ResourceHelper.Companion.getDrawable import breez_sdk_liquid_notification.ResourceHelper.Companion.getString @@ -66,7 +67,7 @@ class NotificationHelper { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) - as NotificationManager + as NotificationManager if (notificationManager.areNotificationsEnabled()) { return notificationManager } @@ -82,10 +83,11 @@ class NotificationHelper { groupDescription: String, ) { getNotificationManager(context)?.also { manager -> - val channelGroup = NotificationChannelGroup( - groupId, - groupName, - ) + val channelGroup = + NotificationChannelGroup( + groupId, + groupName, + ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { channelGroup.description = groupDescription @@ -106,21 +108,25 @@ class NotificationHelper { ) { getNotificationManager(context)?.also { manager -> val applicationId = context.applicationContext.packageName - val notificationChannel = NotificationChannel( - "${applicationId}.${channelId}", - channelName, - importance - ).apply { - description = channelDescription - group = groupId - } + val notificationChannel = + NotificationChannel( + "$applicationId.$channelId", + channelName, + importance, + ).apply { + description = channelDescription + group = groupId + } manager.createNotificationChannel(notificationChannel) } } @SuppressLint("NewApi") - fun registerNotificationChannels(context: Context, defaultClickAction: String? = null) { + fun registerNotificationChannels( + context: Context, + defaultClickAction: String? = null, + ) { this.defaultClickAction = defaultClickAction getNotificationManager(context)?.also { manager -> @@ -136,59 +142,65 @@ class NotificationHelper { notificationManager: NotificationManager, ) { val applicationId = context.applicationContext.packageName - val foregroundServiceNotificationChannel = NotificationChannel( - "${applicationId}.${NOTIFICATION_CHANNEL_FOREGROUND_SERVICE}", - getString( - context, - FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME, - DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME - ), - NotificationManager.IMPORTANCE_LOW - ).apply { - description = getString( - context, - FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION, - DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION - ) - } - val lnurlPayNotificationChannel = NotificationChannel( - "${applicationId}.${NOTIFICATION_CHANNEL_LNURL_PAY}", - getString( - context, - LNURL_PAY_NOTIFICATION_CHANNEL_NAME, - DEFAULT_LNURL_PAY_NOTIFICATION_CHANNEL_NAME - ), - NotificationManager.IMPORTANCE_DEFAULT - ).apply { - description = getString( - context, - LNURL_PAY_NOTIFICATION_CHANNEL_DESCRIPTION, - DEFAULT_LNURL_PAY_NOTIFICATION_CHANNEL_DESCRIPTION - ) - group = LNURL_PAY_WORKGROUP_ID - } - val swapTxConfirmedNotificationChannel = NotificationChannel( - "${applicationId}.${NOTIFICATION_CHANNEL_SWAP_UPDATED}", - getString( - context, - SWAP_UPDATED_NOTIFICATION_CHANNEL_NAME, - DEFAULT_SWAP_UPDATED_NOTIFICATION_CHANNEL_NAME - ), - NotificationManager.IMPORTANCE_DEFAULT - ).apply { - description = getString( - context, - SWAP_UPDATED_NOTIFICATION_CHANNEL_DESCRIPTION, - DEFAULT_SWAP_UPDATED_NOTIFICATION_CHANNEL_DESCRIPTION - ) - group = SWAP_UPDATED_WORKGROUP_ID - } + val foregroundServiceNotificationChannel = + NotificationChannel( + "$applicationId.${NOTIFICATION_CHANNEL_FOREGROUND_SERVICE}", + getString( + context, + FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME, + DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME, + ), + NotificationManager.IMPORTANCE_LOW, + ).apply { + description = + getString( + context, + FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION, + DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION, + ) + } + val replaceableNotificationChannel = + NotificationChannel( + "$applicationId.${NOTIFICATION_CHANNEL_REPLACEABLE}", + getString( + context, + DISMISSIBLE_NOTIFICATION_CHANNEL_NAME, + DEFAULT_DISMISSIBLE_NOTIFICATION_CHANNEL_NAME, + ), + NotificationManager.IMPORTANCE_DEFAULT, + ).apply { + description = + getString( + context, + DISMISSIBLE_NOTIFICATION_CHANNEL_DESCRIPTION, + DEFAULT_DISMISSIBLE_NOTIFICATION_CHANNEL_DESCRIPTION, + ) + group = REPLACEABLE_WORKGROUP_ID + } + val dismissibleNotificationChannel = + NotificationChannel( + "$applicationId.${NOTIFICATION_CHANNEL_DISMISSIBLE}", + getString( + context, + DISMISSIBLE_NOTIFICATION_CHANNEL_NAME, + DEFAULT_DISMISSIBLE_NOTIFICATION_CHANNEL_NAME, + ), + NotificationManager.IMPORTANCE_DEFAULT, + ).apply { + description = + getString( + context, + DISMISSIBLE_NOTIFICATION_CHANNEL_DESCRIPTION, + DEFAULT_DISMISSIBLE_NOTIFICATION_CHANNEL_DESCRIPTION, + ) + group = DISMISSIBLE_WORKGROUP_ID + } notificationManager.createNotificationChannels( listOf( foregroundServiceNotificationChannel, - lnurlPayNotificationChannel, - swapTxConfirmedNotificationChannel - ) + replaceableNotificationChannel, + dismissibleNotificationChannel, + ), ) } @@ -197,40 +209,44 @@ class NotificationHelper { context: Context, notificationManager: NotificationManager, ) { - val lnurlPayNotificationChannelGroup = NotificationChannelGroup( - LNURL_PAY_WORKGROUP_ID, - getString( - context, - LNURL_PAY_WORKGROUP_NAME, - DEFAULT_LNURL_PAY_WORKGROUP_NAME - ), - ) - val swapTxConfirmedNotificationChannelGroup = NotificationChannelGroup( - SWAP_UPDATED_WORKGROUP_ID, - getString( - context, - SWAP_UPDATED_WORKGROUP_NAME, - DEFAULT_SWAP_UPDATED_WORKGROUP_NAME - ), - ) + val replaceableNotificationChannelGroup = + NotificationChannelGroup( + REPLACEABLE_WORKGROUP_ID, + getString( + context, + REPLACEABLE_WORKGROUP_NAME, + DEFAULT_REPLACEABLE_WORKGROUP_NAME, + ), + ) + val dismissibleNotificationChannelGroup = + NotificationChannelGroup( + DISMISSIBLE_WORKGROUP_ID, + getString( + context, + DISMISSIBLE_WORKGROUP_NAME, + DEFAULT_DISMISSIBLE_WORKGROUP_NAME, + ), + ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - lnurlPayNotificationChannelGroup.description = getString( - context, - LNURL_PAY_WORKGROUP_DESCRIPTION, - DEFAULT_LNURL_PAY_WORKGROUP_DESCRIPTION - ) - swapTxConfirmedNotificationChannelGroup.description = getString( - context, - SWAP_UPDATED_WORKGROUP_DESCRIPTION, - DEFAULT_SWAP_UPDATED_WORKGROUP_DESCRIPTION - ) + replaceableNotificationChannelGroup.description = + getString( + context, + REPLACEABLE_WORKGROUP_DESCRIPTION, + DEFAULT_REPLACEABLE_WORKGROUP_DESCRIPTION, + ) + dismissibleNotificationChannelGroup.description = + getString( + context, + DISMISSIBLE_WORKGROUP_DESCRIPTION, + DEFAULT_DISMISSIBLE_WORKGROUP_DESCRIPTION, + ) } notificationManager.createNotificationChannelGroups( listOf( - lnurlPayNotificationChannelGroup, - swapTxConfirmedNotificationChannelGroup - ) + replaceableNotificationChannelGroup, + dismissibleNotificationChannelGroup, + ), ) } @@ -240,43 +256,55 @@ class NotificationHelper { getColor( context, NOTIFICATION_COLOR, - DEFAULT_NOTIFICATION_COLOR + DEFAULT_NOTIFICATION_COLOR, ) - return NotificationCompat.Builder( - context, - "${context.applicationInfo.packageName}.$NOTIFICATION_CHANNEL_FOREGROUND_SERVICE" - ) - .apply { + return NotificationCompat + .Builder( + context, + "${context.applicationInfo.packageName}.$NOTIFICATION_CHANNEL_FOREGROUND_SERVICE", + ).apply { setContentTitle( getString( context, FOREGROUND_SERVICE_NOTIFICATION_TITLE, - DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_TITLE - ) + DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_TITLE, + ), ) setSmallIcon( getDrawable( context, NOTIFICATION_ICON, - android.R.drawable.sym_def_app_icon - ) + android.R.drawable.sym_def_app_icon, + ), ) setColorized(true) setOngoing(true) color = notificationColor - }.build().also { + }.build() + .also { if (ActivityCompat.checkSelfPermission( context, - Manifest.permission.POST_NOTIFICATIONS + Manifest.permission.POST_NOTIFICATIONS, ) == PackageManager.PERMISSION_GRANTED ) { - NotificationManagerCompat.from(context) + NotificationManagerCompat + .from(context) .notify(NOTIFICATION_ID_FOREGROUND_SERVICE, it) } } } + @SuppressLint("NewApi") + fun cancelNotification( + context: Context, + notificationID: Int, + ) { + getNotificationManager(context)?.also { manager -> + manager.cancel(notificationID) + } + } + @SuppressLint("MissingPermission") fun notifyChannel( context: Context, @@ -285,12 +313,19 @@ class NotificationHelper { contentText: String? = null, clickAction: String? = defaultClickAction, ): Notification { - val notificationID: Int = System.currentTimeMillis().toInt() / 1000 + val notificationID: Int = + if (channelId == + NOTIFICATION_CHANNEL_DISMISSIBLE + ) { + System.currentTimeMillis().toInt() / 1000 + } else { + NOTIFICATION_ID_REPLACEABLE + } val notificationColor = getColor( context, NOTIFICATION_COLOR, - DEFAULT_NOTIFICATION_COLOR + DEFAULT_NOTIFICATION_COLOR, ) val notificationIntent = @@ -298,39 +333,52 @@ class NotificationHelper { notificationIntent.putExtra("click_action", clickAction) val flags = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT - val approvePendingIntent = PendingIntent.getActivity( - context, - 0, - notificationIntent, - flags - ) + if (Build.VERSION.SDK_INT >= + Build.VERSION_CODES.S + ) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + val approvePendingIntent = + PendingIntent.getActivity( + context, + 0, + notificationIntent, + flags, + ) val buttonTitle = "Open" - val notificationAction = NotificationCompat.Action.Builder( - android.R.drawable.ic_delete, - buttonTitle, - approvePendingIntent - ).build() + val notificationAction = + NotificationCompat.Action + .Builder( + android.R.drawable.ic_delete, + buttonTitle, + approvePendingIntent, + ).build() - val contentIntent = TaskStackBuilder.create(context).run { - addNextIntentWithParentStack(notificationIntent) - approvePendingIntent - } + val contentIntent = + TaskStackBuilder.create(context).run { + addNextIntentWithParentStack(notificationIntent) + approvePendingIntent + } - return NotificationCompat.Builder( - context, - "${context.applicationInfo.packageName}.${channelId}" - ) - .apply { + // Cancel any current replaceable notification + cancelNotification(context, NOTIFICATION_ID_REPLACEABLE) + + return NotificationCompat + .Builder( + context, + "${context.applicationInfo.packageName}.$channelId", + ).apply { setContentTitle(contentTitle) setContentText(contentText) setSmallIcon( getDrawable( context, NOTIFICATION_ICON, - android.R.drawable.sym_def_app_icon - ) + android.R.drawable.sym_def_app_icon, + ), ) setContentIntent(contentIntent) addAction(notificationAction) @@ -338,10 +386,11 @@ class NotificationHelper { // Dismiss on click setOngoing(false) setAutoCancel(true) - }.build().also { + }.build() + .also { if (ActivityCompat.checkSelfPermission( context, - Manifest.permission.POST_NOTIFICATIONS + Manifest.permission.POST_NOTIFICATIONS, ) == PackageManager.PERMISSION_GRANTED ) { // Required for notification to persist after work is complete @@ -349,11 +398,12 @@ class NotificationHelper { delay(200) if (ActivityCompat.checkSelfPermission( context, - Manifest.permission.POST_NOTIFICATIONS + Manifest.permission.POST_NOTIFICATIONS, ) == PackageManager.PERMISSION_GRANTED ) { // Use notificationID - NotificationManagerCompat.from(context) + NotificationManagerCompat + .from(context) .notify(notificationID, it) } } diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ResourceHelper.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ResourceHelper.kt index 298f8b8..720c346 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ResourceHelper.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ResourceHelper.kt @@ -12,38 +12,44 @@ import android.os.Bundle import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat - class ResourceHelper { companion object { private const val ILLEGAL_RESOURCE_ID = 0 - private fun getBundle(context: Context): Bundle? { - return try { + private fun getBundle(context: Context): Bundle? = + try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.packageManager.getApplicationInfo( - context.packageName, - PackageManager.ApplicationInfoFlags.of(0) - ).metaData + context.packageManager + .getApplicationInfo( + context.packageName, + PackageManager.ApplicationInfoFlags.of(0), + ).metaData } else { @Suppress("DEPRECATION") context.packageManager .getApplicationInfo( context.packageName, - PackageManager.GET_META_DATA + PackageManager.GET_META_DATA, ).metaData } } catch (_: NameNotFoundException) { null } - } @SuppressLint("DiscouragedApi") - private fun getResourceId(context: Context, name: String, defType: String): Int? { - return context.resources.getIdentifier(name, defType, context.packageName) + private fun getResourceId( + context: Context, + name: String, + defType: String, + ): Int? = + context.resources + .getIdentifier(name, defType, context.packageName) .takeIf { it != ILLEGAL_RESOURCE_ID } - } - private fun isDrawableValid(context: Context, resourceId: Int): Boolean { + private fun isDrawableValid( + context: Context, + resourceId: Int, + ): Boolean { if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O) { return true } @@ -57,21 +63,29 @@ class ResourceHelper { } } - fun getColor(context: Context, name: String, fallback: String): Int { + fun getColor( + context: Context, + name: String, + fallback: String, + ): Int { val color = getResourceId(context, name, "color")?.let { ContextCompat.getColor(context, it) } ?: run { getBundle(context)?.getInt(name, 0) } return color.takeUnless { it == 0 } ?: Color.parseColor(fallback) } - fun getDrawable(context: Context, name: String, fallback: Int): Int { + fun getDrawable( + context: Context, + name: String, + fallback: Int, + ): Int { val id = getResourceId(context, name, "drawable")?.takeIf { isDrawableValid(context, it) } ?: run { getResourceId(context, name, "mipmap")?.takeIf { isDrawableValid( context, - it + it, ) } } @@ -79,16 +93,22 @@ class ResourceHelper { return id ?: fallback } - fun getString(context: Context, name: String, fallback: String): String { - return getString(context, name, null, fallback) - } + fun getString( + context: Context, + name: String, + fallback: String, + ): String = getString(context, name, null, fallback) fun getString( - context: Context, name: String, validateContains: String?, fallback: String + context: Context, + name: String, + validateContains: String?, + fallback: String, ): String { - val str = getResourceId(context, name, "string")?.let { context.getString(it) } ?: run { - getBundle(context)?.getString(name, fallback) ?: run { fallback } - } + val str = + getResourceId(context, name, "string")?.let { context.getString(it) } ?: run { + getBundle(context)?.getString(name, fallback) ?: run { fallback } + } return if (validateContains == null || str.contains(validateContains)) str else fallback } diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ServiceLogger.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ServiceLogger.kt index 98f8b27..7c37852 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ServiceLogger.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ServiceLogger.kt @@ -4,10 +4,16 @@ import android.util.Log import breez_sdk_liquid.LogEntry import breez_sdk_liquid.Logger -class ServiceLogger(private val logger: Logger?) { +class ServiceLogger( + private val logger: Logger?, +) { constructor() : this(null) - fun log(tag: String, message: String, level: String) { + fun log( + tag: String, + message: String, + level: String, + ) { logger?.log(LogEntry(message, level)) ?: when (level) { "ERROR" -> Log.e(tag, message) "WARN" -> Log.w(tag, message) @@ -17,4 +23,4 @@ class ServiceLogger(private val logger: Logger?) { else -> {} } } -} \ No newline at end of file +} diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/Job.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/Job.kt index d8e318c..fea0651 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/Job.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/Job.kt @@ -9,7 +9,7 @@ interface Job : EventListener { */ fun start(liquidSDK: BindingLiquidSdk) - /** When the short service timeout is reached it calls `onShutdown` + /** When the short service timeout is reached it calls `onShutdown` * to cleanup the job. */ fun onShutdown() diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/LnurlPay.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/LnurlPay.kt index c17d537..4f4a968 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/LnurlPay.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/LnurlPay.kt @@ -55,4 +55,6 @@ interface LnurlPayJob : Job { } } -class InvalidLnurlPayException(message: String) : Exception(message) +class InvalidLnurlPayException( + message: String, +) : Exception(message) diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/LnurlPayInfo.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/LnurlPayInfo.kt index 4a59162..b5bf463 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/LnurlPayInfo.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/LnurlPayInfo.kt @@ -8,7 +8,7 @@ import breez_sdk_liquid_notification.Constants.DEFAULT_LNURL_PAY_NOTIFICATION_FA import breez_sdk_liquid_notification.Constants.LNURL_PAY_INFO_NOTIFICATION_TITLE import breez_sdk_liquid_notification.Constants.LNURL_PAY_METADATA_PLAIN_TEXT import breez_sdk_liquid_notification.Constants.LNURL_PAY_NOTIFICATION_FAILURE_TITLE -import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_LNURL_PAY +import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_REPLACEABLE import breez_sdk_liquid_notification.NotificationHelper.Companion.notifyChannel import breez_sdk_liquid_notification.ResourceHelper.Companion.getString import breez_sdk_liquid_notification.SdkForegroundService @@ -59,24 +59,28 @@ class LnurlPayInfoJob( throw InvalidLnurlPayException("Minimum sendable amount is invalid") } // Format the response - val plainTextMetadata = getString( - context, LNURL_PAY_METADATA_PLAIN_TEXT, DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT - ) - val response = LnurlPayInfoResponse( - request.callbackURL, - maxSendableMsat, - minSendableMsat, - "[[\"text/plain\",\"$plainTextMetadata\"]]", - "payRequest", - ) + val plainTextMetadata = + getString( + context, + LNURL_PAY_METADATA_PLAIN_TEXT, + DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT, + ) + val response = + LnurlPayInfoResponse( + request.callbackURL, + maxSendableMsat, + minSendableMsat, + "[[\"text/plain\",\"$plainTextMetadata\"]]", + "payRequest", + ) val success = replyServer(Json.encodeToString(response), request.replyURL) notifyChannel( context, - NOTIFICATION_CHANNEL_LNURL_PAY, + NOTIFICATION_CHANNEL_REPLACEABLE, getString( context, if (success) LNURL_PAY_INFO_NOTIFICATION_TITLE else LNURL_PAY_NOTIFICATION_FAILURE_TITLE, - if (success) DEFAULT_LNURL_PAY_INFO_NOTIFICATION_TITLE else DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE + if (success) DEFAULT_LNURL_PAY_INFO_NOTIFICATION_TITLE else DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE, ), ) } catch (e: Exception) { @@ -86,11 +90,11 @@ class LnurlPayInfoJob( } notifyChannel( context, - NOTIFICATION_CHANNEL_LNURL_PAY, + NOTIFICATION_CHANNEL_REPLACEABLE, getString( context, LNURL_PAY_NOTIFICATION_FAILURE_TITLE, - DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE + DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE, ), ) } diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/LnurlPayInvoice.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/LnurlPayInvoice.kt index 2ab682a..00c976f 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/LnurlPayInvoice.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/LnurlPayInvoice.kt @@ -12,7 +12,7 @@ import breez_sdk_liquid_notification.Constants.DEFAULT_LNURL_PAY_NOTIFICATION_FA import breez_sdk_liquid_notification.Constants.LNURL_PAY_INVOICE_NOTIFICATION_TITLE import breez_sdk_liquid_notification.Constants.LNURL_PAY_METADATA_PLAIN_TEXT import breez_sdk_liquid_notification.Constants.LNURL_PAY_NOTIFICATION_FAILURE_TITLE -import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_LNURL_PAY +import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_REPLACEABLE import breez_sdk_liquid_notification.NotificationHelper.Companion.notifyChannel import breez_sdk_liquid_notification.ResourceHelper.Companion.getString import breez_sdk_liquid_notification.SdkForegroundService @@ -57,21 +57,24 @@ class LnurlPayInvoiceJob( if (amountSat < limits.receive.minSat || amountSat > limits.receive.maxSat) { throw InvalidLnurlPayException("Invalid amount requested ${request.amount}") } - val plainTextMetadata = getString( - context, - LNURL_PAY_METADATA_PLAIN_TEXT, - DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT - ) - val prepareReceivePaymentRes = liquidSDK.prepareReceivePayment( - PrepareReceiveRequest(PaymentMethod.LIGHTNING, ReceiveAmount.Bitcoin(amountSat)) - ) - val receivePaymentResponse = liquidSDK.receivePayment( - ReceivePaymentRequest( - prepareReceivePaymentRes, - description = "[[\"text/plain\",\"$plainTextMetadata\"]]", - useDescriptionHash = true + val plainTextMetadata = + getString( + context, + LNURL_PAY_METADATA_PLAIN_TEXT, + DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT, + ) + val prepareReceivePaymentRes = + liquidSDK.prepareReceivePayment( + PrepareReceiveRequest(PaymentMethod.LIGHTNING, ReceiveAmount.Bitcoin(amountSat)), + ) + val receivePaymentResponse = + liquidSDK.receivePayment( + ReceivePaymentRequest( + prepareReceivePaymentRes, + description = "[[\"text/plain\",\"$plainTextMetadata\"]]", + useDescriptionHash = true, + ), ) - ) val response = LnurlPayInvoiceResponse( receivePaymentResponse.destination, @@ -80,11 +83,11 @@ class LnurlPayInvoiceJob( val success = replyServer(Json.encodeToString(response), request.replyURL) notifyChannel( context, - NOTIFICATION_CHANNEL_LNURL_PAY, + NOTIFICATION_CHANNEL_REPLACEABLE, getString( context, if (success) LNURL_PAY_INVOICE_NOTIFICATION_TITLE else LNURL_PAY_NOTIFICATION_FAILURE_TITLE, - if (success) DEFAULT_LNURL_PAY_INVOICE_NOTIFICATION_TITLE else DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE + if (success) DEFAULT_LNURL_PAY_INVOICE_NOTIFICATION_TITLE else DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE, ), ) } catch (e: Exception) { @@ -94,11 +97,11 @@ class LnurlPayInvoiceJob( } notifyChannel( context, - NOTIFICATION_CHANNEL_LNURL_PAY, + NOTIFICATION_CHANNEL_REPLACEABLE, getString( context, LNURL_PAY_NOTIFICATION_FAILURE_TITLE, - DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE + DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE, ), ) } diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt index 516fa48..55ccb8a 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt @@ -16,7 +16,7 @@ import breez_sdk_liquid_notification.Constants.DEFAULT_PAYMENT_WAITING_FEE_ACCEP import breez_sdk_liquid_notification.Constants.DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE import breez_sdk_liquid_notification.Constants.DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT import breez_sdk_liquid_notification.Constants.DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE -import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_SWAP_UPDATED +import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_DISMISSIBLE import breez_sdk_liquid_notification.Constants.PAYMENT_RECEIVED_NOTIFICATION_TEXT import breez_sdk_liquid_notification.Constants.PAYMENT_RECEIVED_NOTIFICATION_TITLE import breez_sdk_liquid_notification.Constants.PAYMENT_SENT_NOTIFICATION_TEXT @@ -127,7 +127,7 @@ class SwapUpdatedJob( is SdkEvent.PaymentWaitingFeeAcceptance -> handlePaymentWaitingFeeAcceptance(e.details) else -> { - logger.log(TAG, "Received event: ${e}", "TRACE") + logger.log(TAG, "Received event: $e", "TRACE") } } } @@ -137,18 +137,18 @@ class SwapUpdatedJob( } private fun hashId(id: String): String = - MessageDigest.getInstance("SHA-256") + MessageDigest + .getInstance("SHA-256") .digest(id.toByteArray()) .fold(StringBuilder()) { sb, it -> sb.append("%02x".format(it)) } .toString() - private fun getSwapId(details: PaymentDetails?): String? { - return when (details) { + private fun getSwapId(details: PaymentDetails?): String? = + when (details) { is PaymentDetails.Bitcoin -> details.swapId is PaymentDetails.Lightning -> details.swapId else -> null } - } private fun paymentClaimIsBroadcasted(details: PaymentDetails?): Boolean { return when (details) { @@ -166,7 +166,7 @@ class SwapUpdatedJob( logger.log( TAG, "Received payment event: ${this.swapIdHash} ${payment.status}", - "TRACE" + "TRACE", ) notifySuccess(payment) } @@ -181,7 +181,7 @@ class SwapUpdatedJob( logger.log( TAG, "Payment waiting fee acceptance: ${this.swapIdHash}", - "TRACE" + "TRACE", ) notifyPaymentWaitingFeeAcceptance(payment) } @@ -194,20 +194,21 @@ class SwapUpdatedJob( val received = payment.paymentType == PaymentType.RECEIVE notifyChannel( context, - NOTIFICATION_CHANNEL_SWAP_UPDATED, + NOTIFICATION_CHANNEL_DISMISSIBLE, getString( context, if (received) PAYMENT_RECEIVED_NOTIFICATION_TITLE else PAYMENT_SENT_NOTIFICATION_TITLE, - if (received) DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TITLE else DEFAULT_PAYMENT_SENT_NOTIFICATION_TITLE + if (received) DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TITLE else DEFAULT_PAYMENT_SENT_NOTIFICATION_TITLE, ), String.format( getString( context, if (received) PAYMENT_RECEIVED_NOTIFICATION_TEXT else PAYMENT_SENT_NOTIFICATION_TEXT, "%d", - if (received) DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TEXT else DEFAULT_PAYMENT_SENT_NOTIFICATION_TEXT - ), payment.amountSat.toLong() - ) + if (received) DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TEXT else DEFAULT_PAYMENT_SENT_NOTIFICATION_TEXT, + ), + payment.amountSat.toLong(), + ), ) this.notified = true fgService.onFinished(this) @@ -219,17 +220,17 @@ class SwapUpdatedJob( logger.log(TAG, "Payment with swap ID ${getSwapId(payment.details)} requires fee acceptance", "INFO") notifyChannel( context, - NOTIFICATION_CHANNEL_SWAP_UPDATED, + NOTIFICATION_CHANNEL_DISMISSIBLE, getString( context, PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE, - DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE + DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE, ), getString( context, PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT, - DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT - ) + DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT, + ), ) this.notified = true fgService.onFinished(this) @@ -241,16 +242,16 @@ class SwapUpdatedJob( logger.log(TAG, "Swap $swapIdHash processing failed", "INFO") notifyChannel( context, - NOTIFICATION_CHANNEL_SWAP_UPDATED, + NOTIFICATION_CHANNEL_DISMISSIBLE, getString( context, SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE, - DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE + DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE, ), getString( context, SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT, - DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT + DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT, ), ) } diff --git a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Constants.swift b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Constants.swift index 5e26b54..5f76e6f 100644 --- a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Constants.swift +++ b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Constants.swift @@ -2,8 +2,8 @@ import Foundation struct Constants { // Notification Threads - static let NOTIFICATION_THREAD_LNURL_PAY = "LNURL_PAY" - static let NOTIFICATION_THREAD_SWAP_UPDATED = "SWAP_UPDATED" + static let NOTIFICATION_THREAD_DISMISSIBLE = "DISMISSIBLE" + static let NOTIFICATION_THREAD_REPLACEABLE = "REPLACEABLE" // Message Data static let MESSAGE_DATA_TYPE = "notification_type" diff --git a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/LnurlPay.swift b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/LnurlPay.swift index 77bc6b6..b838eae 100644 --- a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/LnurlPay.swift +++ b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/LnurlPay.swift @@ -33,12 +33,12 @@ class LnurlPayTask : TaskProtocol { public func onEvent(e: SdkEvent) {} func onShutdown() { - displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY) + displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE) } func replyServer(encodable: Encodable, replyURL: String) { guard let serverReplyURL = URL(string: replyURL) else { - self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY) + self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE) return } var request = URLRequest(url: serverReplyURL) @@ -48,9 +48,9 @@ class LnurlPayTask : TaskProtocol { let statusCode = (response as! HTTPURLResponse).statusCode if statusCode == 200 { - self.displayPushNotification(title: self.successNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY) + self.displayPushNotification(title: self.successNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE) } else { - self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY) + self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE) return } } @@ -68,7 +68,7 @@ class LnurlPayTask : TaskProtocol { task.resume() } let title = failNotificationTitle != nil ? failNotificationTitle! : self.failNotificationTitle - self.displayPushNotification(title: title, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY) + self.displayPushNotification(title: title, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE) } } diff --git a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/LnurlPayInfo.swift b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/LnurlPayInfo.swift index 0c4c297..3cb9698 100644 --- a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/LnurlPayInfo.swift +++ b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/LnurlPayInfo.swift @@ -37,7 +37,7 @@ class LnurlPayInfoTask : LnurlPayTask { request = try JSONDecoder().decode(LnurlInfoRequest.self, from: self.payload.data(using: .utf8)!) } catch let e { self.logger.log(tag: TAG, line: "failed to decode payload: \(e)", level: "ERROR") - self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY) + self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE) throw e } diff --git a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/LnurlPayInvoice.swift b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/LnurlPayInvoice.swift index 2143d95..b6ef6f6 100644 --- a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/LnurlPayInvoice.swift +++ b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/LnurlPayInvoice.swift @@ -31,7 +31,7 @@ class LnurlPayInvoiceTask : LnurlPayTask { request = try JSONDecoder().decode(LnurlInvoiceRequest.self, from: self.payload.data(using: .utf8)!) } catch let e { self.logger.log(tag: TAG, line: "failed to decode payload: \(e)", level: "ERROR") - self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY) + self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE) throw e } diff --git a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift index 8e6c572..e8ae7af 100644 --- a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift +++ b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift @@ -133,7 +133,7 @@ class SwapUpdatedTask : TaskProtocol { func onShutdown() { let notificationTitle = ResourceHelper.shared.getString(key: Constants.SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE, fallback: Constants.DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE) let notificationBody = ResourceHelper.shared.getString(key: Constants.SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT, fallback: Constants.DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT) - self.displayPushNotification(title: notificationTitle, body: notificationBody, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_SWAP_UPDATED) + self.displayPushNotification(title: notificationTitle, body: notificationBody, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_DISMISSIBLE) } func notifySuccess(payment: Payment) { @@ -145,7 +145,7 @@ class SwapUpdatedTask : TaskProtocol { validateContains: "%d", fallback: received ? Constants.DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TITLE: Constants.DEFAULT_PAYMENT_SENT_NOTIFICATION_TITLE) self.notified = true - self.displayPushNotification(title: String(format: notificationTitle, payment.amountSat), logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_SWAP_UPDATED) + self.displayPushNotification(title: String(format: notificationTitle, payment.amountSat), logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_DISMISSIBLE) } } @@ -159,7 +159,7 @@ class SwapUpdatedTask : TaskProtocol { key: Constants.PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT, fallback: Constants.DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT) self.notified = true - self.displayPushNotification(title: notificationTitle, body: notificationBody, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_SWAP_UPDATED) + self.displayPushNotification(title: notificationTitle, body: notificationBody, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_DISMISSIBLE) } } } diff --git a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/TaskProtocol.swift b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/TaskProtocol.swift index ed75e5b..3953d6b 100644 --- a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/TaskProtocol.swift +++ b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/TaskProtocol.swift @@ -10,6 +10,28 @@ public protocol TaskProtocol : EventListener { } extension TaskProtocol { + func removePushNotifications(threadIdentifier: String, logger: ServiceLogger) { + let semaphore = DispatchSemaphore(value: 0) + let notificationCenter = UNUserNotificationCenter.current() + + notificationCenter.getDeliveredNotifications(completionHandler: { notifications in + defer { + semaphore.signal() + } + + let removableNotifications = notifications.filter({ $0.request.content.threadIdentifier == threadIdentifier }) + guard !removableNotifications.isEmpty else { + return + } + // The call to removeDeliveredNotifications() is async in a background thread and + // needs to be complete before calling contentHandler() + notificationCenter.removeDeliveredNotifications(withIdentifiers: removableNotifications.map({ $0.request.identifier })) + logger.log(tag: "TaskProtocol", line:"removePushNotifications: \(removableNotifications.count)", level: "INFO") + }) + + semaphore.wait() + } + func displayPushNotification(title: String, body: String? = nil, logger: ServiceLogger, threadIdentifier: String? = nil) { logger.log(tag: "TaskProtocol", line:"displayPushNotification \(title)", level: "INFO") guard @@ -18,6 +40,8 @@ extension TaskProtocol { else { return } + + removePushNotifications(threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE, logger: logger) if let body = body { bestAttemptContent.body = body @@ -28,6 +52,10 @@ extension TaskProtocol { } bestAttemptContent.title = title - contentHandler(bestAttemptContent) + // The call to contentHandler() needs to be done with a slight delay otherwise + // it will be killed before its finished removing the notifications + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + contentHandler(bestAttemptContent) + } } }