blob: 74055230f450401e2bb3b9110f51427b04c9c027 [file] [log] [blame]
/*
* 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.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.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);
/**
* 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<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.
/**
* 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<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
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;
}
}
}