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