ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android 监听用户打开系统相机录像行为:TaoToken 统一 Key 接入与配置验证

Android 监听用户打开系统相机录像行为:TaoToken 统一 Key 接入与配置验证 1. 为什么 Android 端监听系统相机录像这么难Android 系统相机是一个相对独立的系统应用用户从桌面图标、锁屏快捷方式、第三方 App 的Intent拉起它之后录像文件最终会落到MediaStore里。很多做行为审计、家长守护、企业设备管理或者场景联动的开发者第一反应都是「我能不能监听用户打开相机并开始录像」。答案是可以但要分清楚你能拿到的是「录像完成后的回调」还是「录像进行中的状态」——这两件事在 Android 上的实现路径完全不同。我先把结论摆出来Android 没有官方 API 让你实时知道「用户此刻正在录像」但你可以通过MediaStore的ContentObserver感知新视频落盘通过系统相机广播部分 ROM 支持感知录像完成事件再配合前台应用检测判断当前是不是相机在前台。三者组合起来就能在真机上稳定复现「用户打开系统相机录像」这个行为的监听结果。这篇面向需要做行为审计或场景联动的开发者给出可复制的权限与清单配置、监听回调骨架以及用 TaoToken 统一 Key/API 通道完成联调验证的具体动作。TaoToken 在这里的角色不是监听本身而是当你的监听回调触发后需要调用大模型做内容理解、事件分类或上报摘要时用一套统一 Key 打通多家模型省掉每个模型单独申请和切换的麻烦。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 通道是 https://taotoken.net/api 后面配置验证会用到。2. 前置准备权限、清单与 TaoToken 统一 Key2.1 监听录像行为需要哪些权限录像文件落盘后要读取MediaStoreAndroid 10API 29之后引入了分区存储读取其他应用写入的媒体文件需要READ_MEDIA_VIDEOAndroid 13或READ_EXTERNAL_STORAGEAndroid 12 及以下。如果你只是监听自己进程内的变化权限要求会低一些但监听系统相机写入的文件必须拿到媒体读取权限。!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.READ_MEDIA_VIDEO / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 / uses-permission android:nameandroid.permission.QUERY_ALL_PACKAGES tools:ignoreQueryAllPackagesPermission /QUERY_ALL_PACKAGES是为了检测当前前台应用是不是相机Android 11 之后包可见性收紧不加这个你查不到相机包名。注意这个权限在 Google Play 上架时会被审核做设备管理类应用一般没问题普通消费级应用要谨慎。2.2 注册录像完成广播接收器部分 ROM尤其是早期版本和部分定制系统在录像完成后会发出广播常见 action 是com.android.camera.NEW_VIDEO和android.hardware.action.NEW_VIDEO。这个广播不是 AOSP 标准能不能收到取决于厂商所以它只能作为辅助手段不能作为唯一方案。receiver android:name.receiver.CameraVideoReceiver android:enabledtrue android:exportedtrue intent-filter android:priority2147483647 action android:namecom.android.camera.NEW_VIDEO / action android:nameandroid.hardware.action.NEW_VIDEO / category android:nameandroid.intent.category.DEFAULT / data android:mimeType*/* / /intent-filter /receiverpriority拉到最大是为了尽量先于其他接收器拿到事件但 Android 8.0 之后静态注册的隐式广播大部分被限制这个接收器在部分机型上可能收不到所以下面还要用ContentObserver兜底。2.3 用 TaoToken 统一 Key 打通后续模型调用监听回调触发后你大概率要做两件事把事件上报到服务端或者调用大模型对视频元数据做分类摘要。如果每个模型都单独申请 Key、单独改 base_url维护成本很高。TaoToken 提供统一 Key 和统一 API 通道兼容 OpenAI 风格的接口你只需要在控制台创建一个 Key就能在代码里通过同一个base_url调用不同模型。先去控制台创建 Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 然后在 API Keys 页面复制https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言的调用示例。Key 拿到后先别急着写进代码用模型对话页面验证一下通道是否通https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。3. 可复制配置ContentObserver 前台检测 广播三件套3.1 ContentObserver 监听 MediaStore 新增视频这是最可靠的一条路径。系统相机录像完成后会把文件插入MediaStore.Video.Media.EXTERNAL_CONTENT_URI我们注册一个ContentObserver监听这个 Uri 的变化一旦有新增就读取最新一条记录。public class VideoContentObserver extends ContentObserver { private final Context context; private static final String TAG VideoObserver; public VideoContentObserver(Context context, Handler handler) { super(handler); this.context context; } Override public void onChange(boolean selfChange, Uri uri) { super.onChange(selfChange, uri); if (uri null) return; Log.d(TAG, MediaStore changed: uri.toString()); queryLatestVideo(); } private void queryLatestVideo() { String[] projection { MediaStore.Video.Media._ID, MediaStore.Video.Media.DISPLAY_NAME, MediaStore.Video.Media.DATA, MediaStore.Video.Media.DURATION, MediaStore.Video.Media.DATE_ADDED, MediaStore.Video.Media.SIZE }; String sortOrder MediaStore.Video.Media.DATE_ADDED DESC LIMIT 1; try (Cursor cursor context.getContentResolver().query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projection, null, null, sortOrder)) { if (cursor ! null cursor.moveToFirst()) { String name cursor.getString(cursor.getColumnIndexOrThrow( MediaStore.Video.Media.DISPLAY_NAME)); long duration cursor.getLong(cursor.getColumnIndexOrThrow( MediaStore.Video.Media.DURATION)); long size cursor.getLong(cursor.getColumnIndexOrThrow( MediaStore.Video.Media.SIZE)); Log.d(TAG, new video: name duration duration size size); onVideoDetected(name, duration, size); } } } private void onVideoDetected(String name, long duration, long size) { // 这里做你的业务上报、分类、调用模型 } }注册和注销要跟生命周期绑定否则会内存泄漏private VideoContentObserver observer; Override protected void onResume() { super.onResume(); observer new VideoContentObserver(this, new Handler(Looper.getMainLooper())); getContentResolver().registerContentObserver( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, observer); } Override protected void onPause() { super.onPause(); if (observer ! null) { getContentResolver().unregisterContentObserver(observer); } }registerContentObserver第二个参数传true表示监听子 Uri这样新增、修改、删除都能触发。实测下来录像开始和结束都会触发onChange所以你要用DURATION和SIZE过滤掉时长为 0 的中间态只处理真正落盘完成的视频。3.2 前台应用检测判断相机是否活跃ContentObserver只能告诉你「有视频落盘」不能告诉你「用户此刻在录像」。要判断相机是否在前台用UsageStatsManager查询最近的前台应用包名。public boolean isCameraForeground(Context context) { UsageStatsManager usm (UsageStatsManager) context.getSystemService( Context.USAGE_STATS_SERVICE); long now System.currentTimeMillis(); ListUsageStats stats usm.queryUsageStats( UsageStatsManager.INTERVAL_DAILY, now - 60_000, now); if (stats null) return false; String topPackage null; long topTime 0; for (UsageStats s : stats) { if (s.getLastTimeUsed() topTime) { topTime s.getLastTimeUsed(); topPackage s.getPackageName(); } } if (topPackage null) return false; return topPackage.contains(camera) || topPackage.contains(Camera); }UsageStatsManager需要用户在设置里手动授予「使用情况访问权限」代码里要引导用户跳转Intent intent new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS); startActivity(intent);这个权限不是运行时权限没法用requestPermissions申请只能跳设置页让用户手动开。做设备管理类应用时通常由 MDM 统一配置消费级应用要提前跟用户说明用途。3.3 广播接收器兜底把 2.2 里注册的接收器实现补全作为ContentObserver的补充public class CameraVideoReceiver extends BroadcastReceiver { Override public void onReceive(Context context, Intent intent) { if (intent null || intent.getData() null) return; Uri uri intent.getData(); try (Cursor cursor context.getContentResolver().query( uri, null, null, null, null)) { if (cursor ! null cursor.moveToFirst()) { String path cursor.getString(cursor.getColumnIndexOrThrow(_data)); String name cursor.getString(cursor.getColumnIndexOrThrow(_display_name)); Log.d(CameraReceiver, path path name name); } } } }注意_data字段在 Android 10 之后对第三方应用不可见getColumnIndexOrThrow可能抛异常生产环境要用getColumnIndex加判空。4. 验证请求用 TaoToken 通道联调监听结果4.1 监听触发后调用模型做事件分类假设你的业务是检测到新录像后把视频元数据文件名、时长、大小、拍摄时间发给模型让模型判断这是「正常拍摄」还是「可疑行为」然后决定是否上报。用 TaoToken 的统一通道代码里只需要一个base_url和一个 Key。import requests TAOTOKEN_API https://taotoken.net/api API_KEY sk-你的TaoTokenKey def classify_video_event(name, duration, size): prompt ( f视频文件名{name}时长{duration}毫秒 f大小{size}字节。请判断这是正常拍摄还是可疑行为 f只返回 normal 或 suspicious。 ) resp requests.post( f{TAOTOKEN_API}/v1/chat/completions, headers{ Authorization: fBearer {API_KEY}, Content-Type: application/json }, json{ model: gpt-4o-mini, messages: [{role: user, content: prompt}], temperature: 0 }, timeout15 ) resp.raise_for_status() return resp.json()[choices][0][message][content].strip()把classify_video_event挂到 3.1 的onVideoDetected里就完成了「监听 → 分类 → 上报」的闭环。模型名可以按需替换TaoToken 通道兼容多家模型换模型不用改base_url。4.2 真机验证步骤先在模型对话页面确认通道可用https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 发一条测试消息看是否正常返回。然后按下面步骤在真机复现第一步安装 App进入主界面确认ContentObserver已注册Logcat 里能看到registerContentObserver相关日志。第二步打开系统相机切到录像模式录一段 5 秒以上的视频停止录制。第三步观察 Logcat应该先看到MediaStore changed再看到new video: xxx duration5000 sizexxx。第四步如果isCameraForeground返回 true说明前台检测生效。第五步看模型分类结果是否返回normal或suspicious。实测下来ContentObserver在大多数机型上都能稳定触发广播接收器在部分 ROM 上收不到前台检测依赖用户授权。三者组合的覆盖率最高。4.3 用 Coding Plan 做长期联调如果你要长期迭代这套监听逻辑反复改代码、跑真机、调模型建议用 TaoToken 的 Coding Plan把编码和模型调用放在同一个通道里管理省掉频繁切换 Key 的麻烦https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。Claude Code 接入方式在 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-codeutm_campaignrewrite Anthropic 兼容通道在 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-code-anthropicutm_campaignrewrite 。5. 本篇常见错排查5.1 收不到广播最常见的原因是 Android 8.0 之后静态注册的隐式广播被限制com.android.camera.NEW_VIDEO不在豁免列表里。解决办法是改用ContentObserver作为主路径广播只做辅助。另外部分 ROM 根本不发这个广播不要在这上面死磕。5.2 ContentObserver 触发多次录像开始、暂停、恢复、结束都会触发onChange导致同一个视频被处理多次。解决办法是用DATE_ADDED加SIZE做去重维护一个已处理视频 ID 的集合或者只在DURATION 0且SIZE 阈值时才处理。5.3 读取 MediaStore 返回空Android 13 之后没申请READ_MEDIA_VIDEO会直接返回空 Cursor不报错。检查权限是否在运行时申请了并且用户是否真的授予了。Android 12 及以下用READ_EXTERNAL_STORAGE注意maxSdkVersion要设对。5.4 前台检测一直返回 falseUsageStatsManager需要「使用情况访问权限」没授权时queryUsageStats返回空列表。检查是否跳转设置页让用户开了权限部分 ROM 还需要在电池优化白名单里加你的应用。5.5 TaoToken 调用返回 401Key 没复制完整或者Authorization头格式不对。正确格式是Bearer sk-xxx注意Bearer和 Key 之间有一个空格。如果还是 401去 API Keys 页面重新生成一个https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。接入细节看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。5.6 模型返回超时监听回调里做网络请求要放到子线程别在主线程调。另外给requests.post设timeout避免回调阻塞。如果经常超时检查网络环境或者换个响应更快的模型。6. 把监听链路和模型通道接起来整套方案的核心是三条链路并行ContentObserver负责感知视频落盘UsageStatsManager负责判断相机是否在前台广播接收器做兜底。三条链路任意一条触发都可以走 TaoToken 统一通道调用模型做后续处理。Key 在控制台创建通道用https://taotoken.net/api模型对话页面可以快速验证通道是否通。真机验证时重点看 Logcat 里MediaStore changed和new video两条日志是否按顺序出现出现了就说明监听生效剩下的就是业务逻辑的事。
返回列表