17、联系人通知:监听联系人变化(ContentObserver)、通知栏提醒生日/纪念日、NotificationChannel适配

说实话,做联系人系统开发,最头疼的不是怎么存数据,而是怎么让App感知到「数据变了」。你想想看,用户在系统通讯录里新增了一个好友,或者改了生日,你的App如果还傻乎乎地显示旧数据,那体验就太差了。

我个人习惯,只要涉及联系人数据的实时性,一定会用 ContentObserver 来监听变化。这玩意儿是Android系统提供的一个观察者模式接口,专门用来监听ContentProvider的数据变更。说白了,就是给数据源装个「门铃」,一有动静就通知你。

17.1 用ContentObserver监听联系人变化

我记得第一次做联系人同步功能时,踩了个大坑。当时我直接用定时器每隔5秒去查一次ContactsContract,结果被用户骂「耗电怪兽」。后来才意识到,用ContentObserver才是正统做法——系统会在数据真正变化时才回调,省电又高效。

实现起来其实不复杂。你需要注册一个观察者,指定要监听的URI。系统联系人数据库的URI是 ContactsContract.Contacts.CONTENT_URI。一旦联系人表有增删改,你的观察者就会收到通知。

核心思路: 在Activity或Service中注册ContentObserver,重写onChange方法,在里面执行你的刷新逻辑。
// 注册ContentObserver的示例
getContentResolver().registerContentObserver(
    ContactsContract.Contacts.CONTENT_URI,
    true, // 是否监听子路径
    new ContentObserver(new Handler(Looper.getMainLooper())) {
        @Override
        public void onChange(boolean selfChange, Uri uri) {
            super.onChange(selfChange, uri);
            // 这里就是联系人数据变化的回调
            // 我一般在这里重新查询联系人列表,刷新UI
            Log.d("ContactObserver", "联系人数据变了,URI: " + uri.toString());
            loadContacts(); // 你的刷新方法
        }
    }
);

这里有个细节要注意:selfChange 参数表示是不是你自己改的。如果你自己的App修改了联系人,这个值会是true。我曾经在项目里没判断这个,结果自己改完数据又触发了一次刷新,形成了死循环。嗯,后来加了个判断就解决了。

小技巧: 如果你只需要监听特定联系人的变化,可以传具体的URI,比如 ContactsContract.Contacts.CONTENT_URI.buildUpon().appendPath(id).build()。这样回调次数更少,性能更好。

17.2 通知栏提醒生日/纪念日

联系人系统里,生日和纪念日提醒是个很贴心的功能。我做过一个版本,用户反馈说「你们App居然记得我老婆生日,比我自己还靠谱」。其实实现起来就是两步:查数据库 + 发通知。

首先,你得从联系人数据里把生日字段捞出来。Android的联系人数据库里,生日信息存在 ContactsContract.CommonDataKinds.Event 表中,类型是 TYPE_BIRTHDAY

// 查询联系人生日的示例
String[] projection = new String[]{
    ContactsContract.Data.CONTACT_ID,
    ContactsContract.Data.DISPLAY_NAME,
    ContactsContract.CommonDataKinds.Event.START_DATE
};
String selection = ContactsContract.Data.MIMETYPE + " = ? AND " +
    ContactsContract.CommonDataKinds.Event.TYPE + " = ?";
String[] selectionArgs = new String[]{
    ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE,
    String.valueOf(ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY)
};
Cursor cursor = getContentResolver().query(
    ContactsContract.Data.CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    null
);

拿到生日数据后,你需要判断「今天是不是某人的生日」。这里有个坑——年份问题。很多联系人只存了月日,没有年份。我建议你只比较月日,忽略年份。否则你可能会错过很多提醒。

然后就是发通知了。Android 8.0(API 26)之后,所有通知都必须走NotificationChannel。如果你不创建Channel,通知根本不会显示。我刚开始适配时也懵过,后来发现其实很简单。

注意: 如果你没有适配NotificationChannel,在Android 8.0+设备上,通知会被系统静默丢弃。用户根本看不到你的提醒。我曾经因为这个被测试妹子追着骂了一下午。

17.3 NotificationChannel适配详解

