Kotlin how to get and open a video - android

New to Kotlin and i'm trying to simple get any video from gallery and open into device's default app player.
My function to get all videos. It seens to work well, but the returned Uri is like 'content://...', i don't know if this is the right or it should be something like 'file://...'
private val videos = mutableListOf<Uri>()
private fun getAllVideos() {
val uriExternal = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(MediaStore.Video.Media._ID)
contentResolver.query(uriExternal, projection, null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val videoUri = Uri.withAppendedPath(uriExternal, "" + cursor.getString(0))
videos.add(videoUri)
}
}
}
Then i try to open the Uri like this, but i always get an error from the player and nothing works.
val intent = Intent(Intent.ACTION_VIEW).apply {
data = videos.first()
type = "video/*"
}
startActivity(intent)
I searched but dont found any updated tutorial that don't use "MediaStore.Video.Media.DATA" (it's deprecated now). Something i'm doing wrong?

but the returned Uri is like 'content://...', i don't know if this is the right
Yes, it is.
Then i try to open the Uri like this, but i always get an error from the player and nothing works.
First, either remove the type or use the correct MIME type. Do not use a wildcard.
Second, add FLAG_GRANT_READ_URI_PERMISSION to the Intent. Without it, the other app has no rights to access the content.
Also, make sure you only go through that code if there is at least one element in the list, as otherwise your first() call will throw an exception.

Related

How to load an image using a URI stored in Room as a String

I am making a practice application to load the inventory of a store, inside the screen I press a floating button that generates a dialog that asks for an image among several data, which the user selects from their gallery, later when pressing the save button in the dialog, the image and the rest of the data are saved in the ViewModel and ROOM, to then generate an item on the main screen that shows these data at the same time that they are printed with Log.d
When generating the item after saving, it is shown correctly, however, if I restart the application the image disappears. Both when generating the image and when restarting the application, the Log.d print shows the same URI in both cases.
My main goal is to save an image, its address, or URI in ROOM, and then load the item with the image. My research leads me to believe that it is correct to save the URI as a string, but I am unaware of the proper practices for saving an image and I am open to other ways to reach a solution if necessary.
First, in a dialog to create an item, I select an image from the gallery like this and save it in the ViewModel and later in ROOM:
val singlePhotoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
selectedImageUri = uri
Log.d("URI RESULT", "$uri")
viewModel.onDialogChanged(
uri.toString()
)
}
)
I save it in ROOM when I press the 'Save' button:
DialogButton(
ButtonDefaults.buttonColors(
backgroundColor = verdeBillete,
contentColor = Color.White
), "Guardar", modifier = Modifier, onClick = {
viewModel.viewModelScope.launch {
viewModel.onAddSelected(
inventoryItem(
0,
uri,
)
)
}
onDismiss()
})
//add to ViewModel
fun onAddSelected(addItem: inventoryItem) {
viewModelScope.launch {
addItem(addItem)
getInventory()
}
}
//ROOM Table
#Entity(tableName = "inventory")
data class inventoryItem(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "r_id")
var id: Int = 0,
#ColumnInfo(name = "r_uri")
val uri: String,
)
Then I currently try to load the image like this:
Log.d("Loading items", item.uri)
AsyncImage(
model = Uri.parse(item.uri),
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Crop
)
Just after selecting the image from the gallery, the image is visible, however, after restarting the application the image disappears. In both cases the printed URI in Log.d is the same.
Also, I have permission for:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
Update: After reading both answers from CommonsWare and Gowtham K K (Thank you both!) and trying to implement them, I couldn't write the code myself, so I entered the content of the post (the question and both answers) into chatgpt and asked for a solution, which presented me with the following solution which worked for me.
To use takePersistableUriPermission, you must do the following:
First, you need to have permissions to read or write the URI that you
want to save persistently. You can do this by adding the following
line of code in your AndroidManifest.xml file:
or
Then, you need to obtain the URI that you want to save persistently.
For example, if you want to save the URI of an image selected from the
gallery, you can use the ActivityResultContracts.PickVisualMedia
method as follows:
val singlePhotoPickerLauncher =
rememberLauncherForActivityResult( contract =
ActivityResultContracts.PickVisualMedia(), onResult = { uri ->
selectedImageUri = uri } )
Once you have the URI, you can use takePersistableUriPermission to
save it persistently. The takePersistableUriPermission method should
be used on the ContentResolver and takes two parameters: the URI and
the access mode (read or write). For example:
contentResolver.takePersistableUriPermission(uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION) or
contentResolver.takePersistableUriPermission(uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
Finally, you can save the URI in your ROOM database as a text string
and load it in your application when necessary. For example:
val inventoryItem = inventoryItem(0, uri.toString())
viewModel.onAddSelected(inventoryItem)
Putting everything together:
var selectedImageUri by remember {
mutableStateOf<Uri?>(null)
}
val singlePhotoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
selectedImageUri = uri
Log.d("URI RESULT", "$uri")
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION//or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
val resolver = mContext.contentResolver
resolver.takePersistableUriPermission(uri!!, flags)
viewModel.onDialogChanged( //**save to database function**
uri.toString()
)
}
)
This is because the URI would get revoked when app process get killed.
For Storage access framework URIs you can get long term permission using takePersistableUriPermission .
But It might not work for ActivityResultContracts.PickVisualMedia() as far as I know.
In your case you can make your own copy of the image after getting first time and save it in the app specific storage and save that URI in your db.
This will be more flexible. Even if original file gets deleted / you will still can get access of your copied image.

