美文网首页
Android 沙箱机制

Android 沙箱机制

作者: NIIIICO | 来源:发表于2023-07-12 16:28 被阅读0次

一、存储空间分类

1、内部存储,无需权限,卸载删除

getCacheDir(): /data/user/0/com.example.storagedemo/cache
getFilesDir(): /data/user/0/com.example.storagedemo/files

2、外部存储,无需权限,卸载删除

getExternalCacheDir(): /storage/emulated/0/Android/data/com.example.storagedemo/cache
getExternalFilesDir(Environment.DIRECTORY_PICTURES): /storage/emulated/0/Android/data/com.example.storagedemo/files/Pictures

3、外部存储,需要权限或通过MediaStore操作,卸载不删除

Environment.getExternalStorageDirectory(): /storage/emulated/0
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES): /storage/emulated/0/Pictures

二、存储空间使用

1、Android 11增加新的权限,允许管理所有文件,可以随意操作存储空间

android.permission.MANAGE_EXTERNAL_STORAGE

// 在AndroidManifest.xml中添加以下权限(Android 11,SDK_INT = 30增加)
<uses-permission
    android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
    tools:ignore="ScopedStorage" />

// 权限检测与申请
class MainActivity : AppCompatActivity(), View.OnClickListener {
    companion object {
        const val TAG = "StorageDemo"
    }

    private var mRequestManagePLauncher: ActivityResultLauncher<Intent>? = null

    private fun initView() {
        mRequestManagePLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
                if (hasManageP()) {
                    Toast.makeText(this@MainActivity, "申请管理权限成功", Toast.LENGTH_SHORT).show()
                } else {
                    Toast.makeText(this@MainActivity, "申请管理权限失败,请重试", Toast.LENGTH_SHORT).show()
                }
            }
    }

    /**
     * 判断管理权限
     */
    private fun hasManageP(): Boolean {
        return Environment.isExternalStorageManager()
    }

    /**
     * 请求管理权限
     */
    private fun requestManagerP() {
        mRequestManagePLauncher?.launch(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION))
            .apply {
                if (this == null) {
                    Toast.makeText(this@MainActivity, "跳转异常,请稍后重试", Toast.LENGTH_SHORT).show()
                }
            }
    }

    /**
     * 如果有管理权限,则可以随意操作存储空间
     */
    private fun writeFileWithP() {
        if (hasManageP()) {
            val writeFile = File(
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
                "write.txt"
            )
            var fOS: FileOutputStream? = null
            try {
                fOS = FileOutputStream(writeFile)
                fOS.write("我是测试内容".toByteArray())
                Toast.makeText(
                    this@MainActivity,
                    "写入成功:${writeFile.absolutePath}",
                    Toast.LENGTH_SHORT
                ).show()
            } catch (e: java.lang.Exception) {
                Toast.makeText(this@MainActivity, "写入失败:$e", Toast.LENGTH_SHORT).show()
            } finally {
                try {
                    fOS?.close()
                } catch (e: java.lang.Exception) {
                    Log.v(TAG, "error:$e")
                }
            }
        } else {
            requestManagerP()
        }
    }
}
2、如果不申请MANAGE_EXTERNAL_STORAGE权限的话,则需要通过MediaStore操作存储空间

通过MediaStore获取到的Uri主要有以下几种:
外部Uri
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
内部Uri
MediaStore.Video.Media.INTERNAL_CONTENT_URI
MediaStore.Audio.Media.INTERNAL_CONTENT_URI
MediaStore.Images.Media.INTERNAL_CONTENT_URI

共享文件,包括媒体文件和非媒体文件
MediaStore.Files.getContentUri("external") // 操作external.db数据库

(1)查询

需要注意:
1、当targetSdk 29时,如果想通过MediaStore获取公共媒体文件,则必须申请READ_EXTERNAL_STORAGE权限;
2、当targetSdk 30时,即使申请了READ_EXTERNAL_STORAGE权限,也可能无法获取到公共媒体文件(自己创建的除外),此时必须申请MANAGE_EXTERNAL_STORAGE才可以继续获取。

class MainActivity : AppCompatActivity(), View.OnClickListener {
    companion object {
        const val TAG = "StorageDemo"
        const val REQUEST_READP_CDOE = 10000
    }

