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
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()
}