共享存储空间访问的几个方法
1.直接构造路径
2.MediaStore 获取 路径 或 Uri
3.SAF 获取 Uri (Storage Access Framework, 存储访问框架)
->传送门
Android 6 之前无需申请动态权限,在AndroidManifest.xml 里静态声明存储权限,即可访问共享存储空间、其它目录下的文件了
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Android 6 之后需要动态申请权限,除了在AndroidManifest.xml 里声明存储权限外,还需要在代码里动态申请
主要API,检查权限
ActivityCompat.checkSelfPermission(context, permission)
主要API,申请权限
ActivityCompat.requestPermissions(activity, permissionArray, requestCode)
调用了申请代码,系统才会给用户选择是否授予权限,用户授权才能访问共享存储空间
外部空间的共享目录、APP其它目录,不能通过路径访问,不能直接增删改,只能通过 Uri 访问
适配方法
1.硬件设备不升级 Android10
2.设置 APP 的 targetSdkVersion < 29 (Google给开发者的缓冲,应用商店会逐步要求把 targetSdkVersion 升上去的)
3.禁用分区存储(Google给开发者的缓冲,targetSdkVersion==29 时才有效,targetSdkVersion==30 无效 即 Android 11 后会忽略该字段,强制开启分区存储)
AndroidManifest.xml 的 application 标签
android:requestLegacyExternalStorage="true"
4.老实按官方的要求做适配(代码在 Android11 的)
外部空间的共享目录媒体文件可以通过路径直接访问,权限是只读
开放访问共享存储空间API
1.声明管理权限
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
2.动态申请所有文件访问权限
private void testAllFiles() {
//运行设备>=Android 11.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
//检查是否已经有权限
if (!Environment.isExternalStorageManager()) {
//跳转新页面申请权限
startActivityForResult(new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION), 101);
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//申请权限结果
if (requestCode == 101) {
if (Environment.isExternalStorageManager()) {
Toast.makeText(MainActivity.this, "访问所有文件权限申请成功", Toast.LENGTH_SHORT).show();
//遍历目录
showAllFiles();
}
}
}
3.遍历目录、读写文件
private void showAllFiles() {
File file = Environment.getExternalStorageDirectory();
File[] list = file.listFiles();
for (int i = 0; i < list.length; i++) {
String name = list[i].getName();
Log.d("test", "fileName:" + name);
}
}
1.APP私有目录用起来
主要API
ContextWrapper.getExternalCacheDir()
ContextWrapper.getExternalFilesDir("")
ContextWrapper.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
ContextWrapper.getExternalFilesDir(Environment.DIRECTORY_MUSIC)
ContextWrapper.getExternalFilesDir(Environment.DIRECTORY_MOVIES)
2.适配共享存储
MediaStore 和 SAF 获取 Uri
在 Android 11(API 30)及以上版本中,使用 MediaStore
时需要遵循分区存储(Scoped Storage)的最佳实践。
Context -> ContentResolver -> Cursor -> Uri
MediaStore 示例
// Android 10+
private fun saveBitmapToPublicGallery(bitmap: Bitmap, context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10+ 使用 MediaStore 在公有目录写入文件
val contentValues = ContentValues().apply {
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
val dateString = dateFormat.format(Date())
put(MediaStore.Images.Media.DISPLAY_NAME, "saved_qrcode_image_${dateString}.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(
MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + "/${packageName}"
) // 存储目录
}
context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)?.let { uri ->
context.contentResolver.openOutputStream(uri).use { outputStream ->
if (outputStream != null) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
Log.d("TAG", "saveBitmapToPublicGallery: $uri")
Toast.makeText(context, "图片已保存: $uri", Toast.LENGTH_LONG).show()
}
}
} ?: {
Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show()
}
} else {
// 处理 Android 10 及以下版本的情况
// 这里可以使用旧的方法直接写入外部存储,需要权限 WRITE_EXTERNAL_STORAGE
PermissionX.init(this)
.permissions(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
// 有权限后 写入文件
saveBitmapToJpeg(bitmap, context)
} else {
Toast.makeText(
this,
"These permissions are denied: $deniedList",
Toast.LENGTH_LONG
).show()
}
}
}
}
// Android 10 以下
private fun saveBitmapToJpeg(bitmap: Bitmap, context: Context) {
val publicDirectory =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
val dir = File(publicDirectory, packageName)
if (dir.exists().not() && dir.mkdirs().not()) {
Log.d("TAG", "saveBitmapToJpeg: mkdirs failed")
Toast.makeText(context, "目录创建失败", Toast.LENGTH_SHORT).show()
return
}
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
val dateString = dateFormat.format(Date())
val filePath = File(dir, "saved_qrcode_image_$dateString.jpg")
try {
FileOutputStream(filePath).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
Log.d("TAG", "saveBitmapToJpeg: ${filePath.absolutePath}")
Toast.makeText(context, "图片已保存: ${filePath.absolutePath}", Toast.LENGTH_LONG)
.show()
}
} catch (e: IOException) {
e.printStackTrace()
Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show()
}
}
在使用 SAF
时,用户需要手动选择文件,应用无法直接访问文件系统。
Intent -> Uri
// 这里调用时传入 Activity Result API
fun openFilePicker(launcher: ActivityResultLauncher<Intent>) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*" // 指定文件类型,可以根据需要调整
}
launcher.launch(intent)
}
3.适配其它目录
在Android 11上需要申请访问所有文件的权限
(一般APP没必要用,文件管理器、病毒扫描等才用得到)
N. Android 7 FileProvider 适配 不在本文范围内