diff --git a/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkFcmPackage.kt b/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkFcmPackage.kt index 77854735..f384dd56 100644 --- a/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkFcmPackage.kt +++ b/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkFcmPackage.kt @@ -3,15 +3,20 @@ package com.anonymous.bulwarkmobile import com.facebook.react.ReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags import com.facebook.react.uimanager.ViewManager class BulwarkFcmPackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext): List = - listOf( - BulwarkFcmModule(reactContext), - BulwarkUnifiedPushModule(reactContext), - BulwarkClientCertModule(reactContext), - ) + buildList { + add(BulwarkFcmModule(reactContext)) + add(BulwarkUnifiedPushModule(reactContext)) + add(BulwarkClientCertModule(reactContext)) + // The bridge registers React Native's own module under this name. + if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) { + add(BulwarkHeadlessJsTaskSupportModule(reactContext)) + } + } override fun createViewManagers(reactContext: ReactApplicationContext): List> = emptyList() diff --git a/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkHeadlessJsTaskSupportModule.kt b/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkHeadlessJsTaskSupportModule.kt new file mode 100644 index 00000000..5058e4a0 --- /dev/null +++ b/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkHeadlessJsTaskSupportModule.kt @@ -0,0 +1,35 @@ +package com.anonymous.bulwarkmobile + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.jstasks.HeadlessJsTaskContext + +/** + * React Native's HeadlessJsTaskSupport module, which AppRegistry calls when a + * headless JS task's promise settles. RN 0.81 registers it only for the old + * bridge (CoreModulesPackage); the bridgeless runtime's CoreReactPackage + * leaves it out, so in this app JS could never report a finished task and + * every BulwarkPushTask ran until its 30 s timeout, keeping the service or job + * and its wake lock alive that long. Same logic as the RN module. + */ +class BulwarkHeadlessJsTaskSupportModule(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext) { + + override fun getName(): String = "HeadlessJsTaskSupport" + + @ReactMethod + fun notifyTaskFinished(taskId: Double) { + val tasks = HeadlessJsTaskContext.getInstance(reactApplicationContext) + val id = taskId.toInt() + if (tasks.isTaskRunning(id)) tasks.finishTask(id) + } + + @ReactMethod + fun notifyTaskRetry(taskId: Double, promise: Promise) { + val tasks = HeadlessJsTaskContext.getInstance(reactApplicationContext) + val id = taskId.toInt() + promise.resolve(tasks.isTaskRunning(id) && tasks.retryTask(id)) + } +} diff --git a/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkMessagingService.kt b/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkMessagingService.kt index 4d26a8b0..41c553db 100644 --- a/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkMessagingService.kt +++ b/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkMessagingService.kt @@ -8,9 +8,9 @@ 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.HeadlessJsTaskService import com.facebook.react.bridge.Arguments import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage @@ -46,6 +46,7 @@ class BulwarkMessagingService : FirebaseMessagingService() { 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. @@ -62,9 +63,20 @@ class BulwarkMessagingService : FirebaseMessagingService() { 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) - HeadlessJsTaskService.acquireWakeLockNow(context) } catch (e: IllegalStateException) { // Android 8+ refuses to start a service from the background // unless the app is temporarily allowlisted. A high-priority @@ -72,11 +84,20 @@ class BulwarkMessagingService : FirebaseMessagingService() { // 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 diff --git a/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkPushTaskService.kt b/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkPushTaskService.kt index 63d98199..4af9f11a 100644 --- a/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkPushTaskService.kt +++ b/android/app/src/main/java/com/anonymous/bulwarkmobile/BulwarkPushTaskService.kt @@ -7,6 +7,12 @@ import com.facebook.react.jstasks.HeadlessJsTaskConfig import com.facebook.react.HeadlessJsTaskService class BulwarkPushTaskService : HeadlessJsTaskService() { + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = + super.onStartCommand(intent, flags, startId).also { + // startTask has taken React Native's wake lock by now. + BulwarkMessagingService.releaseStartWakeLock() + } + override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? { val extras = intent?.extras ?: return null return taskConfig(extras)