blob: 9841070f8939f425c4548a5be980d85c868d35a0 [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.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.media.CamcorderProfile;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.LayoutInflater;
import android.view.Gravity;
import android.view.View;
import android.widget.ImageView;
import com.android.camera.Storage;
import com.android.camera.debug.Log;
import com.android.camera.util.CameraUtil;
import com.android.camera2.R;
import com.bumptech.glide.Glide;
import com.bumptech.glide.presenter.target.ImageViewTarget;
import com.bumptech.glide.presenter.target.Target;
import java.io.File;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
* 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 {
/** The minimum id to use to query for all media at a given media store uri */
static final int QUERY_ALL_MEDIA_ID = -1;
private static final String CAMERA_PATH = Storage.DIRECTORY + "%";
private static final String SELECT_BY_PATH = MediaStore.MediaColumns.DATA + " LIKE ?";
protected final long mContentId;
protected final String mTitle;
protected final String mMimeType;
protected final long mDateTakenInSeconds;
protected final long mDateModifiedInSeconds;
protected final String mPath;
// width and height should be adjusted according to orientation.
protected final int mWidth;
protected final int mHeight;
protected final long mSizeInBytes;
protected final double mLatitude;
protected final double mLongitude;
protected final Bundle mMetaData;
/**
* Used for thumbnail loading optimization. True if this data has a
* corresponding visible view.
*/
protected Boolean mUsing = false;
public LocalMediaData(long contentId, String title, String mimeType,
long dateTakenInSeconds, long dateModifiedInSeconds, String path,
int width, int height, long sizeInBytes, double latitude,
double longitude) {
mContentId = contentId;
mTitle = title;
mMimeType = mimeType;
mDateTakenInSeconds = dateTakenInSeconds;
mDateModifiedInSeconds = dateModifiedInSeconds;
mPath = path;
mWidth = width;
mHeight = height;
mSizeInBytes = sizeInBytes;
mLatitude = latitude;
mLongitude = longitude;
mMetaData = new Bundle();
}
private interface CursorToLocalData {
public LocalData build(Cursor cursor);
}
private static List<LocalData> queryLocalMediaData(ContentResolver contentResolver,
Uri contentUri, String[] projection, long minimumId, String orderBy,
CursorToLocalData builder) {
String selection = SELECT_BY_PATH + " AND " + MediaStore.MediaColumns._ID + " > ?";
String[] selectionArgs = new String[] { CAMERA_PATH, Long.toString(minimumId) };
Cursor cursor = contentResolver.query(contentUri, projection,
selection, selectionArgs, orderBy);
List<LocalData> result = new ArrayList<LocalData>();
if (cursor != null) {
while (cursor.moveToNext()) {
LocalData data = builder.build(cursor);
if (data != null) {
result.add(data);
} else {
final int dataIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA);
Log.e(TAG, "Error loading data:" + cursor.getString(dataIndex));
}
}
cursor.close();
}
return result;
}
@Override
public long getDateTaken() {
return mDateTakenInSeconds;
}
@Override
public long getDateModified() {
return mDateModifiedInSeconds;
}
@Override
public long getContentId() {
return mContentId;
}
@Override
public String getTitle() {
return mTitle;
}
@Override
public int getWidth() {
return mWidth;
}
@Override
public int getHeight() {
return mHeight;
}
@Override
public int getRotation() {
return 0;
}
@Override
public String getPath() {
return mPath;
}
@Override
public long getSizeInBytes() {
return mSizeInBytes;
}
@Override
public boolean isUIActionSupported(int action) {
return false;
}
@Override
public boolean isDataActionSupported(int action) {
return false;
}
@Override
public boolean delete(Context context) {
File f = new File(mPath);
return f.delete();
}
@Override
public void onFullScreen(boolean fullScreen) {
// do nothing.
}
@Override
public boolean canSwipeInFullScreen() {
return true;
}
protected ImageView fillImageView(Context context, ImageView v,
int decodeWidth, int decodeHeight, int placeHolderResourceId,
LocalDataAdapter adapter, boolean isInProgress) {
SizedImageViewTarget target = (SizedImageViewTarget) v.getTag();
if (target != null) {
target = new SizedImageViewTarget(v);
v.setTag(target);
}
Glide.with(context)
.load(mPath)
.fitCenter()
.placeholder(placeHolderResourceId)
.into(target);
return v;
}
@Override
public View getView(Context context, View recycled, int decodeWidth, int decodeHeight,
int placeHolderResourceId, LocalDataAdapter adapter, boolean isInProgress) {
final ImageView imageView;
if (recycled != null) {
imageView = (ImageView) recycled;
} else {
imageView = new ImageView(context);
}
return fillImageView(context, imageView, decodeWidth, decodeHeight,
placeHolderResourceId, adapter, isInProgress);
}
@Override
public void loadFullImage(Context context, int width, int height, View view,
LocalDataAdapter adapter) {
// Default is do nothing.
// Can be implemented by sub-classes.
}
@Override
public void prepare() {
synchronized (mUsing) {
mUsing = true;
}
}
@Override
public void recycle(View view) {
synchronized (mUsing) {
mUsing = false;
}
}
@Override
public double[] getLatLong() {
if (mLatitude == 0 && mLongitude == 0) {
return null;
}
return new double[] {
mLatitude, mLongitude
};
}
protected boolean isUsing() {
synchronized (mUsing) {
return mUsing;
}
}
@Override
public String getMimeType() {
return mMimeType;
}
@Override
public MediaDetails getMediaDetails(Context context) {
DateFormat dateFormatter = DateFormat.getDateTimeInstance();
MediaDetails mediaDetails = new MediaDetails();
mediaDetails.addDetail(MediaDetails.INDEX_TITLE, mTitle);
mediaDetails.addDetail(MediaDetails.INDEX_WIDTH, mWidth);
mediaDetails.addDetail(MediaDetails.INDEX_HEIGHT, mHeight);
mediaDetails.addDetail(MediaDetails.INDEX_PATH, mPath);
mediaDetails.addDetail(MediaDetails.INDEX_DATETIME,
dateFormatter.format(new Date(mDateModifiedInSeconds * 1000)));
if (mSizeInBytes > 0) {
mediaDetails.addDetail(MediaDetails.INDEX_SIZE, mSizeInBytes);
}
if (mLatitude != 0 && mLongitude != 0) {
String locationString = String.format(Locale.getDefault(), "%f, %f", mLatitude,
mLongitude);
mediaDetails.addDetail(MediaDetails.INDEX_LOCATION, locationString);
}
return mediaDetails;
}
@Override
public abstract int getViewType();
@Override
public Bundle getMetadata() {
return mMetaData;
}
@Override
public boolean isMetadataUpdated() {
return MetadataLoader.isMetadataCached(this);
}
public static final class PhotoData extends LocalMediaData {
private static final Log.Tag TAG = new Log.Tag("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;
// GL max texture size: keep bitmaps below this value.
private static final int MAXIMUM_TEXTURE_SIZE = 2048;
static final Uri CONTENT_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
private 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.
*/
private 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 = ACTION_DEMOTE | ACTION_PROMOTE | ACTION_ZOOM;
private static final int mSupportedDataActions =
DATA_ACTION_DELETE | DATA_ACTION_EDIT | DATA_ACTION_SHARE;
/** from MediaStore, can only be 0, 90, 180, 270 */
private final int mOrientation;
/** @see #getSignature() */
private final String mSignature;
public static LocalData fromContentUri(ContentResolver cr, Uri contentUri) {
List<LocalData> newPhotos = query(cr, contentUri, QUERY_ALL_MEDIA_ID);
if (newPhotos.isEmpty()) {
return null;
}
return newPhotos.get(0);
}
public PhotoData(long id, String title, String mimeType,
long dateTakenInSeconds, long dateModifiedInSeconds,
String path, int orientation, int width, int height,
long sizeInBytes, double latitude, double longitude) {
super(id, title, mimeType, dateTakenInSeconds, dateModifiedInSeconds,
path, width, height, sizeInBytes, latitude, longitude);
mOrientation = orientation;
mSignature = mimeType + orientation + dateModifiedInSeconds;
}
static List<LocalData> query(ContentResolver cr, Uri uri, long lastId) {
return queryLocalMediaData(cr, uri, QUERY_PROJECTION, lastId, QUERY_ORDER,
new PhotoDataBuilder());
}
private static PhotoData buildFromCursor(Cursor c) {
long id = c.getLong(COL_ID);
String title = c.getString(COL_TITLE);
String mimeType = c.getString(COL_MIME_TYPE);
long dateTakenInSeconds = c.getLong(COL_DATE_TAKEN);
long dateModifiedInSeconds = c.getLong(COL_DATE_MODIFIED);
String path = c.getString(COL_DATA);
int orientation = c.getInt(COL_ORIENTATION);
int width = c.getInt(COL_WIDTH);
int height = c.getInt(COL_HEIGHT);
if (width <= 0 || height <= 0) {
Log.w(TAG, "Zero dimension in ContentResolver for "
+ path + ":" + width + "x" + height);
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, opts);
if (opts.outWidth > 0 && opts.outHeight > 0) {
width = opts.outWidth;
height = opts.outHeight;
} else {
Log.w(TAG, "Dimension decode failed for " + path);
Bitmap b = BitmapFactory.decodeFile(path);
if (b == null) {
Log.w(TAG, "PhotoData skipped."
+ " Decoding " + path + "failed.");
return null;
}
width = b.getWidth();
height = b.getHeight();
if (width == 0 || height == 0) {
Log.w(TAG, "PhotoData skipped. Bitmap size 0 for " + path);
return null;
}
}
}
long sizeInBytes = c.getLong(COL_SIZE);
double latitude = c.getDouble(COL_LATITUDE);
double longitude = c.getDouble(COL_LONGITUDE);
PhotoData result = new PhotoData(id, title, mimeType, dateTakenInSeconds,
dateModifiedInSeconds, path, orientation, width, height,
sizeInBytes, latitude, longitude);
return result;
}
@Override
public int getRotation() {
return mOrientation;
}
@Override
public String toString() {
return "Photo:" + ",data=" + mPath + ",mimeType=" + mMimeType
+ "," + mWidth + "x" + mHeight + ",orientation=" + mOrientation
+ ",date=" + new Date(mDateTakenInSeconds);
}
@Override
public int getViewType() {
return VIEW_TYPE_REMOVABLE;
}
@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 context) {
ContentResolver cr = context.getContentResolver();
cr.delete(CONTENT_URI, MediaStore.Images.ImageColumns._ID + "=" + mContentId, null);
return super.delete(context);
}
@Override
public Uri getUri() {
Uri baseUri = CONTENT_URI;
return baseUri.buildUpon().appendPath(String.valueOf(mContentId)).build();
}
@Override
public MediaDetails getMediaDetails(Context context) {
MediaDetails mediaDetails = super.getMediaDetails(context);
MediaDetails.extractExifInfo(mediaDetails, mPath);
mediaDetails.addDetail(MediaDetails.INDEX_ORIENTATION, mOrientation);
return mediaDetails;
}
@Override
public int getLocalDataType() {
return LOCAL_IMAGE;
}
@Override
public LocalData refresh(Context context) {
PhotoData newData = null;
Cursor c = context.getContentResolver().query(getUri(), QUERY_PROJECTION, null,
null, null);
if (c != null) {
if (c.moveToFirst()) {
newData = buildFromCursor(c);
}
c.close();
}
return newData;
}
@Override
public String getSignature() {
return mSignature;
}
@Override
protected ImageView fillImageView(Context context, final ImageView v, final int decodeWidth,
final int decodeHeight, int placeHolderResourceId, LocalDataAdapter adapter,
boolean isInProgress) {
loadImage(context, v, decodeWidth, decodeHeight, placeHolderResourceId, false);
return v;
}
private void loadImage(Context context, ImageView imageView, int decodeWidth,
int decodeHeight, int placeHolderResourceId, boolean full) {
ThumbTarget thumbTarget = (ThumbTarget) imageView.getTag();
if (thumbTarget == null) {
thumbTarget = new ThumbTarget(imageView);
imageView.setTag(thumbTarget);
}
// Make sure we've reset all state related to showing thumb and full whenever
// we load a thumbnail because loading a thumbnail only happens when we're changing
// images.
if (!full) {
thumbTarget.clearTargets();
}
Glide.with(context)
.using(new ImageModelLoader(context))
.load(this)
.placeholder(placeHolderResourceId)
.fitCenter()
.into(thumbTarget.getTarget(decodeWidth, decodeHeight, full));
}
@Override
public void recycle(View view) {
super.recycle(view);
if (view != null) {
ThumbTarget thumbTarget = (ThumbTarget) view.getTag();
thumbTarget.clearTargets();
}
}
@Override
public LocalDataViewType getItemViewType() {
return LocalDataViewType.PHOTO;
}
@Override
public void loadFullImage(Context context, int w, int h, View v, LocalDataAdapter adapter)
{
loadImage(context, (ImageView) v, w, h, 0, true);
}
private static class PhotoDataBuilder implements CursorToLocalData {
@Override
public PhotoData build(Cursor cursor) {
return LocalMediaData.PhotoData.buildFromCursor(cursor);
}
}
/**
* In the filmstrip we want to load a small version of an image and then load a
* larger version when we switch to full screen mode. This class manages
* that transition and loading process by keeping one target for the smaller thumbnail
* and a second target for the larger version.
*/
private static class ThumbTarget {
private final SizedImageViewTarget mThumbTarget;
private final SizedImageViewTarget mFullTarget;
private boolean fullLoaded = false;
public ThumbTarget(ImageView imageView) {
mThumbTarget = new SizedImageViewTarget(imageView) {
@Override
public void onImageReady(Bitmap bitmap) {
// If we manage to load the thumb after the full, we don't
// want to replace the higher quality full with the thumb.
if (!fullLoaded) {
super.onImageReady(bitmap);
}
}
};
mFullTarget = new SizedImageViewTarget(imageView) {
@Override
public void onImageReady(Bitmap bitmap) {
// When the full is loaded, we no longer need the thumb.
fullLoaded = true;
Glide.cancel(mThumbTarget);
super.onImageReady(bitmap);
}
@Override
public void setPlaceholder(Drawable placeholder) {
// We always load the thumb first which will set the placeholder.
// If we were to set the placeholder here too, instead of showing
// the thumb while we load the full, we will instead revert back
// to the placeholder.
}
};
}
public Target getTarget(int width, int height, boolean full) {
final SizedImageViewTarget result = full ? mFullTarget : mThumbTarget;
// Limit the target size so we don't load a bitmap larger than the max size we
// can display.
width = Math.min(width, MAXIMUM_TEXTURE_SIZE);
height = Math.min(height, MAXIMUM_TEXTURE_SIZE);
result.setSize(width, height);
return result;
}
public void clearTargets() {
fullLoaded = false;
Glide.cancel(mThumbTarget);
Glide.cancel(mFullTarget);
}
}
}
public static final 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_SIZE = 8;
public static final int COL_LATITUDE = 9;
public static final int COL_LONGITUDE = 10;
public static final int COL_DURATION = 11;
static final Uri CONTENT_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
private static final int mSupportedUIActions = ACTION_DEMOTE | ACTION_PROMOTE;
private static final int mSupportedDataActions =
DATA_ACTION_DELETE | DATA_ACTION_PLAY | DATA_ACTION_SHARE;
private 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.
*/
private 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.SIZE, // 8 long
MediaStore.Video.VideoColumns.LATITUDE, // 9 double
MediaStore.Video.VideoColumns.LONGITUDE, // 10 double
MediaStore.Video.VideoColumns.DURATION // 11 long
};
/** The duration in milliseconds. */
private final long mDurationInSeconds;
private final String mSignature;
public VideoData(long id, String title, String mimeType,
long dateTakenInSeconds, long dateModifiedInSeconds,
String path, int width, int height, long sizeInBytes,
double latitude, double longitude, long durationInSeconds) {
super(id, title, mimeType, dateTakenInSeconds, dateModifiedInSeconds,
path, width, height, sizeInBytes, latitude, longitude);
mDurationInSeconds = durationInSeconds;
mSignature = mimeType + dateModifiedInSeconds;
}
public static LocalData fromContentUri(ContentResolver cr, Uri contentUri) {
List<LocalData> newVideos = query(cr, contentUri, QUERY_ALL_MEDIA_ID);
if (newVideos.isEmpty()) {
return null;
}
return newVideos.get(0);
}
static List<LocalData> query(ContentResolver cr, Uri uri, long lastId) {
return queryLocalMediaData(cr, uri, QUERY_PROJECTION, lastId, QUERY_ORDER,
new VideoDataBuilder());
}
/**
* We can't trust the media store and we can't afford the performance overhead of
* synchronously decoding the video header for every item when loading our data set
* from the media store, so we instead run the metadata loader in the background
* to decode the video header for each item and prefer whatever values it obtains.
*/
private int getBestWidth() {
int metadataWidth = VideoRotationMetadataLoader.getWidth(this);
if (metadataWidth > 0) {
return metadataWidth;
} else {
return mWidth;
}
}
private int getBestHeight() {
int metadataHeight = VideoRotationMetadataLoader.getHeight(this);
if (metadataHeight > 0) {
return metadataHeight;
} else {
return mHeight;
}
}
/**
* If the metadata loader has determined from the video header that we need to rotate the video
* 90 or 270 degrees, then we swap the width and height.
*/
@Override
public int getWidth() {
return VideoRotationMetadataLoader.isRotated(this) ? getBestHeight() : getBestWidth();
}
@Override
public int getHeight() {
return VideoRotationMetadataLoader.isRotated(this) ? getBestWidth() : getBestHeight();
}
private static VideoData buildFromCursor(Cursor c) {
long id = c.getLong(COL_ID);
String title = c.getString(COL_TITLE);
String mimeType = c.getString(COL_MIME_TYPE);
long dateTakenInSeconds = c.getLong(COL_DATE_TAKEN);
long dateModifiedInSeconds = c.getLong(COL_DATE_MODIFIED);
String path = c.getString(COL_DATA);
int width = c.getInt(COL_WIDTH);
int height = c.getInt(COL_HEIGHT);
// If the media store doesn't contain a width and a height, use the width and height
// of the default camera mode instead. When the metadata loader runs, it will set the
// correct values.
if (width == 0 || height == 0) {
Log.w(TAG, "failed to retrieve width and height from the media store, defaulting " +
" to camera profile");
CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
width = profile.videoFrameWidth;
height = profile.videoFrameHeight;
}
long sizeInBytes = c.getLong(COL_SIZE);
double latitude = c.getDouble(COL_LATITUDE);
double longitude = c.getDouble(COL_LONGITUDE);
long durationInSeconds = c.getLong(COL_DURATION) / 1000;
VideoData d = new VideoData(id, title, mimeType, dateTakenInSeconds,
dateModifiedInSeconds, path, width, height, sizeInBytes,
latitude, longitude, durationInSeconds);
return d;
}
@Override
public String toString() {
return "Video:" + ",data=" + mPath + ",mimeType=" + mMimeType
+ "," + mWidth + "x" + mHeight + ",date=" + new Date(mDateTakenInSeconds);
}
@Override
public int getViewType() {
return VIEW_TYPE_REMOVABLE;
}
@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 context) {
ContentResolver cr = context.getContentResolver();
cr.delete(CONTENT_URI, MediaStore.Video.VideoColumns._ID + "=" + mContentId, null);
return super.delete(context);
}
@Override
public Uri getUri() {
Uri baseUri = CONTENT_URI;
return baseUri.buildUpon().appendPath(String.valueOf(mContentId)).build();
}
@Override
public MediaDetails getMediaDetails(Context context) {
MediaDetails mediaDetails = super.getMediaDetails(context);
String duration = MediaDetails.formatDuration(context, mDurationInSeconds);
mediaDetails.addDetail(MediaDetails.INDEX_DURATION, duration);
return mediaDetails;
}
@Override
public int getLocalDataType() {
return LOCAL_VIDEO;
}
@Override
public LocalData refresh(Context context) {
Cursor c = context.getContentResolver().query(getUri(), QUERY_PROJECTION, null,
null, null);
if (c == null || !c.moveToFirst()) {
return null;
}
VideoData newData = buildFromCursor(c);
return newData;
}
@Override
public String getSignature() {
return mSignature;
}
@Override
protected ImageView fillImageView(Context context, final ImageView v, final int decodeWidth,
final int decodeHeight, int placeHolderResourceId, LocalDataAdapter adapter,
boolean isInProgress) {
SizedImageViewTarget target = (SizedImageViewTarget) v.getTag();
if (target == null) {
target = new SizedImageViewTarget(v);
v.setTag(target);
}
target.setSize(decodeWidth, decodeHeight);
Glide.with(context)
.using(new VideoModelLoader(context))
.loadFromVideo(this)
.placeholder(placeHolderResourceId)
.fitCenter()
.into(target);
return v;
}
@Override
public View getView(final Context context, View recycled,
int decodeWidth, int decodeHeight, int placeHolderResourceId,
LocalDataAdapter adapter, boolean isInProgress) {
final VideoViewHolder viewHolder;
final View result;
if (recycled != null) {
result = recycled;
viewHolder = (VideoViewHolder) recycled.getTag();
} else {
result = LayoutInflater.from(context).inflate(R.layout.filmstrip_video, null);
ImageView videoView = (ImageView) result.findViewById(R.id.video_view);
ImageView playButton = (ImageView) result.findViewById(R.id.play_button);
viewHolder = new VideoViewHolder(videoView, playButton);
result.setTag(viewHolder);
}
fillImageView(context, viewHolder.mVideoView, decodeWidth, decodeHeight,
placeHolderResourceId, adapter, isInProgress);
// ImageView for the play icon.
viewHolder.mPlayButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO: refactor this into activities to avoid this class
// conversion.
CameraUtil.playVideo((Activity) context, getUri(), mTitle);
}
});
return result;
}
@Override
public void recycle(View view) {
super.recycle(view);
VideoViewHolder videoViewHolder = (VideoViewHolder) view.getTag();
Target target = (Target) videoViewHolder.mVideoView.getTag();
Glide.cancel(target);
}
@Override
public LocalDataViewType getItemViewType() {
return LocalDataViewType.VIDEO;
}
}
private static class VideoDataBuilder implements CursorToLocalData {
@Override
public VideoData build(Cursor cursor) {
return LocalMediaData.VideoData.buildFromCursor(cursor);
}
}
private static class VideoViewHolder {
private final ImageView mVideoView;
private final ImageView mPlayButton;
public VideoViewHolder(ImageView videoView, ImageView playButton) {
mVideoView = videoView;
mPlayButton = playButton;
}
}
/**
* Normally Glide will figure out the necessary size based on the view
* the image is being loaded into. In filmstrip the view isn't immediately
* laid out after being requested from the data, which can cause Glide to give
* up on obtaining the view dimensions. To avoid that, we manually set the
* dimensions.
*/
private static class SizedImageViewTarget extends ImageViewTarget {
private int mWidth;
private int mHeight;
public SizedImageViewTarget(ImageView imageView) {
super(imageView);
}
public void setSize(int width, int height) {
mWidth = width;
mHeight = height;
}
@Override
public void getSize(final SizeReadyCallback cb) {
cb.onSizeReady(mWidth, mHeight);
}
}
}