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 知识体系总览
下面这张图把黑名单功能的整体架构梳理了一下,你可以对照着看:
23.6 避坑指南
最后分享几个我踩过的坑:
- 权限问题:我曾经在 Android 11 上测试,发现广播接收器收不到来电广播了。查了半天才知道,Google 限制了非系统应用监听来电状态。所以如果你的应用不是系统应用,老老实实用
CallScreeningService。 - 短信拦截的优先级:默认短信应用会先收到广播。如果你的应用不是默认短信应用,
abortBroadcast()可能无效。解决办法是在清单里把优先级设到最高,但也不能保证100%生效。 - 匹配性能:黑名单表如果数据量大了(比如几万条),每次来电都全表扫描会卡。建议用
LIKE查询配合索引,或者把常用黑名单缓存到内存里。
我的建议:黑名单功能虽然看起来简单,但涉及系统权限、广播机制、数据库设计等多个方面。做之前先把 Android 版本兼容性列清楚,不同版本用不同方案。这样上线后少挨骂。