react-native-webrtc/react-native-callkeep

πŸ“± Android: App goes to background after answering call, CallKeep screen remains on top

Opened this issue Β· 3 comments

When answering an incoming call using React Native CallKeep on Android, the app automatically moves to the background, while the CallKeep UI (system call screen) remains visible in the foreground.
Expected behavior is that the app should come to the foreground (or at least be accessible immediately) once the call is answered.

Steps to Reproduce

Use displayIncomingCall to show an incoming call.

Answer the call from the CallKeep screen.

Observe that:

The call connects.

The React Native app goes to background.

The CallKeep screen remains visible or locks focus.

Expected Behavior

After answering the call, the app should either:

Automatically return to foreground, or

The CallKeep UI should dismiss, allowing the user to interact with the app directly.

React Native version ,0.79.3
React Native CallKeep version 4.3.16
Android Version Android 13

here is my call keep setup

import { AppState, Platform } from 'react-native';
import RNCallKeep from 'react-native-callkeep';
import uuid from 'react-native-uuid';

const options = {
ios: {
appName: "BluebirdMessenger",
maximumCallGroups: '1',
maximumCallsPerCallGroup: '1',
includesCallsInRecents: false,
supportsVideo: true,
},
android: {
alertTitle: 'Permissions required',
alertDescription: 'This application needs to access your phone accounts to make calls',
cancelButton: 'Cancel',
okButton: 'ok',
imageName: 'ic_launcher', // Make sure this icon exists in your drawable folder
additionalPermissions: [
'android.permission.RECORD_AUDIO',
'android.permission.CAMERA',
'android.permission.READ_PHONE_STATE',
'android.permission.CALL_PHONE',
'android.permission.FOREGROUND_SERVICE',
'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
],
selfManaged: false, // ⚠️ CRITICAL: Set to false for Android
foregroundService: {
channelId: 'com.bluebirdmessenger.calls',
channelName: 'Incoming Calls',
notificationTitle: 'Incoming call',
notificationIcon: 'ic_launcher',
},
},
};

export type IncomingCallParams = {
callerName: string;
handle: string;
callUUID?: string;
hasVideo?: boolean;
meta?: Record<string, any>;
};

let isSetup = false;

const callMetadata = new Map<string, Record<string, any>>();

export function getNewCallUUID(): string {
return uuid.v4().toString();
}

// In CallKeepService.js
export async function setupCallKeep(): Promise {
if (isSetup) {
console.log('βœ… CallKeep already setup');
return;
}

try {
console.log('πŸš€ Setting up CallKeep on', Platform.OS);

// Setup CallKeep
await RNCallKeep.setup(options as any);

// Set available
RNCallKeep.setAvailable(true);

// Android-specific setup
if (Platform.OS === 'android') {
  // Check if ConnectionService is available
  try {
    const isConnectionServiceAvailable = await RNCallKeep.isConnectionServiceAvailable();
    console.log('πŸ“ž ConnectionService available:', isConnectionServiceAvailable);
  } catch (e) {
    console.log('⚠️ Could not check ConnectionService availability', e);
  }

  // Register phone account
  try {
    await RNCallKeep.registerPhoneAccount();
    console.log('πŸ“ž Phone account registered');
  } catch (e) {
    console.log('⚠️ Failed to register phone account', e);
  }

  // Check if phone account exists
  try {
    const hasAccount = await RNCallKeep.hasPhoneAccount();
    console.log('πŸ“ž Has phone account:', hasAccount);
    
    if (!hasAccount) {
      // Try to check if phone account is enabled
      const isEnabled = await RNCallKeep.checkPhoneAccountEnabled();
      console.log('πŸ“ž Phone account enabled:', isEnabled);
      
      if (!isEnabled) {
        // Open phone accounts settings
        console.log('⚠️ Phone account not enabled. Opening settings...');
        await RNCallKeep.openPhoneAccounts();
      }
    }
  } catch (e) {
    console.log('⚠️ Error checking phone account', e);
  }

  // Set foreground service settings
  try {
    if (typeof RNCallKeep.setForegroundServiceSettings === 'function') {
      RNCallKeep.setForegroundServiceSettings({
        channelId: 'com.bluebirdmessenger.calls',
        channelName: 'Incoming Calls',
        notificationTitle: 'Incoming call',
        notificationIcon: 'ic_launcher',
      });
      console.log('πŸ“ž Foreground service settings configured');
    }
  } catch (e) {
    console.log('⚠️ Failed to set foreground service settings', e);
  }
}

isSetup = true;
console.log('βœ… CallKeep setup complete on', Platform.OS);

} catch (error) {
console.error('❌ CallKeep setup failed:', error);
throw error;
}
}

// export async function displayIncomingCall(params: IncomingCallParams): Promise {
// await setupCallKeep();
// const callUUID = params.callUUID ?? getNewCallUUID();
// RNCallKeep.displayIncomingCall(callUUID, params.handle, params.callerName, 'generic', params.hasVideo ?? true);
// // store meta for later navigation on accept
// if (params.meta) {
// callMetadata.set(callUUID, params.meta);
// }
// return callUUID;
// }
export async function displayIncomingCall(params: IncomingCallParams): Promise {
await setupCallKeep();
const callUUID = params.callUUID ?? getNewCallUUID();
console.log(callUUID, 'calludid')

try {
RNCallKeep.displayIncomingCall(
callUUID,
params.handle,
params.callerName || "Unknown",
"generic",
params.hasVideo ?? false
);
console.log('Incoming call displayed βœ…');
} catch (e) {
console.error('displayIncomingCall failed ❌', e);
}
console.log('displayed')

if (params.meta) {
callMetadata.set(callUUID, params.meta);
}
return callUUID;
}

