23、联系人黑名单:自定义黑名单表、拦截来电与短信、黑名单管理UI、系统拦截器集成

黑名单功能,说白了就是给用户一个「我不想接你电话、收你短信」的权利。我在做第一个联系人项目时,客户提了个需求:不仅要拦截黑名单号码,还要能自定义拦截规则,比如只拦截来电不拦截短信。嗯,这个功能看似简单,但坑不少。今天我们就把它拆开揉碎了讲清楚。

23.1 自定义黑名单表设计

我个人习惯把黑名单表设计得尽量灵活。你想想看,用户可能想拉黑一个号码,也可能想拉黑一个地区前缀,甚至想拉黑所有未知号码。所以表结构不能太死板。

-- 黑名单表
CREATE TABLE blacklist (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    phone_number TEXT NOT NULL,          -- 被拉黑的号码
    rule_type INTEGER DEFAULT 0,         -- 0:精确匹配, 1:前缀匹配, 2:通配符
    block_call INTEGER DEFAULT 1,        -- 是否拦截来电
    block_sms INTEGER DEFAULT 1,         -- 是否拦截短信
    create_time INTEGER NOT NULL,
    update_time INTEGER NOT NULL,
    remark TEXT                          -- 备注,比如「推销电话」
);

-- 索引,查询时快很多
CREATE INDEX idx_blacklist_number ON blacklist(phone_number);
CREATE INDEX idx_blacklist_type ON blacklist(rule_type);

这里有个细节:rule_type 字段。我曾经遇到一个需求,用户想拦截所有「400」开头的电话。如果只做精确匹配,那就得把几万个400号码全加进去,显然不现实。所以加了前缀匹配和通配符支持。

核心思路:黑名单表不只是存号码,还要存匹配规则。这样一条记录就能拦截一大类号码。

23.2 拦截来电与短信的实现

拦截来电和短信,Android 提供了两种方式:广播接收器和系统拦截器。我们先说广播接收器的方式,因为它最简单。

23.2.1 拦截来电

来电时系统会发一个 TelephonyManager.EXTRA_INCOMING_NUMBER 的广播。我们在广播接收器里查黑名单表,匹配上了就直接挂断。

public class CallBlockerReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (!TelephonyManager.ACTION_PHONE_STATE_CHANGED.equals(action)) {
            return;
        }

        String number = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
        if (TextUtils.isEmpty(number)) {
            return;
        }

        // 查黑名单
        BlacklistDao dao = new BlacklistDao(context);
        if (dao.isBlocked(number, BlockType.CALL)) {
            // 挂断电话
            endCall(context);
            // 记录拦截日志
            Log.i("CallBlocker", "拦截来电: " + number);
        }
    }

    private void endCall(Context context) {
        // 这个方法需要反射调用,因为官方没有公开API
        try {
            TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
            Method method = tm.getClass().getMethod("endCall");
            method.invoke(tm);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意:从 Android 10 开始,endCall() 这个反射方法被限制了。如果你的应用不是系统应用,建议使用系统拦截器方案(后面会讲)。我曾经在这个坑里爬了三天...

23.2.2 拦截短信

短信拦截相对简单,监听 SMS_RECEIVED_ACTION 广播,匹配到黑名单号码后调用 abortBroadcast() 阻止广播继续传递。

public class SmsBlockerReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (!"android.provider.Telephony.SMS_RECEIVED".equals(intent.getAction())) {
            return;
        }

        Bundle bundle = intent.getExtras();
        if (bundle == null) return;

        Object[] pdus = (Object[]) bundle.get("pdus");
        for (Object pdu : pdus) {
            SmsMessage sms = SmsMessage.createFromPdu((byte[]) pdu);
            String sender = sms.getOriginatingAddress();

            BlacklistDao dao = new BlacklistDao(context);
            if (dao.isBlocked(sender, BlockType.SMS)) {
                abortBroadcast();  // 关键!阻止短信进入收件箱
                Log.i("SmsBlocker", "拦截短信来自: " + sender);
                break;
            }
        }
    }
}

