在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点:
- DocumentFile 删除操作效率太慢,大概需要花费 80 毫秒 ,建议直接删除 cache目录的 uri,效果快准狠!!!
- 一定要记得 try cache,该方法很容易抛异常
-
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()
}
}
}
主要就是以上这些了,希望能帮到你~
如有问题,欢迎一起探讨!!