0

I've a delete files functionality in my app (can be any file on the device). I'm enlisting my attempts at deleting files and then I'll highlight the issues that I'm facing. Device I tested on was running Android 10 (API 29), but I need to delete SD card files from API 21 and above.

Approach 1: Delete using ContentResolver

This approach works fine with phone storage and SD card storage, except that I'm unable to delete files from SD card Downloads folder, while the same works for phone storage Downloads folder.

applicationContext.contentResolver.delete(file.uri, null, null)

I did some research and came across an SO post, where the recommended approach to delete files on SD card is via DocumentsProvider or using DocumentFile.

Approach 2: Delete by using DocumentFile

I followed this answer and traversed the document tree uri, got the file and called delete(). To my confusion, delete() returns true but doesn't delete the file. This approach didn't even delete files from other folders on SD card, while approach 1 was able to.

The permission I have on the SD card document tree were taken as follows.

fun openSafTreePicker(){
    val intent = Intent(ACTION_OPEN_DOCUMENT_TREE).apply {
        flags = FLAG_GRANT_PERSISTABLE_URI_PERMISSION 
                    or FLAG_GRANT_READ_URI_PERMISSION 
                    or FLAG_GRANT_WRITE_URI_PERMISSION
    }
    startActivityForResult(intent, requestCode)
}

// On activity result taking the persistable permission
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    val uri = data.data!!
    val flags = (data.flags) and (FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION)        
    applicationContext.contentResolver.takePersistableUriPermission(uri, flags)
    AppPrefs.sdCardUri = uri
    Log.d(TAG, AppPrefs.sdCardUri)
    // AppPrefs.sdCardUri = content://com.android.externalstorage.documents/tree/EAB3-F2BF%3A
    // Delete the selected files
    val selection = SelectionManager.getEntireSelection()
    selection.forEach { 
        when {
          it.isSdFile -> deleteSdCardFile(it.path)
          it.isPhoneFile -> applicationContext.contentResolver.delete(it.uri, null, null) 
        }
    }
}

I tried to delete the file using the static methods available in DocumentsContract. I was unable to delete any files from SD card, while the deleteDocument() returned true.

 private fun deleteSdCardFile(filePath: String) {
     val segments = filePath.split("/")
      // Skip first 3 segments /storage/ABC-1234/0/ corresponding to storage volume and user.
     val docPath = StringBuilder()
     for (index in 3 until segments.size) {
         if(index < segments.size-1) docPath.append(segments[index]).append("/")
         else docPath.append(segments[index])
     }
     val treeUri = Uri.parse(AppPrefs.sdCardUri)
     val docUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, docPath.toString())
     DocumentsContract.deleteDocument(applicationContext.contentResolver, docUri)
 }

Questions:

  1. What is the correct way of deleting files from SD card? MediaStore seems to work fine except for the SD card Downloads folder.
  2. What is the mystery behind files not deleting from SD card Downloads folder.
  3. Why does delete() and deleteDocument() from DocumentFile and DocumentsContract return true even though they haven't deleted the file?
