【Android笔记】Target改动记录 & 文件说明&权限说明

1,312 阅读9分钟

一、版本变动参考文档

1、OPPO 开放平台-OPPO开发者服务中心

2、行为变更:以 Android 12 为目标平台的应用  |  Android 开发者  |  Android Developers

二、文件基本概念

image.png

1、内存RAM:随机存取存储器(Random Access Memory)

2、内部存储:/data/user/0/

  • 开发更熟悉的/data/data/<package>
  • 实际上是/data/user/<current_user_id>/<package>的一个链接,current_user_id默认为0,第一个用户

files目录:/data/data//files【val path = filesDir.absolutePath】
cache目录:/data/data//cache【val path = cacheDir.absolutePath】
shared目录:/data/data//shared_prefs【一般sharePrefs会保存在这里】 databases目录:data/data//databses【一般sqlite会保存在这里】

3、外部存储:

1、 简单介绍

1、支持外置SD卡,所以有内外存概念;

  • 后续安卓内部存储空间通过fuse技术挂载到/storage/emulated/0上,这个挂载点就是外部存储
  • 图片里可以看到SDCard是个软链接,可以不用太在意【安卓古早机子和新机结构都这样】

storage/emulated/0/基本文件夹
sdcard/基本文件夹

2、 外部也分为私有/公有存储目录

  1. 外部私有目录的作用:大文件推荐存外部私有存储

/storage/emulated/0/Android/data/<package>
files目录: /storage/emulated/0/Android/data//files
【val path = getExternalFilesDir("")?.absolutePath ?: ""】
cache目录: /storage/emulated/0/Android/data//cache
【val path = externalCacheDir?.absolutePath ?: ""】

  • getExternalFilesDir()是需要一个字符串参数的,空串就是files;* 传入"test",则获取到:/storage/emulated/0/Android/data/<package>/files/test更推荐用这个,getExternalStoragePublicDirectory在android 10 (api 29)已过时

  • Environment有官方推荐命名,例如:DIRECT_DCIM

3、除了私有目录以外的目录,都是公有目录。程序保存在公有目录中的数据,在应用被删除后,仍然保留

常见的有:Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones等

val ringtoneDir = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "ringtones").apply { mkdirs() }
val ringtoneFile = File(ringtoneDir, "selected_ringtone.mp3")    
//对应
getExternalFilesDir->/storage/emulated/0/Android/data/com.kakusummer.androidutilsturbo/files/Download/ringtones/selected_rin

2、方法介绍

1、Environment.getExternalStoragePublicDirectory,存放外部公有

  • 存在这个位置可以在手机"最近"文件中显示,DIRECTORY_DOCUMENTS目录显示不出来

Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)

  • getExternalFilesDir(String type):这是 Context 类的方法,用来获取应用专属的外部存储目录

2、系统文件夹介绍与多媒体

三、文件版本变迁

一、Android Q(10) - API 29

1、基本知识

  1. 外部私有目录,你可以在不需要特殊权限的情况下进行读写操作

  2. 外部公共目录

  • Android 10 及以上版本:访问外部存储文件需要使用 Scoped Storage,需要特别注意 Android 10 引入的存储权限限制。你可以使用 MediaStore API 来访问公共目录,并且无需 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE 权限,除非你需要访问所有文件。
  • Android 9(API 级别 28)及以下版本:需要声明 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 权限。

1、 提出了分区存储-Scoped Storage模式;

2、 私有目录的读写没有变化,和之前一样,仍可以使用File那一套,且不需任何权限【通过intent获取的content:// 可mediaplayer播放,荣耀等部分机子必须得申请权限才能使用,狗屎】

  • 这个链接在Activity能播放,广播接收器和Service里播不了

3、 对于公有目录必须使用

  • 使用MediaStore提供的API:访问图片、视频、音频这三类(基本思路是通过contentResolver与对应MediaStore标志结合)。
  • 其他使用SAF(存储访问框架)。

