title: Writing custom Content Provider for photos on phone (part 2)
body: <p>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.</p>
  
  <p>I've got a <a href="https://stackoverflow.com/questions/29675268/writing-custom-content-provider-for-photos-on-phone">previous post which goes on and on with this</a>, 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 <em>thumbnail</em> 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. </p>
  
  <p>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 <em>extending</em> that data with some EXIF tags from the JPEG file.</p>
  
  <p>Having found a <a href="https://stackoverflow.com/questions/15865979/how-to-implement-a-contentprovider-for-providing-image-to-gmail-facebook-evern">great post here on StackOverflow</a>, 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)?</p>
  
  <p>The URIs I'll support, are <code>context://AUTH/photo/#</code>, to get a specific image (given an <code>IMAGE_ID</code>, from the thumbnail). Furthermore, to make it a bit more interesting, I also want to be able to <em>write</em> data to the EXIF tag <code>UserComment</code>: <code>context://AUTH/photo/#/comment/*</code> (the comment String is the last parameter there). Does that look sensible?</p>
  
  <p><strong>Some questions:</strong> (updated)</p>
  
  <ol>
  <li><code>getType()</code> is confusing. Again, this is lending from the app course. When returning an image, I guess the type should always be <code>image/jpeg</code> (or maybe PNG)?</li>
  </ol>
  
  <blockquote>
    <p><strong>Edit:</strong> Learning more stuff, I now understand that the URI returned from the <code>insert()</code> 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 <code>null</code>, or an URI to the edited photo: <code>context://AUTH/photo/7271</code> (where 7271 mimicks the <code>PHOTO_ID</code>).</p>
  </blockquote>
  
  <p>Below is my (unfinished!) code. Please have a look, especially at the <code>query()</code> and <code>insert()</code> functions :-)</p>
  
  <h2>PhotoContract.java</h2>
  
  <pre><code>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??
          }
      }
  }
  </code></pre>
  
  <h2>PhotoProvider.java:</h2>
  
  <pre><code>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/&lt;any number&gt; meaning any photo ID
          matcher.addURI(authority, PhotoContract.PATH_PHOTO + "/#", PHOTO);
  
          // matches photo/&lt;photo id&gt;/comment/&lt;any comment&gt;
          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;
      }
  }
  </code></pre>