Siddharth Kamaria
  • 2,448
  • 2
  • 17
  • 37
  • `flags = FLAG_GRANT_PERSISTABLE_URI_PERMISSION or FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION` Remove those flags. They are useless. YOU cannot grant anything. You should be glad that you are granted access. You can inspect those flags in onActivityResult to see if read and or write is granted to you. – blackapps Jan 09 '21 at 09:49
  • `val treeUri = Uri.parse(AppPrefs.sdCardUri)` Please show more complete cide. We want to see exactly the value of sdCardUri to begin with. Adapt your post please. – blackapps Jan 09 '21 at 09:54
  • To reassure you: it is quite possible to remove files from a removable micro sd card using SAF. – blackapps Jan 09 '21 at 09:55
  • `// docPath is the relative file...` Please post complete code . – blackapps Jan 09 '21 at 09:56
  • I suggest to let the user choose the folder where the file resides. In this way the code in onActivityResult to delete the file is as short as possible. Please post that code. – blackapps Jan 09 '21 at 10:01
  • @blackapps I've added more code. I'm looking for a solution by which the user grants the permission to directory once, and then I can delete the files from that directory tree without prompting for permission again. – Siddharth Kamaria Jan 09 '21 at 10:31
  • I do not see the code i asked for in my last comment. Further it is quite possible what you ask in your last comment. – blackapps Jan 09 '21 at 10:42
  • In this way the code in onActivityResult to delete the file is as short as possible. Please post that code. -- Sorry I didn't follow what code you're looking for? I can post that here once I know what you're looking for. – Siddharth Kamaria Jan 09 '21 at 10:44
  • @blackapps Afaik, you wanted to see the code where the delete SAF logic is called from onActivityResult. I've added that part. It is basically a for loop iterating through all selected files one by one and trying to delete it. – Siddharth Kamaria Jan 09 '21 at 10:54
  • I have no idea about your selection manager. Please do as i asked. Keep it simple. No for loops too. You only need one file name for the file you want to delete in the directory the used choosed. Do not call other functions in onActivityResult too. – blackapps Jan 09 '21 at 11:21
  • @blackapps What I'm struggling with is that 1.) I have a persistent permission on directory, so onActivityResult is only going to get called once. 2.) I've a RecyclerView where I show all files / media, from where the user can delete. Asking user to choose that particular file in SAF, say 10-15 files every time they want to delete 10-15 files is simply too much. Plus, selecting the same files they have chosen in app again in SAF is not what I want. – Siddharth Kamaria Jan 09 '21 at 11:25
  • That all does not matter. I can help you to delete a file. So just for solving your problem you will post the code i asked for. Once you can delete one file you can try to implement it in your app or whatever. Grab the help you can get! – blackapps Jan 09 '21 at 11:43
  • @blackapps Sounds cool, show me how to delete one file and then I can take it from there. For now, you can safely assume there is no for loop in the onActivityResult and I get a treeUri since I use ACTION_OPEN_DOCUMENT_TREE. Let me know what needs to be done from there. – Siddharth Kamaria Jan 09 '21 at 11:46
  • 1
    Sorry.. but you should post a new onActivityResult where we see that you try to delete a file in the choosen folder. Telll which folder the user chooses to begin with. Make a DocumentFile variable for the choosen folder. – blackapps Jan 09 '21 at 11:49
  • @blackapps I just now got it to work via DocumentFile, I get what you're trying to say now. I started with `DocumentFile.fromTreeUri()` and then navigated to the correct file by making use of `document.findFile()` recursively. Thank you! – Siddharth Kamaria Jan 09 '21 at 12:06
  • Yes that is how it should be done. DocumentFile is notoriously slow. So if speed matters try DocumentsContract. – blackapps Jan 09 '21 at 12:07
  • I've a question - how do you get documents provider uri for use with DocumentsContract.deleteDocument(). What I have is a tree URI and a relative path to file within that tree. – Siddharth Kamaria Jan 09 '21 at 12:10
  • At the moment i cannot help you as i have no code at hand while laying under a palm tree ;-) – blackapps Jan 09 '21 at 12:12
  • Sure whenever you get some example, please do post it here. I'll accept it as an answer. I didn't find a single example for DocumentsContract, and the one in official docs is a one line example - and I'm pretty sure I'm using it wrong in the code above. – Siddharth Kamaria Jan 09 '21 at 12:14
  • applicationContext.contentResolver.delete(file.uri, null, null) not working for me. Test on android 8.1 . Uri of sdcard is retrieved from mediastore – Đốc.tc Oct 12 '21 at 03:38
  • Siddharth Kamaria maybe it was a misunderstanding when i saw you write this i went looking for the method and found it wrong so i objected to it. You write that: "Approach 1: Delete using ContentResolver This approach works fine with phone storage and SD card storage" Because the above method is completely powerless against deleting any files on the sdcard – Đốc.tc Oct 13 '21 at 07:14
  • @Mr.Lemon Yeah that didn't work at all for SD card. I tried only for the Downloads folder before posting the question. Apologies for the same. – Siddharth Kamaria Oct 13 '21 at 08:43
  • @Siddharth Kamaria i'm doing research on how different android versions are doing. Hope to have an answer soon to answer your question – Đốc.tc Oct 13 '21 at 10:04
  • @Mr.Lemon That is a cool study. I managed to solve it with `DocumentFile`. You can see there in one of my comments: "I just now got it to work via DocumentFile, I get what you're trying to say now. I started with `DocumentFile.fromTreeUri()` and then navigated to the correct file by making use of `document.findFile()` recursively. " – Siddharth Kamaria Oct 13 '21 at 13:34
  • 1
    @Siddharth Kamaria I have checked and found that the android versions work differently. In this case we need to separate into 3 types: 1. Android API <= 4.2.2 + Android Q, 2. API > 30. 3. Android >4.4.2 & < Android Q – Đốc.tc Oct 15 '21 at 02:16
  • Yeah that is a correct observation. Since I support API 21 to 30, I had to make two spilts at Android Q. – Siddharth Kamaria Oct 19 '21 at 06:35
  • @Siddharth Kamaria I've done quite a bit of research on how in-memory operations work in the ways Android recommends, so if you have any doubts, don't hesitate to ask me. – Đốc.tc Oct 19 '21 at 08:39
  • So many approaches: File, MediaStore, DocumentsContract/DocumentsFile(docId/docUri) – thecr0w Jun 28 '22 at 01:15