4、暴力申请 MANAGE_EXTERNAL_STORAGE权限(API 30):

  • 不用刻意使用SAF,可以使用File那一套;
  • Google play可能会比较严格,国内倒还好。
  • 这种机子就算写了清单文件READ_EXTERNAL_STORAGE或 WRITE_EXTERNAL_STORAGE权限,设置页也读不出来。

5、申请其他三个细分的权限

  1. 清单文件
  • 这俩最高只支持到Target32,项目中如果设置大于32,APP设置页就不会显示这个开关,如果设置小于等于32,即使是高版本的机子也能显示
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />
  • 具体显示的样式看机子,低版本可能只有一个开关,高版本会细化为两个开关

image.png

  1. 大于Targt32的解决方案
  • 申请文件管理

  • 优点:简单粗暴

  • 缺点:部分应用可能上不了架【Google Play推荐文件管理项目使用该权限】

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

if (!Environment.isExternalStorageManager()) {
    //跳转新页面申请权限
    val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
    intent.data = Uri.parse("package:$packageName")
    startActivityForResult(intent, 101)
} 
  1. 声明对应的权限,代码中动态申请对应的
<!--Android 13版本适配,细化存储权限-->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

2、 Scoped Storage模式介绍

1、提供外部存储空间视图模式:Legacy View、Filtered View

  1. Legacy View-兼容模式:兼容以前访问逻辑,申请权限后App可访问外部存储,拥有完整的访问权限
  • Android Q上,TargetSDK大于或等于29的APP默认Filtered View,反之则默认被赋予Legacy View;
  • AndroidManifest.xml中设置新属性requestLegacyExternalStorage来修改外部存储空间视图模式,true为兼容模式
  • 非常重要!!!项目里最好都写起来
android:requestLegacyExternalStorage="true"

<meta-data
    android:name="ScopedStorage"
    android:value="true" />

从MediaStore接口中查询到的DATA字段将在Android Q开始废弃,不应该利用它来访问文件或者判断文件是否存在;

使用MediaStore.MediaColumns.RELATIVE_PATH代替data

3、SAF和MediaStore

  1. Android Q & Android 11存储适配(一) 基础知识点梳理
  2. Android 10(Q)/11(R) 分区存储适配 详解

二、Android 11 - Target 30

Ⅰ - 文件相关

应用targetSdkVersion >= 30,都会强制打开分区存储,requestLegacyExternalStorage将会无效,同时APP将不能直接访问外部存储。

1、暴力解决方案 声明管理所有文件:MANAGE_EXTERNAL_STORAGE;

三、Android 12 - Target 31

1、清单文件

1、android:exported
  • 若设置为true,不同项目可通过隐式唤起

  • 设置为false时,一个项目内不同module也可隐式唤起

2、必须确定:PendingIntent.FLAG_IMMUTABLE,PendingIntent.FLAG_IMMUTABLE与通知/Service
  1. 官网可以看到他们的add api情况
  • FLAG_IMMUTABLE API level 23-6.0
  • FLAG_MUTABLE [API level 31]-12
  1. 我是直接minApi 24的,直接使用下列方式适配
  • 本体声明:PendingIntent.FLAG_IMMUTABL
  • 更新使用:PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
  1. 使用介绍
  • 低版本 Android(API 30 及以下) :不会报错或有问题,PendingIntent.FLAG_IMMUTABLE 会被忽略,仍然可以正常工作。
  • 高版本 Android(API 31 及以上) :需要明确指定 FLAG_IMMUTABLE 或 FLAG_MUTABLE,否则可能导致 PendingIntent 无法正常使用。

四、拓展阅读

1、SAF的使用:使用“存储访问框架”打开文件  |  Android 开发者  

1、参考文章:轻松适配Android 10 Scoped Storage 分区存储 | HurryYu

2、权限请求

1、新建项目自带权限(2023-7-28)

  1. 媒体音量控制;
  2. 读取应用列表;
  3. 读取剪切板;
  4. 写入剪切板;

2、常见问题

  1. Android fragment中 onRequestPermissionsResult不起作用
  • 防止被Activity拦截

