Android 分区存储适配

image

在发布 Android 10 时官方明确表态:

2020年,主要平台版本将要求所有应用都使用分区存储,无论应用的目标 SDK 级别是多少。因此,您应该提前确保您的应用能够使用分区存储。为此,请确保针对搭载 Android 10(API 级别 29)及更高版本的设备启用了该行为。翻译成通俗语言,不管是使用requestLegacyExternalStorage=true的方式以兼容模式运行还是降低targetSDK都无法在接下来2020年的Android(API 29)10更新中被豁免。
所以为了应用的稳定性,应该尽快进行适配。

分区存储权限介绍

Android 中存储可以分为两大类:私有存储和共享存储

私有存储 (Private Storage) : 每个应用在都拥有自己的私有目录,其它应用看不到,彼此也无法访问到该目录:
内部存储私有目录 (/data/data/packageName) ;
外部存储私有目录 (/sdcard/Android/data/packageName),

共享存储 (Shared Storage) : 存储其他应用可访问文件, 包含媒体文件、文档文件以及其他文件,对应设备DCIM、Pictures、Alarms、Music、Notifications、Podcasts、Ringtones、Movies、Download等目录。

Android 10 中主要对共享目录的权限进行了详细划分,不再能通过绝对路径访问。

访问不同分区的方式

  • 私有目录:和以前的版本一致,可通过 File() API 访问,无需申请权限。
  • 共享目录:需要通过MediaStore和Storage Access Framework API 访问,视具体情况申请权限,下面详细介绍。

其中,对共享目录的权限进行了细分:

  • 无需申请权限的操作:

    通过 MediaStore API对媒体集、文件集进行媒体/文件的添加、对 自身APP 创建的 媒体/文件 进行查询、修改、删除的操作。

  • 需要申请READ_EXTERNAL_STORAGE 权限:

    通过 MediaStore API对所有的媒体集进行查询、修改、删除的操作。

  • 调用 Storage Access Framework API :

    会启动系统的文件选择器向用户申请操作指定的文件

在 targetSDK = 29 APP 中,在 AndroidManifes 设置 requestLegacyExternalStorage="true" 启用兼容模式,以传统分区模式运行。


    
        ...
    
 

注意:如果某个应用在安装时启用了传统外部存储,则该应用会保持此模式,直到卸载为止。无论设备后续是否升级为搭载 Android 10 或更高版本,或者应用后续是否更新为以 Android 10 或更高版本为目标平台,此兼容性行为均适用。

分区存储运行情况

  • targetSdkVersion = 28,运行后正常读写。
  • targetSdkVersion = 29,不删除应用,targetSdkVersion 由 28 修改到 29,覆盖安装,运行后正常读写。
  • targetSdkVersion = 29,删除应用,重新运行,读写报错,程序崩溃 (open failed: EACCES (Permission denied))
  • targetSdkVersion = 29,添加android:requestLegacyExternalStorage="true"(不启用分区存储),读写正常不报错
  • targetSdkVersion = 30,不删除应用,targetSdkVersion 由 29 修改到 30,读写报错,程序崩溃 (open failed: EACCES (Permission denied))
  • targetSdkVersion = 30,不删除应用,targetSdkVersion 由 29 修改到 30,增加 android:preserveLegacyExternalStorage="true",读写正常不报错
  • targetSdkVersion = 30,删除应用,重新运行,读写报错,程序崩溃 (open failed: EACCES (Permission denied))

以下两种情况,不会受到分区存储的影响

  • 系统通过OTA升级到Android 10/11
  • 应用通过更新升级到targetSdkVersion >= 29

专有存储目录

应用读取或写入专有目录中的文件时,不需要获取存储权限。获取当前应用专有存储目录路径有以下方法:

  • Context.getFilesDir()
  • Context.getCacheDir()
  • Context.getExternalFilesDir(String type)
  • Context.getExternalCacheDir()

共享媒体集合存储

在共享媒体集合存储中保存媒体文件时,需要根据文件的类型选择MediaStore。把相关数据放入到ContentValues中,最后把ContentValues插入到ContentResolver中,并获得返回的Uri,通过Uri获得OutputStream,进行文件的存储。

文件存储

val values = ContentValues()
values.put(MediaStore.Images.Media.DISPLAY_NAME, "${System.currentTimeMillis()}.png")
values.put(MediaStore.Images.Media.DESCRIPTION, "media description")
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png")
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
uri?.let { _uri ->
    val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.bg_dog)
    val stream = contentResolver.openOutputStream(_uri)
    stream?.let { _stream ->
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, _stream)
        _stream.close()
        Toast.makeText(this, "文件创建成功 $_uri", Toast.LENGTH_SHORT).show()
    }
}

文件查询

val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, "${MediaStore.MediaColumns.DATE_ADDED} desc")
if (cursor != null) {
    val list: MutableList = ArrayList()
    while (cursor.moveToNext()) {
        val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
        val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
        list.add(uri)
    }
    Toast.makeText(this, "图片数量 ${cursor.count}", Toast.LENGTH_SHORT).show()
    cursor.close()
}

Android 11 新增权限

android.permission.MANAGE_EXTERNAL_STORAGE

类似以前的 READ_EXTERNAL_STORAGE + WRITE_EXTERNAL_STORAGE ,除了应用专有目录都可以访问

可通过执行以下操作向用户请求名为所有文件访问权限的特殊应用访问权限:

  • 在清单中声明 MANAGE_EXTERNAL_STORAGE 权限。
  • 使用 ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION intent 操作将用户引导至一个系统设置页面,在该页面上,用户可以为您的应用启用以下选项:授予所有文件的管理权限。

在 Google Play 上架的话,需要提交使用此权限的说明,只有指定的几种类型的 APP 才能使用。

你可能感兴趣的:(Android 分区存储适配)