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