I have been studying Content Providers by reading Android Developer Fundamentals (Version 1).
Under section "11.1 Share Data Through Content Providers" / "The query() method" there is a note that states
Note: The insert, delete, and update methods are provided for
convenience and clarity. Technically, the query method could handle
all requests, including those to insert, delete, and update data.
How can query method be used to insert / delete / and update data? The query method has the following signature and does not take in custom SQL strings.
public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder){}
I have not been able to find any resources that shows how this can be done. All seems to utilize the provided methods.
Frankly, that is a really poor job in that training guide. All it will do is confuse people.
I presume what they mean is that there is nothing magic about query() that forces it to only perform SQL SELECT statements. A ContentProvider is a facade — you can use it to store and retrieve from pretty much whatever you want, not just SQLite.
So, you could have your query() method:
Examine the Uri
Look for it to be of the form content://your.authority/something/insert/put/data/here
Parse the Uri to get the put, data, and here values
Insert those in a table under some pre-determined columns
Return an empty MatrixCursor
Or, it could:
Examine the Uri
Look for it to be of the form content://your.authority/something/insert
Insert a row using the projection for the columns and the selectionArgs as the values to put in those columns
Return an empty MatrixCursor
I do not know why anyone would do that, but it is certainly possible.
Related
I have several questions related to the official LoaderManager example when implementing filtering in an application (i.e. SQLite database, using Content Provider, using CONTENT_URI when no filtering is used, or forming the URI for filtering via concatenation of the CONTENT_FILTER_URI and the content of the mCurFilter variable.
Fistly, I need confirmation, that I understand the filtering URI correctly. Earlier, I did use the CONTENT_URI and set the selection, selectionArgs, and sortOrder when creating new CursorLoader(...) in the onCreateLoader method of the activity that implements LoaderManager.LoaderCallbacks<Cursor>. I did not understand why others use a special CONTENT_FILTER_URI.
The official example passes null for the selectionArgs, and the filter value is probably extracted as filter = uri.getLastPathSegment(); and added used for the selectionArgs. In my case, I am passing null also for the selection and forming it in the content provider like:
selection = ProductTable.COLUMN_NAME + " like ?";
selectionArgs = new String[]{ "%" + filter +"%" };
My understanding now is that:
The filter value can be used also other ways than simply put to a simple and single SQL command, and this is the reason to use the CONTENT_FILTER_URI.
The CONTENT_FILTER_URI makes getting the information more abstract, i.e. less SQL specific, more web-service like access to the data.
With respect to that, I am still confused why also the sortOrder is not expressed in the URI somehow. Is it a kind of tradeoff? (I hope the official example simply does not take a slopy approach, and there is real reason to form the select argument outside the content provider.)
Should I always parse the user-entered filter value? The reason is that a user can enter whatever filter value -- also the forbidden characters can be inserted. This way the unknown URI path can be formed and the URI may not be recognized. Or even worse, there may be a chance that some kind of SQL injection can be done, or at least the application can be crashed. Am I too paranoic when thinking about URI and the injection errors? Are there some rules or tools mentioned at developer.android.com or elsewhere that are related to the problem?
I am looking to add additional content to my app as extra databases.
Should I put them in apks and content providers, so they can be updated from google play directly, or just as new db files downloaded straight into the app.
My problem with the content-provider/apk method, is they all have to be declared in the android manifest, and I might have multiple databases even hundreds, so would need 100s of content-provider declarations in my manifest, even when the user may have only a couple or even none of them.
Unless there is a way I can generate the manifest dynamically? Or load the content-providers outside of the manifest?
Thanks
I think the cleanest way to do this is to have one unique ContentProvider for all your db files.
You should build your URI's around database selection.
For example : content://com.your.package/a_db_file/something/things/5
Then when implementing your ContentProvider, parse the Uri to get the a_db_file segment, open the corresponding db file, then do the needed work according to the rest of the segments.
Maybe you will need a method like getCorrectDb(String a_db_file).
Inside this method you should make the correct call to a sqlLiteOpenHelper that properly match the needed db file.
Also take a look at UriMatcher, it might be useful for you. :)
In the end you should have something like:
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
// TODO : Parse uri to get a_db_file string.
SQLiteDatabase database = getCorrectDb(a_db_file);
// TODO : get cursor from db according to other segments of uri.
return cursor.
}
I'd use one content provider to keep things simple and utilize the "attach database" sql command.
I was reading the doc, but I am still not too sure. Its says to use getContentResolver(), but then that really isn't using CursorLoader. So is there a way to do it through CursorLoader? I know how to do it with query(). Are the steps very similar? Even just a link that explains exactly this would be helpful.
Please note, do not link me to the Google doc as they do not have an example that ever uses the insert() method from ContentProvider using a CursorLoader.
Thanks in advance!!
Edit: I should probably mention the reason I am confused with this is because calling a new CursorLoader automatically calls ContentProviders query() method. But how can I do the same for insert?
Check out my blog post on the subject:
Content Resolvers and Content Providers
The CursorLoader has nothing to do with it.
Insertion is a totally different concept... it has absolutely nothing to do with the CursorLoader. When coupled with the LoaderManager, the CursorLoader automatically queries your database and updates itself when the ContentObserver is notified of a datastore change. It has nothing to do with the actual process of inserting data into your database.
How requests to the ContentResolver are resolved
When you insert (or query or update or delete) data into your database via the content provider, you don't communicate with the provider directly. Instead, you use the ContentResolver object to communicate with the provider (note that the ContentResolver is a private instance variable in your application's global Context) . More specifically, the sequence of steps performed is:
You call getContentResolver().insert(Uri, ContentValues);
The ContentResolver object determines the authority of the Uri.
The ContentResolver relays the request to the content provider registered with the authority (this is why you need to specify the authority in the AndroidManifest.xml).
The content provider receives the request and performs the specified operation (in this case insert). How and where the data is inserted depends on how you implemented the insert method (ContentProvider is an abstract class that requires the user to implement insert, query, delete, update, and getType).
Hopefully you were able to wrap your head around that at least a little. The reason why there are so many steps involved is because Android (1) allows applications to have more than one content provider, and (2) needs to ensure that apps can securely share data with other third-party apps. (It wasn't because it wanted to confuse you, I promise).
Inserting data via the ContentProvider
Now that you (hopefully) have a better idea of how the ContentResolver is able to relay these requests to the content provider, inserting the data is fairly straight forward:
First, decide which uri you want to have matched by your content provider. This depends on how you decided to match your uris with the UriMatcher. Each uri you have represents a different means of inserting data into your internal database (i.e. if your app has two tables, you will probably have two uris, one for each table).
Create a new ContentValues object and use it to package the data you wish to send to the content provider. The ContentValues object maps column names to data values. In the below example, the column name is "column_1" and the value being inserted under that column is "value_1":
ContentValues values = new ContentValues();
values.put("column_1", "value_1");
Once received, the content provider will (in your case) pass the values object to your SQLiteDatabase (via the SQLiteDatabase.insert(String table, String nullColumnHack, ContentValues values) method). Unlike the ContentProvider, this method is implemented for you... the SQLiteDatabase knows how to handle the values object and will insert the row into the database, returning the row id of the inserted row, or -1 if the insertion failed.
... and that's how you insert data into your database.
TL;DR
Use getContentResolver().insert(Uri, ContentValues);
Convert Cursor to ContentValues for easy database insertion.
Its says to use getContentResolver(), but then that really isn't using CursorLoader
Besides what Alex said, there's nothing preventing you from iterating through the Cursor returned, putting them into ContentValues and then inserting that (say, to a different DB).
Just an example from the top of my head (a simple Cursor of two String columns):
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (cursor.moveToFirst()) {
ArrayList<ContentValues> values = new ArrayList<ContentValues>();
do {
ContentValues row = new ContentValues();
DatabaseUtils.cursorStringToContentValues(cursor, fieldFirstName, row);
DatabaseUtils.cursorStringToContentValues(cursor, fieldLastName, row);
values.add(row);
} while (cursor.moveToNext());
ContentValues[] cv = new ContentValues[values.size()];
values.toArray(cv);
getContentResolver().bulkInsert(CONTENT_NAMES, cv);
}
}
There are probably more efficient ways to do that (and definitely some integrity checks), but that was my first thought...
I am looking to perform the following query (in pseudo-code) on Android:
SELECT C.ID, C.NAME, CASE ISNULL(G.GROUPID,0) = 0 THEN 0 ELSE 1 END INGROUP
FROM CONTACTS C
LEFT JOIN GROUPMEMBERSHIP G ON G.CONTACTID = C.ID AND G.GROUPID = ?
I am looking to select the ID and Name of ALL contacts in the system address book, via the default Contacts ContentProvider, along with a
0/1 field indicating whether the contact is a member of group ? .
I could of course get all contacts easily enough, then loop through and query the membership separately easy enough in my Adapter class, but I'd imagine performing the two queries as one outer joined query would yield much better performance.
Can I do this with the standard high-level string-projection and ContentResolver.query() method? Or would this kind of query require digging into more direct SQL execution?
Edit: Okay, so this doesn't actually solve the question asked, because eidylon is tied to an existing ContentProvider as mentioned in their question. However, this does cover how you do a JOIN if you own the ContentProvider source and API. So I'll leave it for those who want to know how to handle that case.
This is easy! But unintuitive... :)
query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
Okay, so what is URI? Typically, you have one URI per table.
content://com.example.coolapp.contacts serves data out of your CONTACTS table.
content://com.example.coolapp.groupmembers serves data out of your GROUPMEMBERSHIP table.
But URI is really just a string. Use it however you like. Make a block of code in your ContentProvider that responds to content://com.example.coolapp.contacts_in_group. Within that block of code in the ContentProvider, you can get raw access to your SQLite DB, unfettered by the limited query() data model. Feel free to use it!
Define your selection fields however you like. They don't have to map to table column names -- map them how you need to, in order to get your parameters in.
Define your projection how you need -- It may contain columns from both tables after the join.
Bing, you're done. Google does this same model internally in their own code -- Go look at the Contacts provider API -- you see "bla.RawContact" and "bla.Contact" and etc as content URIs. Each serves data out of the same table in the DB -- the different URIs just provide different views of that same table!
Nope, you can't do that kind of queries with the ContentResolver.query() method.
You will need to write something like this:
SQLiteDatabase db = YourActivity.getDbHelper().getReadableDatabase();
String query = yourLongQuery;
Cursor c = db.rawQuery(query, null);
YourActivity.startManagingCursor(c);
c.setNotificationUri(YourActivity.getContentResolver(), YourContentProvider.CONTENT_URI);
You can't do that because ContentResolver has only one query method:
query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
there's no parameter for tables or FROM clauses.
I got following problem, I need to use a Content Provider to read a
Database of an other App.
first I want all rows, and after analyzing the data only e.g. the rows
from _id = 1, 3 and 5.
how can I call a Content provider and select only these rows?
or is it possible to create a subset Cursor form an given Cursor?
Thanks in advance.
If you're talking to another app, I assume you're querying the other app's ContentProvider to get the data from them in the first place.
In this situation, the cleanest answer seems not to build your own ContentProvider that filters/wraps theirs. Instead query their ContentProvider from your application directly, and use the select clause in your query() to specify the conditions that define the subset of data you want to be given.