Android 10 适配文件读取
简介出发点
官方文档的说明是为了让用户能更好地管理自己的文件并减少混乱,Android 10 引入了称为分区存储的隐私权变更,即以 Android 10及更高版本为目标平台的应用,在默认情况下,只能看到本应用专有的目录。言下之意就是Android 10 我们是没有权限操作非本应用下的其他文件夹的,这就出现了这个所谓的适配,就是将非本应用下的文件获取出来,然后放到沙盒里面去在进行操作。
图片、视频等媒体文件的获取以及操作
作为一个Android开发,我们都知道通过以下几个步骤就可以获取到图片在文件夹里面的路径
private val IMAGE_PROJECTION = arrayOf(
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media._ID,
MediaStore.Images.Media.BUCKET_ID,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME
)
val imageCursor: Cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION,
null, null, IMAGE_PROJECTION[4] + " DESC")!!
while(imageCursor.moveToNext()){
var path = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[0]))
这时候输出path就可以看到文件路径,但是这是在targetSdkVersion < 29 也就是Android10以前才能使用的方法,在Android10获取到这个路径无法进行图片加载以及上传等操作,这时候就需要对这个文件路径进行操作适配,一般有两种方法。
1.在AndroidManifest.xml文件的application节点添加以下代码:
android:preserveLegacyExternalStorage="true"
这句代码有什么作用呢?简单的说就是让开发者暂时关闭存储分区,忽略Android 10 内存特性;为什么说是暂时?因为在Android11 targetSdkVersion = 30 的时候就会强制开启存储分区,这句代码就没用了,所以不推荐这种方法。
2.将文件添加到沙盒,在进行操作:
在Android10 我们无法通过MediaStore.Images.Media.DATA去获取文件路径,而是要通过ID进行操作
//获取图片id
val id = imageCursor.getInt(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[2]))
//通过id获取沙盒地址
var path = MediaStore.Images.Media
.EXTERNAL_CONTENT_URI
.buildUpon()
.appendPath(id.toString()).build().toString()
//
通过id获取到的这个path沙盒地址可以进行图片的加载,给Imageview设置图片,但是这个path路径无法进行上传等操作,这时候需要将文件保存到沙盒,在进行上传等操作。
将文件保存到沙盒工具类:
@RequiresApi(Build.VERSION_CODES.Q)
private fun saveUriToFile(context: Context, uri: Uri): File?{
if (uri.scheme == ContentResolver.SCHEME_FILE)
return File(requireNotNull(uri.path))
else if (uri.scheme == ContentResolver.SCHEME_CONTENT){
//
val contentResolver = context.contentResolver
val displayName = run {
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.let {
if(it.moveToFirst())
it.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
else null
}
}
val ios = contentResolver.openInputStream(uri)
if (ios != null){
return File("${context.externalCacheDir!!.absolutePath}/$displayName")
.apply {
val fos = FileOutputStream(this)
FileUtils.copy(ios, fos)
fos.close()
ios.close()
}
}else
return null
}else
return null
}
返回值是一个File,toString就可以拿到地址,直接进行上传等操作。
视频获取操作说明
视频的获取和图片的获取都是一样的了,出发点都是MediaStore,把Images改成Video就可以了。
private val VIDEO_PROJECTION = arrayOf(
MediaStore.Video.Media.DATA,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media._ID
)
fun getVideo(context: Context):String?{
val videoCursor:Cursor =
context.contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
VIDEO_PROJECTION,null,null, null)!!
if (videoCursor.moveToNext()){
var path = videoCursor.getString(videoCursor.getColumnIndexOrThrow(VIDEO_PROJECTION[0]))
val name = videoCursor.getString(videoCursor.getColumnIndexOrThrow(VIDEO_PROJECTION[1]))
val id = videoCursor.getInt(videoCursor.getColumnIndexOrThrow(VIDEO_PROJECTION[2]))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
/**
* 这个地址是沙盒地址
* 可以直接使用glide进行加载,但是不能做上传等操作
*
* content://media/external/video/media/1302869
* */
path = MediaStore.Video.Media
.EXTERNAL_CONTENT_URI
.buildUpon()
.appendPath(id.toString()).build().toString()
/**
* 将文件保存到沙盒内,在对沙盒进行上传等操作
*
* /storage/emulated/0/Android/data/com.yangchoi.adnroidadaptation/cache/c8f0cbd3bb674544b6756c1f4d2333ee.mp4
* */
val fileQ = saveUriToFile(context, Uri.parse(path))
Log.e("videoTAG","地址:$path 沙盒地址:$fileQ")
return fileQ.toString()
}else{
//在Android10 这个path无法直接显示
//这个地址无法直接展示
//需要在AndroidManifest.xml文件中添加以下代码android:requestLegacyExternalStorage="true"
Log.e("videoTAG","path:$path name:$name id:$id")
return path
}
}else
return null
}
文件、文件夹的创建以及读写
官方明确说明,在Android 10 无法在公共目录(storage/emulated/0/)下创建文件夹,所以Android 10 不能使用Environment.getExternalStorageDirectory().path的方式创建文件夹。
文件夹创建步骤:
1.添加权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
private fun requestPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 先判断有没有读写权限
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) === PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) === PackageManager.PERMISSION_GRANTED) {
//文件操作
} else {
ActivityCompat.requestPermissions(this, arrayOf<String>(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE),SUCCESS_CODE)
}
} else {
//文件操作
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == SUCCESS_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) === PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) === PackageManager.PERMISSION_GRANTED) {
//文件操作
} else {
//Permission error
}
}
}
读写权限获取到了还有一点需要注意,Android 10 在操作内存的时候需要在AndroidManifest.xml的application节点方法添加以下代码,不然还是无法操作的:
<application
android:requestLegacyExternalStorage="true"
2.创建文件夹:
Android10只能在/storage/emulated/0/Android/data/包名/files目录下创建
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
val path = ContextCompat.getExternalFilesDirs(context, null)[0].absolutePath + "/testdirs/"
val dirFile = File(path)
if (!dirFile.exists()){
dirFile.mkdirs()
}
}else{
...
}
3.创建文件:
文件的创建必须依托与之前创建的文件夹路径
//文件夹路径
val filePath = ContextCompat.getExternalFilesDirs(this, null)[0].absolutePath + "/testdirs/"
//
var fileDirs = File(filePath)
if(fileDirs.exists()){
var file = File(filePath + fileName)
//不存在则创建
if(!file.exists()){
file.createNewFile()
}
}
4.给文件写入内容:
val file = File(strFilePath)
if (file.exists()){
val raf = RandomAccessFile(file, "rwd")
raf.seek(file.length())
raf.write(strContent.toByteArray())
raf.close()
}else{
//... 文件夹不存在
}
保存文件到公共目录
/**
* 保存文件到公共目录 比如 picture download等目录,这里用download目录作为例子
* @param context
* @param sourcePath 公共目录根地址storage/emulated/0/
* @param fileName 保存的文件名称
* @param saveDirName 保存的文件夹名称
* */
@RequiresApi(Build.VERSION_CODES.Q)
fun saveToStorageFile(context: Context, sourcePath:String, fileName:String, saveDirName:String){
val values = ContentValues()
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName)
/**
* 文件类型
* apk application/vnd.android.package-archive
* 图片 image/png
*
* 要保存其他内容对此属性进行修改就行
* */
values.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive")
values.put(MediaStore.Downloads.RELATIVE_PATH, "Download/" + saveDirName + "/")
val external = MediaStore.Downloads.EXTERNAL_CONTENT_URI
val resolver: ContentResolver = context.getContentResolver()
val insertUri = resolver.insert(external, values) ?: return
val mFilePath = insertUri.toString()
var `is`: InputStream? = null
var os: OutputStream? = null
try {
os = resolver.openOutputStream(insertUri)
if (os == null) {
return
}
var read: Int
val sourceFile: File = File(sourcePath)
if (sourceFile.exists()) { // 文件存在时
`is` = FileInputStream(sourceFile) // 读入原文件
val buffer = ByteArray(1444)
while (`is`.read(buffer).also { read = it } != -1) {
os.write(buffer, 0, read)
}
`is`.close()
os.close()
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
} finally {
`is`?.close()
os?.close()
}
}
明文http请求限制
当targetSdkVersion >= 29时,直接使用http明文请求会触发以下异常:
onFailure:CLEARTEXT communication to www.wanandroid.com not permitted by network security policy
配置方法:
在res文件下创建xml文件夹,并创建network_config.xml文件
文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
在AndroidManifest.xml文件的application节点添加以下代码:
android:networkSecurityConfig="@xml/network_config"
结果:
请求定位权限
在api29以前获取定位权限只需要注册ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION就可以了,注册了这两个权限可以同时在前台和后台获取到定位信息,但是在API 29 以后官方新增了一个ACCESS_BACKGROUND_LOCATION权限,想要在后台获取定位信息需要通过该权限去获取。
AndroidManifest.xml声明以下权限:
<!-- 前台定位权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- 后台定位权限 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
动态获取权限:
@RequiresApi(Build.VERSION_CODES.M)
private fun Context.checkLocationPermissionAPI29(locationRequestCode : Int) {
if (checkSinglePermission(Manifest.permission.ACCESS_FINE_LOCATION) &&
checkSinglePermission(Manifest.permission.ACCESS_COARSE_LOCATION) &&
checkSinglePermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) return
val permList = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION)
requestPermissions(permList, locationRequestCode)
}
private fun Context.checkSinglePermission(permission: String) : Boolean {
return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}
以上就是部分Android 10 的适配内容了,主要还是工作中遇到的问题。
代码存放在Github欢迎翻阅查看
想进一步了解可以导入项目并在不同版本的手机上运行查看效果。