Android11适配-实现清理其他应用缓存目录

本文介绍了在Android 11中如何通过SAF访问和管理Android/data目录,提供了一个工具类`CleanRDataUtil`用于获取应用缓存目录并实现清理功能。在Android 11中,由于分区存储的限制,无法直接访问特定目录,但通过DocumentsContract API可以实现间接访问。文章详细讲解了实现步骤,并提供了关键代码示例,帮助开发者解决在新系统版本下的适配问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在Android 11.0中开始强制执行分区存储,在10.0中可以使用

android:requestLegacyExternalStorage="true"

来拒绝分区存储,但在11.0中这种做法也不行了。那么该怎么办呢?正好这段时间我的手机更新到了Android11,先看我实现的效果~

在11.0中,不能访问 Android/data 以及 Android/obb 目录了,就连使用系统的的文件管理也不能访问了,会跳转一个新的应用来访问该目录如下图所示:

经我测试发现,Android 11 中访问除  Android/data 以及 Android/obb 以外的目录,不需要申请存储权限,即可以通过 File(path) 实现访问,如果有删除操作的,需要申请 所有文件管理权限,否则会删不掉,下面工具类在会给到。

查阅后我发现已经有大神通过SAF来实现在应用内访问该目录了,可以参考:Android11 无Root 访问data目录实现。今天闲逛看到官方文档中这样写到,好家伙,看样子这个bug已经被修复了,但现在实际测试还是可以访问到,也许没准哪天突然用不了了,趁还能用的时候抓紧时间吧~😝

 

回归正题,本文也是参考该方法来显示应用内访问其它应用cache目录,来实现清理缓存的功能。先上最主要的工具类,主要功能都在里面

/**
 *    author : fySpring
 *    date   : 2021/3/18 7:00 PM
 *    desc   : 针对Android R 的适配,实现访问 Android/data 目录
 */
class CleanRDataUtil {
    companion object {
        private const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY =
            "com.android.externalstorage.documents"
        private const val ANDROID_DOCUMENT_ID = "primary:Android"
        //如果你需要访问 obb 目录,把 data 改成 obb 即可
        private const val ANDROID_DATA_DOCUMENT_ID = "primary:Android/data"

        private val androidDataTreeUri = DocumentsContract.buildTreeDocumentUri(
            EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
            ANDROID_DATA_DOCUMENT_ID
        )
        
        // Android/data 目录 uri
        private val androidChildDataTreeUri = DocumentsContract.buildChildDocumentsUriUsingTree(
            androidDataTreeUri,
            ANDROID_DATA_DOCUMENT_ID
        )


        //获取 data目录下所有文件夹uri
        fun getAndroidDataUri(contentResolver: ContentResolver): MutableMap<String, Uri> {
            val packageMap: MutableMap<String, Uri> = mutableMapOf()
            val cursor = contentResolver.query(androidChildDataTreeUri,
                    arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME,
                            DocumentsContract.Document.COLUMN_DOCUMENT_ID,
                            DocumentsContract.Document.COLUMN_MIME_TYPE), null, null, null)
            cursor?.let {
                while (it.moveToNext()) {
                    val name = it.getString(it.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME))
                    val id = it.getString(it.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID))
                    val type = it.getString(it.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE))
                    if (type == DocumentsContract.Document.MIME_TYPE_DIR) {
                        packageMap[name] = DocumentsContract.buildChildDocumentsUriUsingTree(androidChildDataTreeUri, id)
                    }
                }
            }
            cursor?.close()
            return packageMap
        }

        //获取URI下 cache 目录,替换名称也可以获取其他目录
        fun getAndroidDataCacheUri(contentResolver: ContentResolver, uri: Uri): Uri? {
            var result: Uri? = null
            val cursor = contentResolver.query(uri,
                    arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE),
                    null, null, null)
            cursor?.let {
                while (it.moveToNext()) {
                    val name = it.getString(it.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME))
                    val type = it.getString(it.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE))
                    if (type == DocumentsContract.Document.MIME_TYPE_DIR && name.toLowerCase() == "cache") {
                        val id = it.getString(it.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID))
                        result = DocumentsContract.buildChildDocumentsUriUsingTree(uri, id)
                    }
                }
            }
            cursor?.close()
            return result
        }


        fun scanFile(contentResolver: ContentResolver, uri: Uri): MutableList<DocumentData> {
            val documentList = mutableListOf<DocumentData>()
            val cursor = contentResolver.query(
                uri,
                arrayOf(
                    DocumentsContract.Document.COLUMN_DISPLAY_NAME,
                    DocumentsContract.Document.COLUMN_MIME_TYPE,
                    DocumentsContract.Document.COLUMN_DOCUMENT_ID,
                    DocumentsContract.Document.COLUMN_SIZE,
                    DocumentsContract.Document.COLUMN_LAST_MODIFIED
                ),
                null, null, null
            )
            cursor?.let {
                while (it.moveToNext()) {
                    val name =
                        it.getString(it.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME))
                    val type =
                        it.getString(it.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE))
                    val id =
                        it.getString(it.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID))
                    val size = it.getLong(it.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE))
                    val date =
                        it.getLong(it.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED))
                    if (type == DocumentsContract.Document.MIME_TYPE_DIR) {
                        documentList.addAll(scanFile(contentResolver, DocumentsContract.buildChildDocumentsUriUsingTree(uri, id)))
                    } else {
                        documentList.add(DocumentData(id, size, name, uri, date, type))
                    }
                }
            }

            cursor?.close()
            return documentList
        }

        fun scanFileForSize(contentResolver: ContentResolver, uri: Uri): Long {
            var totalSize = 0L
            val cursor = contentResolver.query(
                uri,
                arrayOf(
                    DocumentsContract.Document.COLUMN_DISPLAY_NAME,
                    DocumentsContract.Document.COLUMN_MIME_TYPE,
                    DocumentsContract.Document.COLUMN_DOCUMENT_ID,
                    DocumentsContract.Document.COLUMN_SIZE
                ),
                null, null, null
            )
            cursor?.let {
                while (it.moveToNext()) {
                    val name =
                        it.getString(it.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME))
                    val type =
                        it.getString(it.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE))
                    val id =
                        it.getString(it.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID))
                    val size = it.getLong(it.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE))
                    if (type == DocumentsContract.Document.MIME_TYPE_DIR) {
                        totalSize += scanFileForSize(
                            contentResolver,
                            DocumentsContract.buildChildDocumentsUriUsingTree(uri, id)
                        )
                    } else {
                        totalSize += size
                    }
                }
            }

            cursor?.close()
            return totalSize
        }

        /**
         * 删除文件
         */
        fun deleteCurDocument(context: Context, uri: Uri) {
            try {
                DocumentsContract.deleteDocument(context.contentResolver, uri)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

        /**
         * 申请所有文件管理权限
         */
        fun requestForManageAllFilePermission(context: Activity, code: Int) {
            val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
            intent.data = Uri.parse("package:${context.packageName}")
            context.startActivityForResult(intent, code)
        }

        /**
         * 判断是否获取MANAGE_EXTERNAL_STORAGE权限
         */
        fun isHaveAllManagePermission() : Boolean {
            return Environment.isExternalStorageManager()
        }

        /**
         * 直接获取 data 权限
         */
        @RequiresApi(Build.VERSION_CODES.Q)
        fun startForDataPermission(activity: Activity, code: Int) {
            Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
                flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
                        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
                        Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
                putExtra(
                    DocumentsContract.EXTRA_INITIAL_URI,
                    DocumentFile.fromTreeUri(activity, androidDataTreeUri)?.uri
                )
            }.also {
                activity.startActivityForResult(it, code)
            }
        }


        /**
         * 判断是否已经获取了 data 权限
         */
        fun isDataGrant(context: Context): Boolean {
            for (persistedUriPermission in context.contentResolver.persistedUriPermissions) {
                if ((persistedUriPermission.uri == androidDataTreeUri) &&
                    persistedUriPermission.isWritePermission &&
                    persistedUriPermission.isReadPermission
                ) {
                    return true
                }
            }
            return false
        }
    }
}

