<
Android 6 10 11 存储适配
>
上一篇

Compose学习笔记 views 与 compose
下一篇

访问共享存储空间

前言

共享存储空间访问的几个方法

1.直接构造路径

2.MediaStore 获取 路径 或 Uri

3.SAF 获取 Uri (Storage Access Framework, 存储访问框架)

->传送门


正文

Before M.

Android 6 之前无需申请动态权限,在AndroidManifest.xml 里静态声明存储权限,即可访问共享存储空间、其它目录下的文件了

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

M. Android 6 动态权限

Android 6 之后需要动态申请权限,除了在AndroidManifest.xml 里声明存储权限外,还需要在代码里动态申请

主要API,检查权限

ActivityCompat.checkSelfPermission(context, permission)

主要API,申请权限

ActivityCompat.requestPermissions(activity, permissionArray, requestCode)

调用了申请代码,系统才会给用户选择是否授予权限,用户授权才能访问共享存储空间

Q. Android 10 Scoped Storage(分区存储)

外部空间的共享目录、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 的)

R. Android 11 Scoped Storage(分区存储)

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);
    }
}

Scoped Storage(分区存储)适配建议

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

使用 MediaStore 获取 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()
    }
}

使用 Storage Access Framework (SAF) 获取 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 适配 不在本文范围内

Top
Foot