拓冰建站拓冰建站
首页 / 资讯中心 / 正文

安卓手机获取通话记录信息:基于 ContentResolver 与 CallLog 的权限配置与读取验证

1. 安卓读取通话记录到底难在哪很多刚接触安卓系统数据读取的朋友第一反应是「不就是查个数据库吗」结果真机一跑要么 Cursor 返回 null要么直接抛 SecurityException要么在 Android 10 以上机型上被系统静默拦截。安卓的通话记录读取本质上是三件事叠在一起权限声明、运行时授权、以及通过 ContentResolver 去查 CallLog.Calls 这张系统表。任何一环没对齐代码就白写。这篇内容面向的是需要在安卓 App 里读取本机通话记录的开发者包括做通话管理工具、客服回访辅助、设备数据备份类功能的同学。核心链路是在 AndroidManifest 里声明 READ_CALL_LOG运行时动态申请拿到授权后用 ContentResolver 查询 CallLog.Calls解析出号码、通话时长、通话类型、时间戳等字段。我会把可复制的权限片段、查询代码骨架、真机验证步骤以及我实际踩过的坑都摊开讲。需要提前说清楚一点通话记录属于敏感数据Google Play 对 READ_CALL_LOG 的审核非常严格只有默认拨号器或符合特定政策的应用才能上架。如果你只是本地调试或企业内部使用问题不大如果要上架务必先确认自己的应用类别是否被允许。这个前提想明白了后面的代码才有意义。2. 用 TaoToken 辅助生成权限与查询代码写这类系统 API 代码时最容易卡住的不是逻辑而是字段名记不全、权限组合拿不准、不同 Android 版本行为差异大。我自己的做法是先用 TaoToken 的模型对话把代码骨架和版本差异问清楚再落到工程里改。它的模型对话入口在 https://taotoken.net/api 对应的控制台里打开模型对话页面就能直接问比如「Android 14 读取 CallLog 需要哪些权限运行时申请怎么写」它会给出带版本判断的完整片段省去反复翻文档的时间。如果你是在做长期的安卓工具类项目反复要生成权限模板、查询封装、字段解析代码可以考虑 Coding Plan把常用的系统数据读取场景沉淀成可复用的对话上下文后面每次新建模块直接调用不用从零描述需求。接入文档在 https://taotoken.net/api 的 doc 路径下API Keys 在 console 的 api-keys 页面生成整个流程不复杂。这里要强调TaoToken 只是帮你生成和梳理代码的辅助工具它不替代 Android Studio也不替代真机调试。权限能不能拿到、Cursor 能不能返回数据最终还是要靠设备验证。下面进入正题。3. 权限声明与运行时申请的可复制配置3.1 AndroidManifest 声明先在AndroidManifest.xml的manifest节点下加入权限声明。读取通话记录至少需要 READ_CALL_LOG如果你还要写入或删除记录才需要 WRITE_CALL_LOGuses-permission android:nameandroid.permission.READ_CALL_LOG / uses-permission android:nameandroid.permission.WRITE_CALL_LOG /注意只读场景不要顺手把 WRITE_CALL_LOG 也加上权限越多审核和用户信任成本越高。3.2 运行时动态申请Android 6.0 以后危险权限必须运行时申请。下面是一个可以直接用的申请片段用 ActivityResultLauncher 的写法避免旧版 onRequestPermissionsResult 的碎片化private final ActivityResultLauncherString callLogPermissionLauncher registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted - { if (granted) { Log.i(CallLog, READ_CALL_LOG 已授权可以开始查询); queryCallLog(); } else { Log.w(CallLog, 用户拒绝了通话记录权限); } }); private void requestCallLogPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) PackageManager.PERMISSION_GRANTED) { queryCallLog(); } else { callLogPermissionLauncher.launch(Manifest.permission.READ_CALL_LOG); } }这段代码的关键点是先检查再申请已授权就直接查询没授权才弹窗。很多新手直接调 launch结果在已授权机型上反复弹窗体验很差。3.3 查询 CallLog.Calls 的代码骨架拿到权限后通过 ContentResolver 查询。下面这段是完整可跑的骨架包含字段解析和资源释放private ListRecordEntity queryCallLog() { ContentResolver resolver getContentResolver(); Cursor cursor null; ListRecordEntity recordList new ArrayList(); try { cursor resolver.query( CallLog.Calls.CONTENT_URI, null, null, null, CallLog.Calls.DATE desc); if (cursor null) { Log.e(CallLog, cursor 为 null检查权限或 URI); return recordList; } int nameIdx cursor.getColumnIndex(CallLog.Calls.CACHED_NAME); int numberIdx cursor.getColumnIndex(CallLog.Calls.NUMBER); int typeIdx cursor.getColumnIndex(CallLog.Calls.TYPE); int dateIdx cursor.getColumnIndex(CallLog.Calls.DATE); int durationIdx cursor.getColumnIndex(CallLog.Calls.DURATION); int newIdx cursor.getColumnIndex(CallLog.Calls.NEW); while (cursor.moveToNext()) { RecordEntity record new RecordEntity(); record.name nameIdx 0 ? cursor.getString(nameIdx) : ; record.number numberIdx 0 ? cursor.getString(numberIdx) : ; record.type typeIdx 0 ? cursor.getInt(typeIdx) : 0; record.lDate dateIdx 0 ? cursor.getLong(dateIdx) : 0L; record.duration durationIdx 0 ? cursor.getLong(durationIdx) : 0L; record.isNew newIdx 0 ? cursor.getInt(newIdx) : 0; recordList.add(record); } } catch (SecurityException e) { Log.e(CallLog, 权限异常 e.getMessage()); } finally { if (cursor ! null) { cursor.close(); } } return recordList; }实体类保持简单即可private static class RecordEntity { String name; String number; int type; long lDate; long duration; int isNew; Override public String toString() { return RecordEntity{name name , number number , type type , date lDate , duration duration , isNew isNew }; } }字段含义对照表如下方便你按需取用字段含义类型CACHED_NAME联系人缓存名称可能为空StringNUMBER通话号码StringTYPE通话类型1 来电 2 去电 3 未接intDATE通话时间戳毫秒longDURATION通话时长秒longNEW是否是新记录1 为新int4. 真机验证与成功结果确认代码写完后别急着上模拟器。通话记录依赖真实通话数据模拟器里通常是空的建议用真机验证。步骤是安装 App进入触发查询的页面系统弹出权限对话框点允许然后看 Logcat 输出。我实测下来一条正常的日志大概长这样CallLog: RecordEntity{name张三, number13800000000, type2, date1710000000000, duration45, isNew0} CallLog: RecordEntity{name, number10086, type1, date1709990000000, duration12, isNew1}如果能看到号码、时长、类型都正确解析说明整条链路跑通了。这里有个细节CACHED_NAME 对陌生号码会是空字符串这是正常的不要以为是解析失败。另外 DATE 是毫秒时间戳展示时用 SimpleDateFormat 转一下即可。验证时建议按这个顺序排查先确认权限对话框是否弹出再确认 Logcat 里有没有 SecurityException最后看 cursor 是否为 null。三步定位基本能覆盖九成问题。5. 本篇常见错误排查5.1 SecurityException: Permission Denial最常见的就是没申请权限或用户拒绝后仍强行查询。表现是直接抛异常日志里带requires android.permission.READ_CALL_LOG。解决方式是确保运行时申请走完并且在查询前用 checkSelfPermission 再确认一次。如果用户勾选了「不再询问」需要引导到系统设置页手动开启。5.2 cursor 返回 null权限没问题但 cursor 为 null通常是 URI 写错或设备本身没有通话记录。确认用的是CallLog.Calls.CONTENT_URI不要手写成字符串。另外部分定制 ROM 对系统表做了限制这种情况换原生或主流机型复测。5.3 字段索引返回 -1用 getColumnIndex 拿到 -1说明该字段在当前系统版本不存在。比如某些低版本没有 CACHED_PHOTO_ID。稳妥做法是像上面代码那样索引小于 0 时给默认值避免 getString(-1) 直接崩溃。5.4 Android 10 以上查询受限Android 10 起对通话记录访问收紧了部分场景需要配合默认拨号器角色。如果你的应用不是拨号器可能拿不到完整数据。这个属于系统策略不是代码问题需要从产品定位上规避。5.5 忘记关闭 CursorCursor 不关闭会导致资源泄漏长时间运行后查询变慢甚至失败。务必在 finally 里 close这是硬性要求。6. 继续把链路跑稳的建议权限和查询跑通只是第一步。真正上线前你还要处理用户拒绝权限后的降级逻辑、通话记录的分页加载、以及不同 ROM 的兼容性。我自己的习惯是把权限申请、查询、字段解析拆成独立方法方便单测和复用。如果你在接入过程中遇到报错或者想快速生成适配不同 Android 版本的权限模板可以直接用 TaoToken 的 API Keys 配合接入文档来调试文档里有完整的调用示例。需要验证模型对某段代码的理解是否准确时打开模型对话贴进去问就行。长期做安卓系统数据类项目的用 Coding Plan 把常用场景沉淀下来后面新建模块会省很多重复描述的时间。链路跑通之后剩下的就是把它做稳。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门