Android newbie here.
I'm starting to learn about ContentProviders and I've set up my first ContentProvider which internally accesses a private SQLiteOpenHelper class to read and write data out of my database.
I take it one of the main benefits of ContentProviders is that you put all your data accessing code in the one place and the only time you're supposed to access the database is via ContentResolvers which use the ContentProvider's URI? [correct me if i'm wrong, i just figure that is the case as all the examples put SQLiteOpenHelper as a private class]
So I've recently written an update method in my ContentProvider which clears a column in my database. It looks roughly like this
#Override
public int update(Uri uri, ContentValues values, String where,
String[] whereArgs) {
SQLiteDatabase database = dbHelper.getWritableDatabase();
int count;
switch (uriMatcher.match(uri)) {
case FACT_ID:
String segment = uri.getPathSegments().get(1);
count = database.update(TABLE_FACT, values,
KEY_ID
+ "="
+ segment
+ (!TextUtils.isEmpty(where) ? " AND (" + where
+ ')' : ""), whereArgs);
break;
case CLEAR_DATESEEN:
ContentValues cv = new ContentValues();
cv.putNull(KEY_DATESEEN);
count = database.update(TABLE_FACT, cv, null, null);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
The CLEAR_DATESEEN bit of the code is the one that clears the column.
This works but I was just wondering, doesn't this mean that any app on the device that calls that URI should be able to clear that column as well? What if I did not want other apps messing with my data?
Is there any way to prevent certain apps or only allow certain apps to be able to call my ContentProvider?
Unless you have set-up a special permission and provide that information to other apps (developers), your content provider is accessible only for your app. Please see Content Provider Permissions.
Related
Veracode Static Scan report points SQL Injection flaw in my Content Provider implementation.
Previously, I posted this question related to all my doubts regarding this flaw.
And after few discussions I came to a conclusion that there might be chances of it being a false positive in the report. Because according to what I researched and read, I was following the security guidelines mentioned in Android docs and other referenced sources to avoid SQL Injection.
There is suggestion everywhere to perform at least some input validation on the data passed to SQL queries.I want to cover this possibility which be the reason of flaw.
Everyone is asking me to sanitize data before passing to query.
How do I exactly sanitize variables passed to selectionArgs array passed to delete(), update() method of Content Provider?
Will DatabaseUtils.sqlEscapeString() be sufficient?
Please suggest!
Here's the implementation where I need to sanitize the variable:
public Loader<Cursor> onCreateLoader(int id, Bundle b) {
switch (id) {
case THOUGHT_LOADER:
return new CursorLoader(getActivity(), NewsFeedTable.CONTENT_URI, NewsFeedTable.PROJECTION, NewsFeedTable._id + "=?", new String[]{tid}, null);
case COMMENT_LOADER:
return new CursorLoader(getActivity(), CommentTable.CONTENT_URI, CommentTable.PROJECTION, CommentTable.COLUMN_TID + "=?", new String[]{tid}, null);
default:
return null;
}
}
Report points to the flaw :Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (CWEID 89) at this line
deleted = db.delete(BulletinTable.TABLE_NAME, selection, selectionArgs); in the below code:
#Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
if (uri.equals(Contract.BASE_CONTENT_URI)) {
deleteDatabase();
return 1;
}
SQLiteDatabase db = openHelper.getWritableDatabase();
int deleted = 0;
switch (matcher.match(uri)) {
case BULLETIN:
deleted = db.delete(BulletinTable.TABLE_NAME, selection, selectionArgs);
break;
case CLASSROOMS:
deleted = db.delete(ClassroomsTable.TABLE_NAME, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
if (deleted > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return deleted;
}
Values in the selectionArgs array never need to be sanitized, because they cannot be interpreted as SQL commands (that's the whole point of having separate parameter values).
The purpose of sqlEscapeString() is to format a string so that it can be put into an SQL command (i.e., escape single quotes; all other characters have no meaning inside an SQL string). But when you know that there is a string, you should selectionArgs instead, so this function is not helpful.
You need to sanitize only strings that end up in the SQL command itself. In this case, this would be selection. If this value comes from the user, or from some other app, then you have no control over how much stuff your DELETE statement actually does (it could call SQL functions, or execute subqueries that access other parts of the database).
For practical purposes, it is not possible to sanitize strings that are intended to contain SQL commands, because then you would need a full SQL parser. If your content provider is available for external code, you should allow deleting specific items only through the URI, and disallow custom selections.
update: looking at "vnd.android.cursor.dir/vnd.google.note" and "vnd.android.cursor.item/vnd.google.note" it seemed to me as though the cursor was for one table.
From the examples it appears as though content provider were designed to work with one table. I do know how to use multiple tables in sqlite but it seems to me that the content provider seems to be about picking one row or multiple rows from one table.
see http://developer.android.com/guide/topics/providers/content-provider-creating.html
Also, see the notepad sample in adt-bundle-windows-x86-20131030\sdk\samples\android-19\legacy\NotePad\src\com\example\android\notepad
Suppose I want to have notes by topic.
I would like to have a Topics table with columns _id and Title_text.
I would like to have the Notes table with columns _id and foreign key Topic_id and Note_text.
How would one design the Topics and Notes?
But looking at the Notes sample, the content URIs and docs on content providers, it appears as though having multiple related tables is an afterthought and is not obvious to me.
from NotepadProvider.java, Notepad.java:
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.google.note";
/**
* The MIME type of a {#link #CONTENT_URI} sub-directory of a single
* note.
*/
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.google.note";
public static final Uri CONTENT_ID_URI_BASE
= Uri.parse(SCHEME + AUTHORITY + PATH_NOTE_ID);
/**
* The content URI match pattern for a single note, specified by its ID. Use this to match
* incoming URIs or to construct an Intent.
*/
public static final Uri CONTENT_ID_URI_PATTERN
= Uri.parse(SCHEME + AUTHORITY + PATH_NOTE_ID + "/#");
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
...
switch (sUriMatcher.match(uri)) {
// If the incoming URI is for notes, chooses the Notes projection
case NOTES:
qb.setProjectionMap(sNotesProjectionMap);
break;
/* If the incoming URI is for a single note identified by its ID, chooses the
* note ID projection, and appends "_ID = <noteID>" to the where clause, so that
* it selects that single note
*/
case NOTE_ID:
qb.setProjectionMap(sNotesProjectionMap);
qb.appendWhere(
NotePad.Notes._ID + // the name of the ID column
"=" +
// the position of the note ID itself in the incoming URI
uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION));
break;
When creating a ContentProvider, the expectation is that other apps are going to use your database, and with that I mean other people who know nothing about your database scheme. To make things easy for them, you create and document your URIs:
To access all the books
content://org.example.bookprovider/books
to access books by id
content://org.example.bookprovider/books/#
to access books by author name
content://org.example.bookprovider/books/author
Create as many URIs as you need, that’s up to you. This way the user of your Provider can very easily access your database info, and maybe that’s why you are getting the impression that the Provider is designed to work with one table databases, but no, internally is where the work is done.
In your ContentProvider subclass, you can use a UriMatcher to identify those different URIs that are going to be passed to your ContentProvider methods (query, insert, update, delete). If the data the Uri is requesting is stored in several tables, you can actually do the JOINs and GROUP BYs or whatever you need with SQLiteQueryBuilder , e.g.
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder mQueryBuilder = new SQLiteQueryBuilder();
. . .
String Joins = " t1 INNER JOIN table2 t2 ON t2._id = t1._id"
+ " INNER JOIN table3 t3 ON t3._id = t1._id";
switch (mUriMatcher.match(uri)) {
case DATA_COLLECTION_URI:
mQueryBuilder.setTables(YourDataContract.TABLE1_NAME + Joins);
mQueryBuilder.setProjectionMap(. . .);
break;
case SINGLE_DATA_URI:
mQueryBuilder.setTables(YourDataContract.TABLE1_NAME + Joins);
mQueryBuilder.setProjectionMap(. . .);
mQueryBuilder.appendWhere(Table1._ID + "=" + uri.getPathSegments().get(1));
break;
case . . .
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
. . .
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
Cursor c = mQueryBuilder.query(db, projection, selection, selectionArgs, groupBy, having, orderBy);
return c;
}
Hope it helps.
Excuse me, but I don't understand your question.
ContentProvider is designed (a one of it's aims)to wrap access to your tabels. Design of database schema is up to you.
Generally, you need to:
Define your tables/ It should be made by execution of sql command in class which extends SQLiteOpenHelper
Define an uri for them
Define a logic for queries to this tables as it was made for NOTE_ID
Update
For JOIN operations SQLiteQueryBuilder is usually used. In setTables() you need to write names of tables with JOIN clause, e.g.
.setTables(NoteColumns.TABLENAME +
" LEFT OUTER JOIN " + TopicColumns.TABLENAME + " ON " +
NoteColumns.ID + " = " + TopicColumns.ID);
Here is my code for multiple table query in content provider with projectionMap
//HashMap for Projection
mGroupImageUri = new HashMap<>();
mGroupImageUri.put(RosterConstants.JID,RosterProvider.TABLE_ROSTER+"."+RosterConstants.JID);
mGroupImageUri.put(RosterConstants.USER_NAME,RosterProvider.TABLE_ROSTER+"."+RosterConstants.USER_NAME);
mGroupImageUri.put(ChatConstants.MESSAGE,"c."+ChatConstants.MESSAGE+ " AS "+ ChatConstants.MESSAGE);
mGroupImageUri.put(ChatConstants.SENDER,"c."+ChatConstants.SENDER+" AS "+ChatConstants.SENDER);
mGroupImageUri.put(ChatConstants.URL_LOCAL,"c."+ChatConstants.URL_LOCAL+" AS "+ChatConstants.URL_LOCAL);
//case for content type of uri
case IMAGE_URI:
qBuilder.setTables(RosterProvider.TABLE_ROSTER
+ " LEFT OUTER JOIN "+ TABLE_NAME + " c"
+ " ON c."+ ChatConstants.JID + "=" + RosterProvider.TABLE_ROSTER + "."+RosterConstants.JID);
qBuilder.setProjectionMap(mGroupImageUri);
break;
//ContentResolver query for Projection form, selection and selection args
String[] PROJECTION_FROM = new String[]{
RosterConstants.JID,
RosterConstants.USER_NAME,
ChatConstants.MESSAGE,
ChatConstants.SENDER,
ChatConstants.URL_LOCAL
};
String selection = RosterProvider.TABLE_ROSTER +"."+RosterConstants.JID+ "='" + jid + "' AND " + "c."+ChatConstants.FILE_TYPE+"="+ChatConstants.IMAGE;
String[] selectionArgu = null;
String order = "c."+ChatConstants.MESSAGE+" ASC";
Cursor cursor = mContentReolver.query(ChatProvider.CONTENT_URI_GROUP_IMAGE_URI,
PROJECTION_FROM,selection, null,order);
//#ChatProvider.CONTENT_URI_GROUP_IMAGE_URI = 'your content type uri'
//#TABLE_NAME = 'table1'
//#RosterProvider.TABLE_ROSTER ='table2'
I know this is a common problem, but nothing I've found solves my issue. I've created a ContentProvider, closely following the tutorial here. My Activity, in its onCreate method immediately after super.onCreate does this:
StitchProvider stitchProvider = new StitchProvider();
stitchProvider.delete(STITCHES_URI, null, null);
StitchProvider is my ContentProvider. I've followed this code through the debugger and depending on where I put breakpoints, one of two things happen, but both lead to a NullPointerException in LogCat. The first option is I put a breakpoint here:
public SQLData(Context c) {
super(c, DATABASENAME, null, DATABASEVERSION);
}
SQLData is my database class. If I put the breakpoint here, I see that c, the Context, is equal to android.app.Application#412a9b80. The code then returns to the onCreate method of StitchProvider:
public boolean onCreate() {
mDB = new SQLData(getContext());
return true;
}
and mDB becomes com.MyKnitCards.project.SQLData#412ab668. So far so good I think, but when I try to go past the return statement I get the NullPointerException. If I move the breakpoint to within the delete method of StitchProvider everything goes fine until I get to the getWriteableDatabase line, at which point I get a NullPointerException. Here's the delete method of StitchProvider:
public int delete(Uri uri, String selection, String[] selectionArgs) {
int uriType = sURIMatcher.match(uri);
int rowsAffected = 0;
switch (uriType)
{
case STITCHES:
SQLiteDatabase sqlDB = mDB.getWritableDatabase();
rowsAffected = sqlDB.delete(STITCHTABLE_BASEPATH, selection, selectionArgs);
break;
case STITCHES_ID:
SQLiteDatabase sqlDBwithID = mDB.getWritableDatabase();
String id = uri.getLastPathSegment();
if (TextUtils.isEmpty(selection))
{
rowsAffected = sqlDBwithID.delete(STITCHTABLE_BASEPATH, SQLData.KEY_ROWID + "=" + id, null);
}
else
{
rowsAffected = sqlDBwithID.delete(STITCHTABLE_BASEPATH, selection + " and " + SQLData.KEY_ROWID + "=" + id, selectionArgs);
}
break;
default:
throw new IllegalArgumentException("Unknown or Invalid URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsAffected;
}
One thing I have noticed, is that if I set a breakpoint at SQLData's onCreate method, I never get there. I think part of the problem is that the database is not getting created, but I don't know why. I'll post as much code or LogCat as people want, put I don't want to overwhelm folks with code either. Anyway, as always, if you have any suggestions, that'd be great, and thanks!
I see that you instantiate your provider in code, something that you shouldn't be doing as a ContentProvider is managed by the Android system. The correct way of using a ContentProvider is through a ContentResolver which can be obtain in an Activity with getContentResolver().
You can find more about ContentProviders and how to use them in the official tutorial on the android developers site.
I'm writing a ContentProvider to work with a database. In this database, I have a "trip" table, and a "day" table. Each record in the trip table is associated with multiple records in the day table, i.e. each trip spans multiple days, but any one day record can belong to only one trip. If a trip is deleted, so must all the days associated with it.
My question is this, what is the standard or generally accepted method of dealing with this table relationship from a ContentProvider when deleting? When my delete method gets a URI to delete a particular trip, should it also delete the associated days, or should it just delete the trip and require the program using the ContentProvider to do the necessary logic of deleting the days as well?
Delete all at once, one call from the app with the trip id to delete:
#Override
public int delete( Uri uri, String selection, String[] selectionArgs )
{
SQLiteDatabase db;
int count;
db = _dbHelper.getWritableDatabase();
switch( uriMatcher.match( uri ) )
{
case TRIP_ID:
count = db.delete( TRIP_TABLE, KEY_TRIP_ID + "=?", new String[]{ uri.getLastPathSegment() } );
count += db.delete( DAY_TABLE, KEY_TRIP_ID + "=?", new String[]{ uri.getLastPathSegment() } );
return count;
}
}
Or delete only exactly what's requested, multiple calls from the app with the trip and as many days as needed:
#Override
public int delete( Uri uri, String selection, String[] selectionArgs )
{
SQLiteDatabase db;
int count;
db = _dbHelper.getWritableDatabase();
switch( uriMatcher.match( uri ) )
{
case TRIP_ID:
count = db.delete( TRIP_TABLE, KEY_TRIP_ID + "=?", new String[]{ uri.getLastPathSegment() } );
return count;
case Day_ID:
count = db.delete( DAY_TABLE, KEY_DAY_ID + "=?", new String[]{ uri.getLastPathSegment() } );
return count;
}
}
When my delete method gets a URI to delete a particular trip, should it also delete the associated days, or should it just delete the trip and require the program using the ContentProvider to do the necessary logic of deleting the days as well?
I would vote the former, perhaps using a FOREIGN KEY relationship within SQLite rather than by manually doing it yourself. Since day records have no life outside of the trip, deleting a trip should delete those dependent records.
I am learning Android and I am stuck on an issue involving calling a custom content provider. I have been using an example in an instructional book and although it describes how to create the custom provider there is no clear example how to call the specific methods in it. I am specifically looking into how to delete a single record from the custom content provider.
Here is the code for the custom content provider (EarthquakeProvider.java):
#Override
public int delete(Uri uri, String where, String[] whereArgs) {
int count;
switch (uriMatcher.match(uri)) {
case QUAKES:
count = earthquakeDB.delete(EARTHQUAKE_TABLE, where, whereArgs);
break;
case QUAKE_ID:
String segment = uri.getPathSegments().get(1);
count = earthquakeDB.delete(EARTHQUAKE_TABLE, KEY_ID + "="
+ segment
+ (!TextUtils.isEmpty(where) ? " AND ("
+ where + ')' : ""), whereArgs);
break;
default: throw new IllegalArgumentException("Unsupported URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
I am trying to call the delete method from the main activity to delete a single entry, not the entire database. I want to use about an OnLongClickListener for the selected record that is displayed in a array list view in the main activity.
This is what I have come up with I have so far in my main activity for this method:
earthquakeListView.setOnItemLongClickListener(new OnItemLongClickListener() {
#Override
public boolean onItemLongClick(AdapterView _av, View _v, int _index,
long arg3) {
ContentResolver cr = getContentResolver();
cr.delete(earthquakeProvider.CONTENT_URI, null, null);
return false;
}
I know the above code doesn't work, but this is as close as I could get with my current understanding.
Any help on this would be very much appreciated.
cr.delete(earthquakeProvider.CONTENT_URI, null, null);
This is your problem. First, some context:
Content URIs: (source)
content://authority/path/##
The number at the end is optional. If present, the URI references a specific row in the database where row._id=(the number). If absent, it references the table as a whole.
the delete() call accepts a URI, a where clause, and a set of strings which get substituted in. Example: Say you have a database of people.
cr.delete(
Person.CONTENT_URI,
"sex=? AND eyecolor=?",
new String[]{"male", "blue"});
Will search the entire person table, and delete anyone whose sex is male and whose eye color is blue.
If the where clause and where values are null, then the delete() call will match every row in the table. This causes the behavior you see.
There are two methods to specify the row you want:
First option, you could append the number to the URI:
cr.delete(
EarthquakeProvider.CONTENT_URI.buildUpon().appendPath(String.valueOf(_id)).build(),
null, null);
This restricts the URI to a specific row, and the path will be through your case QUAKE_ID: statement and so will only delete one row no matter what.
Second option, you could use a where clause:
cr.delete(EarthquakeProvider.CONTENT_URI, "_id=?", String.valueOf(_id)));
Either way, you will restrict the delete to a single row, as you need it to. The latter makes for prettier code, but the former is more efficient, due to the way the ContentProvider and ContentObservers work.
As a last note: In your ContentProvider you need to add a call to
ContentResolver.notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork). This helps notify cursors to re-fetch the database query and helps out a lot with automation.