    private fun queryPic() {
        if (hasReadP()) {
            val imageExternalUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
            // 查询哪几类
            val projection = arrayOf(
                MediaStore.MediaColumns._ID,
                MediaStore.MediaColumns.DATA,
                MediaStore.MediaColumns.RELATIVE_PATH,
                MediaStore.MediaColumns.DISPLAY_NAME
            )
            // 查询条件
            val selection =
                "${MediaStore.MediaColumns.RELATIVE_PATH} = ? "
            // 参数
            val selectionArgs = arrayOf(
                Environment.DIRECTORY_DCIM + "/Camera/"
            )
            // 排序条件
            val order = MediaStore.MediaColumns._ID
            val cursor = contentResolver.query(
                imageExternalUri,
                projection,
                selection,
                selectionArgs,
                order
            )
            cursor?.apply {
                val idIndex = cursor.getColumnIndex(MediaStore.MediaColumns._ID)
                val dataIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA)
                val relativePathIndex = cursor.getColumnIndex(MediaStore.MediaColumns.RELATIVE_PATH)
                val displayNameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
                while (cursor.moveToNext()) {
                    val log = """
                    id = ${cursor.getString(idIndex)}
                    data = ${cursor.getString(dataIndex)}
                    relativePath = ${cursor.getString(relativePathIndex)}
                    displayNameIndex = ${cursor.getString(displayNameIndex)}
                """.trimIndent()
                    Log.v(TAG, log)
                }
                cursor.close()
            }
        } else {
            requestReadP()
        }
    }

    /**
     * 判断读取权限
     */
    private fun hasReadP(): Boolean {
        return ContextCompat.checkSelfPermission(
            this,
            Manifest.permission.READ_EXTERNAL_STORAGE
        ) == PackageManager.PERMISSION_GRANTED
    }

    private fun requestReadP() {
        ActivityCompat.requestPermissions(
            this,
            arrayOf(
                Manifest.permission.READ_EXTERNAL_STORAGE
            ),
            REQUEST_READP_CDOE
        )
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (requestCode == REQUEST_READP_CDOE) {
            if (grantResults.isNotEmpty()) {
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(this@MainActivity, "读取权限申请成功", Toast.LENGTH_SHORT).show()
                } else {
                    Toast.makeText(this@MainActivity, "读取权限申请失败,请稍后重试", Toast.LENGTH_SHORT).show()
                }
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }
}
(2)插入

需要注意:
1、如果插入的文件手动删除时,可能导致MediaStore不更新,再次插入报错:QLiteConstraintException: UNIQUE constraint failed: files._data
2、此时,可以通过改变每次插入的文件名称避免此问题;也可以在插入前先搜索是否有同样信息的文件,执行update或delete,防止出错。

    private fun insertPic() {
        val imageExternalUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI

        val values = ContentValues()
        values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/Camera/")
        values.put(
            MediaStore.MediaColumns.DISPLAY_NAME,
            "testPic_${System.currentTimeMillis()}.png"
        )
        values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
        val resultUri = contentResolver.insert(imageExternalUri, values)
        resultUri?.apply {
            val lightOpenBitmap = BitmapFactory.decodeResource(resources, R.drawable.light_open)
            val oPS = contentResolver.openOutputStream(resultUri)
            try {
                lightOpenBitmap.compress(Bitmap.CompressFormat.PNG, 100, oPS)
                Toast.makeText(this@MainActivity, "插入成功", Toast.LENGTH_SHORT).show()
            } catch (e: java.lang.Exception) {
                Toast.makeText(this@MainActivity, "插入失败,请稍后重试", Toast.LENGTH_SHORT).show()
            }
            oPS?.apply {
                try {
                    oPS.close()
                } catch (e: java.lang.Exception) {
                    Log.v(TAG, "error:$e")
                }
            }
        }
    }
(3)更新

需要注意:
1、需要通过查询拿到文件的原始Uri,如果直接通过MediaStore.Images.Media.EXTERNAL_CONTENT_URI进行更新的话,会报错:java.lang.IllegalArgumentException: Movement of content://media/external/images/media which isn't part of well-defined collection not allowed
2、应用只能修改自己插入的文件,其他文件或者应用卸载前插入的文件,应用不再有权限修改,否则会报:android.app.RecoverableSecurityException: com.example.storatedemo1 has no access to content://media/external/images/media/1000014102
3、Android 11手机,申请了MANAGE_EXTERNAL_STORAGE,可随意操作

     /**
     * 更新
     */
    private fun updatePic() {
        if (hasReadP()) {
            val imageExternalUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI

            val values = ContentValues()
            values.put(
                MediaStore.MediaColumns.RELATIVE_PATH,
                Environment.DIRECTORY_DCIM + "/Camera/"
            )
            values.put(
                MediaStore.MediaColumns.DISPLAY_NAME,
                "updatePic_${System.currentTimeMillis()}.png"
            )
            values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")

            // 查询条件
            val selection =
                "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DISPLAY_NAME} like ?"
            // 参数
            val selectionArgs = arrayOf(
                Environment.DIRECTORY_DCIM + "/Camera/",
                "testPic_%.png"
            )
            val cursor =
                contentResolver.query(imageExternalUri, null, selection, selectionArgs, null)
            cursor?.apply {
                if (cursor.moveToNext()) {
                    val idIndex = cursor.getColumnIndex(MediaStore.MediaColumns._ID)
                    val imageUri =
                        ContentUris.withAppendedId(imageExternalUri, cursor.getLong(idIndex))
                    val count = contentResolver.update(imageUri, values, null, null)
                    if (count > 0) {
                        Toast.makeText(this@MainActivity, "更新成功", Toast.LENGTH_SHORT).show()
                    } else {
                        Toast.makeText(this@MainActivity, "更新失败", Toast.LENGTH_SHORT).show()
                    }
                } else {
                    Toast.makeText(this@MainActivity, "没有找到要更新的图片", Toast.LENGTH_SHORT).show()
                }
                cursor.close()
            }
        } else {
            requestReadP()
        }
    }
