refactor delete for SDK 30+ to use SAF

This commit is contained in:
darthpaul 2022-02-12 13:19:01 +00:00
parent bbd4b9c241
commit 023193e894
5 changed files with 178 additions and 1 deletions

View file

@ -223,7 +223,26 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
val sdOtgPattern = Pattern.compile(SD_OTG_SHORT)
if (requestCode == OPEN_DOCUMENT_TREE_FOR_ANDROID_DATA_OR_OBB) {
if (requestCode == OPEN_DOCUMENT_TREE_SINGLE_FILE) {
if (resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
val treeUri = resultData.data
val checkedUri = createFirstParentTreeUri(checkedDocumentPath)
if (treeUri != checkedUri) {
toast("Please select the directory: ${checkedDocumentPath.getFirstParentPath(this)}")
return
}
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
applicationContext.contentResolver.takePersistableUriPermission(treeUri, takeFlags)
funAfterSAFPermission?.invoke(true)
funAfterSAFPermission = null
} else {
funAfterSAFPermission?.invoke(false)
}
} else if (requestCode == OPEN_DOCUMENT_TREE_FOR_ANDROID_DATA_OR_OBB) {
if (resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
if (isProperAndroidRoot(checkedDocumentPath, resultData.data!!)) {
if (resultData.dataString == baseConfig.OTGTreeUri || resultData.dataString == baseConfig.sdTreeUri) {
@ -404,6 +423,19 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
}
}
fun handleSAFDeleteSdk30Dialog(path: String, callback: (success: Boolean) -> Unit): Boolean {
return if (!packageName.startsWith("com.simplemobiletools")) {
callback(true)
false
} else if (isShowingSAFDialogForDeleteSdk30(path)) {
funAfterSAFPermission = callback
true
} else {
callback(true)
false
}
}
fun handleAndroidSAFDialog(path: String, callback: (success: Boolean) -> Unit): Boolean {
return if (!packageName.startsWith("com.simplemobiletools")) {
callback(true)

View file

@ -1,5 +1,6 @@
package com.simplemobiletools.commons.extensions
import android.annotation.SuppressLint
import android.app.Activity
import android.app.TimePickerDialog
import android.content.*
@ -147,6 +148,42 @@ fun BaseSimpleActivity.isShowingSAFDialog(path: String): Boolean {
}
}
@SuppressLint("InlinedApi")
fun BaseSimpleActivity.isShowingSAFDialogForDeleteSdk30(path: String): Boolean {
val pathUri = createFirstParentDocumentUri(path)
return if (!hasProperStoredFirstParentUri(pathUri.toString())) {
runOnUiThread {
if (!isDestroyed && !isFinishing) {
WritePermissionDialog(this, false) {
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pathUri)
try {
startActivityForResult(this, OPEN_DOCUMENT_TREE_SINGLE_FILE)
checkedDocumentPath = path
return@apply
} catch (e: Exception) {
e.printStackTrace()
type = "*/*"
}
try {
startActivityForResult(this, OPEN_DOCUMENT_TREE_SINGLE_FILE)
checkedDocumentPath = path
} catch (e: Exception) {
e.printStackTrace()
toast(R.string.unknown_error_occurred)
}
}
}
}
}
true
} else {
false
}
}
fun BaseSimpleActivity.isShowingAndroidSAFDialog(path: String): Boolean {
return if (isRestrictedSAFOnlyRoot(path) && (getAndroidTreeUri(path).isEmpty() || !hasProperStoredAndroidTreeUri(path))) {
runOnUiThread {
@ -679,6 +716,14 @@ fun BaseSimpleActivity.deleteFileBg(fileDirItem: FileDirItem, allowDeleteFolder:
trySAFFileDelete(fileDirItem, allowDeleteFolder, callback)
}
}
} else if (isRPlus() && isAccessibleWithSAFSdk30(path)) {
handleSAFDeleteSdk30Dialog(path) {
if (it) {
deleteDocumentWithSAFSdk30(fileDirItem, allowDeleteFolder, callback)
} else {
callback?.invoke(false)
}
}
} else if (isRPlus()) {
val fileUris = getFileUrisFromFileDirItems(arrayListOf(fileDirItem)).second
deleteSDK30Uris(fileUris) { success ->

View file

@ -155,12 +155,24 @@ fun Context.isPathOnInternalStorage(path: String) = internalStoragePath.isNotEmp
private const val ANDROID_DATA_DIR = "/Android/data/"
private const val ANDROID_OBB_DIR = "/Android/obb/"
private const val DOWNLOAD_DIR = "Download"
val DIRS_ACCESSIBLE_ONLY_WITH_SAF = listOf(ANDROID_DATA_DIR, ANDROID_OBB_DIR)
private val DIRS_INACCESSIBLE_WITH_SAF_SDK_30 = listOf(DOWNLOAD_DIR)
fun Context.getSAFOnlyDirs(): List<String> {
return DIRS_ACCESSIBLE_ONLY_WITH_SAF.map { "$internalStoragePath$it" }
}
fun Context.isAccessibleWithSAFSdk30(path: String): Boolean {
val firstParentPath = path.getFirstParentPath(this)
val firstParentDir = path.getFirstParentDirName(this)
return firstParentPath != path.getBasePath(this) &&
DIRS_INACCESSIBLE_WITH_SAF_SDK_30.all {
firstParentDir != it
}
}
fun Context.isSAFOnlyRoot(path: String): Boolean {
return getSAFOnlyDirs().any { "${path.trimEnd('/')}/".startsWith(it) }
}
@ -187,6 +199,11 @@ fun Context.hasProperStoredTreeUri(isOTG: Boolean): Boolean {
return hasProperUri
}
fun Context.hasProperStoredFirstParentUri(path: String): Boolean {
val firstParentUri = createFirstParentTreeUri(path)
return contentResolver.persistedUriPermissions.any { it.uri.toString() == firstParentUri.toString() }
}
fun Context.hasProperStoredAndroidTreeUri(path: String): Boolean {
val uri = getAndroidTreeUri(path)
val hasProperUri = contentResolver.persistedUriPermissions.any { it.uri.toString() == uri }
@ -237,6 +254,55 @@ fun Context.createDocumentUri(fullPath: String): Uri {
return DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
}
fun Context.createFirstParentDocumentUri(fullPath: String): Uri {
val storageId = if (fullPath.startsWith('/')) {
when {
fullPath.startsWith(internalStoragePath) -> "primary"
else -> fullPath.substringAfter("/storage/", "").substringBefore('/')
}
} else {
fullPath.substringBefore(':', "").substringAfterLast('/')
}
val rootParentDirName = fullPath.getFirstParentDirName(this)
val treeUri = DocumentsContract.buildTreeDocumentUri(EXTERNAL_STORAGE_PROVIDER_AUTHORITY, "$storageId:")
val documentId = "${storageId}:$rootParentDirName"
return DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
}
fun Context.createFirstParentTreeUri(fullPath: String): Uri {
val storageId = if (fullPath.startsWith('/')) {
when {
fullPath.startsWith(internalStoragePath) -> "primary"
else -> fullPath.substringAfter("/storage/", "").substringBefore('/')
}
} else {
fullPath.substringBefore(':', "").substringAfterLast('/')
}
val rootParentDirName = fullPath.getFirstParentDirName(this)
val firstParentId = "$storageId:$rootParentDirName"
val treeUri = DocumentsContract.buildTreeDocumentUri(EXTERNAL_STORAGE_PROVIDER_AUTHORITY, firstParentId)
return treeUri
}
fun Context.createDocumentUriUsingFirstParentTreeUri(fullPath: String): Uri {
val storageId = if (fullPath.startsWith('/')) {
when {
fullPath.startsWith(internalStoragePath) -> "primary"
else -> fullPath.substringAfter("/storage/", "").substringBefore('/')
}
} else {
fullPath.substringBefore(':', "").substringAfterLast('/')
}
val relativePath = when {
fullPath.startsWith(internalStoragePath) -> fullPath.substring(internalStoragePath.length).trim('/')
else -> fullPath.substringAfter(storageId).trim('/')
}
val treeUri = createFirstParentTreeUri(fullPath)
val documentId = "${storageId}:$relativePath"
return DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
}
fun Context.createAndroidDataOrObbUri(fullPath: String): Uri {
val path = if (isAndroidDataDir(fullPath)) {
fullPath.getBasePath(this).trimEnd('/').plus(ANDROID_DATA_DIR)
@ -753,6 +819,26 @@ fun Context.renameAndroidSAFDocument(oldPath: String, newPath: String): Boolean
}
}
fun Context.deleteDocumentWithSAFSdk30(fileDirItem: FileDirItem, allowDeleteFolder: Boolean, callback: ((wasSuccess: Boolean) -> Unit)?) {
try {
var fileDeleted = false
if (fileDirItem.isDirectory.not() || allowDeleteFolder) {
val firstParentTreeUri = createFirstParentTreeUri(fileDirItem.path)
val fileUri = createDocumentUriUsingFirstParentTreeUri(fileDirItem.path)
fileDeleted = DocumentsContract.deleteDocument(contentResolver, fileUri)
}
if (fileDeleted) {
deleteFromMediaStore(fileDirItem.path)
callback?.invoke(true)
}
} catch (e: Exception) {
callback?.invoke(false)
showErrorToast(e)
}
}
fun Context.getAndroidSAFFileSize(path: String): Long {
val treeUri = getAndroidTreeUri(path).toUri()
val documentId = getAndroidSAFDocumentId(path)

View file

@ -37,6 +37,19 @@ fun String.getBasePath(context: Context): String {
}
}
fun String.getFirstParentDirName(context: Context): String {
val basePath = getBasePath(context)
val pathWithoutBasePath = substring(basePath.length + 1)
return pathWithoutBasePath.substringBefore("/")
}
fun String.getFirstParentPath(context: Context): String {
val basePath = getBasePath(context)
val pathWithoutBasePath = substring(basePath.length + 1)
val firstParentPath = pathWithoutBasePath.substringBefore("/")
return "$basePath/$firstParentPath"
}
fun String.isAValidFilename(): Boolean {
val ILLEGAL_CHARACTERS = charArrayOf('/', '\n', '\r', '\t', '\u0000', '`', '?', '*', '\\', '<', '>', '|', '\"', ':')
ILLEGAL_CHARACTERS.forEach {

View file

@ -198,6 +198,7 @@ const val LICENSE_APNG = 268435456
const val OPEN_DOCUMENT_TREE_FOR_ANDROID_DATA_OR_OBB = 1000
const val OPEN_DOCUMENT_TREE_OTG = 1001
const val OPEN_DOCUMENT_TREE_SD = 1002
const val OPEN_DOCUMENT_TREE_SINGLE_FILE = 1003
const val REQUEST_SET_AS = 1002
const val REQUEST_EDIT_IMAGE = 1003
const val SELECT_EXPORT_SETTINGS_FILE_INTENT = 1004