一、MediaStore 全流程实战
1. 查询媒体文件 - 精准定位目标
基础查询模板
fun queryImages(contentResolver: ContentResolver): List<Uri> {
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED
)
val selection = "${MediaStore.Images.Media.DATE_ADDED} >= ?"
val selectionArgs = arrayOf(
(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30)).toString()
)
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
val cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)
return cursor?.use {
val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val uris = mutableListOf<Uri>()
while (it.moveToNext()) {
val id = it.getLong(idColumn)
val contentUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
)
uris.add(contentUri)
}
uris
} ?: emptyList()
}
高级查询技巧

2. 创建新媒体文件 - 安全写入共享存储
图片保存实现
suspend fun saveImageToGallery(context: Context, bitmap: Bitmap, fileName: String) {
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp")
put(MediaStore.Images.Media.IS_PENDING, 1)
}
}
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
?: throw IOException("Failed to create new MediaStore record")
try {
resolver.openOutputStream(uri)?.use { outputStream ->
if (!bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)) {
throw IOException("Failed to save bitmap")
}
}
} catch (e: Exception) {
resolver.delete(uri, null, null)
throw e
} finally {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.clear()
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(uri, contentValues, null, null)
}
}
}
3. 文件操作的高级技巧
批量删除(Android 11+)
List<Uri> urisToDelete = Arrays.asList(uri1, uri2, uri3);
PendingIntent deleteIntent = MediaStore.createDeleteRequest(
getContentResolver(),
urisToDelete
);
try {
startIntentSenderForResult(
deleteIntent.getIntentSender(),
REQUEST_CODE_DELETE,
null, 0, 0, 0
);
} catch (IntentSender.SendIntentException e) {
Log.e(TAG, "Failed to start delete intent", e);
}
获取文件元数据
fun getImageMetadata(contentResolver: ContentResolver, uri: Uri): ImageMetadata? {
val projection = arrayOf(
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.HEIGHT,
MediaStore.Images.Media.LATITUDE,
MediaStore.Images.Media.LONGITUDE
)
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
return ImageMetadata(
name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)),
width = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.WIDTH)),
height = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.HEIGHT)),
location = if (!cursor.isNull(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.LATITUDE))) {
Pair(
cursor.getDouble(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.LATITUDE)),
cursor.getDouble(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.LONGITUDE))
)
} else null
)
}
}
return null
}
二、存储访问框架(SAF)应用
1. 三大核心 Intent 详解

2. 文件选择实战
选择单个文件
fun openFilePicker(activity: Activity) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf(
"application/pdf",
"application/msword",
"image/*"
))
}
activity.startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_OPEN_DOCUMENT && resultCode == RESULT_OK) {
data?.data?.let { uri ->
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
openFile(uri)
}
}
}
选择多个文件
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.setType("*/*")
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
startActivityForResult(intent, REQUEST_CODE_OPEN_MULTIPLE)
// 处理结果
if (requestCode == REQUEST_CODE_OPEN_MULTIPLE && resultCode == RESULT_OK) {
ClipData clipData = data.getClipData()
if (clipData != null) {
for (int i = 0
Uri uri = clipData.getItemAt(i).getUri()
// 处理每个URI
}
} else if (data.getData() != null) {
// 单个文件的情况
Uri uri = data.getData()
}
}
3. 目录访问与管理
请求目录访问权限
fun requestDirectoryAccess(activity: Activity) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
putExtra(DocumentsContract.EXTRA_INITIAL_URI,
Uri.parse("content://com.android.externalstorage.documents/document/primary:Documents"))
}
}
activity.startActivityForResult(intent, REQUEST_CODE_DIRECTORY)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_DIRECTORY && resultCode == RESULT_OK) {
data?.data?.let { treeUri ->
contentResolver.takePersistableUriPermission(
treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
saveTreeUri(treeUri)
}
}
}
使用 DocumentFile 操作目录
DocumentFile treeDir = DocumentFile.fromTreeUri(context, treeUri);
for (DocumentFile file : treeDir.listFiles()) {
if (file.isDirectory()) {
Log.d(TAG, "Directory: " + file.getName());
} else {
Log.d(TAG, "File: " + file.getName() + " Size: " + file.length());
}
}
DocumentFile newFile = treeDir.createFile(
"text/plain",
"new_document.txt"
);
try (OutputStream out = getContentResolver().openOutputStream(newFile.getUri())) {
out.write("Hello SAF World!".getBytes());
}
newFile.delete();
三、跨应用文件共享实战
1. FileProvider 配置与使用
配置步骤
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<paths>
<files-path name="internal_files" path="." />
<external-files-path name="external_files" path="." />
<cache-path name="cache_files" path="." />
<external-path name="shared_downloads" path="Download/Shared" />
</paths>
生成共享URI
fun getShareableUri(context: Context, file: File): Uri {
return FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
}
fun createShareIntent(context: Context, file: File): Intent {
val uri = getShareableUri(context, file)
return Intent(Intent.ACTION_SEND).apply {
type = context.contentResolver.getType(uri)
putExtra(Intent.EXTRA_STREAM, uri)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
}
2. 安全接收外部文件
处理接收的文件
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent?.action == Intent.ACTION_VIEW) {
handleIncomingFile(intent.data)
}
}
private fun handleIncomingFile(uri: Uri?) {
uri ?: return
val modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION
if (context.checkUriPermission(uri, Process.myPid(), Process.myUid(),
modeFlags) != PackageManager.PERMISSION_GRANTED) {
contentResolver.takePersistableUriPermission(uri, modeFlags)
}
val inputStream = contentResolver.openInputStream(uri)
val outputDir = context.getExternalFilesDir(null)
val outputFile = File(outputDir, "received_file_${System.currentTimeMillis()}")
inputStream?.use { input ->
FileOutputStream(outputFile).use { output ->
input.copyTo(output)
}
}
processReceivedFile(outputFile)
}
四、分区存储兼容性全攻略
1. 版本兼容处理策略

2. 兼容性工具类实现
object StorageCompat {
fun saveImage(context: Context, bitmap: Bitmap, fileName: String) {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
saveViaMediaStore(context, bitmap, fileName)
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
if (hasStoragePermission(context)) {
saveToPublicDirectory(context, bitmap, fileName)
} else {
requestStoragePermission(context)
}
}
else -> {
saveToPublicDirectory(context, bitmap, fileName)
}
}
}
fun loadImage(context: Context, uri: Uri): Bitmap? {
return try {
context.contentResolver.openInputStream(uri)?.use { stream ->
BitmapFactory.decodeStream(stream)
}
} catch (e: SecurityException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestStoragePermission(context)
}
null
}
}
private fun saveViaMediaStore(context: Context, bitmap: Bitmap, fileName: String) {
}
private fun saveToPublicDirectory(context: Context, bitmap: Bitmap, fileName: String) {
val picturesDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES
)
val imageFile = File(picturesDir, fileName)
FileOutputStream(imageFile).use { out ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
}
MediaScannerConnection.scanFile(
context,
arrayOf(imageFile.absolutePath),
null,
null
)
}
}
3. 权限兼容处理
fun requestNeededPermissions(activity: Activity) {
val permissionsToRequest = mutableListOf<String>()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
if (!hasPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
permissionsToRequest.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
} else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
if (!hasPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)) {
permissionsToRequest.add(Manifest.permission.READ_EXTERNAL_STORAGE)
}
} else {
if (!hasPermission(activity, Manifest.permission.READ_MEDIA_IMAGES)) {
permissionsToRequest.add(Manifest.permission.READ_MEDIA_IMAGES)
}
if (!hasPermission(activity, Manifest.permission.READ_MEDIA_VIDEO)) {
permissionsToRequest.add(Manifest.permission.READ_MEDIA_VIDEO)
}
}
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
activity,
permissionsToRequest.toTypedArray(),
REQUEST_CODE_STORAGE_PERMISSION
)
}
}
五、用户交互设计最佳实践
1. 权限请求时机优化

2. 文件操作进度反馈
fun copyFileWithProgress(
context: Context,
sourceUri: Uri,
destUri: Uri,
progressCallback: (Float) -> Unit
) {
val input = context.contentResolver.openInputStream(sourceUri)
val output = context.contentResolver.openOutputStream(destUri)
input?.use { inStream ->
output?.use { outStream ->
val totalBytes = context.contentResolver.openAssetFileDescriptor(sourceUri, "r")
?.use { it.length } ?: 0L
var copiedBytes = 0L
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytesRead = inStream.read(buffer)
while (bytesRead >= 0) {
outStream.write(buffer, 0, bytesRead)
copiedBytes += bytesRead
val progress = if (totalBytes > 0) {
copiedBytes.toFloat() / totalBytes
} else 0f
progressCallback(progress)
bytesRead = inStream.read(buffer)
}
}
}
}