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