「Android」存储空间:分享与访问
Android存储空间概览
存储位置的类别
- 内部存储空间目录:这些目录既包括用于存储持久性文件的专属位置,也包括用于存储缓存数据的其他位置。系统会阻止其他应用访问这些位置,并且在 Android 10(API 级别 29)及更高版本中,系统会对这些位置进行加密。这些特征使得这些位置非常适合存储只有应用本身才能访问的敏感数据。
外部存储空间目录:这些目录既包括用于存储持久性文件的专属位置,也包括用于存储缓存数据的其他位置。虽然其他应用可以在具有适当权限的情况下访问这些目录,但存储在这些目录中的文件仅供应用自己使用。如果明确打算创建其他应用能够访问的文件,应用应改为将这些文件存储在外部存储空间的共享存储空间部分。
- 应用专属存储空间:存储应用私有数据,外部存储应用私有目录对应Android/data/packagename,内部存储应用私有目录对应data/data/packagename。应用私有目录访问通过File path
- 共享存储:存储其他应用可访问文件, 包含媒体文件、文档文件以及其他文件,对应设备 DCIM、Pictures、Alarms、Music、Notifications、Podcasts、Ringtones、Movies、Download等目录。共享目录访问通过 MediaStore API 或者 Storage Access Framework
对外部存储空间的访问和所需权限
Android 为对外部存储空间的读写访问定义了以下权限:READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE。
在较低版本的 Android 系统中,应用需要声明这些权限才能访问位于外部存储空间中应用专属目录之外的任何文件。Android 系统的版本越新,就越依赖于文件的用途而不是位置来确定应用对文件的访问能力。这种基于用途的存储模型可增强用户隐私保护,因为应用只能访问其在设备文件系统中实际使用的区域。
分区存储
为了让用户更好地管理自己的文件并减少混乱,以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被赋予了对外部存储空间的分区访问权限(即分区存储)。此类应用只能访问外部存储空间上的应用专属目录,以及本应用所创建的特定类型的媒体文件。
除非应用需要访问存储在应用专属目录和 MediaStore API 可以访问的目录之外的文件,否则请使用分区存储。如果您将应用专属文件存储在外部存储空间中,则可以将这些文件存放在外部存储空间中的应用专属目录内,以便更加轻松地采用分区存储。这样,在启用分区存储后,您的应用将可以继续访问这些文件。
常用的 MIME 类型
text/plain
、text/rtf
、text/html
、text/json
,接收方应注册text/*
image/jpg
、image/png
、image/gif
,接收方应注册image/*
video/mp4
、video/3gp
,接收方应注册video/*
application/pdf
,接收方应注册支持的文件扩展名
总结
如何分享文件?
应用通常需要将自己的一个或多个文件提供给其他应用。例如,图库可能需要向图片编辑器提供文件,或者文件管理应用可能需要允许用户在外部存储区域之间复制和粘贴文件。发送方应用可以通过响应来自接收方应用的请求分享文件。
在所有情况下,如需将应用中的文件提供给其他应用,唯一安全的做法就是向接收方应用发送文件的内容 URI,并授予对该 URI 的临时访问权限。具有临时 URI 访问权限的内容 URI 之所以安全,是因为它们仅供接收该 URI 的应用使用,并且会自动过期。Android FileProvider 组件提供了 getUriForFile() 方法,用于生成文件的内容 URI。
若要安全地将应用中的文件提供给其他应用,需要配置应用,以内容 URI 的形式提供文件的安全句柄。Android FileProvider 组件会根据 XML 中指定的内容生成文件的内容 URI。
若要为应用定义 FileProvider,需要在应用清单中添加一个条目。此条目指定生成内容 URI 时使用的授权以及 XML 文件的名称,该 XML 文件指定应用可共享的目录。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<application
...>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.myapp.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
...
</application>
</manifest>
android:authorities 属性指定希望用于由 FileProvider 生成的内容 URI 的 URI 授权。示例中的授权为 com.example.myapp.fileprovider。请指定由应用的 android:package 值加上字符串“fileprovider”构成的授权值。
<provider> 的 <meta-data> 子元素指向一个 XML 文件,该文件指定要共享的目录。android:resource 属性是该文件的路径和名称,不包含 .xml 扩展名。
将 FileProvider 添加到应用清单后,需要指定包含要共享的文件的目录。若要指定目录,首先需要在项目的 res/xml/ 子目录中创建 filepaths.xml 文件。在此文件中,通过为每个目录添加 XML 元素以指定目录。以下代码段展示了 res/xml/filepaths.xml 的内容示例。该代码段还展示了如何共享内部存储区域中的 files/ 目录的子目录:
<paths>
<files-path path="images/" name="myimages" />
</paths>
<files-path> 标记共享了应用内部存储空间的 files/ 目录中的目录。path 属性共享了 files/ 的 images/ 子目录。name 属性指示 FileProvider 将路径段 myimages 添加到 files/images/ 子目录中文件的内容 URI 中。
<paths> 元素可以有多个子元素,每个子元素指定一个不同的共享目录。除了 <files-path> 元素之外,还可以使用 <external-path> 元素共享外部存储空间中的目录,使用 <cache-path> 元素共享内部缓存目录中的目录。
已经完整地指定了 FileProvider,该提供器可用于为应用内部存储空间中的 files/ 目录中的文件或 files/ 的子目录中的文件生成内容 URI。当应用为文件生成内容 URI 时,会包含 <provider> 元素中指定的授权 (com.example.myapp.fileprovider)、路径 myimages/ 以及文件的名称。
如果根据本问中的代码段定义 FileProvider,并请求文件 default_image.jpg 的内容 URI,FileProvider 将返回以下 URI:
content://com.example.myapp.fileprovider/myimages/default_image.jpg
如何访问文件?
当使用直接文件路径访问文件时:
Android 9及以下
Old Way。
Android 10
需要停用分区存储:
<manifest ... >
<!-- This attribute is "false" by default on apps targeting
Android 10 or higher. -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
其他则与Android 9及以下保持一致即可。
Android 11
- 申请 READ_EXTERNAL_STORAGE 权限
- 为了从媒体库中访问文件,请使用
ContentResolver
对象:
String[] projection = new String[] {
media-database-columns-to-retrieve
};
String selection = sql-where-clause-with-placeholder-variables;
String[] selectionArgs = new String[] {
values-of-placeholder-variables
};
String sortOrder = sql-order-by-clause;
Cursor cursor = getApplicationContext().getContentResolver().query(
MediaStore.media-type.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
);
while (cursor.moveToNext()) {
// Use an ID column from the projection to get
// a URI representing the media item itself.
}
系统会自动扫描外部存储卷,并将媒体文件添加到以下明确定义的集合中:
- 图片(包括照片和屏幕截图),存储在 DCIM/ 和 Pictures/ 目录中。系统将这些文件添加到 MediaStore.Images 表格中。
- 视频,存储在 DCIM/、Movies/ 和 Pictures/ 目录中。系统将这些文件添加到 MediaStore.Video 表格中。
- 音频文件,存储在 Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/ 和 Ringtones/ 目录中,以及位于 Music/ 或 Movies/ 目录中的音频播放列表中。系统将这些文件添加到 MediaStore.Audio 表格中。
- 下载的文件,存储在 Download/ 目录中。在搭载 Android 10(API 级别 29)及更高版本的设备上,这些文件存储在 MediaStore.Downloads 表格中。此表格在 Android 9(API 级别 28)及更低版本中不可用。
媒体库还包含一个名为 MediaStore.Files 的集合。其内容取决于您的应用是否使用分区存储(适用于以 Android 10 或更高版本为目标平台的应用):
- 如果启用了分区存储,集合只会显示您的应用创建的照片、视频和音频文件。
- 如果分区存储不可用或未使用,集合将显示所有类型的媒体文件。
例子
获取最新的图片文件:
public static String getLatestImgPath(Context context) {
String imgPath = "";
// 查询路径和修改时间
String[] projection = {MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DATE_MODIFIED};
// 查询并排序
Cursor cursor = context.getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
MediaStore.Files.FileColumns.DATE_MODIFIED + " DESC");
// 添加进List
if (cursor.moveToNext()) {
long time = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED));
imgPath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
if (!cursor.isClosed()) {
cursor.close();
}
return imgPath;
}
如需查找满足一组特定条件(例如时长为 5 分钟或更长时间)的媒体:
// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.
// Container for information about each video.
class Video {
private final Uri uri;
private final String name;
private final int duration;
private final int size;
public Video(Uri uri, String name, int duration, int size) {
this.uri = uri;
this.name = name;
this.duration = duration;
this.size = size;
}
}
List<Video> videoList = new ArrayList<Video>();
String[] projection = new String[] {
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.SIZE
};
String selection = MediaStore.Video.Media.DURATION +
" >= ?";
String[] selectionArgs = new String[] {
String.valueOf(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES));
};
String sortOrder = MediaStore.Video.Media.DISPLAY_NAME + " ASC";
try (Cursor cursor = getApplicationContext().getContentResolver().query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)) {
// Cache column indices.
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
int nameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME);
int durationColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
int sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE);
while (cursor.moveToNext()) {
// Get values of columns for a given video.
long id = cursor.getLong(idColumn);
String name = cursor.getString(nameColumn);
int duration = cursor.getInt(durationColumn);
int size = cursor.getInt(sizeColumn);
Uri contentUri = ContentUris.withAppendedId(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
// Stores column values and the contentUri in a local object
// that represents the media file.
videoList.add(new Video(contentUri, name, duration, size));
}
}
参考
https://blog.csdn.net/houdada...
https://blog.csdn.net/jingzz1...
https://www.jianshu.com/p/d0c...
https://www.jianshu.com/p/d55...
https://www.hurryyu.com/2020/...
https://developer.android.com...
https://developer.android.com...
https://developer.android.goo...
https://developer.android.goo...
https://developer.android.goo...
https://blog.csdn.net/guolin_...
https://github.com/guolindev/...
https://guolin.blog.csdn.net/...
https://blog.csdn.net/da_caoy...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。