mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
Every background push kept the app awake for the full 30 s timeout of the BulwarkPushTask headless task, however quickly the task was done. AppRegistry reports a finished task through the native HeadlessJsTaskSupport module, and React Native 0.81 registers that module only for the old bridge (CoreModulesPackage); the bridgeless runtime's CoreReactPackage leaves it out. JS could never say it was done, so the service, or the job, and its wake lock waited for the timeout. BulwarkHeadlessJsTaskSupportModule provides the module under the same name when the app runs bridgeless. The push path also leaked a wake lock now and then. startHeadlessTask called HeadlessJsTaskService.acquireWakeLockNow on the FCM worker thread while the service took the same static lock on the main thread. The method is not thread-safe, so both could create a lock, and the one that was overwritten stayed held. The FCM thread now takes its own wake lock, timed at 10 s, and the service drops it as soon as it holds React Native's. Measured on the API 35 emulator with the R8 release build: - push handled by the job: 30,000 ms before, 75-155 ms now - push handled by the service: the service ran 31 s before, it is gone within a second now - `com.facebook.react.i` wake lock still held 5 s after a push: 2 of 4 pushes before, 0 of 8 now
117 lines
5.0 KiB
Kotlin
117 lines
5.0 KiB
Kotlin
package com.anonymous.bulwarkmobile
|
|
|
|
import android.app.ActivityManager
|
|
import android.app.NotificationChannel
|
|
import android.app.NotificationManager
|
|
import android.app.PendingIntent
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.PowerManager
|
|
import android.util.Log
|
|
import androidx.core.app.NotificationCompat
|
|
import com.facebook.react.bridge.Arguments
|
|
import com.google.firebase.messaging.FirebaseMessagingService
|
|
import com.google.firebase.messaging.RemoteMessage
|
|
|
|
class BulwarkMessagingService : FirebaseMessagingService() {
|
|
override fun onNewToken(token: String) {
|
|
val params = Arguments.createMap().apply { putString("token", token) }
|
|
BulwarkFcmModule.emit("fcm:newToken", params)
|
|
}
|
|
|
|
override fun onMessageReceived(message: RemoteMessage) {
|
|
ensureChannel(this)
|
|
|
|
val data = message.data
|
|
|
|
// Hand off to JS via a headless task only when the app isn't already
|
|
// running in the foreground - HeadlessJsTaskContext throws if started
|
|
// while foreground. When the app is open, the main JS instance already
|
|
// receives JMAP push directly, so the fcm:message event below is
|
|
// sufficient for it to refresh state.
|
|
if (!isAppInForeground()) {
|
|
startHeadlessTask(applicationContext, data)
|
|
}
|
|
|
|
val params = Arguments.createMap().apply {
|
|
val dataMap = Arguments.createMap()
|
|
for ((k, v) in data) dataMap.putString(k, v)
|
|
putMap("data", dataMap)
|
|
}
|
|
BulwarkFcmModule.emit("fcm:message", params)
|
|
}
|
|
|
|
companion object {
|
|
const val CHANNEL_ID = "bulwark_mail"
|
|
private const val TAG = "BulwarkPush"
|
|
private const val START_WAKE_LOCK_MS = 10_000L
|
|
|
|
// Shared with BulwarkUnifiedPushService - a push arriving over either
|
|
// transport is dispatched the same way.
|
|
fun isAppInForeground(): Boolean {
|
|
val info = ActivityManager.RunningAppProcessInfo()
|
|
ActivityManager.getMyMemoryState(info)
|
|
return info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND ||
|
|
info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
|
|
}
|
|
|
|
fun startHeadlessTask(context: Context, data: Map<String, String>) {
|
|
val intent = Intent(context, BulwarkPushTaskService::class.java)
|
|
val bundle = Bundle().apply {
|
|
for ((k, v) in data) putString(k, v)
|
|
}
|
|
intent.putExtras(bundle)
|
|
// Keep the device awake until the service holds React Native's
|
|
// wake lock; BulwarkPushTaskService releases this one then. Not
|
|
// HeadlessJsTaskService.acquireWakeLockNow: it is not
|
|
// thread-safe, and FCM calls this on a worker thread while the
|
|
// service takes the same static lock on the main thread, so both
|
|
// could create a lock and the overwritten one stayed held.
|
|
startWakeLock = context.getSystemService(PowerManager::class.java)
|
|
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "bulwark:push-start")
|
|
.apply {
|
|
setReferenceCounted(false)
|
|
acquire(START_WAKE_LOCK_MS)
|
|
}
|
|
try {
|
|
context.startService(intent)
|
|
} catch (e: IllegalStateException) {
|
|
// Android 8+ refuses to start a service from the background
|
|
// unless the app is temporarily allowlisted. A high-priority
|
|
// FCM message does that; a downgraded one or a UnifiedPush
|
|
// message may not (BackgroundServiceStartNotAllowedException
|
|
// on 12+), and uncaught this crashed the app on every such
|
|
// push. Run the same task from a job instead.
|
|
releaseStartWakeLock()
|
|
Log.w(TAG, "Push service start refused in the background, using a job: ${e.message}")
|
|
BulwarkPushJobService.schedule(context, data)
|
|
}
|
|
}
|
|
|
|
@Volatile private var startWakeLock: PowerManager.WakeLock? = null
|
|
|
|
/** Drops the wake lock taken in [startHeadlessTask]; it also times out on its own. */
|
|
fun releaseStartWakeLock() {
|
|
startWakeLock?.release()
|
|
startWakeLock = null
|
|
}
|
|
|
|
fun ensureChannel(context: Context) {
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
|
|
val channel = NotificationChannel(
|
|
CHANNEL_ID,
|
|
"Mail notifications",
|
|
NotificationManager.IMPORTANCE_HIGH,
|
|
).apply {
|
|
description = "Incoming email alerts"
|
|
enableVibration(true)
|
|
}
|
|
manager.createNotificationChannel(channel)
|
|
}
|
|
}
|
|
}
|