2 Answers2

0

You can't delete any files from the SD card unless you have read & write URIs to this storage. Suppose that the user has granted the URIs (read), then you can delete all files in SD card recursively:

// AAAA-BBBB is the SD card's ID
val files = DocumentFileCompat.fromSimplePath(context, storageId = "AAAA-BBBB", basePath = "Music")
// the path will be /storage/AAAA-BBBB/Music
val success = files?.deleteRecursively(context)

DocumentFileCompat is available to download here.

Anggrayudi H
  • 14,977
  • 11
  • 54
  • 87
0

I think that's quite a lot of souce code for me to mention in this answer. Accessing the sdcard in addition to asking the user to indicate the access path, there is also a small shortcut from android 7 - android 9.

Here is my entire process to try to delete 1 file in sdcard written in kotlin language for your reference :

const val SDCARD_URI = "SDCARD_URI"

class ClearSdcardFileApi {

companion object {
    fun getSdcardPath(mContext: Context): String? {
        try {
            val mStorageManager =
                mContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager
            val storageVolumeClazz = Class.forName("android.os.storage.StorageVolume")
            val getVolumeList = mStorageManager.javaClass.getMethod("getVolumeList")
            val getPath = storageVolumeClazz.getMethod("getPath")
            val isRemovable = storageVolumeClazz.getMethod("isRemovable")
            val result = getVolumeList.invoke(mStorageManager) ?: return null
            val length = Array.getLength(result)
            for (i in 0 until length) {
                val storageVolumeElement = Array.get(result, i)
                val paths = getPath.invoke(storageVolumeElement) as String
                val removable = isRemovable.invoke(storageVolumeElement) as Boolean
                if (removable) {
                    return paths
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
            return null
        }
        return null
    }

    fun isSdcardFilePath(context: Context, path: String): Boolean {
        val pathSdcard = getPathSdcard(context) ?: return false
        return path.startsWith(pathSdcard)
    }

    suspend fun clearFileSdcard(
        activity: AppCompatActivity,
        deletePaths: List<String>
    ): List<Boolean> {
        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
            activity.openSafTreePicker(deletePaths)
        } else {
            activity.takeCardUriPermission(deletePaths)
        }
    }

    private suspend fun AppCompatActivity.openSafTreePicker(
        deletePaths: List<String>
    ) = suspendCoroutine<List<Boolean>> { continuation ->
        val activityResultLauncher = activityResultRegistry.register(
            System.currentTimeMillis().toString(),
            ActivityResultContracts.StartIntentSenderForResult()
        ) { result ->
            var deletedFiles = emptyList<Boolean>()
            try {
                deletedFiles = createPathAndDelete(this, result, deletePaths)
            } catch (e: Exception) {
                deletedFiles = emptyList()
            } finally {
                continuation.resume(deletedFiles)
            }
        }

        requestPermissionSdcardPermission(
            this,
            activityResultLauncher,
            deletePaths,
            continuation
        )

    }

    private suspend fun AppCompatActivity.takeCardUriPermission(
        deletePaths: List<String>
    ) =
        suspendCoroutine<List<Boolean>> { continuation ->
            val activityResultLauncher = activityResultRegistry.register(
                System.currentTimeMillis().toString(),
                ActivityResultContracts.StartIntentSenderForResult()
            ) { result ->
                var deletedFiles = emptyList<Boolean>()
                try {
                    deletedFiles = createPathAndDelete(this, result, deletePaths)
                } catch (e: Exception) {
                } finally {
                    continuation.resume(deletedFiles)
                }
            }
            requestPermissionSdcardPermission(
                this,
                activityResultLauncher,
                deletePaths,
                continuation
            )
        }

    private fun findAndDeleteDocument(
        activity: AppCompatActivity,
        treeUri: Uri,
        pathFileDelete: String
    ): Boolean {
        activity.contentResolver.takePersistableUriPermission(
            treeUri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )
        val pickedDir = DocumentFile.fromTreeUri(activity, treeUri)
        return if (pickedDir != null) {
            saveTreeUriToShare(activity, treeUri)
            if (pickedDir.canWrite()) {
                val listDirectoryPath = convertPathDelete(activity, pathFileDelete)
                val documentFileDelete = findDocumentFile(activity, listDirectoryPath, pickedDir)
                if (pickedDir.uri != documentFileDelete.uri) {
                    documentFileDelete.delete()
                } else {
                    false
                }
            } else {
                false
            }
        } else {
            false
        }
    }

    private fun saveTreeUriToShare(context: Context, treeUri: Uri) {
        context.getSharedPreferences(context.applicationContext.packageName, MODE_PRIVATE)
            .edit().putString(
                SDCARD_URI, treeUri.toString()
            ).apply()
    }

    private fun getTreeUriFromShare(context: Context): String? {
        return context.getSharedPreferences(
            context.applicationContext.packageName,
            MODE_PRIVATE
        ).getString(SDCARD_URI, null)
    }

    private fun hasUriSdcard(context: Context): Boolean {
        val treeUriFromShare = getTreeUriFromShare(context) ?: return false
        val pickedDir =
            DocumentFile.fromTreeUri(context, Uri.parse(treeUriFromShare)) ?: return false
        return pickedDir.canWrite()
    }

    private fun requestPermissionSdcardPermission(
        activity: AppCompatActivity,
        activityResultLauncher: ActivityResultLauncher<IntentSenderRequest>,
        deletePaths: List<String>,
        continuation: Continuation<List<Boolean>>
    ) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            val pathSdcard = getPathSdcard(activity)
            if (pathSdcard != null) {
                if (hasUriSdcard(activity)) {
                    val treeUri = Uri.parse(getTreeUriFromShare(activity))
                    val deleteFiles = ArrayList<Boolean>()
                    deletePaths.forEach {
                        deleteFiles.add(
                            findAndDeleteDocument(
                                activity,
                                treeUri,
                                it
                            )
                        )
                    }
                    continuation.resume(deleteFiles)
                } else {
                    val sdCard = File(pathSdcard)
                    val storageManager =
                        activity.getSystemService(Context.STORAGE_SERVICE) as StorageManager?
                            ?: return
                    val volumeSdcard =
                        storageManager.getStorageVolume(sdCard) ?: return
                    val intent = volumeSdcard.createAccessIntent(null)

                    val pendingIntent =
                        PendingIntent.getActivity(
                            activity,
                            4010,
                            intent,
                            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
                        )
                    val intentSenderRequest =
                        IntentSenderRequest.Builder(pendingIntent.intentSender).build()
                    activityResultLauncher.launch(intentSenderRequest)
                }
            } else {
                continuation.resume(emptyList())
            }
        } else {
            if (hasUriSdcard(activity)) {
                val treeUri = Uri.parse(getTreeUriFromShare(activity))
                val deleteFiles = ArrayList<Boolean>()
                deletePaths.forEach {
                    deleteFiles.add(
                        findAndDeleteDocument(
                            activity,
                            treeUri,
                            it
                        )
                    )
                }

                continuation.resume(deleteFiles)
            } else {
                val intent = Intent(ACTION_OPEN_DOCUMENT_TREE).apply {
                    flags =
                        FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
                                FLAG_GRANT_READ_URI_PERMISSION or
                                FLAG_GRANT_WRITE_URI_PERMISSION
                }

                val pendingIntent =
                    PendingIntent.getActivity(
                        activity,
                        4011,
                        intent,
                        PendingIntent.FLAG_UPDATE_CURRENT
                    )
                val intentSenderRequest =
                    IntentSenderRequest.Builder(pendingIntent.intentSender).build()
                activityResultLauncher.launch(intentSenderRequest)

                MainScope().launch {
                    Toast.makeText(
                        activity,
                        activity.resources.getString(R.string.please_choosing_disk_which_contains_selected_files),
                        Toast.LENGTH_LONG
                    ).show()
                }
            }
        }
    }

