资讯详情

cw-omnibus 文档访问持久化实战:ACTION_GET_CONTENT / ACTION_OPEN_DOCUMENT 与 takePersistableUriPermission 深度解析

📅 2026/10/15 2:03:28 | 华诺云谱 👁 阅读
cw-omnibus 文档访问持久化实战:ACTION_GET_CONTENT / ACTION_OPEN_DOCUMENT 与 takePersistableUriPermission 深度解析
示例工程教程【免费下载链接】cw-omnibusSource code to omnibus edition of _The Busy Coders Guide to Android Development_项目地址https://gitcode.com/gh_mirrors/cw/cw-omnibus点击查看免费下载导读本文围绕 cw-omnibusThe Busy Coders Guide to Android Development全量示例合集中的 Durable 示例 展开讲解 Android 中如何让通过ACTION_GET_CONTENT或ACTION_OPEN_DOCUMENT获得的文档内容具备持久访问能力。读完本文你将掌握takePersistableUriPermission()的授权原理、getPersistedUriPermissions()的权限校验方式以及可持久化则持久化、否则回退本地副本的双保险策略并理解为何要用IntentService承载这类后台处理。该示例配套源码位于 Documents/Durable对应的书籍章节为 consuming documents消费文档。示例定位从临时访问到持久访问在 Android 中通过ACTION_GET_CONTENT或ACTION_OPEN_DOCUMENT让用户选择内容后应用拿到的是一份临时 URI 授权temporary grant一旦进程重启或授权过期这份授权就会失效应用可能再也无法访问该文档。Durable 示例要解决的问题正是这一点——它演示如何持久访问通过这两种 Action 获取的内容。对比仓库中的 Consumer 示例基础版消费文档可以看清本示例的增量价值Consumer 只负责发起选择、读取元数据Display name / Size不处理任何持久化而 Durable 在拿到 URI 后会先尝试takePersistableUriPermission()将授权升级为可持久化授权一旦失败则把内容复制到应用私有目录从源码结构看这正是持久访问与临时访问的分水岭。两条内容获取路径ACTION_GET_CONTENT 与 ACTION_OPEN_DOCUMENTDurable 的界面操作入口定义在 ConsumerFragment.java菜单配置见 actions.xml两个菜单项get与open对应文案在 strings.xmlGet Content 与 Open Document。ACTION_OPEN_DOCUMENTOpen Document 菜单项TargetApi(Build.VERSION_CODES.KITKAT) private void open() { Intent i new Intent() .setType(*/*) .setAction(Intent.ACTION_OPEN_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE); startActivityForResult(i, REQUEST_OPEN); }要点ACTION_OPEN_DOCUMENT自API 19Android 4.4 KitKat引入因此open()方法标注了TargetApi(Build.VERSION_CODES.KITKAT)setType(*/*)表示允许任意 MIME 类型的文档必须添加CATEGORY_OPENABLE类别声明只接受可打开的文档菜单中的open项默认android:enabledfalse仅在Build.VERSION.SDK_INT KITKAT时才启用见 ConsumerFragment.java保证旧系统上不会暴露不可用的入口选择请求码为REQUEST_OPEN 1337。ACTION_GET_CONTENTGet Content 菜单项private void get() { Intent i new Intent() .setType(image/png) .setAction(Intent.ACTION_GET_CONTENT) .addCategory(Intent.CATEGORY_OPENABLE); startActivityForResult(i, REQUEST_GET); }要点ACTION_GET_CONTENT是历史悠久的通用内容选择 Action不要求 API 19兼容性更广本示例将类型限定为image/png用于演示与open()的差异可对比*/*与具体 MIME 的用法同样需要CATEGORY_OPENABLE请求码为REQUEST_GET REQUEST_OPEN 1 1338。两条路径最终都在onActivityResult()中汇合只要resultCode Activity.RESULT_OK就把resultData.getData()得到的文档 URI 连同Intent.FLAG_GRANT_READ_URI_PERMISSION一起交给DurablizerService处理见 ConsumerFragment.java。核心机制持久化 URI 权限takePersistableUriPermissionDurable 的灵魂代码位于 DurablizerService.java 的obtainDurablePermission()方法private boolean obtainDurablePermission(Uri document) { boolean weHaveDurablePermissionfalse; if (Build.VERSION.SDK_INTBuild.VERSION_CODES.KITKAT) { int permsIntent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; try { getContentResolver() .takePersistableUriPermission(document, perms); for (UriPermission perm : getContentResolver().getPersistedUriPermissions()) { if (perm.getUri().equals(document)) { weHaveDurablePermissiontrue; } } } catch (SecurityException e) { // OK, we were not offered any persistable permissions } } return(weHaveDurablePermission); }原理拆解takePersistableUriPermission(Uri, int modeFlags)将当前的一次性授权升级为可持久化的授权。升级成功的先决条件是返回结果 Intent 的 flags 中包含FLAG_GRANT_PERSISTABLE_URI_PERMISSION且来源提供者ContentProvider愿意授予持久权限请求的权限位是READ | WRITE两者组合。需要注意如果实际拿到手的授权并不包含写权限例如本示例转发 Intent 时只附加了FLAG_GRANT_READ_URI_PERMISSION那么请求超出实际范围的权限位就可能抛出SecurityException——代码正是通过try/catch捕获它将其视为对方没有提供可持久化权限的正常分支权限是否真的落地不只看takePersistableUriPermission()是否抛异常代码还会遍历getPersistedUriPermissions()系统返回的全部已持久化授权列表逐条比对 URI 是否命中。这种二次确认比单凭调用成功与否更可靠整个调用只针对content://方案的 URI。ACTION_GET_CONTENT的结果通常来自第三方应用很多时候并不支持持久化这正是回退分支存在的意义getPersistedUriPermissions()返回的授权在应用重启后依然有效——这是Durable持久一词的由来见下方 Diceware 示例的佐证。底层依赖链仓库中另一处使用完全相同模式的代码是 Diceware 示例的 PassphraseFragment.java它把持久化成功的 URI 写入SharedPreferences键uri下次启动直接读取该 URI 并通过openInputStream访问。从源码结构看这验证了takePersistableUriPermission授予的权限在进程重启、应用再次启动后仍然存在与 Durable 的DurablizerService是同一套尝试持久化 → 失败则本地副本的模板只是线程模型改用了 RxJava。兜底策略本地副本makeLocalCopy当持久化授权不可用时Durable 选择把文档内容复制到应用私有目录从而在复制完成的那一时刻得到一份彻底属于自己、不再依赖任何授权的内容private Uri makeLocalCopy(Uri document) { DocumentFileCompat docFilebuildDocFileForUri(document); Uri resultnull; if (docFile.getName()!null) { try { String ext MimeTypeMap.getSingleton().getExtensionFromMimeType(docFile.getType()); if (ext!null) { ext.ext; } File fFile.createTempFile(cw_, ext, getFilesDir()); docFile.copyTo(f); resultUri.fromFile(f); } catch (Exception e) { Log.e(getClass().getSimpleName(), Exception copying content to file, e); } } return(result); }要点扩展名通过MimeTypeMap.getSingleton().getExtensionFromMimeType(docFile.getType())由 MIME 类型反推例如image/png→.png取不到时为null不附加扩展名临时文件创建在getFilesDir()应用内部存储前缀cw_因此完全不需要任何存储权限DocumentFileCompat.copyTo(f)完成从 URI 文档到本地文件的拷贝最终返回Uri.fromFile(f)的file://URI进入后续统一处理流程。统一处理流程DurablizerService 与 EventBus 回传为什么用 IntentServiceDurablizerService继承自IntentService见 DurablizerService.java核心逻辑都在onHandleIntent()中Override protected void onHandleIntent(Intent intent) { Uri documentintent.getData(); boolean weHaveDurablePermissionobtainDurablePermission(document); if (!weHaveDurablePermission) { documentmakeLocalCopy(document); } if (weHaveDurablePermission || document!null) { Log.d(getClass().getSimpleName(), document.toString()); DocumentFileCompat docFilebuildDocFileForUri(document); Log.d(getClass().getSimpleName(), Display name: docFile.getName()); Log.d(getClass().getSimpleName(), Size: Long.toString(docFile.length())); EventBus.getDefault().post(new ContentReadyEvent(docFile)); } }使用IntentService而非直接在 Activity 中处理的理由非常充分takePersistableUriPermission与权限遍历涉及与系统ContentProvider的跨进程通信可能耗时makeLocalCopy可能涉及大文件的完整拷贝绝不能阻塞主线程全部工作在onHandleIntent的后台线程串行执行天然安全处理完成后通过EventBus.post()发送ContentReadyEvent将结果带回 UI 线程。结果回传 UIConsumerFragment.java 通过 EventBus 订阅事件Subscribe(threadModeThreadMode.MAIN) public void onContentReady(DurablizerService.ContentReadyEvent event) { logToTranscript(event.docFile.getUri().toString()); logToTranscript(Display name: event.docFile.getName()); logToTranscript(Size: Long.toString(event.docFile.length())); }Fragment 在onStart()中注册 EventBus、在onStop()中反注册见 ConsumerFragment.java避免泄漏与重复接收threadModeThreadMode.MAIN确保在主线程更新 UI事件类ContentReadyEvent携带DocumentFileCompat对象可直接取 URI、显示名与大小。DocumentFileCompat统一 content:// 与 file:// 的访问层buildDocFileForUri()根据 URI scheme 分派见 DurablizerService.javaprivate DocumentFileCompat buildDocFileForUri(Uri document) { DocumentFileCompat docFile; if (document.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { docFileDocumentFileCompat.fromSingleUri(this, document); } else { docFileDocumentFileCompat.fromFile(new File(document.getPath())); } return(docFile); }DocumentFileCompat来自依赖com.commonsware.cwac:document:0.2.0CWAC-Document 库对content://URI 使用fromSingleUri()对回退产生的file://URI 使用fromFile()这一抽象让持久化成功的 content URI与本地副本的 file URI两种形态在后续展示getName()、length()上完全对齐。界面与展示界面极简一个ScrollView包裹TextView见 activity_main.xmllogToTranscript()追加文本并自动滚动到底部ConsumerFragment.java把 URI、显示名、大小逐行打印出来作为操作反馈。工程配置与运行前提build.gradle 依赖Durable 的 app/build.gradle 展示了完整工程配置apply plugin: com.android.application android { compileSdkVersion 27 buildToolsVersion 27.0.3 defaultConfig { minSdkVersion 15 targetSdkVersion 27 applicationId com.commonsware.android.documents.durable } } repositories { maven { url https://s3.amazonaws.com/repo.commonsware.com } } dependencies { implementation com.android.support:support-fragment:27.1.0 implementation org.greenrobot:eventbus:3.0.0 implementation com.commonsware.cwac:document:0.2.0 }关键点minSdkVersion 15保证应用可在 KitKat 之前的设备运行对应代码中大量Build.VERSION.SDK_INT KITKAT的运行时判断support-fragment提供FragmentActivity与FragmentMainActivity继承FragmentActivity动态添加ConsumerFragment见 MainActivity.javaeventbus3.0.0 承担服务 → UI 的线程内事件回传cwac:document0.2.0 提供DocumentFileCompat文档抽象仓库源repo.commonsware.com负责分发 CWAC 系列库。AndroidManifest 注册AndroidManifest.xml 中DurablizerService的注册方式值得注意service android:name.DurablizerService android:exportedfalse/serviceandroid:exportedfalse明确该服务仅供本应用内部使用不对外暴露——配合ConsumerFragment中startService(new Intent(getActivity(), DurablizerService.class)...)的显式 Intent 调用符合 Android 8.0 前后对后台服务的常规约束应用主题基于Theme.Holo.Light.DarkActionBar见 styles.xml。运行与验证要点在 KitKat 及以上设备运行Open Document 菜单可用选择任意文件系统文档选择器若来源文档提供者支持持久授权ACTION_OPEN_DOCUMENT的提供者通常支持界面将打印原始content://URI证明直接持久化成功若来源不支持常见于部分ACTION_GET_CONTENT场景服务回退为本地副本界面打印file://URI显示名与大小同样有效可以结合 Logcat 观察DurablizerService的调试日志Display name / Size与 UI 输出互为印证。与仓库其他示例的印证Durable 模式的完整生命周期Durable 是仓库文档持久访问主题族的起点其余示例共同补全了这一模式的边界示例文件印证点ConsumerDocuments/Consumer/app/src/main/java/com/commonsware/android/documents/consumer/ConsumerFragment.java无持久化基线CursorLoaderOpenableColumns查询 DISPLAY_NAME / SIZE展示临时授权下的一次性消费DicewareDocuments/Diceware/app/src/main/java/com/commonsware/android/diceware/PassphraseFragment.java持久化 URI 写入 SharedPreferences 跨启动复用验证持久语义失败时同样走makeLocalCopy兜底TinyTextEditorDocuments/TinyTextEditor/app/src/main/java/com/commonsware/android/tte/DocumentStorageService.java读写双向场景takePersistableUriPermission后关闭文档时调用releasePersistableUriPermission()释放授权补齐了 Durable 未覆盖的释放环节DocumentTreeDocuments/DocumentTree/app/src/main/java/com/commonsware/android/documenttree/DocumentHelper.java面向目录的ACTION_OPEN_DOCUMENT_TREE是单文档持久化在整棵树维度上的延伸从这套示例族的源码结构可以推断出完整的持久化生命周期选择文档GET_CONTENT / OPEN_DOCUMENT→ 尝试 takePersistableUriPermission → 成功则复用持久授权可跨进程重启→ 失败则复制本地副本 → 长期使用后如 TinyTextEditor 关闭文档时releasePersistableUriPermission 释放授权。Durable 正是这条链路的起点与最小闭环。小结Durable 模式的关键要点授权升级takePersistableUriPermission()是持久访问的唯一官方途径前提是结果 Intent 带FLAG_GRANT_PERSISTABLE_URI_PERMISSION且来源提供者愿意授予结果校验用getPersistedUriPermissions()二次确认授权是否真正落地而非只依赖调用返回值异常兜底SecurityException视为未提供可持久化权限的正常信号回退到makeLocalCopy本地副本保证任何来源的文档都能在当下被可靠使用线程模型将授权与拷贝等耗时工作放进IntentService通过 EventBus 回传主线程更新 UI避免阻塞界面兼容分层minSdk 15 运行时KITKAT判断让老设备走ACTION_GET_CONTENT路径、新设备启用ACTION_OPEN_DOCUMENT各得其所。这套Durable 双保险模式是 Android 文档消费类应用文本编辑器、文件管理器、笔记应用等在《Busy Coders Guide》体系下的标准答案仓库中的 Durable 完整源码 可作为直接对照实现的参考蓝本。赞分享示例工程教程【免费下载链接】cw-omnibusSource code to omnibus edition of _The Busy Coders Guide to Android Development_项目地址https://gitcode.com/gh_mirrors/cw/cw-omnibus点击查看免费下载相关推荐cw-omnibus 实战解析用 ACTION_OPEN_DOCUMENT 消费 Android 文档内容cw omnibus 实战解析用 ACTION_OPEN_DOCUMENT 消费 Android 文档内容 导读 在 Android 应用开发中让用户挑选示例工程教程cw-omnibus 实战用 ACTION_OPEN_DOCUMENT 打造可持久化词表的 Diceware 密码生成器cw omnibus 实战用 ACTION_OPEN_DOCUMENT 打造可持久化词表的 Diceware 密码生成器 本文围绕 cw omnibus 仓库示例工程教程Android Snackbar 带操作按钮实战cw-omnibus SnackbarAction 示例深度解析Android Snackbar 带操作按钮实战cw omnibus SnackbarAction 示例深度解析 本篇技术指南基于 cw omnibus 仓库示例工程教程上一篇Rockstar语法完全解析掌握scream、shout、whisper等摇滚式输出下一篇PowerToys 上手屏幕取色、批量压图、一键启动第一次使用 10 分钟跑通创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