1 /* 2 * Copyright (C) 2007 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; 18 19 import com.android.camera.gallery.IImage; 20 import com.android.camera.gallery.IImageList; 21 import com.android.camera.gallery.VideoObject; 22 23 import android.app.Activity; 24 import android.app.AlertDialog; 25 import android.app.Dialog; 26 import android.app.ProgressDialog; 27 import android.content.ActivityNotFoundException; 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.DialogInterface; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.content.SharedPreferences; 34 import android.content.pm.ActivityInfo; 35 import android.content.res.Configuration; 36 import android.content.res.Resources; 37 import android.graphics.Bitmap; 38 import android.graphics.BitmapFactory; 39 import android.graphics.Canvas; 40 import android.graphics.Paint; 41 import android.graphics.Rect; 42 import android.graphics.drawable.Drawable; 43 import android.net.Uri; 44 import android.os.Bundle; 45 import android.os.Handler; 46 import android.os.PowerManager; 47 import android.preference.PreferenceManager; 48 import android.provider.MediaStore; 49 import android.util.Log; 50 import android.view.ContextMenu; 51 import android.view.KeyEvent; 52 import android.view.Menu; 53 import android.view.MenuItem; 54 import android.view.View; 55 import android.view.Window; 56 import android.view.View.OnClickListener; 57 import android.view.animation.Animation; 58 import android.view.animation.AnimationUtils; 59 import android.widget.Button; 60 import android.widget.TextView; 61 62 import java.util.ArrayList; 63 import java.util.HashSet; 64 65 public class ImageGallery extends Activity implements 66 GridViewSpecial.Listener, GridViewSpecial.DrawAdapter { 67 private static final String STATE_SCROLL_POSITION = "scroll_position"; 68 private static final String STATE_SELECTED_INDEX = "first_index"; 69 70 private static final String TAG = "ImageGallery"; 71 private static final float INVALID_POSITION = -1f; 72 private IImageList mAllImages; 73 private int mInclusion; 74 boolean mSortAscending = false; 75 private View mNoImagesView; 76 public static final int CROP_MSG = 2; 77 78 private Dialog mMediaScanningDialog; 79 private MenuItem mSlideShowItem; 80 private SharedPreferences mPrefs; 81 private long mVideoSizeLimit = Long.MAX_VALUE; 82 private View mFooterOrganizeView; 83 84 private BroadcastReceiver mReceiver = null; 85 86 private final Handler mHandler = new Handler(); 87 private boolean mLayoutComplete; 88 private boolean mPausing = true; 89 private ImageLoader mLoader; 90 private GridViewSpecial mGvs; 91 92 private Uri mCropResultUri; 93 94 // The index of the first picture in GridViewSpecial. 95 private int mSelectedIndex = GridViewSpecial.INDEX_NONE; 96 private float mScrollPosition = INVALID_POSITION; 97 private boolean mConfigurationChanged = false; 98 99 private HashSet<IImage> mMultiSelected = null; 100 101 @Override onCreate(Bundle icicle)102 public void onCreate(Bundle icicle) { 103 super.onCreate(icicle); 104 105 mPrefs = PreferenceManager.getDefaultSharedPreferences(this); 106 107 // Must be called before setContentView(). 108 requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); 109 110 setContentView(R.layout.image_gallery_2); 111 112 getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, 113 R.layout.custom_gallery_title); 114 115 mNoImagesView = findViewById(R.id.no_images); 116 117 mGvs = (GridViewSpecial) findViewById(R.id.grid); 118 mGvs.setListener(this); 119 120 mFooterOrganizeView = findViewById(R.id.footer_organize); 121 122 // consume all click events on the footer view 123 mFooterOrganizeView.setOnClickListener(Util.getNullOnClickListener()); 124 initializeFooterButtons(); 125 126 if (isPickIntent()) { 127 mVideoSizeLimit = getIntent().getLongExtra( 128 MediaStore.EXTRA_SIZE_LIMIT, Long.MAX_VALUE); 129 } else { 130 mVideoSizeLimit = Long.MAX_VALUE; 131 mGvs.setOnCreateContextMenuListener( 132 new CreateContextMenuListener()); 133 } 134 135 setupInclusion(); 136 137 mLoader = new ImageLoader(mHandler); 138 } 139 initializeFooterButtons()140 private void initializeFooterButtons() { 141 Button deleteButton = (Button) findViewById(R.id.button_delete); 142 deleteButton.setOnClickListener(new OnClickListener() { 143 public void onClick(View v) { 144 onDeleteMultipleClicked(); 145 } 146 }); 147 148 Button closeButton = (Button) findViewById(R.id.button_close); 149 closeButton.setOnClickListener(new OnClickListener() { 150 public void onClick(View v) { 151 closeMultiSelectMode(); 152 } 153 }); 154 } 155 addSlideShowMenu(Menu menu, int position)156 private MenuItem addSlideShowMenu(Menu menu, int position) { 157 return menu.add(0, 207, position, R.string.slide_show) 158 .setOnMenuItemClickListener( 159 new MenuItem.OnMenuItemClickListener() { 160 public boolean onMenuItemClick(MenuItem item) { 161 return onSlideShowClicked(); 162 } 163 }).setIcon(android.R.drawable.ic_menu_slideshow); 164 } 165 166 public boolean onSlideShowClicked() { 167 if (!canHandleEvent()) { 168 return false; 169 } 170 IImage img = getCurrentImage(); 171 if (img == null) { 172 img = mAllImages.getImageAt(0); 173 if (img == null) { 174 return true; 175 } 176 } 177 Uri targetUri = img.fullSizeImageUri(); 178 Uri thisUri = getIntent().getData(); 179 if (thisUri != null) { 180 String bucket = thisUri.getQueryParameter("bucketId"); 181 if (bucket != null) { 182 targetUri = targetUri.buildUpon() 183 .appendQueryParameter("bucketId", bucket) 184 .build(); 185 } 186 } 187 Intent intent = new Intent(Intent.ACTION_VIEW, targetUri); 188 intent.putExtra("slideshow", true); 189 startActivity(intent); 190 return true; 191 } 192 193 private final Runnable mDeletePhotoRunnable = new Runnable() { 194 public void run() { 195 if (!canHandleEvent()) return; 196 stopCheckingThumbnails(); 197 198 IImage currentImage = getCurrentImage(); 199 200 // The selection will be cleared when mGvs.stop() is called, so 201 // we need to call getCurrentImage() before mGvs.stop(). 202 mGvs.stop(); 203 204 if (currentImage != null) { 205 mAllImages.removeImage(currentImage); 206 } 207 mGvs.setImageList(mAllImages); 208 mGvs.start(); 209 210 checkThumbnails(); 211 mNoImagesView.setVisibility(mAllImages.isEmpty() 212 ? View.VISIBLE 213 : View.GONE); 214 } 215 }; 216 217 private Uri getCurrentImageUri() { 218 IImage image = getCurrentImage(); 219 if (image != null) { 220 return image.fullSizeImageUri(); 221 } else { 222 return null; 223 } 224 } 225 226 private IImage getCurrentImage() { 227 int currentSelection = mGvs.getCurrentSelection(); 228 if (currentSelection < 0 229 || currentSelection >= mAllImages.getCount()) { 230 return null; 231 } else { 232 return mAllImages.getImageAt(currentSelection); 233 } 234 } 235 236 @Override 237 public void onConfigurationChanged(Configuration newConfig) { 238 super.onConfigurationChanged(newConfig); 239 mConfigurationChanged = true; 240 } 241 242 boolean canHandleEvent() { 243 // Don't process event in pause state. 244 return (!mPausing) && (mLayoutComplete); 245 } 246 247 @Override 248 public boolean onKeyDown(int keyCode, KeyEvent event) { 249 if (!canHandleEvent()) return false; 250 switch (keyCode) { 251 case KeyEvent.KEYCODE_DEL: 252 IImage image = getCurrentImage(); 253 if (image != null) { 254 MenuHelper.deleteImage( 255 this, mDeletePhotoRunnable, getCurrentImage()); 256 } 257 return true; 258 } 259 return super.onKeyDown(keyCode, event); 260 } 261 262 private boolean isPickIntent() { 263 String action = getIntent().getAction(); 264 return (Intent.ACTION_PICK.equals(action) 265 || Intent.ACTION_GET_CONTENT.equals(action)); 266 } 267 268 private void launchCropperOrFinish(IImage img) { 269 Bundle myExtras = getIntent().getExtras(); 270 271 long size = MenuHelper.getImageFileSize(img); 272 if (size < 0) { 273 // Return if the image file is not available. 274 return; 275 } 276 277 if (size > mVideoSizeLimit) { 278 DialogInterface.OnClickListener buttonListener = 279 new DialogInterface.OnClickListener() { 280 public void onClick(DialogInterface dialog, int which) { 281 dialog.dismiss(); 282 } 283 }; 284 new AlertDialog.Builder(this) 285 .setIcon(android.R.drawable.ic_dialog_info) 286 .setTitle(R.string.file_info_title) 287 .setMessage(R.string.video_exceed_mms_limit) 288 .setNeutralButton(R.string.details_ok, buttonListener) 289 .show(); 290 return; 291 } 292 293 String cropValue = myExtras != null ? myExtras.getString("crop") : null; 294 if (cropValue != null) { 295 Bundle newExtras = new Bundle(); 296 if (cropValue.equals("circle")) { 297 newExtras.putString("circleCrop", "true"); 298 } 299 300 Intent cropIntent = new Intent(); 301 cropIntent.setData(img.fullSizeImageUri()); 302 cropIntent.setClass(this, CropImage.class); 303 cropIntent.putExtras(newExtras); 304 305 /* pass through any extras that were passed in */ 306 cropIntent.putExtras(myExtras); 307 startActivityForResult(cropIntent, CROP_MSG); 308 } else { 309 Intent result = new Intent(null, img.fullSizeImageUri()); 310 if (myExtras != null && myExtras.getBoolean("return-data")) { 311 // The size of a transaction should be below 100K. 312 Bitmap bitmap = img.fullSizeBitmap( 313 IImage.UNCONSTRAINED, 100 * 1024); 314 if (bitmap != null) { 315 result.putExtra("data", bitmap); 316 } 317 } 318 setResult(RESULT_OK, result); 319 finish(); 320 } 321 } 322 323 @Override 324 protected void onActivityResult(int requestCode, int resultCode, 325 Intent data) { 326 switch (requestCode) { 327 case MenuHelper.RESULT_COMMON_MENU_CROP: { 328 if (resultCode == RESULT_OK) { 329 330 // The CropImage activity passes back the Uri of the cropped 331 // image as the Action rather than the Data. 332 // We store this URI so we can move the selection box to it 333 // later. 334 mCropResultUri = Uri.parse(data.getAction()); 335 } 336 break; 337 } 338 case CROP_MSG: { 339 if (resultCode == RESULT_OK) { 340 setResult(resultCode, data); 341 finish(); 342 } 343 break; 344 } 345 } 346 } 347 348 @Override 349 public void onPause() { 350 super.onPause(); 351 mPausing = true; 352 353 mLoader.stop(); 354 355 mGvs.stop(); 356 357 if (mReceiver != null) { 358 unregisterReceiver(mReceiver); 359 mReceiver = null; 360 } 361 362 // Now that we've paused the threads that are using the cursor it is 363 // safe to deactivate it. 364 mAllImages.deactivate(); 365 mAllImages = null; 366 } 367 368 private void rebake(boolean unmounted, boolean scanning) { 369 stopCheckingThumbnails(); 370 mGvs.stop(); 371 if (mAllImages != null) { 372 mAllImages.deactivate(); 373 mAllImages = null; 374 } 375 376 if (mMediaScanningDialog != null) { 377 mMediaScanningDialog.cancel(); 378 mMediaScanningDialog = null; 379 } 380 381 if (scanning) { 382 mMediaScanningDialog = ProgressDialog.show( 383 this, 384 null, 385 getResources().getString(R.string.wait), 386 true, 387 true); 388 } 389 390 mAllImages = allImages(!unmounted && !scanning); 391 392 mGvs.setImageList(mAllImages); 393 mGvs.setDrawAdapter(this); 394 mGvs.setLoader(mLoader); 395 mGvs.start(); 396 checkThumbnails(); 397 mNoImagesView.setVisibility(mAllImages.getCount() > 0 398 ? View.GONE 399 : View.VISIBLE); 400 } 401 402 @Override 403 protected void onSaveInstanceState(Bundle state) { 404 super.onSaveInstanceState(state); 405 state.putFloat(STATE_SCROLL_POSITION, mScrollPosition); 406 state.putInt(STATE_SELECTED_INDEX, mSelectedIndex); 407 } 408 409 @Override 410 protected void onRestoreInstanceState(Bundle state) { 411 super.onRestoreInstanceState(state); 412 mScrollPosition = state.getFloat( 413 STATE_SCROLL_POSITION, INVALID_POSITION); 414 mSelectedIndex = state.getInt(STATE_SELECTED_INDEX, 0); 415 } 416 417 @Override 418 public void onResume() { 419 super.onResume(); 420 421 mGvs.setSizeChoice(Integer.parseInt( 422 mPrefs.getString("pref_gallery_size_key", "1"))); 423 mGvs.requestFocus(); 424 425 String sortOrder = mPrefs.getString("pref_gallery_sort_key", null); 426 if (sortOrder != null) { 427 mSortAscending = sortOrder.equals("ascending"); 428 } 429 430 mPausing = false; 431 432 // install an intent filter to receive SD card related events. 433 IntentFilter intentFilter = 434 new IntentFilter(Intent.ACTION_MEDIA_MOUNTED); 435 intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); 436 intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); 437 intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED); 438 intentFilter.addAction(Intent.ACTION_MEDIA_EJECT); 439 intentFilter.addDataScheme("file"); 440 441 mReceiver = new BroadcastReceiver() { 442 @Override 443 public void onReceive(Context context, Intent intent) { 444 String action = intent.getAction(); 445 if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) { 446 // SD card available 447 // TODO put up a "please wait" message 448 // TODO also listen for the media scanner finished message 449 } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) { 450 // SD card unavailable 451 rebake(true, false); 452 } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) { 453 rebake(false, true); 454 } else if (action.equals( 455 Intent.ACTION_MEDIA_SCANNER_FINISHED)) { 456 rebake(false, false); 457 } else if (action.equals(Intent.ACTION_MEDIA_EJECT)) { 458 rebake(true, false); 459 } 460 } 461 }; 462 registerReceiver(mReceiver, intentFilter); 463 rebake(false, ImageManager.isMediaScannerScanning( 464 getContentResolver())); 465 } 466 467 private void stopCheckingThumbnails() { 468 mLoader.stopCheckingThumbnails(); 469 } 470 471 private void checkThumbnails() { 472 ImageLoader.ThumbCheckCallback cb = new MyThumbCheckCallback(); 473 mLoader.startCheckingThumbnails(mAllImages, cb); 474 } 475 476 @Override 477 public boolean onCreateOptionsMenu(Menu menu) { 478 if (isPickIntent()) { 479 String type = getIntent().resolveType(this); 480 if (type != null) { 481 if (isImageType(type)) { 482 MenuHelper.addCapturePictureMenuItems(menu, this); 483 } else if (isVideoType(type)) { 484 MenuHelper.addCaptureVideoMenuItems(menu, this); 485 } 486 } 487 } else { 488 MenuHelper.addCaptureMenuItems(menu, this); 489 if ((mInclusion & ImageManager.INCLUDE_IMAGES) != 0) { 490 mSlideShowItem = addSlideShowMenu(menu, 5); 491 } 492 493 MenuItem item = menu.add(0, 0, 1000, R.string.camerasettings); 494 item.setOnMenuItemClickListener( 495 new MenuItem.OnMenuItemClickListener() { 496 public boolean onMenuItemClick(MenuItem item) { 497 Intent preferences = new Intent(); 498 preferences.setClass(ImageGallery.this, 499 GallerySettings.class); 500 startActivity(preferences); 501 return true; 502 } 503 }); 504 item.setAlphabeticShortcut('p'); 505 item.setIcon(android.R.drawable.ic_menu_preferences); 506 507 item = menu.add(0, 0, 900, R.string.multiselect); 508 item.setOnMenuItemClickListener( 509 new MenuItem.OnMenuItemClickListener() { 510 public boolean onMenuItemClick(MenuItem item) { 511 if (isInMultiSelectMode()) { 512 closeMultiSelectMode(); 513 } else { 514 openMultiSelectMode(); 515 } 516 return true; 517 } 518 }); 519 item.setIcon(R.drawable.ic_menu_multiselect_gallery); 520 } 521 return true; 522 } 523 524 @Override 525 public boolean onPrepareOptionsMenu(Menu menu) { 526 if (!canHandleEvent()) return false; 527 if ((mInclusion & ImageManager.INCLUDE_IMAGES) != 0) { 528 boolean videoSelected = isVideoSelected(); 529 // TODO: Only enable slide show if there is at least one image in 530 // the folder. 531 if (mSlideShowItem != null) { 532 mSlideShowItem.setEnabled(!videoSelected); 533 } 534 } 535 536 return true; 537 } 538 539 private boolean isVideoSelected() { 540 IImage image = getCurrentImage(); 541 return (image != null) && ImageManager.isVideo(image); 542 } 543 544 private boolean isImageType(String type) { 545 return type.equals("vnd.android.cursor.dir/image") 546 || type.equals("image/*"); 547 } 548 549 private boolean isVideoType(String type) { 550 return type.equals("vnd.android.cursor.dir/video") 551 || type.equals("video/*"); 552 } 553 554 // According to the intent, setup what we include (image/video) in the 555 // gallery and the title of the gallery. 556 private void setupInclusion() { 557 mInclusion = ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS; 558 559 Intent intent = getIntent(); 560 if (intent != null) { 561 String type = intent.resolveType(this); 562 TextView leftText = (TextView) findViewById(R.id.left_text); 563 if (type != null) { 564 if (isImageType(type)) { 565 mInclusion = ImageManager.INCLUDE_IMAGES; 566 if (isPickIntent()) { 567 leftText.setText(R.string.pick_photos_gallery_title); 568 } else { 569 leftText.setText(R.string.photos_gallery_title); 570 } 571 } 572 if (isVideoType(type)) { 573 mInclusion = ImageManager.INCLUDE_VIDEOS; 574 if (isPickIntent()) { 575 leftText.setText(R.string.pick_videos_gallery_title); 576 } else { 577 leftText.setText(R.string.videos_gallery_title); 578 } 579 } 580 } 581 Bundle extras = intent.getExtras(); 582 String title = (extras != null) 583 ? extras.getString("windowTitle") 584 : null; 585 if (title != null && title.length() > 0) { 586 leftText.setText(title); 587 } 588 589 if (extras != null) { 590 mInclusion = (ImageManager.INCLUDE_IMAGES 591 | ImageManager.INCLUDE_VIDEOS) 592 & extras.getInt("mediaTypes", mInclusion); 593 } 594 595 if (extras != null && extras.getBoolean("pick-drm")) { 596 Log.d(TAG, "pick-drm is true"); 597 mInclusion = ImageManager.INCLUDE_DRM_IMAGES; 598 } 599 } 600 } 601 602 // Returns the image list which contains the subset of image/video we want. 603 private IImageList allImages(boolean storageAvailable) { 604 Uri uri = getIntent().getData(); 605 IImageList imageList; 606 if (!storageAvailable) { 607 imageList = ImageManager.emptyImageList(); 608 } else { 609 imageList = ImageManager.allImages( 610 getContentResolver(), 611 ImageManager.DataLocation.EXTERNAL, 612 mInclusion, 613 mSortAscending 614 ? ImageManager.SORT_ASCENDING 615 : ImageManager.SORT_DESCENDING, 616 (uri != null) 617 ? uri.getQueryParameter("bucketId") 618 : null); 619 } 620 return imageList; 621 } 622 623 private void toggleMultiSelected(IImage image) { 624 int original = mMultiSelected.size(); 625 if (!mMultiSelected.add(image)) { 626 mMultiSelected.remove(image); 627 } 628 mGvs.invalidate(); 629 if (original == 0) showFooter(); 630 if (mMultiSelected.size() == 0) hideFooter(); 631 } 632 633 public void onImageClicked(int index) { 634 if (index < 0 || index >= mAllImages.getCount()) { 635 return; 636 } 637 mSelectedIndex = index; 638 mGvs.setSelectedIndex(index); 639 640 IImage image = mAllImages.getImageAt(index); 641 642 if (isInMultiSelectMode()) { 643 toggleMultiSelected(image); 644 return; 645 } 646 647 if (isPickIntent()) { 648 launchCropperOrFinish(image); 649 } else { 650 Intent intent; 651 if (image instanceof VideoObject) { 652 intent = new Intent( 653 Intent.ACTION_VIEW, image.fullSizeImageUri()); 654 intent.putExtra(MediaStore.EXTRA_SCREEN_ORIENTATION, 655 ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 656 } else { 657 intent = new Intent(this, ViewImage.class); 658 intent.putExtra(ViewImage.KEY_IMAGE_LIST, mAllImages); 659 intent.setData(image.fullSizeImageUri()); 660 } 661 startActivity(intent); 662 } 663 } 664 665 public void onImageTapped(int index) { 666 // In the multiselect mode, once the finger finishes tapping, we hide 667 // the selection box by setting the selected index to none. However, if 668 // we use the dpad center key, we will keep the selected index in order 669 // to show the the selection box. We do this because we have the 670 // multiselect marker on the images to indicate which of them are selected, 671 // so we don't need the selection box, but in the dpad case we still 672 // need the selection box to show as a "cursor". 673 674 if (isInMultiSelectMode()) { 675 mGvs.setSelectedIndex(GridViewSpecial.INDEX_NONE); 676 toggleMultiSelected(mAllImages.getImageAt(index)); 677 } else { 678 onImageClicked(index); 679 } 680 } 681 682 private final class MyThumbCheckCallback implements 683 ImageLoader.ThumbCheckCallback { 684 private final TextView mProgressTextView; 685 private final String mProgressTextFormatString; 686 boolean mDidSetProgress = false; 687 private long mLastUpdateTime; // initialized to 0 688 private final PowerManager.WakeLock mWakeLock; 689 690 private MyThumbCheckCallback() { 691 Resources resources = getResources(); 692 mProgressTextView = (TextView) findViewById(R.id.loading_text); 693 mProgressTextFormatString = resources.getString( 694 R.string.loading_progress_format_string); 695 PowerManager pm = (PowerManager) 696 getSystemService(Context.POWER_SERVICE); 697 mWakeLock = pm.newWakeLock( 698 PowerManager.SCREEN_BRIGHT_WAKE_LOCK, 699 "ImageGallery.checkThumbnails"); 700 mWakeLock.acquire(); 701 } 702 703 public boolean checking(final int count, final int maxCount) { 704 if (!mLayoutComplete) { 705 return true; 706 } 707 708 if (!mDidSetProgress) { 709 mHandler.post(new Runnable() { 710 public void run() { 711 findViewById(R.id.loading_text).setVisibility( 712 View.VISIBLE); 713 findViewById(android.R.id.progress).setVisibility( 714 View.VISIBLE); 715 } 716 }); 717 mDidSetProgress = true; 718 } 719 mGvs.postInvalidate(); 720 721 // Update the progress text. (Only if it has been one 722 // second since last update, to avoid the UI thread 723 // being overwhelmed by the update). 724 long currentTime = System.currentTimeMillis(); 725 if (currentTime - mLastUpdateTime > 1000) { 726 mHandler.post(new Runnable() { 727 public void run() { 728 String s = String.format(mProgressTextFormatString, 729 maxCount - count); 730 mProgressTextView.setText(s); 731 } 732 }); 733 mLastUpdateTime = currentTime; 734 } 735 return !mPausing; 736 } 737 738 public void done() { 739 // done() should only be called once. Use mWakeLock to verify this. 740 assert mWakeLock.isHeld(); 741 742 mWakeLock.release(); 743 mHandler.post(new Runnable() { 744 public void run() { 745 findViewById(R.id.loading_text).setVisibility(View.GONE); 746 findViewById(android.R.id.progress).setVisibility( 747 View.GONE); 748 } 749 }); 750 } 751 } 752 753 private class CreateContextMenuListener implements 754 View.OnCreateContextMenuListener { 755 public void onCreateContextMenu(ContextMenu menu, View v, 756 ContextMenu.ContextMenuInfo menuInfo) { 757 if (!canHandleEvent()) return; 758 759 if (getCurrentImage() == null) { 760 return; 761 } 762 763 boolean isImage = ImageManager.isImage(getCurrentImage()); 764 if (isImage) { 765 menu.add(0, 0, 0, R.string.view).setOnMenuItemClickListener( 766 new MenuItem.OnMenuItemClickListener() { 767 public boolean onMenuItemClick(MenuItem item) { 768 if (!canHandleEvent()) return false; 769 onImageClicked(mGvs.getCurrentSelection()); 770 return true; 771 } 772 }); 773 } 774 775 menu.setHeaderTitle(isImage 776 ? R.string.context_menu_header 777 : R.string.video_context_menu_header); 778 if ((mInclusion & (ImageManager.INCLUDE_IMAGES 779 | ImageManager.INCLUDE_VIDEOS)) != 0) { 780 MenuHelper.MenuItemsResult r = MenuHelper.addImageMenuItems( 781 menu, 782 MenuHelper.INCLUDE_ALL, 783 ImageGallery.this, 784 mHandler, 785 mDeletePhotoRunnable, 786 new MenuHelper.MenuInvoker() { 787 public void run(MenuHelper.MenuCallback cb) { 788 if (!canHandleEvent()) { 789 return; 790 } 791 cb.run(getCurrentImageUri(), getCurrentImage()); 792 mGvs.invalidateImage(mGvs.getCurrentSelection()); 793 } 794 }); 795 if (r != null) { 796 r.gettingReadyToOpen(menu, getCurrentImage()); 797 } 798 799 if (isImage) { 800 addSlideShowMenu(menu, 1000); 801 } 802 } 803 } 804 } 805 806 public void onLayoutComplete(boolean changed) { 807 mLayoutComplete = true; 808 if (mCropResultUri != null) { 809 IImage image = mAllImages.getImageForUri(mCropResultUri); 810 mCropResultUri = null; 811 if (image != null) { 812 mSelectedIndex = mAllImages.getImageIndex(image); 813 } 814 } 815 mGvs.setSelectedIndex(mSelectedIndex); 816 if (mScrollPosition == INVALID_POSITION) { 817 if (mSortAscending) { 818 mGvs.scrollTo(0, mGvs.getHeight()); 819 } else { 820 mGvs.scrollToImage(0); 821 } 822 } else if (mConfigurationChanged) { 823 mConfigurationChanged = false; 824 mGvs.scrollTo(mScrollPosition); 825 if (mGvs.getCurrentSelection() != GridViewSpecial.INDEX_NONE) { 826 mGvs.scrollToVisible(mSelectedIndex); 827 } 828 } else { 829 mGvs.scrollTo(mScrollPosition); 830 } 831 } 832 833 public void onScroll(float scrollPosition) { 834 mScrollPosition = scrollPosition; 835 } 836 837 private Drawable mVideoOverlay; 838 private Drawable mVideoMmsErrorOverlay; 839 private Drawable mMultiSelectTrue; 840 private Drawable mMultiSelectFalse; 841 842 // mSrcRect and mDstRect are only used in drawImage, but we put them as 843 // instance variables to reduce the memory allocation overhead because 844 // drawImage() is called a lot. 845 private final Rect mSrcRect = new Rect(); 846 private final Rect mDstRect = new Rect(); 847 848 public void drawImage(Canvas canvas, IImage image, 849 Bitmap b, int xPos, int yPos, int w, int h) { 850 if (b != null) { 851 // if the image is close to the target size then crop, 852 // otherwise scale both the bitmap and the view should be 853 // square but I suppose that could change in the future. 854 855 int bw = b.getWidth(); 856 int bh = b.getHeight(); 857 858 int deltaW = bw - w; 859 int deltaH = bh - h; 860 861 if (deltaW < 10 && deltaH < 10) { 862 int halfDeltaW = deltaW / 2; 863 int halfDeltaH = deltaH / 2; 864 mSrcRect.set(0 + halfDeltaW, 0 + halfDeltaH, 865 bw - halfDeltaW, bh - halfDeltaH); 866 mDstRect.set(xPos, yPos, xPos + w, yPos + h); 867 canvas.drawBitmap(b, mSrcRect, mDstRect, null); 868 } else { 869 mSrcRect.set(0, 0, bw, bh); 870 mDstRect.set(xPos, yPos, xPos + w, yPos + h); 871 canvas.drawBitmap(b, mSrcRect, mDstRect, null); 872 } 873 } else { 874 // If the thumbnail cannot be drawn, put up an error icon 875 // instead 876 Bitmap error = getErrorBitmap(image); 877 int width = error.getWidth(); 878 int height = error.getHeight(); 879 mSrcRect.set(0, 0, width, height); 880 int left = (w - width) / 2 + xPos; 881 int top = (w - height) / 2 + yPos; 882 mDstRect.set(left, top, left + width, top + height); 883 canvas.drawBitmap(error, mSrcRect, mDstRect, null); 884 } 885 886 if (ImageManager.isVideo(image)) { 887 Drawable overlay = null; 888 long size = MenuHelper.getImageFileSize(image); 889 if (size >= 0 && size <= mVideoSizeLimit) { 890 if (mVideoOverlay == null) { 891 mVideoOverlay = getResources().getDrawable( 892 R.drawable.ic_gallery_video_overlay); 893 } 894 overlay = mVideoOverlay; 895 } else { 896 if (mVideoMmsErrorOverlay == null) { 897 mVideoMmsErrorOverlay = getResources().getDrawable( 898 R.drawable.ic_error_mms_video_overlay); 899 } 900 overlay = mVideoMmsErrorOverlay; 901 Paint paint = new Paint(); 902 paint.setARGB(0x80, 0x00, 0x00, 0x00); 903 canvas.drawRect(xPos, yPos, xPos + w, yPos + h, paint); 904 } 905 int width = overlay.getIntrinsicWidth(); 906 int height = overlay.getIntrinsicHeight(); 907 int left = (w - width) / 2 + xPos; 908 int top = (h - height) / 2 + yPos; 909 mSrcRect.set(left, top, left + width, top + height); 910 overlay.setBounds(mSrcRect); 911 overlay.draw(canvas); 912 } 913 } 914 915 public boolean needsDecoration() { 916 return (mMultiSelected != null); 917 } 918 919 public void drawDecoration(Canvas canvas, IImage image, 920 int xPos, int yPos, int w, int h) { 921 if (mMultiSelected != null) { 922 initializeMultiSelectDrawables(); 923 924 Drawable checkBox = mMultiSelected.contains(image) 925 ? mMultiSelectTrue 926 : mMultiSelectFalse; 927 int width = checkBox.getIntrinsicWidth(); 928 int height = checkBox.getIntrinsicHeight(); 929 int left = 5 + xPos; 930 int top = h - height - 5 + yPos; 931 mSrcRect.set(left, top, left + width, top + height); 932 checkBox.setBounds(mSrcRect); 933 checkBox.draw(canvas); 934 } 935 } 936 937 private void initializeMultiSelectDrawables() { 938 if (mMultiSelectTrue == null) { 939 mMultiSelectTrue = getResources() 940 .getDrawable(R.drawable.btn_check_buttonless_on); 941 } 942 if (mMultiSelectFalse == null) { 943 mMultiSelectFalse = getResources() 944 .getDrawable(R.drawable.btn_check_buttonless_off); 945 } 946 } 947 948 private Bitmap mMissingImageThumbnailBitmap; 949 private Bitmap mMissingVideoThumbnailBitmap; 950 951 // Create this bitmap lazily, and only once for all the ImageBlocks to 952 // use 953 public Bitmap getErrorBitmap(IImage image) { 954 if (ImageManager.isImage(image)) { 955 if (mMissingImageThumbnailBitmap == null) { 956 mMissingImageThumbnailBitmap = BitmapFactory.decodeResource( 957 getResources(), 958 R.drawable.ic_missing_thumbnail_picture); 959 } 960 return mMissingImageThumbnailBitmap; 961 } else { 962 if (mMissingVideoThumbnailBitmap == null) { 963 mMissingVideoThumbnailBitmap = BitmapFactory.decodeResource( 964 getResources(), R.drawable.ic_missing_thumbnail_video); 965 } 966 return mMissingVideoThumbnailBitmap; 967 } 968 } 969 970 private Animation mFooterAppear; 971 private Animation mFooterDisappear; 972 973 private void showFooter() { 974 mFooterOrganizeView.setVisibility(View.VISIBLE); 975 if (mFooterAppear == null) { 976 mFooterAppear = AnimationUtils.loadAnimation( 977 this, R.anim.footer_appear); 978 } 979 mFooterOrganizeView.startAnimation(mFooterAppear); 980 } 981 982 private void hideFooter() { 983 if (mFooterOrganizeView.getVisibility() != View.GONE) { 984 mFooterOrganizeView.setVisibility(View.GONE); 985 if (mFooterDisappear == null) { 986 mFooterDisappear = AnimationUtils.loadAnimation( 987 this, R.anim.footer_disappear); 988 } 989 mFooterOrganizeView.startAnimation(mFooterDisappear); 990 } 991 } 992 993 private void onDeleteMultipleClicked() { 994 Runnable action = new Runnable() { 995 public void run() { 996 ArrayList<Uri> uriList = new ArrayList<Uri>(); 997 for (IImage image : mMultiSelected) { 998 uriList.add(image.fullSizeImageUri()); 999 } 1000 closeMultiSelectMode(); 1001 Intent intent = new Intent(ImageGallery.this, 1002 DeleteImage.class); 1003 intent.putExtra("delete-uris", uriList); 1004 try { 1005 startActivity(intent); 1006 } catch (ActivityNotFoundException ex) { 1007 Log.e(TAG, "Delete images fail", ex); 1008 } 1009 } 1010 }; 1011 MenuHelper.deleteMultiple(this, action); 1012 } 1013 1014 private boolean isInMultiSelectMode() { 1015 return mMultiSelected != null; 1016 } 1017 1018 private void closeMultiSelectMode() { 1019 if (mMultiSelected == null) return; 1020 mMultiSelected = null; 1021 mGvs.invalidate(); 1022 hideFooter(); 1023 } 1024 1025 private void openMultiSelectMode() { 1026 if (mMultiSelected != null) return; 1027 mMultiSelected = new HashSet<IImage>(); 1028 mGvs.invalidate(); 1029 } 1030 1031 } 1032