Identify the selected option from Intent.ACTION_CHOOSER from the registerForActivityResult

I am using an action chooser intent to ask the user to choose one of the following from a fragment:
MediaStore.ACTION_IMAGE_CAPTURE
MediaStore.ACTION_VIDEO_CAPTURE
Intent.ACTION_GET_CONTENT
I want to be able to distinguish the selected action of the user because I have different functions per action.
Below is my current code.
private val intentLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
//Identify the intent selected
//TODO: image from camera
//TODO: video from camera
//TODO: any file
}
}
private fun dispatchActionChooserIntent() {
Intent(Intent.ACTION_CHOOSER).also { actionChooserIntent ->
val cameraIntent = createCameraIntent(MediaStore.ACTION_IMAGE_CAPTURE)
val videoIntent = createCameraIntent(MediaStore.ACTION_VIDEO_CAPTURE)
val filePickerIntent = createFilePickerIntent()
actionChooserIntent.putExtra(Intent.EXTRA_INTENT, filePickerIntent);
actionChooserIntent.putExtra(
Intent.EXTRA_INITIAL_INTENTS,
arrayOf<Intent>(cameraIntent, videoIntent)
);
cameraIntent.putExtra("intentAction",Intent.ACTION_CHOOSER)
actionChooserIntent.putExtra(Intent.EXTRA_TITLE, "")
}
}
private fun createFilePickerIntent(fileType: String = "*/*"): Intent {
return Intent(Intent.ACTION_GET_CONTENT).also { filePickerIntent ->
filePickerIntent.type = fileType
filePickerIntent.addCategory(Intent.CATEGORY_OPENABLE)
filePickerIntent.resolveActivity(
(activity as AppCompatActivity).applicationContext.packageManager)
}
}
private fun createCameraIntent(cameraAction: String): Intent {
return Intent(cameraAction).also { cameraIntent ->
// Ensure that there's a camera activity to handle the intent
cameraIntent.resolveActivity(
(activity as AppCompatActivity).applicationContext.packageManager)
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraIntentURI)
}
}
the result only includes the resultCode and the data
Sample result from taking a photo
Sample result from taking a video
Sample result from choosing a file
If the received uri is null the camera was choosen.
The MediaStore.ACTION_VIDEO_CAPTURE honors Intent.EXTRA_OUTPUT and you will use your fileprovider there.
In contrary to the camera action you will get the supplied uri back. (Google has learned something).
So if you receive an uri in onActivityResult you can check for uri.getAuthority().
if the authority is the same as your fileprovider you know it is from video capture.
There can be two solutions.
Android way
When you receive the URI as data as a result. Process that URI and try to get the mime type. Based on mime type you can understand what type of data you have received. Check how it can be done here https://developer.android.com/training/secure-file-sharing/retrieve-info
Logical way
Convert the Uri into an instance of type File. Get the extension of a file and check what type of extension it is.
jpeg, png, etc fall into the image category, mp4 kind of extension is a video and the rest can be any other file. Try this to get the file extension https://www.tutorialkart.com/kotlin/kotlin-get-file-extension/#:~:text=Get%20File%20Extension%20in%20Kotlin,extension%20is%20a%20String%20value.
I would recommend exploring option 1. That's the right and safe approach.
Good luck!

How do you use a Uri opened with ActivityResultContracts.OpenDocumentTree() and query the MediaStore for all audio in that folder?

