blob: 131c7e7dbda919166090ec2d3f246f20f43306bb [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.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.android.camera.ui.FilmStripView;
import com.android.camera.util.CameraUtil;
import com.android.camera.util.PhotoSphereHelper;
import com.android.camera2.R;
import java.io.File;
import java.text.DateFormat;
import java.util.Date;
/**
* 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.
*/
public abstract class LocalMediaData implements LocalData {
protected long id;
protected String title;
protected String mimeType;
protected long dateTakenInSeconds;
protected long dateModifiedInSeconds;
protected String path;
// width and height should be adjusted according to orientation.
protected int width;
protected int height;
protected long sizeInBytes;
protected double latitude;
protected double longitude;
/** The panorama metadata information of this media data. */
protected PhotoSphereHelper.PanoramaMetadata mPanoramaMetadata;
/** Used to load photo sphere metadata from image files. */
protected PanoramaMetadataLoader mPanoramaMetadataLoader = null;
/**
* Used for thumbnail loading optimization. True if this data
* has a corresponding visible view.
*/
protected Boolean mUsing = false;
@Override
public long getDateTaken() {
return dateTakenInSeconds;
}
@Override
public long getDateModified() {
return dateModifiedInSeconds;
}
@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(PhotoSphereHelper.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 PanoramaMetadataLoader.PanoramaMetadataCallback() {
@Override
public void onPanoramaMetadataLoaded(PhotoSphereHelper.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;
}
}
@Override
public double[] getLatLong() {
if (latitude == 0 && longitude == 0) {
return null;
}
return new double[] {
latitude, longitude
};
}
protected boolean isUsing() {
synchronized (mUsing) {
return mUsing;
}
}
@Override
public abstract int getViewType();
protected abstract BitmapLoadTask getBitmapLoadTask(
ImageView v, int decodeWidth, int decodeHeight);
public static class PhotoData extends LocalMediaData {
private static final String TAG = "CAM_PhotoData";
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;
public static final int COL_SIZE = 9;
public static final int COL_LATITUDE = 10;
public static final int COL_LONGITUDE = 11;
static final Uri CONTENT_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
static final String QUERY_ORDER = MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC, "
+ MediaStore.Images.ImageColumns._ID + " DESC";
/**
* These values should be kept in sync with column IDs (COL_*) above.
*/
static final String[] QUERY_PROJECTION = {
MediaStore.Images.ImageColumns._ID, // 0, int
MediaStore.Images.ImageColumns.TITLE, // 1, string
MediaStore.Images.ImageColumns.MIME_TYPE, // 2, string
MediaStore.Images.ImageColumns.DATE_TAKEN, // 3, int
MediaStore.Images.ImageColumns.DATE_MODIFIED, // 4, int
MediaStore.Images.ImageColumns.DATA, // 5, string
MediaStore.Images.ImageColumns.ORIENTATION, // 6, int, 0, 90, 180, 270
MediaStore.Images.ImageColumns.WIDTH, // 7, int
MediaStore.Images.ImageColumns.HEIGHT, // 8, int
MediaStore.Images.ImageColumns.SIZE, // 9, long
MediaStore.Images.ImageColumns.LATITUDE, // 10, double
MediaStore.Images.ImageColumns.LONGITUDE // 11, double
};
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 PhotoData buildFromCursor(Cursor c) {
PhotoData d = new PhotoData();
d.id = c.getLong(COL_ID);
d.title = c.getString(COL_TITLE);
d.mimeType = c.getString(COL_MIME_TYPE);
d.dateTakenInSeconds = c.getLong(COL_DATE_TAKEN);
d.dateModifiedInSeconds = 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;
}
d.sizeInBytes = c.getLong(COL_SIZE);
d.latitude = c.getDouble(COL_LATITUDE);
d.longitude = c.getDouble(COL_LONGITUDE);
return d;
}
@Override
public String toString() {
return "Photo:" + ",data=" + path + ",mimeType=" + mimeType
+ "," + width + "x" + height + ",orientation=" + orientation
+ ",date=" + new Date(dateTakenInSeconds);
}
@Override
public int getViewType() {
return TYPE_REMOVABLE_VIEW;
}
@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, MediaStore.Images.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 MediaDetails getMediaDetails(Context context) {
DateFormat formater = DateFormat.getDateTimeInstance();
MediaDetails mediaDetails = new MediaDetails();
mediaDetails.addDetail(MediaDetails.INDEX_TITLE, title);
mediaDetails.addDetail(MediaDetails.INDEX_WIDTH, width);
mediaDetails.addDetail(MediaDetails.INDEX_HEIGHT, height);
mediaDetails.addDetail(MediaDetails.INDEX_PATH, path);
mediaDetails.addDetail(MediaDetails.INDEX_DATETIME,
formater.format(new Date(dateModifiedInSeconds * 1000)));
if (sizeInBytes > 0)
mediaDetails.addDetail(MediaDetails.INDEX_SIZE, sizeInBytes);
MediaDetails.extractExifInfo(mediaDetails, path);
if (latitude != 0 && longitude != 0) {
mediaDetails.addDetail(MediaDetails.INDEX_LOCATION, latitude + ", " + longitude);
}
return mediaDetails;
}
@Override
public int getLocalDataType() {
if (mPanoramaMetadata != null && mPanoramaMetadata.mUsePanoramaViewer) {
return LOCAL_PHOTO_SPHERE;
}
return LOCAL_IMAGE;
}
@Override
public boolean refresh(ContentResolver resolver) {
Cursor c = resolver.query(
getContentUri(), QUERY_PROJECTION, null, null, null);
if (c == null || !c.moveToFirst()) {
return false;
}
PhotoData newData = buildFromCursor(c);
id = newData.id;
title = newData.title;
mimeType = newData.mimeType;
dateTakenInSeconds = newData.dateTakenInSeconds;
dateModifiedInSeconds = newData.dateModifiedInSeconds;
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;
}
}
}
public static class VideoData 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 = MediaStore.Video.VideoColumns.DATE_TAKEN + " DESC, "
+ MediaStore.Video.VideoColumns._ID + " DESC";
/**
* These values should be kept in sync with column IDs (COL_*) above.
*/
static final String[] QUERY_PROJECTION = {
MediaStore.Video.VideoColumns._ID, // 0, int
MediaStore.Video.VideoColumns.TITLE, // 1, string
MediaStore.Video.VideoColumns.MIME_TYPE, // 2, string
MediaStore.Video.VideoColumns.DATE_TAKEN, // 3, int
MediaStore.Video.VideoColumns.DATE_MODIFIED, // 4, int
MediaStore.Video.VideoColumns.DATA, // 5, string
MediaStore.Video.VideoColumns.WIDTH, // 6, int
MediaStore.Video.VideoColumns.HEIGHT, // 7, int
MediaStore.Video.VideoColumns.RESOLUTION // 8, string
};
private Uri mPlayUri;
static VideoData buildFromCursor(Cursor c) {
VideoData d = new VideoData();
d.id = c.getLong(COL_ID);
d.title = c.getString(COL_TITLE);
d.mimeType = c.getString(COL_MIME_TYPE);
d.dateTakenInSeconds = c.getLong(COL_DATE_TAKEN);
d.dateModifiedInSeconds = 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();
String rotation = null;
try {
retriever.setDataSource(d.path);
} catch (IllegalArgumentException ex) {
retriever.release();
Log.e(TAG, "MediaMetadataRetriever.setDataSource() fail:"
+ ex.getMessage());
return null;
}
rotation = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
if (d.width == 0 || d.height == 0) {
retrieveVideoDimension(retriever, d);
}
retriever.release();
if (d.width == 0 || d.height == 0) {
// Width or height is still not available.
Log.e(TAG, "Unable to retrieve dimension of video:" + d.path);
return null;
}
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(dateTakenInSeconds);
}
@Override
public int getViewType() {
return TYPE_REMOVABLE_VIEW;
}
@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, MediaStore.Video.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 MediaDetails getMediaDetails(Context context) {
// TODO: Return valid MediaDetails for videos.
return new MediaDetails();
}
@Override
public int getLocalDataType() {
return LOCAL_VIDEO;
}
@Override
public boolean refresh(ContentResolver resolver) {
Cursor c = resolver.query(
getContentUri(), QUERY_PROJECTION, null, null, null);
if (c == null && !c.moveToFirst()) {
return false;
}
VideoData newData = buildFromCursor(c);
if (newData == null) {
return false;
}
id = newData.id;
title = newData.title;
mimeType = newData.mimeType;
dateTakenInSeconds = newData.dateTakenInSeconds;
dateModifiedInSeconds = newData.dateModifiedInSeconds;
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 View.OnClickListener() {
@Override
public void onClick(View v) {
CameraUtil.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;
}
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;
}
}
/**
* Extracts video height/width if available. If
* unavailable, set to 0.
* @param retriever An initialized metadata retriever.
* @param d The {@link VideoData} whose width/height are to update.
*/
private static void retrieveVideoDimension(
MediaMetadataRetriever retriever, VideoData d) {
String val = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
d.width = (val == null) ? 0 : Integer.parseInt(val);
val = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
d.height = (val == null) ? 0 : Integer.parseInt(val);
}
}
/**
* An {@link AsyncTask} class that loads the bitmap in the background thread.
* Sub-classes should implement their own
* {@code BitmapLoadTask#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);
}
}
}