Android - R/W to removable SD card

2019-08-29 07:44发布

问题:

I am creating app, that should move files from internal storage (I mean /storage/emulated/0/*) to an external removable storage (micro SD card). I find the path to root of that SD card, but I am unable to create folder or move file there. I have no idea what to do to make it work. I found something called Storage Access Framework, but I did not understand, how to use it. I will appreciate any help to get read/write permission for specific SD card root path.

Thank you in advance.

回答1:

Since api 19 (KitKat) it's almost impossible to write to SDcard content directly as a simple File because they are mounted as READ ONLY at least for default user (system can still write to it).

DocumentsProvider API is rather convoluted (even sample in docs is a hard read), that's why DocumentFile was added to simulate File behavior for easier access.

Assuming user already granted read/write storage permissions, we need to obtain document URI of SDcard root (in an Activity):

var sdCardUri : Uri? = null

private fun requestSDCardPermissions(){
    if(Build.VERSION.SDK_INT < 24){
        startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), REQ_PICK_DIRECTORY)
        return
    }
    // find removable device using getStorageVolumes
    val sm = getSystemService(Context.STORAGE_SERVICE) as StorageManager
    val sdCard = sm.storageVolumes.find { it.isRemovable }
    if(sdCard != null){
        startActivityForResult(sdCard.createAccessIntent(null), REQ_SD_CARD_ACCESS)
    }
}

Before API 24 user needs to open document picker and manually select SD card themselves. This is not ideal but Android team overlooked the fact that SD card API is lacking.

In newer version getStorageVolumes() allows us to find SDcard through code, and only display explicit warning to the user that it will be accessed. This is NOT the same dialog as Read/Write storage permission.

Now to handle obtained Uri we only need to take data.data of the result. It might be good place to store it in shared preferences to prevent asking user over and over for it:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if(requestCode == REQ_SD_CARD_ACCESS || requestCode == REQ_PICK_DIRECTORY){
        if(resultCode == RESULT_OK) {
            if(data == null){
                Log.e(TAG, "Error obtaining access")
            }else{
                sdCardUri = data.data
                Log.d("StorageAccess", "obtained access to $sdCardUri")
                // optionally store uri in preferences as well here { ... }
            }
        }else
            Toast.makeText(this, "access denied", Toast.LENGTH_SHORT).show()
        return
    }
    super.onActivityResult(requestCode, resultCode, data)
}

I will leave out most error checks/file exist validations, but now You can use obtained sdCardUri like this (copying file "sample.txt" from internal storage root to SDcard root):

private fun copyToSDCard(){
    val sdCardRoot = DocumentFile.fromTreeUri(this, sdCardUri)
    val internalFile = File(Environment.getExternalStorageDirectory(), "sample.txt")
    // get or create file
    val sdCardFile = sdCardRoot.findFile("sample.txt") ?: sdCardRoot.createFile(null, "sample.txt")
    val outStream = contentResolver.openOutputStream(sdCardFile.uri)
    outStream.write(internalFile.readBytes())
    outStream.flush()
    outStream.close()
    Toast.makeText(this, "copied to SDCard", Toast.LENGTH_SHORT).show()
}

And other way around (from SDcard to internal storage):

private fun copyToInternal(){
    val sdCardRoot = DocumentFile.fromTreeUri(this, sdCardUri)
    val internalFile = File(Environment.getExternalStorageDirectory(), "sample.txt")
    val sdCardFile = sdCardRoot.findFile("sample.txt")
    val inStream = contentResolver.openInputStream(sdCardFile.uri)
    internalFile.writeBytes(inStream.readBytes())
    inStream.close()
    Toast.makeText(this, "copied to internal", Toast.LENGTH_SHORT).show()
}