小技巧:短信广播接收器需要在清单文件中设置 android:priority="1000",优先级越高越先收到广播。这样你才有机会在系统处理之前拦截掉。

23.3 黑名单管理UI

UI 设计上,我个人偏好简洁清晰。用户加黑名单时,最常用的操作就是「从通话记录添加」和「手动输入」。所以界面要围绕这两个入口设计。

<!-- activity_blacklist.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- 顶部操作栏 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp">

        <Button
            android:id="@+id/btn_add_from_call_log"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="从通话记录添加" />

        <Button
            android:id="@+id/btn_add_manual"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="手动输入" />
    </LinearLayout>

    <!-- 黑名单列表 -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_blacklist"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

</LinearLayout>

列表项我习惯展示:号码、拦截类型(来电/短信/全部)、添加时间。右滑可以删除,长按可以编辑规则。嗯,交互上尽量跟系统联系人列表保持一致,用户学习成本低。

23.4 系统拦截器集成

从 Android 7.0 开始,Google 推出了 CallScreeningService,这是官方推荐的拦截来电方式。相比广播接收器,它更稳定,权限要求也更明确。

public class MyCallScreeningService extends CallScreeningService {
    @Override
    public void onScreenCall(Call.Details callDetails) {
        String number = callDetails.getHandle().getSchemeSpecificPart();

        BlacklistDao dao = new BlacklistDao(this);
        if (dao.isBlocked(number, BlockType.CALL)) {
            // 构建响应:拒绝来电
            CallResponse response = new CallResponse.Builder()
                    .setDisallowCall(true)          // 阻止通话
                    .setRejectCall(true)            // 挂断
                    .setSkipNotification(true)      // 不显示通知
                    .setSkipCallLog(true)            // 不记录到通话记录
                    .build();
            respondToCall(callDetails, response);
        } else {
            // 允许来电
            respondToCall(callDetails, null);
        }
    }
}

注册这个服务需要在清单文件中声明:

<service
    android:name=".MyCallScreeningService"
    android:permission="android.permission.BIND_SCREENING_SERVICE">
    <intent-filter>
        <action android:name="android.telecom.CallScreeningService" />
    </intent-filter>
</service>

为什么推荐用 CallScreeningService? 因为它不需要 android.permission.CALL_PHONE 权限,也不需要反射调用 endCall()。说白了,这是官方给的正规路子,兼容性最好。

23.5 知识体系总览

下面这张图把黑名单功能的整体架构梳理了一下,你可以对照着看:

黑名单功能架构图 数据层 SQLite黑名单表 号码 + 规则类型 拦截类型 + 时间戳 逻辑层 匹配算法 精确/前缀/通配符 拦截决策 拦截层 来电拦截 短信拦截 系统拦截器 UI层 - 黑名单管理界面 列表展示 | 添加/删除 | 编辑规则 | 从通话记录导入 数据驱动逻辑,逻辑控制拦截,UI管理数据 关键:Android 10+ 推荐使用 CallScreeningService 替代广播接收器

23.6 避坑指南

最后分享几个我踩过的坑:

  • 权限问题:我曾经在 Android 11 上测试,发现广播接收器收不到来电广播了。查了半天才知道,Google 限制了非系统应用监听来电状态。所以如果你的应用不是系统应用,老老实实用 CallScreeningService
  • 短信拦截的优先级:默认短信应用会先收到广播。如果你的应用不是默认短信应用,abortBroadcast() 可能无效。解决办法是在清单里把优先级设到最高,但也不能保证100%生效。
  • 匹配性能:黑名单表如果数据量大了(比如几万条),每次来电都全表扫描会卡。建议用 LIKE 查询配合索引,或者把常用黑名单缓存到内存里。

我的建议:黑名单功能虽然看起来简单,但涉及系统权限、广播机制、数据库设计等多个方面。做之前先把 Android 版本兼容性列清楚,不同版本用不同方案。这样上线后少挨骂。

公众号:蓝海资料掘金营,微信deep3321