diff options
Diffstat (limited to 'src/com/android/camera/data/LocalData.java')
-rw-r--r-- | src/com/android/camera/data/LocalData.java | 726 |
1 files changed, 726 insertions, 0 deletions
diff --git a/src/com/android/camera/data/LocalData.java b/src/com/android/camera/data/LocalData.java new file mode 100644 index 000000000..efccfe332 --- /dev/null +++ b/src/com/android/camera/data/LocalData.java @@ -0,0 +1,726 @@ +/* + * 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 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; +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.gallery3d.R; +import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata; +import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper; + +import java.io.File; +import java.util.Comparator; +import java.util.Date; + +/** + * 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); + long getDateTaken(); + long getDateModified(); + String getTitle(); + boolean isDataActionSupported(int action); + boolean delete(Context c); + void onFullScreen(boolean fullScreen); + boolean canSwipeInFullScreen(); + String getPath(); + + static class NewestFirstComparator implements Comparator<LocalData> { + + /** 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. + + /** +<<<<<<< HEAD + * 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 subclassing BitmapLoadTask and overriding + * doInBackground() to return a bitmap. +======= + * 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. +>>>>>>> Add LocalDataAdapter and wrappers. + */ + 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; + } + } + + /** + * Returns the content URI of this data item. + */ + private Uri getContentUri() { + Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; + return baseUri.buildUpon().appendPath(String.valueOf(id)).build(); + } + + @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<Void, Void, Bitmap> { + 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 + 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 = CONTENT_URI.buildUpon() + .appendPath(String.valueOf(d.id)).build(); + 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 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 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; + } + } +} + |