I\'m working on a custom Content Provider for my app. This is part of a course I\'m taking on Android apps, so please don\'t expect the rationale for doing all this to be to
I believe I managed to figure this out, and it wasn't so mysterious after all. Writing a custom content, in this case, provider really boiled down to implementing my own query
and update
methods. I didn't need any insert
or delete
, since I'm only reading the MediaStore.
To get started, I designed two URIs; one for query and one for update. The query URI followed the pattern, content://AUTH/photo/#
, where # is the IMAGE_ID from MediaStore.Images.Thumbnails. This method essentially runs another query against MediaStore.Images.Media, to get the "standard" data on the specific image, but then gets some additional meta data on the image via the ExifInterface
. All this is returned in a MatrixCursor
.
The update URI matches the pattern, content://AUTH/photo/#/comment
, which then sets (writes) the UserComment
EXIF tag in the file with that ID -- again via the ExifInterface
.
To answer my question, I'm posting updated code from my PhotoContract and PhotoProvider classes. Essentially, if you're writing a content provider which does not interface the SQLite database -- but something else on the device -- all you need to do is implement those operations in the appropriate methods in your provider class.
import android.content.ContentResolver;
import android.content.ContentUris;
import android.net.Uri;
import android.provider.BaseColumns;
import android.provider.MediaStore;
public class PhotoContract {
public static final String CONTENT_AUTHORITY = "com.example.android.myFunkyApp.app";
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
public static final String PATH_PHOTO = "photo";
public static final String PATH_COMMENT = "comment";
//public static final String PATH_AUTHOR = "author";
public static final class ThumbEntry {
public static final String COLUMN_THUMB_ID = MediaStore.Images.Thumbnails._ID;
public static final String COLUMN_DATA = MediaStore.Images.Thumbnails.DATA;
public static final String COLUMN_IMAGE_ID = MediaStore.Images.Thumbnails.IMAGE_ID;
}
public static final class PhotoEntry {
public static final Uri CONTENT_URI =
BASE_CONTENT_URI.buildUpon().appendPath(PATH_PHOTO).build();
public static final String COLUMN_IMAGE_ID = MediaStore.Images.Media._ID;
public static final String COLUMN_DISPLAY_NAME = MediaStore.Images.Media.DISPLAY_NAME;
public static final String COLUMN_DATA = MediaStore.Images.Media.DATA;
public static final String COLUMN_DESC = MediaStore.Images.Media.DESCRIPTION;
public static final String COLUMN_DATE_TAKEN = MediaStore.Images.Media.DATE_TAKEN;
public static final String COLUMN_DATE_ADDED = MediaStore.Images.Media.DATE_ADDED;
public static final String COLUMN_TITLE = MediaStore.Images.Media.TITLE;
public static final String COLUMN_SIZE = MediaStore.Images.Media.SIZE;
public static final String COLUMN_ORIENTATION = MediaStore.Images.Media.ORIENTATION;
public static final String COLUMN_EXIF_COMMENT = "UserComment";
//public static final String COLUMN_EXIF_AUTHOR = "Author";
public static final String CONTENT_TYPE =
ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PHOTO;
public static final String CONTENT_ITEM_TYPE =
ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PHOTO;
// makes an URI to a specific image_id
public static final Uri buildPhotoUriWithId(Long photo_id) {
return CONTENT_URI.buildUpon().appendPath(Long.toString(photo_id)).build();
}
// since we will "redirect" the URI towards MediaStore, we need to be able to extract IMAGE_ID
public static Long getImageIdFromUri(Uri uri) {
return Long.parseLong(uri.getPathSegments().get(1)); // always in position 1
}
}
}
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.AbstractWindowedCursor;
import android.database.Cursor;
import android.database.CursorWindow;
import android.database.CursorWrapper;
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.ExifInterface;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.v4.content.CursorLoader;
import android.util.Log;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
public class PhotoProvider extends ContentProvider {
// The URI Matcher used by this content provider.
private static final UriMatcher sUriMatcher = buildUriMatcher();
static final int PHOTO = 100;
static final int PHOTO_COMMENT = 101;
static final int PHOTO_AUTHOR = 102;
static UriMatcher buildUriMatcher() {
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
final String authority = PhotoContract.CONTENT_AUTHORITY;
// matches photo/<any number> meaning any photo ID
matcher.addURI(authority, PhotoContract.PATH_PHOTO + "/#", PHOTO);
// matches photo/<photo id>/comment/ (comment text in ContentValues)
matcher.addURI(authority, PhotoContract.PATH_PHOTO + "/#/" + PhotoContract.PATH_COMMENT, PHOTO_COMMENT);
// matches photo/<photo id>/author/ (author name in ContentValues)
//matcher.addURI(authority, PhotoContract.PATH_PHOTO + "/#/" + PhotoContract.PATH_AUTHOR, PHOTO_AUTHOR);
return matcher;
}
@Override
public String getType(Uri uri) {
// Use the Uri Matcher to determine what kind of URI this is.
final int match = sUriMatcher.match(uri);
// Note: We always return single row of data, so content-type is always "a dir"
switch (match) {
case PHOTO_COMMENT:
return PhotoContract.PhotoEntry.CONTENT_TYPE;
case PHOTO_AUTHOR:
return PhotoContract.PhotoEntry.CONTENT_TYPE;
case PHOTO:
return PhotoContract.PhotoEntry.CONTENT_TYPE;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
MatrixCursor retCursor = new MatrixCursor(projection);
// open the specified image through the MediaStore to get base columns
// then open image file through ExifInterface to get detail columns
Long IMAGE_ID = PhotoContract.PhotoEntry.getImageIdFromUri(uri);
Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
baseUri = Uri.withAppendedPath(baseUri, ""+ IMAGE_ID);
// http://androidsnippets.com/get-file-path-of-gallery-image
// run query against MediaStore, projection = null means "get all fields"
Cursor c = getContext().getContentResolver().query(baseUri, null, null, null, null);
if (!c.moveToFirst()) {
return null;
}
// match returned fields against projection, and copy into row[]
Object[] row = new Object[projection.length];
int i = 0;
/* // Cursor.getType() Requires API level > 10...
for (String colName : projection) {
int idx = c.getColumnIndex(colName);
if (idx <= 0) return null; // ERROR
int colType = c.getType(idx);
switch (colType) {
case Cursor.FIELD_TYPE_INTEGER: {
row[i++] = c.getLong(idx);
break;
}
case Cursor.FIELD_TYPE_FLOAT: {
row[i++] = c.getFloat(idx);
break;
}
case Cursor.FIELD_TYPE_STRING: {
row[i++] = c.getString(idx);
break;
}
case Cursor.FIELD_TYPE_BLOB: {
row[i++] = c.getBlob(idx);
break;
}
}
}
*/
//http://stackoverflow.com/questions/11658239/cursor-gettype-for-api-level-11
CursorWrapper cw = (CursorWrapper)c;
Class<?> cursorWrapper = CursorWrapper.class;
Field mCursor = null;
try {
mCursor = cursorWrapper.getDeclaredField("mCursor");
mCursor.setAccessible(true);
AbstractWindowedCursor abstractWindowedCursor = (AbstractWindowedCursor)mCursor.get(cw);
CursorWindow cursorWindow = abstractWindowedCursor.getWindow();
int pos = abstractWindowedCursor.getPosition();
// NB! Expect resulting cursor to contain data in same order as projection!
for (String colName : projection) {
int idx = c.getColumnIndex(colName);
// simple solution: If column name NOT FOUND in MediaStore, assume it's an EXIF tag
// and skip
if (idx >= 0) {
if (cursorWindow.isNull(pos, idx)) {
//Cursor.FIELD_TYPE_NULL
row[i++] = null;
} else if (cursorWindow.isLong(pos, idx)) {
//Cursor.FIELD_TYPE_INTEGER
row[i++] = c.getLong(idx);
} else if (cursorWindow.isFloat(pos, idx)) {
//Cursor.FIELD_TYPE_FLOAT
row[i++] = c.getFloat(idx);
} else if (cursorWindow.isString(pos, idx)) {
//Cursor.FIELD_TYPE_STRING
row[i++] = c.getString(idx);
} else if (cursorWindow.isBlob(pos, idx)) {
//Cursor.FIELD_TYPE_BLOB
row[i++] = c.getBlob(idx);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
// have now handled the first i fields in projection. If there are any more, we expect
// these to be valid EXIF tags. Should obviously make this more robust...
try {
ExifInterface exif = new ExifInterface((String) row[2]);
while (i < projection.length) {
row[i] = exif.getAttribute("UserComment"); //projection[i]); // String (or null)
i++;
}
} catch (IOException e) {
e.printStackTrace();
}
retCursor.addRow(row);
return retCursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
// URI identifies IMAGE_ID and which EXIF tag to set; content://AUTH/photo/<image_id>/comment or /author
// first, get IMAGE_ID and prepare URI (to get file path from MediaStore)
Long IMAGE_ID = PhotoContract.PhotoEntry.getImageIdFromUri(uri);
Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
baseUri = Uri.withAppendedPath(baseUri, ""+ IMAGE_ID);
// get DATA (path/filename) from MediaStore -- only need that specific piece of information
String[] MS_projection = {MediaStore.Images.Media.DATA};
// http://androidsnippets.com/get-file-path-of-gallery-image
Cursor c = getContext().getContentResolver().query(baseUri, MS_projection, null, null, null);
if (!c.moveToFirst()) return -1; // can't get image path/filename...
String thumbData = c.getString(0);
// then, use URIMatcher to identify the "operation" (i.e., which tag to set)
final int match = sUriMatcher.match(uri);
String EXIF_tag;
switch (match) {
case PHOTO_COMMENT: {
EXIF_tag = "UserComment";
break;
}
case PHOTO_AUTHOR: {
EXIF_tag = "Author";
break;
}
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
if (EXIF_tag == null) return -1;
// finally, set tag in image
try {
ExifInterface exif = new ExifInterface(thumbData);
String textToSet = values.get(EXIF_tag).toString();
if (textToSet == null)
throw new IOException("Can't retrieve text to set from ContentValues, on key = " + EXIF_tag);
if (textToSet.length() > 48)
throw new IOException("Data too long (" + textToSet.length() + "), on key = " + EXIF_tag);
exif.setAttribute(EXIF_tag, textToSet);
exif.saveAttributes();
} catch (IOException e) {
e.printStackTrace();
}
return 1; // 1 image updated
}
}