diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/activities/BaseSimpleActivity.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/activities/BaseSimpleActivity.kt index 45c6c367e..8fb648bcd 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/activities/BaseSimpleActivity.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/activities/BaseSimpleActivity.kt @@ -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) diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Activity.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Activity.kt index f311e247b..5e607d1bf 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Activity.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Activity.kt @@ -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 -> diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context-storage.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context-storage.kt index 908a166b8..a59115322 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context-storage.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context-storage.kt @@ -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 { 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) diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/String.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/String.kt index f1092621d..851055b52 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/String.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/String.kt @@ -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 { diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/Constants.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/Constants.kt index 5b91d57c4..6f251b311 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/Constants.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/Constants.kt @@ -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