26、联系人历史记录:最近联系人表、通话记录关联、时间线展示、清除历史记录
说实话,历史记录这个功能,很多开发者容易把它当成「附属品」来做。但我在实际项目中踩过坑——用户最常看的,其实就是最近联系过谁。你想想看,通讯录里几百号人,谁记得住上次跟谁打过电话?所以历史记录做得好不好,直接决定了用户愿不愿意用你这个App。
这一章,我们就来聊聊怎么把联系人历史记录做得既完整又流畅。我会从表结构设计开始,一步步带你实现通话记录关联、时间线展示,最后再搞定清除历史记录这个看似简单实则坑多的功能。
最近联系人表的设计
先说说表结构。我个人习惯把最近联系人单独拆一张表,而不是直接写在通话记录表里。为什么?因为通话记录可能一天就几百条,但最近联系人只需要保留最近几十个。分开存,查询效率高,维护也方便。
CREATE TABLE recent_contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contact_id INTEGER NOT NULL,
phone_number TEXT NOT NULL,
contact_name TEXT,
last_contact_time INTEGER NOT NULL, -- 时间戳,毫秒
contact_count INTEGER DEFAULT 1, -- 累计联系次数
contact_type INTEGER DEFAULT 0, -- 0: 呼出, 1: 呼入, 2: 未接
avatar_uri TEXT,
is_deleted INTEGER DEFAULT 0
);
CREATE INDEX idx_recent_time ON recent_contacts(last_contact_time DESC);
CREATE INDEX idx_recent_contact ON recent_contacts(contact_id);
嗯,这里要注意一个细节:contact_type 字段我存的是「最后一次联系的类型」。为什么不是存所有类型?因为最近联系人列表只需要展示最后一次是打出去还是接进来。如果你非要存全部类型,那表结构就复杂了,查询也慢。
通话记录关联
最近联系人和通话记录,说白了就是「摘要」和「明细」的关系。最近联系人表存的是聚合后的数据,通话记录表存的是每一条原始记录。
那怎么关联呢?我建议用 contact_id + phone_number 做联合键。为什么不用单独的 contact_id?因为同一个联系人可能有多个号码,你总得区分是跟哪个号码通的电话吧?
public class CallLogHelper {
public static void syncRecentContact(Context context, CallLogEntry entry) {
ContentValues values = new ContentValues();
values.put("contact_id", entry.contactId);
values.put("phone_number", entry.phoneNumber);
values.put("contact_name", entry.contactName);
values.put("last_contact_time", entry.callTime);
values.put("contact_type", entry.callType);
// 先查一下有没有这条记录
Cursor cursor = context.getContentResolver().query(
RecentContactProvider.CONTENT_URI,
new String[]{"id", "contact_count"},
"contact_id = ? AND phone_number = ?",
new String[]{String.valueOf(entry.contactId), entry.phoneNumber},
null
);
if (cursor != null && cursor.moveToFirst()) {
// 有记录就更新
int count = cursor.getInt(1);
values.put("contact_count", count + 1);
context.getContentResolver().update(
RecentContactProvider.CONTENT_URI,
values,
"id = ?",
new String[]{String.valueOf(cursor.getInt(0))}
);
} else {
// 没有就插入
context.getContentResolver().insert(
RecentContactProvider.CONTENT_URI,
values
);
}
if (cursor != null) {
cursor.close();
}
}
}
这段代码看起来简单,但我曾经在这里翻过车。问题是这样的:每次通话结束都去更新最近联系人表,如果用户一天打了50个电话,那就要更新50次。后来我改成「只在App启动和切到前台时批量同步」,性能就好多了。
时间线展示
时间线展示,说白了就是把最近联系人按时间分组展示。常见的分组方式有:今天、昨天、本周、更早。这个分组逻辑看起来简单,但实现起来有几个坑。
先看一个分组查询的例子:
SELECT
CASE
WHEN date(last_contact_time / 1000, 'unixepoch', 'localtime') = date('now', 'localtime')
THEN '今天'
WHEN date(last_contact_time / 1000, 'unixepoch', 'localtime') = date('now', '-1 day', 'localtime')
THEN '昨天'
WHEN strftime('%W', last_contact_time / 1000, 'unixepoch', 'localtime') = strftime('%W', 'now', 'localtime')
THEN '本周'
ELSE '更早'
END AS time_group,
contact_name,
phone_number,
last_contact_time,
contact_type
FROM recent_contacts
WHERE is_deleted = 0
ORDER BY last_contact_time DESC
LIMIT 50;
嗯,这里要注意时区问题。我刚开始做的时候直接用 UTC 时间比较,结果发现「今天」的分组在晚上8点以后就乱了。后来统一用 localtime 才搞定。
展示的时候,我建议用 RecyclerView 配合多个 ViewType。每个时间组用一个 Header 类型,里面的条目用另一个类型。这样结构清晰,滑动性能也好。
清除历史记录
清除历史记录这个功能,看起来就是 DELETE 一下,对吧?但实际做起来,要考虑的事情还挺多的。
首先,是「清除全部」还是「清除部分」?我建议两种都支持。用户可能想清掉某一个人的记录,也可能想一键清空。看代码:
public class HistoryCleanupManager {
private static final long CLEANUP_THRESHOLD = 30 * 24 * 60 * 60 * 1000L; // 30天
public static void clearAll(Context context) {
ContentValues values = new ContentValues();
values.put("is_deleted", 1);
context.getContentResolver().update(
RecentContactProvider.CONTENT_URI,
values,
null,
null
);
}
public static void clearSingle(Context context, long contactId) {
ContentValues values = new ContentValues();
values.put("is_deleted", 1);
context.getContentResolver().update(
RecentContactProvider.CONTENT_URI,
values,
"contact_id = ?",
new String[]{String.valueOf(contactId)}
);
}
public static void autoCleanup(Context context) {
// 自动清理超过30天的记录
long threshold = System.currentTimeMillis() - CLEANUP_THRESHOLD;
context.getContentResolver().delete(
RecentContactProvider.CONTENT_URI,
"last_contact_time < ?",
new String[]{String.valueOf(threshold)}
);
}
}
注意,我这里用的是「软删除」——把 is_deleted 置为1,而不是真的 DELETE。为什么?因为用户可能误操作,想恢复。软删除给了你后悔的机会。我一般在设置里加一个「最近删除」的入口,保留7天内的已删除记录。
还有一个容易被忽略的点:清除历史记录后,要刷新 UI。我建议用 LiveData 或者 Flow 来观察数据库变化,这样清除操作完成后,UI 自动更新,不需要手动调用 notifyDataSetChanged。
整体架构图
下面这张图展示了最近联系人模块的整体架构,从数据源到 UI 展示的完整链路:
从这张图可以看出来,整个模块的核心就是中间的同步引擎。它负责把通话记录、联系人数据和用户操作事件整合起来,写入最近联系人表,然后 UI 层通过观察数据库变化来更新展示。
最后说一句:历史记录这个功能,看似简单,但做好了能让用户体验提升一大截。我见过太多 App 要么不做历史记录,要么做得特别粗糙。你按照这一章的方法去做,至少能超过市面上80%的通讯录App。
公众号:蓝海资料掘金营,微信deep3321