You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,通过轻量发布订阅模式做全局状态传递即可,不需要额外引入状态管理库:

  1. 抽离独立的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);
  1. 在应用根组件初始化监听,保证应用启动后随时能接收事件:
import { useEffect } from 'react';
import { setupCallKeepListeners } from './src/utils/callkeep';
// 其他根组件依赖,比如导航容器、全局组件

const App = () => {
  useEffect(() => {
    setupCallKeepListeners();
    // 其他初始化逻辑,比如声网SDK初始化、信令连接
  }, []);

  return (
    <>
      {/* 你的根导航组件 */}
      <GlobalIncomingCallModal />
    </>
  )
}
  1. 编写全局自定义来电弹窗组件,订阅事件控制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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 11:54:15