Storage Access Framework - set last modified date of local DocumentFile - android

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);

Related

Android R CameraX with scoped storage getting image column id after capture

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)

Deleting by Uri with ContentResolver

I am trying to delete content (chanel's program) like:
content://android.media.tv/preview_program/317
with this code:
getContentResolver().delete(TvContractCompat.buildPreviewProgramUri(ALlProgramIDs.get(i)), null, null);
// ALlProgramIDs.get(i) = 317; long type
or this code:
getContentResolver().delete(Uri.parse(ALsProgramIDs.get(i)), null, null);
// ALsProgramIDs.get(i) = content://android.media.tv/preview_program/317
and nothing to be happen. Programs are not deleting.
BUT this code:
getContentResolver().delete(Uri.parse(ALsProgramIDs.get(i).substring(0, ALsProgramIDs.get(i).length() - 3)), null, null);
// LsProgramIDs.get(i).substring(0, ALsProgramIDs.get(i).length() - 3 = content://android.media.tv/preview_program/
works fine - everything from this Uri (or storage) is deleted.
What is wrong with deleting certain IDs?
Seems done like here:
https://developer.android.com/reference/androidx/tvprovider/media/tv/PreviewProgram
Usage example when deleting a preview program:
getContentResolver().delete(TvContractCompat.buildPreviewProgramUri(existingProgram.getId()), null, null);
getContentResolver().delete(TvContractCompat.buildPreviewProgramUri(programId), null, null);
Should work, so you may want to connect the debugger and step through it to verify you're passing the ID you want to delete and that the built URI is correct. An alternative that will make your code more readable is to use the PreviewChannelHelper's deletePreviewProgram method.
PreviewChannelHelper(context).deletePreviewProgram(id);

How to update metadata of audio file in Android Q media store?

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

Get a Map With All the song on the Device and their Paths in Flutter

Ok, this question is pretty straightforward, how can I get all the Songs on a device (or If on the entire device is not possible at least in one folder, for example, the music one) in a map containing all the songs with their respective path in something like this:
var song= {
// Key: Value
'title': 'All Star',
'author': 'Smash Mouth', //Optional but would like to get this too
'album': 'All Star', //Optional but would like to get this too
'path': 'storage/emulated/0/Music'
};
How Could I do it?
Android has a mediaplayer class, which scans all media files and generates corresponding database information when booting and plugging and unplugging sd cards. You can get information about music files in the following way.
private List<SingListBean> getAllSing(Context context) {
int flag = 1;
List<SingListBean> listBeans = new ArrayList<>();
Cursor cursor = context.getContentResolver().query
(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
while (cursor.moveToNext()) {
String singName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
String singAlbum = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
Long _size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE));
int duration = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));
String url = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));
listBeans.add(new SingListBean(flag++, singName, singAlbum, _size, duration, url));
}
return listBeans;
}

Android's Media Scanner: How do I remove files?

I'm writing an app that removes files that may or may not be listed in any one of the types of media libraries such as music or pictures. While I can use the MediaScannerConnection.scanFile method to add files to the media library there doesn't seem to be any call to notify the service that the file has been removed. Sending it the path of the file that no longer exists doesn't result in the desired behavior either. How should I go about removing items from the library that no longer exist on the Android storage?
I was able to put a method together using bits and pieces from these two questions
What is the String 'volumeName' argument of MediaStore.Audio.Playlists.Members.getContentUri referring to?
How can I refresh MediaStore on Android?
Basically I just run a query on each one of the MediaStore types (Audio, Video and Images) selecting by path and deleting any records I find.
public static void RemoveAllForPaths(String[] paths, Context context)
{
private static final String[] FIELDS = { MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.TITLE };
if(paths == null || paths.length == 0) return;
String select = "";
for(String path : paths)
{
if(!select.equals("")) select += " OR ";
select += MediaStore.MediaColumns.DATA + "=?";
}
Uri uri;
Cursor ca;
uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
ca = context.getContentResolver().query(uri, FIELDS, select, paths, null);
for(ca.moveToFirst(); !ca.isAfterLast(); ca.moveToNext()){
int id = ca.getInt(ca.getColumnIndex(MediaStore.MediaColumns._ID));
uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
context.getContentResolver().delete(uri, null, null);
}
ca.close();
// More of the same just setting the URI to Video and Images
}
I'm not entirely sure how safe this is to do but it's the only solution I've found so far and some initial testing seems to be working. I invite others to submit other answers if anyone has any further information on this approach or a better method for performing this functionality.
Answer of Spencer Ruport is right, but you don't need to query and open a cursor in order to delete.
So for one file that is music file the code is simple like that:
public void DeleteMP3FromMediaStore( Context context, String path )
{
Uri rootUri = MediaStore.Audio.Media.getContentUriForPath( path );
context.getContentResolver().delete( rootUri,
MediaStore.MediaColumns.DATA + "=?", new String[]{ path } );
}
P.S. I wanted to comment answer of Spencer Ruport but don't have enough reputation yet.
Easy as pie: whenever you add a file, let MediaStore ContentProvider knows about it using
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(fileToAddInMediaStore)));
For deletion: just use
getContentResolver().delete(Uri.fromFile(fileToDeleteFromMediaStore), null, null)
The following works well for me. You can delete or add files using this.
MediaScannerConnection.scanFile(
context,
new String[]{fileToDelete, fileToAdd},
null, null);
The available method is to remove the item from library.
This post is detailed expressed how to add into or remove from the Media Library.
http://androidyue.github.io/blog/2014/01/19/scan-media-files-in-android/ Hopes this could help you.

Categories

Resources