Skip to content

Commit

Permalink
Stop using WorkManager on Foreground Service
Browse files Browse the repository at this point in the history
Remove isHeadless var as the service is always running in the background
Reduce foreground service notification channels importance to low
Push back shutdown by another 60 seconds
  • Loading branch information
erdemyerebasmaz committed Dec 29, 2023
1 parent 39189b1 commit 94f159b
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 227 deletions.
208 changes: 158 additions & 50 deletions android/app/src/main/java/com/cBreez/client/BreezForegroundService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,29 @@ import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Parcelable
import androidx.annotation.RequiresApi
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import breez_sdk.BlockingBreezServices
import breez_sdk.BreezEvent
import breez_sdk.EventListener
import breez_sdk.PaymentStatus
import com.cBreez.client.BreezNotificationHelper.Companion.dismissForegroundServiceNotification
import com.cBreez.client.BreezNotificationHelper.Companion.notifyForegroundService
import com.cBreez.client.BreezNotificationHelper.Companion.notifyPaymentFailed
import com.cBreez.client.BreezNotificationHelper.Companion.notifyPaymentReceived
import com.cBreez.client.BreezNotificationHelper.Companion.registerNotificationChannels
import com.cBreez.client.BreezSdkConnector.Companion.connectSDK
import com.cBreez.client.Constants.NOTIFICATION_ID_FOREGROUND_SERVICE
import com.google.firebase.messaging.RemoteMessage
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tinylog.kotlin.Logger
import java.util.concurrent.locks.ReentrantLock

