Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
423 views
in Technique[技术] by (71.8m points)

android - Writing custom Content Provider for photos on phone (part 2)

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)

  1. 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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.

PhotoContract

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
        }

    }
}

PhotoProvider

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(b

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...