How to refresh Android's MediaStore upon photo deletion - android

Question: how to make the media store to refresh its entry of a DELETED file?
After deleting a photo in code from the external storage, I still see a slot for the deleted photo in the gallery - blank photo.
It seems that the gallery reflects the media store and the deleted photo is found in the media store until the phone is restarted or generally - until the media is rescanned.
Trying to scan the deleted file did not help scanning deleted files (works just for new or existing files): MediaScannerConnection.scanFile(Application.get(), new String[]{file.getPath()}, null, null) (I tried scanning the parent folder as well).
Also tried ACTION_MEDIA_SCANNER_SCAN_FILE to no avail. Example: Application.get().sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)))
Sending a broadcast receiver to rescan the entire external storage (thus refreshing the media store)did the trick: Application.get().sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.fromFile(Environment.getExternalStorageDirectory())))
BUT, it seems that Android, as of 4.4, throws a security exception when trying to manually send the ACTION_MEDIA_MOUNTED system broadcast. See #CommonsWare's post: http://commonsware.com/blog/2013/11/06/android-4p4-permission-regressions.html
So, I'm stuck with no solution for refreshing the media store upon file(/photo/video/etc.) deletion.

I found the following that works for me in 4.4 on a Nexus 10.
// request scan
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, SELECT_PICTURE);
Intent scanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
scanIntent.setData(Uri.fromFile(refreshFile));
sendBroadcast(scanIntent);
"refreshFile" is the file I deleted that I get from my String "fPath" and then convert it to a file.
String filePath = fPath;
File refreshFile = new File(filePath);

