First up: I have sifted through many of the questions on the topic on SO and have still failed to come up with a correct answer.
Here is a, (way simplified), version of my code:
private static UriMatcher
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int
HOMEWORK_TABLE_REQUEST = 1,
CLASS_TABLE_REQUEST = 2,
SETTINGS_TABLE_REQUEST = 3,
HOMEWORK_ITEM_REQUEST = 4,
CLASS_ITEM_REQUEST = 5,
SETTINGS_ITEM_REQUEST = 6;
static {
uriMatcher.addURI("org.dvc.homeworkReminder.homeworkProvider", "homework", HOMEWORK_TABLE_REQUEST);
uriMatcher.addURI("org.dvc.homeworkreminder.homeworkProvider", "homework/#", HOMEWORK_ITEM_REQUEST);
uriMatcher.addURI("org.dvc.homeworkReminder.homeworkProvider", "class", CLASS_TABLE_REQUEST);
uriMatcher.addURI("org.dvc.homeworkreminder.homeworkProvider", "class/#", CLASS_ITEM_REQUEST);
uriMatcher.addURI("org.dvc.homeworkReminder.homeworkProvider", "settings", SETTINGS_TABLE_REQUEST);
uriMatcher.addURI("org.dvc.homeworkreminder.homeworkProvider", "settings/#", SETTINGS_ITEM_REQUEST);
}
And here is my query method:
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Cursor cursor = null;
Log.i("dfg", "Query method called");
Log.i("dfg", "Uri = " + uri.toString());
Log.i("dfg", "Match: " + uriMatcher.match(uri));
switch(uriMatcher.match(uri)) {
case HOMEWORK_ITEM_REQUEST:
Log.i("dfg", "HOMEWORK_ITEM_REQUEST");
cursor = database.readCursorById(Integer.parseInt(uri.getLastPathSegment()));
break;
case HOMEWORK_TABLE_REQUEST:
Log.i("dfg", "HOMEWORK_TABLE_REQUEST");
cursor = database.readAllHomework();
break;
case CLASS_ITEM_REQUEST:
case CLASS_TABLE_REQUEST:
case SETTINGS_ITEM_REQUEST:
case SETTINGS_TABLE_REQUEST:
cursor = null;
break;
default:
cursor = null;
break;
}
return cursor;
}
I have no implementation for my class requests or settings requests so that's why I'm just defaulting to return null. What's happening is my switch statement is falling all the way through to default:, and causing NPE's galore later on in my code. You'll notice there are 5 Log statements in my code. The following is printed to LogCat. (Why is it called logcat anyways?)
Query method called
Uri = content://org.dvc.homeworkReminder.homeworkProvider/homework/24
Match: -1
Now the uri being tested should match the 2nd pattern I added, correct? I also read about how the Uri.parse() method messes with UriMatcher's wildcards on another thread so I build the above-printed Uri with the following code:
Uri returnUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority("org.dvc.homeworkReminder.homeworkProvider").appendPath("homework").appendPath(String.valueOf(id)).build();
The id variable there depends on some other stuff that isn't really relevant.
My question is why is UriMatcher not working, and how would I go about fixing it?
Fix:
I had incorrect capitalization on the following lines:
uriMatcher.addURI("org.dvc.homeworkreminder.homeworkProvider", "homework/#", HOMEWORK_ITEM_REQUEST);
uriMatcher.addURI("org.dvc.homeworkreminder.homeworkProvider", "class/#", CLASS_ITEM_REQUEST);
uriMatcher.addURI("org.dvc.homeworkreminder.homeworkProvider", "settings/#", SETTINGS_ITEM_REQUEST);
Notice the lowercase r in homeworkreminder. Thanks to Bruce for spotting it!
You're using lower-case 'reminder' when you set up the matcher, but the actual URI has upper-case 'Reminder'.
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 !
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.
I'm trying to create an application to display the call logs based on the call type (incoming calls, outgoing calls, or missing calls). In addition I'm trying to add search and delete functionalities so the user can search for the call (by number) and delete the call. The layout of the app is shown here:
The search functionality works if I hit the 'All' button to display all calls and search by number there, but upon going to other sections to search for a number, such as received or missed, the app crashes.
So far I have managed to run the logcat on Android Studio, and found that the main issue is in my main activity file. I have attached the logcat image here:
There seems to be issues with these pieces of code: my getCalls Method, afterTextChanged Method, and my updateCursor Method.
private Cursor getCalls(int type, String searchString) {
Uri callUri = CallLog.Calls.CONTENT_URI;
ContentResolver contentResolver = getContentResolver();
String[] projection = new String[]{Calls._ID, Calls.NUMBER, Calls.DURATION, Calls.TYPE};
String selection=null;
String[] selectionArgs=null;
if(type != 0){
// filter type calls
selection = Calls.TYPE + "=?";
selectionArgs = new String[]{String.valueOf(type)};
}
if(!TextUtils.isEmpty(searchString)) {
// has some search string
if(TextUtils.isEmpty(selection)) {
// all call types
selection = Calls.NUMBER + " LIKE ?";
selectionArgs = new String[]{"%"+searchString+"%"};
} else {
// some type of call and add search String
selection = selection+" && " + Calls.NUMBER+" LIKE ?";
selectionArgs = new String[]{selectionArgs[0],"'%"+searchString+"%'"};
}
}
String order = Calls.DATE + " DESC ";
//verify permissions to access the user's call log
int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG);
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
cursor = contentResolver.query(callUri, // URI content provider
projection,
selection,
selectionArgs,
order);
}
return cursor;
}
#Override
public void afterTextChanged(Editable s) {
updateCursor();
}
//updates the search
void updateCursor() {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
cursor = null;
}
cursor = getCalls(currentCallType, searchET.getText().toString());
adapter.swapCursor(cursor);
}
Upon running the app, I expected to be able to go to different sections (for example, I go to the received call section) and search for a number, however upon going to the section and tapping on the search bar, the app crashes. I do not understand how there could be issues with these methods.
SQLException says the error is near the & symbol, so the error should be in this line:
selection = selection+" && " + Calls.NUMBER+" LIKE ?";
Let's replace it with this one, because 'AND' should be used in SQL instead of double &:
selection = selection+" AND " + Calls.NUMBER+" LIKE ?";
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);
What is Uri Matcher in android.content.UriMatcher
How to use it?
Can someone please explain meaning of following three line of code?
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(PROVIDER_NAME, "cte", uriCode);
uriMatcher.addURI(PROVIDER_NAME, "cte/*", uriCode);
int res = uriMatcher.match(uri);
UriMatcher is a handy class when you are writing a ContentProvider or some other class that needs to respond to a number of different URIs. In your example, a user could query your provider with URIs such as:
myprovider://cte
or
myprovider://cte/somestring
When you construct a UriMatcher, you need to have separate codes for each URI (not just "uriCode" as in your example). I usually make my UriMatcher instance static, and add the URIs in a static constructor:
private static final int CTE_ALL = 1;
private static final int CTE_FIND = 2;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(PROVIDER_NAME, "cte", CTE_ALL);
uriMatcher.addURI(PROVIDER_NAME, "cte/*", CTE_FIND);
}
Then in your ContentProvider you would do something like this in your query method:
Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
int res = uriMatcher.match(uri);
switch (res) {
case CTE_ALL:
//TODO create a results Cursor with all the CTE results
break;
case CTE_FIND:
//TODO create a results Cursor with the single CTE requested
break;
}
return results;
}
I found the following videos to be useful:
URI Basics
URI Matcher
In essence, what you are trying to do is, have an ID or a number associated to different URIs. When you use addUri, a code/number/ID gets created against the URI. When you request a match(), the corresponding code is returned.
One more thing I wanted to add that wasn't clear for me first time I used UriMatcher.
Is that if you want to parse an HTTP url then as an AUTHORITY parameter in the addURI you need to pass the target domain name. For example:
Uri mUri = Uri.parse("http://example.com/foo");
UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// if not "example.com" below the match will always return -1 result
sURIMatcher.addURI("example.com", "/foo", 123);
int match = sURIMatcher.match(mUri);
UriMatcher Documentation doesn't cover this case and it's not clear what's this authority parameter for.
Gosh if I knew that it would save me some time!