区块链技术博客
www.b2bchain.cn

在 Android 中使用存储功能 (2)求职学习资料

本文介绍了在 Android 中使用存储功能 (2)求职学习资料,有助于帮助完成毕业设计以及求职,是一篇很好的资料。

对技术面试,学习经验等有一些体会,在此分享。

前言

我们在 在 Android 中使用存储功能 (1) 阐述了应该将私有文件存放到专属目录中,应该适配分区存储,不要申请不必要的权限。那么如果你的应用,会产生一些需要共享的图片、音频、视频等媒体文件,即产生那些可以给外部应用访问到的资源,比如显示在图库应用中,应该怎么做呢?

或者你要产生一些共享的文本文件,文档文件,也需要其他应用访问,又该怎么做呢?类似这些需求的文件肯定是不能放置在专属目录中的。

Android 推荐使用 MediaStore APISAF 存储访问框架。下面我们来看看如何使用。

存储共享的媒体文件

媒体文件,主要包括图片、视频、音频,如果需要共享,在 Android 上,应该使用 MediaStore API 去存储。

以前多数人都是使用 File API 去存储,在分区存储上,即使你拥有读写权限,也是不可以通过 File API 读写的。

权限的问题,以 Android 10 为分水岭。

  • 10 以下的,即没有分区存储的版本。只读的话必须有 READ_EXTERNAL_STORAGE 权限,要写入的话必须有 WRITE_EXTERNAL_STORAGE
  • 10 及以上的版本,开启了分区存储,那么不需要任何权限,就可以读写并管理自己应用所创建的相关文件。
    • 如果需要读取其他应用创建的文件,那么就需要 READ_EXTERNAL_STORAGE 权限。
    • WRITE_EXTERNAL_STORAGE 权限呢?不需要了,也就是说,不允许你随便读写你应用之外的外部存储了。要读写,必须经过用户的同意才可以。

因此要记住:开启了分区存储,如果你没有权限,那么只能读写自己创建的文件。

总的来说,不管在任何版本上,期望开发者不要去申请 WRITE_EXTERNAL_STORAGE,只在自己能读写的一亩三分地耕耘,如果有需要其他应用的数据,只申请 READ_EXTERNAL_STORAGE,并且通过 MediaStore API 去访问。如果真的要更改其他应用的数据,那么得向用户提出申请。

外部专属目录那里我们提到了,应用产生的媒体文件,如果需要共享,可以放到 ExternalMediaDirs 中,但是从 Android 10 开始,官方建议还是统一使用 MediaStore API。如果你的媒体文件不需要共享,那么还是存放到专属的 cachefiles

MediaStore API 其实就是通过 ContentProvider 去操作数据库。MediaStore 定义了许多内部类,划分了各种资源的类型,每种类型都是一个数据库表,内部定义了表的各种列属性,如果对数据库不熟的自行了解吧,这里不讨。MediaStore 内部类如下:

1. 图片

MediaStore.Images。对应目录如下:

  • DCIM
  • Pictures
    • Screenshots

2. 视频

MediaStore.Video。对应目录如下:

  • DCIM
  • Pictures
  • Movies

3. 音频

MediaStore.Audio。对应目录如下:

  • Alarms
  • Audiobooks
  • Music
  • Notifications
  • Podcasts
  • Ringtones
  • 以及位于 Music/ 或 Movies/ 目录中的 Playlists。

4. 下载

MediaStore.Downloads。对应目录

  • Downloads

仅在 Android 10 及以上版本使用

5. 文件集合

MediaStore.Files

内容取决于应用是否使用分区存储

  • 如果启用了分区存储,那么只会显示你的应用创建的照片、视频和音频文件。
  • 如果没有分区存储,那么将显示所有类型的媒体文件。

既然我们要适配分区存储,那么就不管版本新旧,我们都按照分区存储的规则来,就不要区分版本了,虽然 Android 10 以下的版本,只要有权限就可以任意读写外部存储,但我们还是不能作孽,而且统一操作对我们开发也有好处。

1. 情况 1:应用只共享文件,不需要访问其他文件

如果你的应用只是需要共享自己的媒体文件出去,而不需要访问其他应用的媒体文件,那么权限声明只到 Android 9,即 API 28 即可,因为有了分区存储,你使用 MediaStore API 操作自己的媒体文件并不需要任何的权限。如下:

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

当然,申请权限时也得判断下当前系统版本,只有 10 以下的才需要申请。

2. 情况 2:应用不仅共享文件,还需要读取其他文件

如果是这种情况,那么你的 READ_EXTERNAL_STORAGE 的 maxSdkVersion 属性就得去掉了。并且在 10 及以上版本也得申请读取权限。如下:

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

3. 情况 3:应用不仅共享文件,还需要读取和修改其他文件

