I'm retrieving list of distinct folders list having video files with number of videos in each folder, and this is working fine in devices having Android P and below, but when I run on devices having Android Q the app crashes.
How can I make it work for devices running Android Q
java.lang.IllegalArgumentException: Invalid column DISTINCT
bucket_display_name
Logcat:
java.lang.IllegalArgumentException: Invalid column DISTINCT bucket_display_name
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:170)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:140)
at android.content.ContentProviderProxy.query(ContentProviderNative.java:423)
at android.content.ContentResolver.query(ContentResolver.java:944)
at android.content.ContentResolver.query(ContentResolver.java:880)
at android.content.ContentResolver.query(ContentResolver.java:836)
at com.aisar.mediaplayer.fragments.VideoFolderFragment$MediaQuery.getAllVideo(VideoFolderFragment.java:364)
at com.aisar.mediaplayer.fragments.VideoFolderFragment$VideosLoader.loadVideos(VideoFolderFragment.java:434)
at com.aisar.mediaplayer.fragments.VideoFolderFragment$VideosLoader.access$1100(VideoFolderFragment.java:413)
at com.aisar.mediaplayer.fragments.VideoFolderFragment$5.run(VideoFolderFragment.java:189)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:289)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
My Code:
public class MediaQuery {
private Context context;
private int count = 0;
private Cursor cursor;
List<ModelVideoFolder> videoItems;
public MediaQuery(Context context) {
this.context = context;
}
public List<ModelVideoFolder> getAllVideo(String query) {
String selection = null;
String[] projection = {
"DISTINCT " + MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
MediaStore.Video.Media.BUCKET_ID
};
cursor = context.getContentResolver().query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
null,
query);
videoItems = new ArrayList<>();
ModelVideoFolder videoItem;
while (cursor.moveToNext()) {
videoItem = new ModelVideoFolder(
"" + cursor.getString(1),
"" + cursor.getString(0),
"",
"",
"" + getVideosCount(cursor.getString(1))
);
videoItems.add(videoItem);
}
return videoItems;
}
public int getVideosCount(String BUCKET_ID) {
int count = 0;
String selection = null;
String[] projection = {
MediaStore.Video.Media.BUCKET_ID,
};
Cursor cursor = getActivity().getContentResolver().query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
null,
null);
while (cursor.moveToNext()) {
if (BUCKET_ID.equals(cursor.getString(0))) {
//add only those videos that are in selected/chosen folder
count++;
}
}
return count;
}
}
This is due to the restrictions in Android Q.
In Android Q the projection must contain only valid column names without additional statements. Is not possible anymore to embed any type of SQL statement in the projection.
So, projections such as "DISTINCT " + YourColumName, or even trying to make a column alias such as "ExistingColumnName AS AnotherName" will always fail.
The workaround is to perform multiple queries (cursors) to get your required metrics, and construct with the results a CursorWrapper or MatrixCursor.
See the next issue link, where is stated this behavior as expected, since is part of the improved storage security model in Q:
https://issuetracker.google.com/issues/130965914
For your specific problem, a solution could be as next:
First query for a cursor to obtain the list of the BUCKET_ID values where all the videos are located. In the selection you can filter to target only video files by using MediaStore.Files.FileColumns.MEDIA_TYPE = MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO
With the retrieved cursor, iterate all the BUCKET_ID values to perform individual queries per bucket and retrieve the video records, from which you can resolve the count. While iterating keep track of each BUCKET_ID and skip any already queried. And don't forget to also perform the same MEDIA_TYPE filter selection, to avoid querying none-video files that may reside in the same bucket.
Try the next snippet based in your question code, I haven't test it but you may get an idea about how to proceed:
public static class MediaQuery
{
#NonNull
public static HashMap<String, ModelVideoFolder> get(#NonNull final Context context)
{
final HashMap<String, ModelVideoFolder> output = new HashMap<>();
final Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
final String[] projection = {MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
MediaStore.Video.Media.BUCKET_ID};
try (final Cursor cursor = context.getContentResolver().query(contentUri,
projection, null, null, null))
{
if ((cursor != null) && (cursor.moveToFirst() == true))
{
final int columnBucketName = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_DISPLAY_NAME);
final int columnBucketId = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_ID);
do
{
final String bucketName = cursor.getString(columnBucketName);
final String bucketId = cursor.getString(columnBucketId);
if (output.containsKey(bucketId) == false)
{
final int count = MediaQuery.getCount(context, contentUri, bucketId);
final ModelVideoFolder item = new ModelVideoFolder(
bucketName, bucketId, null, null, count);
output.put(bucketId, item);
}
} while (cursor.moveToNext());
}
}
return output;
}
private static int getCount(#NonNull final Context context, #NonNull final Uri contentUri,
#NonNull final String bucketId)
{
try (final Cursor cursor = context.getContentResolver().query(contentUri,
null, MediaStore.Video.Media.BUCKET_ID + "=?", new String[]{bucketId}, null))
{
return ((cursor == null) || (cursor.moveToFirst() == false)) ? 0 : cursor.getCount();
}
}
}
The DISTINCT keyword actually belongs to the SELECT statement, not to a column. For example SELECT DISTINCT Country, Name FROM CountriesTable. Therefore adding DISTINCT to a column projection is a hack which randomly worked in the previous Android versions and probably stopped working in Android 10 due to some changes. Since the ContentResolver doesn't allow raw queries, you just have to filter unique folders inside your code, e. g. by using a HashSet.
I was facing the same problem. DISTINCT keyword doesn't work in Android 10, use hashset for distinct.
Related
Here, this renameFile(..) func is working in Android API 30. But, it is not working in Android API 29 and shows the error like :
java.lang.IllegalArgumentException: Movement of content://media/external/file/116 which isn't part of well-defined collection not allowed
Update-Note:
---Begins---
In-order to work with sdk-29 we have to use Uri as extUri = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL) like:
private static Uri extUri = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL);
in place of below code. And also update MediaStore.Files.FileColumns to MediaStore.Downloads
---Ends---
Uri extUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
String relativeLocation = Environment.DIRECTORY_DOWNLOADS + File.separator + "AppFolder";
function renameFile(...)
boolean renameFile(Context context, String newName, String displayName) {
try {
Long id = getIdFromDisplayName(displayName);
ContentResolver contentResolver = context.getContentResolver();
Uri mUri = ContentUris.withAppendedId(extUri, id);
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 1);
contentResolver.update(mUri, contentValues, null, null);
contentValues.clear();
contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, newName);
// contentValues.put(MediaStore.Files.FileColumns.MIME_TYPE, "files/pdf");
// contentValues.put(MediaStore.Files.FileColumns.RELATIVE_PATH, relativeLocation);
// contentValues.put(MediaStore.Files.FileColumns.TITLE, "SomeName");
// contentValues.put(MediaStore.Files.FileColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
// contentValues.put(MediaStore.Files.FileColumns.DATE_TAKEN, System.currentTimeMillis());
contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 0);
contentResolver.update(mUri, contentValues, null, null);
return true;
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
function getIdFromDisplayName(...)
#RequiresApi(api = Build.VERSION_CODES.Q)
Long getIdFromDisplayName(String displayName) {
String[] projection;
projection = new String[]{MediaStore.Files.FileColumns._ID};
// TODO This will break if we have no matching item in the MediaStore.
Cursor cursor = getContentResolver().query(extUri, projection,
MediaStore.Files.FileColumns.DISPLAY_NAME + " LIKE ?", new String[]{displayName}, null);
assert cursor != null;
cursor.moveToFirst();
if (cursor.getCount() > 0) {
int columnIndex = cursor.getColumnIndex(projection[0]);
long fileId = cursor.getLong(columnIndex);
cursor.close();
return fileId;
}
return null;
}
java.lang.IllegalArgumentException: Movement of content://media/external/file/116 which isn't part of well-defined collection not allowed
So it is for Android Q not allowed if you use the collection;
Uri extUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
But is is allowed for a 'well-defined collection' like:
Uri extUri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
// Use "Pictures/MyFolder" for RELATIVE_PATH
I leave it to you to find other well-defined collections.
Why this is only for Android Q i dont know.
You can see the message in the java file: https://android.googlesource.com/platform/packages/providers/MediaProvider/+/refs/heads/master/src/com/android/providers/media/MediaProvider.java
Quote:
// We only support movement under well-defined collections
switch (match) {
case AUDIO_MEDIA_ID:
case VIDEO_MEDIA_ID:
case IMAGES_MEDIA_ID:
case DOWNLOADS_ID:
break;
default:
throw new IllegalArgumentException("Movement of " + uri
+ " which isn't part of well-defined collection not allowed");
}
If the rename fails use SAF (as mentioned before). How to rename a file in Android knowing only its media content Uri
I had to face the rename problem myself (Android 29) and the solution above did not suffice.
This was because I had a physical SD card on which were located the
files I wanted to rename.
Then, instruction:
extUri = MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
did not work; instead,I had to:
list the "external volumes"(according to Android terms)
Set<String> lVls = MediaStore.getExternalVolumeNames(this);
..which gave me 2 volumes:
"external_primary" (the built-in external storage)
"bc21-eafa" (the SD card external storage)
Initialize 'extUri' with that second value, like that:
extUri = MediaStore.Audio.Media.getContentUri("bc21-eafa");
Apply the rest of the procedure as described in this article.
Thanks to all !
Because of the Android Privacy Changes in Android Q (API29) it's not possible to add songs to a Playlist and it throws this error message when adding a track.
java.lang.SecurityException: com.mp3player.mp3player has no access to content://media/external_primary/audio/media/117
I know that we can this catch this as a RecoverableSecurityException and grant permission for each file individually.
But this is really a hassle and i'm wondering if there is another way.
I already found a post from User #Theo with the same exact problem but without any answers.
Code for adding songs to a playlist
public void addToPlayList(Context c, long songID, long playListID, String playlistName) {
Uri playListUri = MediaStore.Audio.Playlists.Members.getContentUri("external", playListID);
String[] columns = {
MediaStore.Audio.Playlists.Members.AUDIO_ID,
MediaStore.Audio.Playlists.Members.PLAY_ORDER,
};
ContentResolver resolver = c.getContentResolver();
Cursor cursor = resolver.query(playListUri, columns, null, null, null);
int playOrder = 0;
if (cursor != null) {
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
playOrder = cursor.getInt(0) + 1;
}
cursor.close();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, songID);
contentValues.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, playOrder);
resolver.insert(playListUri, contentValues);
resolver.notifyChange(Uri.parse("content://media"), null);
}
EDIT
Looks like the thrown Exception is not an instance of RecoverableSecurityException so how do we get access to that content?
you can use playListUri.getpath() and solve it.
I am having trouble trying to delete a row of data with a specific ID, instead of deleting all the data that is in my database through a content provider.
Here is the delete content provider method:
#Override
public int delete(#NonNull Uri uri, #Nullable String selection,
#Nullable String[] selectionArgs) {
switch(sUriMatcher.match(uri))
{
case CARD:
selection = (selection == null) ? "1" : selection;
break;
case CARD_WITH_ID:
long id = ContentUris.parseId(uri);
selection = String.format("%s = ?", Contract.Columns._ID);
selectionArgs = new String[]{String.valueOf(id)};
break;
default:
throw new IllegalArgumentException("Illegal delete URI");
}
SQLiteDatabase db = mCardSQLite.getWritableDatabase();
int count = db.delete(Contract.TABLE_NAME, selection, selectionArgs);
if(count > 0)
{
//Notify observers of the change
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
I'm trying to get the CARD_WITH_ID section of the switch statement to run. I can't understand why it is not.
Here is the query I'm trying to test this with
getContentResolver().delete(CardProvider.Contract.CONTENT_URI,CardProvider.Contract.Columns._ID ,null);
How can I select only one column and not all of them like I'm doing in the code above?
yes, the URI you provided to .delete() is incorrect. That means the content of uri from sUriMatcher.match(uri) you provide is CardProvider.Contract.CONTENT_URI. That URI is not URI for specific id and of course will match to CARD section
to make .delete() go into CARD_WITH_ID you need to do this:
long id = 1; //id of row that you want to delete
Uri deleteUri = ContentUris.withAppendedId(CardProvider.Contract.CONTENT_URI, id);
//notice code below, not necessary to provide where and selectionArgs as CARD_WITH_ID section wil override it
getContentResolver().delete(uri, null, null);
And make sure to register that URI in sUriMatcher
static {
...
sUriMatcher.addURI(YOUR_CONTENT_AUTHORITY, YOUR_PATH + "/#", CARD_WITH_ID);
I'm developing camera app now and can record videos in app folder successfully.
Btw when I read videos from app folder with Cursor, it doesn't refresh cursor. (It means when I record video with app and whenever I want to view videos via app, it shows old videos. even when I close app and re-open app it shows old videos. )
Here's source code.
public static List<MyVideo> getAppVideos(Context context) {
final String[] projection = { MediaStore.Video.Media.DATA, MediaStore.Video.Media.DATE_TAKEN, MediaStore.Video.Media.DURATION};
final String selection = MediaStore.Images.Media.BUCKET_ID + " = ?";
final String[] selectionArgs = { APP_VIDEO_BUCKET_ID };
final Cursor cursor = context.getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
null);
ArrayList<MyVideo> result = new ArrayList<MyVideo>(cursor.getCount());
if (cursor.moveToFirst()) {
final int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA);
final int dateColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_TAKEN);
final int durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
do {
final String data = cursor.getString(dataColumn);
final String createdDate = cursor.getString(dateColumn);
final String durationTime = cursor.getString(durationColumn);
result.add(new MyVideo(data, DateUtils.convertToSeconds(durationTime), "^ " + DateUtils.getDateFromMilliSeconds(Long.parseLong(createdDate), "MMM dd, yyyy")));
} while (cursor.moveToNext());
}
cursor.close();
return result;
}
Could anyone please suggest solution?
If CursorDemo extends CursorAdapter, then you have to use adapter.swapCursor(cursor_update);
That should swap the old cursor out for the new one and reload the data. With swapCursor,since the old cursor is not closed.
So I have this simple method for getting all words from the dictionary:
public static void getFromDictionary(Activity a) {
System.out.println("Inside getFromDictionary");
ContentResolver resolver = a
.getContentResolver();
String[] projection = new String[] { UserDictionary.Words._ID,
UserDictionary.Words.WORD, UserDictionary.Words.FREQUENCY };
Cursor cursor = resolver.query(UserDictionary.Words.CONTENT_URI,
projection, null, null, null);
while (cursor.moveToNext()) {
String word = cursor.getString(1);
Log.e("DICCIONARIO", word);
}
}
Well, I tried it in different phones and the while bucle won't enter (there is nothing in the cursor).
What am I doing wrong?
Try to limit your projection list by only one item as UserDictionary.Words._ID
or put null as projection parameter