How to match Uri's correctly in ContentProvider? - android

This is what I have, but its failing saying that the table does not exists. I am positive it does incase anyone asks that. But this is some of the code that does that:
What gets called
/*Constants*/
public static final String AUTHORITY = "content://com.smartcal.eventprovider";
private static final int EVENTS_INFO = 1;
baseUri = Uri.withAppendedPath(baseUri, "events_info");
return new CursorLoader(this, baseUri, args.getStringArray("projection"),
args.getString("selection"), args.getStringArray("selectionArgs"), args.getBoolean("sortOrder") ? args.getString("sortOrder") : null );
What matches is
private String getTable(Uri uri) {
String table = "";
switch(sUriMatcher.match(uri)){
case EVENTS_INFO: table = "events_info";
}
return table;
}
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static
{
sUriMatcher.addURI(AUTHORITY, "events_info", 1);
}
And my query() method in custom ContentProvider
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
database = openHelper.getWritableDatabase();
return database.query(getTable(uri), projection, selection, selectionArgs, null, null, null);
}
Error:
06-19 17:34:14.366: E/AndroidRuntime(686): Caused by:
java.lang.IllegalStateException: Invalid tables
EDIT:
When using Alex's IllegalArgumentException, this is what I got back from the error:
06-19 19:17:42.277: E/AndroidRuntime(1134): Caused by:
java.lang.IllegalArgumentException: Unknown URI
content://com.smartcal.eventprovider/events_info
As you can tell, it should match, assuming the sUriMatcher.addURI() method is working correctly.

