Android 10 已经发布了很长一段时间了,并且 Android 11 已经有很大一部分人在使用了,那么你的程序对他做了适配吗?
在 10.0 中,作用域存储变得非常重要,这个新的功能颠覆了我们一直惯用外置存储的方式,因此大量的 app 都面临着代码的适配
本篇文章对作用域存储,以及如何进行适配,做了比较详细的介绍
在 7.0 以前我们访问内存卡中的文件时可以通过 Uri.fromFile ,将 File 转换成 Uri 对象,这个 uri 对象表示这本地真实路径。
复制代码
在 7.0 后,这种通过真实路径来获取的 Uri 被认为是不安全的,所以提供了一种新的解决方案,就是通过 FileProvide 来实现文件的访问,FileProvider 是一种比较特殊的内容提供器,他使用了类似于内容提供器的机制来对数据进行保护。
在7.0以前,访问一个图片如下所示:
String fileName = "defaultImage.jpg";
File file = new File("文件路径", fileName);
Uri uri = Uri.fromFile(file);
复制代码
7.0后,访问如下所示:
File file = new File(CACHE_IMG, fileName);
Uri imageUri=FileProvider.getUriForFile(activity,"com.sandan.fileprovider", file);//这里进行替换uri的获得方式
复制代码
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.sandan.fileprovider"//这里需要和上面部分字符串相同
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
复制代码
<resource xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="images"
path="" />
//path 表示共享的具体路径,这里为空表示整个SD卡进行共享
</resource>
复制代码
然而上面这种真的好吗,对用开发者而且这算是好处吧,但是对用用户而言,上述的无疑一些流氓作用,因为开发者完全可以访问的内存中的所有位置,并作出一些改变,导致 SD 卡中的空间变得非常乱,即使卸载了 app,但是一些垃圾文件却还在内存中。
10.0 中,为了解决上述问题, google 在 Android 10 中加入了作用域功能
什么是作用域呢?就是 Android 系统对 SD 卡做了很大的限制,从 10.0 开始,每个程序只能有权在自己的外置存储空间关联的目录下读取和创建相应的文件,也称作沙箱。获取改目录的代码是:getExternalFilesDir() ,关联的目录路径大致如下:
/storage/emulated/0/Android/data/<包名>/files
复制代码
将数据放在这个目录下,你可以使用之前的方法对文件进行读写,不需要作出任何变更和适配。但是这个文件夹中的文件会随着应用卸载而被随之删除。
那如果需要访问其他目录怎么办呢,比如获取相册中的图片,向相册中添加一张图片。为此,Android 系统针对系统文件类型进行了分类**:图片,音频,视频 这三类文件可以通过 MediaStore API 来进行访问,这种称为共享空间,其他的系统文件需要使用 系统的文件选择器来进行访问,**
另外,如果程序向媒体库写入图片,视频,音频,将会自动用于读写权限,不需要额外申请权限,如果你要读取其他程序向媒体贡献的图片,视频,音频,则必须要申请 READ_EXTERNAL_STORAGE 权限,WRITE_EXTERNAL_STORAGE 权限会在未来的版本中被废弃。
获取系统图片:
val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, "${MediaStore.MediaColumns.DATE_ADDED} desc")
if (cursor != null) {
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
println("image uri is $uri")
}
cursor.close()
}
复制代码
val mimeTypes = arrayOf(
FileIntentUtils.getMap("doc"),
FileIntentUtils.getMap("pdf"), FileIntentUtils.getMap("ppt"),
FileIntentUtils.getMap("xls"), FileIntentUtils.getMap("xlsx")
)
FileIntentUtils.openBle(this, REQUEST_CHOICE_FILE, mimeTypes)
/**
* 选择文件
*/
fun openBle(activity: Activity, code: Int, types: Array<String>) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/*";
intent.putExtra(Intent.EXTRA_MIME_TYPES, types)
activity.startActivityForResult(intent, code)
}
/**
* 获取常见文件类型
* @param key
* @return
*/
fun getMap(key: String): String {
val map: MutableMap<String, String> = HashMap()
map["rar"] = "application/x-rar-compressed"
map["jpg"] = "image/jpeg"
map["png"] = "image/jpeg"
map["jpeg"] = "image/jpeg"
map["zip"] = "application/zip"
map["pdf"] = "application/pdf"
map["doc"] = "application/msword"
map["docx"] = "application/msword"
map["wps"] = "application/msword"
map["xls"] = "application/vnd.ms-excel"
map["et"] = "application/vnd.ms-excel"
map["xlsx"] = "application/vnd.ms-excel"
map["ppt"] = "application/vnd.ms-powerpoint"
map["html"] = "text/html"
map["htm"] = "text/html"
map["txt"] = "text/html"
map["mp3"] = "audio/mpeg"
map["mp4"] = "video/mp4"
map["3gp"] = "video/3gpp"
map["wav"] = "audio/x-wav"
map["avi"] = "video/x-msvideo"
map["flv"] = "flv-application/octet-stream"
map[""] = "*/*"
return map[key] ?: "application/msword"
}
2,选择文件后,intent 会返回一个 uri,然后将 uri 转为 file
/**
* uri 转 file
*/
fun uriToFile(context: Context, uri: Uri): File? = when (uri.scheme) {
ContentResolver.SCHEME_FILE -> uri.toFile()
ContentResolver.SCHEME_CONTENT -> {
val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor?.let { it ->
if (it.moveToFirst()) {
//如果是 10.0 以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//保存到本地
val ois = context.contentResolver.openInputStream(uri)
val displayName =
it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
ois?.let { input ->
val file = File(
context.externalCacheDir?.absolutePath + File.separator,
displayName
)
if (file.exists()) file.delete()
file.createNewFile()
file.outputStream().use { input.copyTo(it) }
file
}
} else {
//com.blankj:utilcodex:1.30.5
UriUtils.uri2File(uri)
}
} else {
it.close()
null
}
}
}
else -> null
}
通过以上步骤,就可以将 uri 转成一个 file 对象,并且支持上传。
//打开文件
data.fileData?.file?.also { file ->
val index = file.name.lastIndexOf(".")
val suffix = file.name.substring(index + 1, file.name.length)
//android 10 之后,需要将文件复制到公有目录下,其他应用才可以打开
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
//10.0 以上则将文件复制到共享目录
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
FileIntentUtils.copyToDownloadAndroidQ(this@WorkReleaseActivity, suffix, file.inputStream(), file.name,"tidycar")
} else {
//否则直接转为 uri
file.toUri()
}
launch(Dispatchers.Main) {
dismissLoading()
//打开文件
FileIntentUtils.openFileEx( uri, suffix, this@WorkReleaseActivity )
}
}
}
/**
* 复制或下载文件到公有目录
*
* @param context
* @param mimeType 文件类型
* @param input 输入流
* @param fileName 文件名称
* @param saveDirName 文件夹名称
* @return
*/
@RequiresApi(api = Build.VERSION_CODES.Q)
fun copyToDownloadAndroidQ( context: Context, mimeType: String?, input: InputStream, fileName: String,saveDirName: String): Uri? {
val file = File(
Environment.getExternalStorageDirectory().path + "/Download/$saveDirName",
fileName
)
//如果公有目录中已经存在相同文件,则直接返回
if (file.exists()) {
return file.toUri()
}
if (!FileQUtils.isExternalStorageReadable()) {
throw RuntimeException("External storage cannot be written!")
}
val values = ContentValues()
//显示名称
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName)
//存储文件的类型
values.put(MediaStore.Downloads.MIME_TYPE, mimeType)
//公有文件路径
values.put(
MediaStore.Downloads.RELATIVE_PATH,
"Download/" + saveDirName.replace("/".toRegex(), "") + "/"
)
//生成一个Uri
val external = MediaStore.Downloads.EXTERNAL_CONTENT_URI
val resolver = context.contentResolver
//写入
val insertUri = resolver.insert(external, values) ?: return null
val fos: OutputStream?
try {
//输出流
fos = resolver.openOutputStream(insertUri)
if (fos == null) return null
var read: Int
val buffer = ByteArray(1444)
while (input.read(buffer).also { read = it } != -1) {
//写入uri中
fos.write(buffer, 0, read)
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
return insertUri
}
在共享目录中,创建一个文件夹,然后将文件复制进去,最后返回 uri 即可
/**
* 打开文件
*/
fun openFileEx(uri: Uri?, fileType: String, context: Context) {
try {
val intent = Intent()
intent.action = Intent.ACTION_VIEW
intent.addCategory("android.intent.category.DEFAULT")
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// 判断版本大于等于7.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val builder = VmPolicy.Builder()
StrictMode.setVmPolicy(builder.build())
}
//getMap 在最上面有代码
intent.setDataAndType(uri, getMap(fileType))
context.startActivity(intent)
} catch (e: Exception) {
}
}
如果你的项目还没有适配,就赶紧提上日程吧!!
Happy Codeing!