I am trying to get all audio contained in a folder that the user picks.
The user is asked to pick a folder like this
getUri = registerForActivityResult(
ActivityResultContracts.OpenDocumentTree(),
activityResultCallback
)
getUri.launch(null)
I am trying to query the folder like this
var selection = MediaStore.Audio.Media.DISPLAY_NAME + " != ?"
val selectionArgs = arrayListOf("")
if (data != null){
selection += " AND "
selection += if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
MediaStore.MediaColumns.RELATIVE_PATH + " LIKE ? "
} else {
MediaStore.Audio.Media.DATA + " LIKE ? "
}
selectionArgs.add("%$data%")
}
I have tried the following for data
uri.path.toString()
uri.encodedPath.toString()
uri.pathSegments.last().toString()
uri.pathSegments.joinToString ("/")
None of that is working. The query is empty when there should be three files. These files are in the MediaStore, as a query of all audio files in the MediaStore contains them. The docs are not very helpful (as usual). The stack overflow posts I found leave out how to get a path.
Perhaps I should ask how to get the user to pick a folder and get all the music from the folder they chose? The docs lead me to both code snippets above, so I would be floored if there was no way to connect them (except this is Android I am dealing with, so nothing surprises me anymore).
OpenDocumentTree gives you access to only the Uri you receive and its children. It does not give you access to any path at all, much less one you could use with the MediaStore APIs (nor would you want it to, given that the MediaStore APIs are terrible to begin with).
Instead, the easiest way to deal with the Uri returned by OpenDocumentTree is to pass it to DocumentFile.fromTreeUri(), which gives a DocumentFile object that you can call listFiles() on and use getType() to search for mime types starting with audio/:
// Assuming you have your uri returned by OpenDocumentTree
val rootDirectoryFile = DocumentFile.fromTreeUri(uri)
val directories = ArrayDeque(rootDirectoryFile)
val audioFileUris = mutableListOf<Uri>()
// Loop through all of the subdirectories, starting with the root
while (directories.isNotEmpty()) {
val currentDirectory = directories.removeFirst()
// List all of the files in the current directory
val files = currentDirectory.listFiles()
for (file in files) {
if (file.isDirectory) {
// Add subdirectories to the list to search through
directories.add(file)
} else if (file.type?.startsWith("audio/")) {
// Add Uri of the audio file to the list
audioFileUris += file.uri
}
}
}
// Now audioFileUris has the Uris of all audio files

Getting real path from MediaStore API

I'm building a built-in file manager for my app and I'm using MediaStore to query for the files.
I got everything working, except that the URI I get back is not to Glide's liking.
class java.io.FileNotFoundException: /external/images/media/37: open failed: ENOENT (No such file or directory)
I understand the issue at hand is that there's no protocol added so glide doesn't know how to read it, but I don't know how to add the protocol, I was under the impression that using ContentUris.withAppendedId would handle that for me but it appears to not be.
val mediaList = mutableListOf<Uri>()
val query = buildQuery(folderId, onlyImages)
query?.use { cursor ->
val columnIndexId = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
val columnIndexMimeType = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE)
val columnIndexDuration = cursor.getColumnIndexOrThrow(MediaStore.Video.VideoColumns.DURATION)
while (cursor.moveToNext())
{
val id = cursor.getLong(columnIndexId)
val contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
mediaList.add(contentUri)
}
}
return mediaList
I'm trying to void using DATA as I already know that's depreciated and I just need this solution work with Android 8+.
Are you converting the contentUri to a path? You shouldn't be doing that. Your contentUri should look like it does below, and then Glide will accept it:
content://media/external/images/media/37

Resolve content:// Uri into actual filepath

I'm building an App that manages audiobook libraries
Using the Intent ACTION_OPEN_DOCUMENT_TREE to let the user choose a Directory as a library, I get an Uri of the form : content:// as a result.
Is there a way to convert the given "content://" Uri to a "file:// filepath" ? ( if that is possible of course )
Or can I tweak the file chooser activity to accept only folders that have an actual file:// path ?
Thank you very much
EDIT : progress !
I managed, using the content resolver, to find a path of the form "1407-1105:Audiobooks" for the SD card, and "primary:Audiobooks" for the main volume. That seems more readable, but I have the same problem still.
Finally found the solution ! Maybe it is a little bit ugly, but that does seems to work !
fun resolveContentUri(uri:Uri): String {
val docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri))
val docCursor = context.contentResolver.query(docUri, null, null, null, null)
var str:String = ""
// get a string of the form : primary:Audiobooks or 1407-1105:Audiobooks
while(docCursor!!.moveToNext()) {
str = docCursor.getString(0)
if(str.matches(Regex(".*:.*"))) break //Maybe useless
}
docCursor.close()
val split = str.split(":")
val base: File =
if(split[0] == "primary") getExternalStorageDirectory()
else File("/storage/${split[0]}")
if(!base.isDirectory) throw Exception("'$uri' cannot be resolved in a valid path")
return File(base,split[1]).canonicalPath
}

Categories

Resources