Your problem might stem from the fact that your code isn't very well organized...
First thing,
private String getTable(Uri uri) {
switch(sUriMatcher.match(uri)) {
case EVENTS_INFO:
return "events_info"; // return
/** PROVIDE A DEFAULT CASE HERE **/
default:
// If the URI doesn't match any of the known patterns, throw an exception.
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
Second thing,
/** Use constants, not raw ints **/
sUriMatcher.addURI(AUTHORITY, "events_info", EVENT_INFO);
Other than that, you might want to provide some more info.

Have you already installed the app and is the table new? If so, and you are not updating the schema in your sql helper class, you need to uninstall the app first. Then install the app in your emulator device. The db is only created once, and the create method in the sql helper will not be called after that first time, even if you are installing or running a new build/apk.

Related

Failed to find provider info for com.example.serialprovider.provider.SampleProvider

I've been trying to get data from another app's custom ContentProvider class but I keep getting this error: Failed to find provider info for com.example.serialprovider.provider.SampleProvider..
I searched a lot for similar issues online but still didn't know what's wrong, I checked the manifest multiple times, and I even took a copy of the authorities attribute to use it in the receiver app but still, the receiver app can't find the provider.
Here's the declaration in the manifest:
<provider
android:name=".provider.SampleProvider"
android:authorities="com.example.serialprovider.provider.SampleProvider"
android:enabled="true"
android:exported="true" />
and here's the implementation of onCreate and query methods in the Provider class (I'm using RoomDatabase):
public class SampleProvider extends ContentProvider {
public SampleProvider() {
}
private static final String AUTHORITY = "com.example.serialprovider.provider.SampleProvider";
private static final String TABLE_NAME = "devicepin";
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(AUTHORITY, TABLE_NAME, 1);
}
#Override
public boolean onCreate() {
return true;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
if (sURIMatcher.match(uri) == 1) {
final Context context = getContext();
AppDao dao = DatabaseClient.getInstance(context).getAppDatabase().appDao();
final Cursor cursor = dao.get();
cursor.setNotificationUri(getContext().getContentResolver(), uri);
cursor.close();
return cursor;
} else {
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
}
and here's how I try to get the cursor in the other app "receiver":
private void getPin(){
new Thread(() -> {
ContentResolver resolver = getContentResolver();
try{
Cursor cursor = resolver.query(Uri.parse("content://com.example.serialprovider.provider.SampleProvider/devciepin"), null, null, null, null);
cursor.close();
}
catch (Exception e){
e.printStackTrace();
}
}).start();
}
cursor is always null, when I surround it with try and catch blocks, the "failed to find provider info" is what I get as an exception.
Turns out the code is alright, but there's some new restrictions that were introduced in Android 11 (API 30) when accessing the ContentProvider from another app.
Quoting the Documentation on Android 11 behavior changes:
If your app shares a content URI with another app, the intent must grant URI access permissions by setting at least one of the following intent flags: FLAG_GRANT_READ_URI_PERMISSION and FLAG_GRANT_WRITE_URI_PERMISSION. That way, if the other app targets Android 11, it can still access the content URI. Your app must include the intent flags even when the content URI is associated with a content provider that your app doesn't own.
If your app owns the content provider that's associated with the content URI, verify that the content provider isn't exported. We already recommend this security best practice.

Global variable defined in Content Provider and initialized in onCreate() is null in query()

I defined a content provider and made my database helper a global variable, which I initialize in the onCreate method. However, the value of this global variable is null, in the query() method.
This is my content provider:
/** Tag for the log messages */
public static final String LOG_TAG = FeedingProvider.class.getSimpleName();
// Declaring a FeedingDbHelper variable;
private FeedingDbHelper mDbHelper;
/** URI matcher code for the content URI for the pets table */
private static final int FEEDINGS = 100;
/** URI matcher code for the content URI for a single pet in the pets table */
private static final int FEEDING_ID = 101;
/**
* UriMatcher object to match a content URI to a corresponding code.
* The input passed into the constructor represents the code to return for the root URI.
*/
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(FeedingContract.CONTENT_AUTHORITY, FeedingContract.PATH_FEEDINGS,
FEEDINGS);
sUriMatcher.addURI(FeedingContract.CONTENT_AUTHORITY, FeedingContract.PATH_FEEDINGS +
"/#", FEEDING_ID);
}
/**
* Initialize the provider and the database helper object.
*/
#Override
public boolean onCreate() {
mDbHelper = new FeedingDbHelper(this.getContext());
// Make sure the variable is a global variable, so it can be referenced from other
// ContentProvider methods.
return true;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Log.d(LOG_TAG, "Test in query: " + (mDbHelper == null));
SQLiteDatabase db = mDbHelper.getReadableDatabase();
Cursor cursor;
int match = sUriMatcher.match(uri);
switch (match) {
case FEEDINGS:
cursor = db.query(FeedingContract.FeedingEntry.TABLE_NAME, projection, selection,
selectionArgs, null, null, sortOrder);
break;
case FEEDING_ID:
selection = FeedingEntry._ID + "=?";
selectionArgs = new String[] {
String.valueOf(ContentUris.parseId(uri))
};
cursor = db.query(FeedingContract.FeedingEntry.TABLE_NAME, projection, selection,
selectionArgs, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Can't find uri in PetProvider.query()");
}
Log.e("Gerke", "Cursor: " + DatabaseUtils.dumpCursorToString(cursor));
return cursor;
}
The resulting error message is:
"java.lang.NullPointerException: Attempt to invoke virtual method 'android.database.sqlite.SQLiteDatabase com.example.android.smartfeed.data.FeedingDbHelper.getReadableDatabase()' on a null object reference"
My questions is: why is the global variable mDbHelper null and not an instance of my dbHelper?
Also tried saving the context to a global variable, but this also does not work.
I found my mistake:
The definition of the ContentProvider is fine, but when using it, I tried creating a new instance of it, which is wrong.
So for everyone else having my problem:
The communication with the ContentProvider is done via the ContentResolver. To get an instance of the ContentResolver, call getContentResolver();

App crashes when using Content Resolver and Content Provider

I have an activity class which uses the ContentResolver's query() method with Uri & projection[] being set as two of its arguments and rest are set as 'null'.
Like:
ContentResolver resolverCatalog = getContentResolver();
Cursor cursor = resolverCatalog.query(PetsEntry.CONTENT_URI,projection,null,null,null);
However in the ContentProvider class the query() method is defined as:
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
// Making an instance of the SQLiteOpenHelper class named as 'SQLdbHelper'
SQLdbHelper PetdbHelper = new SQLdbHelper(getContext());
//getting access to database
SQLiteDatabase database_query = PetdbHelper.getReadableDatabase();
// This cursor will hold the result of the query
Cursor cursor_query;
// Figure out if the URI matcher can match the URI to a specific code
int match = sUriMatcher.match(uri);
switch (match) {
case PETS:
cursor_query = database_query.query(TABLE_NAME,projection,null,null,null,null,null);
break;
case PET_ID:
selection = PetContract.PetsEntry._ID + "=?";
selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
// This will perform a query on the pets table where the _id equals 3 to return a
// Cursor containing that row of the table.
cursor_query = database_query.query(TABLE_NAME, projection, selection, selectionArgs,
null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Cannot query unknown URI " + uri);
}
return cursor_query;
}
The PETS & PETS_ID are defined (within this ContentProvider class) as:
public class PetProvider extends ContentProvider {
//object made of the helper class for the provider, to get access of the database
private SQLdbHelper PetdbHelper;
private static final int PETS = 1;
private static final int PET_ID = 2;
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(CONTENT_AUTHORITY, PATH_PETS, PETS);
sUriMatcher.addURI(CONTENT_AUTHORITY, PATH_PETS_ID, PET_ID);
}...//class continues
In the AndroidManifest I've wrote:
<provider
android:name=".data.PetProvider"
android:authorities="com.example.android.petsretry.data"
android:exported="false">
</provider>
Have tried multiple things over it but couldn't get out of it...Please help!
Thanks in advance...
Have found the solution, problem was with the Android Manifest file, correct code is:
<provider android:name=".data.PetProvider" android:authorities="com.example.android.petsretry.data" android:exported="false"/>
All 'name', 'authorities' and 'exported' property should have been inside the opening tag of the provider instead of being in between the opening & closing tag of provider...
Hoof...

Using SearchManager's SUGGEST_COLUMN_ICON_1 to display a local file

I'm implementing my own SearchRecentSuggestionsProvider and everything's working except one thing: I can't get the device search to display icons for the results. I'd like to display images from my application's data folder (located at /{sdcard}/Android/data/package_name/files/)
According to the documentation, it's achievable by using SearchManager.SUGGEST_COLUMN_ICON_1, and it apparently supports a number of schemes, including ContentResolver.SCHEME_FILE, which is file. Here's a quote from the official docs:
Column name for suggestions cursor. Optional. If your cursor includes this column, then all suggestions will be provided in a format that includes space for two small icons, one at the left and one at the right of each suggestion. The data in the column must be a resource ID of a drawable, or a URI in one of the following formats:
content (SCHEME_CONTENT)
android.resource (SCHEME_ANDROID_RESOURCE)
file (SCHEME_FILE)
I've tried a number of obvious things, including manual creation of the file URI and automated creation using Uri.Builder(). None of this worked.
I also found someone else asking about the same thing on Google Groups, and it's sadly unanswered: https://groups.google.com/forum/#!topic/android-developers/MJj7GIaONjc
Does anyone have any experience in getting the device search to display local images from the device?
UPDATE (December 15):
I've just tried using the ContentProvider with a SearchView as the searchable info, and it works exactly as expected - including the cover art images. Still, global search doesn't show it...
I had a similar issue and could not make the other solutions work. I finally substituted the cursor with a new one containing the data I needed in query().
public class RecentQueriesProvider extends SearchRecentSuggestionsProvider {
public final static String AUTHORITY = "com.package.RecentQueriesProvider";
public final static int MODE = DATABASE_MODE_QUERIES;
public RecentQueriesProvider() {
setupSuggestions(AUTHORITY, MODE);
}
// We override query() to replace the history icon in the recent searches suggestions. We create a new cursor
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Cursor superCursor = super.query(uri, projection, selection, selectionArgs, sortOrder);
Uri iconUri = Uri.parse("android.resource://" + getContext().getPackageName() + "/drawable/ic_action_time");
MatrixCursor newCursor = new MatrixCursor(superCursor.getColumnNames());
superCursor.moveToFirst();
while (superCursor.moveToNext()){
newCursor.addRow(new Object[]{
superCursor.getInt(superCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT)),
iconUri,
superCursor.getString(superCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)),
superCursor.getString(superCursor.getColumnIndex("suggest_intent_query")),
superCursor.getInt(superCursor.getColumnIndex("_id"))
});
}
return newCursor;
}
}
One way is to copy source code from android.content.SearchRecentSuggestionsProvider, place it in your class that extends ContentProvider, and customize setupSuggestions(String, int). Specifically, you would be changing this:
Uri uriFile = Uri.fromFile(new File("path/to/file"));
mSuggestionProjection = new String [] {
"0 AS " + SearchManager.SUGGEST_COLUMN_FORMAT,
// Here, you would return a file uri: 'uriFile'
"'android.resource://system/"
+ com.android.internal.R.drawable.ic_menu_recent_history + "' AS "
+ SearchManager.SUGGEST_COLUMN_ICON_1,
"display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
"query AS " + SearchManager.SUGGEST_COLUMN_QUERY,
"_id"
};
I prefer the following though. Extend SearchRecentSuggestionsProvider and override the query(...) method. Here, intercept SearchManager.SUGGEST_URI_PATH_QUERY and return a cursor.
public class SearchSuggestionProvider extends SearchRecentSuggestionsProvider {
private UriMatcher matcher;
private static final int URI_MATCH_SUGGEST = 1;
public SearchSuggestionProvider() {
super();
matcher = new UriMatcher(UriMatcher.NO_MATCH);
// Add uri to return URI_MATCH_SUGGEST
matcher.addURI(SearchSuggestionProvider.class.getName(),
SearchManager.SUGGEST_URI_PATH_QUERY, URI_MATCH_SUGGEST);
setupSuggestions(SearchSuggestionProvider.class.getName(),
DATABASE_MODE_QUERIES);
}
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// special case for actual suggestions (from search manager)
if (matcher.match(uri) == URI_MATCH_SUGGEST) {
// File to use
File f = new File("/path/to/file");
Uri uriFile = Uri.fromFile(f);
final String[] PROJECTION = new String[] {
"_id",
"display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
"query AS " + SearchManager.SUGGEST_COLUMN_QUERY,
"'" + uriFile + "'" + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1,
};
final Uri URI = Uri.parse("content://" +
SearchSuggestionProvider.class.getName() + "/suggestions");
// return cursor
return getContext().getContentResolver().query(
URI,
PROJECTION,
"display1 LIKE ?",
new String[] {selectionArgs[0] + "%"},
"date DESC");
}
// Let super method handle the query if the check fails
return super.query(uri, projection, selection, selectionArgs, sortOrder);
}
}

