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);
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 creating an inventory app.It has a fab button an menu button with two items.Everything works fine but whenever I am trying to insert a new product through the item from the menu.It doesn't add immediately.
First I have to go back from the application or kill it and after when I enter again the the new product with the dummy value is recorded there.I want to resolve the issue that when I add a new product it immediately add the data with values and I don't have to go back every time to see it is inserted.Also If added a new product using fab button the data I added previously using the insert item is being added and displayed.
I have seen the notification uri and there is no error showing in the LOGCAT
ProductProvider.java
package com.example.bahubali.inventoryapp.data;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
public class ProductProvider extends ContentProvider {
//Tag for the log messages
public static final String LOG_TAG = ProductProvider.class.getSimpleName();
//Database helper that will provide access to the database.
private ProductDbHelper mDbHelper;
/** URI matcher code for the content URI for the product table */
public static final int PRODUCTS = 100;
/** URI matcher code for the content URI for a single product in the pets table */
public static final int PRODUCT_ID = 101;
/** URI matcher object to match a context URI to a corresponding code.
* The input passed into the constructor represents the code to return for the root URI.
* It's common to use NO_MATCH as the input for this case.
*/
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// Static initializer. This is run the first time anything is called from this class.
static {
// The calls to addURI() go here, for all of the content URI patterns that the provider
// should recognize. All paths added to the UriMatcher have a corresponding code to return
// when a match is found.
sUriMatcher.addURI(ProductContract.CONTENT_AUTHORITY, ProductContract.PATH_PRODUCTS, PRODUCTS);
// The content URI of the form "content://com.example.android.pets/pets/#" will map to the
// integer code {#link #PETS_ID}. This URI is used to provide access to ONE single row
// of the pets table.
sUriMatcher.addURI(ProductContract.CONTENT_AUTHORITY, ProductContract.PATH_PRODUCTS + "/#", PRODUCT_ID);
}
/**
* Initialize the provider and the database helper object.
*/
#Override
public boolean onCreate()
{
/*
* Creates a new helper object. This method always returns quickly.
* until SQLiteOpenHelper.getWritableDatabase is called
*/
mDbHelper = new ProductDbHelper(getContext());
return true;
}
/*
*Perform the query to the given URI.Use the given projection,selection,selectionArgs, and sort
* order.
*/
#Override
public Cursor query(#NonNull Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
// Get readable database
SQLiteDatabase database = mDbHelper.getReadableDatabase();
// This cursor will hold the result of the query
Cursor cursor ;
// Figure out if the URI matcher can match the URI to a specific code
int match = sUriMatcher.match(uri);
switch (match) {
case PRODUCTS:
// For the PRODUCTS code, query the pets table directly with the given
// projection, selection, selection arguments, and sort order. The cursor
// could contain multiple rows of the products table.
// Perform database query on pets
cursor = database.query(ProductContract.ProductEntry.TABLE_NAME,
projection,selection,selectionArgs,null,null,sortOrder);
break;
case PRODUCT_ID:
// For the PRODUCT_ID code, extract out the ID from the URI.
// For an example URI such as "content://com.example.android.products/products/3",
// the selection will be "_id=?" and the selection argument will be a
// String array containing the actual ID of 3 in this case.
//
// For every "?" in the selection, we need to have an element in the selection
// arguments that will fill in the "?". Since we have 1 question mark in the
// selection, we have 1 String in the selection arguments' String array.
selection = PRODUCT_ID + "=?";
selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
// This will perform a query on the products table where the _id equals 3 to return a
// Cursor containing that row of the table.
cursor = database.query(ProductContract.ProductEntry.TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
sortOrder);
break;
default:
throw new IllegalArgumentException("Cannot query unknown URI " + uri);
}
//Set notification URI to the cursor,
//So we know what content URI the cursor was created for.
cursor.setNotificationUri(getContext().getContentResolver(),uri);
//If the data at this URI changes, then we need to update the cursor.
return cursor;
}
/*
*Insert a new data into the provider with the given Constructor.
*/
#Nullable
#Override
public Uri insert(#NonNull Uri uri,
#Nullable ContentValues contentValues) {
final int match = sUriMatcher.match(uri);
switch (match){
case PRODUCTS:
return insertProduct(uri,contentValues);
default:
throw new IllegalArgumentException("Insertion is not supported for " +uri);
}
}
/*
Insert a product in the database with the given content values.Return the new content URI
for that specific row in the database.
*/
private Uri insertProduct(Uri uri, ContentValues contentValues){
//Check that the name is not null
String name = contentValues.getAsString(ProductContract.ProductEntry.COLUMN_PRODUCT_NAME);
if (name == null){
throw new IllegalArgumentException("Product requires a name");
}
//Check that the price is not null
Integer price = contentValues.getAsInteger(ProductContract.ProductEntry.COLUMN_PRODUCT_PRICE);
if (price != null && price < 0 ){
throw new IllegalArgumentException("Product requires valid price");
}
//Get writeable database
SQLiteDatabase database = mDbHelper.getWritableDatabase();
//Insert the new product with the given values
long id = database.insert(ProductContract.ProductEntry.TABLE_NAME,null,contentValues);
//If the Id is -1, then the insertion failed.Log on an error to return null
if (id == -1){
Log.e(LOG_TAG,"Failed to insert a row" + uri);
return null;
}
//Notify all listeners that the data has changed for the product content URI
getContext().getContentResolver().notifyChange(uri,null);
return ContentUris.withAppendedId(uri,id);
}
/*
*Updates the data at the given selection and the selection arguments, with the new content
* values.
*/
#Override
public int update(#NonNull Uri uri,
#Nullable ContentValues contentValues,
#Nullable String selection,
#Nullable String[] selectionArgs) {
final int match = sUriMatcher.match(uri);
switch (match) {
case PRODUCTS:
return updateProduct(uri, contentValues, selection, selectionArgs);
case PRODUCT_ID:
// For the PRODUCT_D code, extract out the ID from the URI,
// so we know which row to update. Selection will be "_id=?" and selection
// arguments will be a String array containing the actual ID.
selection = ProductContract.ProductEntry._ID + "=?";
selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
return updateProduct(uri, contentValues, selection, selectionArgs);
default:
throw new IllegalArgumentException("Update is not supported for " + uri);
}
}
/**
* Update products in the database with the given content values. Apply the changes to the rows
* specified in the selection and selection arguments (which could be 0 or 1 or more pets).
* Return the number of rows that were successfully updated.
*/
private int updateProduct(Uri uri,ContentValues contentValues,String selection,String[] selectionArgs){
// If the {#link ProductEntry#COLUMN_PRODUCT_NAME} key is present,
// check that the name value is not null.
if (contentValues.containsKey(ProductContract.ProductEntry.COLUMN_PRODUCT_NAME)) {
String name = contentValues.getAsString(ProductContract.ProductEntry.COLUMN_PRODUCT_NAME);
if (name == null) {
throw new IllegalArgumentException("Product requires a name");
}
}
// If the {#link ProductEntry#COLUMN_PRODUCT_PRICE} key is present,
// check that the price value is valid.
if (contentValues.containsKey(ProductContract.ProductEntry.COLUMN_PRODUCT_PRICE)) {
// Check that the weight is greater than or equal to 0 kg
Integer price = contentValues.getAsInteger(ProductContract.ProductEntry.COLUMN_PRODUCT_PRICE);
if (price!= null && price < 0) {
throw new IllegalArgumentException("Product requires valid weight");
}
}
// If there are no values to update, then don't try to update the database
if (contentValues.size() == 0) {
return 0;
}
// Otherwise, get writeable database to update the data
SQLiteDatabase database = mDbHelper.getWritableDatabase();
//perform the update on the database and the get the number of rows affected
int rowsUpdated = database.update(ProductContract.ProductEntry.TABLE_NAME,contentValues,selection
,selectionArgs);
//If 1 or more rows were updated, then notify all the listeners that the data at the
//given URI has changed
if (rowsUpdated != 0){
getContext().getContentResolver().notifyChange(uri,null);
}
//Return the no. of rows updated
return rowsUpdated;
}
/*
*Delete the data at the given selection and selection arguments.
*/
#Override
public int delete(#NonNull Uri uri,
#Nullable String selection,
#Nullable String[] selectionArgs) {
// Get writeable database
SQLiteDatabase database = mDbHelper.getWritableDatabase();
//Track the no. of rows that were deleted
int rowsDeleted;
final int match = sUriMatcher.match(uri);
switch (match) {
case PRODUCTS:
// Delete all rows that match the selection and selection args
rowsDeleted = database.delete(ProductContract.ProductEntry.TABLE_NAME,selection,selectionArgs);
break;
case PRODUCT_ID:
// Delete a single row given by the ID in the URI
selection = ProductContract.ProductEntry._ID + "=?";
selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
rowsDeleted = database.delete(ProductContract.ProductEntry.TABLE_NAME,selection,selectionArgs);
break;
default:
throw new IllegalArgumentException("Deletion is not supported for " + uri);
}
//If 1 or more rows were deleted, then notify all listeners that the data at the given
//given uri has changed
if (rowsDeleted != 0){
getContext().getContentResolver().notifyChange(uri,null);
}
return rowsDeleted;
}
/*
*Return the MIME type of data for the content URI.
*/
#Nullable
#Override
public String getType(#NonNull Uri uri) {
final int match = sUriMatcher.match(uri);
switch (match){
case PRODUCTS:
return ProductContract.ProductEntry.CONTENT_LIST_TYPE;
case PRODUCT_ID:
return ProductContract.ProductEntry.CONTENT_ITEM_TYPE;
default:
throw new IllegalStateException("UNKNOWN URI" + uri + " with match" + match);
}
}
}
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'.