如何在Expo/React Native中实现Android后台来电弹窗(含接听拒接)
Android 来电弹窗(含接听/拒接)实现方案(Expo + React Native)
Expo 方案限制说明
纯 Expo 环境无法直接实现该需求:Expo 未提供访问系统电话状态监听、悬浮窗显示的官方 API,且社区现有相关包大多存在兼容性问题或无法支持后台触发逻辑。若坚持使用 Expo,需通过 Expo Development Build 集成自定义原生模块,步骤复杂度接近裸 React Native 项目,因此更推荐直接使用裸 React Native 方案。
React Native 裸项目实现步骤
1. 配置 Android 权限与组件
在 android/app/src/main/AndroidManifest.xml 中添加以下内容:
<!-- 基础权限 --> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- Android 10+ 接听权限 --> <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" /> <!-- 来电状态广播接收器 --> <receiver android:name=".PhoneStateReceiver"> <intent-filter> <action android:name="android.intent.action.PHONE_STATE" /> </intent-filter> </receiver> <!-- 前台服务(保证后台能显示弹窗) --> <service android:name=".CallListenerService" android:foregroundServiceType="phoneCall" />
2. 实现原生来电监听与弹窗逻辑
创建来电状态广播接收器 PhoneStateReceiver.java
import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.telephony.TelephonyManager; public class PhoneStateReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE); if (TelephonyManager.EXTRA_STATE_RINGING.equals(state)) { String incomingNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER); // 启动前台服务显示弹窗 Intent serviceIntent = new Intent(context, CallListenerService.class); serviceIntent.putExtra("incomingNumber", incomingNumber); context.startForegroundService(serviceIntent); } else if (TelephonyManager.EXTRA_STATE_IDLE.equals(state)) { // 来电结束后停止服务 Intent serviceIntent = new Intent(context, CallListenerService.class); context.stopService(serviceIntent); } } }
创建前台服务 CallListenerService.java(负责显示悬浮弹窗与接听/拒接)
import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Service; import android.content.Intent; import android.graphics.PixelFormat; import android.os.Build; import android.os.IBinder; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import android.widget.Button; import android.widget.TextView; import android.telecom.TelecomManager; import android.os.SystemClock; public class CallListenerService extends Service { private WindowManager windowManager; private View floatingView; private String incomingNumber; @Override public void onCreate() { super.onCreate(); // 创建前台服务通知渠道(Android 8+ 强制要求) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel("call_service", "来电监听", NotificationManager.IMPORTANCE_LOW); NotificationManager manager = getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); } // 启动前台服务,避免被系统回收 Notification notification = new Notification.Builder(this, "call_service") .setContentTitle("监听来电") .setContentText("正在监控来电状态") .setSmallIcon(R.mipmap.ic_launcher) .build(); startForeground(1, notification); } @Override public int onStartCommand(Intent intent, int flags, int startId) { incomingNumber = intent.getStringExtra("incomingNumber"); showFloatingWindow(); return START_STICKY; } private void showFloatingWindow() { windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); LayoutInflater inflater = LayoutInflater.from(this); floatingView = inflater.inflate(R.layout.call_popup, null); // 配置悬浮窗参数 WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT ); params.gravity = Gravity.CENTER; // 设置来电号码 TextView numberTv = floatingView.findViewById(R.id.incoming_number); numberTv.setText("来电号码:" + incomingNumber); // 接听按钮逻辑 Button answerBtn = floatingView.findViewById(R.id.answer_btn); answerBtn.setOnClickListener(v -> { answerCall(); removeFloatingWindow(); stopSelf(); }); // 拒接按钮逻辑 Button rejectBtn = floatingView.findViewById(R.id.reject_btn); rejectBtn.setOnClickListener(v -> { rejectCall(); removeFloatingWindow(); stopSelf(); }); windowManager.addView(floatingView, params); } private void answerCall() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { TelecomManager telecomManager = getSystemService(TelecomManager.class); if (telecomManager != null && checkSelfPermission(android.Manifest.permission.ANSWER_PHONE_CALLS) == android.content.pm.PackageManager.PERMISSION_GRANTED) { telecomManager.acceptRingingCall(); } } else { // 低版本模拟耳机按键接听 Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); intent.putExtra(Intent.EXTRA_KEY_EVENT, new android.view.KeyEvent( android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_HEADSETHOOK)); sendOrderedBroadcast(intent, null); SystemClock.sleep(100); intent.putExtra(Intent.EXTRA_KEY_EVENT, new android.view.KeyEvent( android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_HEADSETHOOK)); sendOrderedBroadcast(intent, null); } } private void rejectCall() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { TelecomManager telecomManager = getSystemService(TelecomManager.class); if (telecomManager != null) { telecomManager.endCall(); } } else { // 低版本通过反射调用挂断方法 android.telephony.TelephonyManager telephonyManager = getSystemService(android.telephony.TelephonyManager.class); try { java.lang.reflect.Method method = telephonyManager.getClass().getMethod("endCall"); method.invoke(telephonyManager); } catch (Exception e) { e.printStackTrace(); } } } private void removeFloatingWindow() { if (floatingView != null && windowManager != null) { windowManager.removeView(floatingView); } } @Override public void onDestroy() { super.onDestroy(); removeFloatingWindow(); } @Override public IBinder onBind(Intent intent) { return null; } }
创建弹窗布局 call_popup.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:background="#ffffff" android:padding="16dp" android:elevation="8dp"> <TextView android:id="@+id/incoming_number" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="18sp" android:textColor="#333333" android:layout_gravity="center_horizontal" android:marginBottom="16dp"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gap="16dp"> <Button android:id="@+id/answer_btn" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="接听" android:background="#4CAF50" android:textColor="#ffffff"/> <Button android:id="@+id/reject_btn" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="拒接" android:background="#F44336" android:textColor="#ffffff"/> </LinearLayout> </LinearLayout>
3. React Native 端申请动态权限
在 App 组件中添加权限申请逻辑:
import { PermissionsAndroid, Platform, useEffect } from 'react-native'; const requestRequiredPermissions = async () => { if (Platform.OS !== 'android') return; const permissions = [ PermissionsAndroid.PERMISSIONS.READ_PHONE_STATE, PermissionsAndroid.PERMISSIONS.CALL_PHONE, PermissionsAndroid.PERMISSIONS.SYSTEM_ALERT_WINDOW, PermissionsAndroid.PERMISSIONS.ANSWER_PHONE_CALLS, ]; const granted = await PermissionsAndroid.requestMultiple(permissions); // 检查权限是否全部授予 Object.entries(granted).forEach(([perm, result]) => { if (result !== PermissionsAndroid.RESULTS.GRANTED) { console.warn(`权限 ${perm} 未授予,功能可能受限`); } }); }; export default function App() { useEffect(() => { requestRequiredPermissions(); }, []); // ... 其他组件逻辑 }
关键注意事项
- Android 6+ 需动态申请权限,其中
SYSTEM_ALERT_WINDOW部分机型需用户手动在系统设置中开启「显示悬浮窗」权限。 - 国产 ROM(小米、华为等)需手动给应用开启「后台运行权限」和「自启动权限」,否则服务可能被系统回收。
- 不同 Android 版本的接听/拒接 API 存在差异,代码中已做基础适配,部分特殊机型可能需要额外兼容。
内容的提问来源于stack exchange,提问作者Enes Atlam
相关产品推荐
相关产品推荐

