I have a ContentProvider and I need to match some URIs containing UUIDs as wildcards.
UriMatcher from ContentProvider:
public static final Uri CONTENT_URI_NOTIFICATIONS = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH_NOTIFICATIONS);
public static final Uri CONTENT_URI_USERS = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH_USERS);
private static final int NOTIFICATIONS = 40;
private static final int USER_ID = 70;
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static
{
sURIMatcher.addURI(AUTHORITY, BASE_PATH_NOTIFICATIONS, NOTIFICATIONS);
sURIMatcher.addURI(AUTHORITY, BASE_PATH_USERS + "/*", USER_ID);
}
Query Code for NOTIFICATIONS:
Uri uri = Uri.parse(String.valueOf(MyContentProvider.CONTENT_URI_NOTIFICATIONS));
return new CursorLoader(getActivity(), uri, projection, null, null, null);
Query Code for USER_ID:
String userId = "73279386-5459-4316-9ff9-7c6b7b84029a";
Uri uri = Uri.parse(MyContentProvider.CONTENT_URI_USERS + "/" + userId);
return new CursorLoader(getActivity(), uri, projection, null, null, null);
From the above UriMatcher, the NOTIFICATIONS URI matches, but the USER_ID does not. Any idea what could be wrong here ? Worth noting is that when I used to have integers instead of UUIDs for representing users and used to have # instead of * in the UriMatcher, everything worked as intended. After switching to the wildcard, the matcher stopped matching the URIs containing UUIDs.
Apparently the order in which you add the URIs counts. If you have set like below, USER_DETAILS won't be recognized anymore. You have to switch the order and add USER_DETAILS first.
sURIMatcher.addURI(AUTHORITY, BASE_PATH_USERS + "/*", USER_ID);
sURIMatcher.addURI(AUTHORITY, BASE_PATH_USERS + "/details", USER_DETAILS);
Related
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);
}
}
I'm having trouble with matching URIs in my ContentProvider. It will match paths with one or two "tags" (not sure what the correct term is), for example it can recognize box/#. But as soon as I add a second tag, for example box/#/item, I throw an InvalidArgumentException and my app crashes. I've read in several threads on here that this issue can sometimes be solved by changing the order of the URIs added to the UriMatcher; I tried this, but to no avail. Anyone know what the problem is? I've excerpted relevant parts of my code below.
From my ContentProvider:
private static final String AUTHORITY = "com.example.boxdatabase.DatabaseProvider";
// Type of query
public static final int BOXES = 100;
...
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
...
// URI Matcher for queries
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(AUTHORITY, "box/#/item", BOX_ITEM);
sURIMatcher.addURI(AUTHORITY, "box/#/item/#", BOX_ITEM_ID);
sURIMatcher.addURI(AUTHORITY, "box", BOXES);
sURIMatcher.addURI(AUTHORITY, "box/#", BOXES_ID);
sURIMatcher.addURI(AUTHORITY, "item", ITEMS);
sURIMatcher.addURI(AUTHORITY, "item/#", ITEMS_ID);
}
An example of where I try to access my ContentProvider and it fails:
return new CursorLoader(this, Uri.withAppendedPath(
DatabaseProvider.CONTENT_URI, "box/" + boxId + "/item"),
DatabaseContract.BoxItemEntry.ALL_COLUMNS, null, null, null);
Try this:
sURIMatcher.addURI(AUTHORITY, "box", BOXES);
sURIMatcher.addURI(AUTHORITY, "box/#", BOXES_ID);
sURIMatcher.addURI(AUTHORITY, "box/#/item", BOX_ITEM);
sURIMatcher.addURI(AUTHORITY, "box/#/item/#", BOX_ITEM_ID);
sURIMatcher.addURI(AUTHORITY, "item", ITEMS);
sURIMatcher.addURI(AUTHORITY, "item/#", ITEMS_ID);
The order is important, read my explanations here:
https://stackoverflow.com/a/15015687/534471
I am reading this tutorial on implementing my own ContentProvide for working with SQLite. Int the ContentProvider.query there are a few thing that puzzles me. It seems very hardcoded to just one table (the todo table in the tutorial), but maybe Im just not getting it? Now if I wanted to query another table, lets say nodo, how would I change the ContentProvider?
Should I append the table names somehow in queryBuilder.setTables(String inTables)?
What about the CONTENT_TYPE and CONTENT_ITEM_TYPE, should there be one for each table?
That about the TODO and TODO_ID varibles and the switch in the query method?
It seems I need to have a lot of if/switch conditions to support multiple tables with the same ContentProvider, is this the way to go or am I on a wrong path?
Thank you
Søren
Now if I wanted to query another table, lets say nodo, how would I change the ContentProvider?
Querying a new table would mean that you need to add a new Uri, since the Uri selects the datasource, similar to using a different table.
You would be adding essentially all the hardcoded values that are already there for the todos for your other table. For example:
// ------- usually the same for all
private static final String AUTHORITY = "de.vogella.android.todos.contentprovider";
// ------- define some Uris
private static final String PATH_TODOS = "todos";
private static final String PATH_REMINDERS = "reminders";
public static final Uri CONTENT_URI_TODOS = Uri.parse("content://" + AUTHORITY
+ "/" + PATH_TODOS);
public static final Uri CONTENT_URI_REMINDERS = Uri.parse("content://" + AUTHORITY
+ "/" + PATH_REMINDERS);
// ------- maybe also define CONTENT_TYPE for each
// ------- setup UriMatcher
private static final int TODOS = 10;
private static final int TODO_ID = 20;
private static final int REMINDERS = 30;
private static final int REMINDERS_ID = 40;
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(AUTHORITY, PATH_TODOS, TODOS);
sURIMatcher.addURI(AUTHORITY, PATH_TODOS + "/#", TODO_ID);
sURIMatcher.addURI(AUTHORITY, PATH_REMINDERS, REMINDERS);
sURIMatcher.addURI(AUTHORITY, PATH_REMINDERS + "/#", REMINDERS_ID);
}
//#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// Using SQLiteQueryBuilder instead of query() method
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
int uriType = sURIMatcher.match(uri);
switch (uriType) {
case TODO_ID:
// Adding the ID to the original query
queryBuilder.appendWhere(TodoTable.COLUMN_ID + "="
+ uri.getLastPathSegment());
//$FALL-THROUGH$
case TODOS:
queryBuilder.setTables(TodoTable.TABLE_TODO);
break;
case REMINDERS_ID:
// Adding the ID to the original query
queryBuilder.appendWhere(ReminderTable.COLUMN_ID + "="
+ uri.getLastPathSegment());
//$FALL-THROUGH$
case REMINDERS:
queryBuilder.setTables(ReminderTable.TABLE_REMINDER);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
Should I append the table names somehow in queryBuilder.setTables(String inTables)?
Yes, if different Uris read from different tables then set the table based on the Uri match.
What about the CONTENT_TYPE and CONTENT_ITEM_TYPE, should there be one for each table?
Depends on the actual content type. If they are different and you need a type yes. But you don't need to have them at all. That example defines them but doesn't even use them. It would need to return the type in getType, see documentation.
That about the TODO and TODO_ID varibles and the switch in the query method?
Those are constants defined for the UriMatcher which is explained nicely here. It's basically a simplification for String matching. A big ContentProvider can have 100 different Uris and selecting the right table in query would be painful if you would have to write if (uri.getPath().equals("todos") { /* code */ } else if (uri.. all the way.
Here's solution to your question, using UriMatcher, you can implement multiple tables in a content provider.
Content type and content item can be as follows and they can be wrapped in a separate class for each table
public static final String GENERAL_CONTENT_TYPE = "vnd.android.cursor.dir/vnd.myfirstapp.db.member" ;
public static final String SPECIFIC_CONTENT_TYPE = "vnd.android.cursor.item/vnd.myfirstapp.db.member" ;
`vnd.android.cursor.dir/vnd.yourownanything.anything.tablename'
this defines the general content type
`vnd.android.cursor.item/vnd.anthingasabove.table'
this also defines the specific and it is constant to any app those strings(words) vnd.android.cursor.dir and .item must be like that and after /vnd. must be like that
and in the class that extends contentprovider you just uset the same instance of UriMatcher to map the tables
Am I misunderstanding something here? I'm trying to implement a ContentProvider in Android and for some reason the calling URI is not being matched.
In my ContentProvider I define the following:
private static final int GET_COURSES = 100;
public static final Uri COURSES_URI = Uri.withAppendedPath(CONTENT_URI, CourseTable.NAME);
private static final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
static
{
matcher.addURI(AUTHORITY, COURSES_URI.toString(), GET_COURSES);
}
Then, in my query call:
public Cursor query(Uri uri, ...)
{
int type = matcher.match(uri);
.
.
Here, type is always -1... In the debug window I've viewed both the passing in uri and COURSES_URI and the string representations are identical...
Any suggestions?
Thanks
Update:
I call the Content Provider using:
new CursorLoader(this, CoursesProvider.COURSES_URI, null, null, null, null);
... this is boggling my mind... just got uri.equals(COURSES_URI) == true, so something must be incorrect in the UriMatcher
Problem solved...
The initial problem was that COURSES_URI also contained the AUTHORITY path:
private static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + DBManager.DB_NAME);
private static final Uri COURSES_URI = Uri.withAppendedPath(CONTENT_URI, CourseTable.NAME);
In the matcher.AddURI(authority,path,code) method the authority portion of path should be removed.
This can be obtained using COURSES_URI.getPath().substring(1) (substring to remove the leading '/' returned by getPath())
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.