From 13ff0ec8a4f2a5e210a36d759410ff25769755b0 Mon Sep 17 00:00:00 2001 From: sim Date: Thu, 22 May 2025 09:44:18 +0200 Subject: [PATCH] Ensure the RaiseToForeground service is bound before sending push msg --- .../java/io/heckel/ntfy/up/Distributor.kt | 10 +- .../io/heckel/ntfy/up/RaiseAppToForeground.kt | 148 ++++++++++++++---- .../ntfy/up/RaiseAppToForegroundFactory.kt | 2 +- 3 files changed, 122 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/up/Distributor.kt b/app/src/main/java/io/heckel/ntfy/up/Distributor.kt index 7ea70681..7b31a66f 100644 --- a/app/src/main/java/io/heckel/ntfy/up/Distributor.kt +++ b/app/src/main/java/io/heckel/ntfy/up/Distributor.kt @@ -11,13 +11,9 @@ import io.heckel.ntfy.util.Log class Distributor(val context: Context) { fun sendMessage(app: String, connectorToken: String, message: ByteArray) { Log.d(TAG, "Sending MESSAGE to $app (token=$connectorToken): ${message.size} bytes") - RaiseAppToForegroundFactory.getInstance(context, app).raise() - val broadcastIntent = Intent() - broadcastIntent.`package` = app - broadcastIntent.action = ACTION_MESSAGE - broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken) - broadcastIntent.putExtra(EXTRA_BYTES_MESSAGE, message) - context.sendBroadcast(broadcastIntent) + RaiseAppToForegroundFactory + .getInstance(context, app) + .raiseAndSend(connectorToken, message) } fun sendEndpoint(app: String, connectorToken: String, endpoint: String) { diff --git a/app/src/main/java/io/heckel/ntfy/up/RaiseAppToForeground.kt b/app/src/main/java/io/heckel/ntfy/up/RaiseAppToForeground.kt index 64471489..7e5e2f35 100644 --- a/app/src/main/java/io/heckel/ntfy/up/RaiseAppToForeground.kt +++ b/app/src/main/java/io/heckel/ntfy/up/RaiseAppToForeground.kt @@ -6,6 +6,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.content.pm.PackageManager import android.os.Build import android.os.IBinder import android.util.Log @@ -16,11 +17,20 @@ import java.util.concurrent.TimeUnit class RaiseAppToForeground(private val context: Context, private val app: String, private val onUnbound: () -> Unit): ServiceConnection, Runnable { + class Message(val token: String, val content: ByteArray) + + private enum class Bound { + Binding, + Bound, + Unbound + } + /** * Is the service bound ? This is a per service connection */ - private var bound = false + private var bound = Bound.Unbound private var scheduledFuture: ScheduledFuture<*>? = null + private val msgsQueue = mutableListOf() private val foregroundImportance = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { listOf( @@ -47,51 +57,121 @@ class RaiseAppToForeground(private val context: Context, private val app: String return false } + + private fun hasRaiseToForegroundService(): Boolean { + val intent = Intent(ACTION).apply { + `package` = app + } + return ( + if (Build.VERSION.SDK_INT >= 33) { + context.packageManager.queryIntentServices( + intent, + PackageManager.ResolveInfoFlags.of( + PackageManager.GET_META_DATA.toLong() + + PackageManager.GET_RESOLVED_FILTER.toLong(), + ), + ) + } else { + context.packageManager.queryIntentServices( + Intent(ACTION_REGISTER), + PackageManager.GET_RESOLVED_FILTER, + ) + } + ).any { + it.serviceInfo.exported + } + } + + + private fun send(message: Message) { + Log.d(TAG, "Sending msg for $app") + val broadcastIntent = Intent() + broadcastIntent.`package` = app + broadcastIntent.action = ACTION_MESSAGE + broadcastIntent.putExtra(EXTRA_TOKEN, message.token) + broadcastIntent.putExtra(EXTRA_BYTES_MESSAGE, message.content) + context.sendBroadcast(broadcastIntent) + } + + /** + * Queue message when the service is binding + */ + private fun queue(message: Message) { + msgsQueue.add(message) + } + + /** + * If the service is already bound, we delay its unbinding + */ + private fun delayUnbinding() { + /** + * Close current scheduledFuture. We interrupt if it is running (mayInterruptIfRunning = true), so [run] won't + * unbind this new connection after we release the lock. + */ + scheduledFuture?.cancel(true) + /** Call [run] (unbind) in 5 seconds */ + scheduledFuture = unbindExecutor.schedule(this, 5L, TimeUnit.SECONDS) + } + + private fun bind() { + Log.d(TAG, "Binding to ${this.app}") + val intent = Intent().apply { + `package` = this@RaiseAppToForeground.app + action = ACTION + } + /** Bind to the target raise to the foreground service */ + context.bindService(intent, this, Context.BIND_AUTO_CREATE) + /** Call [run] (unbind) in 5 seconds */ + scheduledFuture = unbindExecutor.schedule(this, 5L, TimeUnit.SECONDS) + bound = Bound.Binding + } + /** * Raise [app] to the foreground, to follow AND_3 specifications * * @return `true` if have successfully raised app to foreground */ - fun raise(): Boolean { + fun raiseAndSend(token: String, message: ByteArray): Boolean { + val msg = Message(token, message) // Per instance lock synchronized(this) { - if (bound) { - Log.w(TAG, "This service connection is already bound to $app. Aborting.") - /** - * Close current scheduledFuture. We interrupt if it is running, so [run] won't - * unbind this new connection after we release the lock. - */ - scheduledFuture?.cancel(/* mayInterruptIfRunning = */ true) - /** Call [run] (unbind) in 5 seconds */ - scheduledFuture = unbindExecutor.schedule(this, 5L, TimeUnit.SECONDS) - return true - } else if (checkForeground()) { - Log.d(TAG, "Binding to $app") - val intent = Intent().apply { - `package` = app - action = ACTION + when (bound) { + Bound.Bound -> { + Log.d(TAG, "Service connection already bound to ${this.app}") + delayUnbinding() + send(msg) + } + Bound.Binding -> { + delayUnbinding() + queue(msg) + } + Bound.Unbound -> { + val isForeground = checkForeground() + val targetHasService = hasRaiseToForegroundService() + if (isForeground && targetHasService) { + bind() + queue(msg) + } else { + Log.d( + TAG, + "Cannot raise to foreground: isForeground=$isForeground, targetHasService=$targetHasService" + ) + send(msg) + return false + } } - //val sConnection = RaiseAppToForeground(context, app) - /** Bind to the target raise to the foreground service */ - context.bindService(intent, this, Context.BIND_AUTO_CREATE) - /** Call [run] (unbind) in 5 seconds */ - scheduledFuture = unbindExecutor.schedule(this, 5L, TimeUnit.SECONDS) - Log.d(TAG, "Bound to $app") - bound = true - return true - } else { - Log.d(TAG, "We are not in foreground, can't raise $app to foreground") - return false } + return true } } private fun unbind() { // Per instance lock synchronized(this) { - if (bound) { + if (bound != Bound.Unbound) { + msgsQueue.clear() context.unbindService(this) - bound = false + bound = Bound.Unbound onUnbound() Log.d(TAG, "Unbound") } @@ -100,10 +180,18 @@ class RaiseAppToForeground(private val context: Context, private val app: String override fun onServiceConnected(name: ComponentName?, service: IBinder?) { Log.d(TAG, "onServiceConnected $name") + synchronized(this) { + bound = Bound.Bound + } + msgsQueue.forEach { msg -> + send(msg) + } + msgsQueue.clear() } override fun onServiceDisconnected(name: ComponentName?) { Log.d(TAG, "onServiceDisconnected $name") + unbind() } override fun onBindingDied(name: ComponentName?) { diff --git a/app/src/main/java/io/heckel/ntfy/up/RaiseAppToForegroundFactory.kt b/app/src/main/java/io/heckel/ntfy/up/RaiseAppToForegroundFactory.kt index de2b71a9..e7f69a6a 100644 --- a/app/src/main/java/io/heckel/ntfy/up/RaiseAppToForegroundFactory.kt +++ b/app/src/main/java/io/heckel/ntfy/up/RaiseAppToForegroundFactory.kt @@ -10,7 +10,7 @@ import android.util.Log * We just want to avoid tens of it. * * \* When [getInstance] returns an existing instance, that runs [remove] before - * [RaiseAppToForeground.raise] is called. + * [RaiseAppToForeground.raiseAndSend] is called. */ object RaiseAppToForegroundFactory { fun getInstance(context: Context, app: String): RaiseAppToForeground {