I'm struggling to update metadata in the mediastore with a DocumentFile/TreeUri.
This is how I tried to do it:
boolean canWrite = documentFile.canWrite(); //returns true
Uri mediaUri = MediaStore.getMediaUri(context, documentFile.getUri());
ContentValuesvalues = new ContentValues();
values.put(MediaStore.MediaColumns.DATE_MODIFIED, newLastModified);
boolean success = context.getContentResolver().update(mediaUri, values,null, null) > 0;
It fails and the logcat reads:
W/MediaProvider: Ignoring mutation of date_modified
What am I doing wrong? The DocumentFile is writable, it is received via Intent.ACTION_OPEN_DOCUMENT_TREE and takePersistableUriPermission was called on the folder.
I also tried updating the entry with IS_PENDING before and removed after:
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.IS_PENDING, 1);
boolean success = context.getContentResolver().update(mediaUri, values,
null, null) > 0; //returns false
returns for whatever reason also false, but I see that the file is prefixed with .pendingxxxx, so it seemed to work
ContentValuesvalues = new ContentValues();
values.put(MediaStore.MediaColumns.DATE_MODIFIED, newLastModified);
values.put(MediaStore.MediaColumns.IS_PENDING, 0);
boolean success = context.getContentResolver().update(mediaUri, values,null, null) > 0; //returns false
Indexed value of File#lastModified() extracted from this media item.
This constant represents a column name that can be used with a ContentProvider through a ContentValues or Cursor object. The values stored in this column are Cursor#FIELD_TYPE_INTEGER , and are read-only and cannot be mutated.
doc
you can't update this,just use File.setLastModified
Related
I need to update contact first and last name from emulator contact list.
my code:
val contentResolver = APP_ACTIVITY?.contentResolver
val contentValues = ContentValues().apply {
put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,"$firstName")
put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME,"$lastName")
}
Log.d("TAG", "editContactEmulator:$contentValues") //**prints: data2=Michael data3=Lou**
val contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId.toString().toLong())
Log.d("TAG", "editContactEmulator: $contactUri") // **prints:content://com.android.contacts/contacts/1**
val res = contentResolver?.update(contactUri,contentValues, null, null)
Log.d("TAG", "editContactEmulator : $res") **//RETURNS 0**
so, the new values ( first and last name) are correct, the uri is the correct uri of the contact I want to update. BUT res is still 0 which means it did not update any rows.
Any Ideas?
( I added READ_CONTACTS and WRITE_CONTACTS permission - so that not the problem)
The app I am working on needs a new lease on life and one renewing task for Android R and above: is getting the id of an image taken by CameraX from scoped storage. The purpose being to save the id in a datatable for upload later.
With regards to permissions the App only saves files to its own external directory, so no permissions are necessary.
After CameraX has captured the image using the following OutputOptions:
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, imgName)
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + File.separator + "Custom directory")
val outputOptions = ImageCapture.OutputFileOptions.Builder(
activityContext.contentResolver,
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL),
contentValues
).build()
The onImageSaved CameraX callback method from the imageCapture.takePicture object returns an ImageCapture.OutputFileResults:
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri
This is where things get fuzzy; I have the Uri of the saved image. I tried using the Uri to retrieve the image column id by searching for the file name with:
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME
)
val selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?"
return contentResolver.query(
savedUri, // the image capture uri
projection, // query columns
selection, // query
arrayOf(Environment.DIRECTORY_DCIM + File.separator + "Custom directory"), // query arguments
null // order by
)?.use { cursor ->
if (cursor.count > 0) {
while (cursor.moveToNext()) {
val fileName =
cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME))
Log.d(TAG, "index $fileName")
if (fileName.equals(displayName)) {
return#use cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID))
}
}
}
return#use null
}
I consistently get null results. So I tried the following content uri in the place of the savedUri:
val contentUri =
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
This also only gives null results and 0 cursor.count columns. Can someone please point me in the right direction on why the above file queries are giving null results?
Edit 1
After reading blackapps comment I went and read the documentation again. My understanding of scoped storage it seems is definitely lacking. I was using getExternalFilesDir before to get an output path and all was fine until I added getExternalFilesDir(Environment.DIRECTORY_DCIM) which according to the below comment is seen as public. That is when I started to meddle with the above MediaStore api and got tangled into thinking to much or little depending on my point of view.
Edit 2
Some more reading of the App specific sources where it does not state anywhere that getExternalFilesDir method will be removed in Api 32 or 33. So it can be used to even save in public directories. The example given is:
val file = File(context.getExternalFilesDir(
Environment.DIRECTORY_PICTURES), albumName)
Further:
It's important that you use directory names provided by API constants like DIRECTORY_PICTURES. These directory names ensure that the files are treated properly by the system.
Caveat to the above quoted documentation is that this excludes DIRECTORY_DCIM: Why is this not in the documentation?
Edit 3:
A good question to ask when uncertain if you are dealing with scoped storage is whether the files are:
Shareable media files (images, audio files, videos)
Updating metadata of audio file in media store is not working in Android Q OS, it works in all other OS.
I am using content provider with uri specified as MediaStore.Audio.Media.EXTERNAL_CONTENT_URI. It is working fine in all below Android Q device. Below is the code that I am using to update track metadata.
ContentValues cv = new ContentValues();
ContentResolver resolver = getContentResolver();
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
cv.put(MediaStore.Audio.Media.TITLE, newTitle);
cv.put(MediaStore.Audio.Media.ALBUM, newAlbumName);
cv.put(MediaStore.Audio.Media.ARTIST, newArtistName);
int rowsUpdated = resolver.update(uri, cv,
MediaStore.Audio.Media._ID + " = ? ", new String[]{audioId});
For Android Q device, rowsUpdated is always 0 with no exception.
How are other music player updating tracks metadata in Android Q ?
Finally, it took some time but I figured that out.
First, you need to obtain access to file. Here you can read about that
Next, I found out that to update title or artist fields (maybe others to, I didn't test them) you need to set column MediaStore.Audio.Media.IS_PENDING value to 1. Like that:
val id = //Your audio file id
val values = ContentValues()
values.put(MediaStore.Audio.Media.IS_PENDING, 1)
val uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)
contentResolver.update(uri, values, null, null)
And then you can edit fields that you need. Also to end the update process set MediaStore.Audio.Media.IS_PENDING to 0 again:
val id = //Your audio file id
val title = //New title
val artist = //New artist
val values = ContentValues()
values.put(MediaStore.Audio.Media.IS_PENDING, 0)
values.put(MediaStore.Audio.Media.TITLE, title)
values.put(MediaStore.Audio.Media.ARTIST, artist)
val uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)
contentResolver.update(uri, values, null, null)
So in one function, it would look like this:
#RequiresApi(value = android.os.Build.VERSION_CODES.Q)
fun updateMetadata(contentResolver: ContentResolver, id: Long, title: String, artist: String) {
val uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)
val values = ContentValues()
values.put(MediaStore.Audio.Media.IS_PENDING, 1)
contentResolver.update(uri, values, null, null)
values.clear()
values.put(MediaStore.Audio.Media.IS_PENDING, 0)
values.put(MediaStore.Audio.Media.TITLE, title)
values.put(MediaStore.Audio.Media.ARTIST, artist)
contentResolver.update(uri, values, null, null)
}
It's written in Kotlin but I think you will figure out how to do that in java.
UPDATE
By updating MediaStore you don't updating real file at any android version. That means, if a file would be updated (for example: renamed) and/or scanned by MediaScannerConnection your changes will be lost. This answer is right.
Using Android Q and beyond you have to first get the file
i.e
resolver.openInputStream(uri)?.use { stream -> outputFile.copyInputStreamToFile(stream) }
return outputFile.absolutePath
Helper Function
private fun File.copyInputStreamToFile(inputStream: InputStream?) {
this.outputStream().use { fileOut ->
inputStream?.copyTo(fileOut)
}
}
Then alter the metadata via a third party, I use J Audio Tagger
Then over write the old file
// From https://developer.android.com/reference/android/content/ContentProvider
// String: Access mode for the file. May be
// "r" for read-only access,
// "w" for write-only access (erasing whatever data is currently in the file),
// "wa" for write-only access to append to any existing data,
// "rw" for read and write access on any existing data, and
// "rwt" for read and write access that truncates any existing file. This value must never be null.
mContext.application.contentResolver.openOutputStream(uri, "w")?.use { stream ->
stream.write(file.readBytes())
}
This works fine when the file was created by your app
I've been updating meta data in the MediaStore through a ContentResolver, but this no longer works with Android Q (API 29). The following code gives me a warning, and the description is not updated:
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DESCRIPTION, "Some text");
res = getContext().getContentResolver().update(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values,
MediaStore.Images.Media._ID + "= ?", new String[]{sImageId});
android.process.media W/MediaProvider: Ignoring mutation of
description from com.example.android.someapp.app
This Medium post describes how Google has changed the API for accessing and updating files, but what about updating just the meta data? The warning seems to tell me Google no longer wants to allow third party apps to use the MediaStore, and I also found where the warning comes from:
https://android.googlesource.com/platform/packages/providers/MediaProvider/+/master/src/com/android/providers/media/MediaProvider.java#2960
I want to copy or move files from the internal storage to the sd card. I do this via the Storage Access Framework (SAF) and the DocumentFile class...
Copying is stream based and the DocumentFile does not have a function like the File class to set the last modified date.
I know, that I move/copy files to the sd card, so I know that I create a local file. With this information, is it somehow possible to update the last modified date of the underlying file of the DocumentFile?
It seems like you can't move/copy files from your internel storage to the sd card without losing the last modified date...
Reading - Working
public long lastModified(DocumentFile file, Context context)
{
long lastModified = 0;
final Cursor cursor = context.getContentResolver().query(file.getUri(), null, null, null, null);
try
{
if (cursor.moveToFirst())
lastModified = cursor.getLong(cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED));
}
finally
{
cursor.close();
}
return lastModified;
}
WRITING - NOT WORKING
public boolean setLastModified(DocumentFile file, Context context, long time)
{
ContentValues updateValues = new ContentValues();
updateValues.put(DocumentsContract.Document.COLUMN_LAST_MODIFIED, time);
int updated = context.getContentResolver().update(file.getUri(), updateValues, null, null);
return updated == 1;
}
This fails with a java.lang.UnsupportedOperationException: Update not supported exception...
Probably you need permission "android.permission.WRITE_USER_DICTIONARY" in manifest
Since API >=26 you can use refresh. This should work to update the Documentfile instantly . This works for me:
context.getContentResolver().refresh(file.getUri(), null, null, null);
I am just learning how to work with the Android Calendars. So far, I am able to display the info about existing calendars. I can also create my own local calendars -- the test code like:
private void createCalendarTest()
{
Uri.Builder builder = Calendars.CONTENT_URI.buildUpon();
builder.appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(Calendars.ACCOUNT_NAME, "private")
.appendQueryParameter(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL);
Uri uri = builder.build();
ContentValues values = new ContentValues();
values.put(Calendars.NAME, "TEST");
values.put(Calendars.CALENDAR_DISPLAY_NAME, "Calendar named TEST");
values.put(Calendars.SYNC_EVENTS, false);
values.put(Calendars.VISIBLE, true);
getContentResolver().insert(uri, values);
}
Actually, I can create many calendars that differ only in _ID. I have read elsewhere that I can create a calendar only when using the sync adapter. Now, how can I delete the calendar? I expect the URI must also contain the sync adapter info, and the _ID of the deleted calendar. I tried the following code, but I was unsuccessful:
private void deleteCalendarTest()
{
Uri.Builder builder = Calendars.CONTENT_URI.buildUpon();
builder.appendPath("6") // here for testing; I know the calender has this ID
.appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(Calendars.ACCOUNT_NAME, "private")
.appendQueryParameter(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL);
Uri uri = builder.build();
getContentResolver().delete(uri, null, null);
Toast.makeText(this, "??? deleteCalendarTest() not working", Toast.LENGTH_SHORT).show();
}
How can I fix it?
After reading with more attention the documentation, i found out you should add to the content values the following fields too:
values.put(CalendarContract.Calendars.SYNC_EVENTS, 1);
values.put(CalendarContract.Calendars.ACCOUNT_NAME, "private");
values.put(CalendarContract.Calendars.ACCOUNT_TYPE,CalendarContract.ACCOUNT_TYPE_LOCAL);
Then everything else should be fine and you should be able to delete the inserted calendar! ;)
"andrea-rinaldi" was right. The below code snippet worked for me. The "calendarHandler" is an instance of the helper class that extends AsyncQueryHandler, the one you used to create the calendar.
Uri calUri = CalendarContract.Calendars.CONTENT_URI;
calUri = calUri.buildUpon()
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, BuildConfig.APPLICATION_ID)
.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
.build();
calendarHandler.startDelete(0,-1,calUri,null,null);
This Kotlin solution worked for me:
val uri = ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarId.toLong())
contentResolver.delete(uri, null, null)
I'm removing the calendar with the application (not a SyncAdapter) so there's no need to append any query parameters. Just append the calender's id to the content uri and use a ContentResolver to delete the calendar.