/* * Copyright (C) 2013 The Android Open Source Project * * 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 com.android.camera.data; import java.io.File; import java.util.Comparator; import java.util.Date; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.AsyncTask; import android.provider.MediaStore; import android.provider.MediaStore.Images.ImageColumns; import android.provider.MediaStore.Video.VideoColumns; import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import com.android.camera.Util; import com.android.camera.data.PanoramaMetadataLoader.PanoramaMetadataCallback; import com.android.camera.ui.FilmStripView; import com.android.camera.util.PhotoSphereHelper.PanoramaMetadata; import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper; import com.android.camera2.R; /** * An abstract interface that represents the local media data. Also implements * Comparable interface so we can sort in DataAdapter. */ public interface LocalData extends FilmStripView.ImageData { static final String TAG = "CAM_LocalData"; public static final int ACTION_NONE = 0; public static final int ACTION_PLAY = 1; public static final int ACTION_DELETE = (1 << 1); View getView(Context c, int width, int height, Drawable placeHolder); /** * Gets the date when this data is created. The returned date is also used * for sorting data. * * @return The date when this data is created. * @see {@link NewestFirstComparator} */ long getDateTaken(); /** * Gets the date when this data is modified. The returned date is also used * for sorting data. * * @return The date when this data is modified. * @see {@link NewestFirstComparator} */ long getDateModified(); /** Gets the title of this data */ String getTitle(); /** * Checks if the data actions (delete/play ...) can be applied on this data. * * @param actions The actions to check. * @return Whether all the actions are supported. */ boolean isDataActionSupported(int actions); boolean delete(Context c); void onFullScreen(boolean fullScreen); /** Returns {@code true} if it allows swipe to filmstrip in full screen. */ boolean canSwipeInFullScreen(); /** * Returns the path to the data on the storage. * * @return Empty path if there's none. */ String getPath(); /** * Returns the content URI of this data item. * * @return {@code Uri.EMPTY} if not valid. */ Uri getContentUri(); /** * Refresh the data content. * * @param resolver {@link ContentResolver} to refresh the data. * @return {@code true} if success, {@code false} otherwise. */ boolean refresh(ContentResolver resolver); static class NewestFirstComparator implements Comparator { /** Compare taken/modified date of LocalData in descent order to make newer data in the front. The negative numbers here are always considered "bigger" than positive ones. Thus, if any one of the numbers is negative, the logic is reversed. */ private static int compareDate(long v1, long v2) { if (v1 >= 0 && v2 >= 0) { return ((v1 < v2) ? 1 : ((v1 > v2) ? -1 : 0)); } return ((v2 < v1) ? 1 : ((v2 > v1) ? -1 : 0)); } @Override public int compare(LocalData d1, LocalData d2) { int cmp = compareDate(d1.getDateTaken(), d2.getDateTaken()); if (cmp == 0) { cmp = compareDate(d1.getDateModified(), d2.getDateModified()); } if (cmp == 0) { cmp = d1.getTitle().compareTo(d2.getTitle()); } return cmp; } } // Implementations below. /** * A base class for all the local media files. The bitmap is loaded in * background thread. Subclasses should implement their own background * loading thread by sub-classing BitmapLoadTask and overriding * doInBackground() to return a bitmap. */ abstract static class LocalMediaData implements LocalData { protected long id; protected String title; protected String mimeType; protected long dateTaken; protected long dateModified; protected String path; // width and height should be adjusted according to orientation. protected int width; protected int height; /** The panorama metadata information of this media data. */ private PanoramaMetadata mPanoramaMetadata; /** Used to load photo sphere metadata from image files. */ private PanoramaMetadataLoader mPanoramaMetadataLoader = null; // true if this data has a corresponding visible view. protected Boolean mUsing = false; @Override public long getDateTaken() { return dateTaken; } @Override public long getDateModified() { return dateModified; } @Override public String getTitle() { return new String(title); } @Override public int getWidth() { return width; } @Override public int getHeight() { return height; } @Override public String getPath() { return path; } @Override public boolean isUIActionSupported(int action) { return false; } @Override public boolean isDataActionSupported(int action) { return false; } @Override public boolean delete(Context ctx) { File f = new File(path); return f.delete(); } @Override public void viewPhotoSphere(PanoramaViewHelper helper) { helper.showPanorama(getContentUri()); } @Override public void isPhotoSphere(Context context, final PanoramaSupportCallback callback) { // If we already have metadata, use it. if (mPanoramaMetadata != null) { callback.panoramaInfoAvailable(mPanoramaMetadata.mUsePanoramaViewer, mPanoramaMetadata.mIsPanorama360); } // Otherwise prepare a loader, if we don't have one already. if (mPanoramaMetadataLoader == null) { mPanoramaMetadataLoader = new PanoramaMetadataLoader(getContentUri()); } // Load the metadata asynchronously. mPanoramaMetadataLoader.getPanoramaMetadata(context, new PanoramaMetadataCallback() { @Override public void onPanoramaMetadataLoaded(PanoramaMetadata metadata) { // Store the metadata and remove the loader to free up space. mPanoramaMetadata = metadata; mPanoramaMetadataLoader = null; callback.panoramaInfoAvailable(metadata.mUsePanoramaViewer, metadata.mIsPanorama360); } }); } @Override public void onFullScreen(boolean fullScreen) { // do nothing. } @Override public boolean canSwipeInFullScreen() { return true; } protected ImageView fillImageView(Context ctx, ImageView v, int decodeWidth, int decodeHeight, Drawable placeHolder) { v.setScaleType(ImageView.ScaleType.FIT_XY); v.setImageDrawable(placeHolder); BitmapLoadTask task = getBitmapLoadTask(v, decodeWidth, decodeHeight); task.execute(); return v; } @Override public View getView(Context ctx, int decodeWidth, int decodeHeight, Drawable placeHolder) { return fillImageView(ctx, new ImageView(ctx), decodeWidth, decodeHeight, placeHolder); } @Override public void prepare() { synchronized (mUsing) { mUsing = true; } } @Override public void recycle() { synchronized (mUsing) { mUsing = false; } } protected boolean isUsing() { synchronized (mUsing) { return mUsing; } } @Override public abstract int getType(); protected abstract BitmapLoadTask getBitmapLoadTask( ImageView v, int decodeWidth, int decodeHeight); /** * An AsyncTask class that loads the bitmap in the background thread. * Sub-classes should implement their own "protected Bitmap doInBackground(Void... )" */ protected abstract class BitmapLoadTask extends AsyncTask { protected ImageView mView; protected BitmapLoadTask(ImageView v) { mView = v; } @Override protected void onPostExecute(Bitmap bitmap) { if (!isUsing()) return; if (bitmap == null) { Log.e(TAG, "Failed decoding bitmap for file:" + path); return; } BitmapDrawable d = new BitmapDrawable(bitmap); mView.setScaleType(ImageView.ScaleType.FIT_XY); mView.setImageDrawable(d); } } } static class Photo extends LocalMediaData { public static final int COL_ID = 0; public static final int COL_TITLE = 1; public static final int COL_MIME_TYPE = 2; public static final int COL_DATE_TAKEN = 3; public static final int COL_DATE_MODIFIED = 4; public static final int COL_DATA = 5; public static final int COL_ORIENTATION = 6; public static final int COL_WIDTH = 7; public static final int COL_HEIGHT = 8; static final Uri CONTENT_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; static final String QUERY_ORDER = ImageColumns.DATE_TAKEN + " DESC, " + ImageColumns._ID + " DESC"; /** * These values should be kept in sync with column IDs (COL_*) above. */ static final String[] QUERY_PROJECTION = { ImageColumns._ID, // 0, int ImageColumns.TITLE, // 1, string ImageColumns.MIME_TYPE, // 2, string ImageColumns.DATE_TAKEN, // 3, int ImageColumns.DATE_MODIFIED, // 4, int ImageColumns.DATA, // 5, string ImageColumns.ORIENTATION, // 6, int, 0, 90, 180, 270 ImageColumns.WIDTH, // 7, int ImageColumns.HEIGHT, // 8, int }; private static final int mSupportedUIActions = FilmStripView.ImageData.ACTION_DEMOTE | FilmStripView.ImageData.ACTION_PROMOTE; private static final int mSupportedDataActions = LocalData.ACTION_DELETE; /** 32K buffer. */ private static final byte[] DECODE_TEMP_STORAGE = new byte[32 * 1024]; /** from MediaStore, can only be 0, 90, 180, 270 */ public int orientation; static Photo buildFromCursor(Cursor c) { Photo d = new Photo(); d.id = c.getLong(COL_ID); d.title = c.getString(COL_TITLE); d.mimeType = c.getString(COL_MIME_TYPE); d.dateTaken = c.getLong(COL_DATE_TAKEN); d.dateModified = c.getLong(COL_DATE_MODIFIED); d.path = c.getString(COL_DATA); d.orientation = c.getInt(COL_ORIENTATION); d.width = c.getInt(COL_WIDTH); d.height = c.getInt(COL_HEIGHT); if (d.width <= 0 || d.height <= 0) { Log.w(TAG, "Warning! zero dimension for " + d.path + ":" + d.width + "x" + d.height); BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inJustDecodeBounds = true; BitmapFactory.decodeFile(d.path, opts); if (opts.outWidth != -1 && opts.outHeight != -1) { d.width = opts.outWidth; d.height = opts.outHeight; } else { Log.w(TAG, "Warning! dimension decode failed for " + d.path); Bitmap b = BitmapFactory.decodeFile(d.path); if (b == null) { return null; } d.width = b.getWidth(); d.height = b.getHeight(); } } if (d.orientation == 90 || d.orientation == 270) { int b = d.width; d.width = d.height; d.height = b; } return d; } @Override public String toString() { return "Photo:" + ",data=" + path + ",mimeType=" + mimeType + "," + width + "x" + height + ",orientation=" + orientation + ",date=" + new Date(dateTaken); } @Override public int getType() { return TYPE_PHOTO; } @Override public boolean isUIActionSupported(int action) { return ((action & mSupportedUIActions) == action); } @Override public boolean isDataActionSupported(int action) { return ((action & mSupportedDataActions) == action); } @Override public boolean delete(Context c) { ContentResolver cr = c.getContentResolver(); cr.delete(CONTENT_URI, ImageColumns._ID + "=" + id, null); return super.delete(c); } @Override public Uri getContentUri() { Uri baseUri = CONTENT_URI; return baseUri.buildUpon().appendPath(String.valueOf(id)).build(); } @Override public boolean refresh(ContentResolver resolver) { Cursor c = resolver.query( getContentUri(), QUERY_PROJECTION, null, null, null); if (c == null || !c.moveToFirst()) { return false; } Photo newData = buildFromCursor(c); id = newData.id; title = newData.title; mimeType = newData.mimeType; dateTaken = newData.dateTaken; dateModified = newData.dateModified; path = newData.path; orientation = newData.orientation; width = newData.width; height = newData.height; return true; } @Override protected BitmapLoadTask getBitmapLoadTask( ImageView v, int decodeWidth, int decodeHeight) { return new PhotoBitmapLoadTask(v, decodeWidth, decodeHeight); } private final class PhotoBitmapLoadTask extends BitmapLoadTask { private int mDecodeWidth; private int mDecodeHeight; public PhotoBitmapLoadTask(ImageView v, int decodeWidth, int decodeHeight) { super(v); mDecodeWidth = decodeWidth; mDecodeHeight = decodeHeight; } @Override protected Bitmap doInBackground(Void... v) { BitmapFactory.Options opts = null; Bitmap b; int sample = 1; while (mDecodeWidth * sample < width || mDecodeHeight * sample < height) { sample *= 2; } opts = new BitmapFactory.Options(); opts.inSampleSize = sample; opts.inTempStorage = DECODE_TEMP_STORAGE; if (isCancelled() || !isUsing()) { return null; } b = BitmapFactory.decodeFile(path, opts); if (orientation != 0) { if (isCancelled() || !isUsing()) { return null; } Matrix m = new Matrix(); m.setRotate(orientation); b = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false); } return b; } } } static class Video extends LocalMediaData { public static final int COL_ID = 0; public static final int COL_TITLE = 1; public static final int COL_MIME_TYPE = 2; public static final int COL_DATE_TAKEN = 3; public static final int COL_DATE_MODIFIED = 4; public static final int COL_DATA = 5; public static final int COL_WIDTH = 6; public static final int COL_HEIGHT = 7; public static final int COL_RESOLUTION = 8; static final Uri CONTENT_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; private static final int mSupportedUIActions = FilmStripView.ImageData.ACTION_DEMOTE | FilmStripView.ImageData.ACTION_PROMOTE; private static final int mSupportedDataActions = LocalData.ACTION_DELETE | LocalData.ACTION_PLAY; static final String QUERY_ORDER = VideoColumns.DATE_TAKEN + " DESC, " + VideoColumns._ID + " DESC"; /** * These values should be kept in sync with column IDs (COL_*) above. */ static final String[] QUERY_PROJECTION = { VideoColumns._ID, // 0, int VideoColumns.TITLE, // 1, string VideoColumns.MIME_TYPE, // 2, string VideoColumns.DATE_TAKEN, // 3, int VideoColumns.DATE_MODIFIED, // 4, int VideoColumns.DATA, // 5, string VideoColumns.WIDTH, // 6, int VideoColumns.HEIGHT, // 7, int VideoColumns.RESOLUTION // 8, string }; private Uri mPlayUri; static Video buildFromCursor(Cursor c) { Video d = new Video(); d.id = c.getLong(COL_ID); d.title = c.getString(COL_TITLE); d.mimeType = c.getString(COL_MIME_TYPE); d.dateTaken = c.getLong(COL_DATE_TAKEN); d.dateModified = c.getLong(COL_DATE_MODIFIED); d.path = c.getString(COL_DATA); d.width = c.getInt(COL_WIDTH); d.height = c.getInt(COL_HEIGHT); d.mPlayUri = d.getContentUri(); MediaMetadataRetriever retriever = new MediaMetadataRetriever(); retriever.setDataSource(d.path); String rotation = retriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); if (d.width == 0 || d.height == 0) { d.width = Integer.parseInt(retriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)); d.height = Integer.parseInt(retriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)); } retriever.release(); if (rotation != null && (rotation.equals("90") || rotation.equals("270"))) { int b = d.width; d.width = d.height; d.height = b; } return d; } @Override public String toString() { return "Video:" + ",data=" + path + ",mimeType=" + mimeType + "," + width + "x" + height + ",date=" + new Date(dateTaken); } @Override public int getType() { return TYPE_PHOTO; } @Override public boolean isUIActionSupported(int action) { return ((action & mSupportedUIActions) == action); } @Override public boolean isDataActionSupported(int action) { return ((action & mSupportedDataActions) == action); } @Override public boolean delete(Context ctx) { ContentResolver cr = ctx.getContentResolver(); cr.delete(CONTENT_URI, VideoColumns._ID + "=" + id, null); return super.delete(ctx); } @Override public Uri getContentUri() { Uri baseUri = CONTENT_URI; return baseUri.buildUpon().appendPath(String.valueOf(id)).build(); } @Override public boolean refresh(ContentResolver resolver) { Cursor c = resolver.query( getContentUri(), QUERY_PROJECTION, null, null, null); if (c == null && !c.moveToFirst()) { return false; } Video newData = buildFromCursor(c); id = newData.id; title = newData.title; mimeType = newData.mimeType; dateTaken = newData.dateTaken; dateModified = newData.dateModified; path = newData.path; width = newData.width; height = newData.height; mPlayUri = newData.mPlayUri; return true; } @Override public View getView(final Context ctx, int decodeWidth, int decodeHeight, Drawable placeHolder) { // ImageView for the bitmap. ImageView iv = new ImageView(ctx); iv.setLayoutParams(new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)); fillImageView(ctx, iv, decodeWidth, decodeHeight, placeHolder); // ImageView for the play icon. ImageView icon = new ImageView(ctx); icon.setImageResource(R.drawable.ic_control_play); icon.setScaleType(ImageView.ScaleType.CENTER); icon.setLayoutParams(new FrameLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); icon.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Util.playVideo(ctx, mPlayUri, title); } }); FrameLayout f = new FrameLayout(ctx); f.addView(iv); f.addView(icon); return f; } @Override protected BitmapLoadTask getBitmapLoadTask( ImageView v, int decodeWidth, int decodeHeight) { return new VideoBitmapLoadTask(v); } private final class VideoBitmapLoadTask extends BitmapLoadTask { public VideoBitmapLoadTask(ImageView v) { super(v); } @Override protected Bitmap doInBackground(Void... v) { if (isCancelled() || !isUsing()) { return null; } android.media.MediaMetadataRetriever retriever = new MediaMetadataRetriever(); retriever.setDataSource(path); byte[] data = retriever.getEmbeddedPicture(); Bitmap bitmap = null; if (isCancelled() || !isUsing()) { retriever.release(); return null; } if (data != null) { bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); } if (bitmap == null) { bitmap = retriever.getFrameAtTime(); } retriever.release(); return bitmap; } } } /** * A LocalData that does nothing but only shows a view. */ public static class LocalViewData implements LocalData { private int mWidth; private int mHeight; private View mView; private long mDateTaken; private long mDateModified; public LocalViewData(View v, int width, int height, int dateTaken, int dateModified) { mView = v; mWidth = width; mHeight = height; mDateTaken = dateTaken; mDateModified = dateModified; } @Override public long getDateTaken() { return mDateTaken; } @Override public long getDateModified() { return mDateModified; } @Override public String getTitle() { return ""; } @Override public int getWidth() { return mWidth; } @Override public int getHeight() { return mHeight; } @Override public int getType() { return FilmStripView.ImageData.TYPE_PHOTO; } @Override public String getPath() { return ""; } @Override public Uri getContentUri() { return Uri.EMPTY; } @Override public boolean refresh(ContentResolver resolver) { return false; } @Override public boolean isUIActionSupported(int action) { return false; } @Override public boolean isDataActionSupported(int action) { return false; } @Override public boolean delete(Context c) { return false; } @Override public View getView(Context c, int width, int height, Drawable placeHolder) { return mView; } @Override public void prepare() { // do nothing. } @Override public void recycle() { // do nothing. } @Override public void isPhotoSphere(Context context, PanoramaSupportCallback callback) { // Not a photo sphere panorama. callback.panoramaInfoAvailable(false, false); } @Override public void viewPhotoSphere(PanoramaViewHelper helper) { // do nothing. } @Override public void onFullScreen(boolean fullScreen) { // do nothing. } @Override public boolean canSwipeInFullScreen() { return true; } } }