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
17package com.android.camera.data;
18
19import android.app.Activity;
20import android.content.ContentResolver;
21import android.content.ContentValues;
22import android.content.Context;
23import android.database.Cursor;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.Matrix;
27import android.graphics.drawable.BitmapDrawable;
28import android.graphics.drawable.Drawable;
29import android.media.MediaMetadataRetriever;
30import android.net.Uri;
31import android.os.AsyncTask;
32import android.provider.MediaStore;
33import android.provider.MediaStore.Images;
34import android.util.Log;
35import android.view.Gravity;
36import android.view.View;
37import android.view.ViewGroup;
38import android.widget.FrameLayout;
39import android.widget.ImageView;
40
41import com.android.camera.ui.FilmStripView;
42import com.android.camera.util.CameraUtil;
43import com.android.camera.util.PhotoSphereHelper;
44import com.android.camera2.R;
45
46import java.io.File;
47import java.text.DateFormat;
48import java.util.Date;
49import 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 */
57public 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