Home | History | Annotate | Download | only in data
      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 }
    913