NotificationChannel说白了就是给通知分个类。比如生日提醒是一个Channel,纪念日提醒是另一个Channel。用户可以在系统设置里分别控制每个Channel的开关、声音、震动等。

我个人习惯在Application的onCreate里统一创建所有Channel。这样App启动时就已经准备好了,后续发通知直接引用Channel ID就行。

// 创建NotificationChannel的示例
private void createNotificationChannels() {
    // 生日提醒Channel
    NotificationChannel birthdayChannel = new NotificationChannel(
        "channel_birthday",           // Channel ID
        "生日提醒",                    // Channel名称(用户可见)
        NotificationManager.IMPORTANCE_HIGH // 重要性
    );
    birthdayChannel.setDescription("提醒你联系人的生日");
    birthdayChannel.enableVibration(true);
    birthdayChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);

    // 纪念日Channel
    NotificationChannel anniversaryChannel = new NotificationChannel(
        "channel_anniversary",
        "纪念日提醒",
        NotificationManager.IMPORTANCE_DEFAULT
    );
    anniversaryChannel.setDescription("提醒你联系人的纪念日");
    anniversaryChannel.setSound(null, null); // 纪念日可以静音

    NotificationManager manager = getSystemService(NotificationManager.class);
    manager.createNotificationChannel(birthdayChannel);
    manager.createNotificationChannel(anniversaryChannel);
}

发通知的时候,只需要在Builder里指定Channel ID即可:

NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "channel_birthday")
    .setSmallIcon(R.drawable.ic_birthday)
    .setContentTitle("生日提醒")
    .setContentText("今天是 " + contactName + " 的生日!")
    .setPriority(NotificationCompat.PRIORITY_HIGH)
    .setAutoCancel(true);

NotificationManagerCompat.from(this).notify(contactId, builder.build());

这里有个小细节:setPriority 在Android 8.0+上其实已经被Channel的importance替代了。但为了兼容低版本,我还是会保留这个设置。说白了,就是「向下兼容,向上适配」。

17.4 整体流程梳理

我把整个联系人通知系统的流程画了张图,方便你理解各个模块之间的关系:

联系人通知系统流程图 系统联系人数据库 ContactsProvider ContentObserver 监听数据变化 数据变更 查询生日/纪念日 Event表 + TYPE判断 触发查询 匹配今天日期 月日比较(忽略年份) NotificationChannel Channel适配(8.0+) 匹配成功 发送通知 NotificationManager.notify() 用户操作 新增/修改联系人 写入数据 整个流程:用户操作 → 数据库变更 → Observer感知 → 查询匹配 → 发通知

从图上你可以看到,整个流程是闭环的。用户一操作,系统数据库就变了,ContentObserver立刻感知到,然后触发查询逻辑,匹配到生日或纪念日就发通知。一气呵成。

17.5 避坑指南

最后分享几个我踩过的坑,希望能帮你省点时间:

  • ContentObserver记得反注册:在Activity或Service销毁时,一定要调用 getContentResolver().unregisterContentObserver(observer)。我曾经忘了反注册,导致内存泄漏,被用户投诉App越来越卡。
  • 通知不要重复发:如果用户一天内多次打开联系人页面,可能会触发多次查询。我建议用SharedPreferences记录一下「今天是否已经发过某人的生日提醒」,避免重复打扰用户。
  • Channel一旦创建就不能改:NotificationChannel的配置(比如重要性、声音)只能在创建时设置。如果你想修改,只能删除Channel再重建,或者让用户去系统设置里手动改。所以创建前想清楚。
  • 低版本兼容:如果你的App还要支持Android 7.0及以下,记得用 NotificationCompatNotificationManagerCompat。这些兼容库会自动处理版本差异。
我的建议: 如果你做的是联系人管理类App,生日提醒功能最好加上「提前一天提醒」的选项。很多用户喜欢提前准备礼物,这个细节能提升不少好感度。

好了,关于联系人通知这块,核心就是这些。ContentObserver监听变化,NotificationChannel适配新版本,再加上生日/纪念日的匹配逻辑,一套完整的通知系统就搭起来了。代码量不大,但细节不少,多测试几遍就稳了。


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