I had the same issue. I wrote the following code and it worked on all versions from lollipop to oreo. I also called the mediastore.scanfile() method to ensure that MediaStore is updated. Adding the code below - you might not want to use the "delete()" method in future as the scanfile() might be comprehensive. But, if you want to support older phones then delete() would probably be safer.
// fileID == MediaStore.Images.Media._ID; for the file when you get the file from the content
// resolver
public static boolean deleteCREntryForFilePath(Context context, String filePath, long fileID) {
boolean fDeleted = false;
ContentResolver cr = context.getContentResolver();
int rowsDeleted = 0;
Uri imageURI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
String deleteStr = MediaStore.Images.Media._ID + "=" + fileID;
MediaScannerConnection.scanFile(context, new String[]{filePath}, null, null);
// Remove entry from database
rowsDeleted = context.getContentResolver().delete(
imageURI, deleteStr, null);
if (rowsDeleted > 0)
fDeleted = true;
return(fDeleted);
}
Here is the code to get the file-id (name of the function is getfileId()) . It works for different file-types. You cannot compile the code as it is because it uses an internal object-type but you should be easily able to convert this for generic use.
public static String[] getCombinedEntityColumns(Constants.DELASHARE_OBJECT_TYPES objType) {
String[] entityColumns = new String[5];
switch (objType) {
case DELASHARE_OBJECT_PICTURE:
case DELASHARE_OBJECT_MUSIC:
case DELASHARE_OBJECT_VIDEO: {
entityColumns[0] = MediaStore.Images.Media.DISPLAY_NAME;
entityColumns[1] = MediaStore.Images.Media.DATA;
entityColumns[2] = MediaStore.Images.Media._ID;
entityColumns[3] = MediaStore.Images.Media.DATE_ADDED;
//entityColumns[3] = MediaStore.Images.Media.DATE_TAKEN;
entityColumns[4] = MediaStore.Images.Media.SIZE;
break;
}
case DELASHARE_OBJECT_APK:
case DELASHARE_OBJECT_DOCUMENT:
case DELASHARE_OBJECT_DOWNLOAD:
case DELASHARE_OBJECT_SEARCH_RESULTS:
default: {
entityColumns[0] = MediaStore.Files.FileColumns.DISPLAY_NAME;
entityColumns[1] = MediaStore.Files.FileColumns.DATA;
entityColumns[2] = MediaStore.Files.FileColumns._ID;
entityColumns[3] = MediaStore.Files.FileColumns.DATE_MODIFIED;
entityColumns[4] = MediaStore.Files.FileColumns.SIZE;
break;
}
}
return (entityColumns);
}
public static Uri getCategoryUri(Constants.DELASHARE_OBJECT_TYPES categoryObjType) {
Uri objUri = null;
switch(categoryObjType) {
case DELASHARE_OBJECT_PICTURE:
objUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
break;
case DELASHARE_OBJECT_VIDEO:
objUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
break;
case DELASHARE_OBJECT_MUSIC:
objUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
break;
case DELASHARE_OBJECT_DOWNLOAD: {
File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
objUri = Uri.fromFile( downloadDir);
//objUri = MediaStore.Files.getContentUri("external");
break;
}
case DELASHARE_OBJECT_APK:
case DELASHARE_OBJECT_DOCUMENT:
case DELASHARE_OBJECT_SEARCH_RESULTS:
default:
objUri = MediaStore.Files.getContentUri("external");
break;
}
return(objUri);
}
public static long getFileId(Context context, String dirPath, String filePath, String fileName, Constants.DELASHARE_OBJECT_TYPES objType) {
boolean fIDFound = false;
long id = 0;
if (!fIDFound) {
String sortOrder = null;
String[] entityColumns = getCombinedEntityColumns(objType);
Uri categoryUri = getCategoryUri(objType);
String selection = null;
String[] selectionArgs = new String[]{Constants.DELA_PERCENT_STR + dirPath};
ContentResolver cr = context.getContentResolver();
Cursor cursor = null;
switch (objType) {
case DELASHARE_OBJECT_PICTURE:
selection = MediaStore.Images.Media.DATA + " LIKE ?";
break;
case DELASHARE_OBJECT_VIDEO:
selection = MediaStore.Video.Media.DATA + " LIKE ?";
break;
case DELASHARE_OBJECT_DOCUMENT:
default:
selection = MediaStore.Files.FileColumns.DATA + " LIKE ?";
break;
}
cursor = cr.query(
categoryUri,
entityColumns,
selection,
selectionArgs,
sortOrder);
if (cursor != null && cursor.moveToFirst()) {
id = cursor.getLong(cursor.getColumnIndex(entityColumns[2]));
if (id != 0) {
fIDFound = true;
}
}
if (cursor != null) {
cursor.close();
cursor = null;
}
}
return(id);
}

I had the same question as you now that the sendBroadcast approach is disallowed in 4.4 and found a good solution here using the Media Store content provider: https://stackoverflow.com/a/20780472/1060805
I tested it out on Android 4.4 and it works nicely. I think it is a solid approach.

Try
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"+ Environment.getExternalStorageDirectory())));
Add this to your manifest:
<intent-filter>
<action android:name="android.intent.action.MEDIA_MOUNTED" />
<data android:scheme="file" />
</intent-filter>

Related

Why is DocumentsContract.getTreeDocumentId(uri) giving me the doc ID for a parent of the Uri argument?

