Auto dismiss replaceable notifications (#711)

This commit is contained in:
Ross Savage
2025-02-04 16:53:40 +01:00
committed by GitHub
parent bf4a364c4b
commit d25598e6a4
19 changed files with 495 additions and 348 deletions

View File

@@ -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!!
}

View File

@@ -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"
}

View File

@@ -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,7 +65,8 @@ abstract class ForegroundService : SdkForegroundService, EventListener, Service(
/** Stop the service */
private val serviceTimeoutHandler = Handler(Looper.getMainLooper())
private val serviceTimeoutRunnable: Runnable = Runnable {
private val serviceTimeoutRunnable: Runnable =
Runnable {
logger.log(TAG, "Reached service timeout...", "DEBUG")
synchronized(this) {
jobs.forEach { job -> job.onShutdown() }
@@ -73,7 +76,8 @@ abstract class ForegroundService : SdkForegroundService, EventListener, Service(
}
private val shutdownHandler = Handler(Looper.getMainLooper())
private val shutdownRunnable: Runnable = Runnable {
private val shutdownRunnable: Runnable =
Runnable {
logger.log(TAG, "Reached scheduled shutdown...", "DEBUG")
shutdown()
}
@@ -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(
MESSAGE_TYPE_SWAP_UPDATED ->
SwapUpdatedJob(
applicationContext,
this,
payload,
logger
logger,
)
MESSAGE_TYPE_LNURL_PAY_INFO -> LnurlPayInfoJob(
MESSAGE_TYPE_LNURL_PAY_INFO ->
LnurlPayInfoJob(
applicationContext,
this,
payload,
logger
logger,
)
MESSAGE_TYPE_LNURL_PAY_INVOICE -> LnurlPayInvoiceJob(
MESSAGE_TYPE_LNURL_PAY_INVOICE ->
LnurlPayInvoiceJob(
applicationContext,
this,
payload,
logger
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 ->
serviceScope.launch(
Dispatchers.IO +
CoroutineExceptionHandler { _, e ->
logger.log(TAG, "Breez Liquid SDK connection failed $e", "ERROR")
delayedShutdown()
}) {
},
) {
liquidSDK ?: run {
liquidSDK = connectSDK(connectRequest, sdkListener, logger)
}

View File

@@ -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(
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
it.getParcelableExtra(
EXTRA_REMOTE_MESSAGE,
Message::class.java
) else 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<Message?> {
return arrayOfNulls(size)
}
override fun newArray(size: Int): Array<Message?> = arrayOfNulls(size)
}
}

View File

@@ -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) {
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 */

View File

@@ -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
@@ -82,7 +83,8 @@ class NotificationHelper {
groupDescription: String,
) {
getNotificationManager(context)?.also { manager ->
val channelGroup = NotificationChannelGroup(
val channelGroup =
NotificationChannelGroup(
groupId,
groupName,
)
@@ -106,10 +108,11 @@ class NotificationHelper {
) {
getNotificationManager(context)?.also { manager ->
val applicationId = context.applicationContext.packageName
val notificationChannel = NotificationChannel(
"${applicationId}.${channelId}",
val notificationChannel =
NotificationChannel(
"$applicationId.$channelId",
channelName,
importance
importance,
).apply {
description = channelDescription
group = groupId
@@ -120,7 +123,10 @@ class NotificationHelper {
}
@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}",
val foregroundServiceNotificationChannel =
NotificationChannel(
"$applicationId.${NOTIFICATION_CHANNEL_FOREGROUND_SERVICE}",
getString(
context,
FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME,
DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME
DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME,
),
NotificationManager.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_LOW,
).apply {
description = getString(
description =
getString(
context,
FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION,
DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION
DEFAULT_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION,
)
}
val lnurlPayNotificationChannel = NotificationChannel(
"${applicationId}.${NOTIFICATION_CHANNEL_LNURL_PAY}",
val replaceableNotificationChannel =
NotificationChannel(
"$applicationId.${NOTIFICATION_CHANNEL_REPLACEABLE}",
getString(
context,
LNURL_PAY_NOTIFICATION_CHANNEL_NAME,
DEFAULT_LNURL_PAY_NOTIFICATION_CHANNEL_NAME
DISMISSIBLE_NOTIFICATION_CHANNEL_NAME,
DEFAULT_DISMISSIBLE_NOTIFICATION_CHANNEL_NAME,
),
NotificationManager.IMPORTANCE_DEFAULT
NotificationManager.IMPORTANCE_DEFAULT,
).apply {
description = getString(
description =
getString(
context,
LNURL_PAY_NOTIFICATION_CHANNEL_DESCRIPTION,
DEFAULT_LNURL_PAY_NOTIFICATION_CHANNEL_DESCRIPTION
DISMISSIBLE_NOTIFICATION_CHANNEL_DESCRIPTION,
DEFAULT_DISMISSIBLE_NOTIFICATION_CHANNEL_DESCRIPTION,
)
group = LNURL_PAY_WORKGROUP_ID
group = REPLACEABLE_WORKGROUP_ID
}
val swapTxConfirmedNotificationChannel = NotificationChannel(
"${applicationId}.${NOTIFICATION_CHANNEL_SWAP_UPDATED}",
val dismissibleNotificationChannel =
NotificationChannel(
"$applicationId.${NOTIFICATION_CHANNEL_DISMISSIBLE}",
getString(
context,
SWAP_UPDATED_NOTIFICATION_CHANNEL_NAME,
DEFAULT_SWAP_UPDATED_NOTIFICATION_CHANNEL_NAME
DISMISSIBLE_NOTIFICATION_CHANNEL_NAME,
DEFAULT_DISMISSIBLE_NOTIFICATION_CHANNEL_NAME,
),
NotificationManager.IMPORTANCE_DEFAULT
NotificationManager.IMPORTANCE_DEFAULT,
).apply {
description = getString(
description =
getString(
context,
SWAP_UPDATED_NOTIFICATION_CHANNEL_DESCRIPTION,
DEFAULT_SWAP_UPDATED_NOTIFICATION_CHANNEL_DESCRIPTION
DISMISSIBLE_NOTIFICATION_CHANNEL_DESCRIPTION,
DEFAULT_DISMISSIBLE_NOTIFICATION_CHANNEL_DESCRIPTION,
)
group = SWAP_UPDATED_WORKGROUP_ID
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,
val replaceableNotificationChannelGroup =
NotificationChannelGroup(
REPLACEABLE_WORKGROUP_ID,
getString(
context,
LNURL_PAY_WORKGROUP_NAME,
DEFAULT_LNURL_PAY_WORKGROUP_NAME
REPLACEABLE_WORKGROUP_NAME,
DEFAULT_REPLACEABLE_WORKGROUP_NAME,
),
)
val swapTxConfirmedNotificationChannelGroup = NotificationChannelGroup(
SWAP_UPDATED_WORKGROUP_ID,
val dismissibleNotificationChannelGroup =
NotificationChannelGroup(
DISMISSIBLE_WORKGROUP_ID,
getString(
context,
SWAP_UPDATED_WORKGROUP_NAME,
DEFAULT_SWAP_UPDATED_WORKGROUP_NAME
DISMISSIBLE_WORKGROUP_NAME,
DEFAULT_DISMISSIBLE_WORKGROUP_NAME,
),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
lnurlPayNotificationChannelGroup.description = getString(
replaceableNotificationChannelGroup.description =
getString(
context,
LNURL_PAY_WORKGROUP_DESCRIPTION,
DEFAULT_LNURL_PAY_WORKGROUP_DESCRIPTION
REPLACEABLE_WORKGROUP_DESCRIPTION,
DEFAULT_REPLACEABLE_WORKGROUP_DESCRIPTION,
)
swapTxConfirmedNotificationChannelGroup.description = getString(
dismissibleNotificationChannelGroup.description =
getString(
context,
SWAP_UPDATED_WORKGROUP_DESCRIPTION,
DEFAULT_SWAP_UPDATED_WORKGROUP_DESCRIPTION
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(
return NotificationCompat
.Builder(
context,
"${context.applicationInfo.packageName}.$NOTIFICATION_CHANNEL_FOREGROUND_SERVICE"
)
.apply {
"${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(
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
flags,
)
val buttonTitle = "Open"
val notificationAction = NotificationCompat.Action.Builder(
val notificationAction =
NotificationCompat.Action
.Builder(
android.R.drawable.ic_delete,
buttonTitle,
approvePendingIntent
approvePendingIntent,
).build()
val contentIntent = TaskStackBuilder.create(context).run {
val contentIntent =
TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(notificationIntent)
approvePendingIntent
}
return NotificationCompat.Builder(
// Cancel any current replaceable notification
cancelNotification(context, NOTIFICATION_ID_REPLACEABLE)
return NotificationCompat
.Builder(
context,
"${context.applicationInfo.packageName}.${channelId}"
)
.apply {
"${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)
}
}

View File

@@ -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.packageManager
.getApplicationInfo(
context.packageName,
PackageManager.ApplicationInfoFlags.of(0)
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,14 +93,20 @@ 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 {
val str =
getResourceId(context, name, "string")?.let { context.getString(it) } ?: run {
getBundle(context)?.getString(name, fallback) ?: run { fallback }
}

View File

@@ -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)

View File

@@ -55,4 +55,6 @@ interface LnurlPayJob : Job {
}
}
class InvalidLnurlPayException(message: String) : Exception(message)
class InvalidLnurlPayException(
message: String,
) : Exception(message)

View File

@@ -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,10 +59,14 @@ 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 plainTextMetadata =
getString(
context,
LNURL_PAY_METADATA_PLAIN_TEXT,
DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT,
)
val response = LnurlPayInfoResponse(
val response =
LnurlPayInfoResponse(
request.callbackURL,
maxSendableMsat,
minSendableMsat,
@@ -72,11 +76,11 @@ class LnurlPayInfoJob(
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,
),
)
}

View File

@@ -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,20 +57,23 @@ class LnurlPayInvoiceJob(
if (amountSat < limits.receive.minSat || amountSat > limits.receive.maxSat) {
throw InvalidLnurlPayException("Invalid amount requested ${request.amount}")
}
val plainTextMetadata = getString(
val plainTextMetadata =
getString(
context,
LNURL_PAY_METADATA_PLAIN_TEXT,
DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT
DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT,
)
val prepareReceivePaymentRes = liquidSDK.prepareReceivePayment(
PrepareReceiveRequest(PaymentMethod.LIGHTNING, ReceiveAmount.Bitcoin(amountSat))
val prepareReceivePaymentRes =
liquidSDK.prepareReceivePayment(
PrepareReceiveRequest(PaymentMethod.LIGHTNING, ReceiveAmount.Bitcoin(amountSat)),
)
val receivePaymentResponse = liquidSDK.receivePayment(
val receivePaymentResponse =
liquidSDK.receivePayment(
ReceivePaymentRequest(
prepareReceivePaymentRes,
description = "[[\"text/plain\",\"$plainTextMetadata\"]]",
useDescriptionHash = true
)
useDescriptionHash = true,
),
)
val response =
LnurlPayInvoiceResponse(
@@ -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,
),
)
}

View File

@@ -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,
),
)
}

View File

@@ -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"

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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
@@ -19,6 +41,8 @@ extension TaskProtocol {
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
// 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)
}
}
}