OSDN Git Service

Align id selection from QueryTask to sort order from Media query
[android-x86/packages-apps-Camera2.git] / src / com / android / camera / data / LocalMediaData.java
1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.camera.data;
18
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.media.CamcorderProfile;
25 import android.net.Uri;
26 import android.os.Bundle;
27 import android.provider.MediaStore;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.widget.ImageView;
31
32 import com.android.camera.Storage;
33 import com.android.camera.debug.Log;
34 import com.android.camera2.R;
35 import com.bumptech.glide.BitmapRequestBuilder;
36 import com.bumptech.glide.Glide;
37 import com.bumptech.glide.load.resource.bitmap.BitmapEncoder;
38
39 import java.io.File;
40 import java.text.DateFormat;
41 import java.util.ArrayList;
42 import java.util.Date;
43 import java.util.List;
44 import java.util.Locale;
45
46 /**
47  * A base class for all the local media files. The bitmap is loaded in
48  * background thread. Subclasses should implement their own background loading
49  * thread by sub-classing BitmapLoadTask and overriding doInBackground() to
50  * return a bitmap.
51  */
52 public abstract class LocalMediaData implements LocalData {
53     /** The minimum id to use to query for all media at a given media store uri */
54     static final int QUERY_ALL_MEDIA_ID = -1;
55     private static final String CAMERA_PATH = Storage.DIRECTORY + "%";
56     private static final String SELECT_BY_PATH = MediaStore.MediaColumns.DATA + " LIKE ?";
57     private static final int MEDIASTORE_THUMB_WIDTH = 512;
58     private static final int MEDIASTORE_THUMB_HEIGHT = 384;
59
60     protected final long mContentId;
61     protected final String mTitle;
62     protected final String mMimeType;
63     protected final long mDateTakenInMilliSeconds;
64     protected final long mDateModifiedInSeconds;
65     protected final String mPath;
66     // width and height should be adjusted according to orientation.
67     protected final int mWidth;
68     protected final int mHeight;
69     protected final long mSizeInBytes;
70     protected final double mLatitude;
71     protected final double mLongitude;
72     protected final Bundle mMetaData;
73
74     private static final int JPEG_COMPRESS_QUALITY = 90;
75     private static final BitmapEncoder JPEG_ENCODER =
76             new BitmapEncoder(Bitmap.CompressFormat.JPEG, JPEG_COMPRESS_QUALITY);
77
78     /**
79      * Used for thumbnail loading optimization. True if this data has a
80      * corresponding visible view.
81      */
82     protected Boolean mUsing = false;
83
84     public LocalMediaData(long contentId, String title, String mimeType,
85             long dateTakenInMilliSeconds, long dateModifiedInSeconds, String path,
86             int width, int height, long sizeInBytes, double latitude,
87             double longitude) {
88         mContentId = contentId;
89         mTitle = title;
90         mMimeType = mimeType;
91         mDateTakenInMilliSeconds = dateTakenInMilliSeconds;
92         mDateModifiedInSeconds = dateModifiedInSeconds;
93         mPath = path;
94         mWidth = width;
95         mHeight = height;
96         mSizeInBytes = sizeInBytes;
97         mLatitude = latitude;
98         mLongitude = longitude;
99         mMetaData = new Bundle();
100     }
101
102     private interface CursorToLocalData {
103         public LocalData build(Cursor cursor);
104     }
105
106     private static List<LocalData> queryLocalMediaData(ContentResolver contentResolver,
107             Uri contentUri, String[] projection, long minimumId, String orderBy,
108             CursorToLocalData builder) {
109         String selection = SELECT_BY_PATH + " AND " + MediaStore.MediaColumns._ID + " > ?";
110         String[] selectionArgs = new String[] { CAMERA_PATH, Long.toString(minimumId) };
111
112         Cursor cursor = contentResolver.query(contentUri, projection,
113                 selection, selectionArgs, orderBy);
114         List<LocalData> result = new ArrayList<LocalData>();
115         if (cursor != null) {
116             while (cursor.moveToNext()) {
117                 LocalData data = builder.build(cursor);
118                 if (data != null) {
119                     result.add(data);
120                 } else {
121                     final int dataIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA);
122                     Log.e(TAG, "Error loading data:" + cursor.getString(dataIndex));
123                 }
124             }
125
126             cursor.close();
127         }
128         return result;
129     }
130
131     @Override
132     public long getDateTaken() {
133         return mDateTakenInMilliSeconds;
134     }
135
136     @Override
137     public long getDateModified() {
138         return mDateModifiedInSeconds;
139     }
140
141     @Override
142     public long getContentId() {
143         return mContentId;
144     }
145
146     @Override
147     public String getTitle() {
148         return mTitle;
149     }
150
151     @Override
152     public int getWidth() {
153         return mWidth;
154     }
155
156     @Override
157     public int getHeight() {
158         return mHeight;
159     }
160
161     @Override
162     public int getRotation() {
163         return 0;
164     }
165
166     @Override
167     public String getPath() {
168         return mPath;
169     }
170
171     @Override
172     public long getSizeInBytes() {
173         return mSizeInBytes;
174     }
175
176     @Override
177     public boolean isUIActionSupported(int action) {
178         return false;
179     }
180
181     @Override
182     public boolean isDataActionSupported(int action) {
183         return false;
184     }
185
186     @Override
187     public boolean delete(Context context) {
188         File f = new File(mPath);
189         return f.delete();
190     }
191
192     @Override
193     public void onFullScreen(boolean fullScreen) {
194         // do nothing.
195     }
196
197     @Override
198     public boolean canSwipeInFullScreen() {
199         return true;
200     }
201
202     protected ImageView fillImageView(Context context, ImageView v,
203             int thumbWidth, int thumbHeight, int placeHolderResourceId,
204             LocalDataAdapter adapter, boolean isInProgress) {
205         Glide.with(context)
206             .loadFromMediaStore(getUri(), mMimeType, mDateModifiedInSeconds, 0)
207             .fitCenter()
208             .placeholder(placeHolderResourceId)
209             .into(v);
210
211         v.setContentDescription(context.getResources().getString(
212                 R.string.media_date_content_description,
213                 getReadableDate(mDateModifiedInSeconds)));
214
215         return v;
216     }
217
218     @Override
219     public View getView(Context context, View recycled, int thumbWidth, int thumbHeight,
220             int placeHolderResourceId, LocalDataAdapter adapter, boolean isInProgress,
221             ActionCallback actionCallback) {
222         final ImageView imageView;
223         if (recycled != null) {
224             imageView = (ImageView) recycled;
225         } else {
226             imageView = (ImageView) LayoutInflater.from(context)
227                 .inflate(R.layout.filmstrip_image, null);
228             imageView.setTag(R.id.mediadata_tag_viewtype, getItemViewType().ordinal());
229         }
230
231         return fillImageView(context, imageView, thumbWidth, thumbHeight,
232                 placeHolderResourceId, adapter, isInProgress);
233     }
234
235     @Override
236     public void loadFullImage(Context context, int thumbWidth, int thumbHeight, View view,
237             LocalDataAdapter adapter) {
238         // Default is do nothing.
239         // Can be implemented by sub-classes.
240     }
241
242     @Override
243     public void prepare() {
244         synchronized (mUsing) {
245             mUsing = true;
246         }
247     }
248
249     @Override
250     public void recycle(View view) {
251         synchronized (mUsing) {
252             mUsing = false;
253         }
254     }
255
256     @Override
257     public double[] getLatLong() {
258         if (mLatitude == 0 && mLongitude == 0) {
259             return null;
260         }
261         return new double[] {
262                 mLatitude, mLongitude
263         };
264     }
265
266     protected boolean isUsing() {
267         synchronized (mUsing) {
268             return mUsing;
269         }
270     }
271
272     @Override
273     public String getMimeType() {
274         return mMimeType;
275     }
276
277     @Override
278     public MediaDetails getMediaDetails(Context context) {
279         MediaDetails mediaDetails = new MediaDetails();
280         mediaDetails.addDetail(MediaDetails.INDEX_TITLE, mTitle);
281         mediaDetails.addDetail(MediaDetails.INDEX_WIDTH, mWidth);
282         mediaDetails.addDetail(MediaDetails.INDEX_HEIGHT, mHeight);
283         mediaDetails.addDetail(MediaDetails.INDEX_PATH, mPath);
284         mediaDetails.addDetail(MediaDetails.INDEX_DATETIME,
285                 getReadableDate(mDateModifiedInSeconds));
286         if (mSizeInBytes > 0) {
287             mediaDetails.addDetail(MediaDetails.INDEX_SIZE, mSizeInBytes);
288         }
289         if (mLatitude != 0 && mLongitude != 0) {
290             String locationString = String.format(Locale.getDefault(), "%f, %f", mLatitude,
291                     mLongitude);
292             mediaDetails.addDetail(MediaDetails.INDEX_LOCATION, locationString);
293         }
294         return mediaDetails;
295     }
296
297     private static String getReadableDate(long dateInSeconds) {
298         DateFormat dateFormatter = DateFormat.getDateTimeInstance();
299         return dateFormatter.format(new Date(dateInSeconds * 1000));
300     }
301
302     @Override
303     public abstract int getViewType();
304
305     @Override
306     public Bundle getMetadata() {
307         return mMetaData;
308     }
309
310     @Override
311     public boolean isMetadataUpdated() {
312         return MetadataLoader.isMetadataCached(this);
313     }
314
315     public static final class PhotoData extends LocalMediaData {
316         private static final Log.Tag TAG = new Log.Tag("PhotoData");
317
318         public static final int COL_ID = 0;
319         public static final int COL_TITLE = 1;
320         public static final int COL_MIME_TYPE = 2;
321         public static final int COL_DATE_TAKEN = 3;
322         public static final int COL_DATE_MODIFIED = 4;
323         public static final int COL_DATA = 5;
324         public static final int COL_ORIENTATION = 6;
325         public static final int COL_WIDTH = 7;
326         public static final int COL_HEIGHT = 8;
327         public static final int COL_SIZE = 9;
328         public static final int COL_LATITUDE = 10;
329         public static final int COL_LONGITUDE = 11;
330
331         // GL max texture size: keep bitmaps below this value.
332         private static final int MAXIMUM_TEXTURE_SIZE = 2048;
333
334         static final Uri CONTENT_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
335
336         // Sort all data by ID. This must be aligned with
337         // {@link CameraDataAdapter.QueryTask} which relies on the highest ID
338         // being first in any data returned.
339         private static final String QUERY_ORDER = MediaStore.Images.ImageColumns._ID + " DESC";
340         /**
341          * These values should be kept in sync with column IDs (COL_*) above.
342          */
343         private static final String[] QUERY_PROJECTION = {
344                 MediaStore.Images.ImageColumns._ID,           // 0, int
345                 MediaStore.Images.ImageColumns.TITLE,         // 1, string
346                 MediaStore.Images.ImageColumns.MIME_TYPE,     // 2, string
347                 MediaStore.Images.ImageColumns.DATE_TAKEN,    // 3, int
348                 MediaStore.Images.ImageColumns.DATE_MODIFIED, // 4, int
349                 MediaStore.Images.ImageColumns.DATA,          // 5, string
350                 MediaStore.Images.ImageColumns.ORIENTATION,   // 6, int, 0, 90, 180, 270
351                 MediaStore.Images.ImageColumns.WIDTH,         // 7, int
352                 MediaStore.Images.ImageColumns.HEIGHT,        // 8, int
353                 MediaStore.Images.ImageColumns.SIZE,          // 9, long
354                 MediaStore.Images.ImageColumns.LATITUDE,      // 10, double
355                 MediaStore.Images.ImageColumns.LONGITUDE      // 11, double
356         };
357
358         private static final int mSupportedUIActions = ACTION_DEMOTE | ACTION_PROMOTE | ACTION_ZOOM;
359         private static final int mSupportedDataActions =
360                 DATA_ACTION_DELETE | DATA_ACTION_EDIT | DATA_ACTION_SHARE;
361
362         /** from MediaStore, can only be 0, 90, 180, 270 */
363         private final int mOrientation;
364         /** @see #getSignature() */
365         private final String mSignature;
366
367         public static LocalData fromContentUri(ContentResolver cr, Uri contentUri) {
368             List<LocalData> newPhotos = query(cr, contentUri, QUERY_ALL_MEDIA_ID);
369             if (newPhotos.isEmpty()) {
370                 return null;
371             }
372             return newPhotos.get(0);
373         }
374
375         public PhotoData(long id, String title, String mimeType,
376                 long dateTakenInMilliSeconds, long dateModifiedInSeconds,
377                 String path, int orientation, int width, int height,
378                 long sizeInBytes, double latitude, double longitude) {
379             super(id, title, mimeType, dateTakenInMilliSeconds, dateModifiedInSeconds,
380                     path, width, height, sizeInBytes, latitude, longitude);
381             mOrientation = orientation;
382             mSignature = mimeType + orientation + dateModifiedInSeconds;
383         }
384
385         static List<LocalData> query(ContentResolver cr, Uri uri, long lastId) {
386             return queryLocalMediaData(cr, uri, QUERY_PROJECTION, lastId, QUERY_ORDER,
387                     new PhotoDataBuilder());
388         }
389
390         private static PhotoData buildFromCursor(Cursor c) {
391             long id = c.getLong(COL_ID);
392             String title = c.getString(COL_TITLE);
393             String mimeType = c.getString(COL_MIME_TYPE);
394             long dateTakenInMilliSeconds = c.getLong(COL_DATE_TAKEN);
395             long dateModifiedInSeconds = c.getLong(COL_DATE_MODIFIED);
396             String path = c.getString(COL_DATA);
397             int orientation = c.getInt(COL_ORIENTATION);
398             int width = c.getInt(COL_WIDTH);
399             int height = c.getInt(COL_HEIGHT);
400             if (width <= 0 || height <= 0) {
401                 Log.w(TAG, "Zero dimension in ContentResolver for "
402                         + path + ":" + width + "x" + height);
403                 BitmapFactory.Options opts = new BitmapFactory.Options();
404                 opts.inJustDecodeBounds = true;
405                 BitmapFactory.decodeFile(path, opts);
406                 if (opts.outWidth > 0 && opts.outHeight > 0) {
407                     width = opts.outWidth;
408                     height = opts.outHeight;
409                 } else {
410                     Log.w(TAG, "Dimension decode failed for " + path);
411                     Bitmap b = BitmapFactory.decodeFile(path);
412                     if (b == null) {
413                         Log.w(TAG, "PhotoData skipped."
414                                 + " Decoding " + path + "failed.");
415                         return null;
416                     }
417                     width = b.getWidth();
418                     height = b.getHeight();
419                     if (width == 0 || height == 0) {
420                         Log.w(TAG, "PhotoData skipped. Bitmap size 0 for " + path);
421                         return null;
422                     }
423                 }
424             }
425
426             long sizeInBytes = c.getLong(COL_SIZE);
427             double latitude = c.getDouble(COL_LATITUDE);
428             double longitude = c.getDouble(COL_LONGITUDE);
429             PhotoData result = new PhotoData(id, title, mimeType, dateTakenInMilliSeconds,
430                     dateModifiedInSeconds, path, orientation, width, height,
431                     sizeInBytes, latitude, longitude);
432             return result;
433         }
434
435         @Override
436         public int getRotation() {
437             return mOrientation;
438         }
439
440         @Override
441         public String toString() {
442             return "Photo:" + ",data=" + mPath + ",mimeType=" + mMimeType
443                     + "," + mWidth + "x" + mHeight + ",orientation=" + mOrientation
444                     + ",date=" + new Date(mDateTakenInMilliSeconds);
445         }
446
447         @Override
448         public int getViewType() {
449             return VIEW_TYPE_REMOVABLE;
450         }
451
452         @Override
453         public boolean isUIActionSupported(int action) {
454             return ((action & mSupportedUIActions) == action);
455         }
456
457         @Override
458         public boolean isDataActionSupported(int action) {
459             return ((action & mSupportedDataActions) == action);
460         }
461
462         @Override
463         public boolean delete(Context context) {
464             ContentResolver cr = context.getContentResolver();
465             cr.delete(CONTENT_URI, MediaStore.Images.ImageColumns._ID + "=" + mContentId, null);
466             return super.delete(context);
467         }
468
469         @Override
470         public Uri getUri() {
471             Uri baseUri = CONTENT_URI;
472             return baseUri.buildUpon().appendPath(String.valueOf(mContentId)).build();
473         }
474
475         @Override
476         public MediaDetails getMediaDetails(Context context) {
477             MediaDetails mediaDetails = super.getMediaDetails(context);
478             MediaDetails.extractExifInfo(mediaDetails, mPath);
479             mediaDetails.addDetail(MediaDetails.INDEX_ORIENTATION, mOrientation);
480             return mediaDetails;
481         }
482
483         @Override
484         public int getLocalDataType() {
485             return LOCAL_IMAGE;
486         }
487
488         @Override
489         public LocalData refresh(Context context) {
490             PhotoData newData = null;
491             Cursor c = context.getContentResolver().query(getUri(), QUERY_PROJECTION, null,
492                     null, null);
493             if (c != null) {
494                 if (c.moveToFirst()) {
495                     newData = buildFromCursor(c);
496                 }
497                 c.close();
498             }
499
500             return newData;
501         }
502
503         @Override
504         public String getSignature() {
505             return mSignature;
506         }
507
508         @Override
509         protected ImageView fillImageView(Context context, final ImageView v, final int thumbWidth,
510                 final int thumbHeight, int placeHolderResourceId, LocalDataAdapter adapter,
511                 boolean isInProgress) {
512             loadImage(context, v, thumbWidth, thumbHeight, placeHolderResourceId, false);
513
514             int stringId = R.string.photo_date_content_description;
515             if (PanoramaMetadataLoader.isPanorama(this) ||
516                 PanoramaMetadataLoader.isPanorama360(this)) {
517                 stringId = R.string.panorama_date_content_description;
518             } else if (PanoramaMetadataLoader.isPanoramaAndUseViewer(this)) {
519                 // assume it's a PhotoSphere
520                 stringId = R.string.photosphere_date_content_description;
521             } else if (RgbzMetadataLoader.hasRGBZData(this)) {
522                 stringId = R.string.refocus_date_content_description;
523             }
524
525             v.setContentDescription(context.getResources().getString(
526                     stringId,
527                     getReadableDate(mDateModifiedInSeconds)));
528
529             return v;
530         }
531
532         private void loadImage(Context context, ImageView imageView, int thumbWidth,
533                 int thumbHeight, int placeHolderResourceId, boolean full) {
534
535             //TODO: Figure out why these can be <= 0.
536             if (thumbWidth <= 0 || thumbHeight <=0) {
537                 return;
538             }
539
540             final int overrideWidth;
541             final int overrideHeight;
542             final BitmapRequestBuilder<Uri, Bitmap> thumbnailRequest;
543             if (full) {
544                 // Load up to the maximum size Bitmap we can render.
545                 overrideWidth = Math.min(getWidth(), MAXIMUM_TEXTURE_SIZE);
546                 overrideHeight = Math.min(getHeight(), MAXIMUM_TEXTURE_SIZE);
547
548                 // Load two thumbnails, first the small low quality thumb from the media store,
549                 // then a medium quality thumbWidth/thumbHeight image. Using two thumbnails ensures
550                 // we don't flicker to grey while we load the maximum size image.
551                 thumbnailRequest = loadUri(context)
552                     .override(thumbWidth, thumbHeight)
553                     .fitCenter()
554                     .thumbnail(loadMediaStoreThumb(context));
555             } else {
556                 // Load a medium quality thumbWidth/thumbHeight image.
557                 overrideWidth = thumbWidth;
558                 overrideHeight = thumbHeight;
559
560                 // Load a single small low quality thumbnail from the media store.
561                 thumbnailRequest = loadMediaStoreThumb(context);
562             }
563
564             loadUri(context)
565                 .placeholder(placeHolderResourceId)
566                 .fitCenter()
567                 .override(overrideWidth, overrideHeight)
568                 .thumbnail(thumbnailRequest)
569                 .into(imageView);
570         }
571
572         /** Loads a thumbnail with a size targeted to use MediaStore.Images.Thumbnails. */
573         private BitmapRequestBuilder<Uri, Bitmap> loadMediaStoreThumb(Context context) {
574             return loadUri(context)
575                 .override(MEDIASTORE_THUMB_WIDTH, MEDIASTORE_THUMB_HEIGHT);
576         }
577
578         /** Loads an image using a MediaStore Uri with our default options. */
579         private BitmapRequestBuilder<Uri, Bitmap> loadUri(Context context) {
580             return Glide.with(context)
581                 .loadFromMediaStore(getUri(), mMimeType, mDateModifiedInSeconds, mOrientation)
582                 .asBitmap()
583                 .encoder(JPEG_ENCODER);
584         }
585
586         @Override
587         public void recycle(View view) {
588             super.recycle(view);
589             if (view != null) {
590                 Glide.clear(view);
591             }
592         }
593
594         @Override
595         public LocalDataViewType getItemViewType() {
596             return LocalDataViewType.PHOTO;
597         }
598
599         @Override
600         public void loadFullImage(Context context, int thumbWidth, int thumbHeight, View v,
601             LocalDataAdapter adapter)
602         {
603             loadImage(context, (ImageView) v, thumbWidth, thumbHeight, 0, true);
604         }
605
606         private static class PhotoDataBuilder implements CursorToLocalData {
607             @Override
608             public PhotoData build(Cursor cursor) {
609                 return LocalMediaData.PhotoData.buildFromCursor(cursor);
610             }
611         }
612     }
613
614     public static final class VideoData extends LocalMediaData {
615         public static final int COL_ID = 0;
616         public static final int COL_TITLE = 1;
617         public static final int COL_MIME_TYPE = 2;
618         public static final int COL_DATE_TAKEN = 3;
619         public static final int COL_DATE_MODIFIED = 4;
620         public static final int COL_DATA = 5;
621         public static final int COL_WIDTH = 6;
622         public static final int COL_HEIGHT = 7;
623         public static final int COL_SIZE = 8;
624         public static final int COL_LATITUDE = 9;
625         public static final int COL_LONGITUDE = 10;
626         public static final int COL_DURATION = 11;
627
628         static final Uri CONTENT_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
629
630         private static final int mSupportedUIActions = ACTION_DEMOTE | ACTION_PROMOTE;
631         private static final int mSupportedDataActions =
632                 DATA_ACTION_DELETE | DATA_ACTION_PLAY | DATA_ACTION_SHARE;
633
634         private static final String QUERY_ORDER = MediaStore.Video.VideoColumns.DATE_TAKEN
635                 + " DESC, " + MediaStore.Video.VideoColumns._ID + " DESC";
636         /**
637          * These values should be kept in sync with column IDs (COL_*) above.
638          */
639         private static final String[] QUERY_PROJECTION = {
640                 MediaStore.Video.VideoColumns._ID,           // 0, int
641                 MediaStore.Video.VideoColumns.TITLE,         // 1, string
642                 MediaStore.Video.VideoColumns.MIME_TYPE,     // 2, string
643                 MediaStore.Video.VideoColumns.DATE_TAKEN,    // 3, int
644                 MediaStore.Video.VideoColumns.DATE_MODIFIED, // 4, int
645                 MediaStore.Video.VideoColumns.DATA,          // 5, string
646                 MediaStore.Video.VideoColumns.WIDTH,         // 6, int
647                 MediaStore.Video.VideoColumns.HEIGHT,        // 7, int
648                 MediaStore.Video.VideoColumns.SIZE,          // 8 long
649                 MediaStore.Video.VideoColumns.LATITUDE,      // 9 double
650                 MediaStore.Video.VideoColumns.LONGITUDE,     // 10 double
651                 MediaStore.Video.VideoColumns.DURATION       // 11 long
652         };
653
654         /** The duration in milliseconds. */
655         private final long mDurationInSeconds;
656         private final String mSignature;
657
658         public VideoData(long id, String title, String mimeType,
659                 long dateTakenInMilliSeconds, long dateModifiedInSeconds,
660                 String path, int width, int height, long sizeInBytes,
661                 double latitude, double longitude, long durationInSeconds) {
662             super(id, title, mimeType, dateTakenInMilliSeconds, dateModifiedInSeconds,
663                     path, width, height, sizeInBytes, latitude, longitude);
664             mDurationInSeconds = durationInSeconds;
665             mSignature = mimeType + dateModifiedInSeconds;
666         }
667
668         public static LocalData fromContentUri(ContentResolver cr, Uri contentUri) {
669             List<LocalData> newVideos = query(cr, contentUri, QUERY_ALL_MEDIA_ID);
670             if (newVideos.isEmpty()) {
671                 return null;
672             }
673             return newVideos.get(0);
674         }
675
676         static List<LocalData> query(ContentResolver cr, Uri uri, long lastId) {
677             return queryLocalMediaData(cr, uri, QUERY_PROJECTION, lastId, QUERY_ORDER,
678                     new VideoDataBuilder());
679         }
680
681         /**
682          * We can't trust the media store and we can't afford the performance overhead of
683          * synchronously decoding the video header for every item when loading our data set
684          * from the media store, so we instead run the metadata loader in the background
685          * to decode the video header for each item and prefer whatever values it obtains.
686          */
687         private int getBestWidth() {
688             int metadataWidth = VideoRotationMetadataLoader.getWidth(this);
689             if (metadataWidth > 0) {
690                 return metadataWidth;
691             } else {
692                 return mWidth;
693             }
694         }
695
696         private int getBestHeight() {
697             int metadataHeight = VideoRotationMetadataLoader.getHeight(this);
698             if (metadataHeight > 0) {
699                 return metadataHeight;
700             } else {
701                 return mHeight;
702             }
703         }
704
705         /**
706          * If the metadata loader has determined from the video header that we need to rotate the video
707          * 90 or 270 degrees, then we swap the width and height.
708          */
709         @Override
710         public int getWidth() {
711             return VideoRotationMetadataLoader.isRotated(this) ? getBestHeight() : getBestWidth();
712         }
713
714         @Override
715         public int getHeight() {
716             return VideoRotationMetadataLoader.isRotated(this) ?  getBestWidth() : getBestHeight();
717         }
718
719         private static VideoData buildFromCursor(Cursor c) {
720             long id = c.getLong(COL_ID);
721             String title = c.getString(COL_TITLE);
722             String mimeType = c.getString(COL_MIME_TYPE);
723             long dateTakenInMilliSeconds = c.getLong(COL_DATE_TAKEN);
724             long dateModifiedInSeconds = c.getLong(COL_DATE_MODIFIED);
725             String path = c.getString(COL_DATA);
726             int width = c.getInt(COL_WIDTH);
727             int height = c.getInt(COL_HEIGHT);
728
729             // If the media store doesn't contain a width and a height, use the width and height
730             // of the default camera mode instead. When the metadata loader runs, it will set the
731             // correct values.
732             if (width == 0 || height == 0) {
733                 Log.w(TAG, "failed to retrieve width and height from the media store, defaulting " +
734                         " to camera profile");
735                 CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
736                 width = profile.videoFrameWidth;
737                 height = profile.videoFrameHeight;
738             }
739
740             long sizeInBytes = c.getLong(COL_SIZE);
741             double latitude = c.getDouble(COL_LATITUDE);
742             double longitude = c.getDouble(COL_LONGITUDE);
743             long durationInSeconds = c.getLong(COL_DURATION) / 1000;
744             VideoData d = new VideoData(id, title, mimeType, dateTakenInMilliSeconds,
745                     dateModifiedInSeconds, path, width, height, sizeInBytes,
746                     latitude, longitude, durationInSeconds);
747             return d;
748         }
749
750         @Override
751         public String toString() {
752             return "Video:" + ",data=" + mPath + ",mimeType=" + mMimeType
753                     + "," + mWidth + "x" + mHeight + ",date=" + new Date(mDateTakenInMilliSeconds);
754         }
755
756         @Override
757         public int getViewType() {
758             return VIEW_TYPE_REMOVABLE;
759         }
760
761         @Override
762         public boolean isUIActionSupported(int action) {
763             return ((action & mSupportedUIActions) == action);
764         }
765
766         @Override
767         public boolean isDataActionSupported(int action) {
768             return ((action & mSupportedDataActions) == action);
769         }
770
771         @Override
772         public boolean delete(Context context) {
773             ContentResolver cr = context.getContentResolver();
774             cr.delete(CONTENT_URI, MediaStore.Video.VideoColumns._ID + "=" + mContentId, null);
775             return super.delete(context);
776         }
777
778         @Override
779         public Uri getUri() {
780             Uri baseUri = CONTENT_URI;
781             return baseUri.buildUpon().appendPath(String.valueOf(mContentId)).build();
782         }
783
784         @Override
785         public MediaDetails getMediaDetails(Context context) {
786             MediaDetails mediaDetails = super.getMediaDetails(context);
787             String duration = MediaDetails.formatDuration(context, mDurationInSeconds);
788             mediaDetails.addDetail(MediaDetails.INDEX_DURATION, duration);
789             return mediaDetails;
790         }
791
792         @Override
793         public int getLocalDataType() {
794             return LOCAL_VIDEO;
795         }
796
797         @Override
798         public LocalData refresh(Context context) {
799             Cursor c = context.getContentResolver().query(getUri(), QUERY_PROJECTION, null,
800                     null, null);
801             if (c == null || !c.moveToFirst()) {
802                 return null;
803             }
804             VideoData newData = buildFromCursor(c);
805             return newData;
806         }
807
808         @Override
809         public String getSignature() {
810             return mSignature;
811         }
812
813         @Override
814         protected ImageView fillImageView(Context context, final ImageView v, final int thumbWidth,
815                 final int thumbHeight, int placeHolderResourceId, LocalDataAdapter adapter,
816                 boolean isInProgress) {
817
818             //TODO: Figure out why these can be <= 0.
819             if (thumbWidth <= 0 || thumbHeight <=0) {
820                 return v;
821             }
822
823             Glide.with(context)
824                 .loadFromMediaStore(getUri(), mMimeType, mDateModifiedInSeconds, 0)
825                 .asBitmap()
826                 .encoder(JPEG_ENCODER)
827                 .thumbnail(Glide.with(context)
828                     .loadFromMediaStore(getUri(), mMimeType, mDateModifiedInSeconds, 0)
829                     .asBitmap()
830                     .encoder(JPEG_ENCODER)
831                     .override(MEDIASTORE_THUMB_WIDTH, MEDIASTORE_THUMB_HEIGHT))
832                 .placeholder(placeHolderResourceId)
833                 .fitCenter()
834                 .override(thumbWidth, thumbHeight)
835                 .into(v);
836
837             // Content descriptions applied to parent FrameView
838             // see getView
839
840             return v;
841         }
842
843         @Override
844         public View getView(final Context context, View recycled,
845                 int thumbWidth, int thumbHeight, int placeHolderResourceId,
846                 LocalDataAdapter adapter, boolean isInProgress,
847                 final ActionCallback actionCallback) {
848
849             final VideoViewHolder viewHolder;
850             final View result;
851             if (recycled != null) {
852                 result = recycled;
853                 viewHolder = (VideoViewHolder) recycled.getTag(R.id.mediadata_tag_target);
854             } else {
855                 result = LayoutInflater.from(context).inflate(R.layout.filmstrip_video, null);
856                 result.setTag(R.id.mediadata_tag_viewtype, getItemViewType().ordinal());
857                 ImageView videoView = (ImageView) result.findViewById(R.id.video_view);
858                 ImageView playButton = (ImageView) result.findViewById(R.id.play_button);
859                 viewHolder = new VideoViewHolder(videoView, playButton);
860                 result.setTag(R.id.mediadata_tag_target, viewHolder);
861             }
862
863             fillImageView(context, viewHolder.mVideoView, thumbWidth, thumbHeight,
864                     placeHolderResourceId, adapter, isInProgress);
865
866             // ImageView for the play icon.
867             viewHolder.mPlayButton.setOnClickListener(new View.OnClickListener() {
868                 @Override
869                 public void onClick(View v) {
870                     actionCallback.playVideo(getUri(), mTitle);
871                 }
872             });
873
874             result.setContentDescription(context.getResources().getString(
875                     R.string.video_date_content_description,
876                     getReadableDate(mDateModifiedInSeconds)));
877
878             return result;
879         }
880
881         @Override
882         public void recycle(View view) {
883             super.recycle(view);
884             VideoViewHolder videoViewHolder =
885                     (VideoViewHolder) view.getTag(R.id.mediadata_tag_target);
886             Glide.clear(videoViewHolder.mVideoView);
887         }
888
889         @Override
890         public LocalDataViewType getItemViewType() {
891             return LocalDataViewType.VIDEO;
892         }
893     }
894
895     private static class VideoDataBuilder implements CursorToLocalData {
896
897         @Override
898         public VideoData build(Cursor cursor) {
899             return LocalMediaData.VideoData.buildFromCursor(cursor);
900         }
901     }
902
903      private static class VideoViewHolder {
904         private final ImageView mVideoView;
905         private final ImageView mPlayButton;
906
907         public VideoViewHolder(ImageView videoView, ImageView playButton) {
908             mVideoView = videoView;
909             mPlayButton = playButton;
910         }
911     }
912 }