为 phone 上的照片编写自定义 Content Provider(第 2 部分)

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

我正在为我的应用开发自定义内容提供程序。这是我正在学习 Android 应用程序的课程的一部分,所以请不要指望做这一切的理由太好 ;-) 这里的重点是让我了解 CP。

我有一个 previous post which goes on and on with this,但我想我已经成功地简化了我的问题。所以,我正在研究 "gallery app"。由于我不知道 thumbnail 图像如何以及在何处存储在 phone 上,我决定简单地使用 MediaStore.Images.Thumbnails 访问缩略图,并显示它们在我的 GridView 中。

但是,为了满足上述课程的要求,我将编写 "PhotoProvider" 以在 DetailActivity 中全屏加载单张照片。此外,为了使这更有意义,而不仅仅是 MediaStore.Media.Images 的包装,我 扩展 使用 JPEG 文件中的一些 EXIF 标签的数据。

找到 great post here on Whosebug 并继续研究 class 中提供的源代码后,我得出了以下 classes。也许你想看一看,并帮助我(指出正确的方向)?

我支持的 URI 是 context://AUTH/photo/#,用于获取特定图像(从缩略图中给定 IMAGE_ID)。此外,为了让它更有趣一点,我还希望能够 write 数据到EXIF标签 UserComment: context://AUTH/photo/#/comment/* (评论字符串是最后一个参数)。这样看起来合理吗?

一些问题:(已更新)

  1. getType() 令人困惑。同样,这是从应用程序课程中借出的。返回图像时,我想类型应该总是 image/jpeg (或者 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).

下面是我的(未完成的!)代码。请看一下,尤其是 query()insert() 函数:-)

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;
    }
}

我相信我已经弄明白了,而且它也没有那么神秘。编写自定义内容,在这种情况下,提供者实际上归结为实现我自己的 queryupdate 方法。我不需要任何 insertdelete,因为我只是 阅读 MediaStore。

首先,我设计了两个 URI;一个用于查询,一个用于更新。查询 URI 遵循 content://AUTH/photo/# 模式,其中 # 是来自 MediaStore.Images.Thumbnails 的 IMAGE_ID。此方法本质上是针对 MediaStore.Images.Media 运行另一个查询,以获取特定图像上的 "standard" 数据,然后通过 ExifInterface 获取图像上的一些额外元数据。所有这些都在 MatrixCursor.

中返回

更新 URI 匹配模式 content://AUTH/photo/#/comment,然后 设置 (写入)具有该 ID 的文件中的 UserComment EXIF 标签 --再次通过 ExifInterface

为了回答我的问题,我发布了来自我的 PhotoContract 和 PhotoProvider classes 的更新代码。本质上,如果您正在编写一个内容提供程序,它 接口 SQLite 数据库——但设备上的其他东西——您需要做的就是以适当的方法实现这些操作在您的提供商中 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;
                }
            }
        }
        */

        //
        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
    }
}