Raise app to foreground on new message

This commit is contained in:
sim 2024-10-28 09:32:03 +00:00
parent 5d6b3b887d
commit 41f4fabb33
3 changed files with 169 additions and 2 deletions

View File

@ -23,11 +23,12 @@ object Distributor {
fun sendMessage(context: Context, appToken: String, message: ByteArray) {
val db = getDb(context)
val connectorToken = db.getConnectorToken(appToken) ?: return
val application = getApp(context, connectorToken)
val application = getApp(context, connectorToken) ?: return
if (application == context.packageName) {
LocalNotification.showNotification(context, connectorToken, String(message))
Log.d(TAG, "Local notif shown")
Log.d(TAG, "Local notification shown")
} else {
RaiseAppToForegroundFactory.getInstance(context, application).raise()
val broadcastIntent = Intent()
broadcastIntent.`package` = application
broadcastIntent.action = ACTION_MESSAGE

View File

@ -0,0 +1,127 @@
package org.unifiedpush.distributor.nextpush.distributor
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.util.Log
import kotlinx.coroutines.Runnable
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
class RaiseAppToForeground(private val context: Context, private val app: String, private val onUnbound: () -> Unit): ServiceConnection, Runnable {
/**
* Is the service bound ? This is a per service connection
*/
private var bound = false
private var scheduledFuture: ScheduledFuture<*>? = null
private val foregroundImportance = listOf(
RunningAppProcessInfo.IMPORTANCE_FOREGROUND,
RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE,
)
/**
* @return `true` if the app is in Foreground importance
*/
private fun checkForeground(): Boolean {
val appProcesses = (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).runningAppProcesses
for (appProcess in appProcesses) {
if (appProcess.importance in foregroundImportance) {
Log.i(TAG, "Found foreground process: ${appProcess.processName}")
return true
}
}
return false
}
/**
* Raise [app] to the foreground, to follow AND_3 specifications
*
* @return `true` if have successfully raised app to foreground
*/
fun raise(): Boolean {
// 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/$TARGET_CLASS")
val intent = Intent().apply {
`package` = app
action = ACTION
}
//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
}
}
}
private fun unbind() {
// Per instance lock
synchronized(this) {
if (bound) {
context.unbindService(this)
bound = false
onUnbound()
Log.d(TAG, "Unbound")
}
}
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.d(TAG, "onServiceConnected $name")
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.d(TAG, "onServiceDisconnected $name")
}
override fun onBindingDied(name: ComponentName?) {
Log.d(TAG, "onBindingDied")
unbind()
}
override fun onNullBinding(name: ComponentName?) {
Log.d(TAG, "onBindingDied")
unbind()
}
/**
* Unbinding when the timeout passes.
*/
override fun run() {
Log.d(TAG, "Timeout expired, unbinding")
unbind()
}
private companion object {
private const val TAG = "RaiseAppToForeground"
private const val TARGET_CLASS = "org.unifiedpush.android.connector.RaiseToForegroundService"
private const val ACTION = "org.unifiedpush.android.distributor.RAISE_TO_FOREGROUND"
/** Executor to unbind 5 seconds later */
private val unbindExecutor = Executors.newSingleThreadScheduledExecutor()
}
}

View File

@ -0,0 +1,39 @@
package org.unifiedpush.distributor.nextpush.distributor
import android.content.Context
import android.util.Log
/**
* [RaiseAppToForeground] Factory, to avoid having one service connection per push message
*
* There is a very small chance that 2 connections exist at the same time\*, but that's not important.
* We just want to avoid tens of it.
*
* \* When [getInstance] returns an existing instance, that runs [remove] before
* [RaiseAppToForeground.raise] is called.
*/
object RaiseAppToForegroundFactory {
fun getInstance(context: Context, app: String): RaiseAppToForeground {
synchronized(this) {
return instances[app] ?: new(context, app)
}
}
private fun new(context: Context, app: String): RaiseAppToForeground {
return RaiseAppToForeground(context, app, onUnbound = {
remove(app)
}).also {
Log.d(TAG, "New instance for $app")
instances[app] = it
}
}
private fun remove(app: String) {
Log.d(TAG, "Removing instance for $app")
synchronized(this) {
instances.remove(app)
}
}
private val instances: MutableMap<String, RaiseAppToForeground> = mutableMapOf()
private const val TAG = "RaiseAppToForegroundFactory"
}