The app sends the user to SAF picker with ACTION_OPEN_DOCUMENT_TREE:
void openStoragePicker() {
String messageTitle = "Choose directory app to use";
Intent intent = new Intent(ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(Intent.createChooser(intent, messageTitle), Dry.REQUEST_CHOOSE_APP_DIR);
}
In onActivityResult, we take persistable permission and store a String of the Uri:
#Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
switch (resultCode) {
case Activity.RESULT_OK:
if (requestCode == Dry.REQUEST_CHOOSE_APP_DIR) {
if (resultData == null) {
Log.d(Dry.TAG, "result data null");
} else {
if (resultData.getData() != null) {
Uri uri = resultData.getData();
Storage.releasePersistedPermissions(this);
getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
Storage.setSharedPrefString(uri.toString(), Storage.SHARED_PREF_APP_DIR_URI, this);
dbw.clearAlreadyPlayed();
}
}
}
break;
case Activity.RESULT_CANCELED:
//
break;
}
}
We recreate the tree Uri when we need it:
static Uri getTheDir(Context context) {
String result = Storage.getSharedPrefString(SHARED_PREF_APP_DIR_URI, context);
if (result == DEFAULT_SHARED_PREF_STRING) {
return null;
}
Uri dirUriParsed = Uri.parse(Uri.decode(result));
Log.d(Dry.TAG, "the dir uri parsed: " + dirUriParsed.toPath());
return dirUriParsed;
}
We want to list the files, and we can, using the pattern shown here.
static ArrayList<String> getFiles(Context context) {
ArrayList<String> fileStrings = new ArrayList<>();
Uri rootUri = getTheDir(context);
if (rootUri == null) {
return fileStrings;
}
long startTime = System.currentTimeMillis();
ContentResolver contentResolver = context.getContentResolver();
String theDocToReturnChildrenFor = DocumentsContract.getTreeDocumentId(rootUri);
Log.d(Dry.TAG, "theDocToReturnChildrenFor: " + theDocToReturnChildrenFor);
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, theDocToReturnChildrenFor);
List<Uri> dirNodes = new LinkedList<>();
dirNodes.add(childrenUri);
while(!dirNodes.isEmpty()) {
childrenUri = dirNodes.remove(0);
Cursor c = contentResolver.query(childrenUri, new String[]{DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_MIME_TYPE}, null, null, null);
try {
while (c.moveToNext()) {
final String docId = c.getString(0);
final String name = c.getString(1);
final String mime = c.getString(2);
if (isDirectory(mime)) {
if (Arrays.asList(SUBDIRECTORIES_TO_OMIT).contains(name)) {
continue;
}
final Uri newNode = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, docId);
dirNodes.add(newNode);
} else {
for (String ext: SUPPORTED_FILE_EXTENSIONS) {
if (name.endsWith(ext)) {
fileStrings.add(docId);
break;
}
}
}
}
} finally {
closeQuietly(c);
}
}
Log.d(Dry.TAG, "fileStrings length: " + fileStrings.size() + "time spent building song list: " + ((System.currentTimeMillis() - startTime) / 1000.0) + "s");
return fileStrings;
}
But, this only works as expected when the directory happens to be a top-level directory within the storage volume. If the directory that the user chose is not a direct child of the volume root, then, when we try DocumentsContract.getTreeDocumentId(rootUri), it returns not the document ID for that URI, but rather the document ID for its highest parent before the volume root!
The log call that prints the reconstructed Uri gives this output:
the dir uri parsed: /tree/primary:a test dir/a child test dir/3rd level dir
But the other log call that prints the doc ID prints this:
theDocToReturnChildrenFor: primary:a test dir
Am I doing it wong? Is this an Android bug? I noticed this question describes the exact same behavior from this method. That issue was solvable by following the established recursive listing pattern, but, that user says:
It is almost like getTreeDocumentId(rootUri) is returning what getRootId(rootUri) should be returning.
The docs for this method are not helpful, they are brief and have a typo, leaving the meaning unclear. DocumentsContract.getTreeDocumentId docs.
Target SDK of the app is 30. The device Android version is also api 30 (Android 11).
If someone could help me to get the correct doc ID for the user-selected directory, I would appreciate it.
Uri dirUriParsed = Uri.parse(Uri.decode(result))
Try:
Uri dirUriParsed = Uri.parse(result)

Alternative to MediaStore.Video.Media.EXTERNAL_CONTENT_URI for multiple videos

