自从Android 10推出分区存储(Scoped Storage)后,我们在处理外部存储的文件时面临了新的挑战。本文将探讨在适配分区存储之后,如何有效地获取外部存储的File对象。
一、理解分区存储 🧐
分区存储是为了加强用户数据的安全性和隐私而引入的。这意味着应用现在对存储的访问受到更严格的限制。用户需要适应这一变化,确保应用的兼容性和功能性。
分区存储详细解释
二、申请必要的权限 🔑
即使在分区存储环境中,访问外部存储的File对象仍然需要相应的权限。确保你的应用已经请求了必要的存储权限。这是保证应用顺利运行的基础步骤。
在 AndroidManifest.xml
中添加必要的权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
三、使用新的文件访问方法 🔄
在分区存储模型下,传统的文件访问方法可能不再有效。开发者需要探索新的API,如MediaStore
和Storage Access Framework
,这些API提供了与分区存储兼容的文件访问机制。
- 使用MediaStore访问媒体文件:
import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.provider.MediaStore
fun queryMediaFiles(context: Context) {
val contentResolver: ContentResolver = context.contentResolver
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED
)
val selection = "${MediaStore.Images.Media.MIME_TYPE} = ?"
val selectionArgs = arrayOf("image/jpeg")
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
val queryUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val cursor = contentResolver.query(
queryUri,
projection,
selection,
selectionArgs,
sortOrder
)
cursor?.use {
val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val nameColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
val dateAddedColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)
while (it.moveToNext()) {
val id = it.getLong(idColumn)
val name = it.getString(nameColumn)
val dateAdded = it.getLong(dateAddedColumn)
val contentUri = ContentUris.withAppendedId(queryUri, id)
// 处理查询到的媒体文件信息
// ...
}
}
}
- 使用Storage Access Framework(SAF)选择外部存储中的文件:
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.OpenableColumns
private const val REQUEST_CODE_PICK_FILE = 1001
fun pickFileFromExternalStorage(activity: Activity) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*" // */
}
activity.startActivityForResult(intent, REQUEST_CODE_PICK_FILE)
}
fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?, activity: Activity) {
if (requestCode == REQUEST_CODE_PICK_FILE && resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
activity.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
val fileName = cursor.getString(nameIndex)
// 处理选择的文件
// ...
}
}
}
}
- 复制一份临时文件到应用文件存储中(使用Storage Access Framework 获取到的uri)。
@RequiresApi(Build.VERSION_CODES.Q)
fun copyFileFromUriToAppStorage(context: Context, uri: Uri): File? {
val contentResolver: ContentResolver = context.contentResolver
val displayName = getDisplayName(context, uri)
val inputStream = contentResolver.openInputStream(uri)
var file: File? = null
inputStream?.use { input ->
// 创建临时文件,将输入流的内容写入应用程序的缓存文件中
file = File(context.cacheDir, displayName)
FileOutputStream(file).use { output ->
val buffer = ByteArray(4 * 1024) // 4KB 缓冲区
var read: Int
while (input.read(buffer).also { read = it } != -1) {
output.write(buffer, 0, read)
}
output.flush()
}
}
return file
}
@SuppressLint("Range")
private fun getDisplayName(context: Context, uri: Uri): String {
var displayName = ""
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
displayName = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
return displayName
}
感谢阅读,Best Regards!