【码动四季】秋|《安卓逆向负基础之正向开发实践》02:MuzicBox 本地音乐扫描与基础播放——用 TaoToken 统一 Key 打通调试链路

发布时间:2026/10/18 20:08:50

【码动四季】秋|《安卓逆向负基础之正向开发实践》02:MuzicBox 本地音乐扫描与基础播放——用 TaoToken 统一 Key 打通调试链路 1. 从零跑通 MuzicBox本地音乐扫描与基础播放到底难在哪安卓逆向负基础的同学第一次做正向开发最容易卡住的地方不是 Java 语法而是「系统能力怎么申请、数据从哪来、播放器怎么被驱动」这三件事。MuzicBox 这个项目就是拿来练这条链路的它要读取手机里的本地音乐把歌曲列出来点一下能播放还能切上一首下一首。听起来简单但真正动手你会发现权限在 Android 13 前后是两套写法媒体库查询要经过 ContentResolver 和 Cursor播放又要处理异步准备和资源释放。任何一个环节漏掉界面就是空白或者点了没反应。这篇是《安卓逆向负基础之正向开发实践》的第二篇对应版本 v0.2-local-player。我会把权限声明、MediaStore 扫描、MediaPlayer 播放这三段可复制的代码完整给出来同时补上一个很多人忽略的环节调试阶段用 TaoToken 统一管理模型接口的 Key把「写代码」和「调接口」两条链路分开避免 Key 散落在各个配置文件里。你跟着做能拿到一个真机上能扫描、能播放的最小可运行 Demo。适合谁看刚学完 Activity 和 XML 布局、想做一个完整功能闭环的初学者或者做逆向分析时想搞清楚「正常 App 应该怎么读媒体库」的同学。核心检索词就三个安卓逆向、正向开发、MuzicBox 本地音乐扫描与基础播放。下面从权限开始一步步把链路搭起来。2. TaoToken 前置准备统一 Key 打通调试链路在写扫描和播放代码之前先把调试用的接口配置理清楚。正向开发过程中经常要调用模型接口做代码补全、日志分析或者文档查询如果每个工具都单独配一份 Key改起来很乱。TaoToken 的作用就是提供一个统一的入口把模型对话、编码计划、控制台和 API Key 管理集中在一处。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 注意 API 地址不带 UTM 参数。你需要先拿到一个可用的 Key。进入控制台创建 API Key路径是 https://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 。这个 Key 后面会写进本地配置文件不要提交到 Git 仓库。模型 ID 的选择上做代码相关的任务可以用 coding-plan 里推荐的模型页面在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。如果你只是想先验证接口通不通用模型对话页面最快地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到参数问题先查这里。这里要强调一个原则TaoToken 是接口聚合入口不是让你把生产数据库或者敏感业务直连上去。调试阶段用它统一 Key 是为了减少配置分散正式环境该走自己的网关还是走自己的网关。另外任何涉及网络代理、跨境访问的说法都不要碰我们只谈接口配置本身。配置的核心是三件套Base URL、Key、Model ID。Base URL 用 https://taotoken.net/api Key 用你刚复制的那串Model ID 按 coding-plan 或模型对话页面里列出的填。这三样凑齐后面在 Android 工程里做调试接口调用时就不会出现「Key 找不到」或者「地址写错」的问题。下一节开始进入真正的 Android 配置。3. 可复制配置权限声明、MediaStore 查询与 MediaPlayer 播放这一节是全文的技术主体所有代码都可以直接复制进工程。先看 AndroidManifest.xml 的权限声明。Android 13API 33把媒体读取权限拆开了音频要用 READ_MEDIA_AUDIO而 Android 12 及以下还是 READ_EXTERNAL_STORAGE。所以 Manifest 里两个都要写旧的那个用 maxSdkVersion 限制在 32uses-permission android:nameandroid.permission.READ_MEDIA_AUDIO / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 /注意这两个标签要放在manifest里面、application外面。Manifest 声明只是「登记」真正读取还要运行时授权。运行时根据设备版本选择权限常量private String getAudioPermission() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { return Manifest.permission.READ_MEDIA_AUDIO; } return Manifest.permission.READ_EXTERNAL_STORAGE; }TIRAMISU 对应 API 33。这里判断的是设备实际运行的版本不是 compileSdk。检查权限用 checkSelfPermission没有就发起申请private boolean hasAudioPermission() { return checkSelfPermission(getAudioPermission()) PackageManager.PERMISSION_GRANTED; } private void checkPermissionAndScan() { if (hasAudioPermission()) { scanLocalMusic(); return; } requestPermissions(new String[]{getAudioPermission()}, REQUEST_AUDIO_PERMISSION); }回调里根据 grantResults 决定是否继续扫描Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode ! REQUEST_AUDIO_PERMISSION) return; if (grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { scanLocalMusic(); } else { scanStatusText.setText(R.string.permission_denied); } }权限搞定后是 MediaStore 查询。核心是四个对象MediaStore 定义集合 URI 和字段ContentProvider 在系统侧提供数据ContentResolver 是 App 的访问入口Cursor 保存结果。查询代码放在 LocalMusicScanner 里Uri collection MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; String[] projection { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.DURATION }; String selection MediaStore.Audio.Media.IS_MUSIC ! 0 AND MediaStore.Audio.Media.DURATION 0; String sortOrder MediaStore.Audio.Media.TITLE COLLATE NOCASE ASC; try (Cursor cursor contentResolver.query(collection, projection, selection, null, sortOrder)) { if (cursor null) return songs; int idColumn cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); int titleColumn cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE); int artistColumn cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST); int durationColumn cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION); while (cursor.moveToNext()) { long id cursor.getLong(idColumn); String title cursor.getString(titleColumn); String artist cursor.getString(artistColumn); long duration cursor.getLong(durationColumn); Uri songUri ContentUris.withAppendedId(collection, id); songs.add(new Song(id, title, artist, duration, songUri)); } }这里用 try-with-resources 保证 Cursor 关闭。ContentUris.withAppendedId把记录 ID 拼到集合 URI 后面得到类似content://media/external/audio/media/1000000813的歌曲地址这个地址后面要交给 MediaPlayer。扫描要放在工作线程结果回主线程刷新列表scanExecutor.execute(() - { ListSong scannedSongs LocalMusicScanner.scan(getContentResolver()); runOnUiThread(() - { if (isFinishing() || isDestroyed()) return; songs.clear(); songs.addAll(scannedSongs); songAdapter.notifyDataSetChanged(); }); });播放部分点击列表项拿到 position取出 Song 和 URI创建 MediaPlayer 并异步准备private void playSong(int position) { releasePlayer(); Song song songs.get(position); MediaPlayer newPlayer new MediaPlayer(); mediaPlayer newPlayer; try { newPlayer.setDataSource(this, song.getUri()); newPlayer.setOnPreparedListener(player - { isPreparing false; player.start(); playPauseButton.setText(R.string.pause); }); newPlayer.setOnCompletionListener(player - playNext()); newPlayer.setOnErrorListener((player, what, extra) - { Log.e(TAG, MediaPlayer error: what what , extra extra); releasePlayer(); Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show(); return true; }); newPlayer.prepareAsync(); } catch (IOException e) { Log.e(TAG, setDataSource failed, e); releasePlayer(); } }资源释放不能忘切歌和页面离开都要调用private void releasePlayer() { isPreparing false; if (mediaPlayer ! null) { mediaPlayer.reset(); mediaPlayer.release(); mediaPlayer null; } } Override protected void onStop() { super.onStop(); releasePlayer(); } Override protected void onDestroy() { releasePlayer(); scanExecutor.shutdownNow(); super.onDestroy(); }如果你在工程里用 Codex 或者类似工具做调试auth.json 里也要把三件套写全Base URL 用 https://taotoken.net/api Key 用控制台复制的Model ID 按 coding-plan 页面填。Cline MCP 或者 CC Switch 的配置同理Base URL、Key、Model ID 一个都不能少缺一个就会报认证失败。配置片段建议单独放一个不提交的本地文件避免 Key 泄露。4. 验证请求与成功结果真机跑一遍扫描和播放代码写完先构建 Debug APK跑一遍 Android Lint。构建通过后把 APK 装到真机上。测试素材准备两首 MP3放到手机的 Music/MuzicBox 目录然后让系统媒体扫描器更新 MediaStore。有些设备需要重启或者用文件管理器触发一次扫描否则新放进去的文件查不到。权限弹窗出现时点允许。如果之前拒绝过要去系统设置里手动打开否则 checkSelfPermission 一直返回拒绝扫描不会执行。授权成功后列表应该显示歌曲标题和歌手。我实测用两首带 ID3 标签的文件界面显示的是标签里的 TITLE 和 ARTIST不是文件名。这说明数据确实来自 MediaStore而不是代码截取文件名。点一首歌MediaPlayer 走 setDataSource → prepareAsync → onPrepared → start 这条链能听到声音按钮文字变成「暂停」。点下一首旧播放器 release新播放器创建切换正常。按 Home 键让页面不可见onStop 触发 releasePlayer声音停止。返回应用重新点播放又能正常出声。验证接口配置是否生效可以在模型对话页面发一条测试请求地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。如果返回正常说明 Base URL、Key、Model ID 三件套没问题。这一步和 Android 播放链路是分开的但都属于「调试链路」的一部分统一 Key 的好处就是这里改一次其他地方不用动。判断扫描是否真的成功不要只看界面有没有文字。可以在 scan 方法里打日志输出 cursor.getCount()确认查询返回了几条记录。如果 count 是 0但文件确实在手机里多半是媒体扫描器还没登记或者 selection 条件把记录过滤掉了。IS_MUSIC ! 0 会排除系统没认定为音乐的音频DURATION 0 会排除时长无效的记录这两个条件都可能让文件「消失」。播放成功的判断也不能只看按钮文字。真正开始播放的调用是 MediaPlayer.start()按钮文字只是界面反馈。如果 start 被调用但没声音要检查音频焦点、系统音量、文件是否损坏。这一章还没引入音频焦点管理留到第三章处理。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth调试阶段最容易撞上的几类报错这里对照真实错误信息说清楚。第一类是 401。接口返回 401 基本就是 Key 无效或者没带上。检查三件套Base URL 是不是 https://taotoken.net/api Key 是不是从 API Keys 页面复制的完整串Model ID 是不是页面里列出的。如果用了 Codex 的 auth.json确认字段名没写错Key 没有多余空格。Cline MCP 或者 CC Switch 里配置时Base URL 末尾不要多加斜杠有些客户端对路径拼接敏感。第二类是 local proxy failed。这个报错通常出现在客户端尝试走本地代理但代理没起来。先确认你没有配置任何本地代理端口Base URL 直接指向 https://taotoken.net/api 即可。如果客户端默认开了代理开关关掉它。这类问题和网络环境无关纯粹是客户端配置项的问题。第三类是 reading choices 相关报错。这通常发生在解析模型返回结构时客户端期望的字段和实际返回不一致。先确认 Model ID 填对了不同模型返回结构可能有差异。如果是在模型对话页面测试正常、在代码里报错检查请求体里的 model 字段和实际使用的模型是否一致。文档页面 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里有请求示例对照一下。第四类是 OAuth 相关报错。如果你用的工具走 OAuth 流程报错多半是回调地址或者 token 过期。这类工具建议直接用 API Key 模式避免 OAuth 的额外复杂度。Claude Code 接入时如果报 OAuth 错误检查是不是把 Anthropic 的配置和 TaoToken 的配置混在一起了两者 Base URL 不同不要串用。Claude Code 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里有说明。Android 侧的常见错也要提一下。扫描返回空列表先查权限是否真的授予再查媒体扫描器是否登记了文件。播放报 what1 extra-2147483648 这类错误多半是数据源打不开或者格式不支持检查 URI 是否有效、文件是否损坏。Cursor 忘记关闭会导致资源泄漏用 try-with-resources 可以避免。MediaPlayer 忘记 release 会导致切歌时旧声音还在releasePlayer 要在切歌前调用。还有一个容易忽略的点Activity 销毁后回调还在跑。扫描任务提交后如果页面已经销毁runOnUiThread 里的更新会引用失效的 Activity。代码里用 isFinishing() 和 isDestroyed() 做了检查onDestroy 里 shutdownNow 关闭执行器。这几处不加偶发崩溃很难查。6. 语义一致 CTA把调试链路固定下来走到这里MuzicBox 的扫描和播放链路已经能跑通了。权限、MediaStore、MediaPlayer 这三段代码是后面所有功能的基础第三章加 Service 和通知栏控制时播放器会从 Activity 搬到 Service但 setDataSource 和 prepareAsync 这条链不会变。调试接口这块建议把 TaoToken 的三件套固定成一个本地配置文件Base URL 用 https://taotoken.net/api Key 从 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 复制Model ID 按 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 里列的填。需要验证模型是否可用时去 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 发一条测试请求。遇到接入问题先查 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。长期做编码和 Agent 任务的话coding-plan 页面里有适合的模型组合地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。控制台在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。Claude Code 接入参考 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里的说明。最后留一个实用技巧扫描和播放的日志分开打 TAG扫描用 LocalMusicScanner播放用 MainActivity排查时一眼能看出问题出在哪条链路。Cursor 的列下标不要写死数字用 getColumnIndexOrThrow 按字段名取换设备或者换系统版本时不容易崩。MediaPlayer 的 setDataSource 一定要放在 try 里content URI 打不开会抛 IOException不捕获直接崩。这几处处理好第三章加 Service 的时候会省很多事。
延伸阅读

