Ensure the RaiseToForeground service is bound before sending push msg

This commit is contained in:
sim 2025-05-22 09:44:18 +02:00
parent cce36d8151
commit 13ff0ec8a4
3 changed files with 122 additions and 38 deletions

View file

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

View file

@ -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<Message>()
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?) {

View file

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