In this blog post we are going to look at how we can pick file in Android without messing with Content URI. If you are just beginning with Android. You will notice that you will get a content://
URI after you do Intent.ACTION_OPEN_DOCUMENT
. But this URI is useless if you want to lets say send file to server. To overcome this you have to either use a file picker library or manually check each type and try to get the actual location.
But there’s actually another much more easier and efficient way of doing this. But every way it has it’s disadvantages. We will look at it one by one. I’ll list when it might not be idle to use this approach.
Quick Navigation
Introduction to Storage Access Framework ( SAF )
Storage Access Framework or SAF is a newer approach to accessing the files in Android device. Not only local files but you can also access the online storage provider such as Google Drive.
It is an easy and centralized way to access everything in your device from multiple providers. Let us look at the image below.
We have client that can either create or access the existing file from any one of the listed providers after doing Intent.ACTION_OPEN_DOCUMENT
or Intent.ACTION_CREATE_DOCUMENT
. We will be only looking at opening files in this post.
But SAF comes with its own challenge of not exposing the actual URI. Which is very good from security perspective as it limits the interactions. But how do we get the file to upload it somewhere or do something with it withing our own application ?
This post will exactly do that. We will pick file in Android and not worry about the Content URI. Let us quickly see what we are going to do before we actually do anything.
Our strategy to using SAF
Let us see what we can do to use SAF. We will follow the following steps to utilizing the framework.
- Open the system’s document picker
- Get the Content URI for the selected file(s)
- Copy the contents of file to cache directory
- Use the cached version of the file
- Clear the cache after the operation completes
Those are the steps to using the SAF. We will be following the listed approach to accomplish the task.
Limitations and disadvantages of this approach
It might not be ideal to always use this approach specially because we will be caching a copy to be used locally by our application.
Which will quickly grow the applications cache size. But if you think this will not be an issue with your current project. Then you can use it without any side effects at all.
Also this approach will not work if you want to directly modify the source file rather than a locally cached version. But it’s worth noting that this approach will download the file from remote server which might be advantageous in certain cases.
I was using this approach because I had to send some data via Rest API to remote server. Let us begin.
Opening system’s file picker
We can open the system’s UI for picking the document. Where we can either pick a single file or pick multiple files. We will look at both approaches. There is only a slight difference in picking a single file or multiple file.
We have this object
which will help us show a system document picker. Briefly explained below.
object Picker {
private const val FILE_TYPE = "*/*"
private const val REQUEST_CODE = 1121
fun showPicker(context: Activity) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
// the type of file we want to pick; for now it's all
intent.type = FILE_TYPE
// allows us to pick multiple files
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
context.startActivityForResult(Intent.createChooser(intent, "Select file."), REQUEST_CODE)
}
fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?,
onActionComplete: OnActionComplete
) {
if (data == null) return // data is null we cannot do anything
if (resultCode == RESULT_CANCELED) return // user cancelled the selection
when (requestCode) {
REQUEST_CODE -> {
// user has selected single item
val uri = data.data
uri?.let {
println("DEBUG: $uri")
onActionComplete.onSuccess(uri = arrayListOf(it))
return
}
// user has selected multiple items
val clipData = data.clipData
clipData?.let {
val itemCount = it.itemCount
val uris = arrayListOf<Uri>()
for (index in 0 until itemCount) {
val itemUri = it.getItemAt(index).uri
println("DEBUG: $itemUri")
uris.add(itemUri)
}
onActionComplete.onSuccess(uri = uris)
}
}
}
}
interface OnActionComplete {
fun onSuccess(uri: List<Uri>)
fun onFailure()
}
}
Code language: Kotlin (kotlin)
showPicker
method will show the picker UI and the result will be passed on to onActivityResult
from the Activity
or Fragment
‘s onActivityResult
.
If you look closely you will see that there is OnActionComplete
interface which has methods that will be invoked after the completion of the specific action.
Also Read : Interface in OOP: Guide to Polymorphism and Callbacks
TECHENUM
But how will you use the above object
? See this snippet for showing the file picker
mOpenFilePicker.setOnClickListener {
Picker.showPicker(this)
}
Code language: Kotlin (kotlin)
And for handling the result pass it from the onActivityResult
callback.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Picker.onActivityResult(requestCode, resultCode, data, this)
}
Code language: Kotlin (kotlin)
We have successfully completed the selection action. Let us look at how we can copy the file locally in cache.
Caching the file from the URI
Look at this class named FileCache
which has all the required methods to copy files from content://
URI.
It shouldn’t be too hard to understand what’s going on as I have commented major portions of the code.
class FileCache(
private val mContext: Context
) {
// content resolver
private val contentResolver = mContext.contentResolver
// to get the type of file
private val mimeTypeMap = MimeTypeMap.getSingleton()
private val mCacheLocation = mContext.cacheDir
fun cacheThis(uris: List<Uri>) {
executor.submit {
uris.forEach { uri -> copyFromSource(uri) }
}
}
/**
* Copies the actual data from provided content provider.
*/
private fun copyFromSource(uri: Uri) {
val fileExtension: String = getFileExtension(uri) ?: kotlin.run {
throw RuntimeException("Extension is null for $uri")
}
val fileName = queryName(uri) ?: getFileName(fileExtension)
val inputStream = contentResolver.openInputStream(uri) ?: kotlin.run {
throw RuntimeException("Cannot open for reading $uri")
}
val bufferedInputStream = BufferedInputStream(inputStream)
// the file which will be the new cached file
val outputFile = File(mCacheLocation, fileName)
val bufferedOutputStream = BufferedOutputStream(FileOutputStream(outputFile))
// this will hold the content for each iteration
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var readBytes = 0 // will be -1 if reached the end of file
while (true) {
readBytes = bufferedInputStream.read(buffer)
// check if the read was failure
if (readBytes == -1) {
bufferedOutputStream.flush()
break
}
bufferedOutputStream.write(buffer)
bufferedOutputStream.flush()
}
// close everything
inputStream.close()
bufferedInputStream.close()
bufferedOutputStream.close()
}
private fun getFileExtension(uri: Uri): String? {
return mimeTypeMap.getExtensionFromMimeType(contentResolver.getType(uri))
}
/**
* Tries to get actual name of the file being copied.
* This might be required in some of the cases where you might want to know the file name too.
*
* @param uri
*
*/
@SuppressLint("Recycle")
private fun queryName(uri: Uri): String? {
val returnCursor: Cursor = contentResolver.query(uri, null, null, null, null) ?: return null
val nameIndex: Int = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
returnCursor.moveToFirst()
val name: String = returnCursor.getString(nameIndex)
returnCursor.close()
return name
}
private fun getFileName(fileExtension: String): String {
return "${System.currentTimeMillis().toString()}.$fileExtension"
}
/**
* Remove everything that we have cached.
* You might want to invoke this method before quiting the application.
*/
fun removeAll() {
mContext.cacheDir.deleteRecursively()
}
companion object {
// base buffer size
private const val BASE_BUFFER_SIZE = 1024
// if you want to modify size use binary multiplier 2, 4, 6, 8
private const val DEFAULT_BUFFER_SIZE = BASE_BUFFER_SIZE * 4
private val executor = Executors.newSingleThreadExecutor()
}
}
Code language: Kotlin (kotlin)
This code will copy all the files from source to app’s mContext.cacheDir
which can be queried later by listing files from there.
Let us finally see how we can combine everything in our MainActivity.
Also Read : Async Task in Android is Deprecated: There are Better Ways
TECHENUM
Wrapping everything up
Now that we have completed major portions of the code. Let us see how we can actually use it in our MainActivity
.
The code for the design activity_main.xml
.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.button.MaterialButton
android:id="@+id/open_file_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="Open File Picker"
app:layout_constraintBottom_toBottomOf="@+id/empty_cache"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/empty_cache"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:text="Empty Cache"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
Code language: HTML, XML (xml)
And our final MainActivity looks like
class MainActivity : AppCompatActivity(), Picker.OnActionComplete {
private lateinit var mOpenFilePicker: MaterialButton
private lateinit var mEmptyCache: MaterialButton
private val fc: FileCache by lazy {
FileCache(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mOpenFilePicker = findViewById(R.id.open_file_picker)
mEmptyCache = findViewById(R.id.empty_cache)
mOpenFilePicker.setOnClickListener {
Picker.showPicker(this)
}
mEmptyCache.setOnClickListener {
fc.removeAll()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Picker.onActivityResult(requestCode, resultCode, data, this)
}
override fun onSuccess(uri: List<Uri>) {
fc.cacheThis(uri)
}
override fun onFailure() {
}
}
Code language: Kotlin (kotlin)
Now that is all that we had to do. Everything should be available in the cache directory of the application.
What do you think about this approach to getting android content from content URI without converting it to file URI ?
Also Read : Kotlin Coroutine in Android : Understanding the Basics
TECHENUM