refactor delete for SDK 30+ to use SAF
This commit is contained in:
parent
bbd4b9c241
commit
023193e894
5 changed files with 178 additions and 1 deletions
|
@ -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)
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue