/* * Copyright (C) 2012 mixi, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jp.mixi.compatibility.android.provider; import android.annotation.TargetApi; import android.app.Activity; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.database.ContentObserver; import android.database.Cursor; import android.media.ExifInterface; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.Handler; import android.provider.MediaStore; import android.text.TextUtils; import android.util.Log; import jp.mixi.compatibility.android.media.ExifInterfaceCompat; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.TimeZone; /** * Compatibility class for the issue of various implementations of camera feature. */ public class MediaStoreCompat { private static final int EXIF_DEGREE_FALLBACK_VALUE = -1; public static final String TAG = MediaStoreCompat.class.getSimpleName(); private static final String MEDIA_FILE_NAME_FORMAT = "yyyyMMdd_HHmmss"; private static final String MEDIA_FILE_EXTENSION = ".jpg"; private static final String MEDIA_FILE_PREFIX = "IMG_"; private static final String MEDIA_FILE_DIRECTORY = "Your application name"; private Context mContext; private ContentObserver mObserver; private ArrayList<PhotoContent> mRecentlyUpdatedPhotos; /** * Prepares to deal with various implementations of camera feature and {@link MediaStore}. * You should call {@link MediaStoreCompat#destroy()} on destroying context. * * @param context a context * @param handler a handler */ public MediaStoreCompat(Context context, Handler handler) { mContext = context; mObserver = new ContentObserver(handler) { @Override public void onChange(boolean selfChange) { super.onChange(selfChange); updateLatestPhotos(); } }; mContext.getContentResolver().registerContentObserver( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, mObserver); } /** * Checks whether the device has a camera feature or not. * @param context a context to check for camera feature. * @return true if the device has a camera feature. false otherwise. */ public boolean hasCameraFeature(Context context) { PackageManager pm = context.getApplicationContext().getPackageManager(); return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA); } /** * Invokes a camera capture activity. * @param activity the caller of the camera capture activity. * @param requestCode activity result handling id. * @return a file name to be saved as. */ public String invokeCameraCapture(Activity activity, int requestCode) { if (!hasCameraFeature(mContext)) return null; File toSave = getOutputFileUri(); if (toSave == null) return null; Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(toSave)); intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); activity.startActivityForResult(intent, requestCode); return toSave.toString(); } /** * Should be called on destroying context to dereference to the {@link ContentObserver}. */ public void destroy() { mContext.getContentResolver().unregisterContentObserver(mObserver); } /** * @param data the result {@link Intent} of a camera activity. * @param preparedUri a prepared photo uri to fetch photo data. * @return captured photo {@link Uri} */ public Uri getCapturedPhotoUri(Intent data, String preparedUri) { Uri captured = null; if (data != null) { captured = data.getData(); if (captured == null) data.getParcelableExtra(Intent.EXTRA_STREAM); } File prepared = new File(preparedUri.toString()); if (captured == null || captured.equals(Uri.fromFile(prepared))) { captured = findPhotoFromRecentlyTaken(prepared); if (captured == null) { captured = storeImage(prepared); prepared.delete(); } else { // Found. Compare the destination path // and delete app-private one if there is any copy outside of app-private directory. String realPath = getPathFromUri( mContext.getContentResolver(), captured); if (realPath != null) { if (! prepared.equals(new File(realPath))) prepared.delete(); } } } return captured; } /** * Deletes unnecessary files if exist. * @param uri to delete. */ public void cleanUp(String uri) { File file = new File(uri.toString()); if (file.exists()) file.delete(); } /** * Obtains a captured photo file path from content {@link Uri} of a {@link MediaStore} * @param resolver a content resolver * @param contentUri a content uri * @return captured photo file path string */ public static String getPathFromUri(ContentResolver resolver, Uri contentUri) { final String dataColumn = MediaStore.MediaColumns.DATA; Cursor cursor = null; try { cursor = resolver.query(contentUri, new String[] { dataColumn }, null, null, null); if (cursor == null || !cursor.moveToFirst()) return null; final int index = cursor.getColumnIndex(dataColumn); return cursor.getString(index); } finally { if (cursor != null) cursor.close(); } } /** * Copies file streams. * @param is input stream * @param os output stream * @return the number of bytes. * @throws IOException if something wrong with I/O. */ public static long copyFileStream(FileInputStream is, FileOutputStream os) throws IOException { FileChannel srcChannel = null; FileChannel destChannel = null; try { srcChannel = is.getChannel(); destChannel = os.getChannel(); return srcChannel.transferTo(0, srcChannel.size(), destChannel); } finally { if (srcChannel != null) srcChannel.close(); if (destChannel != null) destChannel.close(); } } /** * Obtains file {@link Uri} that refers to a recently taken photo. * @param file to search * @return photo file uri. */ private Uri findPhotoFromRecentlyTaken(File file) { if (mRecentlyUpdatedPhotos == null) updateLatestPhotos(); long filesize = file.length(); long taken = getExifDateTime(file.getAbsolutePath()); int maxPoint = 0; PhotoContent maxItem = null; for (PhotoContent item : mRecentlyUpdatedPhotos) { int point = 0; if (item.size == filesize) point++; if (item.taken == taken) point++; if (point > maxPoint) { maxPoint = point; maxItem = item; } } if (maxItem != null) { generateThumbnails(maxItem.id); return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, maxItem.id); } return null; } /** * Stores a file to media store with photo orientation and date that taken. * * @param file to store * @return a stored file uri */ private Uri storeImage(File file) { // store image to content provider try { // create parameters for Intent with filename ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.TITLE, file.getName()); values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); values.put(MediaStore.Images.Media.DESCRIPTION, "mixi Photo"); values.put(MediaStore.Images.Media.ORIENTATION, getExifOrientation(file.getAbsolutePath())); long date = getExifDateTime(file.getAbsolutePath()); if (date != -1) values.put(MediaStore.Images.Media.DATE_TAKEN, date); Uri imageUri = mContext.getContentResolver().insert( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); FileOutputStream fos = (FileOutputStream) mContext.getContentResolver().openOutputStream( imageUri); FileInputStream fis = new FileInputStream(file); copyFileStream(fis, fos); fos.close(); fis.close(); generateThumbnails(ContentUris.parseId(imageUri)); return imageUri; } catch (Exception e) { Log.w(TAG, "cannot insert", e); return null; } } /** * Request to update latest photos onto media store. */ private void updateLatestPhotos() { // retrieve the newest five images Cursor c = MediaStore.Images.Media.query(mContext.getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[] { MediaStore.Images.ImageColumns._ID, MediaStore.Images.ImageColumns.DATE_TAKEN, MediaStore.Images.ImageColumns.SIZE, }, null, null, MediaStore.Images.ImageColumns.DATE_ADDED + " DESC"); if (c != null) { try { int count = 0; mRecentlyUpdatedPhotos = new ArrayList<PhotoContent>(); while (c.moveToNext()) { PhotoContent item = new PhotoContent(); item.id = c.getLong(0); item.taken = c.getLong(1); item.size = c.getInt(2); mRecentlyUpdatedPhotos.add(item); if (++count > 5) break; } } finally { c.close(); } } } /** * Read exif info and get orientation value of the photo. * @param filepath to get exif. * @return exif orientation value */ private int getExifOrientation(String filepath) { int degree = 0; ExifInterface exif = null; try { // ExifInterface does not check whether file path is null or not, // so passing null file path argument to its constructor causing SIGSEGV. // We should avoid such a situation by checking file path string. exif = ExifInterfaceCompat.newInstance(filepath); } catch (IOException ex) { Log.e(TAG, "cannot read exif", ex); } if (exif != null) { int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, EXIF_DEGREE_FALLBACK_VALUE); if (orientation != EXIF_DEGREE_FALLBACK_VALUE) { // We only recognize a subset of orientation tag values. switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: degree = 90; break; case ExifInterface.ORIENTATION_ROTATE_180: degree = 180; break; case ExifInterface.ORIENTATION_ROTATE_270: degree = 270; break; default: // do nothing. break; } } } return degree; } /** * Read exif info and get datetime value of the photo. * @param filepath to get datetime * @return when a photo taken. */ private long getExifDateTime(String filepath) { ExifInterface exif = null; try { // ExifInterface does not check whether file path is null or not, // so passing null file path argument to its constructor causing SIGSEGV. // We should avoid such a situation by checking file path string. exif = ExifInterfaceCompat.newInstance(filepath); } catch (IOException ex) { Log.e(TAG, "cannot read exif", ex); } if (exif != null) { String date = exif.getAttribute(ExifInterface.TAG_DATETIME); if (!TextUtils.isEmpty(date)) { try { SimpleDateFormat formatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); formatter.setTimeZone(TimeZone.getTimeZone("UTC")); return formatter.parse(date).getTime(); } catch (ParseException e) { Log.d(TAG, "failed to parse date taken", e); } } } return -1; } /** * Create thumbnail images for a specified photo image. * * @param imageId referes to a photo image. */ private void generateThumbnails(long imageId) { try { MediaStore.Images.Thumbnails.getThumbnail(mContext.getContentResolver(), imageId, MediaStore.Images.Thumbnails.MINI_KIND, null); } catch (NullPointerException e) { // some MediaStores throws NPE, just ignore the result } } /** * Make output file uri based on a external storage directory. * @return a file */ @TargetApi(Build.VERSION_CODES.FROYO) private File getOutputFileUri() { File extDir = new File( Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES), MEDIA_FILE_DIRECTORY); if (!extDir.exists()) { if (!extDir.mkdirs()) return null; } String timeStamp = new SimpleDateFormat(MEDIA_FILE_NAME_FORMAT).format(new Date()); return new File(extDir.getPath() + File.separator + MEDIA_FILE_PREFIX + timeStamp + MEDIA_FILE_EXTENSION); } /** * Entity class for photo content. */ private static class PhotoContent { public long id; public long taken; public int size; } }