Android ContentProvider URI scheme to notify CursorAdapters listening on OUTER JOIN queries

I have an Android ContentProvider which allows to do LEFT OUTER JOIN queries on a SQLite database.
Let's assume in the database I have 3 tables, Users, Articles and Comments. The ContentProvider is something like the following:
public class SampleContentProvider extends ContentProvider {
private static final UriMatcher sUriMatcher;
public static final String AUTHORITY = "com.sample.contentprovider";
private static final int USERS_TABLE = 1;
private static final int USERS_TABLE_ID = 2;
private static final int ARTICLES_TABLE = 3;
private static final int ARTICLES_TABLE_ID = 4;
private static final int COMMENTS_TABLE = 5;
private static final int COMMENTS_TABLE_ID = 6;
private static final int ARTICLES_USERS_JOIN_TABLE = 7;
private static final int COMMENTS_USERS_JOIN_TABLE = 8;
// [...] other ContentProvider methods
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String table = getTableName(uri);
// SQLiteWrapper is a wrapper class to manage a SQLiteHelper
Cursor c = SQLiteWrapper.get(getContext()).getHelper().getReadableDatabase()
.query(table, projection, selection, selectionArgs, null, null, sortOrder);
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
#Override
public Uri insert(Uri uri, ContentValues values) {
String table = getTableName(uri);
// SQLiteWrapper is a wrapper class to manage a SQLiteHelper
long id = SQLiteWrapper.get(getContext()).getHelper().getWritableDatabase()
.insert(table, null, values);
Uri itemUri = ContentUris.withAppendedId(uri, id);
getContext().getContentResolver().notifyChange(itemUri, null);
return itemUri;
}
private String getTableName(Uri uri) {
switch (sUriMatcher.match(uri)) {
case USERS_TABLE:
case USERS_TABLE_ID:
return "Users";
case ARTICLES_TABLE:
case ARTICLES_TABLE_ID:
return "Articles";
case COMMENTS_TABLE:
case COMMENTS_TABLE_ID:
return "Comments";
case ARTICLES_USERS_JOIN_TABLE:
return "Articles a LEFT OUTER JOIN Users u ON (u._id = a.user_id)";
case COMMENTS_USERS_JOIN_TABLE:
return "Comments c LEFT OUTER JOIN Users u ON (u._id = c.user_id)";
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY, "users", USERS_TABLE);
sUriMatcher.addURI(AUTHORITY, "articles", ARTICLES_TABLE);
sUriMatcher.addURI(AUTHORITY, "comments", COMMENTS_TABLE);
sUriMatcher.addURI(AUTHORITY, "users" + "/#", USERS_TABLE_ID);
sUriMatcher.addURI(AUTHORITY, "articles" + "/#", ARTICLES_TABLE_ID);
sUriMatcher.addURI(AUTHORITY, "comments" + "/#", COMMENTS_TABLE_ID);
sUriMatcher.addURI(AUTHORITY, "???", ARTICLES_USERS_JOIN_TABLE); // what uri here?
sUriMatcher.addURI(AUTHORITY, "???", COMMENTS_USERS_JOIN_TABLE); // what uri here?
}
}
What's the best URI scheme to notify all CursorAdapters listening on joined and non-joined queries every time I insert (or update) a row in the Users table?
In other words, if I add or update a new row in one of the tables, I want to send a single notification with getContext().getContentResolver().notifyChange(itemUri, null) so that all the CursorAdapters listening on any query (USERS_TABLE, ARTICLES_USERS_JOIN_TABLE, COMMENTS_USERS_JOIN_TABLE) receive a notification to update their content.
If this is not possible, is there an alternative way to notify all the observers?
You can have special Uri's to query with:
sUriMatcher.addURI(AUTHORITY, "articlesusers", ARTICLES_USERS_JOIN_TABLE);
sUriMatcher.addURI(AUTHORITY, "commentsusers", COMMENTS_USERS_JOIN_TABLE);
But I can't think of a way to send a single notification. It seems your best choice is to send a notification for each Uri that refers to the table being modified. So your insert/update/delete methods would call notifyChange multiple times depending on the table affected. For changes to "users" it would be 3 notifications--users, articlesusers and commentsusers--since they all depend on the "users" table.
As answered by prodaea, here is another alternative you can use for notification Uri. This is not a perfect solution, but it uses only one Uri for notification.
The solution is to use the main Uri without any table name (e.g:content://com.example.app.provider/) as the notification Uri in the query method for ARTICLES_USERS_JOIN_TABLE and COMMENTS_USERS_JOIN_TABLE. So, the related cursor will be notified whenever there is change in any table. There is one limitation though. That is, ARTICLES_USERS_JOIN_TABLE cursor will be notified even when there is change in Articles table.
For tables, Users' andArticles', you can use their specific Uris for notification.

Categories

Resources