android 媒体文件分区适配
最近刚好有个需求做了媒体文件的分区适配,记录以下 官方文档地址:https://developer.android.com/guide/topics/data?hl=zh-cn
问题点
之前的下载文件都在/storage/emulated/0/android/data/pagename/file 中,首先这个属于应用内部的文件系统,而且是不需要读写权限的申请的,它会跟随应用的卸载而删除。
由于Android Q文件存储机制修改成了沙盒模式,同时对应用内部的文件系统加强了访问控制,就是一般用户都看不到这个文件,导致下载了,用户也找不到,即使用了getContentResolver().insert 能看到但是不能播放.
主要目的:
- 非必要少去申请权限
- 不跟随应用卸载而删除
- 下载后方便查看和播放
- 非隐私文件
解决
Android Q文件存储机制修改成了沙盒模式,对于图片和视频这种媒体文件google希望是放在手机自带的媒体文件夹中。 参考资料:https://developer.android.com/training/data-storage/shared/media?hl=zh-cn
手机自带的媒体文件:
- 图片,存储在 DCIM/ 和 Pictures/ 目录中。系统将这些文件添加到 MediaStore.Images 表格中。
- 视频,存储在 DCIM/、Movies/ 和 Pictures/ 目录中。系统将这些文件添加到 MediaStore.Video 表格中。
- 音频文件,存储在 Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/ 和 Ringtones/ 目录中,以及位于 Music/ 或 Movies/ 目录中的音频播放列表中。系统将这些文件添加到 MediaStore.Audio 表格中。
- 下载的文件,存储在 Download/ 目录中。在搭载 Android 10(API 级别 29)及更高版本的设备上,这些文件存储在 MediaStore.Downloads 表格中。此表格在 Android 9(API 级别 28)及更低版本中不可用。
具体的解决方案:将文件存储在媒体库里。 适配方案:google在android10上,已经开启的存储分区,但是也可以关闭android:requestLegacyExternalStorage="true" ,但是在android11上是强制开启的,所以主要的适配方案为:android11以下 和 android11及以上。
代码
下载
android11及以上
@RequiresApi(Build.VERSION_CODES.Q)
fun downR(fileName: String, relativePath: String, inputStream: InputStream) {
val resolver = contentResolver
val audioCollection = MediaStore.Video.Media
.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val videoDetails = ContentValues().apply {
put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
put(MediaStore.Video.Media.IS_PENDING, 1)
put(MediaStore.Video.Media.OWNER_PACKAGE_NAME, packageName)
put(MediaStore.Video.Media.RELATIVE_PATH, relativePath)
}
val videoContentUri = resolver.insert(audioCollection, videoDetails)
videoContentUri?.let {
resolver.openFileDescriptor(videoContentUri, "w", null).use { pfd ->
val output = FileOutputStream(pfd?.fileDescriptor)
output.use {out->
inputStream.copyTo(out) {
println("进度:$it")
}
}
videoDetails.clear()
videoDetails.put(MediaStore.Audio.Media.IS_PENDING, 0)
resolver.update(videoContentUri, videoDetails, null, null)
}
}
}
inline fun InputStream.copyTo(
out: OutputStream,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
process: (Long) -> Unit
): Long {
var bytesCopied: Long = 0
val byteArray = ByteArray(bufferSize)
var bytes = read(byteArray)
while (bytes > 0) {
out.write(byteArray, 0, bytes)
bytesCopied += bytes
bytes = read(byteArray)
process.invoke(bytesCopied)
}
return bytesCopied
}
android11以下
就是普通的文件写入操作,唯一的区别就是路径要跟android11上面下载的路径一致。
fun down(fileName: String, relativePath: String, inputStream: InputStream) {
val savePath = getMoviesFileCache(fileName)
val output: OutputStream = FileOutputStream(savePath)
output.use {out->
inputStream.copyTo(out) {
println("进度:$it")
}
}
}
fun getMoviesFileCache(fileName: String): File? {
val file = File(
Environment.getExternalStorageDirectory().absolutePath + File.separator +
Environment.DIRECTORY_MOVIES + "你的相对路径,android11上的RELATIVE_PATH"
)
if (!file.exists()) {
file.mkdirs()
}
return File(file.absolutePath + File.separator + fileName);
}
查询
android11
private fun queryMediaBean(fileName: String): MediaStoreBean? {
val projection = arrayOf(
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.DATE_ADDED,
MediaStore.Video.Media.RELATIVE_PATH,
)
val selection1 =
"${MediaStore.Video.Media.RELATIVE_PATH} = ? and ${MediaStore.Video.Media.DISPLAY_NAME} = ?"
val selectionArgs1 = arrayOf(
"你的相对路径",
fileName
)
val sortOrder = "${MediaStore.Video.Media.DATE_ADDED} DESC"
BaseApplication.context.contentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
projection,
selection1,
selectionArgs1,
sortOrder
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
val dateModifiedColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED)
val displayNameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
val relativePathColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.RELATIVE_PATH)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val dateModified =
Date(TimeUnit.SECONDS.toMillis(cursor.getLong(dateModifiedColumn)))
val displayName = cursor.getString(displayNameColumn)
val contentUri = ContentUris.withAppendedId(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
id
)
val relativePath = cursor.getString(relativePathColumn)
}
}
}
android11以下
检测文件是否存在就行了
fun queryLocalVideoExist(vehicleId: String, fileName: String) : Boolean {
val filePath: String = us.nonda.zus.download.util.FileUtil.getDCVideoPath(vehicleId+"_"+fileName)
val file = File(filePath)
return file.exists()
}
删除
android11
android11删除文件很麻烦,需要2次确认
private fun performDeleteImage(image: MediaStoreImage) {
try {
this.contentResolver.delete(
image.contentUri,
"${MediaStore.Images.Media._ID} = ?",
arrayOf(image.id.toString())
)
} catch (securityException: SecurityException) {
if (VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val b = securityException is RecoverableSecurityException
val recoverableSecurityException =
securityException as? RecoverableSecurityException
?: throw securityException
pendingDeleteImage = image
startIntentSender(recoverableSecurityException.userAction.actionIntent.intentSender)
} else {
throw securityException
}
}
}
private val DELETE_PERMISSION_REQUEST = 0x1033
private fun startIntentSender(intentSender: IntentSender?) {
intentSender?.let {
startIntentSenderForResult(
intentSender,
DELETE_PERMISSION_REQUEST,
null,
0,
0,
0,
null
)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK && requestCode == DELETE_PERMISSION_REQUEST) {
pendingDeleteImage?.let {
pendingDeleteImage = null
performDeleteImage(it)
}
}
}
android11以下
public static void deleteDCVideo(String fileName) {
try {
String filePath = FileUtil.getDCVideoPath(fileName);
File file = new File(filePath);
if (file.exists()) {
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
|