3、 File类API

  1. listFiles() 返回当前路径下的一系列文件名
  2. getName() 返回此抽象路径名表示的文件或目录的名称。
  3. 通过File对象查看文件路径上是否存在文件,createNewFile来创建新文件
  4. mkdir()和mkdirs()区别 加s的好用,可建立多级多级目录
  5. Java File文件操作样例
  6. 实践: 通过指定存储位置新建File,得到的File文件容量会是0;

image.png

七、传统Intent,SAF和MediaStore

1、样例一:铃声选择方案

使用传统的 Intent 与 RingtoneManager 结合进行铃声选择,并没有显式地使用 SAF

1、 在 Android 10 及更高版本,选择铃声文件的操作可能会受到存储权限的限制。尤其是,当用户选择铃声时,文件访问权限可能被动态地授予或被拒绝,特别是对于存储在外部存储(如 SD 卡)上的文件【目前倒还好,比较正常】 2、 缺陷和改进建议

  1. 使用 SAF 访问铃声文件-特征:通过 Intent.ACTION_CREATE_DOCUMENTIntent.ACTION_OPEN_DOCUMENT 来访问铃声文件。这种方式在 Android 10(API 29)及更高版本中更为推荐,因为它遵循 Scoped Storage 的规则,允许你访问应用所需的文件。
  • 如果你需要访问外部存储中的文件,可以考虑使用 ContentResolver 与 SAF API 来访问铃声文件
  • 使用 MediaStore 来访问媒体文件,或者使用 ACTION_CREATE_DOCUMENT 打开文件选择器。
val pickerIntent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
    putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
    putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true)
    putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentUri)
    putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
    val defaultUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
    putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, defaultUri)
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
}
context.startActivityForResult(pickerIntent, requestCode)

2、样例二:图片选择方案

  • 建议获取到后还是移动到自己私有目录的Image文件夹目录下

1、轻量级:ACTION_GET_CONTENT,Android 1.0 就有,基于内容提供器

  • 返回的 Uri 通常只能 临时使用(系统可能在后台清理缓存,长期持有不稳定)
  • 显示效果融合度也不错,可以点击右侧更多选项进行跳到文件夹页,自己内部的文件夹选项只能展示系统级默认图片文件夹;

private val pickImageLiteLauncher =
    registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
        uri?.let {
            targetUri = it
            context?.let {safeContext->
                Glide.with(safeContext)
                    .load(it)
                    .into(mBindView.ivImage)
            }

        }
    }

pickImageLiteLauncher.launch("image/*")

2、可长期持有:ACTION_OPEN_DOCUMENT,Storage Access Framework (SAF) ,是 Android 4.4 (API 19) 引入的

  • 可长期持有,只要你调用 takePersistableUriPermission 就能长期访问,即使重启也能用,调用一次后把这个 Uri 加到 持久授权列表(persisted permissions)里,存储在系统级别。
  • 之后即使你 杀死 App / 重启手机 / 重新打开 App,只要这个文件还存在,你就能继续访问
fun openGallery(activity: Activity) {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "image/*"
    }
    activity.startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE)
}

八、其他样例

1、Manager和常规读写

     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                // 先判断有没有权限
                if (!Environment.isExternalStorageManager()) {
                    AlertDialog.Builder(context)
                        .setTitle("权限说明")
                        .setMessage("存储权限:用于保存下载申报表功能")
                        .setNegativeButton("取消") { dialog, which -> }
                        .setPositiveButton("授予") { dialog, which ->
                            val intent =
                                Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
                            intent.data = Uri.parse("package:" + AppUtils.getAppPackageName())
                            startActivityForResult(intent, 1024)
                        }
                        .create()
                        .show()
                } else {
                    judgeCopy()
                }
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if (!PermissionUtils.isGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_EXTERNAL_STORAGE)) {
                    PermissionTipDialog.show(childFragmentManager) {
                        if (it) {
                            judgeCopy()
                        }
                    }
                } else {
                    judgeCopy()
                }
            } else {
                judgeCopy()
            }
        }