React Native CallKeep安卓自管理模式自定义来电UI问题
react-native-callkeep 安卓自管理模式自定义来电UI问题解决
一、showIncomingCallUi 事件不触发的排查修复
按以下顺序检查配置,缺项补全即可触发事件:
- 补全安卓清单文件必须配置
首先在AndroidManifest.xml中添加自管理模式需要的权限:
同时在<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" /> <!-- 安卓12及以上后台弹来电UI需要加全屏通知权限 --> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /><application>标签内注册CallKeep的ConnectionService,标记为自管理模式:<service android:name="io.wazo.callkeep.VoiceConnectionService" android:exported="true" android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"> <intent-filter> <action android:name="android.telecom.ConnectionService" /> </intent-filter> <meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS_IN_CALL_LOG" android:value="true" /> </service> - 初始化时打开自管理开关
调用RNCallKeep.setup时必须在android配置项中开启selfManaged: true,这是触发自管理事件的核心开关,示例初始化代码:import RNCallKeep from 'react-native-callkeep'; RNCallKeep.setup({ android: { selfManaged: true, alertTitle: '通话权限申请', alertDescription: '需要麦克风权限实现音频通话', cancelButton: '取消', okButton: '允许', additionalPermissions: [], }, ios: { appName: '你的应用名称', } }); - 调整事件监听注册时机
不要等到进入通话相关页面才注册监听,必须在应用入口(index.js或App.js组件挂载阶段)就完成事件绑定,避免信令到达、系统触发回调时监听还未注册导致事件丢失。 - 检查系统权限状态
安卓12及以上版本,应用在后台时弹自定义来电UI需要申请全屏通知权限,否则系统会拦截自管理事件回调;触发RNCallKeep.displayIncomingCall前必须确认已经拿到麦克风、通知相关权限。
二、独立文件监听事件后,对接业务组件渲染自定义UI的实现
不要在事件监听的独立文件中直接返回UI,通过轻量发布订阅模式做全局状态传递即可,不需要额外引入状态管理库:
- 抽离独立的CallKeep事件管理模块,比如新建
src/utils/callkeep.js,内部实现简单事件总线,统一维护所有通话相关事件:
import RNCallKeep from 'react-native-callkeep'; // 事件常量 const EVENTS = { INCOMING_CALL: 'callkeep/incoming', ANSWER_CALL: 'callkeep/answer', END_CALL: 'callkeep/end', } // 简易发布订阅实现 const listenerMap = {}; const CallKeepBus = { subscribe(event, callback) { if (!listenerMap[event]) listenerMap[event] = []; listenerMap[event].push(callback); return () => { listenerMap[event] = listenerMap[event].filter(cb => cb !== callback); } }, publish(event, payload) { listenerMap[event]?.forEach(callback => callback(payload)); } } // 应用启动时调用该方法初始化监听 export const setupCallKeepListeners = () => { // 先移除重复监听,避免回调多次触发 RNCallKeep.removeEventListener('showIncomingCallUi'); RNCallKeep.removeEventListener('answerCall'); RNCallKeep.removeEventListener('endCall'); RNCallKeep.addEventListener('showIncomingCallUi', (callInfo) => { const { handle, callUUID, name } = callInfo; // 收到来电后把信息抛给业务层 CallKeepBus.publish(EVENTS.INCOMING_CALL, { callUUID, callerId: handle, callerName: name }); // 必须调用该方法将应用拉到前台,否则1秒后系统会自动回退到默认来电UI RNCallKeep.backToForeground(); }); RNCallKeep.addEventListener('answerCall', ({ callUUID }) => { CallKeepBus.publish(EVENTS.ANSWER_CALL, { callUUID }); }); RNCallKeep.addEventListener('endCall', ({ callUUID }) => { CallKeepBus.publish(EVENTS.END_CALL, { callUUID }); }); } // 导出给业务组件使用的订阅方法 export const onIncomingCall = (cb) => CallKeepBus.subscribe(EVENTS.INCOMING_CALL, cb); export const onAnswerCall = (cb) => CallKeepBus.subscribe(EVENTS.ANSWER_CALL, cb); export const onEndCall = (cb) => CallKeepBus.subscribe(EVENTS.END_CALL, cb);
- 在应用根组件初始化监听,保证应用启动后随时能接收事件:
import { useEffect } from 'react'; import { setupCallKeepListeners } from './src/utils/callkeep'; // 其他根组件依赖,比如导航容器、全局组件 const App = () => { useEffect(() => { setupCallKeepListeners(); // 其他初始化逻辑,比如声网SDK初始化、信令连接 }, []); return ( <> {/* 你的根导航组件 */} <GlobalIncomingCallModal /> </> ) }
- 编写全局自定义来电弹窗组件,订阅事件控制UI渲染:
import { useState, useEffect } from 'react'; import { Modal, View, Text, TouchableOpacity, StyleSheet } from 'react-native'; import { onIncomingCall, onAnswerCall, onEndCall } from './src/utils/callkeep'; import RNCallKeep from 'react-native-callkeep'; const GlobalIncomingCallModal = () => { const [callInfo, setCallInfo] = useState(null); const [visible, setVisible] = useState(false); useEffect(() => { const removeIncoming = onIncomingCall((info) => { setCallInfo(info); setVisible(true); }); const removeAnswer = onAnswerCall(() => { setVisible(false); // 这里执行跳转到通话页面、声网加入频道逻辑 }); const removeEnd = onEndCall(() => { setVisible(false); setCallInfo(null); // 这里执行声网离开频道、重置通话状态逻辑 }); return () => { removeIncoming(); removeAnswer(); removeEnd(); } }, []); const handleAnswer = () => { RNCallKeep.answerIncomingCall(callInfo.callUUID); setVisible(false); // 跳转通话页、加入声网频道 }; const handleReject = () => { RNCallKeep.rejectCall(callInfo.callUUID); setVisible(false); setCallInfo(null); }; return ( <Modal visible={visible} animationType="slide"> {/* 完全自定义的来电UI,可自行调整样式和系统电话UI做区分 */} <View style={styles.modalContainer}> <Text style={styles.callTip}>应用内音频来电</Text> <Text style={styles.callerName}>{callInfo?.callerName || '未知联系人'}</Text> <View style={styles.btnGroup}> <TouchableOpacity style={[styles.btn, styles.rejectBtn]} onPress={handleReject}> <Text style={styles.btnText}>挂断</Text> </TouchableOpacity> <TouchableOpacity style={[styles.btn, styles.answerBtn]} onPress={handleAnswer}> <Text style={styles.btnText}>接听</Text> </TouchableOpacity> </View> </View> </Modal> ) } const styles = StyleSheet.create({ modalContainer: { flex: 1, backgroundColor: '#2563eb', alignItems: 'center', justifyContent: 'center' }, callTip: { color: '#fff', fontSize: 20 }, callerName: { color: '#fff', fontSize: 36, marginTop: 24, fontWeight: '500' }, btnGroup: { flexDirection: 'row', marginTop: 120, gap: 80 }, btn: { width: 80, height: 80, borderRadius: 40, alignItems: 'center', justifyContent: 'center' }, rejectBtn: { backgroundColor: '#ef4444' }, answerBtn: { backgroundColor: '#22c55e' }, btnText: { color: '#fff', fontSize: 16 } }); export default GlobalIncomingCallModal;
注意:自管理模式下,收到
showIncomingCallUi回调后必须在1秒内调用RNCallKeep.backToForeground(),否则安卓系统会判定应用未正常展示来电界面,自动回退到系统默认来电UI,后续的接听、挂断事件也不会再下发给应用。如果需要应用在后台被杀状态下接收来电,需要配合厂商推送通道,在推送到达时拉起应用主进程后再调用displayIncomingCall方法。
内容的提问来源于stack exchange,提问作者Manya Sharma
相关产品推荐
相关产品推荐