export function getCallMeta(callUUID: string): Record<string, any> | undefined {
return callMetadata.get(callUUID);
}

export function clearCallMeta(callUUID: string): void {
callMetadata.delete(callUUID);
}

export function answerCall(callUUID: string): void {
RNCallKeep.answerIncomingCall(callUUID);
}

export function endCall(callUUID: string): void {
RNCallKeep.endCall(callUUID);
clearCallMeta(callUUID);
}

export function endAllCalls(): void {
RNCallKeep.endAllCalls();
callMetadata.clear();
}

// Event listeners hook
const registeredEvents: Array<{ event: string; handler: (payload: any) => void }> = [];

type HandlerMap = {
onAnswerCall?: (uuid: string) => void;
onEndCall?: (uuid: string) => void;
onMuteToggle?: (uuid: string, muted: boolean) => void;
onHoldToggle?: (uuid: string, held: boolean) => void;
};

export function registerCallKeepEvents(handlers: HandlerMap = {}): void {
unregisterCallKeepEvents();

const answerHandler = ({ callUUID }: any) => handlers.onAnswerCall?.(callUUID);
const endHandler = ({ callUUID }: any) => handlers.onEndCall?.(callUUID);
const muteHandler = ({ muted, callUUID }: any) => handlers.onMuteToggle?.(callUUID, muted);
const holdHandler = ({ hold, callUUID }: any) => handlers.onHoldToggle?.(callUUID, hold);

RNCallKeep.addEventListener('answerCall', answerHandler);
RNCallKeep.addEventListener('endCall', endHandler);
RNCallKeep.addEventListener('didPerformSetMutedCallAction', muteHandler);
RNCallKeep.addEventListener('didToggleHoldCallAction', holdHandler);

registeredEvents.push(
{ event: 'answerCall', handler: answerHandler },
{ event: 'endCall', handler: endHandler },
{ event: 'didPerformSetMutedCallAction', handler: muteHandler },
{ event: 'didToggleHoldCallAction', handler: holdHandler },
);
}

export function unregisterCallKeepEvents(): void {
if (registeredEvents.length === 0) return;
registeredEvents.forEach(({ event, handler }) => {
try {
// Some versions support removing by event, others by both. Calling both safely.
// @ts-ignore
RNCallKeep.removeEventListener?.(event, handler);
// @ts-ignore
RNCallKeep.removeEventListener?.(event);
} catch (_) { }
});
registeredEvents.splice(0, registeredEvents.length);
}

App.tsx

useEffect(() => {
let appStateSubscription;
let unsubscribeForeground = null;

(async () => {
await setupCallKeep();

registerCallKeepEvents({
  onAnswerCall: async uuid => {
    try {
      const meta = getCallMeta(uuid) || {};
      console.log('πŸ“ž Answer call triggered:', meta);

      const userId = meta.userId || meta.senderId;
      let userDetails = null;

      if (userId) {
        try {
          userDetails = await firebaseMethods.getFirebaseUserDetails(userId);
          console.log('πŸ”Ή Fetched callee user details:', userDetails);
        } catch (err) {
          console.error("Error fetching user details:", err);
          userDetails = {};
        }
      }

      const uid = await getData(StorageKey.FIREBASE_UID);
      const myUserData = { id: uid, userId: uid };

      if (meta.callType === 'video-call') {
        navigationRef?.current?.navigate('VideoCall', {
          roomId: meta.channelId,
          currentUser: myUserData,
          otherUser: userDetails,
          joinOnly: true,
        });
      } else {
        try {
          const res = await genrateAudioCallToken({
            receiver_id: meta.senderId,
            is_notify: 0
          });

          navigationRef?.current?.navigate('AudioCalls', {
            token: res?.channel?.token,
            callId: meta.channelId,
            fromNotification: true,
            otherUser: { id: meta.senderId },
            name: meta.name,
          });
        } catch (error) {
          console.error('Error accepting call:', error);
        }
      }
    } catch (error) {
      console.error('❌ Error navigating to call screen:', error);
    }
  },
  onEndCall: async uuid => {
    socketManager.connect();
    const meta = getCallMeta(uuid) || {};
    console.log('πŸ“ž reject call triggered:', meta);

    const userId = meta.userId || meta.senderId;
    socketManager.callEmit(userId, meta.senderId || userId, 'reject', meta.channelId);
  },
});

})();

// Foreground handler
unsubscribeForeground = messaging().onMessage(async remoteMessage => {

const data = remoteMessage?.data || {};
console.log('πŸ“© FCM Foreground Data:', data);

if (data?.callType === 'voice-call' || data?.callType === 'video-call') {
  await displayIncomingCall({
    callerName: data.callerName || data.caller_name || 'Incoming call',
    handle: data.handle || data.user_id || 'caller',
    hasVideo: data.callType === 'video-call',
    meta: data,
  });
}

});

// // Background handler
// messaging().setBackgroundMessageHandler(async remoteMessage => {
// const data = remoteMessage?.data || {};

// console.log('πŸ“© FCM Background Data:', data);
// if (data?.callType === 'voice-call' || data?.callType === 'video-call') {
// await displayIncomingCall({
// callerName: data.callerName || data.caller_name || 'Incoming call',
// handle: data.handle || data.user_id || 'caller',
// hasVideo: data.callType === 'video-call',
// meta: data,
// });
// }
// });

appStateSubscription = AppState.addEventListener('change', state => {
if (state === 'active') {
setupCallKeep().catch(() => {});
}
});

return () => {
unsubscribeForeground?.();
appStateSubscription?.remove();
};
}, []);

Try using RNCallKeep.backToForeground()

@vskuridina

When I answer a call in Android kill mode, the CallKit screen is displayed, but the app doesn’t open β€” it remains in kill mode and doesn’t become active or launch.