class BreezForegroundService : Service() {
companion object {
Expand All @@ -25,57 +40,87 @@ class BreezForegroundService : Service() {
fun getService(): BreezForegroundService = this@BreezForegroundService
}

// SDK events listener
inner class SDKListener : EventListener {
override fun onEvent(e: BreezEvent) {
Logger.tag(TAG).info { "Received event $e" }
if (e is BreezEvent.InvoicePaid) {
val pD = e.details
Logger.tag(TAG).info {
"Received payment. Bolt11:${pD.bolt11}\nPayment Hash:${pD.paymentHash}"
}
val amountSat = (e.details.payment?.amountMsat ?: ULong.MIN_VALUE) / 1000u
if (!paymentReceivedInBackground.contains(e.details.paymentHash)) {
paymentReceivedInBackground.add(e.details.paymentHash)
notifyPaymentReceived(applicationContext, amountSat = amountSat)
}

// push back shutdown by 120s in case we'll receive more payments
shutdownHandler.removeCallbacksAndMessages(null)
shutdownHandler.postDelayed(shutdownRunnable, 120 * 1000L)
}
}
}

private var breezSDK: BlockingBreezServices? = null
private val serviceScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
private val binder = BreezNodeBinder()

/** True if the service is running headless (that is without a GUI). In that case we should show a notification */
@Volatile
private var isHeadless = true
/**
* State of the wallet, provides access to the business when started. Private so that it's not
* mutated from the outside.
*/
private val _state = MutableLiveData<NodeServiceState>(NodeServiceState.Off)
val state: LiveData<NodeServiceState>
get() = _state

/** Lock for state updates */
private val stateLock = ReentrantLock()

/** List of payments received while the app is in the background */
private val paymentReceivedInBackground = mutableListOf<String>()

override fun onCreate() {
super.onCreate()
Logger.tag(TAG).debug("Creating Breez node service...")
Logger.tag(TAG).debug { "Creating Breez node service..." }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
registerNotificationChannels(applicationContext)
}
startForeground(
NOTIFICATION_ID_FOREGROUND_SERVICE,
notifyForegroundService(applicationContext)
)
Logger.tag(TAG).debug("Breez node service created.")
Logger.tag(TAG).debug { "Breez node service created." }
}

// =========================================================== //
// SERVICE LIFECYCLE //
// =========================================================== //

override fun onBind(intent: Intent?): IBinder {
Logger.tag(TAG).debug("Binding Breez node service from intent=$intent")
// UI is binding to the service. The service is not headless anymore and we can remove the notification.
isHeadless = false
Logger.tag(TAG).debug { "Binding Breez node service from intent=$intent" }
paymentReceivedInBackground.clear()
stopForeground(STOP_FOREGROUND_REMOVE)
dismissForegroundServiceNotification(applicationContext)
return binder
}

/** When unbound, the service is running headless. */
override fun onUnbind(intent: Intent?): Boolean {
isHeadless = true
return false
}

private val shutdownHandler = Handler(Looper.getMainLooper())
private val shutdownRunnable: Runnable = Runnable {
if (isHeadless) {
Logger.tag(TAG).debug("Reached scheduled shutdown...")
Logger.tag(TAG).debug { "Reached scheduled shutdown..." }
if (paymentReceivedInBackground.isEmpty()) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
stopForeground(STOP_FOREGROUND_DETACH)
shutdown()
}
shutdown()
}

/** Shutdown the node, close connections and stop the service */
private fun shutdown() {
Logger.tag(TAG).info("Shutting down Breez node service")
Logger.tag(TAG).info { "Shutting down Breez node service" }
stopSelf()
_state.postValue(NodeServiceState.Off)
}

// =========================================================== //
Expand All @@ -86,47 +131,110 @@ class BreezForegroundService : Service() {
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Logger.tag(TAG)
.debug("Start Breez node service from intent [ intent=$intent, flag=$flags, startId=$startId ]")
getParcelable(
intent, "remote_message",
RemoteMessage::class.java
)?.let { handleNow(it) }
Logger.tag(TAG).debug {
"Start Breez node service from intent [ intent=$intent, flag=$flags, startId=$startId ]"
}
intent.getRemoteMessage()?.let {
if (it.data["notification_type"] == "payment_received") {
val paymentHash = it.data["payment_hash"]
val clickAction = it.data["click_action"]
paymentHash?.let {
startBusiness(paymentHash, clickAction)
}
}
}

shutdownHandler.removeCallbacksAndMessages(null)
shutdownHandler.postDelayed(shutdownRunnable, 60 * 1000L) // push back shutdown by 60s
if (!isHeadless) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
shutdownHandler.postDelayed(shutdownRunnable, 120 * 1000L) // push back shutdown by 120s
return START_NOT_STICKY
}

private fun handleNow(remoteMessage: RemoteMessage): Boolean {
return if (remoteMessage.data["notification_type"] == "payment_received") {
val paymentHash = remoteMessage.data["payment_hash"]
val clickAction = remoteMessage.data["click_action"]
paymentHash?.let {
JobManager.instance.startPaymentReceivedJob(
applicationContext,
paymentHash,
clickAction
)
private fun startBusiness(
paymentHash: String,
clickAction: String?,
) {
val notification = notifyForegroundService(applicationContext)
when {
breezSDK != null && _state.value is NodeServiceState.Running -> {
// NOTE: the notification will NOT be shown if the app is already running
startForeground(NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
serviceScope.launch(
Dispatchers.IO +
CoroutineExceptionHandler { _, e ->
Logger.tag(TAG).error { "Error when polling payment: $e" }
notifyPaymentFailed(applicationContext)
}
) {
Logger.tag(TAG)
.info { "Using current running node service to poll payment" }
pollPaymentHash(paymentHash, clickAction)
}
}

else -> {
startForeground(NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
Logger.tag(TAG).debug { "Starting wallet..." }
stateLock.lock()
if (_state.value != NodeServiceState.Off) {
Logger.tag(TAG).warn {
"ignore attempt to start business in state=${_state.value}"
}
return
} else {
_state.postValue(NodeServiceState.Init)
}
stateLock.unlock()
serviceScope.launch(
Dispatchers.IO +
CoroutineExceptionHandler { _, e ->
Logger.tag(TAG).error { "Error when starting node: $e" }
_state.postValue(NodeServiceState.Error(e))
shutdown()
stopForeground(STOP_FOREGROUND_REMOVE)
}
) {
Logger.tag(TAG).info {
"Starting node from service state=${_state.value?.name}"
}
breezSDK = connectSDK(applicationContext, SDKListener())
pollPaymentHash(paymentHash, clickAction)
_state.postValue(NodeServiceState.Running)
}
}
true
} else {
false
}
}

private fun <T : Parcelable?> getParcelable(
intent: Intent?,
name: String,
clazz: Class<T>,
): T? {
private suspend fun pollPaymentHash(
paymentHash: String,
clickAction: String?,
) {
// Poll for 1 minute
for (i in 1..60) {
val payment = breezSDK!!.paymentByHash(paymentHash)
if (payment?.status == PaymentStatus.COMPLETE) {
if (!paymentReceivedInBackground.contains(paymentHash)) {
paymentReceivedInBackground.add(paymentHash)
notifyPaymentReceived(
applicationContext,
clickAction,
amountSat = payment.amountMsat / 1000u
)
}
return
} else {
Logger.tag(TAG).info { "Payment w/ paymentHash: $paymentHash not received yet." }
withContext(Dispatchers.IO) { Thread.sleep(1_000) }
}
}

// If we reach here then we didn't receive for more than 1 minute
throw Exception("Payment not found before timeout")
}

private fun Intent?.getRemoteMessage(): RemoteMessage? {
@Suppress("DEPRECATION")
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
intent?.getParcelableExtra(name, clazz)
else
intent?.getParcelableExtra(name) as T?
this?.getParcelableExtra("remote_message", RemoteMessage::class.java)
else this?.getParcelableExtra("remote_message")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class BreezNotificationHelper {
val foregroundServiceNotificationChannel = NotificationChannel(
NOTIFICATION_CHANNEL_FOREGROUND_SERVICE,
context.getString(R.string.foreground_service_notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT
NotificationManager.IMPORTANCE_LOW
).apply {
description =
context.getString(R.string.foreground_service_notification_channel_description)
Expand Down Expand Up @@ -132,7 +132,7 @@ class BreezNotificationHelper {

fun notifyPaymentReceived(
context: Context,
clickAction: String? = null,
clickAction: String? = "FLUTTER_NOTIFICATION_CLICK",
amountSat: ULong,
): Notification {
val notificationID: Int = System.currentTimeMillis().toInt() / 1000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ class BreezSdkConnector {
private var ELEMENT_PREFERENCES_KEY_PREFIX =
"VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIHNlY3VyZSBzdG9yYWdlCg"

internal fun connectSDK(applicationContext: Context): BlockingBreezServices {
internal fun connectSDK(
applicationContext: Context,
sdkListener: EventListener,
): BlockingBreezServices {
synchronized(this) {
if (breezSDK == null) {
Logger.tag(TAG).info { "Connecting to Breez SDK" }
Expand All @@ -29,7 +32,7 @@ class BreezSdkConnector {
val config = defaultConfig(EnvironmentType.PRODUCTION, apiKey, nodeConf)
config.workingDir = PathUtils.getDataDirectory(applicationContext)
// Connect to the Breez SDK make it ready for use
breezSDK = connect(config, seed, SDKListener())
breezSDK = connect(config, seed, sdkListener)
Logger.tag(TAG).info { "Connected to Breez SDK" }
}
return breezSDK!!
Expand Down
Loading

0 comments on commit 94f159b

Please sign in to comment.