I'm basically trying to view all the photos and videos in a given folder from my Camera app, when the preview is clicked. So basically the user will view the latest photo while having the ability to swipe to view other photos that were previously taken. I have achieved this for photos, but am unable to do so for videos. There is only one URI related to videos in MediaStore class called MediaStore.Video.Media.EXTERNAL_CONTENT_URI that displays a given video (no way to access other photos/videos) unlike MediaStore.Images.Media.EXTERNAL_CONTENT_URI which is used to open the gallery while having access to multiple photos. Is there any other way I can show all the videos in a given folder while focusing on the latest video?
Code to open gallery
public void openGallery() {
String mediaId = "";
String[] projection = new String[] {
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME
};
final String fileName = config.getLatestMediaFile().getName();
Uri mediaUri;
if(videoCapturer.isLatestMediaVideo()){
mediaUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else {
mediaUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
Log.i(TAG, "MediaURI: " + mediaUri.getPath());
Cursor cursor = getContentResolver().query(
mediaUri, projection, null, null, null);
if(cursor != null){
while (cursor.moveToNext()) {
String name = cursor.getString((cursor.getColumnIndex(MediaStore.Images.ImageColumns.DISPLAY_NAME)));
if(name.equals(fileName)){
mediaId = cursor.getString((cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID)));
break;
}
}
cursor.close();
}
if(!mediaId.equals("")){
mediaUri = mediaUri.buildUpon()
.authority("media")
.appendPath(mediaId)
.build();
}
Log.d("TagInfo","Uri: "+mediaUri);
Intent intent = new Intent(Intent.ACTION_VIEW, mediaUri);
startActivity(intent);
}

Unable to set picture as. "No appps can perform this action"

I am trying to give users an option to set image as wallpaper/whatsapp dp like this.
But I'm stuck with this code
Uri sendUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.a_day_without_thinking_mobile);
Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
intent.setDataAndType(sendUri, "image/jpg");
intent.putExtra("mimeType", "image/jpg");
startActivity(Intent.createChooser(intent,"Set As"));
It shows a dialog that no apps can perform this action.
I also tried to check my Uri through this method
ContentResolver cr = getContentResolver();
String[] projection = {MediaStore.MediaColumns.DATA};
Cursor cur = cr.query(sendUri, projection, null, null, null);
if (cur != null) {
if (cur.moveToFirst()) {
String filePath = cur.getString(0);
if (new File(filePath).exists()) {
Log.d("URI: ","File path exist");
} else {
Log.d("URI: ","File not found");
}
} else {
Log.d("URI: ","URI ok but no enty found");
}
cur.close();
} else {
Log.d("URI: ","URI was invalid for some other reason");
}
And It always returned that the URI was invalid. But I'm sure that the image is valid jpg and is present in raw folder.
I tried changing URI paths but to no success.
Android file read format has been changed after targetSdkVersion >= 24
You can find the details here;
https://stackoverflow.com/a/38858040/1367450

android : file Uri to Content Uri. (converting)

I have two type of Uris.
type one :
content://media/external/images/media/465
content://media/external/images/media/466
type two :
file:///storage/emulated/0/DCIM/Camera/20151112_185009.jpg
file:///storage/emulated/0/testFolder/20151112_185010.jpg
What is difference and how to convert file uri to content uri?
Because, file uri is just causing error. When I call method :
ContentResolver contentResolver = getContentResolver();
fis = (FileInputStream) contentResolver.openInputStream(fileTypeUri);
how do I fix this?
Try It :)
public static Uri getImageContentUri(Context context, File file) {
String filePath = file.getAbsolutePath();
Cursor cursor = context.getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[] { MediaStore.Images.Media._ID },
MediaStore.Images.Media.DATA + "=? ",
new String[] { filePath }, null);
if (cursor != null && cursor.moveToFirst()) {
int id = cursor.getInt(cursor
.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/images/media");
return Uri.withAppendedPath(baseUri, "" + id);
} else {
if (file.exists()) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA, filePath);
return context.getContentResolver().insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} else {
return null;
}
}
}
If you're trying to share data that is stored as part of your app with another app you'll need to use a content:// scheme and not a file:// scheme. This can be accomplished using the FileProvider class found here: https://developer.android.com/reference/android/support/v4/content/FileProvider.html.
By using the FileProvider class you can more precisely and more securely define what files your app can share.
Though be aware that external-cache-path and external-files-path don't work despite what the documentation says. See: how to set FileProvider for file in External Cache dir for more info.