(4)删除

需要注意:
1、应用只能删除自己插入的文件,其他文件或者应用卸载前插入的文件,应用不再有权限删除
2、Android 11手机,申请了MANAGE_EXTERNAL_STORAGE,可随意操作

    /**
     * 删除图片
     */
    private fun deletePic() {
        if (hasReadP()) {
            val imageExternalUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
            // 查询条件
            val selection =
                "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DISPLAY_NAME} like ?"
            // 参数
            val selectionArgs = arrayOf(
                Environment.DIRECTORY_DCIM + "/Camera/",
                "testPic_%.png"
            )
            val deleteCount = contentResolver.delete(imageExternalUri, selection, selectionArgs)
            Toast.makeText(this@MainActivity, "删除了${deleteCount}行", Toast.LENGTH_SHORT).show()
        } else {
            requestReadP()
        }
    }
3、通过SAF操作文件

App可以通过Action来启动系统选择器,让用户做相关的操作:
ACTION_OPEN_DOCUMENT:打开用户选择的文件
ACTION_CREATE_DOCUMENT:在用户选择的位置创建文件
ACTION_OPEN_DOCUMENT_TREE:访问某个目录

class MainActivity : AppCompatActivity() {
    private var mSAFLauncher: ActivityResultLauncher<Intent>? = null

    private fun openFileSAF() {
        // 创建一个intent,并进行跳转
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = "*/*"
        }
        mSAFLauncher?.launch(intent)
    }

    private fun initView() {
        mSAFLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
                if (it.resultCode == Activity.RESULT_OK) {
                    // 获取Uri,通过Uri获取文件信息
                    val uri = it.data?.data
                    uri?.apply {
                        val documentFile = DocumentFile.fromSingleUri(this@MainActivity, this)
                        documentFile?.apply {
                            val log = """
                            name = ${documentFile.name}
                            parentFile = ${documentFile.parentFile}
                            type = ${documentFile.type}
                        """.trimIndent()
                            Log.v(TAG, log)
                        }
                    }
                }
            }
    }
}

申请Uri的永久访问权限

val contentResolver = applicationContext.contentResolver
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, takeFlags)

其他信息可查看官方文档

三、附录

1、Media库常用字段表
字段 描述
_id 主键,自增
_data 绝对路径
_size 大小,单位byte
_display_name 文件名,如:aa.jpg
mime_type 文件类型,如:image/jpeg
title 文件名,无扩展名,如:aa
bucket_display_name 直接包含该文件的文件夹名称
duration 时长
2、常用mime_type值
名称 MIME type
png image/png
gif image/gif
jpeg jpg jpe image/jpeg
txt text conf def list log in text/plain
mp4 mp4v mpg4 video/mp4
mpga mp2 mp2a mp3 m2a m3 audio/mpeg
json application/json

相关文章

  • Android沙箱机制

    一说到沙箱,相信大家都有一个大概的认识:每个App会被分配一个uid,互相之间数据不能随意访问。虽然做上层开发有这...

  • 阅读《Android沙箱机制》

    原文地址https://mp.weixin.qq.com/s?__biz=MzUxODQ3MTk5Mg==&mid...

  • 喜闻乐见之Android简介

    本文主要是对Android系统做一个简介,包括其架构、启动流程、沙箱机制、APK、Darlvik以及ART。 架构...

  • android 内存介绍

    android 使用的沙箱机制,每个应用分配的内存大小是有限的,内存太低就会触发LMK-low memory ...

  • 沙箱机制

    沙箱(网络编程虚拟执行环境) 沙盘英文名sandbox(sandboxie),也叫沙箱,顾名思义可以看做是一种容器...

  • Java沙箱机制的实现——安全管理器、访问控制器

    一、Java沙箱机制 Java沙箱(sandbox)是Java安全模型的核心,那如何理解沙箱呢? 我们知道如果默认...

  • Android Binder机制扫盲

    由于Android系统保护机制(沙箱机制),两个进程是各自运行在自己的进程空间之中的,相互之间进行隔离并不能够直接...

  • 「Android 安全架构」之权限机制剖析(含运行时权限)

    前言 为什么有权限机制? 我们知道 Android 应用程序是沙箱隔离的,每个应用都有一个只有自己具有读写权限的专...

  • 沙箱安全机制

    保护程序安全 保护Java原生的JDK代码 Java安全模型的核心就是Java沙箱(sandbox)。什么是沙箱?...

  • JS沙箱机制

    一.思考 微前端应用加载 刚开始我加载A应用 window.a B应用 window.a 怎样可以俩个应用里的...

网友评论

      本文标题:Android 沙箱机制

      本文链接:https://www.haomeiwen.com/subject/mogyudtx.html