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 too great ;-) The whole point here is for me to learn about CP.
I've got a previous post which goes on and on with this, but I think I've managed to simplify my problem quite a bit. So, I'm working on a "gallery app". Since I don't know how and where thumbnail images are stored on the phone, I've decided to simply use MediaStore.Images.Thumbnails to access the thumbs, and show them in my GridView.
However, to fullfill the requirements of said course, I'll write a "PhotoProvider" to load single photos, full screen, in the DetailActivity. Also, for this to make some sense, and not just be a wrapper around MediaStore.Media.Images, I'm extending that data with some EXIF tags from the JPEG file.
Having found a great post here on StackOverflow, and continuing to wrangle the source code provided in class, I've come up with the following classes. Maybe you want to take a look, and help me out (point me in the right direction)?
The URIs I'll support, are context://AUTH/photo/#
, to get a specific image (given an IMAGE_ID
, from the thumbnail). Furthermore, to make it a bit more interesting, I also want to be able to write data to the EXIF tag UserComment
: context://AUTH/photo/#/comment/*
(the comment String is the last parameter there). Does that look sensible?
Some questions: (updated)
getType()
is confusing. Again, this is lending from the app course. When returning an image, I guess the type should always be image/jpeg
(or maybe PNG)?
Edit: Learning more stuff, I now understand that the URI returned from the insert()
method is, I guess, optional, but it often useful to return a "link" (i.e. URI) to the new (inserted) data! For my case, after updating the EXIF tags, I could return either null
, or an URI to the edited photo: context://AUTH/photo/7271
(where 7271 mimicks the PHOTO_ID
).
Below is my (unfinished!) code. Please have a look, especially at the query()
and insert()
functions :-)
PhotoContract.java
package com.example.android.galleri.app.data;
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 = "no.myapp.android.galleri.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 class PhotoEntry {
public static final Uri CONTENT_URI =
BASE_CONTENT_URI.buildUpon().appendPath(PATH_PHOTO).build();
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";
// should these simply be image/png??
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 buildPhotoWithId(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)); // TODO: is it position 1??
}
// get comment to set in EXIF tag
public static String getCommentFromUri(Uri uri) {
return uri.getPathSegments().get(2); // TODO: is it position 2??
}
}
}
PhotoProvider.java:
package com.example.android.galleri.app.data;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
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;
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_SET_COMMENT = 200;
static UriMatcher buildUriMatcher() {
// 1) The code passed into the constructor represents the code to return for the root
// URI. It's common to use NO_MATCH as the code for this case. Add the constructor below.
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
final String authority = PhotoContract.CONTENT_AUTHORITY;
// 2) Use the addURI function to match each of the types. Use the constants from
// WeatherContract to help define the types to the UriMatcher.
// matches photo/<any number> meaning any photo ID
matcher.addURI(authority, PhotoContract.PATH_PHOTO + "/#", PHOTO);
// matches photo/<photo id>/comment/<any comment>
matcher.addURI(authority, PhotoContract.PATH_PHOTO + "/#/" + PhotoContract.PATH_COMMENT + "/*", PHOTO_SET_COMMENT);
// 3) Return the new matcher!
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);
switch (match) {
case PHOTO_SET_COMMENT:
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; // enough?
}
@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 = Uri.parse("content://media/external/images/media");
Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
baseUri = Uri.withAppendedPath(baseUri, ""+ IMAGE_ID);
String[] MS_projection = {
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DESCRIPTION,
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.TITLE,
MediaStore.Images.Media.SIZE,
MediaStore.Images.Media.ORIENTATION};
// http://androidsnippets.com/get-file-path-of-gallery-image
Cursor c = getContext().getContentResolver().query(baseUri, MS_projection, null, null, null);
// dump fields (the ones we want -- assuming for now we want ALL fields, in SAME ORDER) into MatrixCursor
Object[] row = new Object[projection.length];
row[0] = c.getString(0); // DISPLAY_NAME
row[1] = c.getBlob(1); // etc
row[2] = c.getString(2);
row[3] = c.getLong(3);
row[4] = c.getLong(4);
row[5] = c.getString(5);
row[6] = c.getInt(6);
row[7] = c.getInt(7);
// NB! Extra +2 fields, for EXIF data.
try {
ExifInterface exif = new ExifInterface((String)row[1]);
row[8] = exif.getAttribute("UserComment");
row[9] = exif.getAttribute("Author");
} catch (IOException e) {
e.printStackTrace();
}
retCursor.addRow(row);
return retCursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
String comment_to_set = PhotoContract.PhotoEntry.getCommentFromUri(uri);
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);
String thumbData = c.getString(0);
try {
ExifInterface exif = new ExifInterface(thumbData);
exif.setAttribute("UserComment", comment_to_set);
exif.saveAttributes();
} catch (IOException e) {
e.printStackTrace();
}
return PhotoContract.PhotoEntry.buildPhotoWithId(IMAGE_ID); // return URI to this specific image
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}
See Question&Answers more detail:
os