这种情况比较特殊了,通常来说,我们并不需要去修改其他应用产生的媒体文件,但是也是可能的,比如你要编辑覆盖对方的文件。那么权限申请 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 就都要了。但是分区存储的话依然不需要 WRITE_EXTERNAL_STORAGE 权限,而是变成了需要系统弹出框来向用户申请,下面会说到。所以权限声明和情况 2 一致。


综上来说,有了分区存储,就不要 WRITE_EXTERNAL_STORAGE,即从 10 及以上版本开始就不要了。在 Manifest 中声明后,AS 也会提示你:

在 Android 中使用存储功能 (2)

意思是从 Android 10 开始,不再提供这个写入权限了。所有 AS 的黄色提示都应该注意看,这是官方给你的建议,通常都是最佳的。

如何使用 MediaStore API。

为了简单,我们先以上述 “情况 1” 作为示例。只操作自己的媒体文件。

先来段简单的示例,查询所有的视频:

/**  * 实体类  */ public static class MyVideo {     public final Uri uri;     public final String name;     public final int duration;     public final int size;      public MyVideo(Uri uri, String name, int duration, int size) {         this.uri = uri;         this.name = name;         this.duration = duration;         this.size = size;     }      @NonNull     @Override     public String toString() {         return "MyVideo{" +                 "uri=" + uri +                 ", name='" + name + ''' +                 ", duration=" + duration +                 ", size=" + size +                 '}';     } }  @NonNull public static List<MyVideo> getVideos() {     List<MyVideo> videoList = new ArrayList<>();      // 指定要读取的数据表的列     String[] projection = new String[]{             MediaStore.Video.Media._ID,             MediaStore.Video.Media.DISPLAY_NAME,             MediaStore.Video.Media.DURATION,             MediaStore.Video.Media.SIZE     };     String selection = null;     String[] selectionArgs = null;     String sortOrder = null;      try (             Cursor cursor = MyApp.getApp().getContentResolver().query(                     MediaStore.Video.Media.EXTERNAL_CONTENT_URI, // 指定要读取的表                     projection,                     selection,                     selectionArgs,                     sortOrder             )) {         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()) {             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);              videoList.add(new MyVideo(contentUri, name, duration, size));         }     } catch (Throwable throwable) {         throwable.printStackTrace();     }     return videoList; }

API 很简单,只需要通过 Context 获取 ContentResolver,然后调用它的 insert、delete、update、query 方法即可。这也是标准的 ContentProvider 使用方法。

方法的参数解释如下:

1. projection

需要查询的数据表列属性,上述代码是查询 4 个列。

2. selection

对数据库表进行筛选的条件

3. selectionArgs

条件参数,和 selection 一一对应

4. sortOrder

排序规则,是升序 ASC 还是降序 DESC。


举个例子:

// 筛选视频长度大于等于指定值的 String selection = MediaStore.Video.Media.DURATION + " >= ?"; // 指定视频长度大于 5 分钟 String[] selectionArgs = new String[]{     String.valueOf(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES)) }; // 指定按照名称升序排序 String sortOrder = MediaStore.Video.Media.DISPLAY_NAME + " ASC";

是不是很像我们使用 Room 时的 SQL 语句?其实就是内部执行的就是 SQL。

开启分区存储,即使你手机上已经有很多视频了,执行后会发现没有一条记录,那是因为没有权限,你只能访问自己创建的媒体文件,而目前我们还没创建文件,所以没有记录。

MediaStore 的增删改查示例

既然本质上是通过 ContentProvider 去操作数据库,那么就一定有增删改查。上面我们已经举了查询的例子,下面我们接着看其他操作的例子:

1. 增加记录

下面的示例演示了如何增加一个图片到 MediaStore。

@Nullable public static Uri insertImage(File file) {     ContentResolver resolver = MyApp.getApp().getContentResolver();      Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;      BitmapFactory.Options options = new BitmapFactory.Options();     options.inJustDecodeBounds = true;     BitmapFactory.decodeFile(file.getAbsolutePath(), options);     MediaColumnsBuilder mediaColumnsBuilder = new MediaColumnsBuilder()             .setData(file.getAbsolutePath())             .setDateAdded((int) System.currentTimeMillis())             .setDateModified((int) file.lastModified())             .setDisplayName(file.getName())             .setHeight(options.outHeight)             .setMimeType(options.outMimeType)             .setSize((int) file.length())             .setTitle(file.getName())             .setWidth(options.outWidth);      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {         // mediaColumnsBuilder.getMediaColumnsBuilderFor10()         //         .setBucketId(1);     }      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {         // mediaColumnsBuilder.getMediaColumnsBuilderFor11()     }      ContentValues contentValues = mediaColumnsBuilder.build();      Uri uri = resolver.insert(contentUri, contentValues);     if (uri != null) {         if (Utils.copy(MyApp.getApp(), uri, file)) {             return uri;         }     }     return null; }  public static boolean copy(Context context, Uri uri, File file) {     try (OutputStream outputStream = context.getContentResolver().openOutputStream(uri);          FileInputStream inputStream = new FileInputStream(file)) {         byte[] bs = new byte[1024];         int read;         while ((read = inputStream.read(bs)) != -1) {             outputStream.write(bs, 0, read);         }         return true;     } catch (IOException e) {         e.printStackTrace();     }     return false; }

通常是将文件准备好了,再执行 insert,成功后得到其 uri,再将文件写入到该 uri 中即可。

其中 MediaColumnsBuilder 是我封装的一个类,主要是将各系统版本支持的列属性分开了,以便适配,本质上还是对 ContentValues 进行 put 操作。

新增的记录,对应的文件默认将在 Pictures 目录下。

2. 删除记录

上面 insert 新增了一个记录后,得到 uri 可以用来删除该记录。如下:

@Nullable public static Integer deleteImage(Uri uri) {     String selection = null;     String[] selectionArgs = null;      ContentResolver resolver = MyApp.getApp().getContentResolver();      int numImagesRemoved = resolver.delete(             uri,             selection,             selectionArgs);     return numImagesRemoved >= 0 ? numImagesRemoved : null; }

调用 delete 方法后,会返回该记录在数据库表中的行号。除了通过 uri 直接删除记录外,当然还可以指定 selection 去删除了。

如果没有开启分区存储,属于其他应用的媒体文件也可以删除的。

3. 更新记录

管理记录时,对于不满意的数据还可以进行更改,如下修改名称示例:

@Nullable public static Integer updateImage(Uri uri) {     ContentResolver resolver = MyApp.getApp().getContentResolver();      ContentValues values = new ContentValues();     values.put(MediaStore.Audio.Media.DISPLAY_NAME,             "update display name.jpg");      int numUpdated = resolver.update(             uri,             values,             null,             null);     return numUpdated >= 0 ? numUpdated : null; }  @Nullable public static Integer updateImage(long id) {     ContentResolver resolver = MyApp.getApp().getContentResolver();      String selection = MediaStore.Audio.Media._ID + " = ?";      String[] selectionArgs = new String[]{String.valueOf(id)};      ContentValues values = new ContentValues();     values.put(MediaStore.Audio.Media.DISPLAY_NAME,             "update display name.jpg");      Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;      int numUpdated = resolver.update(             contentUri,             values,             selection,             selectionArgs);     return numUpdated >= 0 ? numUpdated : null; }

更新和新增时类似,也是通过 ContentValues 去指定要修改的列和对应的内容。

如果没有开启分区存储,属于其他应用的媒体文件也可以更新的。

4. 更新其他应用的媒体文件

开启了分区存储,那么通常来说,只能读写和管理自己所创建的文件,但是也有应用需要去修改其他应用的媒体文件。因此就需要申请用户的同意了。如下:

“`java
Uri uri = …; // 其他应用的媒体 uri

// 可写模式打开文件,看是否有权限
try (ParcelFileDescriptor descriptor = getContext().getContentResolver().openFileDescriptor(uri, “w”);){
Toast.makeText(getContext(), “有权限,可以修改文件”, Toast.LENGTH_SHORT).show();
// 有权限,则修改
MediaStoreUtils.updateImage(uri);
} catch (SecurityException | IOException e) {
e.printStackTrace();

// Android 10 及以上,修改其他应用的媒体文件,需要经过用户的同意 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {     RecoverableSecurityException recoverableSecurityException;     if (e instanceof RecoverableSecurityException) {         recoverableSecurityException = (RecoverableSecurityException) e;     } else {         throw new RuntimeException(e.getMessage(), e);     }

前言

我们在 在 Android 中使用存储功能 (1) 阐述了应该将私有文件存放到专属目录中,应该适配分区存储,不要申请不必要的权限。那么如果你的应用,会产生一些需要共享的图片、音频、视频等媒体文件,即产生那些可以给外部应用访问到的资源,比如显示在图库应用中,应该怎么做呢?

或者你要产生一些共享的文本文件,文档文件,也需要其他应用访问,又该怎么做呢?类似这些需求的文件肯定是不能放置在专属目录中的。

Android 推荐使用 MediaStore APISAF 存储访问框架。下面我们来看看如何使用。

存储共享的媒体文件

媒体文件,主要包括图片、视频、音频,如果需要共享,在 Android 上,应该使用 MediaStore API 去存储。

以前多数人都是使用 File API 去存储,在分区存储上,即使你拥有读写权限,也是不可以通过 File API 读写的。

权限的问题,以 Android 10 为分水岭。

  • 10 以下的,即没有分区存储的版本。只读的话必须有 READ_EXTERNAL_STORAGE 权限,要写入的话必须有 WRITE_EXTERNAL_STORAGE
  • 10 及以上的版本,开启了分区存储,那么不需要任何权限,就可以读写并管理自己应用所创建的相关文件。
    • 如果需要读取其他应用创建的文件,那么就需要 READ_EXTERNAL_STORAGE 权限。
    • WRITE_EXTERNAL_STORAGE 权限呢?不需要了,也就是说,不允许你随便读写你应用之外的外部存储了。要读写,必须经过用户的同意才可以。

因此要记住:开启了分区存储,如果你没有权限,那么只能读写自己创建的文件。

总的来说,不管在任何版本上,期望开发者不要去申请 WRITE_EXTERNAL_STORAGE,只在自己能读写的一亩三分地耕耘,如果有需要其他应用的数据,只申请 READ_EXTERNAL_STORAGE,并且通过 MediaStore API 去访问。如果真的要更改其他应用的数据,那么得向用户提出申请。

外部专属目录那里我们提到了,应用产生的媒体文件,如果需要共享,可以放到 ExternalMediaDirs 中,但是从 Android 10 开始,官方建议还是统一使用 MediaStore API。如果你的媒体文件不需要共享,那么还是存放到专属的 cachefiles

MediaStore API 其实就是通过 ContentProvider 去操作数据库。MediaStore 定义了许多内部类,划分了各种资源的类型,每种类型都是一个数据库表,内部定义了表的各种列属性,如果对数据库不熟的自行了解吧,这里不讨。MediaStore 内部类如下:

1. 图片

MediaStore.Images。对应目录如下:

  • DCIM
  • Pictures
    • Screenshots

2. 视频

MediaStore.Video。对应目录如下:

  • DCIM
  • Pictures
  • Movies

3. 音频

MediaStore.Audio。对应目录如下:

  • Alarms
  • Audiobooks
  • Music
  • Notifications
  • Podcasts
  • Ringtones
  • 以及位于 Music/ 或 Movies/ 目录中的 Playlists。

4. 下载

MediaStore.Downloads。对应目录

  • Downloads

仅在 Android 10 及以上版本使用

5. 文件集合

MediaStore.Files

内容取决于应用是否使用分区存储

  • 如果启用了分区存储,那么只会显示你的应用创建的照片、视频和音频文件。
  • 如果没有分区存储,那么将显示所有类型的媒体文件。

既然我们要适配分区存储,那么就不管版本新旧,我们都按照分区存储的规则来,就不要区分版本了,虽然 Android 10 以下的版本,只要有权限就可以任意读写外部存储,但我们还是不能作孽,而且统一操作对我们开发也有好处。

1. 情况 1:应用只共享文件,不需要访问其他文件

如果你的应用只是需要共享自己的媒体文件出去,而不需要访问其他应用的媒体文件,那么权限声明只到 Android 9,即 API 28 即可,因为有了分区存储,你使用 MediaStore API 操作自己的媒体文件并不需要任何的权限。如下:

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

当然,申请权限时也得判断下当前系统版本,只有 10 以下的才需要申请。

2. 情况 2:应用不仅共享文件,还需要读取其他文件

如果是这种情况,那么你的 READ_EXTERNAL_STORAGE 的 maxSdkVersion 属性就得去掉了。并且在 10 及以上版本也得申请读取权限。如下:

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

3. 情况 3:应用不仅共享文件,还需要读取和修改其他文件

这种情况比较特殊了,通常来说,我们并不需要去修改其他应用产生的媒体文件,但是也是可能的,比如你要编辑覆盖对方的文件。那么权限申请 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 就都要了。但是分区存储的话依然不需要 WRITE_EXTERNAL_STORAGE 权限,而是变成了需要系统弹出框来向用户申请,下面会说到。所以权限声明和情况 2 一致。


综上来说,有了分区存储,就不要 WRITE_EXTERNAL_STORAGE,即从 10 及以上版本开始就不要了。在 Manifest 中声明后,AS 也会提示你:

在 Android 中使用存储功能 (2)

意思是从 Android 10 开始,不再提供这个写入权限了。所有 AS 的黄色提示都应该注意看,这是官方给你的建议,通常都是最佳的。

如何使用 MediaStore API。

为了简单,我们先以上述 “情况 1” 作为示例。只操作自己的媒体文件。

先来段简单的示例,查询所有的视频:

/**  * 实体类  */ public static class MyVideo {     public final Uri uri;     public final String name;     public final int duration;     public final int size;      public MyVideo(Uri uri, String name, int duration, int size) {         this.uri = uri;         this.name = name;         this.duration = duration;         this.size = size;     }      @NonNull     @Override     public String toString() {         return "MyVideo{" +                 "uri=" + uri +                 ", name='" + name + ''' +                 ", duration=" + duration +                 ", size=" + size +                 '}';     } }  @NonNull public static List<MyVideo> getVideos() {     List<MyVideo> videoList = new ArrayList<>();      // 指定要读取的数据表的列     String[] projection = new String[]{             MediaStore.Video.Media._ID,             MediaStore.Video.Media.DISPLAY_NAME,             MediaStore.Video.Media.DURATION,             MediaStore.Video.Media.SIZE     };     String selection = null;     String[] selectionArgs = null;     String sortOrder = null;      try (             Cursor cursor = MyApp.getApp().getContentResolver().query(                     MediaStore.Video.Media.EXTERNAL_CONTENT_URI, // 指定要读取的表                     projection,                     selection,                     selectionArgs,                     sortOrder             )) {         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()) {             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);              videoList.add(new MyVideo(contentUri, name, duration, size));         }     } catch (Throwable throwable) {         throwable.printStackTrace();     }     return videoList; }

API 很简单,只需要通过 Context 获取 ContentResolver,然后调用它的 insert、delete、update、query 方法即可。这也是标准的 ContentProvider 使用方法。

方法的参数解释如下:

1. projection

需要查询的数据表列属性,上述代码是查询 4 个列。

2. selection

对数据库表进行筛选的条件

3. selectionArgs

条件参数,和 selection 一一对应

4. sortOrder

排序规则,是升序 ASC 还是降序 DESC。


举个例子:

// 筛选视频长度大于等于指定值的 String selection = MediaStore.Video.Media.DURATION + " >= ?"; // 指定视频长度大于 5 分钟 String[] selectionArgs = new String[]{     String.valueOf(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES)) }; // 指定按照名称升序排序 String sortOrder = MediaStore.Video.Media.DISPLAY_NAME + " ASC";

是不是很像我们使用 Room 时的 SQL 语句?其实就是内部执行的就是 SQL。

开启分区存储,即使你手机上已经有很多视频了,执行后会发现没有一条记录,那是因为没有权限,你只能访问自己创建的媒体文件,而目前我们还没创建文件,所以没有记录。

MediaStore 的增删改查示例

既然本质上是通过 ContentProvider 去操作数据库,那么就一定有增删改查。上面我们已经举了查询的例子,下面我们接着看其他操作的例子:

1. 增加记录

下面的示例演示了如何增加一个图片到 MediaStore。

@Nullable public static Uri insertImage(File file) {     ContentResolver resolver = MyApp.getApp().getContentResolver();      Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;      BitmapFactory.Options options = new BitmapFactory.Options();     options.inJustDecodeBounds = true;     BitmapFactory.decodeFile(file.getAbsolutePath(), options);     MediaColumnsBuilder mediaColumnsBuilder = new MediaColumnsBuilder()             .setData(file.getAbsolutePath())             .setDateAdded((int) System.currentTimeMillis())             .setDateModified((int) file.lastModified())             .setDisplayName(file.getName())             .setHeight(options.outHeight)             .setMimeType(options.outMimeType)             .setSize((int) file.length())             .setTitle(file.getName())             .setWidth(options.outWidth);      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {         // mediaColumnsBuilder.getMediaColumnsBuilderFor10()         //         .setBucketId(1);     }      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {         // mediaColumnsBuilder.getMediaColumnsBuilderFor11()     }      ContentValues contentValues = mediaColumnsBuilder.build();      Uri uri = resolver.insert(contentUri, contentValues);     if (uri != null) {         if (Utils.copy(MyApp.getApp(), uri, file)) {             return uri;         }     }     return null; }  public static boolean copy(Context context, Uri uri, File file) {     try (OutputStream outputStream = context.getContentResolver().openOutputStream(uri);          FileInputStream inputStream = new FileInputStream(file)) {         byte[] bs = new byte[1024];         int read;         while ((read = inputStream.read(bs)) != -1) {             outputStream.write(bs, 0, read);         }         return true;     } catch (IOException e) {         e.printStackTrace();     }     return false; }

通常是将文件准备好了,再执行 insert,成功后得到其 uri,再将文件写入到该 uri 中即可。

其中 MediaColumnsBuilder 是我封装的一个类,主要是将各系统版本支持的列属性分开了,以便适配,本质上还是对 ContentValues 进行 put 操作。

新增的记录,对应的文件默认将在 Pictures 目录下。

2. 删除记录

上面 insert 新增了一个记录后,得到 uri 可以用来删除该记录。如下:

@Nullable public static Integer deleteImage(Uri uri) {     String selection = null;     String[] selectionArgs = null;      ContentResolver resolver = MyApp.getApp().getContentResolver();      int numImagesRemoved = resolver.delete(             uri,             selection,             selectionArgs);     return numImagesRemoved >= 0 ? numImagesRemoved : null; }

调用 delete 方法后,会返回该记录在数据库表中的行号。除了通过 uri 直接删除记录外,当然还可以指定 selection 去删除了。

如果没有开启分区存储,属于其他应用的媒体文件也可以删除的。

3. 更新记录

管理记录时,对于不满意的数据还可以进行更改,如下修改名称示例:

@Nullable public static Integer updateImage(Uri uri) {     ContentResolver resolver = MyApp.getApp().getContentResolver();      ContentValues values = new ContentValues();     values.put(MediaStore.Audio.Media.DISPLAY_NAME,             "update display name.jpg");      int numUpdated = resolver.update(             uri,             values,             null,             null);     return numUpdated >= 0 ? numUpdated : null; }  @Nullable public static Integer updateImage(long id) {     ContentResolver resolver = MyApp.getApp().getContentResolver();      String selection = MediaStore.Audio.Media._ID + " = ?";      String[] selectionArgs = new String[]{String.valueOf(id)};      ContentValues values = new ContentValues();     values.put(MediaStore.Audio.Media.DISPLAY_NAME,             "update display name.jpg");      Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;      int numUpdated = resolver.update(             contentUri,             values,             selection,             selectionArgs);     return numUpdated >= 0 ? numUpdated : null; }

更新和新增时类似,也是通过 ContentValues 去指定要修改的列和对应的内容。

如果没有开启分区存储,属于其他应用的媒体文件也可以更新的。

4. 更新其他应用的媒体文件

开启了分区存储,那么通常来说,只能读写和管理自己所创建的文件,但是也有应用需要去修改其他应用的媒体文件。因此就需要申请用户的同意了。如下:

“`java
Uri uri = …; // 其他应用的媒体 uri

// 可写模式打开文件,看是否有权限
try (ParcelFileDescriptor descriptor = getContext().getContentResolver().openFileDescriptor(uri, “w”);){
Toast.makeText(getContext(), “有权限,可以修改文件”, Toast.LENGTH_SHORT).show();
// 有权限,则修改
MediaStoreUtils.updateImage(uri);
} catch (SecurityException | IOException e) {
e.printStackTrace();

// Android 10 及以上,修改其他应用的媒体文件,需要经过用户的同意 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {     RecoverableSecurityException recoverableSecurityException;     if (e instanceof RecoverableSecurityException) {         recoverableSecurityException = (RecoverableSecurityException) e;     } else {         throw new RuntimeException(e.getMessage(), e);     }

前言

我们在 在 Android 中使用存储功能 (1) 阐述了应该将私有文件存放到专属目录中,应该适配分区存储,不要申请不必要的权限。那么如果你的应用,会产生一些需要共享的图片、音频、视频等媒体文件,即产生那些可以给外部应用访问到的资源,比如显示在图库应用中,应该怎么做呢?

或者你要产生一些共享的文本文件,文档文件,也需要其他应用访问,又该怎么做呢?类似这些需求的文件肯定是不能放置在专属目录中的。

Android 推荐使用 MediaStore APISAF 存储访问框架。下面我们来看看如何使用。

存储共享的媒体文件

媒体文件,主要包括图片、视频、音频,如果需要共享,在 Android 上,应该使用 MediaStore API 去存储。

以前多数人都是使用 File API 去存储,在分区存储上,即使你拥有读写权限,也是不可以通过 File API 读写的。

权限的问题,以 Android 10 为分水岭。

  • 10 以下的,即没有分区存储的版本。只读的话必须有 READ_EXTERNAL_STORAGE 权限,要写入的话必须有 WRITE_EXTERNAL_STORAGE
  • 10 及以上的版本,开启了分区存储,那么不需要任何权限,就可以读写并管理自己应用所创建的相关文件。
    • 如果需要读取其他应用创建的文件,那么就需要 READ_EXTERNAL_STORAGE 权限。
    • WRITE_EXTERNAL_STORAGE 权限呢?不需要了,也就是说,不允许你随便读写你应用之外的外部存储了。要读写,必须经过用户的同意才可以。

因此要记住:开启了分区存储,如果你没有权限,那么只能读写自己创建的文件。

总的来说,不管在任何版本上,期望开发者不要去申请 WRITE_EXTERNAL_STORAGE,只在自己能读写的一亩三分地耕耘,如果有需要其他应用的数据,只申请 READ_EXTERNAL_STORAGE,并且通过 MediaStore API 去访问。如果真的要更改其他应用的数据,那么得向用户提出申请。

外部专属目录那里我们提到了,应用产生的媒体文件,如果需要共享,可以放到 ExternalMediaDirs 中,但是从 Android 10 开始,官方建议还是统一使用 MediaStore API。如果你的媒体文件不需要共享,那么还是存放到专属的 cachefiles

MediaStore API 其实就是通过 ContentProvider 去操作数据库。MediaStore 定义了许多内部类,划分了各种资源的类型,每种类型都是一个数据库表,内部定义了表的各种列属性,如果对数据库不熟的自行了解吧,这里不讨。MediaStore 内部类如下:

1. 图片

MediaStore.Images。对应目录如下:

  • DCIM
  • Pictures
    • Screenshots

2. 视频

MediaStore.Video。对应目录如下:

  • DCIM
  • Pictures
  • Movies

3. 音频

MediaStore.Audio。对应目录如下:

  • Alarms
  • Audiobooks
  • Music
  • Notifications
  • Podcasts
  • Ringtones
  • 以及位于 Music/ 或 Movies/ 目录中的 Playlists。

4. 下载

MediaStore.Downloads。对应目录

  • Downloads

仅在 Android 10 及以上版本使用

5. 文件集合

MediaStore.Files

内容取决于应用是否使用分区存储

  • 如果启用了分区存储,那么只会显示你的应用创建的照片、视频和音频文件。
  • 如果没有分区存储,那么将显示所有类型的媒体文件。

既然我们要适配分区存储,那么就不管版本新旧,我们都按照分区存储的规则来,就不要区分版本了,虽然 Android 10 以下的版本,只要有权限就可以任意读写外部存储,但我们还是不能作孽,而且统一操作对我们开发也有好处。

1. 情况 1:应用只共享文件,不需要访问其他文件

如果你的应用只是需要共享自己的媒体文件出去,而不需要访问其他应用的媒体文件,那么权限声明只到 Android 9,即 API 28 即可,因为有了分区存储,你使用 MediaStore API 操作自己的媒体文件并不需要任何的权限。如下:

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

当然,申请权限时也得判断下当前系统版本,只有 10 以下的才需要申请。

2. 情况 2:应用不仅共享文件,还需要读取其他文件

如果是这种情况,那么你的 READ_EXTERNAL_STORAGE 的 maxSdkVersion 属性就得去掉了。并且在 10 及以上版本也得申请读取权限。如下:

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

3. 情况 3:应用不仅共享文件,还需要读取和修改其他文件

这种情况比较特殊了,通常来说,我们并不需要去修改其他应用产生的媒体文件,但是也是可能的,比如你要编辑覆盖对方的文件。那么权限申请 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 就都要了。但是分区存储的话依然不需要 WRITE_EXTERNAL_STORAGE 权限,而是变成了需要系统弹出框来向用户申请,下面会说到。所以权限声明和情况 2 一致。


综上来说,有了分区存储,就不要 WRITE_EXTERNAL_STORAGE,即从 10 及以上版本开始就不要了。在 Manifest 中声明后,AS 也会提示你:

在 Android 中使用存储功能 (2)

意思是从 Android 10 开始,不再提供这个写入权限了。所有 AS 的黄色提示都应该注意看,这是官方给你的建议,通常都是最佳的。

如何使用 MediaStore API。

为了简单,我们先以上述 “情况 1” 作为示例。只操作自己的媒体文件。

先来段简单的示例,查询所有的视频:

/**  * 实体类  */ public static class MyVideo {     public final Uri uri;     public final String name;     public final int duration;     public final int size;      public MyVideo(Uri uri, String name, int duration, int size) {         this.uri = uri;         this.name = name;         this.duration = duration;         this.size = size;     }      @NonNull     @Override     public String toString() {         return "MyVideo{" +                 "uri=" + uri +                 ", name='" + name + ''' +                 ", duration=" + duration +                 ", size=" + size +                 '}';     } }  @NonNull public static List<MyVideo> getVideos() {     List<MyVideo> videoList = new ArrayList<>();      // 指定要读取的数据表的列     String[] projection = new String[]{             MediaStore.Video.Media._ID,             MediaStore.Video.Media.DISPLAY_NAME,             MediaStore.Video.Media.DURATION,             MediaStore.Video.Media.SIZE     };     String selection = null;     String[] selectionArgs = null;     String sortOrder = null;      try (             Cursor cursor = MyApp.getApp().getContentResolver().query(                     MediaStore.Video.Media.EXTERNAL_CONTENT_URI, // 指定要读取的表                     projection,                     selection,                     selectionArgs,                     sortOrder             )) {         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()) {             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);              videoList.add(new MyVideo(contentUri, name, duration, size));         }     } catch (Throwable throwable) {         throwable.printStackTrace();     }     return videoList; }

API 很简单,只需要通过 Context 获取 ContentResolver,然后调用它的 insert、delete、update、query 方法即可。这也是标准的 ContentProvider 使用方法。

方法的参数解释如下:

1. projection

需要查询的数据表列属性,上述代码是查询 4 个列。

2. selection

对数据库表进行筛选的条件

3. selectionArgs

条件参数,和 selection 一一对应

4. sortOrder

排序规则,是升序 ASC 还是降序 DESC。


举个例子:

// 筛选视频长度大于等于指定值的 String selection = MediaStore.Video.Media.DURATION + " >= ?"; // 指定视频长度大于 5 分钟 String[] selectionArgs = new String[]{     String.valueOf(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES)) }; // 指定按照名称升序排序 String sortOrder = MediaStore.Video.Media.DISPLAY_NAME + " ASC";

是不是很像我们使用 Room 时的 SQL 语句?其实就是内部执行的就是 SQL。

开启分区存储,即使你手机上已经有很多视频了,执行后会发现没有一条记录,那是因为没有权限,你只能访问自己创建的媒体文件,而目前我们还没创建文件,所以没有记录。

MediaStore 的增删改查示例

既然本质上是通过 ContentProvider 去操作数据库,那么就一定有增删改查。上面我们已经举了查询的例子,下面我们接着看其他操作的例子:

1. 增加记录

下面的示例演示了如何增加一个图片到 MediaStore。

@Nullable public static Uri insertImage(File file) {     ContentResolver resolver = MyApp.getApp().getContentResolver();      Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;      BitmapFactory.Options options = new BitmapFactory.Options();     options.inJustDecodeBounds = true;     BitmapFactory.decodeFile(file.getAbsolutePath(), options);     MediaColumnsBuilder mediaColumnsBuilder = new MediaColumnsBuilder()             .setData(file.getAbsolutePath())             .setDateAdded((int) System.currentTimeMillis())             .setDateModified((int) file.lastModified())             .setDisplayName(file.getName())             .setHeight(options.outHeight)             .setMimeType(options.outMimeType)             .setSize((int) file.length())             .setTitle(file.getName())             .setWidth(options.outWidth);      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {         // mediaColumnsBuilder.getMediaColumnsBuilderFor10()         //         .setBucketId(1);     }      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {         // mediaColumnsBuilder.getMediaColumnsBuilderFor11()     }      ContentValues contentValues = mediaColumnsBuilder.build();      Uri uri = resolver.insert(contentUri, contentValues);     if (uri != null) {         if (Utils.copy(MyApp.getApp(), uri, file)) {             return uri;         }     }     return null; }  public static boolean copy(Context context, Uri uri, File file) {     try (OutputStream outputStream = context.getContentResolver().openOutputStream(uri);          FileInputStream inputStream = new FileInputStream(file)) {         byte[] bs = new byte[1024];         int read;         while ((read = inputStream.read(bs)) != -1) {             outputStream.write(bs, 0, read);         }         return true;     } catch (IOException e) {         e.printStackTrace();     }     return false; }

通常是将文件准备好了,再执行 insert,成功后得到其 uri,再将文件写入到该 uri 中即可。

其中 MediaColumnsBuilder 是我封装的一个类,主要是将各系统版本支持的列属性分开了,以便适配,本质上还是对 ContentValues 进行 put 操作。

新增的记录,对应的文件默认将在 Pictures 目录下。

2. 删除记录

上面 insert 新增了一个记录后,得到 uri 可以用来删除该记录。如下:

@Nullable public static Integer deleteImage(Uri uri) {     String selection = null;     String[] selectionArgs = null;      ContentResolver resolver = MyApp.getApp().getContentResolver();      int numImagesRemoved = resolver.delete(             uri,             selection,             selectionArgs);     return numImagesRemoved >= 0 ? numImagesRemoved : null; }

调用 delete 方法后,会返回该记录在数据库表中的行号。除了通过 uri 直接删除记录外,当然还可以指定 selection 去删除了。

如果没有开启分区存储,属于其他应用的媒体文件也可以删除的。

3. 更新记录

管理记录时,对于不满意的数据还可以进行更改,如下修改名称示例:

@Nullable public static Integer updateImage(Uri uri) {     ContentResolver resolver = MyApp.getApp().getContentResolver();      ContentValues values = new ContentValues();     values.put(MediaStore.Audio.Media.DISPLAY_NAME,             "update display name.jpg");      int numUpdated = resolver.update(             uri,             values,             null,             null);     return numUpdated >= 0 ? numUpdated : null; }  @Nullable public static Integer updateImage(long id) {     ContentResolver resolver = MyApp.getApp().getContentResolver();      String selection = MediaStore.Audio.Media._ID + " = ?";      String[] selectionArgs = new String[]{String.valueOf(id)};      ContentValues values = new ContentValues();     values.put(MediaStore.Audio.Media.DISPLAY_NAME,             "update display name.jpg");      Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;      int numUpdated = resolver.update(             contentUri,             values,             selection,             selectionArgs);     return numUpdated >= 0 ? numUpdated : null; }

更新和新增时类似,也是通过 ContentValues 去指定要修改的列和对应的内容。

如果没有开启分区存储,属于其他应用的媒体文件也可以更新的。

4. 更新其他应用的媒体文件

开启了分区存储,那么通常来说,只能读写和管理自己所创建的文件,但是也有应用需要去修改其他应用的媒体文件。因此就需要申请用户的同意了。如下:

“`java
Uri uri = …; // 其他应用的媒体 uri

// 可写模式打开文件,看是否有权限
try (ParcelFileDescriptor descriptor = getContext().getContentResolver().openFileDescriptor(uri, “w”);){
Toast.makeText(getContext(), “有权限,可以修改文件”, Toast.LENGTH_SHORT).show();
// 有权限,则修改
MediaStoreUtils.updateImage(uri);
} catch (SecurityException | IOException e) {
e.printStackTrace();

// Android 10 及以上,修改其他应用的媒体文件,需要经过用户的同意 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {     RecoverableSecurityException recoverableSecurityException;     if (e instanceof RecoverableSecurityException) {         recoverableSecurityException = (RecoverableSecurityException) e;     } else {         throw new RuntimeException(e.getMessage(), e);     }

部分转自互联网,侵权删除联系

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » 在 Android 中使用存储功能 (2)求职学习资料
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们