diff --git a/packages/react-native-callingx/README.md b/packages/react-native-callingx/README.md index 85b156258f..c4aba0c91b 100644 --- a/packages/react-native-callingx/README.md +++ b/packages/react-native-callingx/README.md @@ -55,7 +55,7 @@ await CallingxModule.displayIncomingCall( - `setOnHoldCall(callId, isOnHold)`. - `addEventListener(eventName, callback)`. - `getInitialEvents()` and `getInitialVoipEvents()`. -- `registerBackgroundTask(taskProvider)` / `startBackgroundTask()` / `stopBackgroundTask()` (Android). +- `acquireBackgroundTask(owner)` / `releaseBackgroundTask(owner)` (Android) — ref-counted keep-alive task that keeps the JS runtime/timers alive in the background; the underlying HeadlessJS task starts on the first acquire and stops once all owners release. ## Event names diff --git a/packages/react-native-callingx/android/src/main/AndroidManifest.xml b/packages/react-native-callingx/android/src/main/AndroidManifest.xml index a755d09f6a..7c57b46dec 100644 --- a/packages/react-native-callingx/android/src/main/AndroidManifest.xml +++ b/packages/react-native-callingx/android/src/main/AndroidManifest.xml @@ -1,10 +1,12 @@ + xmlns:tools="http://schemas.android.com/tools"> + + = Build.VERSION_CODES.Q) { - startForeground( - notificationId, - notification, - ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL - ) + startForeground(notificationId, notification, computeForegroundServiceType()) } else { startForeground(notificationId, notification) } @@ -589,6 +595,76 @@ class CallService : Service(), CallRepository.Listener { } } + /** + * Always includes `phoneCall`. Adds the while-in-use types `microphone`/`camera` only when: + * - the platform is Android 11+ (R) — these FGS types were added in API 30; passing them to + * startForeground() on older versions is unsupported, so we keep `phoneCall`-only there, AND + * - the corresponding runtime permission is granted, AND + * - the app currently holds while-in-use access (foreground). + */ + private fun computeForegroundServiceType(): Int { + var type = ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL + + // microphone/camera FGS types require API 30 (R) — keep phoneCall-only below it. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return type + } + if (!LifecycleListener.isInForeground) { + return type + } + + val hasMicPermission = + ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == + PackageManager.PERMISSION_GRANTED + if (hasMicPermission) { + type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE + } + + val hasCameraPermission = + ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == + PackageManager.PERMISSION_GRANTED + if (hasCameraPermission) { + type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA + } + + return type + } + + /** + * Re-issues [startForeground] for the current foreground call with the full + * [computeForegroundServiceType] bitmask. Called when the app enters the foreground (via + * [LifecycleListener]) so the microphone/camera types — which cannot be acquired from the + * background — get activated during the foreground window and then persist when backgrounded. + * + * This does NOT recreate the service: calling startForeground again on a running FGS only + * updates its active type and notification in place. + */ + private fun repromoteForegroundTypeIfNeeded() { + if (!isInForeground) return // service is not foreground yet — nothing to upgrade + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return + + val type = computeForegroundServiceType() + if (type == ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL) { + // Nothing extra to promote (no while-in-use permissions, or not foreground). + return + } + + val foregroundCallId = notificationManager.getForegroundCallId() ?: return + val call = callRepository.getCall(foregroundCallId) ?: return + val notificationId = notificationManager.getOrCreateNotificationId(foregroundCallId) + val notification = notificationManager.createNotification(foregroundCallId, call) + + try { + startForeground(notificationId, notification, type) + } catch (e: Exception) { + Log.e( + TAG, + "[service] repromoteForegroundType: failed to upgrade FGS type: ${e.message}", + e + ) + } + } + /** * Cancels the notification for [callId]. If that notification was the foreground one * and other calls remain, re-promotes the service with the next call's notification. diff --git a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/LifecycleListener.kt b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/LifecycleListener.kt index 040be4670d..cae4c663a0 100644 --- a/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/LifecycleListener.kt +++ b/packages/react-native-callingx/android/src/main/java/io/getstream/rn/callingx/utils/LifecycleListener.kt @@ -1,11 +1,15 @@ package io.getstream.rn.callingx.utils +import android.util.Log import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.common.LifecycleState +import java.util.concurrent.CopyOnWriteArrayList object LifecycleListener : LifecycleEventListener { + private const val TAG = "[Callingx] LifecycleListener" + @Volatile var isInForeground: Boolean = false private set @@ -13,6 +17,16 @@ object LifecycleListener : LifecycleEventListener { private var registered: Boolean = false private var currentContext: ReactApplicationContext? = null + private val foregroundListeners = CopyOnWriteArrayList() + + fun addOnForegroundListener(listener: Runnable) { + foregroundListeners.addIfAbsent(listener) + } + + fun removeOnForegroundListener(listener: Runnable) { + foregroundListeners.remove(listener) + } + fun register(context: ReactApplicationContext) { if (registered) return registered = true @@ -30,6 +44,14 @@ object LifecycleListener : LifecycleEventListener { override fun onHostResume() { isInForeground = true + // Notify after isInForeground is set so listeners observe the foreground state. + foregroundListeners.forEach { listener -> + try { + listener.run() + } catch (e: Throwable) { + Log.w(TAG, "foreground listener threw: ${e.message}") + } + } } override fun onHostPause() { diff --git a/packages/react-native-callingx/src/CallingxModule.ts b/packages/react-native-callingx/src/CallingxModule.ts index aacc782919..211fa79f1b 100644 --- a/packages/react-native-callingx/src/CallingxModule.ts +++ b/packages/react-native-callingx/src/CallingxModule.ts @@ -31,12 +31,23 @@ import { import { isVoipEvent } from './utils/utils'; class CallingxModule implements ICallingxModule { + // Grace period for the debounced keep-alive stop: a re-acquire within this window (e.g. the + // ringing-push -> keep-alive hand-off) cancels the stop and reuses the running task. + private static readonly KEEP_ALIVE_STOP_DEBOUNCE_MS = 2000; + private _isSetup = false; private _isOngoingCallsEnabled = { android: false, ios: false, }; - private _isHeadlessTaskRegistered = false; + // Ref-counted keep-alive ownership over the single native background-task slot. + // This prevents one caller from tearing down the task another still needs. + private _keepAliveOwners = new Set(); + private _keepAliveResolve: (() => void) | undefined; + // Pending debounced stop. When the last owner releases we defer the actual stop briefly; a + // re-acquire within the window cancels it and reuses the running task. This keeps the task + // continuous across the ringing-push -> keep-alive hand-off. + private _keepAliveStopTimer: ReturnType | undefined; private titleTransformer: (memberName: string, incoming: boolean) => string = (memberName: string) => memberName; @@ -71,7 +82,7 @@ class CallingxModule implements ICallingxModule { return NativeCallingModule.isTelecomBacked(); } - setup(options: CallingExpOptions): void { + setup = (options: CallingExpOptions): void => { if (this._isSetup) { return; } @@ -136,28 +147,28 @@ class CallingxModule implements ICallingxModule { } this._isSetup = true; - } + }; - setShouldRejectCallWhenBusy(shouldReject: boolean): void { + setShouldRejectCallWhenBusy = (shouldReject: boolean): void => { NativeCallingModule.setShouldRejectCallWhenBusy(shouldReject); - } + }; - setDefaultAudioDeviceEndpointType( + setDefaultAudioDeviceEndpointType = ( endpointType: DefaultDeviceEndpointType, - ): void { + ): void => { NativeCallingModule.setDefaultAudioDeviceEndpointType(endpointType); - } + }; - getRegisteredCallIds(): string[] { + getRegisteredCallIds = (): string[] => { if (Platform.OS !== 'android') { return []; } return NativeCallingModule.getRegisteredCallIds(); - } + }; - async getAvailableAudioEndpoints( + getAvailableAudioEndpoints = async ( callId: string, - ): Promise { + ): Promise => { const empty: AudioEndpointsSnapshot = { endpoints: [], currentEndpoint: null, @@ -171,37 +182,37 @@ class CallingxModule implements ICallingxModule { } catch { return empty; } - } + }; - requestAudioEndpointChange( + requestAudioEndpointChange = ( callId: string, endpointId: string, - ): Promise { + ): Promise => { if (Platform.OS !== 'android') { return Promise.resolve(); } return NativeCallingModule.requestAudioEndpointChange(callId, endpointId); - } + }; - getInitialEvents(): EventData[] { + getInitialEvents = (): EventData[] => { return NativeCallingModule.getInitialEvents() as EventData[]; - } + }; - getInitialVoipEvents(): VoipEventData[] { + getInitialVoipEvents = (): VoipEventData[] => { return NativeCallingModule.getInitialVoipEvents() as VoipEventData[]; - } + }; //activates call that was registered with the telecom stack - setCurrentCallActive(callId: string): Promise { + setCurrentCallActive = (callId: string): Promise => { return NativeCallingModule.setCurrentCallActive(callId); - } + }; - displayIncomingCall( + displayIncomingCall = ( callId: string, phoneNumber: string, callerName: string, hasVideo: boolean, - ): Promise { + ): Promise => { const displayOptions: InfoDisplayOptions = { displayTitle: this.titleTransformer(callerName, true), }; @@ -212,19 +223,19 @@ class CallingxModule implements ICallingxModule { hasVideo, displayOptions, ); - } + }; - answerIncomingCall(callId: string): Promise { + answerIncomingCall = (callId: string): Promise => { return NativeCallingModule.answerIncomingCall(callId); - } + }; //registers call with the telecom stack - startCall( + startCall = ( callId: string, phoneNumber: string, callerName: string, hasVideo: boolean, - ): Promise { + ): Promise => { const displayOptions: InfoDisplayOptions = { displayTitle: this.titleTransformer(callerName, false), }; @@ -235,14 +246,14 @@ class CallingxModule implements ICallingxModule { hasVideo, displayOptions, ); - } + }; - updateDisplay( + updateDisplay = ( callId: string, phoneNumber: string, callerName: string, incoming: boolean, - ): Promise { + ): Promise => { const displayOptions: InfoDisplayOptions = { displayTitle: this.titleTransformer(callerName, incoming), }; @@ -252,9 +263,12 @@ class CallingxModule implements ICallingxModule { callerName, displayOptions, ); - } + }; - endCallWithReason(callId: string, reason: EndCallReason): Promise { + endCallWithReason = ( + callId: string, + reason: EndCallReason, + ): Promise => { const reasons = Platform.OS === 'ios' ? iosEndCallReasonMap : androidEndCallReasonMap; @@ -263,74 +277,122 @@ class CallingxModule implements ICallingxModule { } return NativeCallingModule.endCallWithReason(callId, reasons[reason]); - } + }; - isCallTracked(callId: string): boolean { + isCallTracked = (callId: string): boolean => { return NativeCallingModule.isCallTracked(callId); - } + }; - hasRegisteredCall(): boolean { + hasRegisteredCall = (): boolean => { return NativeCallingModule.hasRegisteredCall(); - } + }; - setMutedCall(callId: string, isMuted: boolean): Promise { + setMutedCall = (callId: string, isMuted: boolean): Promise => { return NativeCallingModule.setMutedCall(callId, isMuted); - } + }; - setOnHoldCall(callId: string, isOnHold: boolean): Promise { + setOnHoldCall = (callId: string, isOnHold: boolean): Promise => { return NativeCallingModule.setOnHoldCall(callId, isOnHold); - } + }; - registerBackgroundTask(taskProvider: ManagableTask): void { - const stopTask = () => { - this._isHeadlessTaskRegistered = false; - NativeCallingModule.stopBackgroundTask(HEADLESS_TASK_NAME); - }; + private registerBackgroundTask = (taskProvider: ManagableTask): void => { + // We intentionally do NOT route stops through NativeCallingModule.stopBackgroundTask: that uses + // startService, so when no CallService is running (the call already ended -> onDestroy) it would + // spin up a throwaway CallService just to deliver a no-op stop, which then lingers. + const noopStop = () => {}; - setHeadlessTask((taskData: any) => taskProvider(taskData, stopTask)); + setHeadlessTask((taskData: any) => taskProvider(taskData, noopStop)); - this._isHeadlessTaskRegistered = true; NativeCallingModule.registerBackgroundTaskAvailable(); - } + }; - async startBackgroundTask(taskProvider?: ManagableTask): Promise { - // If taskProvider is provided, register it first - if (taskProvider) { - this.registerBackgroundTask(taskProvider); + acquireBackgroundTask = async (owner: string): Promise => { + if (Platform.OS !== 'android') { + return; } - // Check if task is registered - if (!this._isHeadlessTaskRegistered) { - throw new Error( - 'Background task not registered. Call registerBackgroundTask first.', - ); + // A new owner means we keep the task alive — cancel any pending debounced stop. + if (this._keepAliveStopTimer) { + clearTimeout(this._keepAliveStopTimer); + this._keepAliveStopTimer = undefined; } - return NativeCallingModule.startBackgroundTask(HEADLESS_TASK_NAME, 0); - } + const wasEmpty = this._keepAliveOwners.size === 0; + this._keepAliveOwners.add(owner); - stopBackgroundTask(): Promise { - this._isHeadlessTaskRegistered = false; - return NativeCallingModule.stopBackgroundTask(HEADLESS_TASK_NAME); - } + if (!wasEmpty) { + return; + } + + // First owner, or a re-acquire right after the last release (we just cancelled its pending + // stop). Either way we (re-)issue the native start: HeadlessTaskManager ignores it if the task + // is genuinely still running (e.g. CallService stayed alive across the hand-off), or starts a + // fresh one if the task was already torn down by CallService.onDestroy (e.g. declining one call + // and accepting another). Idempotent + self-healing across a CallService teardown. + this.registerBackgroundTask( + () => + new Promise((resolve) => { + if (this._keepAliveOwners.size === 0 && !this._keepAliveStopTimer) { + resolve(); + return; + } + this._keepAliveResolve = resolve; + }), + ); + + try { + await NativeCallingModule.startBackgroundTask(HEADLESS_TASK_NAME, 0); + } catch (e) { + this._keepAliveOwners.delete(owner); + if (this._keepAliveOwners.size === 0) { + this._keepAliveResolve?.(); + this._keepAliveResolve = undefined; + } + // Re-throw so the caller can log/handle and retry. + throw e; + } + }; + + releaseBackgroundTask = async (owner: string): Promise => { + if (Platform.OS !== 'android') { + return; + } + if (!this._keepAliveOwners.delete(owner)) { + return; + } + if (this._keepAliveOwners.size > 0 || this._keepAliveStopTimer) { + return; + } + // Last owner released — defer the actual stop. A re-acquire within the window (e.g. the + // ringing-push -> keep-alive hand-off) cancels this and reuses the still-running task, avoiding + // a stop/restart race on the single native task slot. + this._keepAliveStopTimer = setTimeout(() => { + this._keepAliveStopTimer = undefined; + if (this._keepAliveOwners.size > 0) { + return; + } + this._keepAliveResolve?.(); + this._keepAliveResolve = undefined; + }, CallingxModule.KEEP_ALIVE_STOP_DEBOUNCE_MS); + }; - fulfillAnswerCallAction(callId: string, didFail: boolean): void { + fulfillAnswerCallAction = (callId: string, didFail: boolean): void => { NativeCallingModule.fulfillAnswerCallAction(callId, didFail); - } + }; - fulfillEndCallAction(callId: string, didFail: boolean): void { + fulfillEndCallAction = (callId: string, didFail: boolean): void => { NativeCallingModule.fulfillEndCallAction(callId, didFail); - } + }; - registerVoipToken(): void { + registerVoipToken = (): void => { NativeCallingModule.registerVoipToken(); - } + }; - stopService(): Promise { + stopService = (): Promise => { return NativeCallingModule.stopService(); - } + }; - addEventListener( + addEventListener = ( eventName: T, callback: EventListener< T extends EventName @@ -339,7 +401,7 @@ class CallingxModule implements ICallingxModule { ? VoipEventParams[T] : never >, - ): { remove: () => void } { + ): { remove: () => void } => { type ManagerType = EventManager; const manager: ManagerType = ( @@ -353,11 +415,11 @@ class CallingxModule implements ICallingxModule { manager.removeListener(eventName, callback as any); }, }; - } + }; - log(message: string, level: 'debug' | 'info' | 'warn' | 'error'): void { + log = (message: string, level: 'debug' | 'info' | 'warn' | 'error'): void => { NativeCallingModule.log(message, level); - } + }; } const module = new CallingxModule(); diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index 51c3238e0e..a6879e4799 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -1,5 +1,4 @@ import type { EventListener } from './EventManager'; -import type { ManagableTask } from './utils/headlessTask'; export type DefaultDeviceEndpointType = 'speaker' | 'earpiece'; @@ -160,23 +159,22 @@ export interface ICallingxModule { setOnHoldCall(callId: string, isOnHold: boolean): Promise; /** - * Register a background task provider. This method only registers the task and does not start it. - * The task will be automatically started when the service starts (via displayIncomingCall or startCall) - * if it has been registered. - * @param taskProvider - The task provider function that will be executed when the background task starts. + * Acquire a ref-counted background keep-alive task identified by [owner]. + * + * The underlying HeadlessJS task — which keeps React Native's JS runtime and timers alive while + * the app is backgrounded — is started on the first acquire and stopped only once the last owner + * releases. Multiple independent owners (e.g. ringing-push handling and the keep-call-alive hook) + * can hold it simultaneously without tearing down each other's task. No-op on iOS. + * @param owner - A stable, unique key identifying the holder (e.g. `push:`, `keepalive:`). */ - registerBackgroundTask(taskProvider: ManagableTask): void; + acquireBackgroundTask(owner: string): Promise; /** - * Start the background task. This method will only start the task if: - * 1. A background task has been registered via registerBackgroundTask - * 2. The service is currently started - * @param taskProvider - The task provider function. If not provided, uses the previously registered task. - * @returns Promise that resolves when the task is started, or rejects if conditions are not met. + * Release a keep-alive task previously acquired with [acquireBackgroundTask] for [owner]. + * The native task is stopped only after all owners have released. No-op on iOS. + * @param owner - The same key passed to [acquireBackgroundTask]. */ - startBackgroundTask(taskProvider?: ManagableTask): Promise; - - stopBackgroundTask(taskName: string): Promise; + releaseBackgroundTask(owner: string): Promise; /** * Fulfill or fail a pending CXAnswerCallAction on iOS. diff --git a/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts b/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts index da46fc20e2..fc7892f295 100644 --- a/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts +++ b/packages/react-native-sdk/src/hooks/useAndroidKeepCallAliveEffect.ts @@ -82,6 +82,7 @@ async function startForegroundService(call_cid: string) { */ export const useAndroidKeepCallAliveEffect = () => { const foregroundServiceStartedRef = useRef(false); + const callingxKeepAliveOwnerRef = useRef(undefined); const call = useCall(); keepCallAliveCallRef.current = call; @@ -101,15 +102,46 @@ export const useAndroidKeepCallAliveEffect = () => { } const callingx = getCallingxLibIfAvailable(); - if ( - callingx?.isSetup && - (isRingingCall || (!isRingingCall && callingx?.isOngoingCallsEnabled)) - ) { + const isCallingxManaged = + !!callingx?.isSetup && + (isRingingCall || (!isRingingCall && callingx?.isOngoingCallsEnabled)); + + const isCallEnded = + callingState === CallingState.IDLE || callingState === CallingState.LEFT; + + // Release the callingx keep-alive task when the call ends. + if (callingxKeepAliveOwnerRef.current && isCallEnded) { + const currentOwner = callingxKeepAliveOwnerRef.current; + callingxKeepAliveOwnerRef.current = undefined; + callingx?.releaseBackgroundTask(currentOwner).catch(() => {}); + return undefined; + } + + if (isCallingxManaged && callingx) { + // Mutual exclusion: never acquire the callingx task while the SDK's own keep-alive FGS is + // running — only one keep-alive mechanism should be active per call. + if ( + !callingxKeepAliveOwnerRef.current && + !isCallEnded && + !foregroundServiceStartedRef.current + ) { + const owner = `keepalive:${activeCallCid}`; + callingxKeepAliveOwnerRef.current = owner; + callingx.acquireBackgroundTask(owner).catch((e) => { + if (callingxKeepAliveOwnerRef.current === owner) { + callingxKeepAliveOwnerRef.current = undefined; + } + videoLoggerSystem + .getLogger('useAndroidKeepCallAliveEffect') + .warn('Failed to acquire callingx keep-alive background task', e); + }); + } return undefined; } - // start foreground service as soon as the call is joined - if (shouldStartForegroundService) { + // Start keep-alive FGS as soon as the call is joined — but only if the callingx + // keep-alive task isn't already holding the call alive. + if (shouldStartForegroundService && !callingxKeepAliveOwnerRef.current) { const run = async () => { if (foregroundServiceStartedRef.current) { return; @@ -135,10 +167,7 @@ export const useAndroidKeepCallAliveEffect = () => { return () => { sub.remove(); }; - } else if ( - callingState === CallingState.IDLE || - callingState === CallingState.LEFT - ) { + } else if (isCallEnded) { if (foregroundServiceStartedRef.current) { keepCallAliveCallRef.current = undefined; // stop foreground service when the call is not active @@ -162,6 +191,21 @@ export const useAndroidKeepCallAliveEffect = () => { stopForegroundServiceNoThrow(); foregroundServiceStartedRef.current = false; } + // release the callingx keep-alive task if still held + if (callingxKeepAliveOwnerRef.current) { + const owner = callingxKeepAliveOwnerRef.current; + callingxKeepAliveOwnerRef.current = undefined; + getCallingxLibIfAvailable() + ?.releaseBackgroundTask(owner) + .catch(() => { + videoLoggerSystem + .getLogger('useAndroidKeepCallAliveEffect') + .warn( + 'Failed to release callingx keep-alive background task', + owner, + ); + }); + } }; }, []); }; diff --git a/packages/react-native-sdk/src/utils/push/android.ts b/packages/react-native-sdk/src/utils/push/android.ts index 0305fa7036..62f76109ea 100644 --- a/packages/react-native-sdk/src/utils/push/android.ts +++ b/packages/react-native-sdk/src/utils/push/android.ts @@ -121,163 +121,164 @@ export const firebaseDataHandler = async ( const asForegroundService = canListenToWS(); if (asForegroundService) { - // Listen to call events from WS through fg service - // note: this will replace the current empty fg service runner - //we need to start service (e.g. by calling display incoming call) and than launch bg task, consider making those steps independent - await callingx.startBackgroundTask((_: unknown, stopTask: () => void) => { - return new Promise((resolve) => { - const finishBackgroundTask = () => { + // Listen to call events from WS with headless task, bound to call service. + // By this moment the call service is already running, so we can acquire the background task. + const backgroundTaskOwner = `push:${call_cid}`; + await callingx.acquireBackgroundTask(backgroundTaskOwner).catch((e) => { + logger.error( + `Failed to acquire background task for callCid: ${call_cid} error: ${e}`, + ); + }); + + const finishBackgroundTask = () => { + callingx.log( + `Finishing background task for callCid: ${call_cid}`, + 'debug', + ); + callingx.releaseBackgroundTask(backgroundTaskOwner); + }; + + (async () => { + try { + const _client = await pushConfig.createStreamVideoClient(); + if (!_client) { + logger.debug( + `Closing fg service as there is no client to create from push config`, + ); + finishBackgroundTask(); + return; + } + + const callFromPush = await _client.onRingingCall(call_cid); + const { mustEndCall, endCallReason } = shouldCallBeClosed( + callFromPush, + data, + ); + if (mustEndCall) { + logger.debug( + `Closing fg service callCid: ${call_cid} endCallReason: ${endCallReason}`, + ); + callingx.log( - `Finishing background task for callCid: ${call_cid}`, + `Ending call with callCid: ${call_cid} endCallReason: ${endCallReason}`, 'debug', ); - resolve(undefined); - stopTask(); - }; + callingx.endCallWithReason(call_cid, endCallReason); + callFromPush.leave({ reject: false }).catch((error) => { + logger.error( + `Failed to leave already-ended ringing call ${call_cid}: ${error}`, + ); + }); + finishBackgroundTask(); + return; + } - (async () => { - try { - const _client = await pushConfig.createStreamVideoClient(); - if (!_client) { - logger.debug( - `Closing fg service as there is no client to create from push config`, - ); - finishBackgroundTask(); - return; - } + const unsubscribeFunctions: Array<() => void> = []; + // check if service needs to be closed if accept/decline event was done on another device + const unsubscribe = callFromPush.on('all', (event) => { + const _canListenToWS = canListenToWS(); + if (!_canListenToWS) { + logger.debug( + `Closing fg service from event callCid: ${call_cid} canListenToWS: ${_canListenToWS}`, + { event }, + ); + unsubscribeFunctions.forEach((fn) => fn()); + + finishBackgroundTask(); + return; + } - const callFromPush = await _client.onRingingCall(call_cid); - const { mustEndCall, endCallReason } = shouldCallBeClosed( - callFromPush, - data, + const { + mustEndCall: mustEndCallFromEvent, + endCallReason: endCallReasonFromEvent, + } = shouldCallBeClosed(callFromPush, data); + if (mustEndCallFromEvent) { + logger.debug( + `Closing fg service from event callCid: ${call_cid} canListenToWS: ${_canListenToWS} shouldCallBeClosed`, + { event }, ); - if (mustEndCall) { + unsubscribeFunctions.forEach((fn) => fn()); + + callingx.endCallWithReason(call_cid, endCallReasonFromEvent); + finishBackgroundTask(); + } + }); + + // check if service needs to be closed if call was left + const stateSubscription = callFromPush.state.callingState$.subscribe( + (callingState) => { + if ( + callingState === CallingState.IDLE || + callingState === CallingState.LEFT + ) { logger.debug( - `Closing fg service callCid: ${call_cid} endCallReason: ${endCallReason}`, + `Closing fg service from callingState callCid: ${call_cid} callingState: ${callingState}`, ); - + unsubscribeFunctions.forEach((fn) => fn()); callingx.log( - `Ending call with callCid: ${call_cid} endCallReason: ${endCallReason}`, + `Ending call with callCid: ${call_cid} callingState: ${callingState}`, 'debug', ); - callingx.endCallWithReason(call_cid, endCallReason); - callFromPush.leave({ reject: false }).catch((error) => { - logger.error( - `Failed to leave already-ended ringing call ${call_cid}: ${error}`, - ); - }); - resolve(undefined); - return; + finishBackgroundTask(); } + }, + ); - const unsubscribeFunctions: Array<() => void> = []; - // check if service needs to be closed if accept/decline event was done on another device - const unsubscribe = callFromPush.on('all', (event) => { - const _canListenToWS = canListenToWS(); - if (!_canListenToWS) { - logger.debug( - `Closing fg service from event callCid: ${call_cid} canListenToWS: ${_canListenToWS}`, - { event }, - ); - unsubscribeFunctions.forEach((fn) => fn()); - - finishBackgroundTask(); - return; - } - - const { - mustEndCall: mustEndCallFromEvent, - endCallReason: endCallReasonFromEvent, - } = shouldCallBeClosed(callFromPush, data); - if (mustEndCallFromEvent) { - logger.debug( - `Closing fg service from event callCid: ${call_cid} canListenToWS: ${_canListenToWS} shouldCallBeClosed`, - { event }, - ); - unsubscribeFunctions.forEach((fn) => fn()); - - callingx.endCallWithReason(call_cid, endCallReasonFromEvent); - resolve(undefined); - } - }); - - // check if service needs to be closed if call was left - const stateSubscription = - callFromPush.state.callingState$.subscribe((callingState) => { - if ( - callingState === CallingState.IDLE || - callingState === CallingState.LEFT - ) { - logger.debug( - `Closing fg service from callingState callCid: ${call_cid} callingState: ${callingState}`, - ); - unsubscribeFunctions.forEach((fn) => fn()); - callingx.log( - `Ending call with callCid: ${call_cid} callingState: ${callingState}`, - 'debug', - ); - resolve(undefined); - } + const endCallSubscription = callingx.addEventListener( + 'endCall', + async ({ callId }: { callId: string }) => { + unsubscribeFunctions.forEach((fn) => fn()); + try { + await callFromPush.leave({ + reject: + callFromPush.state.callingState === CallingState.RINGING, + reason: 'decline', }); + } catch (error) { + logger.error( + `Failed to leave call with callCid: ${call_cid} error: ${error}`, + ); + } finally { + callingx.log( + `Ending call with callCid: ${call_cid} callId: ${callId}`, + 'debug', + ); + finishBackgroundTask(); + } + }, + ); - const endCallSubscription = callingx.addEventListener( - 'endCall', - async ({ callId }: { callId: string }) => { - unsubscribeFunctions.forEach((fn) => fn()); - try { - await callFromPush.leave({ - reject: - callFromPush.state.callingState === - CallingState.RINGING, - reason: 'decline', - }); - } catch (error) { - logger.error( - `Failed to leave call with callCid: ${call_cid} error: ${error}`, - ); - } finally { - callingx.log( - `Ending call with callCid: ${call_cid} callId: ${callId}`, - 'debug', - ); - resolve(undefined); - } - }, - ); - - //stop background task when app comes to foreground - const appStateSubscription = AppState.addEventListener( - 'change', - (nextAppState) => { - const _canListenToWS = canListenToWS(); - callingx.log( - `AppState changed to: ${nextAppState} for callCid: ${call_cid} canListenToWS: ${_canListenToWS}`, - 'debug', - ); - if (!_canListenToWS) { - unsubscribeFunctions.forEach((fn) => fn()); - finishBackgroundTask(); - return; - } - }, - ); - - unsubscribeFunctions.push(unsubscribe); - unsubscribeFunctions.push(() => stateSubscription.unsubscribe()); - unsubscribeFunctions.push(() => endCallSubscription.remove()); - unsubscribeFunctions.push(() => appStateSubscription.remove()); - pushUnsubscriptionCallbacks.get(call_cid)?.forEach((cb) => cb()); - pushUnsubscriptionCallbacks.set(call_cid, unsubscribeFunctions); - } catch (error) { + //stop background task when app comes to foreground + const appStateSubscription = AppState.addEventListener( + 'change', + (nextAppState) => { + const _canListenToWS = canListenToWS(); callingx.log( - `Failed to start background task with callCid: ${call_cid} error: ${error}`, - 'error', + `AppState changed to: ${nextAppState} for callCid: ${call_cid} canListenToWS: ${_canListenToWS}`, + 'debug', ); - finishBackgroundTask(); - } - })(); - }); - }); + if (!_canListenToWS) { + unsubscribeFunctions.forEach((fn) => fn()); + finishBackgroundTask(); + return; + } + }, + ); + + unsubscribeFunctions.push(unsubscribe); + unsubscribeFunctions.push(() => stateSubscription.unsubscribe()); + unsubscribeFunctions.push(() => endCallSubscription.remove()); + unsubscribeFunctions.push(() => appStateSubscription.remove()); + pushUnsubscriptionCallbacks.get(call_cid)?.forEach((cb) => cb()); + pushUnsubscriptionCallbacks.set(call_cid, unsubscribeFunctions); + } catch (error) { + callingx.log( + `Failed to start background task with callCid: ${call_cid} error: ${error}`, + 'error', + ); + finishBackgroundTask(); + } + })(); } if (asForegroundService) {