跳过主要内容

安卓 Scoped Storage 在 Capacitor 应用程序中

Android Capacitor 中的scoped storage简介:Android 10到14中发生了什么变化,哪些权限仍然有效,MediaStore,SAF和安全目录。

Android Capacitor 中的scoped storage

Android scoped storage限制了Capacitor应用程序可以在其自身文件夹之外读取和写入的内容。从Android 10开始,完全从Android 11开始,应用程序可以自由使用其私有目录,可以通过MediaStore添加文件到共享集合,并需要用户选择其他任何内容通过Storage Access Framework。这一指南解释了每个Android版本发生了什么变化,哪些Capacitor Directory 值仍然有效,以及如何在Capacitor 8上以支持的方式保存、导入和导出文件。

每个版本发生了什么变化

Android API Capacitor中的变化
10 29 scoped storage被引入。应用程序可以通过 requestLegacyExternalStorage="true" 在开发 API 29 的应用时
11 30 scoped storage对目标 requestLegacyExternalStorage 被忽略。 WRITE_EXTERNAL_STORAGE 不再授予任何权限。 MANAGE_EXTERNAL_STORAGE 被引入。树选择器不能选择下载根或卷根
12 31-32 没有主要的存储变化
13 33 READ_EXTERNAL_STORAGE 被替换为 READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO.其他应用程序的文档没有权限授予访问
14 34 Partial photo access with READ_MEDIA_VISUAL_USER_SELECTED
15 和 16 35-36 没有新存储模型,Play 政策对照片和视频权限实施

Capacitor 8 个目标 API 36,因此您的应用程序始终在现代设备上运行全局存储。没有标志可以恢复旧的行为。

在较旧的 Capacitor code 中

Code 编写用于 Capacitor 2 或 3,或者从 Cordova 迁移,通常在新设备上静默失败:

  • Filesystem.writeFile({ directory: Directory.Documents }) 或 Directory.ExternalStorage 到“将文件保存到手机”。在 Android 11+ 上,这些路径指向共享存储,写入失败或文件无法在重新安装后读取。
  • 读取任意路径,如 /storage/emulated/0/Download/file.pdf 从一个路径中接收
  • 请求 READ_EXTERNAL_STORAGE 列出用户的文档。从 Android 13+ 开始,权限对非媒体文件无效。
  • 使用 Filesystem.requestPermissions() 并且假设 granted 意味着应用程序可以在任何地方写入。
  • 从一个 content:// URI返回的文件路径

仍然不需要任何权限就可以工作

应用程序自己的目录不受scoped storage影响,需要的权限是:

Capacitor Directory Android路径 可见 在卸载时删除
Data, Library, LibraryNoCloud /data/data/<package>/files 否 Yes
Cache /data/data/<package>/cache No Yes
External /storage/emulated/0/Android/data/<package>/files 仅通过USB或一些文件管理器 Yes
ExternalCache /storage/emulated/0/Android/data/<package>/cache 相同 Yes
Documents 共享 Documents 文件夹 Yes No
ExternalStorage 共享存储根目录 Yes No

安全的前四行 Documents 并且 ExternalStorage are the legacy rows: since Android 11 your app can only see files there that it created itself, and after a reinstall it loses access even to those. Treat them as unsupported for new code.

External 保存用户可以找到文件:MediaStore Android/data所以在实践中,它仍然是私有的。

MediaStore

Android 11 之后,应用程序只能看到它自己创建的文件 Downloads, Pictures, Movies, Music 并且 Documents 重新安装后,即使是那些文件也会丢失访问权

@capgo/capacitor-file-sharer 对于大文件(离线视频,地图)有用,因为它可能存储在更大的分区上,文件不属于 Auto Backup。 save():

bun add @capgo/capacitor-file-sharer
bunx cap sync
import { FileSharer } from '@capgo/capacitor-file-sharer';
import { Filesystem, Directory } from '@capacitor/filesystem';

const { uri } = await Filesystem.getUri({ path: 'exports/invoice-1042.pdf', directory: Directory.Data });

const result = await FileSharer.save({
  filename: 'invoice-1042.pdf',
  path: uri,
  contentType: 'application/pdf',
  android: {
    saveDirectory: 'downloads',     // 'downloads' | 'pictures' | 'movies' | 'music' | 'documents'
    relativePath: 'MyApp/Invoices', // subfolder, Android 10+
  },
});

