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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 04:22:22