更多相关文章

2026/10/18 20:08:50

Delphi老项目使用Indy 10.2.3:安装配置、核心编程与避坑指南

简介:一份面向 Delphi 7~2007 开发者的 Indy 10.2.3 网络组件库完整源码包。Indy 是开源跨平台的网络通信组件集,它提供了 TCP/IP、HTTP、FTP、SMTP、POP3 及 SSL/TLS 支持,可直接用于构建客户端与服务端网络应用。资源共 839 个文…

2026/10/18 20:08:50

Git clone报错404:智能HTTP协议与info/refs路径解析

1. 项目概述:这不是网络问题,是 Git 请求路径被拒绝的明确信号“git clone 代码报错:error: RPC failed; result22, HTTP code 404”——这个报错我在过去三年里至少处理过87次,覆盖高校实验室、初创团队和中型技术部…

2026/10/18 21:14:05

YOLOv11管道泄漏检测系统全流程开发实战

管道泄漏检测这个方向,我接触了好几年,说实话在工业现场对它的认知一直在刷新。最早以为泄漏检测就是装个流量计、传感器的事,后来真正做过图像识别类项目才明白,视觉方案在管道巡检场景里有不可替代的位置——不用停产、不用接触…

2026/10/18 21:14:05

用千问大模型实现文档表格转JSON的完整实践指南

每次拿到一堆PDF、Word或者扫描件,里面有大量表格数据需要录入系统,我都觉得是体力活。直到我把千问大模型接进来,让它把文档里的表格信息直接吐成JSON,整个流程才真正跑通。这篇文章就想把我在这个项目里的完整思路、踩过的坑、写…

2026/10/18 21:14:05

JavaScript新闻游戏开发:轻量级叙事系统架构与状态驱动设计

简介:这是一款由英国《金融时报》开发的叙事性新闻游戏,以JavaScript为核心技术栈,面向前端开发者、新闻交互设计学习者及数字人文研究者,帮助理解零工经济下网约车司机的真实生存逻辑与决策困境。玩家需在一周内通过接单、权衡成…

2026/10/18 21:09:04

微信QQ WebView能力限制与H5适配实战指南

1. 这个提示到底在“警告”什么:从用户视角还原真实交互链路你正在微信里点开一条朋友发来的活动链接,页面刚加载一半,顶部突然弹出一行灰底白字:“请在浏览器中打开”。你下意识点右上角三个点,选“在浏览器中打开”&…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