console.log(result.uri); // content:// URI of the saved file

On iOS 中,同样的调用会打开分享面板,用户选择“保存到文件”,因此一个 code 路径可以同时支持两种平台。

On Android 9 以下版本,写入共享存储仍然需要 WRITE_EXTERNAL_STORAGE. 只需为这些版本声明:

<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28" />

Capacitor 8 支持 Android 7 (API 24) 以上版本,因此如果您仍然将应用程序分发到 Android 7 到 9 设备,请保留该行。

照片和视频

对于媒体,优先使用平台流程而不是自己写文件。 @capacitor/camera 将写入相册中 saveToGallery: true,并且只需要在旧 Android 版本上请求旧的存储权限。请参阅我们的指南 在 Capacitor 中拍摄和编辑照片.

读取用户文件:选择器而不是权限

scoped storage 期望用户选择文件。Android 选择器返回一个 URI,应用程序可以读取,即使它没有存储权限。 content:// scoped storage 期望用户选择文件。Android 选择器返回一个 URI,应用程序可以读取,即使它没有存储权限。

import { CapgoFilePicker } from '@capgo/capacitor-file-picker';
import { Filesystem, Directory } from '@capacitor/filesystem';

const { files } = await CapgoFilePicker.pickFiles({ types: ['application/pdf'], limit: 1 });
const picked = files[0];

// Copy into app storage right away: picker grants can expire
await Filesystem.copy({
  from: picked.path!,
  to: `imports/${picked.name}`,
  toDirectory: Directory.Data,
});

对于照片和视频, @capgo/capacitor-file-picker’s pickMedia 和 @capacitor/camera’s chooseFromGallery 使用 Android 13+ 的 Android Photo Picker,不需要任何权限。

避免 READ_MEDIA_* 权限

Google Play 的照片和视频权限政策仅允许 READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO 为需要广泛图库访问的应用程序,例如图库应用程序或照片编辑器。其他应用程序必须使用选择器并在 Play Console 中声明。如果您使用的插件声明了这些权限,请从合并的清单中删除它们:

<uses-permission
    android:name="android.permission.READ_MEDIA_IMAGES"
    tools:node="remove" />
<uses-permission
    android:name="android.permission.READ_MEDIA_VIDEO"
    tools:node="remove" />

添加 xmlns:tools="http://schemas.android.com/tools" 到标签中。 <manifest> 首先。

使用存储访问框架访问文件夹

某些应用程序需要持续访问文件夹:一个笔记应用程序同步到用户文件夹,一个字段应用程序的导出文件夹。存储访问框架(SAF)允许用户授予一个文件夹 ACTION_OPEN_DOCUMENT_TREE:

const { path } = await CapgoFilePicker.pickDirectory();
// On Android: a content://com.android.externalstorage.documents/tree/... URI

三点需要注意

  1. 结果是一个树型URI,而不是路径 不能将其传递给 Filesystem.writeFile或 DocumentsContract 在本地 DocumentFile in native code.
  2. 除非您的应用程序在URI上调用 添加一个小型本地方法,如果您需要这样做 takePersistableUriPermission 某些文件夹无法授予
