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.ContentValues;
     22 import android.content.Context;
     23 import android.database.Cursor;
     24 import android.graphics.Bitmap;
     25 import android.graphics.BitmapFactory;
     26 import android.graphics.Matrix;
     27 import android.graphics.drawable.BitmapDrawable;
     28 import android.graphics.drawable.Drawable;
     29 import android.media.MediaMetadataRetriever;
     30 import android.net.Uri;
     31 import android.os.AsyncTask;
     32 import android.provider.MediaStore;
     33 import android.provider.MediaStore.Images;
     34 import android.util.Log;
     35 import android.view.Gravity;
     36 import android.view.View;
     37 import android.view.ViewGroup;
     38 import android.widget.FrameLayout;
     39 import android.widget.ImageView;
     40 
     41 import com.android.camera.ui.FilmStripView;
     42 import com.android.camera.util.CameraUtil;
     43 import com.android.camera.util.PhotoSphereHelper;
     44 import com.android.camera2.R;
     45 
     46 import java.io.File;
     47 import java.text.DateFormat;
     48 import java.util.Date;
     49 import java.util.Locale;
     50 
     51 /**
     52  * A base class for all the local media files. The bitmap is loaded in
     53  * background thread. Subclasses should implement their own background loading
     54  * thread by sub-classing BitmapLoadTask and overriding doInBackground() to
     55  * return a bitmap.
     56  */
     57 public abstract class LocalMediaData implements LocalData {
     58     protected final long mContentId;
     59     protected final String mTitle;
     60     protected final String mMimeType;
     61     protected final long mDateTakenInSeconds;
     62     protected final long mDateModifiedInSeconds;
     63     protected final String mPath;
     64     // width and height should be adjusted according to orientation.
     65     protected final int mWidth;
     66     protected final int mHeight;
     67     protected final long mSizeInBytes;
     68     protected final double mLatitude;
     69     protected final double mLongitude;
     70 
     71     /** The panorama metadata information of this media data. */
     72     protected PhotoSphereHelper.PanoramaMetadata mPanoramaMetadata;
     73 
     74     /** Used to load photo sphere metadata from image files. */
     75     protected PanoramaMetadataLoader mPanoramaMetadataLoader = null;
     76 
     77     /**
     78      * Used for thumbnail loading optimization. True if this data has a
     79      * corresponding visible view.
     80      */
     81     protected Boolean mUsing = false;
     82 
     83     public LocalMediaData (long contentId, String title, String mimeType,
     84             long dateTakenInSeconds, long dateModifiedInSeconds, String path,
     85             int width, int height, long sizeInBytes, double latitude,
     86             double longitude) {
     87         mContentId = contentId;
     88         mTitle = new String(title);
     89         mMimeType = new String(mimeType);
     90         mDateTakenInSeconds = dateTakenInSeconds;
     91         mDateModifiedInSeconds = dateModifiedInSeconds;
     92         mPath = new String(path);
     93         mWidth = width;
     94         mHeight = height;
     95         mSizeInBytes = sizeInBytes;
     96         mLatitude = latitude;
     97         mLongitude = longitude;
     98     }
     99 
    100     @Override
    101     public long getDateTaken() {
    102         return mDateTakenInSeconds;
    103     }
    104 
    105     @Override
    106     public long getDateModified() {
    107         return mDateModifiedInSeconds;
    108     }
    109 
    110     @Override
    111     public long getContentId() {
    112         return mContentId;
    113     }
    114 
    115     @Override
    116     public String getTitle() {
    117         return new String(mTitle);
    118     }
    119 
    120     @Override
    121     public int getWidth() {
    122         return mWidth;
    123     }
    124 
    125     @Override
    126     public int getHeight() {
    127         return mHeight;
    128     }
    129 
    130     @Override
    131     public int getOrientation() {
    132         return 0;
    133     }
    134 
    135     @Override
    136     public String getPath() {
    137         return mPath;
    138     }
    139 
    140     @Override
    141     public long getSizeInBytes() {
    142         return mSizeInBytes;
    143     }
    144 
    145     @Override
    146     public boolean isUIActionSupported(int action) {
    147         return false;
    148     }
    149 
    150     @Override
    151     public boolean isDataActionSupported(int action) {
    152         return false;
    153     }
    154 
    155     @Override
    156     public boolean delete(Context ctx) {
    157         File f = new File(mPath);
    158         return f.delete();
    159     }
    160 
    161     @Override
    162     public void viewPhotoSphere(PhotoSphereHelper.PanoramaViewHelper helper) {
    163         helper.showPanorama(getContentUri());
    164     }
    165 
    166     @Override
    167     public void isPhotoSphere(Context context, final PanoramaSupportCallback callback) {
    168         // If we already have metadata, use it.
    169         if (mPanoramaMetadata != null) {
    170             callback.panoramaInfoAvailable(mPanoramaMetadata.mUsePanoramaViewer,
    171                     mPanoramaMetadata.mIsPanorama360);
    172         }
    173 
    174         // Otherwise prepare a loader, if we don't have one already.
    175         if (mPanoramaMetadataLoader == null) {
    176             mPanoramaMetadataLoader = new PanoramaMetadataLoader(getContentUri());
    177         }
    178 
    179         // Load the metadata asynchronously.
    180         mPanoramaMetadataLoader.getPanoramaMetadata(context,
    181                 new PanoramaMetadataLoader.PanoramaMetadataCallback() {
    182                     @Override
    183                     public void onPanoramaMetadataLoaded(PhotoSphereHelper.PanoramaMetadata metadata) {
    184                         // Store the metadata and remove the loader to free up
    185                         // space.
    186                         mPanoramaMetadata = metadata;
    187                         mPanoramaMetadataLoader = null;
    188                         callback.panoramaInfoAvailable(metadata.mUsePanoramaViewer,
    189                                 metadata.mIsPanorama360);
    190                     }
    191                 });
    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 ctx, ImageView v,
    205             int decodeWidth, int decodeHeight, Drawable placeHolder,
    206             LocalDataAdapter adapter) {
    207         v.setScaleType(ImageView.ScaleType.FIT_XY);
    208         v.setImageDrawable(placeHolder);
    209 
    210         BitmapLoadTask task = getBitmapLoadTask(v, decodeWidth, decodeHeight,
    211                 ctx.getContentResolver(), adapter);
    212         task.execute();
    213         return v;
    214     }
    215 
    216     @Override
    217     public View getView(Activity activity,
    218             int decodeWidth, int decodeHeight, Drawable placeHolder,
    219             LocalDataAdapter adapter) {
    220         return fillImageView(activity, new ImageView(activity),
    221                 decodeWidth, decodeHeight, placeHolder, adapter);
    222     }
    223 
    224     @Override
    225     public void prepare() {
    226         synchronized (mUsing) {
    227             mUsing = true;
    228         }
    229     }
    230 
    231     @Override
    232     public void recycle() {
    233         synchronized (mUsing) {
    234             mUsing = false;
    235         }
    236     }
    237 
    238     @Override
    239     public double[] getLatLong() {
    240         if (mLatitude == 0 && mLongitude == 0) {
    241             return null;
    242         }
    243         return new double[] {
    244                 mLatitude, mLongitude
    245         };
    246     }
    247 
    248     protected boolean isUsing() {
    249         synchronized (mUsing) {
    250             return mUsing;
    251         }
    252     }
    253 
    254     @Override
    255     public String getMimeType() {
    256         return mMimeType;
    257     }
    258 
    259     @Override
    260     public MediaDetails getMediaDetails(Context context) {
    261         DateFormat dateFormatter = DateFormat.getDateTimeInstance();
    262         MediaDetails mediaDetails = new MediaDetails();
    263         mediaDetails.addDetail(MediaDetails.INDEX_TITLE, mTitle);
    264         mediaDetails.addDetail(MediaDetails.INDEX_WIDTH, mWidth);
    265         mediaDetails.addDetail(MediaDetails.INDEX_HEIGHT, mHeight);
    266         mediaDetails.addDetail(MediaDetails.INDEX_PATH, mPath);
    267         mediaDetails.addDetail(MediaDetails.INDEX_DATETIME,
    268                 dateFormatter.format(new Date(mDateModifiedInSeconds * 1000)));
    269         if (mSizeInBytes > 0) {
    270             mediaDetails.addDetail(MediaDetails.INDEX_SIZE, mSizeInBytes);
    271         }
    272         if (mLatitude != 0 && mLongitude != 0) {
    273             String locationString = String.format(Locale.getDefault(), "%f, %f", mLatitude,
    274                     mLongitude);
    275             mediaDetails.addDetail(MediaDetails.INDEX_LOCATION, locationString);
    276         }
    277         return mediaDetails;
    278     }
    279 
    280     @Override
    281     public abstract int getViewType();
    282 
    283     protected abstract BitmapLoadTask getBitmapLoadTask(
    284             ImageView v, int decodeWidth, int decodeHeight,
    285             ContentResolver resolver, LocalDataAdapter adapter);
    286 
    287     public static final class PhotoData extends LocalMediaData {
    288         private static final String TAG = "CAM_PhotoData";
    289 
    290         public static final int COL_ID = 0;
    291         public static final int COL_TITLE = 1;
    292         public static final int COL_MIME_TYPE = 2;
    293         public static final int COL_DATE_TAKEN = 3;
    294         public static final int COL_DATE_MODIFIED = 4;
    295         public static final int COL_DATA = 5;
    296         public static final int COL_ORIENTATION = 6;
    297         public static final int COL_WIDTH = 7;
    298         public static final int COL_HEIGHT = 8;
    299         public static final int COL_SIZE = 9;
    300         public static final int COL_LATITUDE = 10;
    301         public static final int COL_LONGITUDE = 11;
    302 
    303         static final Uri CONTENT_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    304 
    305         static final String QUERY_ORDER = MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC, "
    306                 + MediaStore.Images.ImageColumns._ID + " DESC";
    307         /**
    308          * These values should be kept in sync with column IDs (COL_*) above.
    309          */
    310         static final String[] QUERY_PROJECTION = {
    311                 MediaStore.Images.ImageColumns._ID,           // 0, int
    312                 MediaStore.Images.ImageColumns.TITLE,         // 1, string
    313                 MediaStore.Images.ImageColumns.MIME_TYPE,     // 2, string
    314                 MediaStore.Images.ImageColumns.DATE_TAKEN,    // 3, int
    315                 MediaStore.Images.ImageColumns.DATE_MODIFIED, // 4, int
    316                 MediaStore.Images.ImageColumns.DATA,          // 5, string
    317                 MediaStore.Images.ImageColumns.ORIENTATION,   // 6, int, 0, 90, 180, 270
    318                 MediaStore.Images.ImageColumns.WIDTH,         // 7, int
    319                 MediaStore.Images.ImageColumns.HEIGHT,        // 8, int
    320                 MediaStore.Images.ImageColumns.SIZE,          // 9, long
    321                 MediaStore.Images.ImageColumns.LATITUDE,      // 10, double
    322                 MediaStore.Images.ImageColumns.LONGITUDE      // 11, double
    323         };
    324 
    325         private static final int mSupportedUIActions =
    326                 FilmStripView.ImageData.ACTION_DEMOTE
    327                         | FilmStripView.ImageData.ACTION_PROMOTE
    328                         | FilmStripView.ImageData.ACTION_ZOOM;
    329         private static final int mSupportedDataActions =
    330                 LocalData.ACTION_DELETE;
    331 
    332         /** 32K buffer. */
    333         private static final byte[] DECODE_TEMP_STORAGE = new byte[32 * 1024];
    334 
    335         /** from MediaStore, can only be 0, 90, 180, 270 */
    336         private final int mOrientation;
    337 
    338         public PhotoData(long id, String title, String mimeType,
    339                 long dateTakenInSeconds, long dateModifiedInSeconds,
    340                 String path, int orientation, int width, int height,
    341                 long sizeInBytes, double latitude, double longitude) {
    342             super(id, title, mimeType, dateTakenInSeconds, dateModifiedInSeconds,
    343                     path, width, height, sizeInBytes, latitude, longitude);
    344             mOrientation = orientation;
    345         }
    346 
    347         static PhotoData buildFromCursor(Cursor c) {
    348             long id = c.getLong(COL_ID);
    349             String title = c.getString(COL_TITLE);
    350             String mimeType = c.getString(COL_MIME_TYPE);
    351             long dateTakenInSeconds = c.getLong(COL_DATE_TAKEN);
    352             long dateModifiedInSeconds = c.getLong(COL_DATE_MODIFIED);
    353             String path = c.getString(COL_DATA);
    354             int orientation = c.getInt(COL_ORIENTATION);
    355             int width = c.getInt(COL_WIDTH);
    356             int height = c.getInt(COL_HEIGHT);
    357             if (width <= 0 || height <= 0) {
    358                 Log.w(TAG, "Zero dimension in ContentResolver for "
    359                         + path + ":" + width + "x" + height);
    360                 BitmapFactory.Options opts = new BitmapFactory.Options();
    361                 opts.inJustDecodeBounds = true;
    362                 BitmapFactory.decodeFile(path, opts);
    363                 if (opts.outWidth > 0 && opts.outHeight > 0) {
    364                     width = opts.outWidth;
    365                     height = opts.outHeight;
    366                 } else {
    367                     Log.w(TAG, "Dimension decode failed for " + path);
    368                     Bitmap b = BitmapFactory.decodeFile(path);
    369                     if (b == null) {
    370                         Log.w(TAG, "PhotoData skipped."
    371                                 + " Decoding " + path + "failed.");
    372                         return null;
    373                     }
    374                     width = b.getWidth();
    375                     height = b.getHeight();
    376                     if (width == 0 || height == 0) {
    377                         Log.w(TAG, "PhotoData skipped. Bitmap size 0 for " + path);
    378                         return null;
    379                     }
    380                 }
    381             }
    382 
    383             long sizeInBytes = c.getLong(COL_SIZE);
    384             double latitude = c.getDouble(COL_LATITUDE);
    385             double longitude = c.getDouble(COL_LONGITUDE);
    386             PhotoData result = new PhotoData(id, title, mimeType, dateTakenInSeconds,
    387                     dateModifiedInSeconds, path, orientation, width, height,
    388                     sizeInBytes, latitude, longitude);
    389             return result;
    390         }
    391 
    392         @Override
    393         public int getOrientation() {
    394             return mOrientation;
    395         }
    396 
    397         @Override
    398         public String toString() {
    399             return "Photo:" + ",data=" + mPath + ",mimeType=" + mMimeType
    400                     + "," + mWidth + "x" + mHeight + ",orientation=" + mOrientation
    401                     + ",date=" + new Date(mDateTakenInSeconds);
    402         }
    403 
    404         @Override
    405         public int getViewType() {
    406             return VIEW_TYPE_REMOVABLE;
    407         }
    408 
    409         @Override
    410         public boolean isUIActionSupported(int action) {
    411             return ((action & mSupportedUIActions) == action);
    412         }
    413 
    414         @Override
    415         public boolean isDataActionSupported(int action) {
    416             return ((action & mSupportedDataActions) == action);
    417         }
    418 
    419         @Override
    420         public boolean delete(Context c) {
    421             ContentResolver cr = c.getContentResolver();
    422             cr.delete(CONTENT_URI, MediaStore.Images.ImageColumns._ID + "=" + mContentId, null);
    423             return super.delete(c);
    424         }
    425 
    426         @Override
    427         public Uri getContentUri() {
    428             Uri baseUri = CONTENT_URI;
    429             return baseUri.buildUpon().appendPath(String.valueOf(mContentId)).build();
    430         }
    431 
    432         @Override
    433         public MediaDetails getMediaDetails(Context context) {
    434             MediaDetails mediaDetails = super.getMediaDetails(context);
    435             MediaDetails.extractExifInfo(mediaDetails, mPath);
    436             mediaDetails.addDetail(MediaDetails.INDEX_ORIENTATION, mOrientation);
    437             return mediaDetails;
    438         }
    439 
    440         @Override
    441         public int getLocalDataType() {
    442             if (mPanoramaMetadata != null) {
    443                 if (mPanoramaMetadata.mIsPanorama360) {
    444                     return LOCAL_360_PHOTO_SPHERE;
    445                 } else if (mPanoramaMetadata.mUsePanoramaViewer) {
    446                     return LOCAL_PHOTO_SPHERE;
    447                 }
    448             }
    449             return LOCAL_IMAGE;
    450         }
    451 
    452         @Override
    453         public LocalData refresh(ContentResolver resolver) {
    454             Cursor c = resolver.query(
    455                     getContentUri(), QUERY_PROJECTION, null, null, null);
    456             if (c == null || !c.moveToFirst()) {
    457                 return null;
    458             }
    459             PhotoData newData = buildFromCursor(c);
    460             return newData;
    461         }
    462 
    463         @Override
    464         public boolean isPhoto() {
    465             return true;
    466         }
    467 
    468         @Override
    469         protected BitmapLoadTask getBitmapLoadTask(
    470                 ImageView v, int decodeWidth, int decodeHeight,
    471                 ContentResolver resolver, LocalDataAdapter adapter) {
    472             return new PhotoBitmapLoadTask(v, decodeWidth, decodeHeight,
    473                     resolver, adapter);
    474         }
    475 
    476         private final class PhotoBitmapLoadTask extends BitmapLoadTask {
    477             private final int mDecodeWidth;
    478             private final int mDecodeHeight;
    479             private final ContentResolver mResolver;
    480             private final LocalDataAdapter mAdapter;
    481 
    482             private boolean mNeedsRefresh;
    483 
    484             public PhotoBitmapLoadTask(ImageView v, int decodeWidth,
    485                     int decodeHeight, ContentResolver resolver,
    486                     LocalDataAdapter adapter) {
    487                 super(v);
    488                 mDecodeWidth = decodeWidth;
    489                 mDecodeHeight = decodeHeight;
    490                 mResolver = resolver;
    491                 mAdapter = adapter;
    492             }
    493 
    494             @Override
    495             protected Bitmap doInBackground(Void... v) {
    496                 int sampleSize = 1;
    497                 if (mWidth > mDecodeWidth || mHeight > mDecodeHeight) {
    498                     int heightRatio = Math.round((float) mHeight / (float) mDecodeHeight);
    499                     int widthRatio = Math.round((float) mWidth / (float) mDecodeWidth);
    500                     sampleSize = Math.max(heightRatio, widthRatio);
    501                 }
    502 
    503                 // For correctness, we need to double check the size here. The
    504                 // good news is that decoding bounds take much less time than
    505                 // decoding samples like < 1%.
    506                 // TODO: better organize the decoding and sampling by using a
    507                 // image cache.
    508                 int decodedWidth = 0;
    509                 int decodedHeight = 0;
    510                 BitmapFactory.Options justBoundsOpts = new BitmapFactory.Options();
    511                 justBoundsOpts.inJustDecodeBounds = true;
    512                 BitmapFactory.decodeFile(mPath, justBoundsOpts);
    513                 if (justBoundsOpts.outWidth > 0 && justBoundsOpts.outHeight > 0) {
    514                     decodedWidth = justBoundsOpts.outWidth;
    515                     decodedHeight = justBoundsOpts.outHeight;
    516                 }
    517 
    518                 // If the width and height is valid and not matching the values
    519                 // from MediaStore, then update the MediaStore. This only
    520                 // happened when the MediaStore had been told a wrong data.
    521                 if (decodedWidth > 0 && decodedHeight > 0 &&
    522                         (decodedWidth != mWidth || decodedHeight != mHeight)) {
    523                     ContentValues values = new ContentValues();
    524                     values.put(Images.Media.WIDTH, decodedWidth);
    525                     values.put(Images.Media.HEIGHT, decodedHeight);
    526                     mResolver.update(getContentUri(), values, null, null);
    527                     mNeedsRefresh = true;
    528                     Log.w(TAG, "Uri " + getContentUri() + " has been updated with" +
    529                             " correct size!");
    530                     return null;
    531                 }
    532 
    533                 BitmapFactory.Options opts = new BitmapFactory.Options();
    534                 opts.inSampleSize = sampleSize;
    535                 opts.inTempStorage = DECODE_TEMP_STORAGE;
    536                 if (isCancelled() || !isUsing()) {
    537                     return null;
    538                 }
    539                 Bitmap b = BitmapFactory.decodeFile(mPath, opts);
    540 
    541                 if (mOrientation != 0 && b != null) {
    542                     if (isCancelled() || !isUsing()) {
    543                         return null;
    544                     }
    545                     Matrix m = new Matrix();
    546                     m.setRotate(mOrientation);
    547                     b = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false);
    548                 }
    549                 return b;
    550             }
    551 
    552             @Override
    553             protected void onPostExecute(Bitmap bitmap) {
    554                 super.onPostExecute(bitmap);
    555                 if (mNeedsRefresh && mAdapter != null) {
    556                     mAdapter.refresh(mResolver, getContentUri());
    557                 }
    558             }
    559         }
    560 
    561         @Override
    562         public boolean rotate90Degrees(Context context, LocalDataAdapter adapter,
    563                 int currentDataId, boolean clockwise) {
    564             RotationTask task = new RotationTask(context, adapter,
    565                     currentDataId, clockwise);
    566             task.execute(this);
    567             return true;
    568         }
    569     }
    570 
    571     public static final class VideoData extends LocalMediaData {
    572         public static final int COL_ID = 0;
    573         public static final int COL_TITLE = 1;
    574         public static final int COL_MIME_TYPE = 2;
    575         public static final int COL_DATE_TAKEN = 3;
    576         public static final int COL_DATE_MODIFIED = 4;
    577         public static final int COL_DATA = 5;
    578         public static final int COL_WIDTH = 6;
    579         public static final int COL_HEIGHT = 7;
    580         public static final int COL_RESOLUTION = 8;
    581         public static final int COL_SIZE = 9;
    582         public static final int COL_LATITUDE = 10;
    583         public static final int COL_LONGITUDE = 11;
    584         public static final int COL_DURATION = 12;
    585 
    586         static final Uri CONTENT_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
    587 
    588         private static final int mSupportedUIActions =
    589                 FilmStripView.ImageData.ACTION_DEMOTE
    590                         | FilmStripView.ImageData.ACTION_PROMOTE;
    591         private static final int mSupportedDataActions =
    592                 LocalData.ACTION_DELETE
    593                         | LocalData.ACTION_PLAY;
    594 
    595         static final String QUERY_ORDER = MediaStore.Video.VideoColumns.DATE_TAKEN + " DESC, "
    596                 + MediaStore.Video.VideoColumns._ID + " DESC";
    597         /**
    598          * These values should be kept in sync with column IDs (COL_*) above.
    599          */
    600         static final String[] QUERY_PROJECTION = {
    601                 MediaStore.Video.VideoColumns._ID,           // 0, int
    602                 MediaStore.Video.VideoColumns.TITLE,         // 1, string
    603                 MediaStore.Video.VideoColumns.MIME_TYPE,     // 2, string
    604                 MediaStore.Video.VideoColumns.DATE_TAKEN,    // 3, int
    605                 MediaStore.Video.VideoColumns.DATE_MODIFIED, // 4, int
    606                 MediaStore.Video.VideoColumns.DATA,          // 5, string
    607                 MediaStore.Video.VideoColumns.WIDTH,         // 6, int
    608                 MediaStore.Video.VideoColumns.HEIGHT,        // 7, int
    609                 MediaStore.Video.VideoColumns.RESOLUTION,    // 8 string
    610                 MediaStore.Video.VideoColumns.SIZE,          // 9 long
    611                 MediaStore.Video.VideoColumns.LATITUDE,      // 10 double
    612                 MediaStore.Video.VideoColumns.LONGITUDE,     // 11 double
    613                 MediaStore.Video.VideoColumns.DURATION       // 12 long
    614         };
    615 
    616         /** The duration in milliseconds. */
    617         private long mDurationInSeconds;
    618 
    619         public VideoData(long id, String title, String mimeType,
    620                 long dateTakenInSeconds, long dateModifiedInSeconds,
    621                 String path, int width, int height, long sizeInBytes,
    622                 double latitude, double longitude, long durationInSeconds) {
    623             super(id, title, mimeType, dateTakenInSeconds, dateModifiedInSeconds,
    624                     path, width, height, sizeInBytes, latitude, longitude);
    625             mDurationInSeconds = durationInSeconds;
    626         }
    627 
    628         static VideoData buildFromCursor(Cursor c) {
    629             long id = c.getLong(COL_ID);
    630             String title = c.getString(COL_TITLE);
    631             String mimeType = c.getString(COL_MIME_TYPE);
    632             long dateTakenInSeconds = c.getLong(COL_DATE_TAKEN);
    633             long dateModifiedInSeconds = c.getLong(COL_DATE_MODIFIED);
    634             String path = c.getString(COL_DATA);
    635             int width = c.getInt(COL_WIDTH);
    636             int height = c.getInt(COL_HEIGHT);
    637             MediaMetadataRetriever retriever = new MediaMetadataRetriever();
    638             String rotation = null;
    639             try {
    640                 retriever.setDataSource(path);
    641             } catch (RuntimeException ex) {
    642                 // setDataSource() can cause RuntimeException beyond
    643                 // IllegalArgumentException. e.g: data contain *.avi file.
    644                 retriever.release();
    645                 Log.e(TAG, "MediaMetadataRetriever.setDataSource() fail:"
    646                         + ex.getMessage());
    647                 return null;
    648             }
    649             rotation = retriever.extractMetadata(
    650                     MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
    651 
    652             // Extracts video height/width if available. If unavailable, set to 0.
    653             if (width == 0 || height == 0) {
    654                 String val = retriever.extractMetadata(
    655                         MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
    656                 width = (val == null) ? 0 : Integer.parseInt(val);
    657                 val = retriever.extractMetadata(
    658                         MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
    659                 height = (val == null) ? 0 : Integer.parseInt(val);
    660             }
    661             retriever.release();
    662             if (width == 0 || height == 0) {
    663                 // Width or height is still not available.
    664                 Log.e(TAG, "Unable to retrieve dimension of video:" + path);
    665                 return null;
    666             }
    667             if (rotation != null
    668                     && (rotation.equals("90") || rotation.equals("270"))) {
    669                 int b = width;
    670                 width = height;
    671                 height = b;
    672             }
    673 
    674             long sizeInBytes = c.getLong(COL_SIZE);
    675             double latitude = c.getDouble(COL_LATITUDE);
    676             double longitude = c.getDouble(COL_LONGITUDE);
    677             long durationInSeconds = c.getLong(COL_DURATION) / 1000;
    678             VideoData d = new VideoData(id, title, mimeType, dateTakenInSeconds,
    679                     dateModifiedInSeconds, path, width, height, sizeInBytes,
    680                     latitude, longitude, durationInSeconds);
    681             return d;
    682         }
    683 
    684         @Override
    685         public String toString() {
    686             return "Video:" + ",data=" + mPath + ",mimeType=" + mMimeType
    687                     + "," + mWidth + "x" + mHeight + ",date=" + new Date(mDateTakenInSeconds);
    688         }
    689 
    690         @Override
    691         public int getViewType() {
    692             return VIEW_TYPE_REMOVABLE;
    693         }
    694 
    695         @Override
    696         public boolean isUIActionSupported(int action) {
    697             return ((action & mSupportedUIActions) == action);
    698         }
    699 
    700         @Override
    701         public boolean isDataActionSupported(int action) {
    702             return ((action & mSupportedDataActions) == action);
    703         }
    704 
    705         @Override
    706         public boolean delete(Context ctx) {
    707             ContentResolver cr = ctx.getContentResolver();
    708             cr.delete(CONTENT_URI, MediaStore.Video.VideoColumns._ID + "=" + mContentId, null);
    709             return super.delete(ctx);
    710         }
    711 
    712         @Override
    713         public Uri getContentUri() {
    714             Uri baseUri = CONTENT_URI;
    715             return baseUri.buildUpon().appendPath(String.valueOf(mContentId)).build();
    716         }
    717 
    718         @Override
    719         public MediaDetails getMediaDetails(Context context) {
    720             MediaDetails mediaDetails = super.getMediaDetails(context);
    721             String duration = MediaDetails.formatDuration(context, mDurationInSeconds);
    722             mediaDetails.addDetail(MediaDetails.INDEX_DURATION, duration);
    723             return mediaDetails;
    724         }
    725 
    726         @Override
    727         public int getLocalDataType() {
    728             return LOCAL_VIDEO;
    729         }
    730 
    731         @Override
    732         public LocalData refresh(ContentResolver resolver) {
    733             Cursor c = resolver.query(
    734                     getContentUri(), QUERY_PROJECTION, null, null, null);
    735             if (c == null || !c.moveToFirst()) {
    736                 return null;
    737             }
    738             VideoData newData = buildFromCursor(c);
    739             return newData;
    740         }
    741 
    742         @Override
    743         public View getView(final Activity activity,
    744                 int decodeWidth, int decodeHeight, Drawable placeHolder,
    745                 LocalDataAdapter adapter) {
    746 
    747             // ImageView for the bitmap.
    748             ImageView iv = new ImageView(activity);
    749             iv.setLayoutParams(new FrameLayout.LayoutParams(
    750                     ViewGroup.LayoutParams.MATCH_PARENT,
    751                     ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
    752             fillImageView(activity, iv, decodeWidth, decodeHeight, placeHolder,
    753                     adapter);
    754 
    755             // ImageView for the play icon.
    756             ImageView icon = new ImageView(activity);
    757             icon.setImageResource(R.drawable.ic_control_play);
    758             icon.setScaleType(ImageView.ScaleType.CENTER);
    759             icon.setLayoutParams(new FrameLayout.LayoutParams(
    760                     ViewGroup.LayoutParams.WRAP_CONTENT,
    761                     ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
    762             icon.setOnClickListener(new View.OnClickListener() {
    763                 @Override
    764                 public void onClick(View v) {
    765                     CameraUtil.playVideo(activity, getContentUri(), mTitle);
    766                 }
    767             });
    768 
    769             FrameLayout f = new FrameLayout(activity);
    770             f.addView(iv);
    771             f.addView(icon);
    772             return f;
    773         }
    774 
    775         @Override
    776         public boolean isPhoto() {
    777             return false;
    778         }
    779 
    780         @Override
    781         protected BitmapLoadTask getBitmapLoadTask(
    782                 ImageView v, int decodeWidth, int decodeHeight,
    783                 ContentResolver resolver, LocalDataAdapter adapter) {
    784             return new VideoBitmapLoadTask(v);
    785         }
    786 
    787         private final class VideoBitmapLoadTask extends BitmapLoadTask {
    788 
    789             public VideoBitmapLoadTask(ImageView v) {
    790                 super(v);
    791             }
    792 
    793             @Override
    794             protected Bitmap doInBackground(Void... v) {
    795                 if (isCancelled() || !isUsing()) {
    796                     return null;
    797                 }
    798                 MediaMetadataRetriever retriever = new MediaMetadataRetriever();
    799                 Bitmap bitmap = null;
    800                 try {
    801                     retriever.setDataSource(mPath);
    802                     byte[] data = retriever.getEmbeddedPicture();
    803                     if (!isCancelled() && isUsing()) {
    804                         if (data != null) {
    805                             bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
    806                         }
    807                         if (bitmap == null) {
    808                             bitmap = retriever.getFrameAtTime();
    809                         }
    810                     }
    811                 } catch (IllegalArgumentException e) {
    812                     Log.e(TAG, "MediaMetadataRetriever.setDataSource() fail:"
    813                             + e.getMessage());
    814                 }
    815                 retriever.release();
    816                 return bitmap;
    817             }
    818         }
    819 
    820         @Override
    821         public boolean rotate90Degrees(Context context, LocalDataAdapter adapter,
    822                 int currentDataId, boolean clockwise) {
    823             // We don't support rotation for video data.
    824             Log.e(TAG, "Unexpected call in rotate90Degrees()");
    825             return false;
    826         }
    827     }
    828 
    829     /**
    830      * An {@link AsyncTask} class that loads the bitmap in the background
    831      * thread. Sub-classes should implement their own
    832      * {@code BitmapLoadTask#doInBackground(Void...)}."
    833      */
    834     protected abstract class BitmapLoadTask extends AsyncTask<Void, Void, Bitmap> {
    835         protected ImageView mView;
    836 
    837         protected BitmapLoadTask(ImageView v) {
    838             mView = v;
    839         }
    840 
    841         @Override
    842         protected void onPostExecute(Bitmap bitmap) {
    843             if (!isUsing()) {
    844                 return;
    845             }
    846             if (bitmap == null) {
    847                 Log.e(TAG, "Failed decoding bitmap for file:" + mPath);
    848                 return;
    849             }
    850             BitmapDrawable d = new BitmapDrawable(bitmap);
    851             mView.setScaleType(ImageView.ScaleType.FIT_XY);
    852             mView.setImageDrawable(d);
    853         }
    854     }
    855 }
    856