使用代码如下,只展示关键代码。具体流程是,根据 /Android/data 目录,获取其下所有的目录 uri ,遍历根据名称(也就是包名)获取icon及名称,由于不能直接拿到整个目录的大小,需要迭代 /Android/data/包名/cache ,获取所有文件大小。

这里需要注意3点:

  1. DocumentFile 删除操作效率太慢,大概需要花费 80 毫秒 ,建议直接删除 cache目录的 uri,效果快准狠!!!
  2. 一定要记得 try cache,该方法很容易抛异常
  3. targetSdkVersion 记得设为30
data class FileArray(
        val fileList: MutableList<File> = mutableListOf(),
        val storageDataList: MutableList<StorageDataBean> = mutableListOf(),
        var size: Long = 0L
)


class StorageDataBean {
    var name: String? = null
    var isFolder = true
    var modifiedTime: Long? = null
    var length: Long = 0
    var uri: Uri? = null
}


var listMap = mutableMapOf<String, FileArray>()
private var allSize = 0L
private val asyncList = mutableListOf<Deferred<Any>>()

 /**
     * Android 11 扫描data目录
     */
    private suspend fun startScanDataAboveR() {
        allSize = 0L
        listMap.clear()
        withContext(Dispatchers.IO) {
            val packageNameList = CleanRDataUtil.getAndroidDataUri(application.contentResolver)
            for ((name, uri) in packageNameList) {
                asyncList.add(async {
                    if (!name.startsWith(".")) {

                        val data = AppCleanData(name, getIcon(name), getName(name), "", 0L)
                        delay(50L)
                        addApp.postValue(data)
                        appList.add(data)

                        val cacheUri =
                            CleanRDataUtil.getAndroidDataCacheUri(application.contentResolver, uri)
                        if (cacheUri != null) {
                            val size =
                                CleanRDataUtil.scanFileForSize(application.contentResolver, cacheUri)

                            val item = StorageDataBean()
                            item.name = name
                            item.uri = cacheUri
                            item.isFolder = false
                            item.length = size

                            if (listMap[name] == null) {
                                listMap[name] = FileArray()
                            }
                            listMap[name]?.let {
                                with(it) {
                                    this.size += size
                                    this.storageDataList.add(item)
                                }
                            }

                            allSize += size
                            allSizeLiveData.postValue(allSize.byteToFitSizeForStorageArray())
                        }
                    }
                })
            }
            asyncList.awaitAll()
            withContext(Dispatchers.Main) {
                Toast.makeText(application, "扫描缓存结束", Toast.LENGTH_SHORT).show()
            }
        }
    }

主要就是以上这些了,希望能帮到你~

如有问题,欢迎一起探讨!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

斯普润丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值