@PluginMethod
public void persist(PluginCall call) {
    Uri uri = Uri.parse(call.getString("uri"));
    getContext().getContentResolver().takePersistableUriPermission(
        uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    call.resolve();
}
  1. 在本地__CAPGO_KEEP_0__. On Android 11+, internal storage root folder is refused, and the picker asks users to create or pick a subfolder. Download folder Android/data and Android/obb.

Ask users to create or pick a subfolder. getPersistedUriPermissions() 启动时询问,询问时再次启动。

on start and ask again when it is gone.

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

All files access: MANAGE_EXTERNAL_STORAGE

如果您需要在非Play商店(企业MDM,侧载式kiosk设备)中分发应用,则这是一个选项。否则,请围绕MediaStore和安全文件访问框架(SAF)进行设计。

If you distribute outside Play (enterprise MDM, sideloaded kiosk devices), it is an option. Otherwise, design around MediaStore and SAF.

Plugin Plugin (Plugin) is not translated as it is a protected token. Scoped storage impact
@capacitor/filesystem 与应用目录无关。 Documents 并且 ExternalStorage 在 Android 11+ 上是遗留的。 requestPermissions() 仅对旧 Android 上的这些目录有效。
@capacitor/camera 使用系统相机和相册选择器,除了存储权限外, saveToGallery 在 Android 9 和更早版本上
@capacitor/share 通过一个 FileProvider。仅列出的路径可以共享 res/xml/file_paths.xml 使用自己的
@capgo/capacitor-file-sharer 和 MediaStore FileProvider ,无需配置 save()__CAPGO_KEEP_0__
@capgo/capacitor-downloader 将下载到应用程序的外部存储,然后使用 save() 导出
@capgo/capacitor-file-picker 基于选择器,Android 13+ 不需要权限
@capgo/capacitor-photo-library 其清单添加 READ_MEDIA_* 以便全库读取,遵循上述 Google Play 政策。 pickMedia() 使用系统选择器且不需要权限,因此如果您只使用此功能,请移除 READ_MEDIA_* 使用 tools:node="remove"

选择使用哪个目录

  • 应用程序数据、设置、数据库: Directory.Data
  • 临时文件和共享导出: Directory.Cache
  • 可重下载的大型媒体: Directory.External 在 Android 上, Directory.LibraryNoCloud On iOS
  • A file the user wants in Downloads or Pictures:MediaStore via FileSharer.save()
  • A file the user brings into the app:picker,then copy into Directory.Data
  • A folder the user wants the app to keep writing to:SAF tree URI with a persisted grant

For the full read and write API,see the Capacitor 文件处理指南.

迁移现有应用

  1. 在您的code中搜索 Directory.Documents, Directory.ExternalStorage, /storage/emulated, and requestLegacyExternalStorage.
  2. 将应用所有权文件移动到 Directory.Data 或 Directory.External在新版本首次启动时,复制您仍然可以读取的任何旧文件,然后删除它们
  3. 替换“保存到设备”功能 FileSharer.save().
  4. 替换路径输入为选择器
  5. Add android:maxSdkVersion 替换路径输入为选择器 READ_MEDIA_* 到遗留存储权限并移除
  6. 除非您需要它

preserveLegacyExternalStorage="true" 在应用中 <application> 在 requestLegacyExternalStorage标签保留了对从使用

Troubleshooting

EACCES (Permission denied) 故障排除 /storage/emulated/0/...写入:您正在直接写入共享存储。使用MediaStore或应用目录。

使用 Directory.Documents 重新安装后: 文件丢失了所有权。 使用 MediaStore 处理用户文件。

FileNotFoundException 读取 content:// URI 后: 临时授权过期。 复制文件时选择它,或者持久化授权以处理多个 URI。

Play Console 警告关于照片和视频权限: 插件添加了 READ_MEDIA_IMAGES 或 READ_MEDIA_VIDEO: HTML 文本片段来自更长的 Capgo UI 字符串(父级键 `consulting_faq_subtitle`)。 页面/区域: 咨询服务页面。 角色: 部分标题或标语。 见于: 页面 consulting.astro。 保留 Capgo 产品/品牌和开发者术语的原始形式。 消息键 `consulting_faq_subtitle`(咨询 FAQ 部分标题)。 | 页面/区域: Appflow 比较/迁移营销文本。 角色: 短 UI 标签或导航项。 见于: 页面 alternatives/ionic-appflow.astro,页面 ionic-enterprise-plugins.astro,页面 solutions/ionic-enterprise-plugins.astro。 消息键 `appflow_plugins_or`(Appflow 插件或)。 tools:node="remove" .

requestPermissions() 移除它们,如果您不需要它们。: 在 Android 11+ 上,legacy 存储权限被授予,但对于共享存储来说是无意义的。

保持原生和 web 部分的同步

存储规则是原生配置,因此对清单或插件的更改需要发布一个存储版本,这 Capgo 构建 在云端可以生成。围绕它们的文件逻辑(路径、文件夹名称、导出流程)是您可以使用 Capgo 实时更新.

实时更新 Capacitor 应用

当 web-layer 的 bug 活跃时,通过 Capgo 将修复推送到用户,而不是等待几天的 app store 审核。用户在后台接收更新,而原生变化仍然在正常的审查路径中。

最新博客文章

Capgo 为您提供了创建真正专业的移动应用所需的最佳见解。