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.gallery.R; 20 21 import com.android.camera.gallery.IImage; 22 import com.android.camera.gallery.IImageList; 23 24 import android.app.Activity; 25 import android.app.Dialog; 26 import android.app.ProgressDialog; 27 import android.content.BroadcastReceiver; 28 import android.content.ContentResolver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.content.res.Resources; 33 import android.database.ContentObserver; 34 import android.graphics.Bitmap; 35 import android.graphics.Canvas; 36 import android.graphics.Matrix; 37 import android.graphics.Paint; 38 import android.graphics.PorterDuff; 39 import android.graphics.PorterDuffXfermode; 40 import android.graphics.Rect; 41 import android.graphics.drawable.Drawable; 42 import android.net.Uri; 43 import android.os.Bundle; 44 import android.os.Environment; 45 import android.os.Handler; 46 import android.os.StatFs; 47 import android.provider.MediaStore; 48 import android.provider.MediaStore.Images; 49 import android.util.Log; 50 import android.view.ContextMenu; 51 import android.view.LayoutInflater; 52 import android.view.Menu; 53 import android.view.MenuItem; 54 import android.view.View; 55 import android.view.ViewGroup; 56 import android.view.ContextMenu.ContextMenuInfo; 57 import android.view.MenuItem.OnMenuItemClickListener; 58 import android.widget.AdapterView; 59 import android.widget.BaseAdapter; 60 import android.widget.GridView; 61 import android.widget.TextView; 62 import android.widget.Toast; 63 import android.widget.AdapterView.AdapterContextMenuInfo; 64 65 import java.util.ArrayList; 66 import java.util.HashMap; 67 import java.util.Map; 68 69 /** 70 * The GalleryPicker activity. 71 */ 72 public class GalleryPicker extends NoSearchActivity { 73 private static final String TAG = "GalleryPicker"; 74 75 Handler mHandler = new Handler(); // handler for the main thread 76 Thread mWorkerThread; 77 BroadcastReceiver mReceiver; 78 ContentObserver mDbObserver; 79 GridView mGridView; 80 GalleryPickerAdapter mAdapter; // mAdapter is only accessed in main thread. 81 boolean mScanning; 82 boolean mUnmounted; 83 84 @Override 85 public void onCreate(Bundle icicle) { 86 super.onCreate(icicle); 87 88 setContentView(R.layout.gallerypicker); 89 90 mGridView = (GridView) findViewById(R.id.albums); 91 92 mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 93 public void onItemClick(AdapterView<?> parent, View view, 94 int position, long id) { 95 launchFolderGallery(position); 96 } 97 }); 98 99 mGridView.setOnCreateContextMenuListener( 100 new View.OnCreateContextMenuListener() { 101 public void onCreateContextMenu(ContextMenu menu, View v, 102 final ContextMenuInfo menuInfo) { 103 onCreateGalleryPickerContextMenu(menu, menuInfo); 104 } 105 }); 106 107 mReceiver = new BroadcastReceiver() { 108 @Override 109 public void onReceive(Context context, Intent intent) { 110 onReceiveMediaBroadcast(intent); 111 } 112 }; 113 114 mDbObserver = new ContentObserver(mHandler) { 115 @Override 116 public void onChange(boolean selfChange) { 117 rebake(false, ImageManager.isMediaScannerScanning( 118 getContentResolver())); 119 } 120 }; 121 122 ImageManager.ensureOSXCompatibleFolder(); 123 } 124 125 Dialog mMediaScanningDialog; 126 127 // Display a dialog if the storage is being scanned now. 128 public void updateScanningDialog(boolean scanning) { 129 boolean prevScanning = (mMediaScanningDialog != null); 130 if (prevScanning == scanning && mAdapter.mItems.size() == 0) return; 131 // Now we are certain the state is changed. 132 if (prevScanning) { 133 mMediaScanningDialog.cancel(); 134 mMediaScanningDialog = null; 135 } else if (scanning && mAdapter.mItems.size() == 0) { 136 mMediaScanningDialog = ProgressDialog.show( 137 this, 138 null, 139 getResources().getString(R.string.wait), 140 true, 141 true); 142 } 143 } 144 145 private View mNoImagesView; 146 147 // Show/Hide the "no images" icon and text. Load resources on demand. 148 private void showNoImagesView() { 149 if (mNoImagesView == null) { 150 ViewGroup root = (ViewGroup) findViewById(R.id.root); 151 getLayoutInflater().inflate(R.layout.gallerypicker_no_images, root); 152 mNoImagesView = findViewById(R.id.no_images); 153 } 154 mNoImagesView.setVisibility(View.VISIBLE); 155 } 156 157 private void hideNoImagesView() { 158 if (mNoImagesView != null) { 159 mNoImagesView.setVisibility(View.GONE); 160 } 161 } 162 163 // The storage status is changed, restart the worker or show "no images". 164 private void rebake(boolean unmounted, boolean scanning) { 165 if (unmounted == mUnmounted && scanning == mScanning) return; 166 abortWorker(); 167 mUnmounted = unmounted; 168 mScanning = scanning; 169 updateScanningDialog(mScanning); 170 if (mUnmounted) { 171 showNoImagesView(); 172 } else { 173 hideNoImagesView(); 174 startWorker(); 175 } 176 } 177 178 // This is called when we receive media-related broadcast. 179 private void onReceiveMediaBroadcast(Intent intent) { 180 String action = intent.getAction(); 181 if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) { 182 // SD card available 183 // TODO put up a "please wait" message 184 } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) { 185 // SD card unavailable 186 rebake(true, false); 187 } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) { 188 rebake(false, true); 189 } else if (action.equals( 190 Intent.ACTION_MEDIA_SCANNER_FINISHED)) { 191 rebake(false, false); 192 } else if (action.equals(Intent.ACTION_MEDIA_EJECT)) { 193 rebake(true, false); 194 } 195 } 196 197 private void launchFolderGallery(int position) { 198 mAdapter.mItems.get(position).launch(this); 199 } 200 201 private void onCreateGalleryPickerContextMenu(ContextMenu menu, 202 final ContextMenuInfo menuInfo) { 203 int position = ((AdapterContextMenuInfo) menuInfo).position; 204 menu.setHeaderTitle(mAdapter.baseTitleForPosition(position)); 205 // "Slide Show" 206 if ((mAdapter.getIncludeMediaTypes(position) 207 & ImageManager.INCLUDE_IMAGES) != 0) { 208 menu.add(R.string.slide_show) 209 .setOnMenuItemClickListener(new OnMenuItemClickListener() { 210 public boolean onMenuItemClick(MenuItem item) { 211 return onSlideShowClicked(menuInfo); 212 } 213 }); 214 } 215 // "View" 216 menu.add(R.string.view) 217 .setOnMenuItemClickListener(new OnMenuItemClickListener() { 218 public boolean onMenuItemClick(MenuItem item) { 219 return onViewClicked(menuInfo); 220 } 221 }); 222 } 223 224 // This is called when the user clicks "Slideshow" from the context menu. 225 private boolean onSlideShowClicked(ContextMenuInfo menuInfo) { 226 AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; 227 int position = info.position; 228 229 if (position < 0 || position >= mAdapter.mItems.size()) { 230 return true; 231 } 232 // Slide show starts from the first image on the list. 233 Item item = mAdapter.mItems.get(position); 234 Uri targetUri = item.mFirstImageUri; 235 236 if (targetUri != null && item.mBucketId != null) { 237 targetUri = targetUri.buildUpon() 238 .appendQueryParameter("bucketId", item.mBucketId) 239 .build(); 240 } 241 Intent intent = new Intent(Intent.ACTION_VIEW, targetUri); 242 intent.putExtra("slideshow", true); 243 startActivity(intent); 244 return true; 245 } 246 247 // This is called when the user clicks "View" from the context menu. 248 private boolean onViewClicked(ContextMenuInfo menuInfo) { 249 AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; 250 launchFolderGallery(info.position); 251 return true; 252 } 253 254 @Override 255 public void onStop() { 256 super.onStop(); 257 258 abortWorker(); 259 260 unregisterReceiver(mReceiver); 261 getContentResolver().unregisterContentObserver(mDbObserver); 262 263 // free up some ram 264 mAdapter = null; 265 mGridView.setAdapter(null); 266 unloadDrawable(); 267 } 268 269 @Override 270 public void onStart() { 271 super.onStart(); 272 273 mAdapter = new GalleryPickerAdapter(getLayoutInflater()); 274 mGridView.setAdapter(mAdapter); 275 276 // install an intent filter to receive SD card related events. 277 IntentFilter intentFilter = new IntentFilter(); 278 intentFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); 279 intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); 280 intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); 281 intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED); 282 intentFilter.addAction(Intent.ACTION_MEDIA_EJECT); 283 intentFilter.addDataScheme("file"); 284 285 registerReceiver(mReceiver, intentFilter); 286 287 getContentResolver().registerContentObserver( 288 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 289 true, mDbObserver); 290 291 // Assume the storage is mounted and not scanning. 292 mUnmounted = false; 293 mScanning = false; 294 startWorker(); 295 } 296 297 // This is used to stop the worker thread. 298 volatile boolean mAbort = false; 299 300 // Create the worker thread. 301 private void startWorker() { 302 mAbort = false; 303 mWorkerThread = new Thread("GalleryPicker Worker") { 304 @Override 305 public void run() { 306 workerRun(); 307 } 308 }; 309 BitmapManager.instance().allowThreadDecoding(mWorkerThread); 310 mWorkerThread.start(); 311 } 312 313 private void abortWorker() { 314 if (mWorkerThread != null) { 315 BitmapManager.instance().cancelThreadDecoding(mWorkerThread, getContentResolver()); 316 mAbort = true; 317 try { 318 mWorkerThread.join(); 319 } catch (InterruptedException ex) { 320 Log.e(TAG, "join interrupted"); 321 } 322 mWorkerThread = null; 323 // Remove all runnables in mHandler. 324 // (We assume that the "what" field in the messages are 0 325 // for runnables). 326 mHandler.removeMessages(0); 327 mAdapter.clear(); 328 mAdapter.updateDisplay(); 329 clearImageLists(); 330 } 331 } 332 333 // This is run in the worker thread. 334 private void workerRun() { 335 // We collect items from checkImageList() and checkBucketIds() and 336 // put them in allItems. Later we give allItems to checkThumbBitmap() 337 // and generated thumbnail bitmaps for each item. We do this instead of 338 // generating thumbnail bitmaps in checkImageList() and checkBucketIds() 339 // because we want to show all the folders first, then update them with 340 // the thumb bitmaps. (Generating thumbnail bitmaps takes some time.) 341 ArrayList<Item> allItems = new ArrayList<Item>(); 342 343 checkScanning(); 344 if (mAbort) return; 345 346 checkImageList(allItems); 347 if (mAbort) return; 348 349 checkBucketIds(allItems); 350 if (mAbort) return; 351 352 checkThumbBitmap(allItems); 353 if (mAbort) return; 354 355 checkLowStorage(); 356 } 357 358 // This is run in the worker thread. 359 private void checkScanning() { 360 ContentResolver cr = getContentResolver(); 361 final boolean scanning = 362 ImageManager.isMediaScannerScanning(cr); 363 mHandler.post(new Runnable() { 364 public void run() { 365 checkScanningFinished(scanning); 366 } 367 }); 368 } 369 370 // This is run in the main thread. 371 private void checkScanningFinished(boolean scanning) { 372 updateScanningDialog(scanning); 373 } 374 375 // This is run in the worker thread. 376 private void checkImageList(ArrayList<Item> allItems) { 377 int length = IMAGE_LIST_DATA.length; 378 IImageList[] lists = new IImageList[length]; 379 for (int i = 0; i < length; i++) { 380 ImageListData data = IMAGE_LIST_DATA[i]; 381 lists[i] = createImageList(data.mInclude, data.mBucketId, 382 getContentResolver()); 383 if (mAbort) return; 384 Item item = null; 385 386 if (lists[i].isEmpty()) continue; 387 388 // i >= 3 means we are looking at All Images/All Videos. 389 // lists[i-3] is the corresponding Camera Images/Camera Videos. 390 // We want to add the "All" list only if it's different from 391 // the "Camera" list. 392 if (i >= 3 && lists[i].getCount() == lists[i - 3].getCount()) { 393 continue; 394 } 395 396 item = new Item(data.mType, 397 data.mBucketId, 398 getResources().getString(data.mStringId), 399 lists[i]); 400 401 allItems.add(item); 402 403 final Item finalItem = item; 404 mHandler.post(new Runnable() { 405 public void run() { 406 updateItem(finalItem); 407 } 408 }); 409 } 410 } 411 412 // This is run in the main thread. 413 private void updateItem(Item item) { 414 // Hide NoImageView if we are going to add the first item 415 if (mAdapter.getCount() == 0) { 416 hideNoImagesView(); 417 } 418 mAdapter.addItem(item); 419 mAdapter.updateDisplay(); 420 } 421 422 private static final String CAMERA_BUCKET = 423 ImageManager.CAMERA_IMAGE_BUCKET_ID; 424 425 // This is run in the worker thread. 426 private void checkBucketIds(ArrayList<Item> allItems) { 427 final IImageList allImages; 428 if (!mScanning && !mUnmounted) { 429 allImages = ImageManager.makeImageList( 430 getContentResolver(), 431 ImageManager.DataLocation.ALL, 432 ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS, 433 ImageManager.SORT_DESCENDING, 434 null); 435 } else { 436 allImages = ImageManager.makeEmptyImageList(); 437 } 438 439 if (mAbort) { 440 allImages.close(); 441 return; 442 } 443 444 HashMap<String, String> hashMap = allImages.getBucketIds(); 445 allImages.close(); 446 if (mAbort) return; 447 448 for (Map.Entry<String, String> entry : hashMap.entrySet()) { 449 String key = entry.getKey(); 450 if (key == null) { 451 continue; 452 } 453 if (!key.equals(CAMERA_BUCKET)) { 454 IImageList list = createImageList( 455 ImageManager.INCLUDE_IMAGES 456 | ImageManager.INCLUDE_VIDEOS, key, 457 getContentResolver()); 458 if (mAbort) return; 459 460 Item item = new Item(Item.TYPE_NORMAL_FOLDERS, key, 461 entry.getValue(), list); 462 463 allItems.add(item); 464 465 final Item finalItem = item; 466 mHandler.post(new Runnable() { 467 public void run() { 468 updateItem(finalItem); 469 } 470 }); 471 } 472 } 473 474 mHandler.post(new Runnable() { 475 public void run() { 476 checkBucketIdsFinished(); 477 } 478 }); 479 } 480 481 // This is run in the main thread. 482 private void checkBucketIdsFinished() { 483 484 // If we just have one folder, open it. 485 // If we have zero folder, show the "no images" icon. 486 if (!mScanning) { 487 int numItems = mAdapter.mItems.size(); 488 if (numItems == 0) { 489 showNoImagesView(); 490 } else if (numItems == 1) { 491 mAdapter.mItems.get(0).launch(this); 492 finish(); 493 return; 494 } 495 } 496 } 497 498 private static final int THUMB_SIZE = 142; 499 // This is run in the worker thread. 500 private void checkThumbBitmap(ArrayList<Item> allItems) { 501 for (Item item : allItems) { 502 final Bitmap b = makeMiniThumbBitmap(THUMB_SIZE, THUMB_SIZE, 503 item.mImageList); 504 if (mAbort) { 505 if (b != null) b.recycle(); 506 return; 507 } 508 509 final Item finalItem = item; 510 mHandler.post(new Runnable() { 511 public void run() { 512 updateThumbBitmap(finalItem, b); 513 } 514 }); 515 } 516 } 517 518 // This is run in the main thread. 519 private void updateThumbBitmap(Item item, Bitmap b) { 520 item.setThumbBitmap(b); 521 mAdapter.updateDisplay(); 522 } 523 524 private static final long LOW_STORAGE_THRESHOLD = 1024 * 1024 * 2; 525 526 // This is run in the worker thread. 527 private void checkLowStorage() { 528 // Check available space only if we are writable 529 if (ImageManager.hasStorage()) { 530 String storageDirectory = Environment 531 .getExternalStorageDirectory().toString(); 532 StatFs stat = new StatFs(storageDirectory); 533 long remaining = (long) stat.getAvailableBlocks() 534 * (long) stat.getBlockSize(); 535 if (remaining < LOW_STORAGE_THRESHOLD) { 536 mHandler.post(new Runnable() { 537 public void run() { 538 checkLowStorageFinished(); 539 } 540 }); 541 } 542 } 543 } 544 545 // This is run in the main thread. 546 // This is called only if the storage is low. 547 private void checkLowStorageFinished() { 548 Toast.makeText(GalleryPicker.this, R.string.not_enough_space, 5000) 549 .show(); 550 } 551 552 // IMAGE_LIST_DATA stores the parameters for the four image lists 553 // we are interested in. The order of the IMAGE_LIST_DATA array is 554 // significant (See the implementation of GalleryPickerAdapter.init). 555 private static final class ImageListData { 556 ImageListData(int type, int include, String bucketId, int stringId) { 557 mType = type; 558 mInclude = include; 559 mBucketId = bucketId; 560 mStringId = stringId; 561 } 562 int mType; 563 int mInclude; 564 String mBucketId; 565 int mStringId; 566 } 567 568 private static final ImageListData[] IMAGE_LIST_DATA = { 569 // Camera Images 570 new ImageListData(Item.TYPE_CAMERA_IMAGES, 571 ImageManager.INCLUDE_IMAGES, 572 ImageManager.CAMERA_IMAGE_BUCKET_ID, 573 R.string.gallery_camera_bucket_name), 574 // Camera Videos 575 new ImageListData(Item.TYPE_CAMERA_VIDEOS, 576 ImageManager.INCLUDE_VIDEOS, 577 ImageManager.CAMERA_IMAGE_BUCKET_ID, 578 R.string.gallery_camera_videos_bucket_name), 579 580 // Camera Medias 581 new ImageListData(Item.TYPE_CAMERA_MEDIAS, 582 ImageManager.INCLUDE_VIDEOS | ImageManager.INCLUDE_IMAGES, 583 ImageManager.CAMERA_IMAGE_BUCKET_ID, 584 R.string.gallery_camera_media_bucket_name), 585 586 // All Images 587 new ImageListData(Item.TYPE_ALL_IMAGES, 588 ImageManager.INCLUDE_IMAGES, 589 null, 590 R.string.all_images), 591 592 // All Videos 593 new ImageListData(Item.TYPE_ALL_VIDEOS, 594 ImageManager.INCLUDE_VIDEOS, 595 null, 596 R.string.all_videos), 597 }; 598 599 600 // These drawables are loaded on-demand. 601 Drawable mFrameGalleryMask; 602 Drawable mCellOutline; 603 Drawable mVideoOverlay; 604 605 private void loadDrawableIfNeeded() { 606 if (mFrameGalleryMask != null) return; // already loaded 607 Resources r = getResources(); 608 mFrameGalleryMask = r.getDrawable( 609 R.drawable.frame_gallery_preview_album_mask); 610 mCellOutline = r.getDrawable(android.R.drawable.gallery_thumb); 611 mVideoOverlay = r.getDrawable(R.drawable.ic_gallery_video_overlay); 612 } 613 614 private void unloadDrawable() { 615 mFrameGalleryMask = null; 616 mCellOutline = null; 617 mVideoOverlay = null; 618 } 619 620 private static void placeImage(Bitmap image, Canvas c, Paint paint, 621 int imageWidth, int widthPadding, int imageHeight, 622 int heightPadding, int offsetX, int offsetY, 623 int pos) { 624 int row = pos / 2; 625 int col = pos - (row * 2); 626 627 int xPos = (col * (imageWidth + widthPadding)) - offsetX; 628 int yPos = (row * (imageHeight + heightPadding)) - offsetY; 629 630 c.drawBitmap(image, xPos, yPos, paint); 631 } 632 633 // This is run in worker thread. 634 private Bitmap makeMiniThumbBitmap(int width, int height, 635 IImageList images) { 636 int count = images.getCount(); 637 // We draw three different version of the folder image depending on the 638 // number of images in the folder. 639 // For a single image, that image draws over the whole folder. 640 // For two or three images, we draw the two most recent photos. 641 // For four or more images, we draw four photos. 642 final int padding = 4; 643 int imageWidth = width; 644 int imageHeight = height; 645 int offsetWidth = 0; 646 int offsetHeight = 0; 647 648 imageWidth = (imageWidth - padding) / 2; // 2 here because we show two 649 // images 650 imageHeight = (imageHeight - padding) / 2; // per row and column 651 652 final Paint p = new Paint(); 653 final Bitmap b = Bitmap.createBitmap(width, height, 654 Bitmap.Config.ARGB_8888); 655 final Canvas c = new Canvas(b); 656 final Matrix m = new Matrix(); 657 658 // draw the whole canvas as transparent 659 p.setColor(0x00000000); 660 c.drawPaint(p); 661 662 // load the drawables 663 loadDrawableIfNeeded(); 664 665 // draw the mask normally 666 p.setColor(0xFFFFFFFF); 667 mFrameGalleryMask.setBounds(0, 0, width, height); 668 mFrameGalleryMask.draw(c); 669 670 Paint pdpaint = new Paint(); 671 pdpaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 672 673 pdpaint.setStyle(Paint.Style.FILL); 674 c.drawRect(0, 0, width, height, pdpaint); 675 676 for (int i = 0; i < 4; i++) { 677 if (mAbort) { 678 return null; 679 } 680 681 Bitmap temp = null; 682 IImage image = i < count ? images.getImageAt(i) : null; 683 684 if (image != null) { 685 temp = image.miniThumbBitmap(); 686 } 687 688 if (temp != null) { 689 if (ImageManager.isVideo(image)) { 690 Bitmap newMap = temp.copy(temp.getConfig(), true); 691 Canvas overlayCanvas = new Canvas(newMap); 692 int overlayWidth = mVideoOverlay.getIntrinsicWidth(); 693 int overlayHeight = mVideoOverlay.getIntrinsicHeight(); 694 int left = (newMap.getWidth() - overlayWidth) / 2; 695 int top = (newMap.getHeight() - overlayHeight) / 2; 696 Rect newBounds = new Rect(left, top, left + overlayWidth, 697 top + overlayHeight); 698 mVideoOverlay.setBounds(newBounds); 699 mVideoOverlay.draw(overlayCanvas); 700 temp.recycle(); 701 temp = newMap; 702 } 703 704 temp = Util.transform(m, temp, imageWidth, 705 imageHeight, true, Util.RECYCLE_INPUT); 706 } 707 708 Bitmap thumb = Bitmap.createBitmap(imageWidth, imageHeight, 709 Bitmap.Config.ARGB_8888); 710 Canvas tempCanvas = new Canvas(thumb); 711 if (temp != null) { 712 tempCanvas.drawBitmap(temp, new Matrix(), new Paint()); 713 } 714 mCellOutline.setBounds(0, 0, imageWidth, imageHeight); 715 mCellOutline.draw(tempCanvas); 716 717 placeImage(thumb, c, pdpaint, imageWidth, padding, imageHeight, 718 padding, offsetWidth, offsetHeight, i); 719 720 thumb.recycle(); 721 722 if (temp != null) { 723 temp.recycle(); 724 } 725 } 726 727 return b; 728 } 729 730 @Override 731 public boolean onCreateOptionsMenu(Menu menu) { 732 super.onCreateOptionsMenu(menu); 733 734 MenuHelper.addCaptureMenuItems(menu, this); 735 736 menu.add(Menu.NONE, Menu.NONE, MenuHelper.POSITION_GALLERY_SETTING, 737 R.string.camerasettings) 738 .setOnMenuItemClickListener(new OnMenuItemClickListener() { 739 public boolean onMenuItemClick(MenuItem item) { 740 Intent preferences = new Intent(); 741 preferences.setClass(GalleryPicker.this, 742 GallerySettings.class); 743 startActivity(preferences); 744 return true; 745 } 746 }) 747 .setAlphabeticShortcut('p') 748 .setIcon(android.R.drawable.ic_menu_preferences); 749 750 return true; 751 } 752 753 // image lists created by createImageList() are collected in mAllLists. 754 // They will be closed in clearImageList, so they don't hold open files 755 // on SD card. We will be killed if we don't close files when the SD card 756 // is unmounted. 757 ArrayList<IImageList> mAllLists = new ArrayList<IImageList>(); 758 759 private IImageList createImageList(int mediaTypes, String bucketId, 760 ContentResolver cr) { 761 IImageList list = ImageManager.makeImageList( 762 cr, 763 ImageManager.DataLocation.ALL, 764 mediaTypes, 765 ImageManager.SORT_DESCENDING, 766 bucketId); 767 mAllLists.add(list); 768 return list; 769 } 770 771 private void clearImageLists() { 772 for (IImageList list : mAllLists) { 773 list.close(); 774 } 775 mAllLists.clear(); 776 } 777 } 778 779 // Item is the underlying data for GalleryPickerAdapter. 780 // It is passed from the activity to the adapter. 781 class Item { 782 public static final int TYPE_NONE = -1; 783 public static final int TYPE_ALL_IMAGES = 0; 784 public static final int TYPE_ALL_VIDEOS = 1; 785 public static final int TYPE_CAMERA_IMAGES = 2; 786 public static final int TYPE_CAMERA_VIDEOS = 3; 787 public static final int TYPE_CAMERA_MEDIAS = 4; 788 public static final int TYPE_NORMAL_FOLDERS = 5; 789 790 public final int mType; 791 public final String mBucketId; 792 public final String mName; 793 public final IImageList mImageList; 794 public final int mCount; 795 public final Uri mFirstImageUri; // could be null if the list is empty 796 797 // The thumbnail bitmap is set by setThumbBitmap() later because we want 798 // to let the user sees the folder icon as soon as possible (and possibly 799 // select them), then present more detailed information when we have it. 800 public Bitmap mThumbBitmap; // the thumbnail bitmap for the image list 801 802 public Item(int type, String bucketId, String name, IImageList list) { 803 mType = type; 804 mBucketId = bucketId; 805 mName = name; 806 mImageList = list; 807 mCount = list.getCount(); 808 if (mCount > 0) { 809 mFirstImageUri = list.getImageAt(0).fullSizeImageUri(); 810 } else { 811 mFirstImageUri = null; 812 } 813 } 814 815 public void setThumbBitmap(Bitmap thumbBitmap) { 816 mThumbBitmap = thumbBitmap; 817 } 818 819 public boolean needsBucketId() { 820 return mType >= TYPE_CAMERA_IMAGES; 821 } 822 823 public void launch(Activity activity) { 824 Uri uri = Images.Media.INTERNAL_CONTENT_URI; 825 if (needsBucketId()) { 826 uri = uri.buildUpon() 827 .appendQueryParameter("bucketId", mBucketId).build(); 828 } 829 Intent intent = new Intent(Intent.ACTION_VIEW, uri); 830 intent.putExtra("windowTitle", mName); 831 intent.putExtra("mediaTypes", getIncludeMediaTypes()); 832 activity.startActivity(intent); 833 } 834 835 public int getIncludeMediaTypes() { 836 return convertItemTypeToIncludedMediaType(mType); 837 } 838 839 public static int convertItemTypeToIncludedMediaType(int itemType) { 840 switch (itemType) { 841 case TYPE_ALL_IMAGES: 842 case TYPE_CAMERA_IMAGES: 843 return ImageManager.INCLUDE_IMAGES; 844 case TYPE_ALL_VIDEOS: 845 case TYPE_CAMERA_VIDEOS: 846 return ImageManager.INCLUDE_VIDEOS; 847 case TYPE_NORMAL_FOLDERS: 848 case TYPE_CAMERA_MEDIAS: 849 default: 850 return ImageManager.INCLUDE_IMAGES 851 | ImageManager.INCLUDE_VIDEOS; 852 } 853 } 854 855 public int getOverlay() { 856 switch (mType) { 857 case TYPE_ALL_IMAGES: 858 case TYPE_CAMERA_IMAGES: 859 return R.drawable.frame_overlay_gallery_camera; 860 case TYPE_ALL_VIDEOS: 861 case TYPE_CAMERA_VIDEOS: 862 case TYPE_CAMERA_MEDIAS: 863 return R.drawable.frame_overlay_gallery_video; 864 case TYPE_NORMAL_FOLDERS: 865 default: 866 return R.drawable.frame_overlay_gallery_folder; 867 } 868 } 869 } 870 871 class GalleryPickerAdapter extends BaseAdapter { 872 ArrayList<Item> mItems = new ArrayList<Item>(); 873 LayoutInflater mInflater; 874 875 GalleryPickerAdapter(LayoutInflater inflater) { 876 mInflater = inflater; 877 } 878 879 public void addItem(Item item) { 880 mItems.add(item); 881 } 882 883 public void updateDisplay() { 884 notifyDataSetChanged(); 885 } 886 887 public void clear() { 888 mItems.clear(); 889 } 890 891 public int getCount() { 892 return mItems.size(); 893 } 894 895 public Object getItem(int position) { 896 return null; 897 } 898 899 public long getItemId(int position) { 900 return position; 901 } 902 903 public String baseTitleForPosition(int position) { 904 return mItems.get(position).mName; 905 } 906 907 public int getIncludeMediaTypes(int position) { 908 return mItems.get(position).getIncludeMediaTypes(); 909 } 910 911 public View getView(final int position, View convertView, 912 ViewGroup parent) { 913 View v; 914 915 if (convertView == null) { 916 v = mInflater.inflate(R.layout.gallery_picker_item, null); 917 } else { 918 v = convertView; 919 } 920 921 TextView titleView = (TextView) v.findViewById(R.id.title); 922 923 GalleryPickerItem iv = 924 (GalleryPickerItem) v.findViewById(R.id.thumbnail); 925 Item item = mItems.get(position); 926 iv.setOverlay(item.getOverlay()); 927 if (item.mThumbBitmap != null) { 928 iv.setImageBitmap(item.mThumbBitmap); 929 String title = item.mName + " (" + item.mCount + ")"; 930 titleView.setText(title); 931 } else { 932 iv.setImageResource(android.R.color.transparent); 933 titleView.setText(item.mName); 934 } 935 936 // An workaround due to a bug in TextView. If the length of text is 937 // different from the previous in convertView, the layout would be 938 // wrong. 939 titleView.requestLayout(); 940 941 return v; 942 } 943 } 944