    private fun findDocumentFile(
        context: Context,
        subDeletedPath: List<String>,
        pickedDir: DocumentFile
    ): DocumentFile {
        var pickedDocument = pickedDir
        val firstItem = subDeletedPath.firstOrNull() ?: return pickedDocument
        pickedDir.listFiles().forEach { document ->
            if (document.name == firstItem) {

                pickedDocument = document

                val filter = subDeletedPath.filter { it != firstItem }

                if (filter.isEmpty()) return@forEach

                pickedDocument = findDocumentFile(
                    context,
                    subDeletedPath.filter { it != firstItem },
                    pickedDocument
                )
            }
        }
        return pickedDocument
    }

    private fun convertPathDelete(
        context: Context,
        pathFileDelete: String
    ): List<String> {

        val pathSdcard = getPathSdcard(context) ?: return emptyList()
        val path = pathFileDelete.replace(pathSdcard, "")
        return path.split(File.separator).filter { it.isNotEmpty() && it.isNotBlank() }.toList()
    }

    private fun getPathSdcard(context: Context): String? {
        try {
            val mStorageManager =
                context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
            val storageVolumeClazz = Class.forName("android.os.storage.StorageVolume")
            val getVolumeList = mStorageManager.javaClass.getMethod("getVolumeList")
            val getPath = storageVolumeClazz.getMethod("getPath")
            val isRemovable = storageVolumeClazz.getMethod("isRemovable")
            val result = getVolumeList.invoke(mStorageManager) ?: return null
            val length = Array.getLength(result)
            for (i in 0 until length) {
                val storageVolumeElement = Array.get(result, i)
                val paths = getPath.invoke(storageVolumeElement) as String
                val removable = isRemovable.invoke(storageVolumeElement) as Boolean
                if (removable) {
                    return paths
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
            return null
        }
        return null
    }

    private fun createPathAndDelete(
        activity: AppCompatActivity,
        result: ActivityResult,
        deletePaths: List<String>
    ): List<Boolean> {
        if (result.resultCode == Activity.RESULT_OK) {
            val intent = result.data ?: return emptyList()
            val treeUri = intent.data ?: return emptyList()
            activity.contentResolver.takePersistableUriPermission(
                treeUri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            )

            val pathSdcard = getPathSdcard(activity)
            if (pathSdcard != null) {
                val nameSdcard: String =
                    pathSdcard.substring(pathSdcard.lastIndexOf(File.separator) + 1)
                if (treeUri.toString().contains(nameSdcard)) {
                    saveTreeUriToShare(activity, treeUri)
                }
            }
            val deleteFiles = ArrayList<Boolean>()
            deletePaths.forEach {
                deleteFiles.add(
                    findAndDeleteDocument(
                        activity,
                        treeUri,
                        it
                    )
                )
            }
            return deleteFiles
        } else {
            return emptyList()
        }
    }
}

}

Đốc.tc
  • 530
  • 4
  • 12