Set as Contact Ringtone? Android

I am trying to learn how to add set as contact ringtone feature. I already know how to set default ringtone but I can't figure how to set as contact ringtone.
I got to the part where I choose contact, but I don't know how to assign ringtone to that contact.
That part is bugging me and I can't seem to find answer in questions that were already asked on this topic.
Here is my code so far:
static public final int CONTACT_CHOOSER_ACTIVITY_CODE = 73729;
private File csound;
private final File rpath = new File(Environment.getExternalStorageDirectory() + "/Ringtone sounds/Ringtones");
#Override
public void onClick(View v) {
setContRing();
}
private void setContRing() {
Boolean success = false;
csound = new File(rpath, FNAME);rpath.mkdirs();
if (!csound.exists()) {
try {
InputStream in = getResources().openRawResource(FPATH);
FileOutputStream out = new FileOutputStream(csound.getPath());
byte[] buff = new byte[1024];
int read = 0;
try {
while ((read = in.read(buff)) > 0) {
out.write(buff, 0, read);
}
} finally {
in.close();
out.close();
}
} catch (Exception e) {
success = false;
}
} else {
success = true;
setContRingtone();
}
if (!success) {
setContRingtone();
}
}
private void setContRingtone() {
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType(ContactsContract.Contacts.CONTENT_TYPE);
startActivityForResult(intent, CONTACT_CHOOSER_ACTIVITY_CODE);
}
});
}
Edit for bounty: I am wondering if someone can show me how to do so, I tried with codes found in other questions but I couldn't apply them to my code. I can copy file but how to get contact and assign ringtone to that contact?
From set custom ringtone to specific contact number
Android has a special column for this: ContactsContract.CUSTOM_RINGTONE.
So, you could use ContactsContract.Contacts.getLookupUri to get your contact's Uri, after that pretty much all that's left is to call ContentResolver.update.
Here's an example of looking up a contact by their phone number, then applying a custom ringtone:
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.PhoneLookup;
// The Uri used to look up a contact by phone number
final Uri lookupUri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, "012-345-6789");
// The columns used for `Contacts.getLookupUri`
final String[] projection = new String[] {
Contacts._ID, Contacts.LOOKUP_KEY
};
// Build your Cursor
final Cursor data = getContentResolver().query(lookupUri, projection, null, null, null);
data.moveToFirst();
try {
// Get the contact lookup Uri
final long contactId = data.getLong(0);
final String lookupKey = data.getString(1);
final Uri contactUri = Contacts.getLookupUri(contactId, lookupKey);
if (contactUri == null) {
// Invalid arguments
return;
}
// Get the path of ringtone you'd like to use
final String storage = Environment.getExternalStorageDirectory().getPath();
final File file = new File(storage + "/AudioRecorder", "hello.mp4");
final String value = Uri.fromFile(file).toString();
// Apply the custom ringtone
final ContentValues values = new ContentValues(1);
values.put(Contacts.CUSTOM_RINGTONE, value);
getContentResolver().update(contactUri, values, null, null);
} finally {
// Don't forget to close your Cursor
data.close();
}
Also, you'll need to add both permissions to read and write contacts:
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
To extend a bit on this, and how to modify it to your need, change phone number 012-345-6789 in this line to the one you are looking for
// The Uri used to look up a contact by phone number
final Uri lookupUri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, "012-345-6789");
And set your default CUSTOM_RINGTONE in your phone ContactsContract. There is another, similar, option here:
Setting contact custom ringtone, how?

Categories

Resources