I am writing a file sharing app for Android (>= API 28) and I have the following use case:
- A user uploads a file from their local storage.
- The user closes the app and after a while reopens it, causing the app to request the list of files to the server to populate the layout.
- The user clicks on a file to preview it.
At this point, the most intuitive step would be, given that the app knows the file URL, just load it inside a WebView pointing to i.e. Google Docs' https://docs.google.com/viewerng/viewer?url=$myFileUrl
or any other file viewing service.
Truth is, I don't want my app to excessively cling on to an external service, so I thought at the moment of the file upload (step 1), a content://
Uri
is attached and stored in a database to use it in step 3.
That way, if the user hasn't renamed and moved the file he uploaded, the app could attempt to open the file locally, saving some bandwidth usage.
As @CommonsWare wrote in this article, the time the app has permission to access a Uri
is limited; in my case it only lasts until the user closes the app. That means using a strategy such as:
MainActivity.kt
// Getting the content:// Uri after a call to the server.
viewModel.selectedUrl.observe(this@MainActivity) { uri ->
val newIntent = Intent(Intent.ACTION_VIEW)
try {
newIntent.data = Uri.parse(uri)
newIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
newIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
try {
startActivity(newIntent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, e.toString(), Toast.LENGTH_LONG).show()
}
// ...
}
Will only work if the file is uploaded and opened straight away. Is it possible to solve this issue in any way or should I just stick to the WebView
option which does not involve any Uri
?
UPDATE: How is the file uploaded?
In my MainActivity
I have a FAB which, when clicked, triggers the following snippet:
MainActivity.kt
val fileResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
viewModel.uploadFile(result, false) // This is the Uri.
}
fileFab.setOnClickListener {
val intent = Intent()
intent.type = "application/*"
intent.action = Intent.ACTION_OPEN_DOCUMENT
fileResultLauncher.launch(intent)
}
...which after the respective ViewModel.kt
and Repository.kt
calls it ends up in a Server
class with the method:
Server.kt
override fun uploadFile(uri: Uri, title: String, isSound: Boolean, callback: IFileUploadCallback) {
Timber.d("Uploading document...")
val inputStream = c.contentResolver.openInputStream(uri)
var fileData: ByteArray? = null
inputStream?.buffered()?.use {
fileData = it.readBytes()
}
fileData?: return
// Call to Volley
// ...
CodePudding user response:
Thanks to @blackapps and @CommonsWare, I was missing the following line in my ViewModel class:
context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
ViewModel.kt
fun uploadFile(result: ActivityResult) {
if (result.resultCode == Activity.RESULT_OK) {
val uri: Uri = result.data?.data!!
context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
viewModelScope.launch(Dispatchers.IO) {
repository.uploadFile(uri, object: Server.IFileUploadCallback {
override fun onSuccess(fileUrl: String, content: String) {
uploadedFileUrl.postValue(RecyclerViewItem.Document(fileUrl, content, uri))
}
})
}
}
}