Android - R/W to removable SD card

后端 未结 1 696
梦如初夏
梦如初夏 2021-01-23 09:22

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

相关标签:
1条回答
  • 2021-01-23 09:59

    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()
    }
    
    0 讨论(0)
提交回复
热议问题