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