val projection = arrayOf( MediaStore.Files.FileColumns.DISPLAY_NAME )
val selection = MediaStore.Files.FileColumns.RELATIVE_PATH + " = '/'"
val mediaStoreVolumes = MediaStore.getExternalVolumeNames(requireActivity())
mediaStoreVolumes.forEach { volume ->
val uri = MediaStore.Files.getContentUri(volume)
val cursor = cr.query(uri, projection, selection, null, null)
if (cursor != null) {
while (cursor.moveToNext()) {
Log.e("Volume: "+volume, "File: "+ cursor.getString(0))
}
cursor.close()
}
}
The above code print file names in 'external_primary' and SDCARD. But for USB OTG it fails with exception:
E FATAL EXCEPTION: main
Process: com.starsolutions.starfilemanager, PID: 10093
java.lang.IllegalArgumentException: Volume a80d-7c6d not found
But the volume name is returned by MediaStore by MediaStore.getExternalVolumeNames()
Is it possible to list files in a USB OTG drive using MediaStore API?
Related
Previously I had logic where I located the needed folder on external storage (using my own file chooser which returned a File to chosen folder) and then just queried all the media files in it, using the path as prefix, there is a logic:
fun loadAudio(path: String): List<Audio> {
val contentResolver = context.contentResolver
val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
MediaStore.Audio.Media.DATA,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ALBUM,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Albums.ALBUM_ID
)
val selection = "${MediaStore.Audio.Media.IS_MUSIC} != 0 AND ${MediaStore.Audio.Media.DATA} LIKE '${path}%'"
val sortOrder = MediaStore.Audio.Media.TITLE + " ASC"
val cursor = contentResolver.query(uri, projection, selection, null, sortOrder)
val tempAudioList = arrayListOf<Audio>()
if (cursor != null && cursor.count > 0) {
while (cursor.moveToNext()) {
val data = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA))
val fileName = data.split(File.separator).let { it[it.size - 1] }
val title = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE))
val album = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM))
val artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST))
val duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION))
val albumId = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Albums.ALBUM_ID))
// Save to audioList
tempAudioList.add(Audio(data, fileName, title, album, artist, duration, albumId))
}
}
cursor!!.close()
return tempAudioList
}
Now android deprecates the direct access using File, and we are forced to use Storage Access Framework (SAF).
So I started to use:
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.putExtra("android.content.extra.SHOW_ADVANCED", true);
startActivityForResult(intent, 42)
Which returns Uri to the folder.
The problem is that: I no longer have the direct filesystem path to the folder, so when I try to query media files using contentResolver.query, I no longer able to write selection like:
val selection = "${MediaStore.Audio.Media.IS_MUSIC} != 0 AND ${MediaStore.Audio.Media.DATA} LIKE '${path}%'"
Because I don't have path anymore.
I switched to use DocumentFile, but when I try to use that Uri in that SQL selection clause, it just doesn't work...
I have 1 workaround like: get the filename from the DocumentFile and then try to use it in query and filter by ${MediaStore.Audio.Media.DISPLAY_NAME} LIKE ${name}, but filesystem might have several files with the same name.
Can somebody suggest me the way I should follow, to select the folder (as I do now with SAF and then be able using contentResolver.query to query all the audios in that folder get their media tags?
Also if I use Uri of the folder or any of it's subfiles in:
contentResolver.query(uri, projection, selection, null, sortOrder)
in doesn't return me any media tags
I am trying to save an image taken from the camera using the following codes:
#RequiresApi(Build.VERSION_CODES.Q)
private fun setImageUri(): Uri {
val resolver = contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "house2.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/OLArt")
}
imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
return imageUri!!
}
The function works well for the first time. however when the image (house2.jpg) already exists, the system will create another file called "house2 (1).jpg", "house2 (2).jpg, etc (instead of replacing the old file)
is there anything I can set in the contentValues to force the resolver to replace the file rather than create copies of it?
below is the codes for the take picture intent.
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, setImageUri()) //<- i paste in the imageUri here
// Ensure that there's a camera activity to handle the intent
takePictureIntent.resolveActivity(packageManager)?.also {
startActivityForResult(takePictureIntent, 102)
}
}
#CommonsWare's comment helped.
The idea is to
Query if file already exists with resolver.query()
If yes, extract the contentUri from the cursor
Otherwise, use resolver.insert()
one thing to note when creating the selection for query is that MediaStore.MediaColumns.RELATIVE_PATH requires a terminating "/"
i.e. 'Pictures/OLArt/' << note the slash after OLArt/
val selection = "${MediaStore.MediaColumns.RELATIVE_PATH}='Pictures/OLArt/' AND "
+ "${MediaStore.MediaColumns.DISPLAY_NAME}='house2.jpg' "
The following is the updated codes.
#RequiresApi(Build.VERSION_CODES.Q)
private fun getExistingImageUriOrNullQ(): Uri? {
val projection = arrayOf(
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DISPLAY_NAME, // unused (for verification use only)
MediaStore.MediaColumns.RELATIVE_PATH, // unused (for verification use only)
MediaStore.MediaColumns.DATE_MODIFIED //used to set signature for Glide
)
// take note of the / after OLArt
val selection = "${MediaStore.MediaColumns.RELATIVE_PATH}='Pictures/OLArt/' AND "
+ "${MediaStore.MediaColumns.DISPLAY_NAME}='house2.jpg' "
contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, selection, null, null ).use { c ->
if (c != null && c.count >= 1) {
print("has cursor result")
c.moveToFirst().let {
val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) )
val displayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) )
val relativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH) )
lastModifiedDate = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) )
imageUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
print("image uri update $displayName $relativePath $imageUri ($lastModifiedDate)")
return imageUri
}
}
}
print("image not created yet")
return null
}
I then add this method into my existing codes
#RequiresApi(Build.VERSION_CODES.Q)
private fun setImageUriQ(): Uri {
val resolver = contentResolver
imageUri = getExistingImageUriOrNullQ() //try to retrieve existing uri (if any)
if (imageUri == null) {
//=========================
// existing codes for resolver.insert
//(SNIPPED)
//=========================
}
return imageUri!!
}
Angel Koh's answer is correct.
I'm just posting it in Java:
#RequiresApi(Build.VERSION_CODES.Q)
public static Uri CheckIfUriExistOnPublicDirectory(Context c ,String[] projection, String selection){
ContentResolver resolver = c.getContentResolver();
Cursor cur = resolver.query(MediaStore.Downloads.EXTERNAL_CONTENT_URI, projection, selection , null, null);
if (cur != null) {
if(cur.getCount()>0){
if (cur.moveToFirst()) {
String filePath = cur.getString(0);
long id = cur.getLong(cur.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));
String displayName = cur.getString(cur.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) );
String relativePath = cur.getString(cur.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH) );
long z = cur.getLong(cur.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) );
return imageUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
} else {
// Uri was ok but no entry found.
}
}else{
// content Uri was invalid or some other error occurred
}
cur.close();
} else {
// content Uri was invalid or some other error occurred
}
return null;
}
and usage of method:
String[] projection = {MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.RELATIVE_PATH,
MediaStore.MediaColumns.DATE_MODIFIED
};
String selection = MediaStore.MediaColumns.RELATIVE_PATH + "='" + Environment.DIRECTORY_DOWNLOADS + File.separator + folderName + File.separator + "' AND "
+ MediaStore.MediaColumns.DISPLAY_NAME+"='" + fileName + "'";
uri = CheckIfUriExistOnPublicDirectory(context,projection,selection);
if(uri != null){
// file already exist
}else{
// file not exist, insert
}
This is the correct expected behavior. The reason why you see different numeration postfixes, is because probably you are saving the files in the same folder, so Android has to create a unique name in order to allow the files to exist in the same location.
The Insert method is meant to create always new records. The Uri that it returns is always a newly inserted record. But if the file is saved in a folder where there is already another file with the same name, then as such file name must be different Android will append the numeric value.
If you wish to replace an existing record, then you must first locate its Uri, and then use it by calling instead the ContentResolver update method.
If you are saving photos from a camera app, then you could use instead the current time as the name, including the milliseconds, to ensure is unique.
Have you tried using the method update?
Check if it creates a new one when there's nothing yet, if it doesn't work, then use insert or update depending if the file has been created.
I'm working on audio and video player. I got all video's folder name using mediastore.video.media.bucket_display_name, it's working perfectly with video files but when i uses mediastore.audio.media.bucket_display_name it shows error. Can anybody help me to get name of folders containing audio files. I need to do this using cursor. And i got this error:
"no such column: bucket_display_name (Sqlite code 1): , while compiling: SELECT bucket_display_name FROM audio"
Use below code to get folders name:
fun getFolderName() {
val uri: Uri
val cursor: Cursor?
uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(MediaStore.Audio.AudioColumns.DATA)
cursor = requireActivity().contentResolver.query(uri, projection, null, null, null)
if (cursor != null) {
column_index_data = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
}
while (cursor!!.moveToNext()) {
absolutePathOfImage = cursor.getString(column_index_data)
val fileName: String = File(absolutePathOfImage).parentFile.name
}
}
I'm using the following code to get all audio files (actually their ids so I can play them later with MediaPlayer) from a directory on my android device:
ContentResolver cr = context.getContentResolver();
Uri audioUri = MediaStore.Audio.Media.getContentUriForPath(dir.getPath()); //dir is a File object representing the dir I'm looking in
Cursor audioCursor = cr.query(audioUri, null, null, null, null);
if (audioCursor != null && audioCursor.moveToFirst()) {
int idColumn = audioCursor.getColumnIndex(MediaStore.Audio.Media._ID);
List<Long> fileIds = new ArrayList<>();
do {
long id = audioCursor.getLong(idColumn);
fileIds.add(id);
} while (audioCursor.moveToNext());
return fileIds;
} else {
return null;
}
For some reason however, it returns an array with 2904 ids for any dir I give it. I'm guessing there are 2904 audio files on my device (when I use Uri audioUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI it also returns 2904 ids). So it seems I'm using getContentUriForPath incorrectly, however the android reference doesn't give any information.
How should this work?
Some more info about what I'm trying to do (if it makes any difference in your answer): I'm still working on the code, trying out different things. Ultimately I want to play the files with MediaPlayer. I only need the audio files in that folder directly, ignoring any subfolders. They should also be ordered by filename.
You can't use getContentUriForPath with a folder, there isn't any content registered at this path. In this case getContentUriForPath return the standard EXTERNAL_CONTENT_URI, for this reason you get all the ids available.
This return all the files contained in a particular folder (e.g. /storage/emulated/0/Music/), ignoring any subfolders and ordered by filename:
Cursor audioCursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
null,
MediaStore.Audio.Media.DATA + " LIKE ? AND " + MediaStore.Audio.Media.DATA + " NOT LIKE ?",
new String[]{path + "%", path + "%/%"},
MediaStore.Audio.Media.DISPLAY_NAME + " ASC");
if (audioCursor != null && audioCursor.moveToFirst()) {
int idColumn = audioCursor.getColumnIndex(MediaStore.Audio.Media._ID);
List<Long> fileIds = new ArrayList<>();
do {
long id = audioCursor.getLong(idColumn);
fileIds.add(id);
} while (audioCursor.moveToNext());
return fileIds;
} else {
return null;
}
Is there a way to limit the number of returned rows to a cursor?
I have a phone with about 4000 contacts, I just need some of them.
this is the code i'm using
db = new dBHelper(this);
ContentResolver cr = getContentResolver();
Cursor cursor;
cursor = cr.query(ContactsContract.Contacts.CONTENT_URI,null, null, null, ContactName + " ASC");
Log.i(TAG, CLASSNAME + " got contacts entries");
for (int it = 0; it <100 ; it++){//cursor.getCount()
Log.i(TAG, CLASSNAME + " getting string");
String mytimes_contacted = cursor.getString(cursor.getColumnIndex(dBHelper.times_contacted));
Log.i(TAG, CLASSNAME + " done from the string");
}
the Log i'm getting is
I/Check(11506): [ContactsPicker] got contacts entries
I/Check(11506): [ContactsPicker] getting first string
D/AndroidRuntime(11506): Shutting down VM
W/dalvikvm(11506): threadid=1: thread exiting with uncaught exception (group=0x2aac8578)
D/dalvikvm(11541): GC_CONCURRENT freed 923K, 46% free 4000K/7303K, external 1685K/2133K, paused 1ms+8ms
E/AndroidRuntime(11506): FATAL EXCEPTION: main
E/AndroidRuntime(11506): java.lang.RuntimeException: Unable to start activity ComponentInfo{~~my package name~~}: android.database.CursorIndexOutOfBoundsException: Index -1 requested, with a size of 3537
To limit the number of results in your cursor try:
cursor = cr.query(ContactsContract.Contacts.CONTENT_URI,null, null, null, ContactName + " LIMIT 100");
while(cursor.moveToNext()) {
// something clever
}
From Android 11, that above solution will not work, you can try this one to fetch the data.
/**
* Call to fetch all media on device, it but be called synchronously since function is called on a background thread
*/
private fun fetchGalleryImages(
context: Context,
offset: Int,
limit: Int
): List<MediaItem> {
val galleryImageUrls = mutableListOf<MediaItem>()
try {
if (EasyPermissions.hasPermissions(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
) {
// Define the columns that will be fetched
val projection = arrayOf(
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.DATE_ADDED,
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.Files.FileColumns.MIME_TYPE,
MediaStore.Files.FileColumns.TITLE,
MediaStore.Video.Media.DURATION
)
val selection =
"${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?"
val selectionArgs = arrayOf(
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(),
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()
)
/**
* Change the way to fetch Media Store
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Get All data in Cursor by sorting in DESC order
context.contentResolver.query(
contentUri(),
projection,
Bundle().apply {
// Limit & Offset
putInt(ContentResolver.QUERY_ARG_LIMIT, limit)
putInt(ContentResolver.QUERY_ARG_OFFSET, offset)
// Sort function
putStringArray( // <-- This should be an array. I spent a whole day trying to figure out what I was doing wrong
ContentResolver.QUERY_ARG_SORT_COLUMNS,
arrayOf(MediaStore.Files.FileColumns.DATE_MODIFIED)
)
putInt(
ContentResolver.QUERY_ARG_SORT_DIRECTION,
ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
)
// Selection
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection)
putStringArray(
ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
selectionArgs
)
}, null
)
} else {
val sortOrder =
"${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC LIMIT $limit OFFSET $offset"
// Get All data in Cursor by sorting in DESC order
context.contentResolver.query(
contentUri(),
projection,
selection,
selectionArgs,
sortOrder
)
}?.use { cursor ->
while (cursor.moveToNext()) {
galleryImageUrls.add(
MediaItem(
cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)),
ContentUris.withAppendedId(
contentUri(),
cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID))
),
cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA)),
cursor.getStringOrNull(cursor.getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE)),
cursor.getLongOrNull(cursor.getColumnIndex(MediaStore.Video.Media.DURATION))
)
)
}
}
}
} catch (ex: Exception) {
ex.printStackTrace()
}
return galleryImageUrls
}
The accepted answer is not valid anymore for android 11. In android 11 a constraint was added to not allow using LIMIT in sort value. You need to use the query with bundle parameters. For instance:
val bundle = Bundle().apply {
putInt(ContentResolver.QUERY_ARG_LIMIT, 100)
}
resolver.query(
ContactsContract.Contacts.CONTENT_URI,
projection,
bundle,
null
)
In android 26 query method is upgraded. This function is using these arguments.
Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal
Below example I'm getting recent 5 pictures.
val whereArgs = arrayOf("image/jpeg", "image/png", "image/jpg")
val projection = arrayOf(MediaStore.Images.ImageColumns._ID,
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.MIME_TYPE)
val selection =
"${MediaStore.Files.FileColumns.MIME_TYPE} = ? OR ${MediaStore.Files.FileColumns.MIME_TYPE} = ? OR ${MediaStore.Files.FileColumns.MIME_TYPE} = ?"
val queryArgs = Bundle()
val sortArgs = arrayOf(MediaStore.Images.ImageColumns.DATE_TAKEN)
queryArgs.putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, sortArgs)
queryArgs.putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING)
queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, 5)
queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection)
queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, whereArgs)
val cursor = context!!.contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
queryArgs,
null)
if (cursor!!.moveToFirst()) {
do {
val imageLocation = cursor.getString(1)
val imageFile = File(imageLocation)
if (imageFile.exists()) {
//access you file from imageLocation
}
} while (cursor.moveToNext())
fiveRecentlyImagesAdapter!!.notifyDataSetChanged()
}
If anyone is looking for Java version of the above Ignacio Tomas Crespo's answer ,
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
cursor = context.getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
.buildUpon()
.encodedQuery("limit=" + offSet + "," + "100")
.build(),
columns,
null,
null,
null);
} else {
Bundle bundle = new Bundle();
bundle.putInt(ContentResolver.QUERY_ARG_LIMIT, 100);
cursor = context.getContentResolver()
.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
columns,
bundle,
null);
}