diff options
33 files changed, 4818 insertions, 3967 deletions
diff --git a/src/com/android/camera/Camera.java b/src/com/android/camera/Camera.java index 85a0884..1873183 100644 --- a/src/com/android/camera/Camera.java +++ b/src/com/android/camera/Camera.java @@ -16,6 +16,10 @@ package com.android.camera; +import com.android.camera.gallery.IAddImageCancelable; +import com.android.camera.gallery.IImage; +import com.android.camera.gallery.IImageList; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -396,7 +400,7 @@ public class Camera extends Activity implements View.OnClickListener, private boolean mCapturing = false; private Uri mLastContentUri; - private ImageManager.IAddImage_cancelable mAddImageCancelable; + private IAddImageCancelable mAddImageCancelable; Bitmap mCaptureOnlyBitmap; @@ -1218,7 +1222,7 @@ public class Camera extends Activity implements View.OnClickListener, } private void updateLastImage() { - ImageManager.IImageList list = ImageManager.instance().allImages( + IImageList list = ImageManager.instance().allImages( this, mContentResolver, dataLocation(), @@ -1227,7 +1231,7 @@ public class Camera extends Activity implements View.OnClickListener, ImageManager.CAMERA_IMAGE_BUCKET_ID); int count = list.getCount(); if (count > 0) { - ImageManager.IImage image = list.getImageAt(count-1); + IImage image = list.getImageAt(count-1); Uri uri = image.fullSizeImageUri(); mThumbController.setData(uri, image.miniThumbBitmap()); } else { @@ -1438,14 +1442,14 @@ public class Camera extends Activity implements View.OnClickListener, } }; - private ImageManager.IImage getImageForURI(Uri uri) { - ImageManager.IImageList list = ImageManager.instance().allImages( + private IImage getImageForURI(Uri uri) { + IImageList list = ImageManager.instance().allImages( this, mContentResolver, dataLocation(), ImageManager.INCLUDE_IMAGES, ImageManager.SORT_ASCENDING); - ImageManager.IImage image = list.getImageForUri(uri); + IImage image = list.getImageForUri(uri); list.deactivate(); return image; } @@ -1591,7 +1595,7 @@ public class Camera extends Activity implements View.OnClickListener, SelectedImageGetter mSelectedImageGetter = new SelectedImageGetter() { - public ImageManager.IImage getCurrentImage() { + public IImage getCurrentImage() { return getImageForURI(getCurrentImageUri()); } public Uri getCurrentImageUri() { diff --git a/src/com/android/camera/CropImage.java b/src/com/android/camera/CropImage.java index cefaf83..514f7be 100644 --- a/src/com/android/camera/CropImage.java +++ b/src/com/android/camera/CropImage.java @@ -16,6 +16,10 @@ package com.android.camera; +import com.android.camera.gallery.IAddImageCancelable; +import com.android.camera.gallery.IImage; +import com.android.camera.gallery.IImageList; + import android.app.Activity; import android.app.ProgressDialog; import android.content.ContentResolver; @@ -54,7 +58,7 @@ public class CropImage extends Activity { private static final String TAG = "CropImage"; private ProgressDialog mFaceDetectionDialog = null; private ProgressDialog mSavingProgressDialog = null; - private ImageManager.IImageList mAllImages; + private IImageList mAllImages; private Bitmap.CompressFormat mSaveFormat = Bitmap.CompressFormat.JPEG; // only used with mSaveUri private Uri mSaveUri = null; private int mAspectX, mAspectY; @@ -73,7 +77,7 @@ public class CropImage extends Activity { Bitmap mCroppedImage; HighlightView mCrop; - ImageManager.IImage mImage; + IImage mImage; public CropImage() { } @@ -606,7 +610,7 @@ public class CropImage extends Activity { directory.toString(), fileName + "-" + x + ".jpg"); - ImageManager.IAddImage_cancelable cancelable = ImageManager.instance().storeImage( + IAddImageCancelable cancelable = ImageManager.instance().storeImage( newUri, CropImage.this, getContentResolver(), diff --git a/src/com/android/camera/ExifInterface.java b/src/com/android/camera/ExifInterface.java index fe6c6c8..4fb4e0e 100644 --- a/src/com/android/camera/ExifInterface.java +++ b/src/com/android/camera/ExifInterface.java @@ -26,32 +26,31 @@ public class ExifInterface { private String mFilename; // Constants used for the Orientation Exif tag. - static final int ORIENTATION_UNDEFINED = 0; - - static final int ORIENTATION_NORMAL = 1; + public static final int ORIENTATION_UNDEFINED = 0; + public static final int ORIENTATION_NORMAL = 1; // left right reversed mirror - static final int ORIENTATION_FLIP_HORIZONTAL = 2; - - static final int ORIENTATION_ROTATE_180 = 3; + public static final int ORIENTATION_FLIP_HORIZONTAL = 2; + public static final int ORIENTATION_ROTATE_180 = 3; // upside down mirror - static final int ORIENTATION_FLIP_VERTICAL = 4; + public static final int ORIENTATION_FLIP_VERTICAL = 4; // flipped about top-left <--> bottom-right axis - static final int ORIENTATION_TRANSPOSE = 5; + public static final int ORIENTATION_TRANSPOSE = 5; // rotate 90 cw to right it - static final int ORIENTATION_ROTATE_90 = 6; + public static final int ORIENTATION_ROTATE_90 = 6; // flipped about top-right <--> bottom-left axis - static final int ORIENTATION_TRANSVERSE = 7; + public static final int ORIENTATION_TRANSVERSE = 7; // rotate 270 to right it - static final int ORIENTATION_ROTATE_270 = 8; + public static final int ORIENTATION_ROTATE_270 = 8; // The Exif tag names - static final String TAG_ORIENTATION = "Orientation"; + public static final String TAG_ORIENTATION = "Orientation"; + static final String TAG_DATE_TIME_ORIGINAL = "DateTimeOriginal"; static final String TAG_MAKE = "Make"; static final String TAG_MODEL = "Model"; diff --git a/src/com/android/camera/GalleryPicker.java b/src/com/android/camera/GalleryPicker.java index 9c687c8..60075b3 100644 --- a/src/com/android/camera/GalleryPicker.java +++ b/src/com/android/camera/GalleryPicker.java @@ -16,6 +16,9 @@ package com.android.camera; +import com.android.camera.gallery.IImage; +import com.android.camera.gallery.IImageList; + import android.app.Activity; import android.app.Dialog; import android.app.ProgressDialog; @@ -332,7 +335,7 @@ public class GalleryPicker extends Activity { public void init(boolean assumeMounted) { mItems.clear(); - ImageManager.IImageList images; + IImageList images; if (assumeMounted) { images = ImageManager.instance().allImages( GalleryPicker.this, @@ -397,7 +400,7 @@ public class GalleryPicker extends Activity { for (int i = 0; i < mItems.size() && !mDone; i++) { final Item item = mItems.get(i); - ImageManager.IImageList list = createImageList( + IImageList list = createImageList( item.getIncludeMediaTypes(), item.mId); try { if (mPausing) { @@ -586,7 +589,7 @@ public class GalleryPicker extends Activity { c.drawBitmap(image, xPos, yPos, paint); } - private Bitmap makeMiniThumbBitmap(int width, int height, ImageManager.IImageList images) { + private Bitmap makeMiniThumbBitmap(int width, int height, IImageList images) { int count = images.getCount(); // We draw three different version of the folder image depending on the number of images in the folder. // For a single image, that image draws over the whole folder. @@ -629,7 +632,7 @@ public class GalleryPicker extends Activity { } Bitmap temp = null; - ImageManager.IImage image = i < count ? images.getImageAt(i) : null; + IImage image = i < count ? images.getImageAt(i) : null; if (image != null) { temp = image.miniThumbBitmap(); @@ -697,7 +700,7 @@ public class GalleryPicker extends Activity { private boolean isEmptyBucket(int mediaTypes, String bucketId) { // TODO: Find a more efficient way of calculating this - ImageManager.IImageList list = createImageList(mediaTypes, bucketId); + IImageList list = createImageList(mediaTypes, bucketId); try { return list.isEmpty(); } @@ -708,7 +711,7 @@ public class GalleryPicker extends Activity { private int bucketItemCount(int mediaTypes, String bucketId) { // TODO: Find a more efficient way of calculating this - ImageManager.IImageList list = createImageList(mediaTypes, bucketId); + IImageList list = createImageList(mediaTypes, bucketId); try { return list.getCount(); } @@ -716,7 +719,7 @@ public class GalleryPicker extends Activity { list.deactivate(); } } - private ImageManager.IImageList createImageList(int mediaTypes, String bucketId) { + private IImageList createImageList(int mediaTypes, String bucketId) { return ImageManager.instance().allImages( this, getContentResolver(), diff --git a/src/com/android/camera/ImageGallery2.java b/src/com/android/camera/ImageGallery2.java index 8f9456b..b9518b9 100644 --- a/src/com/android/camera/ImageGallery2.java +++ b/src/com/android/camera/ImageGallery2.java @@ -60,11 +60,13 @@ import android.widget.Scroller; import java.util.Calendar; import java.util.GregorianCalendar; -import com.android.camera.ImageManager.IImage; +import com.android.camera.gallery.IImage; +import com.android.camera.gallery.IImageList; +import com.android.camera.gallery.VideoObject; public class ImageGallery2 extends Activity { private static final String TAG = "ImageGallery2"; - private ImageManager.IImageList mAllImages; + private IImageList mAllImages; private int mInclusion; private boolean mSortAscending = false; private View mNoImagesView; @@ -167,7 +169,7 @@ public class ImageGallery2 extends Activity { return menu.add(0, 207, position, R.string.slide_show) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { - ImageManager.IImage img = mSelectedImageGetter.getCurrentImage(); + IImage img = mSelectedImageGetter.getCurrentImage(); if (img == null) { img = mAllImages.getImageAt(0); if (img == null) { @@ -207,13 +209,13 @@ public class ImageGallery2 extends Activity { private SelectedImageGetter mSelectedImageGetter = new SelectedImageGetter() { public Uri getCurrentImageUri() { - ImageManager.IImage image = getCurrentImage(); + IImage image = getCurrentImage(); if (image != null) return image.fullSizeImageUri(); else return null; } - public ImageManager.IImage getCurrentImage() { + public IImage getCurrentImage() { int currentSelection = mGvs.mCurrentSelection; if (currentSelection < 0 || currentSelection >= mAllImages.getCount()) return null; @@ -338,7 +340,7 @@ public class ImageGallery2 extends Activity { return (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action)); } - private void launchCropperOrFinish(ImageManager.IImage img) { + private void launchCropperOrFinish(IImage img) { Bundle myExtras = getIntent().getExtras(); long size = MenuHelper.getImageFileSize(img); @@ -421,7 +423,7 @@ public class ImageGallery2 extends Activity { case VIEW_MSG: { if (Config.LOGV) Log.v(TAG, "got VIEW_MSG with " + data); - ImageManager.IImage img = mAllImages.getImageForUri(data.getData()); + IImage img = mAllImages.getImageForUri(data.getData()); launchCropperOrFinish(img); break; } @@ -570,7 +572,7 @@ public class ImageGallery2 extends Activity { pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "ImageGallery2.checkThumbnails"); mWakeLock.acquire(); - ImageManager.IImageList.ThumbCheckCallback r = new ImageManager.IImageList.ThumbCheckCallback() { + IImageList.ThumbCheckCallback r = new IImageList.ThumbCheckCallback() { boolean mDidSetProgress = false; public boolean checking(final int count, final int maxCount) { @@ -604,7 +606,7 @@ public class ImageGallery2 extends Activity { return !mPausing; } }; - ImageManager.IImageList imageList = allImages(true); + IImageList imageList = allImages(true); imageList.checkThumbnails(r, imageList.getCount()); mWakeLock.release(); mThumbnailCheckThread = null; @@ -623,7 +625,7 @@ public class ImageGallery2 extends Activity { mThumbnailCheckThread.start(); mThumbnailCheckThread.toBackground(); - ImageManager.IImageList list = allImages(true); + IImageList list = allImages(true); mNoImagesView.setVisibility(list.getCount() > 0 ? View.GONE : View.VISIBLE); } @@ -677,7 +679,7 @@ public class ImageGallery2 extends Activity { return (image != null) && ImageManager.isVideo(image); } - private synchronized ImageManager.IImageList allImages(boolean assumeMounted) { + private synchronized IImageList allImages(boolean assumeMounted) { if (mAllImages == null) { mNoImagesView = findViewById(R.id.no_images); @@ -1111,7 +1113,7 @@ public class ImageGallery2 extends Activity { } // Create this bitmap lazily, and only once for all the ImageBlocks to use - public Bitmap getErrorBitmap(ImageManager.IImage image) { + public Bitmap getErrorBitmap(IImage image) { if (ImageManager.isImage(image)) { if (mMissingImageThumbnailBitmap == null) { mMissingImageThumbnailBitmap = BitmapFactory.decodeResource(GridViewSpecial.this.getResources(), @@ -1495,7 +1497,7 @@ public class ImageGallery2 extends Activity { if (pos >= count) break; - ImageManager.IImage image = mGallery.mAllImages.getImageAt(pos); + IImage image = mGallery.mAllImages.getImageAt(pos); if (image != null) { // Log.v(TAG, "calling loadImage " + (base + col)); loadImage(base, col, image, xPos, yPos); @@ -1519,7 +1521,7 @@ public class ImageGallery2 extends Activity { return b2; } - private void drawBitmap(ImageManager.IImage image, int base, int baseOffset, Bitmap b, int xPos, int yPos) { + private void drawBitmap(IImage image, int base, int baseOffset, Bitmap b, int xPos, int yPos) { mCanvas.setBitmap(mBitmap); if (b != null) { // if the image is close to the target size then crop, otherwise scale @@ -1636,7 +1638,7 @@ public class ImageGallery2 extends Activity { private void loadImage( final int base, final int baseOffset, - final ImageManager.IImage image, + final IImage image, final int xPos, final int yPos) { synchronized (ImageBlock.this) { @@ -1755,7 +1757,7 @@ public class ImageGallery2 extends Activity { private void onSelect(int index) { if (index >= 0 && index < mGallery.mAllImages.getCount()) { - ImageManager.IImage img = mGallery.mAllImages.getImageAt(index); + IImage img = mGallery.mAllImages.getImageAt(index); if (img == null) return; @@ -1772,7 +1774,7 @@ public class ImageGallery2 extends Activity { } Intent intent = new Intent(Intent.ACTION_VIEW, targetUri); - if (img instanceof ImageManager.VideoObject) { + if (img instanceof VideoObject) { intent.putExtra(MediaStore.EXTRA_SCREEN_ORIENTATION, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } @@ -1826,9 +1828,9 @@ public class ImageGallery2 extends Activity { GridViewSpecial.this.mImageBlockManager.getVisibleRange(mDateRange); - ImageManager.IImage firstImage = mGallery.mAllImages.getImageAt(mDateRange[0]); + IImage firstImage = mGallery.mAllImages.getImageAt(mDateRange[0]); int lastOffset = Math.min(count-1, mDateRange[1]); - ImageManager.IImage lastImage = mGallery.mAllImages.getImageAt(lastOffset); + IImage lastImage = mGallery.mAllImages.getImageAt(lastOffset); GregorianCalendar dateStart = new GregorianCalendar(); GregorianCalendar dateEnd = new GregorianCalendar(); diff --git a/src/com/android/camera/ImageLoader.java b/src/com/android/camera/ImageLoader.java index 8c2a68e..36c9f88 100644 --- a/src/com/android/camera/ImageLoader.java +++ b/src/com/android/camera/ImageLoader.java @@ -16,7 +16,7 @@ package com.android.camera; -import com.android.camera.ImageManager.IImage; +import com.android.camera.gallery.IImage; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -28,7 +28,7 @@ import android.util.Log; import java.util.ArrayList; -class ImageLoader { +public class ImageLoader { private static final String TAG = "ImageLoader"; // queue of work to do in the worker thread @@ -84,17 +84,17 @@ class ImageLoader { } } - public Bitmap getBitmap(IImage image, - LoadedCallback imageLoadedRunnable, + public Bitmap getBitmap(IImage image, + LoadedCallback imageLoadedRunnable, boolean postAtFront, boolean postBack) { return getBitmap(image, 0, imageLoadedRunnable, postAtFront, postBack); } - public Bitmap getBitmap(IImage image, - int tag, - LoadedCallback imageLoadedRunnable, - boolean postAtFront, + public Bitmap getBitmap(IImage image, + int tag, + LoadedCallback imageLoadedRunnable, + boolean postAtFront, boolean postBack) { synchronized (mDecodeThreads) { if (mDecodeThreads.size() == 0) { @@ -105,7 +105,7 @@ class ImageLoader { long t2, t3, t4; synchronized (mQueue) { t2 = System.currentTimeMillis(); - WorkItem w = + WorkItem w = new WorkItem(image, tag, imageLoadedRunnable, postBack); if (!mInProgress.contains(w)) { @@ -131,7 +131,7 @@ class ImageLoader { t3 = System.currentTimeMillis(); } t4 = System.currentTimeMillis(); - // Log.v(TAG, "getBitmap breakdown: tot= " + (t4-t1) + "; " + "; " + + // Log.v(TAG, "getBitmap breakdown: tot= " + (t4-t1) + "; " + "; " + // (t4-t3) + "; " + (t3-t2) + "; " + (t2-t1)); return null; } @@ -160,7 +160,7 @@ class ImageLoader { LoadedCallback mOnLoadedRunnable; boolean mPostBack; - WorkItem(IImage image, int tag, LoadedCallback onLoadedRunnable, + WorkItem(IImage image, int tag, LoadedCallback onLoadedRunnable, boolean postBack) { mImage = image; mTag = tag; @@ -168,11 +168,13 @@ class ImageLoader { mPostBack = postBack; } + @Override public boolean equals(Object other) { WorkItem otherWorkItem = (WorkItem) other; return otherWorkItem.mImage == mImage; } + @Override public int hashCode() { return mImage.fullSizeImageUri().hashCode(); } @@ -197,9 +199,9 @@ class ImageLoader { mDone = false; for (int i = 0; i < mThreadCount; i++) { Thread t = new Thread(new Runnable() { - // pick off items on the queue, one by one, and compute - // their bitmap. place the resulting bitmap in the cache. - // then post a notification back to the ui so things can + // pick off items on the queue, one by one, and compute + // their bitmap. place the resulting bitmap in the cache. + // then post a notification back to the ui so things can // get updated appropriately. public void run() { while (!mDone) { @@ -258,21 +260,21 @@ class ImageLoader { } } - public static Bitmap transform(Matrix scaler, - Bitmap source, - int targetWidth, - int targetHeight, + public static Bitmap transform(Matrix scaler, + Bitmap source, + int targetWidth, + int targetHeight, boolean scaleUp) { int deltaX = source.getWidth() - targetWidth; int deltaY = source.getHeight() - targetHeight; if (!scaleUp && (deltaX < 0 || deltaY < 0)) { /* - * In this case the bitmap is smaller, at least in one dimension, - * than the target. Transform it by placing as much of the image - * as possible into the target and leaving the top/bottom or + * In this case the bitmap is smaller, at least in one dimension, + * than the target. Transform it by placing as much of the image + * as possible into the target and leaving the top/bottom or * left/right (or both) black. */ - Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight, + Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b2); @@ -346,7 +348,7 @@ class ImageLoader { public void stop() { if (Config.LOGV) { - Log.v(TAG, "ImageLoader.stop " + mDecodeThreads.size() + + Log.v(TAG, "ImageLoader.stop " + mDecodeThreads.size() + " threads"); } mDone = true; diff --git a/src/com/android/camera/ImageManager.java b/src/com/android/camera/ImageManager.java index 073b8db..121852e 100755 --- a/src/com/android/camera/ImageManager.java +++ b/src/com/android/camera/ImageManager.java @@ -16,3633 +16,134 @@ package com.android.camera; -import android.content.Context; import android.content.ContentResolver; -import android.content.ContentValues; import android.content.ContentUris; -import android.database.ContentObserver; +import android.content.ContentValues; +import android.content.Context; import android.database.Cursor; -import android.database.DataSetObserver; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Matrix; import android.location.Location; -import android.media.MediaMetadataRetriever; -import android.media.MediaPlayer; import android.net.Uri; import android.os.Environment; import android.os.Handler; -import android.os.ParcelFileDescriptor; -import android.provider.BaseColumns; import android.provider.DrmStore; import android.provider.MediaStore; -import android.provider.MediaStore.Images.ImageColumns; -import android.provider.MediaStore.Images.Thumbnails; -import android.provider.MediaStore.Video.VideoColumns; import android.provider.MediaStore.Images; -import android.provider.MediaStore.MediaColumns; -import android.provider.MediaStore.Video; +import android.provider.MediaStore.Images.ImageColumns; import android.util.Config; import android.util.Log; +import com.android.camera.gallery.BaseCancelable; +import com.android.camera.gallery.BaseImageList; +import com.android.camera.gallery.CanceledException; +import com.android.camera.gallery.DrmImageList; +import com.android.camera.gallery.IAddImageCancelable; +import com.android.camera.gallery.IGetBooleanCancelable; +import com.android.camera.gallery.IImage; +import com.android.camera.gallery.IImageList; +import com.android.camera.gallery.Image; +import com.android.camera.gallery.ImageList; +import com.android.camera.gallery.ImageListUber; +import com.android.camera.gallery.SingleImageList; +import com.android.camera.gallery.Util; +import com.android.camera.gallery.VideoList; + import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.HashMap; /** - * * ImageManager is used to retrieve and store images * in the media content provider. - * */ public class ImageManager { - public static final String CAMERA_IMAGE_BUCKET_NAME = - Environment.getExternalStorageDirectory().toString() + "/DCIM/Camera"; - public static final String CAMERA_IMAGE_BUCKET_ID = getBucketId(CAMERA_IMAGE_BUCKET_NAME); - - /** - * Matches code in MediaProvider.computeBucketValues. Should be a common function. - */ - - public static String getBucketId(String path) { - return String.valueOf(path.toLowerCase().hashCode()); - } - - /** - * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be imported. - * This is a temporary fix for bug#1655552. - */ - public static void ensureOSXCompatibleFolder() { - File nnnAAAAA = new File( - Environment.getExternalStorageDirectory().toString() + "/DCIM/100ANDRO"); - if ((!nnnAAAAA.exists()) && (!nnnAAAAA.mkdir())) { - Log.e(TAG, "create NNNAAAAA file: "+ nnnAAAAA.getPath()+" failed"); - } - } - - // To enable verbose logging for this class, change false to true. The other logic ensures that - // this logging can be disabled by turned off DEBUG and lower, and that it can be enabled by - // "setprop log.tag.ImageManager VERBOSE" if desired. + // To enable verbose logging for this class, change false to true. The other + // logic ensures that this logging can be disabled by turned off DEBUG and + // lower, and that it can be enabled by "setprop log.tag.ImageManager + // VERBOSE" if desired. // // IMPORTANT: Never check in this file set to true! - private static final boolean VERBOSE = Config.LOGD && (false || Config.LOGV); + private static final boolean VERBOSE = + Config.LOGD && (false || Config.LOGV); private static final String TAG = "ImageManager"; + private static ImageManager sInstance = null; - private static final int MINI_THUMB_DATA_FILE_VERSION = 3; - - static public void debug_where(String tag, String msg) { - try { - throw new Exception(); - } catch (Exception ex) { - if (msg != null) { - Log.v(tag, msg); - } - boolean first = true; - for (StackTraceElement s : ex.getStackTrace()) { - if (first) - first = false; - else - Log.v(tag, s.toString()); - } - } - } - - /* - * Compute the sample size as a function of the image size and the target. - * Scale the image down so that both the width and height are just above - * the target. If this means that one of the dimension goes from above - * the target to below the target (e.g. given a width of 480 and an image - * width of 600 but sample size of 2 -- i.e. new width 300 -- bump the - * sample size down by 1. - */ - private static int computeSampleSize(BitmapFactory.Options options, int target) { - int w = options.outWidth; - int h = options.outHeight; - - int candidateW = w / target; - int candidateH = h / target; - int candidate = Math.max(candidateW, candidateH); - - if (candidate == 0) - return 1; - - if (candidate > 1) { - if ((w > target) && (w / candidate) < target) - candidate -= 1; - } - - if (candidate > 1) { - if ((h > target) && (h / candidate) < target) - candidate -= 1; - } - - if (VERBOSE) - Log.v(TAG, "for w/h " + w + "/" + h + " returning " + candidate + "(" + (w/candidate) + " / " + (h/candidate)); - - return candidate; - } - /* - * All implementors of ICancelable should inherit from BaseCancelable - * since it provides some convenience methods such as acknowledgeCancel - * and checkCancel. - */ - public abstract class BaseCancelable implements ICancelable { - boolean mCancel = false; - boolean mFinished = false; - - /* - * Subclasses should call acknowledgeCancel when they're finished with - * their operation. - */ - protected void acknowledgeCancel() { - synchronized (this) { - mFinished = true; - if (!mCancel) - return; - if (mCancel) { - this.notify(); - } - } - } - - public boolean cancel() { - synchronized (this) { - if (mCancel) { - return false; - } - if (mFinished) { - return false; - } - mCancel = true; - boolean retVal = doCancelWork(); - - try { - this.wait(); - } catch (InterruptedException ex) { - // now what??? TODO - } - - return retVal; - } - } - - /* - * Subclasses can call this to see if they have been canceled. - * This is the polling model. - */ - protected void checkCanceled() throws CanceledException { - synchronized (this) { - if (mCancel) - throw new CanceledException(); - } - } + private static Uri sStorageURI = Images.Media.EXTERNAL_CONTENT_URI; + private static Uri sThumbURI = Images.Thumbnails.EXTERNAL_CONTENT_URI; - /* - * Subclasses implement this method to take whatever action - * is necessary when getting canceled. Sometimes it's not - * possible to do anything in which case the "checkCanceled" - * polling model may be used (or some combination). - */ - public abstract boolean doCancelWork(); - } + private static Uri sVideoStorageURI = + Uri.parse("content://media/external/video/media"); - private static final int sBytesPerMiniThumb = 10000; - static final private byte [] sMiniThumbData = new byte[sBytesPerMiniThumb]; + private static Uri sVideoThumbURI = + Uri.parse("content://media/external/video/thumbnails"); /** - * Represents a particular image and provides access - * to the underlying bitmap and two thumbnail bitmaps - * as well as other information such as the id, and - * the path to the actual image data. - */ - abstract class BaseImage implements IImage { - protected ContentResolver mContentResolver; - protected long mId, mMiniThumbMagic; - protected BaseImageList mContainer; - protected HashMap<String, String> mExifData; - protected int mCursorRow; - - protected BaseImage(long id, long miniThumbId, ContentResolver cr, BaseImageList container, int cursorRow) { - mContentResolver = cr; - mId = id; - mMiniThumbMagic = miniThumbId; - mContainer = container; - mCursorRow = cursorRow; - } - - abstract Bitmap.CompressFormat compressionType(); - - public void commitChanges() { - Cursor c = getCursor(); - synchronized (c) { - if (c.moveToPosition(getRow())) { - c.commitUpdates(); - c.requery(); - } - } - } - - /** - * Take a given bitmap and compress it to a file as described - * by the Uri parameter. - * - * @param bitmap the bitmap to be compressed/stored - * @param uri where to store the bitmap - * @return true if we succeeded - */ - protected IGetBoolean_cancelable compressImageToFile( - final Bitmap bitmap, - final byte [] jpegData, - final Uri uri) { - class CompressImageToFile extends BaseCancelable implements IGetBoolean_cancelable { - ThreadSafeOutputStream mOutputStream = null; - - public boolean doCancelWork() { - if (mOutputStream != null) { - try { - mOutputStream.close(); - return true; - } catch (IOException ex) { - // TODO what to do here - } - } - return false; - } - - public boolean get() { - try { - long t1 = System.currentTimeMillis(); - OutputStream delegate = mContentResolver.openOutputStream(uri); - synchronized (this) { - checkCanceled(); - mOutputStream = new ThreadSafeOutputStream(delegate); - } - long t2 = System.currentTimeMillis(); - if (bitmap != null) { - bitmap.compress(compressionType(), 75, mOutputStream); - } else { - long x1 = System.currentTimeMillis(); - mOutputStream.write(jpegData); - long x2 = System.currentTimeMillis(); - if (VERBOSE) Log.v(TAG, "done writing... " + jpegData.length + " bytes took " + (x2-x1)); - } - long t3 = System.currentTimeMillis(); - if (VERBOSE) Log.v(TAG, String.format("CompressImageToFile.get took %d (%d, %d)",(t3-t1),(t2-t1),(t3-t2))); - return true; - } catch (FileNotFoundException ex) { - return false; - } catch (CanceledException ex) { - return false; - } catch (IOException ex) { - return false; - } - finally { - if (mOutputStream != null) { - try { - mOutputStream.close(); - } catch (IOException ex) { - // not much we can do here so ignore - } - } - acknowledgeCancel(); - } - } - } - return new CompressImageToFile(); - } - - @Override - public boolean equals(Object other) { - if (other == null) - return false; - if (!(other instanceof Image)) - return false; - - return fullSizeImageUri().equals(((Image)other).fullSizeImageUri()); - } - - public Bitmap fullSizeBitmap(int targetWidthHeight) { - return fullSizeBitmap(targetWidthHeight, true); - } - - protected Bitmap fullSizeBitmap(int targetWidthHeight, boolean rotateAsNeeded) { - Uri url = mContainer.contentUri(mId); - if (VERBOSE) Log.v(TAG, "getCreateBitmap for " + url); - if (url == null) - return null; - - Bitmap b = null; - if (b == null) { - b = makeBitmap(targetWidthHeight, url); - if (b != null && rotateAsNeeded) { - b = rotate(b, getDegreesRotated()); - } - } - return b; - } - - - public IGetBitmap_cancelable fullSizeBitmap_cancelable(final int targetWidthHeight) { - final class LoadBitmapCancelable extends BaseCancelable implements IGetBitmap_cancelable { - ParcelFileDescriptor mPFD; - BitmapFactory.Options mOptions = new BitmapFactory.Options(); - long mCancelInitiationTime; - - public LoadBitmapCancelable(ParcelFileDescriptor pfdInput) { - mPFD = pfdInput; - } - - public boolean doCancelWork() { - if (VERBOSE) - Log.v(TAG, "requesting bitmap load cancel"); - mCancelInitiationTime = System.currentTimeMillis(); - mOptions.requestCancelDecode(); - return true; - } - - public Bitmap get() { - try { - Bitmap b = makeBitmap(targetWidthHeight, fullSizeImageUri(), mPFD, mOptions); - if (mCancelInitiationTime != 0) { - if (VERBOSE) - Log.v(TAG, "cancelation of bitmap load success==" + (b == null ? "TRUE" : "FALSE") + " -- took " + (System.currentTimeMillis() - mCancelInitiationTime)); - } - if (b != null) { - b = rotate(b, getDegreesRotated()); - } - return b; - } catch (Exception ex) { - return null; - } finally { - acknowledgeCancel(); - } - } - } - - try { - ParcelFileDescriptor pfdInput = mContentResolver.openFileDescriptor(fullSizeImageUri(), "r"); - return new LoadBitmapCancelable(pfdInput); - } catch (FileNotFoundException ex) { - return null; - } catch (UnsupportedOperationException ex) { - return null; - } - } - - public InputStream fullSizeImageData() { - try { - InputStream input = mContentResolver.openInputStream( - fullSizeImageUri()); - return input; - } catch (IOException ex) { - return null; - } - } - - public long fullSizeImageId() { - return mId; - } - - public Uri fullSizeImageUri() { - return mContainer.contentUri(mId); - } - - public IImageList getContainer() { - return mContainer; - } - - Cursor getCursor() { - return mContainer.getCursor(); - } - - public long getDateTaken() { - if (mContainer.indexDateTaken() < 0) return 0; - Cursor c = getCursor(); - synchronized (c) { - c.moveToPosition(getRow()); - return c.getLong(mContainer.indexDateTaken()); - } - } - - protected int getDegreesRotated() { - return 0; - } - - public String getMimeType() { - if (mContainer.indexMimeType() < 0) { - Cursor c = null; - try { - c = mContentResolver.query( - fullSizeImageUri(), - new String[] { "_id", Images.Media.MIME_TYPE }, - null, - null, null); - if (c != null && c.moveToFirst()) { - return c.getString(1); - } else { - return ""; - } - } finally { - if (c != null) - c.close(); - } - } else { - String mimeType = null; - Cursor c = getCursor(); - synchronized(c) { - if (c.moveToPosition(getRow())) { - mimeType = c.getString(mContainer.indexMimeType()); - } - } - return mimeType; - } - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#getDescription() - */ - public String getDescription() { - if (mContainer.indexDescription() < 0) { - Cursor c = null; - try { - c = mContentResolver.query( - fullSizeImageUri(), - new String[] { "_id", Images.Media.DESCRIPTION }, - null, - null, null); - if (c != null && c.moveToFirst()) { - return c.getString(1); - } else { - return ""; - } - } finally { - if (c != null) - c.close(); - } - } else { - String description = null; - Cursor c = getCursor(); - synchronized(c) { - if (c.moveToPosition(getRow())) { - description = c.getString(mContainer.indexDescription()); - } - } - return description; - } - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#getIsPrivate() - */ - public boolean getIsPrivate() { - if (mContainer.indexPrivate() < 0) return false; - boolean isPrivate = false; - Cursor c = getCursor(); - synchronized(c) { - if (c.moveToPosition(getRow())) { - isPrivate = c.getInt(mContainer.indexPrivate()) != 0; - } - } - return isPrivate; - } - - public double getLatitude() { - if (mContainer.indexLatitude() < 0) return 0D; - Cursor c = getCursor(); - synchronized (c) { - c.moveToPosition(getRow()); - return c.getDouble(mContainer.indexLatitude()); - } - } - - public double getLongitude() { - if (mContainer.indexLongitude() < 0) return 0D; - Cursor c = getCursor(); - synchronized (c) { - c.moveToPosition(getRow()); - return c.getDouble(mContainer.indexLongitude()); - } - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#getTitle() - */ - public String getTitle() { - String name = null; - Cursor c = getCursor(); - synchronized(c) { - if (c.moveToPosition(getRow())) { - if (mContainer.indexTitle() != -1) { - name = c.getString(mContainer.indexTitle()); - } - } - } - return name != null && name.length() > 0 ? name : String.valueOf(mId); - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#getDisplayName() - */ - public String getDisplayName() { - if (mContainer.indexDisplayName() < 0) { - Cursor c = null; - try { - c = mContentResolver.query( - fullSizeImageUri(), - new String[] { "_id", Images.Media.DISPLAY_NAME }, - null, - null, null); - if (c != null && c.moveToFirst()) { - return c.getString(1); - } - } finally { - if (c != null) - c.close(); - } - } else { - String name = null; - Cursor c = getCursor(); - synchronized(c) { - if (c.moveToPosition(getRow())) { - name = c.getString(mContainer.indexDisplayName()); - } - } - if (name != null && name.length() > 0) - return name; - } - return String.valueOf(mId); - } - - public String getPicasaId() { - /* - if (mContainer.indexPicasaWeb() < 0) return null; - Cursor c = getCursor(); - synchronized (c) { - c.moveTo(getRow()); - return c.getString(mContainer.indexPicasaWeb()); - } - */ - return null; - } - - public int getRow() { - return mCursorRow; - } - - public int getWidth() { - ParcelFileDescriptor input = null; - try { - Uri uri = fullSizeImageUri(); - input = mContentResolver.openFileDescriptor(uri, "r"); - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options); - return options.outWidth; - } catch (IOException ex) { - return 0; - } finally { - try { - if (input != null) { - input.close(); - } - } catch (IOException ex) { - } - } - } - - public int getHeight() { - ParcelFileDescriptor input = null; - try { - Uri uri = fullSizeImageUri(); - input = mContentResolver.openFileDescriptor(uri, "r"); - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options); - return options.outHeight; - } catch (IOException ex) { - return 0; - } finally { - try { - if (input != null) { - input.close(); - } - } catch (IOException ex) { - } - } - } - - public boolean hasLatLong() { - if (mContainer.indexLatitude() < 0 || mContainer.indexLongitude() < 0) return false; - Cursor c = getCursor(); - synchronized (c) { - c.moveToPosition(getRow()); - return !c.isNull(mContainer.indexLatitude()) && !c.isNull(mContainer.indexLongitude()); - } - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#imageId() - */ - public long imageId() { - return mId; - } - - /** - * Make a bitmap from a given Uri. - * - * @param uri - */ - private Bitmap makeBitmap(int targetWidthOrHeight, Uri uri) { - ParcelFileDescriptor input = null; - try { - input = mContentResolver.openFileDescriptor(uri, "r"); - return makeBitmap(targetWidthOrHeight, uri, input, null); - } catch (IOException ex) { - return null; - } finally { - try { - if (input != null) { - input.close(); - } - } catch (IOException ex) { - } - } - } - - protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options) { - return mContainer.makeBitmap(targetWidthHeight, uri, pfdInput, options); - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#thumb1() - */ - public Bitmap miniThumbBitmap() { - try { - long id = mId; - long dbMagic = mMiniThumbMagic; - if (dbMagic == 0 || dbMagic == id) { - dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), getRow()); - if (VERBOSE) Log.v(TAG, "after computing thumbnail dbMagic is " + dbMagic); - } - - synchronized(sMiniThumbData) { - dbMagic = mMiniThumbMagic; - byte [] data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic); - if (data == null) { - byte[][] createdThumbData = new byte[1][]; - try { - dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), - getRow(), createdThumbData); - } catch (IOException ex) { - // Typically IOException because the sd card is full. - // But createdThumbData may have been filled in, so continue on. - } - data = createdThumbData[0]; - } - if (data == null) { - data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic); - } - if (data == null) { - if (VERBOSE) - Log.v(TAG, "unable to get miniThumbBitmap, data is null"); - } - if (data != null) { - Bitmap b = BitmapFactory.decodeByteArray(data, 0, data.length); - if (b == null) { - if (VERBOSE) { - Log.v(TAG, "couldn't decode byte array for mini thumb, length was " + data.length); - } - } - return b; - } - } - return null; - } catch (Exception ex) { - // Typically IOException because the sd card is full. - if (VERBOSE) { - Log.e(TAG, "miniThumbBitmap got exception " + ex.toString()); - for (StackTraceElement s : ex.getStackTrace()) - Log.e(TAG, "... " + s.toString()); - } - return null; - } - } - - public void onRemove() { - mContainer.mCache.remove(mId); - } - - protected void saveMiniThumb(Bitmap source) throws IOException { - mContainer.saveMiniThumbToFile(source, fullSizeImageId(), 0); - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#setName() - */ - public void setDescription(String description) { - if (mContainer.indexDescription() < 0) return; - Cursor c = getCursor(); - synchronized (c) { - if (c.moveToPosition(getRow())) { - c.updateString(mContainer.indexDescription(), description); - } - } - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#setIsPrivate() - */ - public void setIsPrivate(boolean isPrivate) { - if (mContainer.indexPrivate() < 0) return; - Cursor c = getCursor(); - synchronized (c) { - if (c.moveToPosition(getRow())) { - c.updateInt(mContainer.indexPrivate(), isPrivate ? 1 : 0); - } - } - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#setName() - */ - public void setName(String name) { - Cursor c = getCursor(); - synchronized (c) { - if (c.moveToPosition(getRow())) { - c.updateString(mContainer.indexTitle(), name); - } - } - } - - public void setPicasaId(String id) { - Cursor c = null; - try { - c = mContentResolver.query( - fullSizeImageUri(), - new String[] { "_id", Images.Media.PICASA_ID }, - null, - null, null); - if (c != null && c.moveToFirst()) { - if (VERBOSE) { - Log.v(TAG, "storing picasaid " + id + " for " + fullSizeImageUri()); - } - c.updateString(1, id); - c.commitUpdates(); - if (VERBOSE) { - Log.v(TAG, "updated image with picasa id " + id); - } - } - } finally { - if (c != null) - c.close(); - } - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#thumbUri() - */ - public Uri thumbUri() { - Uri uri = fullSizeImageUri(); - // The value for the query parameter cannot be null :-(, so using a dummy "1" - uri = uri.buildUpon().appendQueryParameter("thumb", "1").build(); - return uri; - } - - @Override - public String toString() { - return fullSizeImageUri().toString(); - } - } - - abstract static class BaseImageList implements IImageList { - Context mContext; - ContentResolver mContentResolver; - Uri mBaseUri, mUri; - int mSort; - String mBucketId; - boolean mDistinct; - Cursor mCursor; - boolean mCursorDeactivated; - protected HashMap<Long, IImage> mCache = new HashMap<Long, IImage>(); - - IImageList.OnChange mListener = null; - Handler mHandler; - protected RandomAccessFile mMiniThumbData; - protected Uri mThumbUri; - - public BaseImageList(Context ctx, ContentResolver cr, Uri uri, int sort, String bucketId) { - mContext = ctx; - mSort = sort; - mUri = uri; - mBaseUri = uri; - mBucketId = bucketId; - - mContentResolver = cr; - } - - String randomAccessFilePath(int version) { - String directoryName = Environment.getExternalStorageDirectory().toString() + "/DCIM/.thumbnails"; - String path = directoryName + "/.thumbdata" + version + "-" + mUri.hashCode(); - return path; - } - - RandomAccessFile miniThumbDataFile() { - if (mMiniThumbData == null) { - String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION); - File directory = new File(new File(path).getParent()); - if (!directory.isDirectory()) { - if (!directory.mkdirs()) { - Log.e(TAG, "!!!! unable to create .thumbnails directory " + directory.toString()); - } - } - File f = new File(path); - if (VERBOSE) Log.v(TAG, "file f is " + f.toString()); - try { - mMiniThumbData = new RandomAccessFile(f, "rw"); - } catch (IOException ex) { - - } - } - return mMiniThumbData; - } - - /** - * Store a given thumbnail in the database. - */ - protected Bitmap storeThumbnail(Bitmap thumb, long imageId) { - if (thumb == null) - return null; - - try { - Uri uri = getThumbnailUri(imageId, thumb.getWidth(), thumb.getHeight()); - if (uri == null) { - return thumb; - } - OutputStream thumbOut = mContentResolver.openOutputStream(uri); - thumb.compress(Bitmap.CompressFormat.JPEG, 60, thumbOut); - thumbOut.close(); - return thumb; - } - catch (Exception ex) { - if (VERBOSE) Log.d(TAG, "unable to store thumbnail: " + ex); - return thumb; - } - } - - /** - * Store a JPEG thumbnail from the EXIF header in the database. - */ - protected boolean storeThumbnail(byte[] jpegThumbnail, long imageId, int width, int height) { - if (jpegThumbnail == null) - return false; - - Uri uri = getThumbnailUri(imageId, width, height); - if (uri == null) { - return false; - } - try { - OutputStream thumbOut = mContentResolver.openOutputStream(uri); - thumbOut.write(jpegThumbnail); - thumbOut.close(); - return true; - } - catch (FileNotFoundException ex) { - return false; - } - catch (IOException ex) { - return false; - } - } - - private Uri getThumbnailUri(long imageId, int width, int height) { - // we do not store thumbnails for DRM'd images - if (mThumbUri == null) { - return null; - } - - Uri uri = null; - Cursor c = null; - try { - c = mContentResolver.query( - mThumbUri, - THUMB_PROJECTION, - Thumbnails.IMAGE_ID + "=?", - new String[]{String.valueOf(imageId)}, - null); - if (c != null && c.moveToFirst()) { - // If, for some reaosn, we already have a row with a matching - // image id, then just update that row rather than creating a - // new row. - uri = ContentUris.withAppendedId(mThumbUri, c.getLong(indexThumbId())); - c.commitUpdates(); - } - } finally { - if (c != null) - c.close(); - } - if (uri == null) { - ContentValues values = new ContentValues(4); - values.put(Images.Thumbnails.KIND, Images.Thumbnails.MINI_KIND); - values.put(Images.Thumbnails.IMAGE_ID, imageId); - values.put(Images.Thumbnails.HEIGHT, height); - values.put(Images.Thumbnails.WIDTH, width); - uri = mContentResolver.insert(mThumbUri, values); - } - return uri; - } - - java.util.Random mRandom = new java.util.Random(System.currentTimeMillis()); - - protected SomewhatFairLock mLock = new SomewhatFairLock(); - - class SomewhatFairLock { - private Object mSync = new Object(); - private boolean mLocked = false; - private ArrayList<Thread> mWaiting = new ArrayList<Thread>(); - - void lock() { -// if (VERBOSE) Log.v(TAG, "lock... thread " + Thread.currentThread().getId()); - synchronized (mSync) { - while (mLocked) { - try { -// if (VERBOSE) Log.v(TAG, "waiting... thread " + Thread.currentThread().getId()); - mWaiting.add(Thread.currentThread()); - mSync.wait(); - if (mWaiting.get(0) == Thread.currentThread()) { - mWaiting.remove(0); - break; - } - } catch (InterruptedException ex) { - // - } - } -// if (VERBOSE) Log.v(TAG, "locked... thread " + Thread.currentThread().getId()); - mLocked = true; - } - } - - void unlock() { -// if (VERBOSE) Log.v(TAG, "unlocking... thread " + Thread.currentThread().getId()); - synchronized (mSync) { - mLocked = false; - mSync.notifyAll(); - } - } - } - - // If the photo has an EXIF thumbnail and it's big enough, extract it and save that JPEG as - // the large thumbnail without re-encoding it. We still have to decompress it though, in - // order to generate the minithumb. - private Bitmap createThumbnailFromEXIF(String filePath, long id) { - if (filePath != null) { - byte [] thumbData = null; - synchronized (ImageManager.instance()) { - thumbData = (new ExifInterface(filePath)).getThumbnail(); - } - if (thumbData != null) { - // Sniff the size of the EXIF thumbnail before decoding it. Photos from the - // device will pass, but images that are side loaded from other cameras may not. - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options); - int width = options.outWidth; - int height = options.outHeight; - if (width >= THUMBNAIL_TARGET_SIZE && height >= THUMBNAIL_TARGET_SIZE) { - if (storeThumbnail(thumbData, id, width, height)) { - // this is used for *encoding* the minithumb, so - // we don't want to dither or convert to 565 here. - // - // Decode with a scaling factor - // to match MINI_THUMB_TARGET_SIZE closely - // which will produce much better scaling quality - // and is significantly faster. - options.inSampleSize = computeSampleSize(options, THUMBNAIL_TARGET_SIZE); - - if (VERBOSE) { - Log.v(TAG, "in createThumbnailFromExif using inSampleSize of " + options.inSampleSize); - } - options.inDither = false; - options.inPreferredConfig = Bitmap.Config.ARGB_8888; - options.inJustDecodeBounds = false; - return BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options); - } - } - } - } - return null; - } - - // The fallback case is to decode the original photo to thumbnail size, then encode it as a - // JPEG. We return the thumbnail Bitmap in order to create the minithumb from it. - private Bitmap createThumbnailFromUri(Cursor c, long id) { - Uri uri = ContentUris.withAppendedId(mBaseUri, id); - Bitmap bitmap = makeBitmap(THUMBNAIL_TARGET_SIZE, uri, null, null); - if (bitmap != null) { - storeThumbnail(bitmap, id); - } else { - uri = ContentUris.withAppendedId(mBaseUri, id); - bitmap = makeBitmap(MINI_THUMB_TARGET_SIZE, uri, null, null); - } - return bitmap; - } - - // returns id - public long checkThumbnail(BaseImage existingImage, Cursor c, int i) throws IOException { - return checkThumbnail(existingImage, c, i, null); - } - - /** - * Checks to see if a mini thumbnail exists in the cache. If not, tries to create it and - * add it to the cache. - * @param existingImage - * @param c - * @param i - * @param createdThumbnailData if this parameter is non-null, and a new mini-thumbnail - * bitmap is created, the new bitmap's data will be stored in createdThumbnailData[0]. - * Note that if the sdcard is full, it's possible that - * createdThumbnailData[0] will be set even if the method throws an IOException. This is - * actually useful, because it allows the caller to use the created thumbnail even if - * the sdcard is full. - * @return - * @throws IOException - */ - public long checkThumbnail(BaseImage existingImage, Cursor c, int i, - byte[][] createdThumbnailData) throws IOException { - long magic, fileMagic = 0, id; - try { - mLock.lock(); - if (existingImage == null) { - // if we don't have an Image object then get the id and magic from - // the cursor. Synchronize on the cursor object. - synchronized (c) { - if (!c.moveToPosition(i)) { - return -1; - } - magic = c.getLong(indexMiniThumbId()); - id = c.getLong(indexId()); - } - } else { - // if we have an Image object then ask them for the magic/id - magic = existingImage.mMiniThumbMagic; - id = existingImage.fullSizeImageId(); - } - - if (magic != 0) { - // check the mini thumb file for the right data. Right is defined as - // having the right magic number at the offset reserved for this "id". - RandomAccessFile r = miniThumbDataFile(); - if (r != null) { - synchronized (r) { - long pos = id * sBytesPerMiniThumb; - try { - // check that we can read the following 9 bytes (1 for the "status" and 8 for the long) - if (r.length() >= pos + 1 + 8) { - r.seek(pos); - if (r.readByte() == 1) { - fileMagic = r.readLong(); - if (fileMagic == magic && magic != 0 && magic != id) { - return magic; - } - } - } - } catch (IOException ex) { - Log.v(TAG, "got exception checking file magic: " + ex); - } - } - } - if (VERBOSE) { - Log.v(TAG, "didn't verify... fileMagic: " + fileMagic + "; magic: " + magic + "; id: " + id + "; "); - } - } - - // If we can't retrieve the thumbnail, first check if there is one embedded in the - // EXIF data. If not, or it's not big enough, decompress the full size image. - Bitmap bitmap = null; - String filePath = null; - synchronized (c) { - if (c.moveToPosition(i)) { - filePath = c.getString(indexData()); - } - } - if (filePath != null) { - String mimeType = c.getString(indexMimeType()); - boolean isVideo = isVideoMimeType(mimeType); - if (isVideo) { - bitmap = createVideoThumbnail(filePath); - } else { - bitmap = createThumbnailFromEXIF(filePath, id); - if (bitmap == null) { - bitmap = createThumbnailFromUri(c, id); - } - } - synchronized (c) { - int degrees = 0; - if (c.moveToPosition(i)) { - int column = indexOrientation(); - if (column >= 0) - degrees = c.getInt(column); - } - if (degrees != 0) { - bitmap = rotate(bitmap, degrees); - } - } - } - - // make a new magic number since things are out of sync - do { - magic = mRandom.nextLong(); - } while (magic == 0); - if (bitmap != null) { - byte [] data = miniThumbData(bitmap); - if (createdThumbnailData != null) { - createdThumbnailData[0] = data; - } - saveMiniThumbToFile(data, id, magic); - } - - synchronized (c) { - c.moveToPosition(i); - c.updateLong(indexMiniThumbId(), magic); - c.commitUpdates(); - c.requery(); - c.moveToPosition(i); - - if (existingImage != null) { - existingImage.mMiniThumbMagic = magic; - } - return magic; - } - } finally { - mLock.unlock(); - } - } - - public void checkThumbnails(ThumbCheckCallback cb, int totalThumbnails) { - Cursor c = Images.Media.query( - mContentResolver, - mBaseUri, - new String[] { "_id", "mini_thumb_magic" }, - thumbnailWhereClause(), - thumbnailWhereClauseArgs(), - "_id ASC"); - - int count = c.getCount(); - if (VERBOSE) - Log.v(TAG, ">>>>>>>>>>> need to check " + c.getCount() + " rows"); - - c.close(); - - if (!ImageManager.hasStorage()) { - if (VERBOSE) - Log.v(TAG, "bailing from the image checker thread -- no storage"); - return; - } - - String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1); - File oldFile = new File(oldPath); - - if (count == 0) { - // now check that we have the right thumbs file -// Log.v(TAG, "count is zero but oldFile.exists() is " + oldFile.exists()); - if (!oldFile.exists()) { - return; - } - } - - c = getCursor(); - try { - if (VERBOSE) Log.v(TAG, "checkThumbnails found " + c.getCount()); - int current = 0; - for (int i = 0; i < c.getCount(); i++) { - try { - checkThumbnail(null, c, i); - } catch (Exception ex) { - Log.e(TAG, "!!!!! failed to check thumbnail... was the sd card removed?"); - break; - } - if (cb != null) { - if (!cb.checking(current, totalThumbnails)) { - if (VERBOSE) Log.v(TAG, "got false from checking... break <<<<<<<<<<<<<<<<<<<<<<<<"); - break; - } - } - current += 1; - } - } finally { - if (VERBOSE) Log.v(TAG, "checkThumbnails existing after reaching count " + c.getCount()); - try { - oldFile.delete(); - } catch (Exception ex) { - // ignore - } - } - } - - protected String thumbnailWhereClause() { - return sMiniThumbIsNull + " and " + sWhereClause; - } - - protected String[] thumbnailWhereClauseArgs() { - return sAcceptableImageTypes; - } - - public void commitChanges() { - synchronized (mCursor) { - mCursor.commitUpdates(); - requery(); - } - } - protected Uri contentUri(long id) { - try { - // does our uri already have an id (single image query)? - // if so just return it - long existingId = ContentUris.parseId(mBaseUri); - if (existingId != id) - Log.e(TAG, "id mismatch"); - return mBaseUri; - } catch (NumberFormatException ex) { - // otherwise tack on the id - return ContentUris.withAppendedId(mBaseUri, id); - } - } - - public void deactivate() { - mCursorDeactivated = true; - try { - mCursor.deactivate(); - } catch (IllegalStateException e) { - // IllegalStateException may be thrown if the cursor is stale. - Log.e(TAG, "Caught exception while deactivating cursor.", e); - } - if (mMiniThumbData != null) { - try { - mMiniThumbData.close(); - mMiniThumbData = null; - } catch (IOException ex) { - - } - } - } - - public void dump(String msg) { - int count = getCount(); - if (VERBOSE) Log.v(TAG, "dump ImageList (count is " + count + ") " + msg); - for (int i = 0; i < count; i++) { - IImage img = getImageAt(i); - if (img == null) - if (VERBOSE) Log.v(TAG, " " + i + ": " + "null"); - else - if (VERBOSE) Log.v(TAG, " " + i + ": " + img.toString()); - } - if (VERBOSE) Log.v(TAG, "end of dump container"); - } - public int getCount() { - Cursor c = getCursor(); - synchronized (c) { - try { - return c.getCount(); - } catch (Exception ex) { - } - return 0; - } - } - - public boolean isEmpty() { - return getCount() == 0; - } - - protected Cursor getCursor() { - synchronized (mCursor) { - if (mCursorDeactivated) { - activateCursor(); - } - return mCursor; - } - } - - protected void activateCursor() { - requery(); - } - - public IImage getImageAt(int i) { - Cursor c = getCursor(); - synchronized (c) { - boolean moved; - try { - moved = c.moveToPosition(i); - } catch (Exception ex) { - return null; - } - if (moved) { - try { - long id = c.getLong(0); - long miniThumbId = 0; - int rotation = 0; - if (indexMiniThumbId() != -1) { - miniThumbId = c.getLong(indexMiniThumbId()); - } - if (indexOrientation() != -1) { - rotation = c.getInt(indexOrientation()); - } - long timestamp = c.getLong(1); - IImage img = mCache.get(id); - if (img == null) { - img = make(id, miniThumbId, mContentResolver, this, timestamp, i, rotation); - mCache.put(id, img); - } - return img; - } catch (Exception ex) { - Log.e(TAG, "got this exception trying to create image object: " + ex); - return null; - } - } else { - Log.e(TAG, "unable to moveTo to " + i + "; count is " + c.getCount()); - return null; - } - } - } - public IImage getImageForUri(Uri uri) { - // TODO make this a hash lookup - for (int i = 0; i < getCount(); i++) { - if (getImageAt(i).fullSizeImageUri().equals(uri)) { - return getImageAt(i); - } - } - return null; - } - private byte [] getMiniThumbFromFile(long id, byte [] data, long magicCheck) { - RandomAccessFile r = miniThumbDataFile(); - if (r == null) - return null; - - long pos = id * sBytesPerMiniThumb; - RandomAccessFile f = r; - synchronized (f) { - try { - f.seek(pos); - if (f.readByte() == 1) { - long magic = f.readLong(); - if (magic != magicCheck) { - if (VERBOSE) Log.v(TAG, "for id " + id + "; magic: " + magic + "; magicCheck: " + magicCheck + " (fail)"); - return null; - } - int length = f.readInt(); - f.read(data, 0, length); - return data; - } else { - return null; - } - } catch (IOException ex) { - long fileLength; - try { - fileLength = f.length(); - } catch (IOException ex1) { - fileLength = -1; - } - if (VERBOSE) { - Log.e(TAG, "couldn't read thumbnail for " + id + "; " + ex.toString() + "; pos is " + pos + "; length is " + fileLength); - } - return null; - } - } - } - protected int getRowFor(IImage imageObj) { - Cursor c = getCursor(); - synchronized (c) { - int index = 0; - long targetId = imageObj.fullSizeImageId(); - if (c.moveToFirst()) { - do { - if (c.getLong(0) == targetId) { - return index; - } - index += 1; - } while (c.moveToNext()); - } - return -1; - } - } - - protected abstract int indexOrientation(); - protected abstract int indexDateTaken(); - protected abstract int indexDescription(); - protected abstract int indexMimeType(); - protected abstract int indexData(); - protected abstract int indexId(); - protected abstract int indexLatitude(); - protected abstract int indexLongitude(); - protected abstract int indexMiniThumbId(); - protected abstract int indexPicasaWeb(); - protected abstract int indexPrivate(); - protected abstract int indexTitle(); - protected abstract int indexDisplayName(); - protected abstract int indexThumbId(); - - protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) { - return null; - } - - protected abstract Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options); - - public boolean removeImage(IImage image) { - Cursor c = getCursor(); - synchronized (c) { - /* - * TODO: consider putting the image in a holding area so - * we can get it back as needed - * TODO: need to delete the thumbnails as well - */ - boolean moved; - try { - moved = c.moveToPosition(image.getRow()); - } catch (Exception ex) { - Log.e(TAG, "removeImage got exception " + ex.toString()); - return false; - } - if (moved) { - Uri u = image.fullSizeImageUri(); - mContentResolver.delete(u, null, null); - image.onRemove(); - requery(); - } - } - return true; - } - - - /* (non-Javadoc) - * @see com.android.camera.IImageList#removeImageAt(int) - */ - public void removeImageAt(int i) { - Cursor c = getCursor(); - synchronized (c) { - /* - * TODO: consider putting the image in a holding area so - * we can get it back as needed - * TODO: need to delete the thumbnails as well - */ - dump("before delete"); - IImage image = getImageAt(i); - boolean moved; - try { - moved = c.moveToPosition(i); - } catch (Exception ex) { - return; - } - if (moved) { - Uri u = image.fullSizeImageUri(); - mContentResolver.delete(u, null, null); - requery(); - image.onRemove(); - } - dump("after delete"); - } - } - - public void removeOnChangeListener(OnChange changeCallback) { - if (changeCallback == mListener) - mListener = null; - } - - protected void requery() { - mCache.clear(); - mCursor.requery(); - mCursorDeactivated = false; - } - - protected void saveMiniThumbToFile(Bitmap bitmap, long id, long magic) throws IOException { - byte[] data = miniThumbData(bitmap); - saveMiniThumbToFile(data, id, magic); - } - - protected void saveMiniThumbToFile(byte[] data, long id, long magic) throws IOException { - RandomAccessFile r = miniThumbDataFile(); - if (r == null) - return; - - long pos = id * sBytesPerMiniThumb; - long t0 = System.currentTimeMillis(); - synchronized (r) { - try { - long t1 = System.currentTimeMillis(); - long t2 = System.currentTimeMillis(); - if (data != null) { - if (data.length > sBytesPerMiniThumb) { - if (VERBOSE) Log.v(TAG, "!!!!!!!!!!!!!!!!!!!!!!!!!!! " + data.length + " > " + sBytesPerMiniThumb); - return; - } - r.seek(pos); - r.writeByte(0); // we have no data in this slot - - // if magic is 0 then leave it alone - if (magic == 0) - r.skipBytes(8); - else - r.writeLong(magic); - r.writeInt(data.length); - r.write(data); - // f.flush(); - r.seek(pos); - r.writeByte(1); // we have data in this slot - long t3 = System.currentTimeMillis(); - - if (VERBOSE) Log.v(TAG, "saveMiniThumbToFile took " + (t3-t0) + "; " + (t1-t0) + " " + (t2-t1) + " " + (t3-t2)); - } - } catch (IOException ex) { - Log.e(TAG, "couldn't save mini thumbnail data for " + id + "; " + ex.toString()); - throw ex; - } - } - } - - public void setOnChangeListener(OnChange changeCallback, Handler h) { - mListener = changeCallback; - mHandler = h; - } - } - - public class CanceledException extends Exception { - - } - public enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL } - - public interface IAddImage_cancelable extends ICancelable { - public void get(); - } - - /* - * The model for canceling an in-progress image save is this. For any - * given part of the task of saving return an ICancelable. The "result" - * from an ICancelable can be retrieved using the get* method. If the - * operation was canceled then null is returned. The act of canceling - * is to call "cancel" -- from another thread. - * - * In general an object which implements ICancelable will need to - * check, periodically, whether they are canceled or not. This works - * well for some things and less well for others. - * - * Right now the actual jpeg encode does not check cancelation but - * the part of encoding which writes the data to disk does. Note, - * though, that there is what appears to be a bug in the jpeg encoder - * in that if the stream that's being written is closed it crashes - * rather than returning an error. TODO fix that. - * - * When an object detects that it is canceling it must, before exiting, - * call acknowledgeCancel. This is necessary because the caller of - * cancel() will block until acknowledgeCancel is called. + * Enumerate type for the location of the images in gallery. */ - public interface ICancelable { - /* - * call cancel() when the unit of work in progress needs to be - * canceled. This should return true if it was possible to - * cancel and false otherwise. If this returns false the caller - * may still be able to cleanup and simulate cancelation. - */ - public boolean cancel(); - } - - public interface IGetBitmap_cancelable extends ICancelable { - // returns the bitmap or null if there was an error or we were canceled - public Bitmap get(); - }; - public interface IGetBoolean_cancelable extends ICancelable { - public boolean get(); - } - public interface IImage { - - public abstract void commitChanges(); - - /** - * Get the bitmap for the full size image. - * @return the bitmap for the full size image. - */ - public abstract Bitmap fullSizeBitmap(int targetWidthOrHeight); + public static enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL } - /** - * - * @return an object which can be canceled while the bitmap is loading - */ - public abstract IGetBitmap_cancelable fullSizeBitmap_cancelable(int targetWidthOrHeight); + public static final Bitmap DEFAULT_THUMBNAIL = + Bitmap.createBitmap(32, 32, Bitmap.Config.RGB_565); + public static final Bitmap NO_IMAGE_BITMAP = + Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); - /** - * Gets the input stream associated with a given full size image. - * This is used, for example, if one wants to email or upload - * the image. - * @return the InputStream associated with the image. - */ - public abstract InputStream fullSizeImageData(); - public abstract long fullSizeImageId(); - public abstract Uri fullSizeImageUri(); - public abstract IImageList getContainer(); - public abstract long getDateTaken(); + public static final int SORT_ASCENDING = 1; + public static final int SORT_DESCENDING = 2; - /** - * Gets the description of the image. - * @return the description of the image. - */ - public abstract String getDescription(); - public abstract String getMimeType(); - public abstract int getHeight(); + public static final int INCLUDE_IMAGES = (1 << 0); + public static final int INCLUDE_DRM_IMAGES = (1 << 1); + public static final int INCLUDE_VIDEOS = (1 << 2); - /** - * Gets the flag telling whether this video/photo is private or public. - * @return the description of the image. - */ - public abstract boolean getIsPrivate(); - - public abstract double getLatitude(); - - public abstract double getLongitude(); - - /** - * Gets the name of the image. - * @return the name of the image. - */ - public abstract String getTitle(); - - public abstract String getDisplayName(); - - public abstract String getPicasaId(); - - public abstract int getRow(); - - public abstract int getWidth(); - - public abstract boolean hasLatLong(); - - public abstract long imageId(); - - public abstract boolean isReadonly(); - - public abstract boolean isDrm(); - - public abstract Bitmap miniThumbBitmap(); - - public abstract void onRemove(); - - public abstract boolean rotateImageBy(int degrees); - - /** - * Sets the description of the image. - */ - public abstract void setDescription(String description); - - /** - * Sets whether the video/photo is private or public. - */ - public abstract void setIsPrivate(boolean isPrivate); - - /** - * Sets the name of the image. - */ - public abstract void setName(String name); - - public abstract void setPicasaId(String id); - - /** - * Get the bitmap for the medium thumbnail. - * @return the bitmap for the medium thumbnail. - */ - public abstract Bitmap thumbBitmap(); - - public abstract Uri thumbUri(); - - public abstract String getDataPath(); - } - - public interface IImageList { - public HashMap<String, String> getBucketIds(); - - public interface OnChange { - public void onChange(IImageList list); - } - - public interface ThumbCheckCallback { - public boolean checking(int current, int count); - } - - public abstract void checkThumbnails(ThumbCheckCallback cb, int totalCount); - - public abstract void commitChanges(); - - public abstract void deactivate(); - - /** - * Returns the count of image objects. - * - * @return the number of images - */ - public abstract int getCount(); - - /** - * @return true if the count of image objects is zero. - */ - - public abstract boolean isEmpty(); - - /** - * Returns the image at the ith position. - * - * @param i the position - * @return the image at the ith position - */ - public abstract IImage getImageAt(int i); - - /** - * Returns the image with a particular Uri. - * - * @param uri - * @return the image with a particular Uri. - */ - public abstract IImage getImageForUri(Uri uri);; - - /** - * - * @param image - * @return true if the image was removed. - */ - public abstract boolean removeImage(IImage image); - /** - * Removes the image at the ith position. - * @param i the position - */ - public abstract void removeImageAt(int i); - - public abstract void removeOnChangeListener(OnChange changeCallback); - public abstract void setOnChangeListener(OnChange changeCallback, Handler h); - } - - class Image extends BaseImage implements IImage { - int mRotation; - - protected Image(long id, long miniThumbId, ContentResolver cr, BaseImageList container, int cursorRow, int rotation) { - super(id, miniThumbId, cr, container, cursorRow); - mRotation = rotation; - } - - public String getDataPath() { - String path = null; - Cursor c = getCursor(); - synchronized (c) { - if (c.moveToPosition(getRow())) { - int column = ((ImageList)getContainer()).indexData(); - if (column >= 0) - path = c.getString(column); - } - } - return path; - } - - protected int getDegreesRotated() { - return mRotation; - } - - protected void setDegreesRotated(int degrees) { - Cursor c = getCursor(); - mRotation = degrees; - synchronized (c) { - if (c.moveToPosition(getRow())) { - int column = ((ImageList)getContainer()).indexOrientation(); - if (column >= 0) { - c.updateInt(column, degrees); - getContainer().commitChanges(); - } - } - } - } - - protected Bitmap.CompressFormat compressionType() { - String mimeType = getMimeType(); - if (mimeType == null) - return Bitmap.CompressFormat.JPEG; - - if (mimeType.equals("image/png")) - return Bitmap.CompressFormat.PNG; - else if (mimeType.equals("image/gif")) - return Bitmap.CompressFormat.PNG; - - return Bitmap.CompressFormat.JPEG; - } - - /** - * Does not replace the tag if already there. Otherwise, adds to the exif tags. - * @param tag - * @param value - */ - public void addExifTag(String tag, String value) { - if (mExifData == null) { - mExifData = new HashMap<String, String>(); - } - if (!mExifData.containsKey(tag)) { - mExifData.put(tag, value); - } else { - if (VERBOSE) Log.v(TAG, "addExifTag where the key already was there: " + tag + " = " + value); - } - } - - /** - * Return the value of the Exif tag as an int. Returns 0 on any type of error. - * @param tag - * @return - */ - public int getExifTagInt(String tag) { - if (mExifData != null) { - String tagValue = mExifData.get(tag); - if (tagValue != null) { - return Integer.parseInt(tagValue); - } - } - return 0; - } - - public boolean isReadonly() { - String mimeType = getMimeType(); - return !"image/jpeg".equals(mimeType) && !"image/png".equals(mimeType); - } - - public boolean isDrm() { - return false; - } - - /** - * Remove tag if already there. Otherwise, does nothing. - * @param tag - */ - public void removeExifTag(String tag) { - if (mExifData == null) { - mExifData = new HashMap<String, String>(); - } - mExifData.remove(tag); - } - - /** - * Replaces the tag if already there. Otherwise, adds to the exif tags. - * @param tag - * @param value - */ - public void replaceExifTag(String tag, String value) { - if (mExifData == null) { - mExifData = new HashMap<String, String>(); - } - if (!mExifData.containsKey(tag)) { - mExifData.remove(tag); - } - mExifData.put(tag, value); - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#saveModifiedImage(android.graphics.Bitmap) - */ - public IGetBoolean_cancelable saveImageContents( - final Bitmap image, - final byte [] jpegData, - final int orientation, - final boolean newFile, - final Cursor cursor) { - final class SaveImageContentsCancelable extends BaseCancelable implements IGetBoolean_cancelable { - IGetBoolean_cancelable mCurrentCancelable = null; - - SaveImageContentsCancelable() { - } - - public boolean doCancelWork() { - synchronized (this) { - if (mCurrentCancelable != null) - mCurrentCancelable.cancel(); - } - return true; - } - - public boolean get() { - try { - Bitmap thumbnail = null; - - long t1 = System.currentTimeMillis(); - Uri uri = mContainer.contentUri(mId); - synchronized (this) { - checkCanceled(); - mCurrentCancelable = compressImageToFile(image, jpegData, uri); - } - - long t2 = System.currentTimeMillis(); - if (!mCurrentCancelable.get()) - return false; - - synchronized (this) { - String filePath; - synchronized (cursor) { - cursor.moveToPosition(0); - filePath = cursor.getString(2); - } - // TODO: If thumbData is present and usable, we should call the version - // of storeThumbnail which takes a byte array, rather than re-encoding - // a new JPEG of the same dimensions. - byte [] thumbData = null; - synchronized (ImageManager.instance()) { - thumbData = (new ExifInterface(filePath)).getThumbnail(); - } - if (VERBOSE) Log.v(TAG, "for file " + filePath + " thumbData is " + thumbData + "; length " + (thumbData!=null ? thumbData.length : -1)); - if (thumbData != null) { - thumbnail = BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length); - if (VERBOSE) Log.v(TAG, "embedded thumbnail bitmap " + thumbnail.getWidth() + "/" + thumbnail.getHeight()); - } - if (thumbnail == null && image != null) { - thumbnail = image; - } - if (thumbnail == null && jpegData != null) { - thumbnail = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length); - } - } - - long t3 = System.currentTimeMillis(); - mContainer.storeThumbnail(thumbnail, Image.this.fullSizeImageId()); - long t4 = System.currentTimeMillis(); - checkCanceled(); - if (VERBOSE) Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>> rotating by " + orientation); - try { - thumbnail = rotate(thumbnail, orientation); - saveMiniThumb(thumbnail); - } catch (IOException e) { - // Ignore if unable to save thumb. - } - long t5 = System.currentTimeMillis(); - checkCanceled(); - - if (VERBOSE) Log.v(TAG, String.format("Timing data %d %d %d %d", t2-t1, t3-t2, t4-t3, t5-t4)); - return true; - } catch (CanceledException ex) { - if (VERBOSE) Log.v(TAG, "got canceled... need to cleanup"); - return false; - } finally { - /* - Cursor c = getCursor(); - synchronized (c) { - if (c.moveTo(getRow())) { - mContainer.requery(); - } - } - */ - acknowledgeCancel(); - } - } - } - return new SaveImageContentsCancelable(); - } - - private void setExifRotation(int degrees) { - try { - Cursor c = getCursor(); - String filePath; - synchronized (c) { - filePath = c.getString(mContainer.indexData()); - } - synchronized (ImageManager.instance()) { - ExifInterface exif = new ExifInterface(filePath); - if (mExifData == null) { - mExifData = exif.getAttributes(); - } - if (degrees < 0) - degrees += 360; - - int orientation = ExifInterface.ORIENTATION_NORMAL; - switch (degrees) { - case 0: - orientation = ExifInterface.ORIENTATION_NORMAL; - break; - case 90: - orientation = ExifInterface.ORIENTATION_ROTATE_90; - break; - case 180: - orientation = ExifInterface.ORIENTATION_ROTATE_180; - break; - case 270: - orientation = ExifInterface.ORIENTATION_ROTATE_270; - break; - } - - replaceExifTag(ExifInterface.TAG_ORIENTATION, Integer.toString(orientation)); - replaceExifTag("UserComment", "saveRotatedImage comment orientation: " + orientation); - exif.saveAttributes(mExifData); - exif.commitChanges(); - } - } catch (Exception ex) { - Log.e(TAG, "unable to save exif data with new orientation " + fullSizeImageUri()); - } - } - - /** - * Save the rotated image by updating the Exif "Orientation" tag. - * @param degrees - * @return - */ - public boolean rotateImageBy(int degrees) { - int newDegrees = getDegreesRotated() + degrees; - setExifRotation(newDegrees); - setDegreesRotated(newDegrees); - - // setting this to zero will force the call to checkCursor to generate fresh thumbs - mMiniThumbMagic = 0; - try { - mContainer.checkThumbnail(this, mContainer.getCursor(), this.getRow()); - } catch (IOException e) { - // Ignore inability to store mini thumbnail. - } - - return true; - } - - public Bitmap thumbBitmap() { - Bitmap bitmap = null; - Cursor c = null; - if (mContainer.mThumbUri != null) { - try { - c = mContentResolver.query( - mContainer.mThumbUri, - THUMB_PROJECTION, - Thumbnails.IMAGE_ID + "=?", - new String[] { String.valueOf(fullSizeImageId()) }, - null); - if (c != null && c.moveToFirst()) { - Uri thumbUri = ContentUris.withAppendedId(mContainer.mThumbUri, c.getLong(((ImageList)mContainer).INDEX_THUMB_ID)); - ParcelFileDescriptor pfdInput; - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inDither = false; - options.inPreferredConfig = Bitmap.Config.ARGB_8888; - pfdInput = mContentResolver.openFileDescriptor(thumbUri, "r"); - bitmap = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options); - pfdInput.close(); - } catch (FileNotFoundException ex) { - Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); - } catch (IOException ex) { - Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); - } catch (NullPointerException ex) { - // we seem to get this if the file doesn't exist anymore - Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); - } catch (OutOfMemoryError ex) { - Log.e(TAG, "failed to allocate memory for thumbnail " - + thumbUri + "; " + ex); - } - } - } catch (Exception ex) { - // sdcard removed? - return null; - } finally { - if (c != null) - c.close(); - } - } - - if (bitmap == null) { - bitmap = fullSizeBitmap(THUMBNAIL_TARGET_SIZE, false); - if (VERBOSE) { - Log.v(TAG, "no thumbnail found... storing new one for " + fullSizeImageId()); - } - bitmap = mContainer.storeThumbnail(bitmap, fullSizeImageId()); - } - - if (bitmap != null) { - bitmap = rotate(bitmap, getDegreesRotated()); - } - - long elapsed = System.currentTimeMillis(); - return bitmap; - } - - } - - final static private String sWhereClause = "(" + Images.Media.MIME_TYPE + " in (?, ?, ?))"; - final static private String[] sAcceptableImageTypes = new String[] { "image/jpeg", "image/png", "image/gif" }; - final static private String sMiniThumbIsNull = "mini_thumb_magic isnull"; + public static final String CAMERA_IMAGE_BUCKET_NAME = + Environment.getExternalStorageDirectory().toString() + + "/DCIM/Camera"; + public static final String CAMERA_IMAGE_BUCKET_ID = + getBucketId(CAMERA_IMAGE_BUCKET_NAME); - private static final String[] IMAGE_PROJECTION = new String[] { - "_id", - "_data", - ImageColumns.DATE_TAKEN, - ImageColumns.MINI_THUMB_MAGIC, - ImageColumns.ORIENTATION, - ImageColumns.MIME_TYPE - }; + public static final int MINI_THUMB_TARGET_SIZE = 96; + public static final int THUMBNAIL_TARGET_SIZE = 320; /** - * Represents an ordered collection of Image objects. - * Provides an api to add and remove an image. + * Matches code in MediaProvider.computeBucketValues. Should be a common + * function. */ - class ImageList extends BaseImageList implements IImageList { - final int INDEX_ID = indexOf(IMAGE_PROJECTION, "_id"); - final int INDEX_DATA = indexOf(IMAGE_PROJECTION, "_data"); - final int INDEX_MIME_TYPE = indexOf(IMAGE_PROJECTION, MediaColumns.MIME_TYPE); - final int INDEX_DATE_TAKEN = indexOf(IMAGE_PROJECTION, ImageColumns.DATE_TAKEN); - final int INDEX_MINI_THUMB_MAGIC = indexOf(IMAGE_PROJECTION, ImageColumns.MINI_THUMB_MAGIC); - final int INDEX_ORIENTATION = indexOf(IMAGE_PROJECTION, ImageColumns.ORIENTATION); - - final int INDEX_THUMB_ID = indexOf(THUMB_PROJECTION, BaseColumns._ID); - final int INDEX_THUMB_IMAGE_ID = indexOf(THUMB_PROJECTION, Images.Thumbnails.IMAGE_ID); - final int INDEX_THUMB_WIDTH = indexOf(THUMB_PROJECTION, Images.Thumbnails.WIDTH); - final int INDEX_THUMB_HEIGHT = indexOf(THUMB_PROJECTION, Images.Thumbnails.HEIGHT); - - boolean mIsRegistered = false; - ContentObserver mContentObserver; - DataSetObserver mDataSetObserver; - - public HashMap<String, String> getBucketIds() { - Cursor c = Images.Media.query( - mContentResolver, - mBaseUri.buildUpon().appendQueryParameter("distinct", "true").build(), - new String[] { - ImageColumns.BUCKET_DISPLAY_NAME, - ImageColumns.BUCKET_ID - }, - whereClause(), - whereClauseArgs(), - sortOrder()); - - HashMap<String, String> hash = new HashMap<String, String>(); - if (c != null && c.moveToFirst()) { - do { - hash.put(c.getString(1), c.getString(0)); - } while (c.moveToNext()); - } - return hash; - } - /** - * ImageList constructor. - * @param cr ContentResolver - */ - public ImageList(Context ctx, ContentResolver cr, Uri imageUri, Uri thumbUri, int sort, String bucketId) { - super(ctx, cr, imageUri, sort, bucketId); - mBaseUri = imageUri; - mThumbUri = thumbUri; - mSort = sort; - - mContentResolver = cr; - - mCursor = createCursor(); - if (mCursor == null) { - Log.e(TAG, "unable to create image cursor for " + mBaseUri); - throw new UnsupportedOperationException(); - } - - if (VERBOSE) { - Log.v(TAG, "for " + mBaseUri.toString() + " got cursor " + mCursor + " with length " + (mCursor != null ? mCursor.getCount() : "-1")); - } - - final Runnable updateRunnable = new Runnable() { - public void run() { - // handling these external updates is causing ANR problems that are unresolved. - // For now ignore them since there shouldn't be anyone modifying the database on the fly. - if (true) - return; - - synchronized (mCursor) { - requery(); - } - if (mListener != null) - mListener.onChange(ImageList.this); - } - }; - - mContentObserver = new ContentObserver(null) { - @Override - public boolean deliverSelfNotifications() { - return false; - } - - @Override - public void onChange(boolean selfChange) { - if (VERBOSE) Log.v(TAG, "MyContentObserver.onChange; selfChange == " + selfChange); - updateRunnable.run(); - } - }; - - mDataSetObserver = new DataSetObserver() { - @Override - public void onChanged() { - if (VERBOSE) Log.v(TAG, "MyDataSetObserver.onChanged"); -// updateRunnable.run(); - } - - @Override - public void onInvalidated() { - if (VERBOSE) Log.v(TAG, "MyDataSetObserver.onInvalidated: " + mCursorDeactivated); - } - }; - - registerObservers(); - } - - private void registerObservers() { - if (mIsRegistered) - return; - - mCursor.registerContentObserver(mContentObserver); - mCursor.registerDataSetObserver(mDataSetObserver); - mIsRegistered = true; - } - - private void unregisterObservers() { - if (!mIsRegistered) - return; - - mCursor.unregisterContentObserver(mContentObserver); - mCursor.unregisterDataSetObserver(mDataSetObserver); - mIsRegistered = false; - } - - public void deactivate() { - super.deactivate(); - unregisterObservers(); - } - - protected void activateCursor() { - super.activateCursor(); - registerObservers(); - } - - protected String whereClause() { - if (mBucketId != null) { - return sWhereClause + " and " + Images.Media.BUCKET_ID + " = '" + mBucketId + "'"; - } else { - return sWhereClause; - } - } - - protected String[] whereClauseArgs() { - return sAcceptableImageTypes; - } - - protected Cursor createCursor() { - Cursor c = - Images.Media.query( - mContentResolver, - mBaseUri, - IMAGE_PROJECTION, - whereClause(), - whereClauseArgs(), - sortOrder()); - if (VERBOSE) - Log.v(TAG, "createCursor got cursor with count " + (c == null ? -1 : c.getCount())); - return c; - } - - protected int indexOrientation() { return INDEX_ORIENTATION; } - protected int indexDateTaken() { return INDEX_DATE_TAKEN; } - protected int indexDescription() { return -1; } - protected int indexMimeType() { return INDEX_MIME_TYPE; } - protected int indexData() { return INDEX_DATA; } - protected int indexId() { return INDEX_ID; } - protected int indexLatitude() { return -1; } - protected int indexLongitude() { return -1; } - protected int indexMiniThumbId() { return INDEX_MINI_THUMB_MAGIC; } - - protected int indexPicasaWeb() { return -1; } - protected int indexPrivate() { return -1; } - protected int indexTitle() { return -1; } - protected int indexDisplayName() { return -1; } - protected int indexThumbId() { return INDEX_THUMB_ID; } - - @Override - protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) { - return new Image(id, miniThumbId, mContentResolver, this, index, rotation); - } - - protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfd, BitmapFactory.Options options) { - Bitmap b = null; - - try { - if (pfd == null) - pfd = makeInputStream(uri); - - if (pfd == null) - return null; - - if (options == null) - options = new BitmapFactory.Options(); - - java.io.FileDescriptor fd = pfd.getFileDescriptor(); - options.inSampleSize = 1; - if (targetWidthHeight != -1) { - options.inJustDecodeBounds = true; - long t1 = System.currentTimeMillis(); - BitmapFactory.decodeFileDescriptor(fd, null, options); - long t2 = System.currentTimeMillis(); - if (options.mCancel || options.outWidth == -1 || options.outHeight == -1) { - return null; - } - options.inSampleSize = computeSampleSize(options, targetWidthHeight); - options.inJustDecodeBounds = false; - } - - options.inDither = false; - options.inPreferredConfig = Bitmap.Config.ARGB_8888; - long t1 = System.currentTimeMillis(); - b = BitmapFactory.decodeFileDescriptor(fd, null, options); - long t2 = System.currentTimeMillis(); - if (VERBOSE) { - Log.v(TAG, "A: got bitmap " + b + " with sampleSize " + options.inSampleSize + " took " + (t2-t1)); - } - } catch (OutOfMemoryError ex) { - if (VERBOSE) Log.v(TAG, "got oom exception " + ex); - return null; - } finally { - try { - pfd.close(); - } catch (IOException ex) { - } - } - return b; - } - - private ParcelFileDescriptor makeInputStream(Uri uri) { - try { - return mContentResolver.openFileDescriptor(uri, "r"); - } catch (IOException ex) { - return null; - } - } - - private String sortOrder() { - // add id to the end so that we don't ever get random sorting - // which could happen, I suppose, if the first two values were - // duplicated - String ascending = (mSort == SORT_ASCENDING ? " ASC" : " DESC"); - return - Images.Media.DATE_TAKEN + ascending + "," + - Images.Media._ID + ascending; - } - + public static String getBucketId(String path) { + return String.valueOf(path.toLowerCase().hashCode()); } /** - * Represents an ordered collection of Image objects from the DRM provider. + * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be + * imported. This is a temporary fix for bug#1655552. */ - class DrmImageList extends ImageList implements IImageList { - private final String[] DRM_IMAGE_PROJECTION = new String[] { - DrmStore.Audio._ID, - DrmStore.Audio.DATA, - DrmStore.Audio.MIME_TYPE, - }; - - final int INDEX_ID = indexOf(DRM_IMAGE_PROJECTION, DrmStore.Audio._ID); - final int INDEX_MIME_TYPE = indexOf(DRM_IMAGE_PROJECTION, DrmStore.Audio.MIME_TYPE); - - public DrmImageList(Context ctx, ContentResolver cr, Uri imageUri, int sort, String bucketId) { - super(ctx, cr, imageUri, null, sort, bucketId); - } - - protected Cursor createCursor() { - return mContentResolver.query(mBaseUri, DRM_IMAGE_PROJECTION, null, null, sortOrder()); - } - - @Override - public void checkThumbnails(ThumbCheckCallback cb, int totalCount) { - // do nothing - } - - @Override - public long checkThumbnail(BaseImage existingImage, Cursor c, int i) { - return 0; - } - - class DrmImage extends Image { - protected DrmImage(long id, ContentResolver cr, BaseImageList container, int cursorRow) { - super(id, 0, cr, container, cursorRow, 0); - } - - public boolean isDrm() { - return true; - } - - public boolean isReadonly() { - return true; - } - - public Bitmap miniThumbBitmap() { - return fullSizeBitmap(MINI_THUMB_TARGET_SIZE); - } - - public Bitmap thumbBitmap() { - return fullSizeBitmap(THUMBNAIL_TARGET_SIZE); - } - - public String getDisplayName() { - return getTitle(); - } - } - - @Override - protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) { - return new DrmImage(id, mContentResolver, this, index); - } - - protected int indexOrientation() { return -1; } - protected int indexDateTaken() { return -1; } - protected int indexDescription() { return -1; } - protected int indexMimeType() { return -1; } - protected int indexId() { return -1; } - protected int indexLatitude() { return -1; } - protected int indexLongitude() { return -1; } - protected int indexMiniThumbId() { return -1; } - protected int indexPicasaWeb() { return -1; } - protected int indexPrivate() { return -1; } - protected int indexTitle() { return -1; } - protected int indexDisplayName() { return -1; } - protected int indexThumbId() { return -1; } - - // TODO review this probably should be based on DATE_TAKEN same as images - private String sortOrder() { - String ascending = (mSort == SORT_ASCENDING ? " ASC" : " DESC"); - return - DrmStore.Images.TITLE + ascending + "," + - DrmStore.Images._ID; - } - } - - class ImageListUber implements IImageList { - private IImageList [] mSubList; - private int mSort; - private IImageList.OnChange mListener = null; - Handler mHandler; - - // This is an array of Longs wherein each Long consists of - // two components. The first component indicates the number of - // consecutive entries that belong to a given sublist. - // The second component indicates which sublist we're referring - // to (an int which is used to index into mSubList). - ArrayList<Long> mSkipList = null; - - int [] mSkipCounts = null; - - public HashMap<String, String> getBucketIds() { - HashMap<String, String> hashMap = new HashMap<String, String>(); - for (IImageList list: mSubList) { - hashMap.putAll(list.getBucketIds()); - } - return hashMap; - } - - public ImageListUber(IImageList [] sublist, int sort) { - mSubList = sublist.clone(); - mSort = sort; - - if (mListener != null) { - for (IImageList list: sublist) { - list.setOnChangeListener(new OnChange() { - public void onChange(IImageList list) { - if (mListener != null) { - mListener.onChange(ImageListUber.this); - } - } - }, mHandler); - } - } - } - - public void checkThumbnails(ThumbCheckCallback cb, int totalThumbnails) { - for (IImageList i : mSubList) { - int count = i.getCount(); - i.checkThumbnails(cb, totalThumbnails); - totalThumbnails -= count; - } - } - - public void commitChanges() { - final IImageList sublist[] = mSubList; - final int length = sublist.length; - for (int i = 0; i < length; i++) - sublist[i].commitChanges(); - } - - public void deactivate() { - final IImageList sublist[] = mSubList; - final int length = sublist.length; - int pos = -1; - while (++pos < length) { - IImageList sub = sublist[pos]; - sub.deactivate(); - } - } - - public int getCount() { - final IImageList sublist[] = mSubList; - final int length = sublist.length; - int count = 0; - for (int i = 0; i < length; i++) - count += sublist[i].getCount(); - return count; - } - - public boolean isEmpty() { - final IImageList sublist[] = mSubList; - final int length = sublist.length; - for (int i = 0; i < length; i++) { - if (! sublist[i].isEmpty()) { - return false; - } - } - return true; - } - - // mSkipCounts is used to tally the counts as we traverse - // the mSkipList. It's a member variable only so that - // we don't have to allocate each time through. Otherwise - // it could just as easily be a local. - - public synchronized IImage getImageAt(int index) { - if (index < 0 || index > getCount()) - throw new IndexOutOfBoundsException("index " + index + " out of range max is " + getCount()); - - // first make sure our allocations are in order - if (mSkipCounts == null || mSubList.length > mSkipCounts.length) - mSkipCounts = new int[mSubList.length]; - - if (mSkipList == null) - mSkipList = new ArrayList<Long>(); - - // zero out the mSkipCounts since that's only used for the - // duration of the function call - for (int i = 0; i < mSubList.length; i++) - mSkipCounts[i] = 0; - - // a counter of how many images we've skipped in - // trying to get to index. alternatively we could - // have decremented index but, alas, I liked this - // way more. - int skipCount = 0; - - // scan the existing mSkipList to see if we've computed - // enough to just return the answer - for (int i = 0; i < mSkipList.size(); i++) { - long v = mSkipList.get(i); - - int offset = (int) (v & 0xFFFF); - int which = (int) (v >> 32); - - if (skipCount + offset > index) { - int subindex = mSkipCounts[which] + (index - skipCount); - IImage img = mSubList[which].getImageAt(subindex); - return img; - } - - skipCount += offset; - mSkipCounts[which] += offset; - } - - // if we get here we haven't computed the answer for - // "index" yet so keep computing. This means running - // through the list of images and either modifying the - // last entry or creating a new one. - long count = 0; - while (true) { - long maxTimestamp = mSort == SORT_ASCENDING ? Long.MAX_VALUE : Long.MIN_VALUE; - int which = -1; - for (int i = 0; i < mSubList.length; i++) { - int pos = mSkipCounts[i]; - IImageList list = mSubList[i]; - if (pos < list.getCount()) { - IImage image = list.getImageAt(pos); - // this should never be null but sometimes the database is - // causing problems and it is null - if (image != null) { - long timestamp = image.getDateTaken(); - if (mSort == SORT_ASCENDING ? (timestamp < maxTimestamp) : (timestamp > maxTimestamp)) { - maxTimestamp = timestamp; - which = i; - } - } - } - } - - if (which == -1) { - if (VERBOSE) Log.v(TAG, "which is -1, returning null"); - return null; - } - - boolean done = false; - count = 1; - if (mSkipList.size() > 0) { - int pos = mSkipList.size() - 1; - long oldEntry = mSkipList.get(pos); - if ((oldEntry >> 32) == which) { - long newEntry = oldEntry + 1; - mSkipList.set(pos, newEntry); - done = true; - } - } - if (!done) { - long newEntry = ((long)which << 32) | count; - if (VERBOSE) { - Log.v(TAG, "new entry is " + Long.toHexString(newEntry)); - } - mSkipList.add(newEntry); - } - - if (skipCount++ == index) { - return mSubList[which].getImageAt(mSkipCounts[which]); - } - mSkipCounts[which] += 1; - } - } - - public IImage getImageForUri(Uri uri) { - // TODO perhaps we can preflight the base of the uri - // against each sublist first - for (int i = 0; i < mSubList.length; i++) { - IImage img = mSubList[i].getImageForUri(uri); - if (img != null) - return img; - } - return null; - } - - /** - * Modify the skip list when an image is deleted by finding - * the relevant entry in mSkipList and decrementing the - * counter. This is simple because deletion can never - * cause change the order of images. - */ - public void modifySkipCountForDeletedImage(int index) { - int skipCount = 0; - - for (int i = 0; i < mSkipList.size(); i++) { - long v = mSkipList.get(i); - - int offset = (int) (v & 0xFFFF); - int which = (int) (v >> 32); - - if (skipCount + offset > index) { - mSkipList.set(i, v-1); - break; - } - - skipCount += offset; - } - } - - public boolean removeImage(IImage image) { - IImageList parent = image.getContainer(); - int pos = -1; - int baseIndex = 0; - while (++pos < mSubList.length) { - IImageList sub = mSubList[pos]; - if (sub == parent) { - if (sub.removeImage(image)) { - modifySkipCountForDeletedImage(baseIndex); - return true; - } else { - break; - } - } - baseIndex += sub.getCount(); - } - return false; - } - - public void removeImageAt(int index) { - IImage img = getImageAt(index); - if (img != null) { - IImageList list = img.getContainer(); - if (list != null) { - list.removeImage(img); - modifySkipCountForDeletedImage(index); - } - } - } - - public void removeOnChangeListener(OnChange changeCallback) { - if (changeCallback == mListener) - mListener = null; - } - - public void setOnChangeListener(OnChange changeCallback, Handler h) { - mListener = changeCallback; - mHandler = h; - } - - } - - public static abstract class SimpleBaseImage implements IImage { - public void commitChanges() { - throw new UnsupportedOperationException(); - } - - public InputStream fullSizeImageData() { - throw new UnsupportedOperationException(); - } - - public long fullSizeImageId() { - return 0; - } - - public Uri fullSizeImageUri() { - throw new UnsupportedOperationException(); - } - - public IImageList getContainer() { - return null; - } - - public long getDateTaken() { - return 0; - } - - public String getMimeType() { - throw new UnsupportedOperationException(); - } - - public String getDescription() { - throw new UnsupportedOperationException(); - } - - public boolean getIsPrivate() { - throw new UnsupportedOperationException(); - } - - public double getLatitude() { - return 0D; - } - - public double getLongitude() { - return 0D; - } - - public String getTitle() { - throw new UnsupportedOperationException(); - } - - public String getDisplayName() { - throw new UnsupportedOperationException(); - } - - public String getPicasaId() { - return null; - } - - public int getRow() { - throw new UnsupportedOperationException(); - } - - public int getHeight() { - return 0; - } - - public int getWidth() { - return 0; - } - - public boolean hasLatLong() { - return false; - } - - public boolean isReadonly() { - return true; - } - - public boolean isDrm() { - return false; - } - - public void onRemove() { - throw new UnsupportedOperationException(); - } - - public boolean rotateImageBy(int degrees) { - return false; - } - - public void setDescription(String description) { - throw new UnsupportedOperationException(); - } - - public void setIsPrivate(boolean isPrivate) { - throw new UnsupportedOperationException(); - } - - public void setName(String name) { - throw new UnsupportedOperationException(); - } - - public void setPicasaId(long id) { - } - - public void setPicasaId(String id) { - } - - public Uri thumbUri() { - throw new UnsupportedOperationException(); - } - } - - class SingleImageList extends BaseImageList implements IImageList { - private IImage mSingleImage; - private ContentResolver mContentResolver; - private Uri mUri; - - class UriImage extends SimpleBaseImage { - - UriImage() { - } - - public String getDataPath() { - return mUri.getPath(); - } - - InputStream getInputStream() { - try { - if (mUri.getScheme().equals("file")) { - String path = mUri.getPath(); - if (VERBOSE) - Log.v(TAG, "path is " + path); - return new java.io.FileInputStream(mUri.getPath()); - } else { - return mContentResolver.openInputStream(mUri); - } - } catch (FileNotFoundException ex) { - return null; - } - } - - ParcelFileDescriptor getPFD() { - try { - if (mUri.getScheme().equals("file")) { - String path = mUri.getPath(); - if (VERBOSE) - Log.v(TAG, "path is " + path); - return ParcelFileDescriptor.open(new File(path), ParcelFileDescriptor.MODE_READ_ONLY); - } else { - return mContentResolver.openFileDescriptor(mUri, "r"); - } - } catch (FileNotFoundException ex) { - return null; - } - } - - /* (non-Javadoc) - * @see com.android.camera.ImageManager.IImage#fullSizeBitmap(int) - */ - public Bitmap fullSizeBitmap(int targetWidthHeight) { - try { - ParcelFileDescriptor pfdInput = getPFD(); - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options); - - if (targetWidthHeight != -1) - options.inSampleSize = computeSampleSize(options, targetWidthHeight); - - options.inJustDecodeBounds = false; - options.inDither = false; - options.inPreferredConfig = Bitmap.Config.ARGB_8888; - - Bitmap b = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options); - if (VERBOSE) { - Log.v(TAG, "B: got bitmap " + b + " with sampleSize " + options.inSampleSize); - } - pfdInput.close(); - return b; - } catch (Exception ex) { - Log.e(TAG, "got exception decoding bitmap " + ex.toString()); - return null; - } - } - - public IGetBitmap_cancelable fullSizeBitmap_cancelable(final int targetWidthOrHeight) { - final class LoadBitmapCancelable extends BaseCancelable implements IGetBitmap_cancelable { - ParcelFileDescriptor pfdInput; - BitmapFactory.Options mOptions = new BitmapFactory.Options(); - long mCancelInitiationTime; - - public LoadBitmapCancelable(ParcelFileDescriptor pfd) { - pfdInput = pfd; - } - - public boolean doCancelWork() { - if (VERBOSE) - Log.v(TAG, "requesting bitmap load cancel"); - mCancelInitiationTime = System.currentTimeMillis(); - mOptions.requestCancelDecode(); - return true; - } - - public Bitmap get() { - try { - Bitmap b = makeBitmap(targetWidthOrHeight, fullSizeImageUri(), pfdInput, mOptions); - if (b == null && mCancelInitiationTime != 0) { - if (VERBOSE) - Log.v(TAG, "cancel returned null bitmap -- took " + (System.currentTimeMillis()-mCancelInitiationTime)); - } - if (VERBOSE) Log.v(TAG, "b is " + b); - return b; - } catch (Exception ex) { - return null; - } finally { - acknowledgeCancel(); - } - } - } - - try { - ParcelFileDescriptor pfdInput = getPFD(); - if (pfdInput == null) - return null; - if (VERBOSE) Log.v(TAG, "inputStream is " + pfdInput); - return new LoadBitmapCancelable(pfdInput); - } catch (UnsupportedOperationException ex) { - return null; - } - } - - @Override - public Uri fullSizeImageUri() { - return mUri; - } - - @Override - public InputStream fullSizeImageData() { - return getInputStream(); - } - - public long imageId() { - return 0; - } - - public Bitmap miniThumbBitmap() { - return thumbBitmap(); - } - - @Override - public String getTitle() { - return mUri.toString(); - } - - @Override - public String getDisplayName() { - return getTitle(); - } - - @Override - public String getDescription() { - return ""; - } - - public Bitmap thumbBitmap() { - Bitmap b = fullSizeBitmap(THUMBNAIL_TARGET_SIZE); - if (b != null) { - Matrix m = new Matrix(); - float scale = Math.min(1F, THUMBNAIL_TARGET_SIZE / (float) b.getWidth()); - m.setScale(scale, scale); - Bitmap scaledBitmap = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true); - return scaledBitmap; - } else { - return null; - } - } - - private BitmapFactory.Options snifBitmapOptions() { - ParcelFileDescriptor input = getPFD(); - if (input == null) - return null; - try { - Uri uri = fullSizeImageUri(); - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options); - return options; - } finally { - try { - if (input != null) { - input.close(); - } - } catch (IOException ex) { - } - } - } - - @Override - public String getMimeType() { - BitmapFactory.Options options = snifBitmapOptions(); - return (options!=null) ? options.outMimeType : ""; - } - - @Override - public int getHeight() { - BitmapFactory.Options options = snifBitmapOptions(); - return (options!=null) ? options.outHeight : 0; - } - - @Override - public int getWidth() { - BitmapFactory.Options options = snifBitmapOptions(); - return (options!=null) ? options.outWidth : 0; - } - } - - public SingleImageList(ContentResolver cr, Uri uri) { - super(null, cr, uri, ImageManager.SORT_ASCENDING, null); - mContentResolver = cr; - mUri = uri; - mSingleImage = new UriImage(); - } - - public HashMap<String, String> getBucketIds() { - throw new UnsupportedOperationException(); - } - - public void deactivate() { - // nothing to do here - } - - public int getCount() { - return 1; - } - - public boolean isEmpty() { - return false; - } - - public IImage getImageAt(int i) { - if (i == 0) - return mSingleImage; - - return null; - } - - public IImage getImageForUri(Uri uri) { - if (uri.equals(mUri)) - return mSingleImage; - else - return null; - } - - public IImage getImageWithId(long id) { - throw new UnsupportedOperationException(); - } - - @Override - protected int indexOrientation() { - return -1; - } - - @Override - protected int indexDateTaken() { - return -1; - } - - @Override - protected int indexMimeType() { - return -1; - } - - @Override - protected int indexDescription() { - return -1; - } - - @Override - protected int indexId() { - return -1; - } - - @Override - protected int indexData() { - return -1; - } - - @Override - protected int indexLatitude() { - return -1; - } - - @Override - protected int indexLongitude() { - return -1; - } - - @Override - protected int indexMiniThumbId() { - return -1; - } - - @Override - protected int indexPicasaWeb() { - return -1; - } - - @Override - protected int indexPrivate() { - return -1; - } - - @Override - protected int indexTitle() { - return -1; - } - - @Override - protected int indexDisplayName() { - return -1; - } - - @Override - protected int indexThumbId() { - return -1; - } - - private InputStream makeInputStream(Uri uri) { - InputStream input = null; - try { - input = mContentResolver.openInputStream(uri); - return input; - } catch (IOException ex) { - return null; - } - } - - @Override - protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options) { - Bitmap b = null; - - try { - if (options == null) - options = new BitmapFactory.Options(); - options.inSampleSize = 1; - - if (targetWidthHeight != -1) { - options.inJustDecodeBounds = true; - BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options); - - options.inSampleSize = computeSampleSize(options, targetWidthHeight); - options.inJustDecodeBounds = false; - } - b = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options); - if (VERBOSE) { - Log.v(TAG, "C: got bitmap " + b + " with sampleSize " + options.inSampleSize); - } - } catch (OutOfMemoryError ex) { - if (VERBOSE) Log.v(TAG, "got oom exception " + ex); - return null; - } finally { - try { - pfdInput.close(); - } catch (IOException ex) { - } - } - return b; - } - } - - class ThreadSafeOutputStream extends OutputStream { - java.io.OutputStream mDelegateStream; - boolean mClosed; - - public ThreadSafeOutputStream(OutputStream delegate) { - mDelegateStream = delegate; - } - - @Override - synchronized public void close() throws IOException { - try { - mClosed = true; - mDelegateStream.close(); - } catch (IOException ex) { - - } - } - - @Override - synchronized public void flush() throws IOException { - super.flush(); - } - - @Override - public void write(byte[] b, int offset, int length) throws IOException { - /* - mDelegateStream.write(b, offset, length); - return; - */ - while (length > 0) { - synchronized (this) { - if (mClosed) - return; - - int writeLength = Math.min(8192, length); - mDelegateStream.write(b, offset, writeLength); - offset += writeLength; - length -= writeLength; - } - } - } - - @Override - synchronized public void write(int oneByte) throws IOException { - if (mClosed) - return; - mDelegateStream.write(oneByte); + public static void ensureOSXCompatibleFolder() { + File nnnAAAAA = new File( + Environment.getExternalStorageDirectory().toString() + + "/DCIM/100ANDRO"); + if ((!nnnAAAAA.exists()) && (!nnnAAAAA.mkdir())) { + Log.e(TAG, "create NNNAAAAA file: " + nnnAAAAA.getPath() + + " failed"); } } - class VideoList extends BaseImageList implements IImageList { - private final String[] sProjection = new String[] { - Video.Media._ID, - Video.Media.DATA, - Video.Media.DATE_TAKEN, - Video.Media.TITLE, - Video.Media.DISPLAY_NAME, - Video.Media.DESCRIPTION, - Video.Media.IS_PRIVATE, - Video.Media.TAGS, - Video.Media.CATEGORY, - Video.Media.LANGUAGE, - Video.Media.LATITUDE, - Video.Media.LONGITUDE, - Video.Media.MINI_THUMB_MAGIC, - Video.Media.MIME_TYPE, - }; - - final int INDEX_ID = indexOf(sProjection, Video.Media._ID); - final int INDEX_DATA = indexOf(sProjection, Video.Media.DATA); - final int INDEX_DATE_TAKEN = indexOf(sProjection, Video.Media.DATE_TAKEN); - final int INDEX_TITLE = indexOf(sProjection, Video.Media.TITLE); - final int INDEX_DISPLAY_NAME = indexOf(sProjection, Video.Media.DISPLAY_NAME); - final int INDEX_MIME_TYPE = indexOf(sProjection, Video.Media.MIME_TYPE); - final int INDEX_DESCRIPTION = indexOf(sProjection, Video.Media.DESCRIPTION); - final int INDEX_PRIVATE = indexOf(sProjection, Video.Media.IS_PRIVATE); - final int INDEX_TAGS = indexOf(sProjection, Video.Media.TAGS); - final int INDEX_CATEGORY = indexOf(sProjection, Video.Media.CATEGORY); - final int INDEX_LANGUAGE = indexOf(sProjection, Video.Media.LANGUAGE); - final int INDEX_LATITUDE = indexOf(sProjection, Video.Media.LATITUDE); - final int INDEX_LONGITUDE = indexOf(sProjection, Video.Media.LONGITUDE); - final int INDEX_MINI_THUMB_MAGIC = indexOf(sProjection, Video.Media.MINI_THUMB_MAGIC); - final int INDEX_THUMB_ID = indexOf(sProjection, BaseColumns._ID); - - public VideoList(Context ctx, ContentResolver cr, Uri uri, Uri thumbUri, - int sort, String bucketId) { - super(ctx, cr, uri, sort, bucketId); - - mCursor = createCursor(); - if (mCursor == null) { - Log.e(TAG, "unable to create video cursor for " + mBaseUri); - throw new UnsupportedOperationException(); - } - - if (Config.LOGV) { - Log.v(TAG, "for " + mUri.toString() + " got cursor " + mCursor + " with length " - + (mCursor != null ? mCursor.getCount() : -1)); - } - - if (mCursor == null) { - throw new UnsupportedOperationException(); - } - if (mCursor != null && mCursor.moveToFirst()) { - int row = 0; - do { - long imageId = mCursor.getLong(indexId()); - long dateTaken = mCursor.getLong(indexDateTaken()); - long miniThumbId = mCursor.getLong(indexMiniThumbId()); - mCache.put(imageId, new VideoObject(imageId, miniThumbId, mContentResolver, - this, dateTaken, row++)); - } while (mCursor.moveToNext()); - } + public static void debugWhere(String tag, String msg) { + Exception ex = new Exception(); + if (msg != null) { + Log.v(tag, msg); } - - public HashMap<String, String> getBucketIds() { - Cursor c = Images.Media.query( - mContentResolver, - mBaseUri.buildUpon().appendQueryParameter("distinct", "true").build(), - new String[] { - VideoColumns.BUCKET_DISPLAY_NAME, - VideoColumns.BUCKET_ID - }, - whereClause(), - whereClauseArgs(), - sortOrder()); - - HashMap<String, String> hash = new HashMap<String, String>(); - if (c != null && c.moveToFirst()) { - do { - hash.put(c.getString(1), c.getString(0)); - } while (c.moveToNext()); - } - return hash; - } - - protected String whereClause() { - if (mBucketId != null) { - return Images.Media.BUCKET_ID + " = '" + mBucketId + "'"; + boolean first = true; + for (StackTraceElement s : ex.getStackTrace()) { + if (first) { + first = false; } else { - return null; + Log.v(tag, s.toString()); } } - - protected String[] whereClauseArgs() { - return null; - } - - @Override - protected String thumbnailWhereClause() { - return sMiniThumbIsNull; - } - - @Override - protected String[] thumbnailWhereClauseArgs() { - return null; - } - - protected Cursor createCursor() { - Cursor c = - Images.Media.query( - mContentResolver, - mBaseUri, - sProjection, - whereClause(), - whereClauseArgs(), - sortOrder()); - if (VERBOSE) - Log.v(TAG, "createCursor got cursor with count " + (c == null ? -1 : c.getCount())); - return c; - } - - protected int indexOrientation() { return -1; } - protected int indexDateTaken() { return INDEX_DATE_TAKEN; } - protected int indexDescription() { return INDEX_DESCRIPTION; } - protected int indexMimeType() { return INDEX_MIME_TYPE; } - protected int indexData() { return INDEX_DATA; } - protected int indexId() { return INDEX_ID; } - protected int indexLatitude() { return INDEX_LATITUDE; } - protected int indexLongitude() { return INDEX_LONGITUDE; } - protected int indexMiniThumbId() { return INDEX_MINI_THUMB_MAGIC; } - protected int indexPicasaWeb() { return -1; } - protected int indexPrivate() { return INDEX_PRIVATE; } - protected int indexTitle() { return INDEX_TITLE; } - protected int indexDisplayName() { return -1; } - protected int indexThumbId() { return INDEX_THUMB_ID; } - - @Override - protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, - long timestamp, int index, int rotation) { - return new VideoObject(id, miniThumbId, mContentResolver, this, timestamp, index); - } - - @Override - protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, - BitmapFactory.Options options) { - MediaPlayer mp = new MediaPlayer(); - Bitmap thumbnail = sDefaultThumbnail; - try { - mp.setDataSource(mContext, uri); -// int duration = mp.getDuration(); -// int at = duration > 2000 ? 1000 : duration / 2; - int at = 1000; - thumbnail = mp.getFrameAt(at); - if (Config.LOGV) { - if ( thumbnail != null) { - Log.v(TAG, "getFrameAt @ " + at + " returned " + thumbnail + "; " + - thumbnail.getWidth() + " " + thumbnail.getHeight()); - } else { - Log.v(TAG, "getFrame @ " + at + " failed for " + uri); - } - } - } catch (IOException ex) { - } catch (IllegalArgumentException ex) { - } catch (SecurityException ex) { - } finally { - mp.release(); - } - return thumbnail; - } - - - private String sortOrder() { - return Video.Media.DATE_TAKEN + (mSort == SORT_ASCENDING ? " ASC " : " DESC"); - } } - private final static Bitmap sDefaultThumbnail = Bitmap.createBitmap(32, 32, Bitmap.Config.RGB_565); - - /** - * Represents a particular video and provides access - * to the underlying data and two thumbnail bitmaps - * as well as other information such as the id, and - * the path to the actual video data. - */ - class VideoObject extends BaseImage implements IImage { - /** - * Constructor. - * - * @param id the image id of the image - * @param cr the content resolver - */ - protected VideoObject(long id, long miniThumbId, ContentResolver cr, VideoList container, - long dateTaken, int row) { - super(id, miniThumbId, cr, container, row); - } - - protected Bitmap.CompressFormat compressionType() { - return Bitmap.CompressFormat.JPEG; - } - - @Override - public boolean equals(Object other) { - if (other == null) - return false; - if (!(other instanceof VideoObject)) - return false; - - return fullSizeImageUri().equals(((VideoObject)other).fullSizeImageUri()); - } - - public String getDataPath() { - String path = null; - Cursor c = getCursor(); - synchronized (c) { - if (c.moveToPosition(getRow())) { - int column = ((VideoList)getContainer()).indexData(); - if (column >= 0) - path = c.getString(column); - } - } - return path; - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#fullSizeBitmap() - */ - public Bitmap fullSizeBitmap(int targetWidthHeight) { - return sNoImageBitmap; - } - - public IGetBitmap_cancelable fullSizeBitmap_cancelable(int targetWidthHeight) { - return null; - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#fullSizeImageData() - */ - public InputStream fullSizeImageData() { - try { - InputStream input = mContentResolver.openInputStream( - fullSizeImageUri()); - return input; - } catch (IOException ex) { - return null; - } - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#fullSizeImageId() - */ - public long fullSizeImageId() { - return mId; - } - - public String getCategory() { - return getStringEntry(((VideoList)mContainer).INDEX_CATEGORY); - } - - public int getHeight() { - return 0; - } - - public String getLanguage() { - return getStringEntry(((VideoList)mContainer).INDEX_LANGUAGE); - } - - public String getPicasaId() { - return null; - } - - private String getStringEntry(int entryName) { - String entry = null; - Cursor c = getCursor(); - synchronized(c) { - if (c.moveToPosition(getRow())) { - entry = c.getString(entryName); - } - } - return entry; - } - - public String getTags() { - return getStringEntry(((VideoList)mContainer).INDEX_TAGS); - } - - public int getWidth() { - return 0; - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#imageId() - */ - public long imageId() { - return mId; - } - - public boolean isReadonly() { - return false; - } - - public boolean isDrm() { - return false; - } - - public boolean rotateImageBy(int degrees) { - return false; - } - - public void setCategory(String category) { - setStringEntry(category, ((VideoList)mContainer).INDEX_CATEGORY); - } - - public void setLanguage(String language) { - setStringEntry(language, ((VideoList)mContainer).INDEX_LANGUAGE); - } - - private void setStringEntry(String entry, int entryName) { - Cursor c = getCursor(); - synchronized (c) { - if (c.moveToPosition(getRow())) { - c.updateString(entryName, entry); - } - } - } - - public void setTags(String tags) { - setStringEntry(tags, ((VideoList)mContainer).INDEX_TAGS); - } - - /* (non-Javadoc) - * @see com.android.camera.IImage#thumb1() - */ - public Bitmap thumbBitmap() { - return fullSizeBitmap(320); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("" + mId); - return sb.toString(); - } - } - - private final static Bitmap sNoImageBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); - - /* - * How much quality to use when storing the thumbnail. - */ - private static ImageManager sInstance = null; - private static final int MINI_THUMB_TARGET_SIZE = 96; - private static final int THUMBNAIL_TARGET_SIZE = 320; - - private static final String[] THUMB_PROJECTION = new String[] { - BaseColumns._ID, // 0 - Images.Thumbnails.IMAGE_ID, // 1 - Images.Thumbnails.WIDTH, - Images.Thumbnails.HEIGHT - }; - - private static Uri sStorageURI = Images.Media.EXTERNAL_CONTENT_URI; - - private static Uri sThumbURI = Images.Thumbnails.EXTERNAL_CONTENT_URI; - - private static Uri sVideoStorageURI = Uri.parse("content://media/external/video/media"); - - private static Uri sVideoThumbURI = Uri.parse("content://media/external/video/thumbnails"); - /** - * Returns an ImageList object that contains - * all of the images. - * @param cr - * @param location - * @param includeImages - * @param includeVideo - * @return the singleton ImageList - */ - static final public int SORT_ASCENDING = 1; - - static final public int SORT_DESCENDING = 2; - - static final public int INCLUDE_IMAGES = (1 << 0); - static final public int INCLUDE_DRM_IMAGES = (1 << 1); - static final public int INCLUDE_VIDEOS = (1 << 2); - - static public DataLocation getDefaultDataLocation() { + public static DataLocation getDefaultDataLocation() { return DataLocation.EXTERNAL; } - private static int indexOf(String [] array, String s) { - for (int i = 0; i < array.length; i++) { - if (array[i].equals(s)) { - return i; - } - } - return -1; - } - /** * Returns the singleton instance of the ImageManager. * @return the ImageManager instance. @@ -3654,105 +155,32 @@ public class ImageManager { return sInstance; } - /** - * Creates a byte[] for a given bitmap of the desired size. Recycles the input bitmap. - */ - static public byte[] miniThumbData(Bitmap source) { - if (source == null) - return null; - - Bitmap miniThumbnail = extractMiniThumb(source, MINI_THUMB_TARGET_SIZE, - MINI_THUMB_TARGET_SIZE); - java.io.ByteArrayOutputStream miniOutStream = new java.io.ByteArrayOutputStream(); - miniThumbnail.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream); - miniThumbnail.recycle(); - - try { - miniOutStream.close(); - byte [] data = miniOutStream.toByteArray(); - return data; - } catch (java.io.IOException ex) { - Log.e(TAG, "got exception ex " + ex); - } - return null; - } - - /** - * Creates a centered bitmap of the desired size. Recycles the input. - * @param source - * @return - */ - static public Bitmap extractMiniThumb(Bitmap source, int width, int height) { - return extractMiniThumb(source, width, height, true); - } - - static public Bitmap extractMiniThumb(Bitmap source, int width, int height, - boolean recycle) { - if (source == null) { - return null; - } - - float scale; - if (source.getWidth() < source.getHeight()) { - scale = width / (float)source.getWidth(); - } else { - scale = height / (float)source.getHeight(); - } - Matrix matrix = new Matrix(); - matrix.setScale(scale, scale); - Bitmap miniThumbnail = ImageLoader.transform(matrix, source, - width, height, false); - - if (recycle && miniThumbnail != source) { - source.recycle(); - } - return miniThumbnail; - } - - // Rotates the bitmap by the specified degree. - // If a new bitmap is created, the original bitmap is recycled. - static Bitmap rotate(Bitmap b, int degrees) { - if (degrees != 0 && b != null) { - Matrix m = new Matrix(); - m.setRotate(degrees, (float) b.getWidth() / 2, (float) b.getHeight() / 2); - - try { - Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true); - if (b != b2) { - b.recycle(); - b = b2; - } - } catch (OutOfMemoryError ex) { - // We have no memory to rotate. Return the original bitmap. - } - } - return b; - } - public static int roundOrientation(int orientationInput) { int orientation = orientationInput; - if (orientation == -1) + if (orientation == -1) { orientation = 0; + } orientation = orientation % 360; int retVal; - if (orientation < (0*90) + 45) { + if (orientation < (0 * 90) + 45) { retVal = 0; - } else if (orientation < (1*90) + 45) { + } else if (orientation < (1 * 90) + 45) { retVal = 90; - } else if (orientation < (2*90) + 45) { + } else if (orientation < (2 * 90) + 45) { retVal = 180; - } else if (orientation < (3*90) + 45) { + } else if (orientation < (3 * 90) + 45) { retVal = 270; } else { retVal = 0; } - if (VERBOSE) Log.v(TAG, "map orientation " + orientationInput + " to " + retVal); + if (VERBOSE) { + Log.v(TAG, "map orientation " + orientationInput + " to " + retVal); + } return retVal; } - /** * @return true if the mimetype is an image mimetype. */ @@ -3761,13 +189,6 @@ public class ImageManager { } /** - * @return true if the mimetype is a video mimetype. - */ - public static boolean isVideoMimeType(String mimeType) { - return mimeType.startsWith("video/"); - } - - /** * @return true if the image is an image. */ public static boolean isImage(IImage image) { @@ -3778,19 +199,13 @@ public class ImageManager { * @return true if the image is a video. */ public static boolean isVideo(IImage image) { - return isVideoMimeType(image.getMimeType()); + return Util.isVideoMimeType(image.getMimeType()); } - public Uri addImage( - final Context ctx, - final ContentResolver cr, - final String imageName, - final String description, - final long dateTaken, - final Location location, - final int orientation, - final String directory, - final String filename) { + public Uri addImage(Context ctx, ContentResolver cr, String imageName, + String description, long dateTaken, Location location, + int orientation, String directory, String filename) { + ContentValues values = new ContentValues(7); values.put(Images.Media.TITLE, imageName); values.put(Images.Media.DISPLAY_NAME, imageName); @@ -3800,17 +215,22 @@ public class ImageManager { values.put(Images.Media.ORIENTATION, orientation); File parentFile = new File(directory); - // Lowercase the path for hashing. This avoids duplicate buckets if the filepath - // case is changed externally. + + // Lowercase the path for hashing. This avoids duplicate buckets if the + // filepath case is changed externally. // Keep the original case for display. String path = parentFile.toString().toLowerCase(); String name = parentFile.getName(); - if (VERBOSE) Log.v(TAG, "addImage id is " + path.hashCode() + "; name " + name + "; path is " + path); + if (VERBOSE) { + Log.v(TAG, "addImage id is " + path.hashCode() + "; name " + + name + "; path is " + path); + } if (location != null) { if (VERBOSE) { - Log.v(TAG, "lat long " + location.getLatitude() + " / " + location.getLongitude()); + Log.v(TAG, "lat long " + location.getLatitude() + " / " + + location.getLongitude()); } values.put(Images.Media.LATITUDE, location.getLatitude()); values.put(Images.Media.LONGITUDE, location.getLongitude()); @@ -3825,21 +245,21 @@ public class ImageManager { Uri uri = cr.insert(sStorageURI, values); // The line above will create a filename that ends in .jpg - // That filename is what will be handed to gmail when a user shares a photo. - // Gmail gets the name of the picture attachment from the "DISPLAY_NAME" field. - // Extract the filename and jam it into the display name. - Cursor c = cr.query( - uri, - new String [] { ImageColumns._ID, Images.Media.DISPLAY_NAME, "_data" }, - null, - null, - null); + // That filename is what will be handed to gmail when a user shares a + // photo. Gmail gets the name of the picture attachment from the + // "DISPLAY_NAME" field. Extract the filename and jam it into the + // display name. + String projection[] = new String [] { + ImageColumns._ID, Images.Media.DISPLAY_NAME, "_data"}; + Cursor c = cr.query(uri, projection, null, null, null); + if (c.moveToFirst()) { String filePath = c.getString(2); if (filePath != null) { int pos = filePath.lastIndexOf("/"); if (pos >= 0) { - filePath = filePath.substring(pos + 1); // pick off the filename + // pick off the filename + filePath = filePath.substring(pos + 1); c.updateString(1, filePath); c.commitUpdates(); } @@ -3849,181 +269,209 @@ public class ImageManager { return uri; } - public IAddImage_cancelable storeImage( - final Uri uri, - final Context ctx, - final ContentResolver cr, - final int orientation, - final Bitmap source, - final byte [] jpegData) { - class AddImageCancelable extends BaseCancelable implements IAddImage_cancelable { - private IGetBoolean_cancelable mSaveImageCancelable; + private static class AddImageCancelable extends BaseCancelable + implements IAddImageCancelable { + private IGetBooleanCancelable mSaveImageCancelable; + private Uri mUri; + private Context mCtx; + private ContentResolver mCr; + private int mOrientation; + private Bitmap mSource; + private byte [] mJpegData; + + public AddImageCancelable(Uri uri, Context ctx, ContentResolver cr, + int orientation, Bitmap source, byte[] jpegData) { + mUri = uri; + mCtx = ctx; + mCr = cr; + mOrientation = orientation; + mSource = source; + mJpegData = jpegData; + } - public boolean doCancelWork() { - if (VERBOSE) { - Log.v(TAG, "calling AddImageCancelable.cancel() " + mSaveImageCancelable); - } + @Override + public boolean doCancelWork() { + if (VERBOSE) { + Log.v(TAG, "calling AddImageCancelable.cancel() " + + mSaveImageCancelable); + } + if (mSaveImageCancelable != null) { + mSaveImageCancelable.cancel(); + } + return true; + } - if (mSaveImageCancelable != null) { - mSaveImageCancelable.cancel(); - } - return true; + public void get() { + if (mSource == null && mJpegData == null) { + throw new IllegalArgumentException("source cannot be null"); } - public void get() { - if (source == null && jpegData == null) { - throw new IllegalArgumentException("source cannot be null"); + try { + long t1 = System.currentTimeMillis(); + synchronized (this) { + if (mCancel) { + throw new CanceledException(); + } } + long id = ContentUris.parseId(mUri); - try { - long t1 = System.currentTimeMillis(); - synchronized (this) { - if (mCancel) { - throw new CanceledException(); - } - } - long id = ContentUris.parseId(uri); + BaseImageList il = new ImageList(mCtx, mCr, sStorageURI, + sThumbURI, SORT_ASCENDING, null); + Image image = new Image(id, 0, mCr, il, il.getCount(), 0); + long t5 = System.currentTimeMillis(); + String[] projection = new String[] { + ImageColumns._ID, + ImageColumns.MINI_THUMB_MAGIC, "_data"}; - BaseImageList il = new ImageList(ctx, cr, sStorageURI, sThumbURI, SORT_ASCENDING, null); - ImageManager.Image image = new Image(id, 0, cr, il, il.getCount(), 0); - long t5 = System.currentTimeMillis(); - Cursor c = cr.query( - uri, - new String [] { ImageColumns._ID, ImageColumns.MINI_THUMB_MAGIC, "_data" }, - null, - null, - null); - c.moveToPosition(0); + Cursor c = mCr.query(mUri, projection, null, null, null); + c.moveToPosition(0); - synchronized (this) { - checkCanceled(); - mSaveImageCancelable = image.saveImageContents(source, jpegData, orientation, true, c); - } + synchronized (this) { + checkCanceled(); + mSaveImageCancelable = image.saveImageContents( + mSource, mJpegData, mOrientation, true, c); + } - if (mSaveImageCancelable.get()) { - long t6 = System.currentTimeMillis(); - if (VERBOSE) Log.v(TAG, "saveImageContents took " + (t6-t5)); - if (VERBOSE) Log.v(TAG, "updating new picture with id " + id); - c.updateLong(1, id); - c.commitUpdates(); - c.close(); - long t7 = System.currentTimeMillis(); - if (VERBOSE) Log.v(TAG, "commit updates to save mini thumb took " + (t7-t6)); - } - else { - c.close(); - throw new CanceledException(); + if (mSaveImageCancelable.get()) { + long t6 = System.currentTimeMillis(); + if (VERBOSE) { + Log.v(TAG, "saveImageContents took " + (t6 - t5)); + Log.v(TAG, "updating new picture with id " + id); } - } catch (CanceledException ex) { + c.updateLong(1, id); + c.commitUpdates(); + c.close(); + long t7 = System.currentTimeMillis(); if (VERBOSE) { - Log.v(TAG, "caught CanceledException"); + Log.v(TAG, "commit updates to save mini thumb took " + + (t7 - t6)); } - if (uri != null) { - if (VERBOSE) { - Log.v(TAG, "canceled... cleaning up this uri: " + uri); - } - cr.delete(uri, null, null); + } else { + c.close(); + throw new CanceledException(); + } + } catch (CanceledException ex) { + if (VERBOSE) { + Log.v(TAG, "caught CanceledException"); + } + if (mUri != null) { + if (VERBOSE) { + Log.v(TAG, "canceled... cleaning up this uri: " + mUri); } - acknowledgeCancel(); + mCr.delete(mUri, null, null); } + acknowledgeCancel(); } } - return new AddImageCancelable(); } - static public IImageList makeImageList(Uri uri, Context ctx, int sort) { + public IAddImageCancelable storeImage( + Uri uri, Context ctx, ContentResolver cr, int orientation, + Bitmap source, byte [] jpegData) { + return new AddImageCancelable( + uri, ctx, cr, orientation, source, jpegData); + } + + public static IImageList makeImageList(Uri uri, Context ctx, int sort) { ContentResolver cr = ctx.getContentResolver(); String uriString = (uri != null) ? uri.toString() : ""; - // TODO we need to figure out whether we're viewing + + // TODO: we need to figure out whether we're viewing // DRM images in a better way. Is there a constant // for content://drm somewhere?? IImageList imageList; if (uriString.startsWith("content://drm")) { imageList = ImageManager.instance().allImages( - ctx, - cr, - ImageManager.DataLocation.ALL, - ImageManager.INCLUDE_DRM_IMAGES, - sort); - } else if (!uriString.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) - && !uriString.startsWith(MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString())) { - imageList = ImageManager.instance().new SingleImageList(cr, uri); + ctx, cr, ImageManager.DataLocation.ALL, + ImageManager.INCLUDE_DRM_IMAGES, sort); + } else if (isSingleImageMode(uriString)) { + imageList = new SingleImageList(cr, uri); } else { String bucketId = uri.getQueryParameter("bucketId"); if (VERBOSE) Log.v(TAG, "bucketId is " + bucketId); imageList = ImageManager.instance().allImages( - ctx, - cr, - ImageManager.DataLocation.ALL, - ImageManager.INCLUDE_IMAGES, - sort, - bucketId); + ctx, cr, ImageManager.DataLocation.ALL, + ImageManager.INCLUDE_IMAGES, sort, bucketId); } return imageList; } - public IImageList emptyImageList() { - return - new IImageList() { - public void checkThumbnails(ImageManager.IImageList.ThumbCheckCallback cb, - int totalThumbnails) { - } + private static boolean isSingleImageMode(String uriString) { + return !uriString.startsWith( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) + && !uriString.startsWith( + MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString()); + } - public void commitChanges() { - } + private static class EmptyImageList implements IImageList { + public void checkThumbnails(IImageList.ThumbCheckCallback cb, + int totalThumbnails) { + } - public void deactivate() { - } + public void commitChanges() { + } - public HashMap<String, String> getBucketIds() { - return new HashMap<String,String>(); - } + public void deactivate() { + } - public int getCount() { - return 0; - } + public HashMap<String, String> getBucketIds() { + return new HashMap<String, String>(); + } - public boolean isEmpty() { - return true; - } + public int getCount() { + return 0; + } - public IImage getImageAt(int i) { - return null; - } + public boolean isEmpty() { + return true; + } - public IImage getImageForUri(Uri uri) { - return null; - } + public IImage getImageAt(int i) { + return null; + } - public boolean removeImage(IImage image) { - return false; - } + public IImage getImageForUri(Uri uri) { + return null; + } - public void removeImageAt(int i) { - } + public boolean removeImage(IImage image) { + return false; + } - public void removeOnChangeListener(ImageManager.IImageList.OnChange changeCallback) { - } + public void removeImageAt(int i) { + } - public void setOnChangeListener(ImageManager.IImageList.OnChange changeCallback, - Handler h) { - } + public void removeOnChangeListener(IImageList.OnChange changeCallback) { + } + + public void setOnChangeListener(IImageList.OnChange changeCallback, + Handler h) { + } - }; } - public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort) { + public IImageList emptyImageList() { + return new EmptyImageList(); + } + + public IImageList allImages(Context ctx, ContentResolver cr, + DataLocation location, int inclusion, int sort) { return allImages(ctx, cr, location, inclusion, sort, null, null); } - public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId) { + public IImageList allImages(Context ctx, ContentResolver cr, + DataLocation location, int inclusion, int sort, String bucketId) { return allImages(ctx, cr, location, inclusion, sort, bucketId, null); } - public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId, Uri specificImageUri) { + public IImageList allImages( + Context ctx, ContentResolver cr, DataLocation location, + int inclusion, int sort, String bucketId, Uri specificImageUri) { if (VERBOSE) { - Log.v(TAG, "allImages " + location + " " + ((inclusion&INCLUDE_IMAGES)!=0) + " + v=" + ((inclusion&INCLUDE_VIDEOS)!=0)); + Log.v(TAG, "allImages " + location + " " + + ((inclusion & INCLUDE_IMAGES) != 0) + " + v=" + + ((inclusion & INCLUDE_VIDEOS) != 0)); } if (cr == null) { @@ -4037,43 +485,60 @@ public class ImageManager { ArrayList<IImageList> l = new ArrayList<IImageList>(); if (VERBOSE) { - Log.v(TAG, "initializing ... haveSdCard == " + haveSdCard + "; inclusion is " + String.format("%x", inclusion)); + Log.v(TAG, "initializing ... haveSdCard == " + haveSdCard + + "; inclusion is " + + String.format("%x", inclusion)); } if (specificImageUri != null) { try { - if (specificImageUri.getScheme().equalsIgnoreCase("content")) - l.add(new ImageList(ctx, cr, specificImageUri, sThumbURI, sort, bucketId)); - else + if (specificImageUri.getScheme() + .equalsIgnoreCase("content")) { + l.add(new ImageList(ctx, cr, specificImageUri, + sThumbURI, sort, bucketId)); + } else { l.add(new SingleImageList(cr, specificImageUri)); + } } catch (UnsupportedOperationException ex) { + // ignore exception } } else { if (haveSdCard && location != DataLocation.INTERNAL) { if ((inclusion & INCLUDE_IMAGES) != 0) { try { - l.add(new ImageList(ctx, cr, sStorageURI, sThumbURI, sort, bucketId)); + l.add(new ImageList(ctx, cr, sStorageURI, + sThumbURI, sort, bucketId)); } catch (UnsupportedOperationException ex) { + // ignore exception } } if ((inclusion & INCLUDE_VIDEOS) != 0) { try { - l.add(new VideoList(ctx, cr, sVideoStorageURI, sVideoThumbURI, sort, bucketId)); + l.add(new VideoList(ctx, cr, sVideoStorageURI, + sVideoThumbURI, sort, bucketId)); } catch (UnsupportedOperationException ex) { + // ignore exception } } } - if (location == DataLocation.INTERNAL || location == DataLocation.ALL) { + if (location == DataLocation.INTERNAL + || location == DataLocation.ALL) { if ((inclusion & INCLUDE_IMAGES) != 0) { try { - l.add(new ImageList(ctx, cr, Images.Media.INTERNAL_CONTENT_URI, - Images.Thumbnails.INTERNAL_CONTENT_URI, sort, bucketId)); + l.add(new ImageList(ctx, cr, + Images.Media.INTERNAL_CONTENT_URI, + Images.Thumbnails.INTERNAL_CONTENT_URI, + sort, bucketId)); } catch (UnsupportedOperationException ex) { + // ignore exception } } if ((inclusion & INCLUDE_DRM_IMAGES) != 0) { try { - l.add(new DrmImageList(ctx, cr, DrmStore.Images.CONTENT_URI, sort, bucketId)); + l.add(new DrmImageList(ctx, cr, + DrmStore.Images.CONTENT_URI, + sort, bucketId)); } catch (UnsupportedOperationException ex) { + // ignore exception } } } @@ -4083,19 +548,24 @@ public class ImageManager { return new ImageListUber(imageList, sort); } else { if (haveSdCard && location != DataLocation.INTERNAL) { - return new ImageList(ctx, cr, sStorageURI, sThumbURI, sort, bucketId); + return new ImageList( + ctx, cr, sStorageURI, sThumbURI, sort, bucketId); } else { - return new ImageList(ctx, cr, Images.Media.INTERNAL_CONTENT_URI, - Images.Thumbnails.INTERNAL_CONTENT_URI, sort, bucketId); + return new ImageList(ctx, cr, + Images.Media.INTERNAL_CONTENT_URI, + Images.Thumbnails.INTERNAL_CONTENT_URI, sort, + bucketId); } } } } - // Create a temporary file to see whether a volume is really writeable. It's important not to - // put it in the root directory which may have a limit on the number of files. - static private boolean checkFsWritable() { - String directoryName = Environment.getExternalStorageDirectory().toString() + "/DCIM"; + private static boolean checkFsWritable() { + // Create a temporary file to see whether a volume is really writeable. + // It's important not to put it in the root directory which may have a + // limit on the number of files. + String directoryName = + Environment.getExternalStorageDirectory().toString() + "/DCIM"; File directory = new File(directoryName); if (!directory.isDirectory()) { if (!directory.mkdirs()) { @@ -4117,11 +587,11 @@ public class ImageManager { } } - static public boolean hasStorage() { + public static boolean hasStorage() { return hasStorage(true); } - static public boolean hasStorage(boolean requireWriteAccess) { + public static boolean hasStorage(boolean requireWriteAccess) { //TODO: After fix the bug, add "if (VERBOSE)" before logging errors. String state = Environment.getExternalStorageState(); Log.v(TAG, "storage state is " + state); @@ -4134,7 +604,8 @@ public class ImageManager { } else { return true; } - } else if (!requireWriteAccess && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { + } else if (!requireWriteAccess + && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { return true; } return false; @@ -4147,7 +618,8 @@ public class ImageManager { if (resolver == null) { return null; } - return resolver.query(uri, projection, selection, selectionArgs, sortOrder); + return resolver.query( + uri, projection, selection, selectionArgs, sortOrder); } catch (UnsupportedOperationException ex) { return null; } @@ -4157,7 +629,8 @@ public class ImageManager { public static boolean isMediaScannerScanning(Context context) { boolean result = false; Cursor cursor = query(context, MediaStore.getMediaScannerUri(), - new String [] { MediaStore.MEDIA_SCANNER_VOLUME }, null, null, null); + new String [] {MediaStore.MEDIA_SCANNER_VOLUME}, + null, null, null); if (cursor != null) { if (cursor.getCount() == 1) { cursor.moveToFirst(); @@ -4166,35 +639,10 @@ public class ImageManager { cursor.close(); } - if (VERBOSE) - Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>>>>>> isMediaScannerScanning returning " + result); - return result; - } - - /** - * Create a video thumbnail for a video. May return null if the video is corrupt. - * @param filePath - * @return - */ - public static Bitmap createVideoThumbnail(String filePath) { - Bitmap bitmap = null; - MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - try { - retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY); - retriever.setDataSource(filePath); - bitmap = retriever.captureFrame(); - } catch(IllegalArgumentException ex) { - // Assume this is a corrupt video file - } catch (RuntimeException ex) { - // Assume this is a corrupt video file. - } finally { - try { - retriever.release(); - } catch (RuntimeException ex) { - // Ignore failures while cleaning up. - } + if (VERBOSE) { + Log.v(TAG, "isMediaScannerScanning returning " + result); } - return bitmap; + return result; } public static String getLastImageThumbPath() { diff --git a/src/com/android/camera/MenuHelper.java b/src/com/android/camera/MenuHelper.java index a88f020..7e9116a 100644 --- a/src/com/android/camera/MenuHelper.java +++ b/src/com/android/camera/MenuHelper.java @@ -16,6 +16,8 @@ package com.android.camera; +import com.android.camera.gallery.IImage; + import java.io.Closeable; import java.util.ArrayList; @@ -44,7 +46,6 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; -import com.android.camera.ImageManager.IImage; public class MenuHelper { static private final String TAG = "MenuHelper"; @@ -92,8 +93,8 @@ public class MenuHelper { public static final int RESULT_COMMON_MENU_CROP = 490; public interface MenuItemsResult { - public void gettingReadyToOpen(Menu menu, ImageManager.IImage image); - public void aboutToCall(MenuItem item, ImageManager.IImage image); + public void gettingReadyToOpen(Menu menu, IImage image); + public void aboutToCall(MenuItem item, IImage image); } public interface MenuInvoker { @@ -101,7 +102,7 @@ public class MenuHelper { } public interface MenuCallback { - public void run(Uri uri, ImageManager.IImage image); + public void run(Uri uri, IImage image); } private static void closeSilently(Closeable target) { @@ -112,7 +113,7 @@ public class MenuHelper { } } - public static long getImageFileSize(ImageManager.IImage image) { + public static long getImageFileSize(IImage image) { java.io.InputStream data = image.fullSizeImageData(); if (data == null) return -1; try { @@ -164,7 +165,7 @@ public class MenuHelper { requiresWriteAccessItems.add(rotateSubmenu.add(0, MENU_IMAGE_ROTATE_LEFT, 50, R.string.rotate_left).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { onInvoke.run(new MenuCallback() { - public void run(Uri u, ImageManager.IImage image) { + public void run(Uri u, IImage image) { if (image == null || image.isReadonly()) return; image.rotateImageBy(-90); @@ -176,7 +177,7 @@ public class MenuHelper { requiresWriteAccessItems.add(rotateSubmenu.add(0, MENU_IMAGE_ROTATE_RIGHT, 60, R.string.rotate_right).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { onInvoke.run(new MenuCallback() { - public void run(Uri u, ImageManager.IImage image) { + public void run(Uri u, IImage image) { if (image == null || image.isReadonly()) return; @@ -195,7 +196,7 @@ public class MenuHelper { new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { onInvoke.run(new MenuCallback() { - public void run(Uri u, ImageManager.IImage image) { + public void run(Uri u, IImage image) { if (u == null) return; @@ -219,7 +220,7 @@ public class MenuHelper { setMenu.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { onInvoke.run(new MenuCallback() { - public void run(Uri u, ImageManager.IImage image) { + public void run(Uri u, IImage image) { if (u == null || image == null) return; @@ -244,7 +245,7 @@ public class MenuHelper { new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { onInvoke.run(new MenuCallback() { - public void run(Uri u, ImageManager.IImage image) { + public void run(Uri u, IImage image) { if (image == null) return; if (!isImage && getImageFileSize(image) > SHARE_FILE_LENGTH_LIMIT ) { Toast.makeText(activity, @@ -295,7 +296,7 @@ public class MenuHelper { MenuItem detailsMenu = menu.add(0, 0, 80, R.string.details).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { onInvoke.run(new MenuCallback() { - public void run(Uri u, ImageManager.IImage image) { + public void run(Uri u, IImage image) { if (image == null) return; @@ -472,7 +473,7 @@ public class MenuHelper { return new MenuItemsResult() { - public void gettingReadyToOpen(Menu menu, ImageManager.IImage image) { + public void gettingReadyToOpen(Menu menu, IImage image) { // protect against null here. this isn't strictly speaking required // but if a client app isn't handling sdcard removal properly it // could happen @@ -496,7 +497,7 @@ public class MenuHelper { item.setEnabled(!isDrm); } } - public void aboutToCall(MenuItem menu, ImageManager.IImage image) { + public void aboutToCall(MenuItem menu, IImage image) { } }; } diff --git a/src/com/android/camera/SelectedImageGetter.java b/src/com/android/camera/SelectedImageGetter.java index 9e8fb96..a9e5db1 100644 --- a/src/com/android/camera/SelectedImageGetter.java +++ b/src/com/android/camera/SelectedImageGetter.java @@ -16,10 +16,12 @@ package com.android.camera; +import com.android.camera.gallery.IImage; + import android.net.Uri; interface SelectedImageGetter { - ImageManager.IImage getCurrentImage(); + IImage getCurrentImage(); Uri getCurrentImageUri(); } diff --git a/src/com/android/camera/SlideShow.java b/src/com/android/camera/SlideShow.java index 08bdc0d..ddbdd1a 100644 --- a/src/com/android/camera/SlideShow.java +++ b/src/com/android/camera/SlideShow.java @@ -17,6 +17,12 @@ package com.android.camera; import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + +import com.android.camera.gallery.IGetBitmapCancelable; +import com.android.camera.gallery.IImage; +import com.android.camera.gallery.IImageList; +import com.android.camera.gallery.SimpleBaseImage; + import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; @@ -44,15 +50,12 @@ import java.io.File; import java.util.ArrayList; import java.util.HashMap; -import com.android.camera.ImageManager.IGetBitmap_cancelable; -import com.android.camera.ImageManager.IImage; -import com.android.camera.ImageManager.IImageList; public class SlideShow extends Activity implements ViewSwitcher.ViewFactory { private static final String TAG = "SlideShow"; static final int LAG = 2000; static final int NEXT_IMAGE_INTERVAL = 3000; - private ImageManager.IImageList mImageList; + private IImageList mImageList; private int mCurrentPosition = 0; private ImageView mSwitcher; private boolean mPosted = false; @@ -231,7 +234,7 @@ public class SlideShow extends Activity implements ViewSwitcher.ViewFactory { } private void loadImage() { - ImageManager.IImage image = mImageList.getImageAt(mCurrentPosition); + IImage image = mImageList.getImageAt(mCurrentPosition); if (image == null) { return; } @@ -315,7 +318,7 @@ public class SlideShow extends Activity implements ViewSwitcher.ViewFactory { // image uri ==> Image object private HashMap<Long, IImage> mCache = new HashMap<Long, IImage>(); - class FileImage extends ImageManager.SimpleBaseImage { + class FileImage extends SimpleBaseImage { long mId; String mPath; @@ -336,7 +339,7 @@ public class SlideShow extends Activity implements ViewSwitcher.ViewFactory { return BitmapFactory.decodeFile(mPath); } - public IGetBitmap_cancelable fullSizeBitmap_cancelable( + public IGetBitmapCancelable fullSizeBitmapCancelable( int targetWidthOrHeight) { return null; } diff --git a/src/com/android/camera/ThumbnailController.java b/src/com/android/camera/ThumbnailController.java index 2851823..bcf41bf 100644 --- a/src/com/android/camera/ThumbnailController.java +++ b/src/com/android/camera/ThumbnailController.java @@ -16,6 +16,8 @@ package com.android.camera; +import com.android.camera.gallery.Util; + import android.content.ContentResolver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -189,7 +191,7 @@ public class ThumbnailController { LayoutParams layoutParams = mButton.getLayoutParams(); final int miniThumbWidth = layoutParams.width - 2 * PADDING_WIDTH; final int miniThumbHeight = layoutParams.height - 2 * PADDING_HEIGHT; - mThumb = ImageManager.extractMiniThumb( + mThumb = Util.extractMiniThumb( original, miniThumbWidth, miniThumbHeight, false); Drawable[] vignetteLayers = new Drawable[2]; diff --git a/src/com/android/camera/VideoCamera.java b/src/com/android/camera/VideoCamera.java index 186d827..5bca20d 100644 --- a/src/com/android/camera/VideoCamera.java +++ b/src/com/android/camera/VideoCamera.java @@ -16,6 +16,10 @@ package com.android.camera; +import com.android.camera.gallery.IImage; +import com.android.camera.gallery.IImageList; +import com.android.camera.gallery.Util; + import java.io.File; import java.io.FileDescriptor; import java.io.IOException; @@ -1026,7 +1030,7 @@ public class VideoCamera extends Activity implements View.OnClickListener, String path = mCurrentVideoFilename; if (path != null) { - Bitmap videoFrame = ImageManager.createVideoThumbnail(path); + Bitmap videoFrame = Util.createVideoThumbnail(path); mVideoFrame.setImageBitmap(videoFrame); mVideoFrame.setVisibility(View.VISIBLE); } @@ -1132,7 +1136,7 @@ public class VideoCamera extends Activity implements View.OnClickListener, } private void acquireVideoThumb() { - Bitmap videoFrame = ImageManager.createVideoThumbnail(mCurrentVideoFilename); + Bitmap videoFrame = Util.createVideoThumbnail(mCurrentVideoFilename); mThumbController.setData(mCurrentVideoUri, videoFrame); } @@ -1153,7 +1157,7 @@ public class VideoCamera extends Activity implements View.OnClickListener, } private void updateLastVideo() { - ImageManager.IImageList list = ImageManager.instance().allImages( + IImageList list = ImageManager.instance().allImages( this, mContentResolver, dataLocation(), @@ -1162,7 +1166,7 @@ public class VideoCamera extends Activity implements View.OnClickListener, ImageManager.CAMERA_IMAGE_BUCKET_ID); int count = list.getCount(); if (count > 0) { - ImageManager.IImage image = list.getImageAt(count-1); + IImage image = list.getImageAt(count-1); Uri uri = image.fullSizeImageUri(); mThumbController.setData(uri, image.miniThumbBitmap()); } else { diff --git a/src/com/android/camera/ViewImage.java b/src/com/android/camera/ViewImage.java index 95402f0..d15b305 100644 --- a/src/com/android/camera/ViewImage.java +++ b/src/com/android/camera/ViewImage.java @@ -16,6 +16,10 @@ package com.android.camera; +import com.android.camera.gallery.IGetBitmapCancelable; +import com.android.camera.gallery.IImage; +import com.android.camera.gallery.IImageList; + import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -101,7 +105,7 @@ public class ViewImage extends Activity implements View.OnClickListener { static final int HYSTERESIS = PADDING * 2; static final int BASE_SCROLL_DURATION = 1000; // ms - ImageManager.IImageList mAllImages; + IImageList mAllImages; private int mSlideShowImageCurrent = 0; private ImageViewTouch [] mSlideShowImageViews = new ImageViewTouch[2]; @@ -297,7 +301,6 @@ public class ViewImage extends Activity implements View.OnClickListener { private static final boolean ANIMATE_TRANSITIONS = false; - static class ScrollHandler extends LinearLayout { private Runnable mFirstLayoutCompletedCallback = null; private Scroller mScrollerHelper; @@ -394,7 +397,7 @@ public class ViewImage extends Activity implements View.OnClickListener { final SelectedImageGetter selectedImageGetter = new SelectedImageGetter() { - public ImageManager.IImage getCurrentImage() { + public IImage getCurrentImage() { return mAllImages.getImageAt(mCurrentPosition); } @@ -1037,7 +1040,7 @@ public class ViewImage extends Activity implements View.OnClickListener { uri = uri.buildUpon().query(null).build(); // TODO smarter/faster here please for (int i = 0; i < mAllImages.getCount(); i++) { - ImageManager.IImage image = mAllImages.getImageAt(i); + IImage image = mAllImages.getImageAt(i); if (image.fullSizeImageUri().equals(uri)) { mCurrentPosition = i; mLastSlideShowImage = mCurrentPosition; @@ -1047,7 +1050,7 @@ public class ViewImage extends Activity implements View.OnClickListener { } private Uri getCurrentUri() { - ImageManager.IImage image = mAllImages.getImageAt(mCurrentPosition); + IImage image = mAllImages.getImageAt(mCurrentPosition); Uri uri = null; if (image != null){ String bucket = null; @@ -1094,7 +1097,7 @@ public class ViewImage extends Activity implements View.OnClickListener { mCurrentPosition = count - 1; } - ImageManager.IImage image = mAllImages.getImageAt(mCurrentPosition); + IImage image = mAllImages.getImageAt(mCurrentPosition); if (mGetter == null) { makeGetter(); @@ -1268,6 +1271,7 @@ class ImageViewTouch extends ImageViewTouchBase { mEnableTrackballScroll = enable; } + @Override protected void postTranslate(float dx, float dy) { super.postTranslate(dx, dy); center(true, false, false); @@ -1294,7 +1298,7 @@ class ImageViewTouch extends ImageViewTouchBase { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: { if (mViewImage.isPickIntent()) { - ImageManager.IImage img = mViewImage.mAllImages + IImage img = mViewImage.mAllImages .getImageAt(mViewImage.mCurrentPosition); mViewImage.setResult(ViewImage.RESULT_OK, new Intent().setData(img.fullSizeImageUri())); @@ -1305,7 +1309,7 @@ class ImageViewTouch extends ImageViewTouchBase { case KeyEvent.KEYCODE_DPAD_LEFT: { panBy(PAN_RATE, 0); int maxOffset = (current == 0) ? 0 : ViewImage.HYSTERESIS; - if (getScale() <= 1F + if (getScale() <= 1F || isShiftedToNextImage(true, maxOffset)) { nextImagePos = current - 1; } else { @@ -1422,7 +1426,7 @@ class ImageGetter { // This is the loader cancelable that gets set while we're loading an image. // If we change position we can cancel the current load using this. - private ImageManager.IGetBitmap_cancelable mLoad; + private IGetBitmapCancelable mLoad; // True if we're canceling the current load. private boolean mCancelCurrent = false; @@ -1440,7 +1444,7 @@ class ImageGetter { synchronized (this) { if (!mReady) { mCancelCurrent = true; - ImageManager.IGetBitmap_cancelable load = mLoad; + IGetBitmapCancelable load = mLoad; if (load != null) { if (Config.LOGV) { Log.v(TAG, "canceling load object"); @@ -1503,7 +1507,7 @@ class ImageGetter { int offset = order[i]; int imageNumber = lastPosition + offset; if (imageNumber >= 0 && imageNumber < imageCount) { - ImageManager.IImage image = mViewImage.mAllImages.getImageAt(lastPosition + offset); + IImage image = mViewImage.mAllImages.getImageAt(lastPosition + offset); if (image == null || isCanceled()) { break; } @@ -1520,14 +1524,14 @@ class ImageGetter { int offset = order[i]; int imageNumber = lastPosition + offset; if (imageNumber >= 0 && imageNumber < imageCount) { - ImageManager.IImage image = mViewImage.mAllImages.getImageAt(lastPosition + offset); + IImage image = mViewImage.mAllImages.getImageAt(lastPosition + offset); if (mCB.wantsFullImage(lastPosition, offset)) { if (Config.LOGV) { Log.v(TAG, "starting FULL IMAGE load at offset " + offset); } int sizeToUse = mCB.fullImageSizeToUse(lastPosition, offset); if (image != null && !isCanceled()) { - mLoad = image.fullSizeBitmap_cancelable(sizeToUse); + mLoad = image.fullSizeBitmapCancelable(sizeToUse); } if (mLoad != null) { long t1; @@ -1565,7 +1569,7 @@ class ImageGetter { } } } - + public ImageGetter(ViewImage viewImage) { mViewImage = viewImage; mGetterThread = new Thread(new ImageGetterRunnable()); @@ -1584,7 +1588,7 @@ class ImageGetter { if (!mReady) { try { mCancelCurrent = true; - ImageManager.IGetBitmap_cancelable load = mLoad; + IGetBitmapCancelable load = mLoad; if (load != null) { load.cancel(); } diff --git a/src/com/android/camera/gallery/BaseCancelable.java b/src/com/android/camera/gallery/BaseCancelable.java new file mode 100644 index 0000000..dbc46cc --- /dev/null +++ b/src/com/android/camera/gallery/BaseCancelable.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + + +/** + * A base class for the interface <code>ICancelable</code>. + */ +public abstract class BaseCancelable implements ICancelable { + protected boolean mCancel = false; + protected boolean mFinished = false; + + /* + * Subclasses should call acknowledgeCancel when they're finished with + * their operation. + */ + protected synchronized void acknowledgeCancel() { + mFinished = true; + if (mCancel) { + this.notify(); + } + } + + public synchronized boolean cancel() { + if (mCancel || mFinished) { + return false; + } + mCancel = true; + boolean retVal = doCancelWork(); + try { + this.wait(); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return retVal; + } + + /* + * Subclasses can call this to see if they have been canceled. + * This is the polling model. + */ + protected synchronized void checkCanceled() throws CanceledException { + if (mCancel) { + throw new CanceledException(); + } + } + + /* + * Subclasses implement this method to take whatever action + * is necessary when getting canceled. Sometimes it's not + * possible to do anything in which case the "checkCanceled" + * polling model may be used (or some combination). + */ + protected abstract boolean doCancelWork(); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/BaseImage.java b/src/com/android/camera/gallery/BaseImage.java new file mode 100644 index 0000000..a43064c --- /dev/null +++ b/src/com/android/camera/gallery/BaseImage.java @@ -0,0 +1,602 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore.Images; +import android.util.Log; + + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; + +/** + * Represents a particular image and provides access to the underlying bitmap + * and two thumbnail bitmaps as well as other information such as the id, and + * the path to the actual image data. + */ +public abstract class BaseImage implements IImage { + + private static final boolean VERBOSE = false; + private static final String TAG = "BaseImage"; + + static final int BYTES_PER_MINTHUMB = 10000; + private static final byte [] sMiniThumbData = new byte[BYTES_PER_MINTHUMB]; + + protected ContentResolver mContentResolver; + protected long mId, mMiniThumbMagic; + protected BaseImageList mContainer; + protected HashMap<String, String> mExifData; + protected int mCursorRow; + + protected BaseImage(long id, long miniThumbId, ContentResolver cr, + BaseImageList container, int cursorRow) { + mContentResolver = cr; + mId = id; + mMiniThumbMagic = miniThumbId; + mContainer = container; + mCursorRow = cursorRow; + } + + protected abstract Bitmap.CompressFormat compressionType(); + + public void commitChanges() { + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + c.commitUpdates(); + c.requery(); + } + } + } + + private class CompressImageToFile extends BaseCancelable + implements IGetBooleanCancelable { + private ThreadSafeOutputStream mOutputStream = null; + + private Bitmap mBitmap; + private Uri mUri; + private byte[] mJpegData; + + public CompressImageToFile(Bitmap bitmap, byte[] jpegData, Uri uri) { + mBitmap = bitmap; + mUri = uri; + mJpegData = jpegData; + } + + @Override + public boolean doCancelWork() { + if (mOutputStream != null) { + mOutputStream.close(); + return true; + } + return false; + } + + public boolean get() { + try { + long t1 = System.currentTimeMillis(); + OutputStream delegate = mContentResolver.openOutputStream(mUri); + synchronized (this) { + checkCanceled(); + mOutputStream = new ThreadSafeOutputStream(delegate); + } + long t2 = System.currentTimeMillis(); + if (mBitmap != null) { + mBitmap.compress(compressionType(), 75, mOutputStream); + } else { + long x1 = System.currentTimeMillis(); + mOutputStream.write(mJpegData); + long x2 = System.currentTimeMillis(); + if (VERBOSE) { + Log.v(TAG, "done writing... " + mJpegData.length + + " bytes took " + (x2 - x1)); + } + } + long t3 = System.currentTimeMillis(); + if (VERBOSE) { + Log.v(TAG, String.format( + "CompressImageToFile.get took %d (%d, %d)", + (t3 - t1), (t2 - t1), (t3 - t2))); + } + return true; + } catch (FileNotFoundException ex) { + return false; + } catch (CanceledException ex) { + return false; + } catch (IOException ex) { + return false; + } finally { + Util.closeSiliently(mOutputStream); + acknowledgeCancel(); + } + } + } + + /** + * Take a given bitmap and compress it to a file as described + * by the Uri parameter. + * + * @param bitmap the bitmap to be compressed/stored + * @param uri where to store the bitmap + * @return true if we succeeded + */ + protected IGetBooleanCancelable compressImageToFile( + Bitmap bitmap, byte [] jpegData, Uri uri) { + return new CompressImageToFile(bitmap, jpegData, uri); + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof Image)) return false; + return fullSizeImageUri().equals(((Image) other).fullSizeImageUri()); + } + + @Override + public int hashCode() { + return fullSizeImageUri().toString().hashCode(); + } + + public Bitmap fullSizeBitmap(int targetWidthHeight) { + return fullSizeBitmap(targetWidthHeight, true); + } + + protected Bitmap fullSizeBitmap( + int targetWidthHeight, boolean rotateAsNeeded) { + Uri url = mContainer.contentUri(mId); + if (VERBOSE) Log.v(TAG, "getCreateBitmap for " + url); + if (url == null) return null; + + Bitmap b = makeBitmap(targetWidthHeight, url); + if (b != null && rotateAsNeeded) { + b = Util.rotate(b, getDegreesRotated()); + } + return b; + } + + private class LoadBitmapCancelable extends BaseCancelable + implements IGetBitmapCancelable { + private ParcelFileDescriptor mPFD; + private BitmapFactory.Options mOptions = new BitmapFactory.Options(); + private long mCancelInitiationTime; + private int mTargetWidthHeight; + + public LoadBitmapCancelable( + ParcelFileDescriptor pfdInput, int targetWidthHeight) { + mPFD = pfdInput; + mTargetWidthHeight = targetWidthHeight; + } + + @Override + public boolean doCancelWork() { + if (VERBOSE) Log.v(TAG, "requesting bitmap load cancel"); + mCancelInitiationTime = System.currentTimeMillis(); + mOptions.requestCancelDecode(); + return true; + } + + public Bitmap get() { + try { + Bitmap b = makeBitmap( + mTargetWidthHeight, fullSizeImageUri(), mPFD, mOptions); + if (mCancelInitiationTime != 0 && VERBOSE) { + Log.v(TAG, "cancelation of bitmap load success==" + + (b == null ? "TRUE" : "FALSE") + " -- took " + + (System.currentTimeMillis() + - mCancelInitiationTime)); + } + if (b != null) { + b = Util.rotate(b, getDegreesRotated()); + } + return b; + } catch (RuntimeException ex) { + return null; + } catch (Error e) { + return null; + } finally { + acknowledgeCancel(); + } + } + } + + + public IGetBitmapCancelable fullSizeBitmapCancelable( + int targetWidthHeight) { + try { + ParcelFileDescriptor pfdInput = mContentResolver + .openFileDescriptor(fullSizeImageUri(), "r"); + return new LoadBitmapCancelable(pfdInput, targetWidthHeight); + } catch (FileNotFoundException ex) { + return null; + } catch (UnsupportedOperationException ex) { + return null; + } + } + + public InputStream fullSizeImageData() { + try { + InputStream input = mContentResolver.openInputStream( + fullSizeImageUri()); + return input; + } catch (IOException ex) { + return null; + } + } + + public long fullSizeImageId() { + return mId; + } + + public Uri fullSizeImageUri() { + return mContainer.contentUri(mId); + } + + public IImageList getContainer() { + return mContainer; + } + + Cursor getCursor() { + return mContainer.getCursor(); + } + + public long getDateTaken() { + if (mContainer.indexDateTaken() < 0) return 0; + Cursor c = getCursor(); + synchronized (c) { + c.moveToPosition(getRow()); + return c.getLong(mContainer.indexDateTaken()); + } + } + + protected int getDegreesRotated() { + return 0; + } + + public String getMimeType() { + if (mContainer.indexMimeType() < 0) { + Cursor c = mContentResolver.query(fullSizeImageUri(), + new String[] { "_id", Images.Media.MIME_TYPE }, + null, null, null); + try { + return c.moveToFirst() ? c.getString(1) : ""; + } finally { + c.close(); + } + } else { + String mimeType = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + mimeType = c.getString(mContainer.indexMimeType()); + } + } + return mimeType; + } + } + + public String getDescription() { + if (mContainer.indexDescription() < 0) { + Cursor c = mContentResolver.query(fullSizeImageUri(), + new String[] { "_id", Images.Media.DESCRIPTION }, + null, null, null); + try { + return c.moveToFirst() ? c.getString(1) : ""; + } finally { + c.close(); + } + } else { + String description = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + description = c.getString(mContainer.indexDescription()); + } + } + return description; + } + } + + public boolean getIsPrivate() { + if (mContainer.indexPrivate() < 0) return false; + boolean isPrivate = false; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + isPrivate = c.getInt(mContainer.indexPrivate()) != 0; + } + } + return isPrivate; + } + + public double getLatitude() { + if (mContainer.indexLatitude() < 0) return 0D; + Cursor c = getCursor(); + synchronized (c) { + c.moveToPosition(getRow()); + return c.getDouble(mContainer.indexLatitude()); + } + } + + public double getLongitude() { + if (mContainer.indexLongitude() < 0) return 0D; + Cursor c = getCursor(); + synchronized (c) { + c.moveToPosition(getRow()); + return c.getDouble(mContainer.indexLongitude()); + } + } + + public String getTitle() { + String name = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + if (mContainer.indexTitle() != -1) { + name = c.getString(mContainer.indexTitle()); + } + } + } + return name != null && name.length() > 0 ? name : String.valueOf(mId); + } + + public String getDisplayName() { + if (mContainer.indexDisplayName() < 0) { + Cursor c = mContentResolver.query(fullSizeImageUri(), + new String[] { "_id", Images.Media.DISPLAY_NAME }, + null, null, null); + try { + if (c.moveToFirst()) return c.getString(1); + } finally { + c.close(); + } + } else { + String name = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + name = c.getString(mContainer.indexDisplayName()); + } + } + if (name != null && name.length() > 0) { + return name; + } + } + return String.valueOf(mId); + } + + public String getPicasaId() { + return null; + } + + public int getRow() { + return mCursorRow; + } + + public int getWidth() { + ParcelFileDescriptor input = null; + try { + Uri uri = fullSizeImageUri(); + input = mContentResolver.openFileDescriptor(uri, "r"); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor( + input.getFileDescriptor(), null, options); + return options.outWidth; + } catch (IOException ex) { + return 0; + } finally { + Util.closeSiliently(input); + } + } + + public int getHeight() { + ParcelFileDescriptor input = null; + try { + Uri uri = fullSizeImageUri(); + input = mContentResolver.openFileDescriptor(uri, "r"); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor( + input.getFileDescriptor(), null, options); + return options.outHeight; + } catch (IOException ex) { + return 0; + } finally { + Util.closeSiliently(input); + } + } + + public boolean hasLatLong() { + if (mContainer.indexLatitude() < 0 || mContainer.indexLongitude() < 0) { + return false; + } + Cursor c = getCursor(); + synchronized (c) { + c.moveToPosition(getRow()); + return !c.isNull(mContainer.indexLatitude()) + && !c.isNull(mContainer.indexLongitude()); + } + } + + public long imageId() { + return mId; + } + + /** + * Make a bitmap from a given Uri. + * + * @param uri + */ + private Bitmap makeBitmap(int targetWidthOrHeight, Uri uri) { + ParcelFileDescriptor input = null; + try { + input = mContentResolver.openFileDescriptor(uri, "r"); + return makeBitmap(targetWidthOrHeight, uri, input, null); + } catch (IOException ex) { + return null; + } finally { + Util.closeSiliently(input); + } + } + + protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, + ParcelFileDescriptor pfdInput, BitmapFactory.Options options) { + return mContainer.makeBitmap(targetWidthHeight, uri, pfdInput, options); + } + + public Bitmap miniThumbBitmap() { + try { + long id = mId; + long dbMagic = mMiniThumbMagic; + if (dbMagic == 0 || dbMagic == id) { + dbMagic = ((BaseImageList) getContainer()) + .checkThumbnail(this, getCursor(), getRow()); + if (VERBOSE) { + Log.v(TAG, "after computing thumbnail dbMagic is " + + dbMagic); + } + } + + synchronized (sMiniThumbData) { + dbMagic = mMiniThumbMagic; + byte [] data = mContainer.getMiniThumbFromFile(id, + sMiniThumbData, dbMagic); + if (data == null) { + byte[][] createdThumbData = new byte[1][]; + try { + dbMagic = ((BaseImageList) getContainer()) + .checkThumbnail(this, getCursor(), getRow(), + createdThumbData); + } catch (IOException ex) { + // Typically IOException because the sd card is full. + // But createdThumbData may have been filled in, so + // continue on. + } + data = createdThumbData[0]; + } + if (data == null) { + data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, + dbMagic); + } + if (data == null) { + if (VERBOSE) { + Log.v(TAG, "unable to get miniThumbBitmap," + + " data is null"); + } + } + if (data != null) { + Bitmap b = BitmapFactory.decodeByteArray(data, 0, + data.length); + if (b == null && VERBOSE) { + Log.v(TAG, "couldn't decode byte array, " + + "length was " + data.length); + } + return b; + } + } + return null; + } catch (Throwable ex) { + if (VERBOSE) { + Log.e(TAG, "miniThumbBitmap got exception " + ex.toString()); + for (StackTraceElement s : ex.getStackTrace()) + Log.e(TAG, "... " + s.toString()); + } + return null; + } + } + + public void onRemove() { + mContainer.mCache.remove(mId); + } + + protected void saveMiniThumb(Bitmap source) throws IOException { + mContainer.saveMiniThumbToFile(source, fullSizeImageId(), 0); + } + + public void setDescription(String description) { + if (mContainer.indexDescription() < 0) return; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + c.updateString(mContainer.indexDescription(), description); + } + } + } + + public void setIsPrivate(boolean isPrivate) { + if (mContainer.indexPrivate() < 0) return; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + c.updateInt(mContainer.indexPrivate(), isPrivate ? 1 : 0); + } + } + } + + public void setName(String name) { + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + c.updateString(mContainer.indexTitle(), name); + } + } + } + + public void setPicasaId(String id) { + Cursor c = null; + try { + c = mContentResolver.query(fullSizeImageUri(), + new String[] { "_id", Images.Media.PICASA_ID }, + null, null, null); + if (c != null && c.moveToFirst()) { + if (VERBOSE) { + Log.v(TAG, "storing picasaid " + id + " for " + + fullSizeImageUri()); + } + c.updateString(1, id); + c.commitUpdates(); + if (VERBOSE) { + Log.v(TAG, "updated image with picasa id " + id); + } + } + } finally { + if (c != null) + c.close(); + } + } + + public Uri thumbUri() { + Uri uri = fullSizeImageUri(); + // The value for the query parameter cannot be null :-(, + // so using a dummy "1" + uri = uri.buildUpon().appendQueryParameter("thumb", "1").build(); + return uri; + } + + @Override + public String toString() { + return fullSizeImageUri().toString(); + } +} diff --git a/src/com/android/camera/gallery/BaseImageList.java b/src/com/android/camera/gallery/BaseImageList.java new file mode 100644 index 0000000..388ffe8 --- /dev/null +++ b/src/com/android/camera/gallery/BaseImageList.java @@ -0,0 +1,870 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.os.ParcelFileDescriptor; +import android.provider.BaseColumns; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.MediaColumns; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.Images.Thumbnails; +import android.util.Log; + +import com.android.camera.ExifInterface; +import com.android.camera.ImageManager; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * A collection of <code>BaseImage</code>s. + */ +public abstract class BaseImageList implements IImageList { + private static final boolean VERBOSE = false; + private static final String TAG = "BaseImageList"; + + private static final int MINI_THUMB_TARGET_SIZE = 96; + private static final int THUMBNAIL_TARGET_SIZE = 320; + private static final int MINI_THUMB_DATA_FILE_VERSION = 3; + + private static final String WHERE_CLAUSE = + "(" + Images.Media.MIME_TYPE + " in (?, ?, ?))"; + + static final String[] IMAGE_PROJECTION = new String[] { + "_id", + "_data", + ImageColumns.DATE_TAKEN, + ImageColumns.MINI_THUMB_MAGIC, + ImageColumns.ORIENTATION, + ImageColumns.MIME_TYPE}; + + static final String[] THUMB_PROJECTION = new String[] { + BaseColumns._ID, // 0 + Images.Thumbnails.IMAGE_ID, // 1 + Images.Thumbnails.WIDTH, + Images.Thumbnails.HEIGHT}; + + static final int INDEX_ID = Util.indexOf(IMAGE_PROJECTION, "_id"); + static final int INDEX_DATA = Util.indexOf(IMAGE_PROJECTION, "_data"); + static final int INDEX_MIME_TYPE = + Util.indexOf(IMAGE_PROJECTION, MediaColumns.MIME_TYPE); + static final int INDEX_DATE_TAKEN = + Util.indexOf(IMAGE_PROJECTION, ImageColumns.DATE_TAKEN); + static final int INDEX_MINI_THUMB_MAGIC = + Util.indexOf(IMAGE_PROJECTION, ImageColumns.MINI_THUMB_MAGIC); + static final int INDEX_ORIENTATION = + Util.indexOf(IMAGE_PROJECTION, ImageColumns.ORIENTATION); + static final int INDEX_THUMB_ID = + Util.indexOf(THUMB_PROJECTION, BaseColumns._ID); + static final int INDEX_THUMB_IMAGE_ID = + Util.indexOf(THUMB_PROJECTION, Images.Thumbnails.IMAGE_ID); + static final int INDEX_THUMB_WIDTH = + Util.indexOf(THUMB_PROJECTION, Images.Thumbnails.WIDTH); + static final int INDEX_THUMB_HEIGHT = + Util.indexOf(THUMB_PROJECTION, Images.Thumbnails.HEIGHT); + + protected static final String[] ACCEPTABLE_IMAGE_TYPES = + new String[] { "image/jpeg", "image/png", "image/gif" }; + protected static final String MINITHUMB_IS_NULL = "mini_thumb_magic isnull"; + + protected ContentResolver mContentResolver; + protected int mSort; + protected Uri mBaseUri; + protected Cursor mCursor; + protected IImageList.OnChange mListener = null; + protected boolean mCursorDeactivated; + protected String mBucketId; + protected Context mContext; + protected Uri mUri; + protected HashMap<Long, IImage> mCache = new HashMap<Long, IImage>(); + protected RandomAccessFile mMiniThumbData; + protected Uri mThumbUri; + + public BaseImageList(Context ctx, ContentResolver cr, Uri uri, int sort, + String bucketId) { + mContext = ctx; + mSort = sort; + mUri = uri; + mBaseUri = uri; + mBucketId = bucketId; + mContentResolver = cr; + } + + String randomAccessFilePath(int version) { + String directoryName = + Environment.getExternalStorageDirectory().toString() + + "/DCIM/.thumbnails"; + return directoryName + "/.thumbdata" + version + "-" + mUri.hashCode(); + } + + RandomAccessFile miniThumbDataFile() { + if (mMiniThumbData == null) { + String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION); + File directory = new File(new File(path).getParent()); + if (!directory.isDirectory()) { + if (!directory.mkdirs()) { + Log.e(TAG, "!!!! unable to create .thumbnails directory " + + directory.toString()); + } + } + File f = new File(path); + if (VERBOSE) Log.v(TAG, "file f is " + f.toString()); + try { + mMiniThumbData = new RandomAccessFile(f, "rw"); + } catch (IOException ex) { + // ignore exception + } + } + return mMiniThumbData; + } + + /** + * Store a given thumbnail in the database. + */ + protected Bitmap storeThumbnail(Bitmap thumb, long imageId) { + if (thumb == null) return null; + try { + Uri uri = getThumbnailUri( + imageId, thumb.getWidth(), thumb.getHeight()); + if (uri == null) { + return thumb; + } + OutputStream thumbOut = mContentResolver.openOutputStream(uri); + thumb.compress(Bitmap.CompressFormat.JPEG, 60, thumbOut); + thumbOut.close(); + return thumb; + } catch (Exception ex) { + if (VERBOSE) Log.d(TAG, "unable to store thumbnail: " + ex); + return thumb; + } + } + + /** + * Store a JPEG thumbnail from the EXIF header in the database. + */ + protected boolean storeThumbnail( + byte[] jpegThumbnail, long imageId, int width, int height) { + if (jpegThumbnail == null) return false; + + Uri uri = getThumbnailUri(imageId, width, height); + if (uri == null) { + return false; + } + try { + OutputStream thumbOut = mContentResolver.openOutputStream(uri); + thumbOut.write(jpegThumbnail); + thumbOut.close(); + return true; + } catch (FileNotFoundException ex) { + return false; + } catch (IOException ex) { + return false; + } + } + + private Uri getThumbnailUri(long imageId, int width, int height) { + // we do not store thumbnails for DRM'd images + if (mThumbUri == null) { + return null; + } + + Uri uri = null; + Cursor c = mContentResolver.query(mThumbUri, THUMB_PROJECTION, + Thumbnails.IMAGE_ID + "=?", + new String[]{String.valueOf(imageId)}, null); + try { + if (c.moveToFirst()) { + // If, for some reaosn, we already have a row with a matching + // image id, then just update that row rather than creating a + // new row. + uri = ContentUris.withAppendedId( + mThumbUri, c.getLong(indexThumbId())); + c.commitUpdates(); + } + } finally { + c.close(); + } + if (uri == null) { + ContentValues values = new ContentValues(4); + values.put(Images.Thumbnails.KIND, Images.Thumbnails.MINI_KIND); + values.put(Images.Thumbnails.IMAGE_ID, imageId); + values.put(Images.Thumbnails.HEIGHT, height); + values.put(Images.Thumbnails.WIDTH, width); + uri = mContentResolver.insert(mThumbUri, values); + } + return uri; + } + + private static final java.util.Random sRandom = + new java.util.Random(System.currentTimeMillis()); + + protected SomewhatFairLock mLock = new SomewhatFairLock(); + + private static class SomewhatFairLock { + private boolean mLocked = false; + private ArrayList<Thread> mWaiting = new ArrayList<Thread>(); + + public synchronized void lock() { + while (mLocked) { + try { + mWaiting.add(Thread.currentThread()); + wait(); + if (mWaiting.get(0) == Thread.currentThread()) { + mWaiting.remove(0); + break; + } + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + mLocked = true; + } + + public synchronized void unlock() { + mLocked = false; + notifyAll(); + } + } + + // If the photo has an EXIF thumbnail and it's big enough, extract it and + // save that JPEG as the large thumbnail without re-encoding it. We still + // have to decompress it though, in order to generate the minithumb. + private Bitmap createThumbnailFromEXIF(String filePath, long id) { + if (filePath == null) return null; + + byte [] thumbData = null; + synchronized (ImageManager.instance()) { + thumbData = (new ExifInterface(filePath)).getThumbnail(); + } + if (thumbData == null) return null; + + // Sniff the size of the EXIF thumbnail before decoding it. Photos + // from the device will pass, but images that are side loaded from + // other cameras may not. + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options); + int width = options.outWidth; + int height = options.outHeight; + if (width >= THUMBNAIL_TARGET_SIZE && height >= THUMBNAIL_TARGET_SIZE + && storeThumbnail(thumbData, id, width, height)) { + // this is used for *encoding* the minithumb, so + // we don't want to dither or convert to 565 here. + // + // Decode with a scaling factor + // to match MINI_THUMB_TARGET_SIZE closely + // which will produce much better scaling quality + // and is significantly faster. + options.inSampleSize = + Util.computeSampleSize(options, THUMBNAIL_TARGET_SIZE); + + if (VERBOSE) { + Log.v(TAG, "in createThumbnailFromExif using inSampleSize of " + + options.inSampleSize); + } + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + options.inJustDecodeBounds = false; + return BitmapFactory.decodeByteArray( + thumbData, 0, thumbData.length, options); + } + return null; + } + + // The fallback case is to decode the original photo to thumbnail size, + // then encode it as a JPEG. We return the thumbnail Bitmap in order to + // create the minithumb from it. + private Bitmap createThumbnailFromUri(Cursor c, long id) { + Uri uri = ContentUris.withAppendedId(mBaseUri, id); + Bitmap bitmap = makeBitmap(THUMBNAIL_TARGET_SIZE, uri, null, null); + if (bitmap != null) { + storeThumbnail(bitmap, id); + } else { + uri = ContentUris.withAppendedId(mBaseUri, id); + bitmap = makeBitmap(MINI_THUMB_TARGET_SIZE, uri, null, null); + } + return bitmap; + } + + // returns id + public long checkThumbnail(BaseImage existingImage, Cursor c, int i) + throws IOException { + return checkThumbnail(existingImage, c, i, null); + } + + /** + * Checks to see if a mini thumbnail exists in the cache. If not, tries to + * create it and add it to the cache. + * @param existingImage + * @param c + * @param i + * @param createdThumbnailData if this parameter is non-null, and a new + * mini-thumbnail bitmap is created, the new bitmap's data will be + * stored in createdThumbnailData[0]. Note that if the sdcard is + * full, it's possible that createdThumbnailData[0] will be set + * even if the method throws an IOException. This is actually + * useful, because it allows the caller to use the created + * thumbnail even if the sdcard is full. + * @throws IOException + */ + public long checkThumbnail(BaseImage existingImage, Cursor c, int i, + byte[][] createdThumbnailData) throws IOException { + long magic, fileMagic = 0, id; + mLock.lock(); + try { + if (existingImage == null) { + // if we don't have an Image object then get the id and magic + // from the cursor. Synchronize on the cursor object. + synchronized (c) { + if (!c.moveToPosition(i)) { + return -1; + } + magic = c.getLong(indexMiniThumbId()); + id = c.getLong(indexId()); + } + } else { + // if we have an Image object then ask them for the magic/id + magic = existingImage.mMiniThumbMagic; + id = existingImage.fullSizeImageId(); + } + + if (magic != 0) { + // check the mini thumb file for the right data. Right is + // defined as having the right magic number at the offset + // reserved for this "id". + RandomAccessFile r = miniThumbDataFile(); + if (r != null) { + synchronized (r) { + long pos = id * BaseImage.BYTES_PER_MINTHUMB; + try { + // check that we can read the following 9 bytes + // (1 for the "status" and 8 for the long) + if (r.length() >= pos + 1 + 8) { + r.seek(pos); + if (r.readByte() == 1) { + fileMagic = r.readLong(); + if (fileMagic == magic && magic != 0 + && magic != id) { + return magic; + } + } + } + } catch (IOException ex) { + Log.v(TAG, "got exception checking file magic: " + + ex); + } + } + } + if (VERBOSE) { + Log.v(TAG, "didn't verify... fileMagic: " + fileMagic + + "; magic: " + magic + "; id: " + id + "; "); + } + } + + // If we can't retrieve the thumbnail, first check if there is one + // embedded in the EXIF data. If not, or it's not big enough, + // decompress the full size image. + Bitmap bitmap = null; + String filePath = null; + synchronized (c) { + if (c.moveToPosition(i)) { + filePath = c.getString(indexData()); + } + } + if (filePath != null) { + String mimeType = c.getString(indexMimeType()); + boolean isVideo = Util.isVideoMimeType(mimeType); + if (isVideo) { + bitmap = Util.createVideoThumbnail(filePath); + } else { + bitmap = createThumbnailFromEXIF(filePath, id); + if (bitmap == null) { + bitmap = createThumbnailFromUri(c, id); + } + } + synchronized (c) { + int degrees = 0; + if (c.moveToPosition(i)) { + int column = indexOrientation(); + if (column >= 0) degrees = c.getInt(column); + } + if (degrees != 0) { + bitmap = Util.rotate(bitmap, degrees); + } + } + } + + // make a new magic number since things are out of sync + do { + magic = sRandom.nextLong(); + } while (magic == 0); + if (bitmap != null) { + byte [] data = Util.miniThumbData(bitmap); + if (createdThumbnailData != null) { + createdThumbnailData[0] = data; + } + saveMiniThumbToFile(data, id, magic); + } + + synchronized (c) { + c.moveToPosition(i); + c.updateLong(indexMiniThumbId(), magic); + c.commitUpdates(); + c.requery(); + c.moveToPosition(i); + + if (existingImage != null) { + existingImage.mMiniThumbMagic = magic; + } + return magic; + } + } finally { + mLock.unlock(); + } + } + + public void checkThumbnails(ThumbCheckCallback cb, int totalThumbnails) { + Cursor c = Images.Media.query(mContentResolver, mBaseUri, + new String[] { "_id", "mini_thumb_magic" }, + thumbnailWhereClause(), thumbnailWhereClauseArgs(), + "_id ASC"); + + int count = c.getCount(); + if (VERBOSE) { + Log.v(TAG, ">>>>>>>>>>> need to check " + c.getCount() + " rows"); + } + c.close(); + + if (!ImageManager.hasStorage()) { + if (VERBOSE) { + Log.v(TAG, "bailing from the image checker thread " + + "-- no storage"); + } + return; + } + + String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1); + File oldFile = new File(oldPath); + + if (count == 0) { + // now check that we have the right thumbs file + if (!oldFile.exists()) { + return; + } + } + + c = getCursor(); + try { + if (VERBOSE) Log.v(TAG, "checkThumbnails found " + c.getCount()); + int current = 0; + for (int i = 0; i < c.getCount(); i++) { + try { + checkThumbnail(null, c, i); + } catch (IOException ex) { + Log.e(TAG, "!!!!! failed to check thumbnail..." + + " was the sd card removed? - " + ex.getMessage()); + break; + } + if (cb != null) { + if (!cb.checking(current, totalThumbnails)) { + if (VERBOSE) { + Log.v(TAG, "got false from checking... break"); + } + break; + } + } + current += 1; + } + } finally { + if (VERBOSE) { + Log.v(TAG, "checkThumbnails existing after reaching count " + + c.getCount()); + } + try { + oldFile.delete(); + } catch (SecurityException ex) { + // ignore + } + } + } + + protected String thumbnailWhereClause() { + return MINITHUMB_IS_NULL + " and " + WHERE_CLAUSE; + } + + protected String[] thumbnailWhereClauseArgs() { + return ACCEPTABLE_IMAGE_TYPES; + } + + public void commitChanges() { + synchronized (mCursor) { + mCursor.commitUpdates(); + requery(); + } + } + protected Uri contentUri(long id) { + try { + // does our uri already have an id (single image query)? + // if so just return it + long existingId = ContentUris.parseId(mBaseUri); + if (existingId != id) Log.e(TAG, "id mismatch"); + return mBaseUri; + } catch (NumberFormatException ex) { + // otherwise tack on the id + return ContentUris.withAppendedId(mBaseUri, id); + } + } + + public void deactivate() { + mCursorDeactivated = true; + try { + mCursor.deactivate(); + } catch (IllegalStateException e) { + // IllegalStateException may be thrown if the cursor is stale. + Log.e(TAG, "Caught exception while deactivating cursor.", e); + } + if (mMiniThumbData != null) { + try { + mMiniThumbData.close(); + mMiniThumbData = null; + } catch (IOException ex) { + // ignore exception + } + } + } + + public void dump(String msg) { + int count = getCount(); + if (VERBOSE) { + Log.v(TAG, "dump ImageList (count is " + count + ") " + msg); + } + for (int i = 0; i < count; i++) { + IImage img = getImageAt(i); + if (VERBOSE) Log.v(TAG, " " + i + ": " + img); + } + if (VERBOSE) Log.v(TAG, "end of dump container"); + } + + public int getCount() { + Cursor c = getCursor(); + synchronized (c) { + try { + return c.getCount(); + } catch (RuntimeException ex) { + return 0; + } + } + } + + public boolean isEmpty() { + return getCount() == 0; + } + + protected Cursor getCursor() { + synchronized (mCursor) { + if (mCursorDeactivated) { + activateCursor(); + } + return mCursor; + } + } + + protected void activateCursor() { + requery(); + } + + public IImage getImageAt(int i) { + Cursor c = getCursor(); + synchronized (c) { + boolean moved; + try { + moved = c.moveToPosition(i); + } catch (RuntimeException ex) { + return null; + } + if (moved) { + try { + long id = c.getLong(0); + long miniThumbId = 0; + int rotation = 0; + if (indexMiniThumbId() != -1) { + miniThumbId = c.getLong(indexMiniThumbId()); + } + if (indexOrientation() != -1) { + rotation = c.getInt(indexOrientation()); + } + long timestamp = c.getLong(1); + IImage img = mCache.get(id); + if (img == null) { + img = make(id, miniThumbId, mContentResolver, this, + timestamp, i, rotation); + mCache.put(id, img); + } + return img; + } catch (RuntimeException ex) { + Log.e(TAG, "got this exception trying to create image: " + + ex); + return null; + } + } else { + Log.e(TAG, "unable to moveTo to " + i + "; count is " + + c.getCount()); + return null; + } + } + } + + public IImage getImageForUri(Uri uri) { + // TODO: make this a hash lookup + for (int i = 0; i < getCount(); i++) { + if (getImageAt(i).fullSizeImageUri().equals(uri)) { + return getImageAt(i); + } + } + return null; + } + + byte [] getMiniThumbFromFile(long id, byte [] data, long magicCheck) { + RandomAccessFile r = miniThumbDataFile(); + if (r == null) return null; + + long pos = id * BaseImage.BYTES_PER_MINTHUMB; + synchronized (r) { + try { + r.seek(pos); + if (r.readByte() == 1) { + long magic = r.readLong(); + if (magic != magicCheck) { + if (VERBOSE) { + Log.v(TAG, "for id " + id + "; magic: " + magic + + "; magicCheck: " + magicCheck + + " (fail)"); + } + return null; + } + int length = r.readInt(); + r.read(data, 0, length); + return data; + } else { + return null; + } + } catch (IOException ex) { + long fileLength; + try { + fileLength = r.length(); + } catch (IOException ex1) { + fileLength = -1; + } + if (VERBOSE) { + Log.e(TAG, "couldn't read thumbnail for " + id + "; " + + ex.toString() + "; pos is " + pos + "; length is " + + fileLength); + } + return null; + } + } + } + protected int getRowFor(IImage imageObj) { + Cursor c = getCursor(); + synchronized (c) { + int index = 0; + long targetId = imageObj.fullSizeImageId(); + if (c.moveToFirst()) { + do { + if (c.getLong(0) == targetId) { + return index; + } + index += 1; + } while (c.moveToNext()); + } + return -1; + } + } + + protected abstract int indexOrientation(); + + protected abstract int indexDateTaken(); + + protected abstract int indexDescription(); + + protected abstract int indexMimeType(); + + protected abstract int indexData(); + + protected abstract int indexId(); + + protected abstract int indexLatitude(); + + protected abstract int indexLongitude(); + + protected abstract int indexMiniThumbId(); + + protected abstract int indexPicasaWeb(); + + protected abstract int indexPrivate(); + + protected abstract int indexTitle(); + + protected abstract int indexDisplayName(); + + protected abstract int indexThumbId(); + + protected IImage make(long id, long miniThumbId, ContentResolver cr, + IImageList list, long timestamp, int index, int rotation) { + return null; + } + + protected abstract Bitmap makeBitmap( + int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, + BitmapFactory.Options options); + + public boolean removeImage(IImage image) { + Cursor c = getCursor(); + synchronized (c) { + /* + * TODO: consider putting the image in a holding area so + * we can get it back as needed + * TODO: need to delete the thumbnails as well + */ + boolean moved; + try { + moved = c.moveToPosition(image.getRow()); + } catch (RuntimeException ex) { + Log.e(TAG, "removeImage got exception " + ex.toString()); + return false; + } + if (moved) { + Uri u = image.fullSizeImageUri(); + mContentResolver.delete(u, null, null); + image.onRemove(); + requery(); + } + } + return true; + } + + public void removeImageAt(int i) { + Cursor c = getCursor(); + synchronized (c) { + /* + * TODO: consider putting the image in a holding area so + * we can get it back as needed + * TODO: need to delete the thumbnails as well + */ + dump("before delete"); + IImage image = getImageAt(i); + boolean moved; + try { + moved = c.moveToPosition(i); + } catch (RuntimeException ex) { + Log.e(TAG, "removeImageAt " + i + " get " + ex); + return; + } + if (moved) { + Uri u = image.fullSizeImageUri(); + mContentResolver.delete(u, null, null); + requery(); + image.onRemove(); + } + dump("after delete"); + } + } + + public void removeOnChangeListener(OnChange changeCallback) { + if (changeCallback == mListener) mListener = null; + } + + protected void requery() { + mCache.clear(); + mCursor.requery(); + mCursorDeactivated = false; + } + + protected void saveMiniThumbToFile(Bitmap bitmap, long id, long magic) + throws IOException { + byte[] data = Util.miniThumbData(bitmap); + saveMiniThumbToFile(data, id, magic); + } + + protected void saveMiniThumbToFile(byte[] data, long id, long magic) + throws IOException { + RandomAccessFile r = miniThumbDataFile(); + if (r == null) return; + + long pos = id * BaseImage.BYTES_PER_MINTHUMB; + long t0 = System.currentTimeMillis(); + synchronized (r) { + try { + long t1 = System.currentTimeMillis(); + long t2 = System.currentTimeMillis(); + if (data != null) { + if (data.length > BaseImage.BYTES_PER_MINTHUMB) { + if (VERBOSE) { + Log.v(TAG, "warning: " + data.length + " > " + + BaseImage.BYTES_PER_MINTHUMB); + } + return; + } + r.seek(pos); + r.writeByte(0); // we have no data in this slot + + // if magic is 0 then leave it alone + if (magic == 0) { + r.skipBytes(8); + } else { + r.writeLong(magic); + } + r.writeInt(data.length); + r.write(data); + // f.flush(); + r.seek(pos); + r.writeByte(1); // we have data in this slot + long t3 = System.currentTimeMillis(); + + if (VERBOSE) { + Log.v(TAG, "saveMiniThumbToFile took " + (t3 - t0) + + "; " + (t1 - t0) + " " + (t2 - t1) + " " + + (t3 - t2)); + } + } + } catch (IOException ex) { + Log.e(TAG, "couldn't save mini thumbnail data for " + + id + "; " + ex.toString()); + throw ex; + } + } + } + + public void setOnChangeListener(OnChange changeCallback, Handler h) { + mListener = changeCallback; + } +} diff --git a/src/com/android/camera/gallery/CanceledException.java b/src/com/android/camera/gallery/CanceledException.java new file mode 100644 index 0000000..02a6b31 --- /dev/null +++ b/src/com/android/camera/gallery/CanceledException.java @@ -0,0 +1,9 @@ +// Copyright 2009 Google Inc. All Rights Reserved. + +package com.android.camera.gallery; + +/** + * Exception which will be thrown when the task has been canceled. + */ +public class CanceledException extends Exception { +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/DrmImageList.java b/src/com/android/camera/gallery/DrmImageList.java new file mode 100644 index 0000000..bceb6ab --- /dev/null +++ b/src/com/android/camera/gallery/DrmImageList.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import com.android.camera.ImageManager; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.provider.DrmStore; + +/** + * Represents an ordered collection of Image objects from the DRM provider. + */ +public class DrmImageList extends ImageList implements IImageList { + + private static final String[] DRM_IMAGE_PROJECTION = new String[] { + DrmStore.Audio._ID, + DrmStore.Audio.DATA, + DrmStore.Audio.MIME_TYPE, + }; + + public DrmImageList(Context ctx, ContentResolver cr, Uri imageUri, + int sort, String bucketId) { + super(ctx, cr, imageUri, null, sort, bucketId); + } + + @Override + protected Cursor createCursor() { + return mContentResolver.query( + mBaseUri, DRM_IMAGE_PROJECTION, null, null, sortOrder()); + } + + @Override + public void checkThumbnails( + IImageList.ThumbCheckCallback cb, int totalCount) { + // do nothing + } + + @Override + public long checkThumbnail(BaseImage existingImage, Cursor c, int i) { + return 0; + } + + private class DrmImage extends Image { + + protected DrmImage(long id, ContentResolver cr, + BaseImageList container, int cursorRow) { + super(id, 0, cr, container, cursorRow, 0); + } + + @Override + public boolean isDrm() { + return true; + } + + @Override + public boolean isReadonly() { + return true; + } + + @Override + public Bitmap miniThumbBitmap() { + return fullSizeBitmap(ImageManager.MINI_THUMB_TARGET_SIZE); + } + + @Override + public Bitmap thumbBitmap() { + return fullSizeBitmap(ImageManager.THUMBNAIL_TARGET_SIZE); + } + + @Override + public String getDisplayName() { + return getTitle(); + } + } + + @Override + protected IImage make(long id, long miniThumbId, ContentResolver cr, + IImageList list, long timestamp, int index, int rotation) { + return new DrmImage(id, mContentResolver, this, index); + } + + @Override + protected int indexOrientation() { + return -1; + } + + @Override + protected int indexDateTaken() { + return -1; + } + + @Override + protected int indexDescription() { + return -1; + } + + @Override + protected int indexMimeType() { + return -1; + } + + @Override + protected int indexId() { + return -1; + } + + @Override + protected int indexLatitude() { + return -1; + } + + @Override + protected int indexLongitude() { + return -1; + } + + @Override + protected int indexMiniThumbId() { + return -1; + } + + @Override + protected int indexPicasaWeb() { + return -1; + } + + @Override + protected int indexPrivate() { + return -1; + } + + @Override + protected int indexTitle() { + return -1; + } + + @Override + protected int indexDisplayName() { + return -1; + } + + @Override + protected int indexThumbId() { + return -1; + } + + // TODO: Review this probably should be based on DATE_TAKEN same as images + private String sortOrder() { + String ascending = + mSort == ImageManager.SORT_ASCENDING ? " ASC" : " DESC"; + return DrmStore.Images.TITLE + ascending + "," + DrmStore.Images._ID; + } +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/IAddImageCancelable.java b/src/com/android/camera/gallery/IAddImageCancelable.java new file mode 100644 index 0000000..30a6880 --- /dev/null +++ b/src/com/android/camera/gallery/IAddImageCancelable.java @@ -0,0 +1,10 @@ +// Copyright 2009 Google Inc. All Rights Reserved. + +package com.android.camera.gallery; + +/** + * Cancelable interface for add image task. + */ +public interface IAddImageCancelable extends ICancelable { + public void get(); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/ICancelable.java b/src/com/android/camera/gallery/ICancelable.java new file mode 100644 index 0000000..ca02e79 --- /dev/null +++ b/src/com/android/camera/gallery/ICancelable.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +/** + * The interface for all the tasks that could be canceled. + */ +public interface ICancelable { + /* + * call cancel() when the unit of work in progress needs to be + * canceled. This should return true if it was possible to + * cancel and false otherwise. If this returns false the caller + * may still be able to cleanup and simulate cancelation. + */ + public boolean cancel(); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/IGetBitmapCancelable.java b/src/com/android/camera/gallery/IGetBitmapCancelable.java new file mode 100644 index 0000000..32f8b91 --- /dev/null +++ b/src/com/android/camera/gallery/IGetBitmapCancelable.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.graphics.Bitmap; + +/** + * An <code>ICancelable</code> interface which will return a bitmap. + */ +public interface IGetBitmapCancelable extends ICancelable { + // returns the bitmap or null if there was an error or we were canceled + public Bitmap get(); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/IGetBooleanCancelable.java b/src/com/android/camera/gallery/IGetBooleanCancelable.java new file mode 100644 index 0000000..c85f223 --- /dev/null +++ b/src/com/android/camera/gallery/IGetBooleanCancelable.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +/** + * An <code>ICancelable</code> interface which will return a boolean value. + */ +public interface IGetBooleanCancelable extends ICancelable { + public boolean get(); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/IImage.java b/src/com/android/camera/gallery/IImage.java new file mode 100644 index 0000000..d2cad98 --- /dev/null +++ b/src/com/android/camera/gallery/IImage.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.graphics.Bitmap; +import android.net.Uri; + +import java.io.InputStream; + +/** + * The interface of all images used in gallery. + */ +public interface IImage { + + public abstract void commitChanges(); + + /** + * Get the bitmap for the full size image. + * @return the bitmap for the full size image. + */ + public abstract Bitmap fullSizeBitmap(int targetWidthOrHeight); + + /** + * + * @return an object which can be canceled while the bitmap is loading + */ + public abstract IGetBitmapCancelable fullSizeBitmapCancelable( + int targetWidthOrHeight); + + /** + * Gets the input stream associated with a given full size image. + * This is used, for example, if one wants to email or upload + * the image. + * @return the InputStream associated with the image. + */ + public abstract InputStream fullSizeImageData(); + public abstract long fullSizeImageId(); + public abstract Uri fullSizeImageUri(); + public abstract IImageList getContainer(); + public abstract long getDateTaken(); + + /** + * Gets the description of the image. + * @return the description of the image. + */ + public abstract String getDescription(); + public abstract String getMimeType(); + public abstract int getHeight(); + + /** + * Gets the flag telling whether this video/photo is private or public. + * @return the description of the image. + */ + public abstract boolean getIsPrivate(); + + public abstract double getLatitude(); + + public abstract double getLongitude(); + + /** + * Gets the name of the image. + * @return the name of the image. + */ + public abstract String getTitle(); + + public abstract String getDisplayName(); + + public abstract String getPicasaId(); + + public abstract int getRow(); + + public abstract int getWidth(); + + public abstract boolean hasLatLong(); + + public abstract long imageId(); + + public abstract boolean isReadonly(); + + public abstract boolean isDrm(); + + public abstract Bitmap miniThumbBitmap(); + + public abstract void onRemove(); + + public abstract boolean rotateImageBy(int degrees); + + /** + * Sets the description of the image. + */ + public abstract void setDescription(String description); + + /** + * Sets whether the video/photo is private or public. + */ + public abstract void setIsPrivate(boolean isPrivate); + + /** + * Sets the name of the image. + */ + public abstract void setName(String name); + + public abstract void setPicasaId(String id); + + /** + * Get the bitmap for the medium thumbnail. + * @return the bitmap for the medium thumbnail. + */ + public abstract Bitmap thumbBitmap(); + + public abstract Uri thumbUri(); + + public abstract String getDataPath(); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/IImageList.java b/src/com/android/camera/gallery/IImageList.java new file mode 100644 index 0000000..04faccd --- /dev/null +++ b/src/com/android/camera/gallery/IImageList.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.net.Uri; +import android.os.Handler; + +import java.util.HashMap; + +/** + * The interface of all image collections used in gallery. + */ +public interface IImageList { + public HashMap<String, String> getBucketIds(); + + /** + * Notify interface when the image list has been changed. + */ + public interface OnChange { + public void onChange(IImageList list); + } + + /** + * Notify interface of how many thumbnails are processed. + */ + public interface ThumbCheckCallback { + public boolean checking(int current, int count); + } + + public abstract void checkThumbnails( + IImageList.ThumbCheckCallback cb, int totalCount); + + public abstract void commitChanges(); + + public abstract void deactivate(); + + /** + * Returns the count of image objects. + * + * @return the number of images + */ + public abstract int getCount(); + + /** + * @return true if the count of image objects is zero. + */ + + public abstract boolean isEmpty(); + + /** + * Returns the image at the ith position. + * + * @param i the position + * @return the image at the ith position + */ + public abstract IImage getImageAt(int i); + + /** + * Returns the image with a particular Uri. + * + * @param uri + * @return the image with a particular Uri. + */ + public abstract IImage getImageForUri(Uri uri); + + /** + * + * @param image + * @return true if the image was removed. + */ + public abstract boolean removeImage(IImage image); + + /** + * Removes the image at the ith position. + * @param i the position + */ + public abstract void removeImageAt(int i); + + public abstract void removeOnChangeListener( + IImageList.OnChange changeCallback); + + public abstract void setOnChangeListener( + IImageList.OnChange changeCallback, Handler h); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/Image.java b/src/com/android/camera/gallery/Image.java new file mode 100644 index 0000000..40d780c --- /dev/null +++ b/src/com/android/camera/gallery/Image.java @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore.Images.Thumbnails; +import android.util.Log; + +import com.android.camera.ExifInterface; +import com.android.camera.ImageManager; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; + +/** + * The class for normal images in gallery. + */ +public class Image extends BaseImage implements IImage { + private static final boolean VERBOSE = false; + private static final String TAG = "BaseImage"; + + private int mRotation; + + public Image(long id, long miniThumbId, ContentResolver cr, + BaseImageList container, int cursorRow, int rotation) { + super(id, miniThumbId, cr, container, cursorRow); + mRotation = rotation; + } + + public String getDataPath() { + String path = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + int column = ((ImageList) getContainer()).indexData(); + if (column >= 0) + path = c.getString(column); + } + } + return path; + } + + @Override + protected int getDegreesRotated() { + return mRotation; + } + + protected void setDegreesRotated(int degrees) { + Cursor c = getCursor(); + mRotation = degrees; + synchronized (c) { + if (c.moveToPosition(getRow())) { + int column = ((ImageList) getContainer()).indexOrientation(); + if (column >= 0) { + c.updateInt(column, degrees); + getContainer().commitChanges(); + } + } + } + } + + @Override + protected Bitmap.CompressFormat compressionType() { + String mimeType = getMimeType(); + if ("image/png".equals(mimeType)) { + return Bitmap.CompressFormat.PNG; + } else if ("image/gif".equals(mimeType)) { + return Bitmap.CompressFormat.PNG; + } + return Bitmap.CompressFormat.JPEG; + } + + /** + * Does not replace the tag if already there. Otherwise, adds to the exif + * tags. + * + * @param tag + * @param value + */ + public void addExifTag(String tag, String value) { + if (mExifData == null) { + mExifData = new HashMap<String, String>(); + } + if (!mExifData.containsKey(tag)) { + mExifData.put(tag, value); + } else { + if (VERBOSE) { + Log.v(TAG, "addExifTag where the key already was there: " + + tag + " = " + value); + } + } + } + + /** + * Return the value of the Exif tag as an int. Returns 0 on any type of + * error. + * + * @param tag + */ + public int getExifTagInt(String tag) { + if (mExifData != null) { + String tagValue = mExifData.get(tag); + if (tagValue != null) { + return Integer.parseInt(tagValue); + } + } + return 0; + } + + public boolean isReadonly() { + String mimeType = getMimeType(); + return !"image/jpeg".equals(mimeType) && !"image/png".equals(mimeType); + } + + public boolean isDrm() { + return false; + } + + /** + * Remove tag if already there. Otherwise, does nothing. + * @param tag + */ + public void removeExifTag(String tag) { + if (mExifData == null) { + mExifData = new HashMap<String, String>(); + } + mExifData.remove(tag); + } + + /** + * Replaces the tag if already there. Otherwise, adds to the exif tags. + * @param tag + * @param value + */ + public void replaceExifTag(String tag, String value) { + if (mExifData == null) { + mExifData = new HashMap<String, String>(); + } + if (!mExifData.containsKey(tag)) { + mExifData.remove(tag); + } + mExifData.put(tag, value); + } + + private class SaveImageContentsCancelable extends BaseCancelable + implements IGetBooleanCancelable { + private Bitmap mImage; + private byte [] mJpegData; + private int mOrientation; + private Cursor mCursor; + IGetBooleanCancelable mCurrentCancelable = null; + + SaveImageContentsCancelable(Bitmap image, byte[] jpegData, + int orientation, Cursor cursor) { + mImage = image; + mJpegData = jpegData; + mOrientation = orientation; + mCursor = cursor; + } + + @Override + public boolean doCancelWork() { + synchronized (this) { + if (mCurrentCancelable != null) mCurrentCancelable.cancel(); + } + return true; + } + + public boolean get() { + try { + Bitmap thumbnail = null; + + long t1 = System.currentTimeMillis(); + Uri uri = mContainer.contentUri(mId); + synchronized (this) { + checkCanceled(); + mCurrentCancelable = + compressImageToFile(mImage, mJpegData, uri); + } + + long t2 = System.currentTimeMillis(); + if (!mCurrentCancelable.get()) return false; + + synchronized (this) { + String filePath; + synchronized (mCursor) { + mCursor.moveToPosition(0); + filePath = mCursor.getString(2); + } + // TODO: If thumbData is present and usable, we should call + // the version of storeThumbnail which takes a byte array, + // rather than re-encoding a new JPEG of the same + // dimensions. + + byte[] thumbData = null; + synchronized (ImageManager.instance()) { + thumbData = + (new ExifInterface(filePath)).getThumbnail(); + } + if (VERBOSE) { + Log.v(TAG, "for file " + filePath + " thumbData is " + + thumbData + "; length " + + (thumbData != null ? thumbData.length : -1)); + } + + if (thumbData != null) { + thumbnail = BitmapFactory.decodeByteArray( + thumbData, 0, thumbData.length); + if (VERBOSE) { + Log.v(TAG, "embedded thumbnail bitmap " + + thumbnail.getWidth() + "/" + + thumbnail.getHeight()); + } + } + if (thumbnail == null && mImage != null) { + thumbnail = mImage; + } + if (thumbnail == null && mJpegData != null) { + thumbnail = BitmapFactory.decodeByteArray( + mJpegData, 0, mJpegData.length); + } + } + + long t3 = System.currentTimeMillis(); + mContainer.storeThumbnail( + thumbnail, Image.this.fullSizeImageId()); + long t4 = System.currentTimeMillis(); + checkCanceled(); + if (VERBOSE) Log.v(TAG, "rotating by " + mOrientation); + try { + thumbnail = Util.rotate(thumbnail, mOrientation); + saveMiniThumb(thumbnail); + } catch (IOException e) { + // Ignore if unable to save thumb. + } + long t5 = System.currentTimeMillis(); + checkCanceled(); + + if (VERBOSE) { + Log.v(TAG, String.format("Timing data %d %d %d %d", + t2 - t1, t3 - t2, t4 - t3, t5 - t4)); + } + return true; + } catch (CanceledException ex) { + if (VERBOSE) Log.v(TAG, "got canceled... need to cleanup"); + return false; + } finally { + /* + * Cursor c = getCursor(); synchronized (c) { if + * (c.moveTo(getRow())) { mContainer.requery(); } } + */ + acknowledgeCancel(); + } + } + } + + public IGetBooleanCancelable saveImageContents(Bitmap image, + byte [] jpegData, int orientation, boolean newFile, Cursor cursor) { + return new SaveImageContentsCancelable( + image, jpegData, orientation, cursor); + } + + private void setExifRotation(int degrees) { + try { + Cursor c = getCursor(); + String filePath; + synchronized (c) { + filePath = c.getString(mContainer.indexData()); + } + synchronized (ImageManager.instance()) { + ExifInterface exif = new ExifInterface(filePath); + if (mExifData == null) { + mExifData = exif.getAttributes(); + } + if (degrees < 0) degrees += 360; + + int orientation = ExifInterface.ORIENTATION_NORMAL; + switch (degrees) { + case 0: + orientation = ExifInterface.ORIENTATION_NORMAL; + break; + case 90: + orientation = ExifInterface.ORIENTATION_ROTATE_90; + break; + case 180: + orientation = ExifInterface.ORIENTATION_ROTATE_180; + break; + case 270: + orientation = ExifInterface.ORIENTATION_ROTATE_270; + break; + } + + replaceExifTag(ExifInterface.TAG_ORIENTATION, + Integer.toString(orientation)); + replaceExifTag("UserComment", + "saveRotatedImage comment orientation: " + orientation); + exif.saveAttributes(mExifData); + exif.commitChanges(); + } + } catch (RuntimeException ex) { + Log.e(TAG, "unable to save exif data with new orientation " + + fullSizeImageUri()); + } + } + + /** + * Save the rotated image by updating the Exif "Orientation" tag. + * @param degrees + */ + public boolean rotateImageBy(int degrees) { + int newDegrees = getDegreesRotated() + degrees; + setExifRotation(newDegrees); + setDegreesRotated(newDegrees); + + // setting this to zero will force the call to checkCursor to generate + // fresh thumbs + mMiniThumbMagic = 0; + try { + mContainer.checkThumbnail( + this, mContainer.getCursor(), this.getRow()); + } catch (IOException e) { + // Ignore inability to store mini thumbnail. + } + return true; + } + + public Bitmap thumbBitmap() { + Bitmap bitmap = null; + if (mContainer.mThumbUri != null) { + Cursor c = mContentResolver.query( + mContainer.mThumbUri, BaseImageList.THUMB_PROJECTION, + Thumbnails.IMAGE_ID + "=?", + new String[] { String.valueOf(fullSizeImageId()) }, + null); + try { + if (c.moveToFirst()) bitmap = decodeCurrentImage(c); + } catch (RuntimeException ex) { + // sdcard removed? + return null; + } finally { + c.close(); + } + } + + if (bitmap == null) { + bitmap = fullSizeBitmap(ImageManager.THUMBNAIL_TARGET_SIZE, false); + if (VERBOSE) { + Log.v(TAG, "no thumbnail found... storing new one for " + + fullSizeImageId()); + } + bitmap = mContainer.storeThumbnail(bitmap, fullSizeImageId()); + } + + if (bitmap != null) { + bitmap = Util.rotate(bitmap, getDegreesRotated()); + } + + long elapsed = System.currentTimeMillis(); + return bitmap; + } + + private Bitmap decodeCurrentImage(Cursor c) { + Uri thumbUri = ContentUris.withAppendedId( + mContainer.mThumbUri, + c.getLong(((ImageList) mContainer).INDEX_THUMB_ID)); + ParcelFileDescriptor pfdInput; + Bitmap bitmap = null; + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + pfdInput = mContentResolver.openFileDescriptor(thumbUri, "r"); + bitmap = BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + pfdInput.close(); + } catch (FileNotFoundException ex) { + Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); + } catch (IOException ex) { + Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); + } catch (NullPointerException ex) { + // we seem to get this if the file doesn't exist anymore + Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); + } catch (OutOfMemoryError ex) { + Log.e(TAG, "failed to allocate memory for thumbnail " + + thumbUri + "; " + ex); + } + return bitmap; + } +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/ImageList.java b/src/com/android/camera/gallery/ImageList.java new file mode 100644 index 0000000..d1c5bff --- /dev/null +++ b/src/com/android/camera/gallery/ImageList.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import com.android.camera.ImageManager; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.util.Log; + +import java.io.IOException; +import java.util.HashMap; + +/** + * Represents an ordered collection of Image objects. Provides an API to add + * and remove an image. + */ +public class ImageList extends BaseImageList implements IImageList { + + private static final String TAG = "ImageList"; + private static final boolean VERBOSE = false; + + boolean mIsRegistered = false; + ContentObserver mContentObserver; + DataSetObserver mDataSetObserver; + + public HashMap<String, String> getBucketIds() { + Uri uri = mBaseUri.buildUpon() + .appendQueryParameter("distinct", "true").build(); + Cursor c = Images.Media.query( + mContentResolver, uri, + new String[] { + ImageColumns.BUCKET_DISPLAY_NAME, + ImageColumns.BUCKET_ID}, + whereClause(), whereClauseArgs(), sortOrder()); + + HashMap<String, String> hash = new HashMap<String, String>(); + if (c != null && c.moveToFirst()) { + do { + hash.put(c.getString(1), c.getString(0)); + } while (c.moveToNext()); + } + return hash; + } + /** + * ImageList constructor. + * @param cr ContentResolver + */ + public ImageList(Context ctx, ContentResolver cr, Uri imageUri, + Uri thumbUri, int sort, String bucketId) { + super(ctx, cr, imageUri, sort, bucketId); + mBaseUri = imageUri; + mThumbUri = thumbUri; + mSort = sort; + + mContentResolver = cr; + + mCursor = createCursor(); + if (mCursor == null) { + Log.e(TAG, "unable to create image cursor for " + mBaseUri); + throw new UnsupportedOperationException(); + } + + if (VERBOSE) { + Log.v(TAG, "for " + mBaseUri.toString() + " got cursor " + + mCursor + " with length " + + (mCursor != null ? mCursor.getCount() : "-1")); + } + + final Runnable updateRunnable = new Runnable() { + public void run() { + + // handling these external updates is causing ANR problems that + // are unresolved. For now ignore them since there shouldn't + // be anyone modifying the database on the fly. + if (true) return; + + synchronized (mCursor) { + requery(); + } + if (mListener != null) mListener.onChange(ImageList.this); + } + }; + + mContentObserver = new ContentObserver(null) { + @Override + public boolean deliverSelfNotifications() { + return false; + } + + @Override + public void onChange(boolean selfChange) { + if (VERBOSE) { + Log.v(TAG, "MyContentObserver.onChange; selfChange == " + + selfChange); + } + updateRunnable.run(); + } + }; + + mDataSetObserver = new DataSetObserver() { + @Override + public void onChanged() { + if (VERBOSE) Log.v(TAG, "MyDataSetObserver.onChanged"); + // handling these external updates is causing ANR problems that + // are unresolved. For now ignore them since there shouldn't + // be anyone modifying the database on the fly. + + // updateRunnable.run(); + } + + @Override + public void onInvalidated() { + if (VERBOSE) { + Log.v(TAG, "MyDataSetObserver.onInvalidated: " + + mCursorDeactivated); + } + } + }; + + registerObservers(); + } + + private void registerObservers() { + if (mIsRegistered) return; + mCursor.registerContentObserver(mContentObserver); + mCursor.registerDataSetObserver(mDataSetObserver); + mIsRegistered = true; + } + + private void unregisterObservers() { + if (!mIsRegistered) return; + mCursor.unregisterContentObserver(mContentObserver); + mCursor.unregisterDataSetObserver(mDataSetObserver); + mIsRegistered = false; + } + + @Override + public void deactivate() { + super.deactivate(); + unregisterObservers(); + } + + @Override + protected void activateCursor() { + super.activateCursor(); + registerObservers(); + } + + private static final String sWhereClause = + "(" + Images.Media.MIME_TYPE + " in (?, ?, ?))"; + + protected String whereClause() { + if (mBucketId != null) { + return sWhereClause + " and " + Images.Media.BUCKET_ID + " = '" + + mBucketId + "'"; + } else { + return sWhereClause; + } + } + + protected String[] whereClauseArgs() { + return ACCEPTABLE_IMAGE_TYPES; + } + + protected Cursor createCursor() { + Cursor c = Images.Media.query( + mContentResolver, mBaseUri, BaseImageList.IMAGE_PROJECTION, + whereClause(), whereClauseArgs(), sortOrder()); + if (VERBOSE) { + Log.v(TAG, "createCursor got cursor with count " + + (c == null ? -1 : c.getCount())); + } + return c; + } + + @Override + protected int indexOrientation() { + return INDEX_ORIENTATION; + } + + @Override + protected int indexDateTaken() { + return INDEX_DATE_TAKEN; + } + + @Override + protected int indexDescription() { + return -1; + } + + @Override + protected int indexMimeType() { + return INDEX_MIME_TYPE; + } + + @Override + protected int indexData() { + return INDEX_DATA; + } + + @Override + protected int indexId() { + return INDEX_ID; + } + + @Override + protected int indexLatitude() { + return -1; + } + + @Override + protected int indexLongitude() { + return -1; + } + + @Override + protected int indexMiniThumbId() { + return INDEX_MINI_THUMB_MAGIC; + } + + @Override + protected int indexPicasaWeb() { + return -1; + } + + @Override + protected int indexPrivate() { + return -1; + } + + @Override + protected int indexTitle() { + return -1; + } + + @Override + protected int indexDisplayName() { + return -1; + } + + @Override + protected int indexThumbId() { + return INDEX_THUMB_ID; + } + + @Override + protected IImage make(long id, long miniThumbId, ContentResolver cr, + IImageList list, long timestamp, int index, int rotation) { + return new Image(id, miniThumbId, mContentResolver, this, index, + rotation); + } + + @Override + protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, + ParcelFileDescriptor pfd, BitmapFactory.Options options) { + Bitmap b = null; + try { + if (pfd == null) pfd = makeInputStream(uri); + if (pfd == null) return null; + if (options == null) options = new BitmapFactory.Options(); + + java.io.FileDescriptor fd = pfd.getFileDescriptor(); + options.inSampleSize = 1; + if (targetWidthHeight != -1) { + options.inJustDecodeBounds = true; + long t1 = System.currentTimeMillis(); + BitmapFactory.decodeFileDescriptor(fd, null, options); + long t2 = System.currentTimeMillis(); + if (options.mCancel || options.outWidth == -1 + || options.outHeight == -1) { + return null; + } + options.inSampleSize = + Util.computeSampleSize(options, targetWidthHeight); + options.inJustDecodeBounds = false; + } + + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + long t1 = System.currentTimeMillis(); + b = BitmapFactory.decodeFileDescriptor(fd, null, options); + long t2 = System.currentTimeMillis(); + if (VERBOSE) { + Log.v(TAG, "A: got bitmap " + b + " with sampleSize " + + options.inSampleSize + " took " + (t2 - t1)); + } + } catch (OutOfMemoryError ex) { + if (VERBOSE) { + Log.v(TAG, "got oom exception " + ex); + } + return null; + } finally { + Util.closeSiliently(pfd); + } + return b; + } + + private ParcelFileDescriptor makeInputStream(Uri uri) { + try { + return mContentResolver.openFileDescriptor(uri, "r"); + } catch (IOException ex) { + return null; + } + } + + private String sortOrder() { + // add id to the end so that we don't ever get random sorting + // which could happen, I suppose, if the first two values were + // duplicated + String ascending = + mSort == ImageManager.SORT_ASCENDING ? " ASC" : " DESC"; + return Images.Media.DATE_TAKEN + ascending + "," + Images.Media._ID + + ascending; + } + +} + diff --git a/src/com/android/camera/gallery/ImageListUber.java b/src/com/android/camera/gallery/ImageListUber.java new file mode 100644 index 0000000..17306f4 --- /dev/null +++ b/src/com/android/camera/gallery/ImageListUber.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import com.android.camera.ImageManager; + +import android.net.Uri; +import android.os.Handler; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * A union of different <code>IImageList</code>. + */ +public class ImageListUber implements IImageList { + private static final boolean VERBOSE = false; + private static final String TAG = "ImageListUber"; + + private IImageList [] mSubList; + private int mSort; + private IImageList.OnChange mListener = null; + private Handler mHandler; + + // This is an array of Longs wherein each Long consists of + // two components. The first component indicates the number of + // consecutive entries that belong to a given sublist. + // The second component indicates which sublist we're referring + // to (an int which is used to index into mSubList). + private ArrayList<Long> mSkipList = null; + private int [] mSkipCounts = null; + + public HashMap<String, String> getBucketIds() { + HashMap<String, String> hashMap = new HashMap<String, String>(); + for (IImageList list : mSubList) { + hashMap.putAll(list.getBucketIds()); + } + return hashMap; + } + + public ImageListUber(IImageList [] sublist, int sort) { + mSubList = sublist.clone(); + mSort = sort; + + if (mListener != null) { + for (IImageList list : sublist) { + list.setOnChangeListener(new OnChange() { + public void onChange(IImageList list) { + if (mListener != null) { + mListener.onChange(ImageListUber.this); + } + } + }, mHandler); + } + } + } + + public void checkThumbnails(ThumbCheckCallback cb, int totalThumbnails) { + for (IImageList i : mSubList) { + int count = i.getCount(); + i.checkThumbnails(cb, totalThumbnails); + totalThumbnails -= count; + } + } + + public void commitChanges() { + final IImageList sublist[] = mSubList; + final int length = sublist.length; + for (int i = 0; i < length; i++) + sublist[i].commitChanges(); + } + + public void deactivate() { + final IImageList sublist[] = mSubList; + final int length = sublist.length; + int pos = -1; + while (++pos < length) { + IImageList sub = sublist[pos]; + sub.deactivate(); + } + } + + public int getCount() { + final IImageList sublist[] = mSubList; + final int length = sublist.length; + int count = 0; + for (int i = 0; i < length; i++) + count += sublist[i].getCount(); + return count; + } + + public boolean isEmpty() { + final IImageList sublist[] = mSubList; + final int length = sublist.length; + for (int i = 0; i < length; i++) { + if (!sublist[i].isEmpty()) { + return false; + } + } + return true; + } + + // mSkipCounts is used to tally the counts as we traverse + // the mSkipList. It's a member variable only so that + // we don't have to allocate each time through. Otherwise + // it could just as easily be a local. + + public synchronized IImage getImageAt(int index) { + if (index < 0 || index > getCount()) { + throw new IndexOutOfBoundsException( + "index " + index + " out of range max is " + getCount()); + } + + // first make sure our allocations are in order + if (mSkipCounts == null || mSubList.length > mSkipCounts.length) { + mSkipCounts = new int[mSubList.length]; + } + + if (mSkipList == null) { + mSkipList = new ArrayList<Long>(); + } + + // zero out the mSkipCounts since that's only used for the + // duration of the function call + for (int i = 0; i < mSubList.length; i++) { + mSkipCounts[i] = 0; + } + + // a counter of how many images we've skipped in + // trying to get to index. alternatively we could + // have decremented index but, alas, I liked this + // way more. + int skipCount = 0; + + // scan the existing mSkipList to see if we've computed + // enough to just return the answer + for (int i = 0; i < mSkipList.size(); i++) { + long v = mSkipList.get(i); + + int offset = (int) (v & 0xFFFF); + int which = (int) (v >> 32); + + if (skipCount + offset > index) { + int subindex = mSkipCounts[which] + (index - skipCount); + IImage img = mSubList[which].getImageAt(subindex); + return img; + } + + skipCount += offset; + mSkipCounts[which] += offset; + } + + // if we get here we haven't computed the answer for + // "index" yet so keep computing. This means running + // through the list of images and either modifying the + // last entry or creating a new one. + long count = 0; + while (true) { + long maxTimestamp = mSort == ImageManager.SORT_ASCENDING + ? Long.MAX_VALUE + : Long.MIN_VALUE; + int which = -1; + for (int i = 0; i < mSubList.length; i++) { + int pos = mSkipCounts[i]; + IImageList list = mSubList[i]; + if (pos < list.getCount()) { + IImage image = list.getImageAt(pos); + // this should never be null but sometimes the database is + // causing problems and it is null + if (image != null) { + long timestamp = image.getDateTaken(); + if (mSort == ImageManager.SORT_ASCENDING + ? (timestamp < maxTimestamp) + : (timestamp > maxTimestamp)) { + maxTimestamp = timestamp; + which = i; + } + } + } + } + + if (which == -1) { + if (VERBOSE) Log.v(TAG, "which is -1, returning null"); + return null; + } + + boolean done = false; + count = 1; + if (mSkipList.size() > 0) { + int pos = mSkipList.size() - 1; + long oldEntry = mSkipList.get(pos); + if ((oldEntry >> 32) == which) { + long newEntry = oldEntry + 1; + mSkipList.set(pos, newEntry); + done = true; + } + } + if (!done) { + long newEntry = ((long) which << 32) | count; + if (VERBOSE) { + Log.v(TAG, "new entry is " + Long.toHexString(newEntry)); + } + mSkipList.add(newEntry); + } + + if (skipCount++ == index) { + return mSubList[which].getImageAt(mSkipCounts[which]); + } + mSkipCounts[which] += 1; + } + } + + public IImage getImageForUri(Uri uri) { + // TODO: perhaps we can preflight the base of the uri + // against each sublist first + for (int i = 0; i < mSubList.length; i++) { + IImage img = mSubList[i].getImageForUri(uri); + if (img != null) return img; + } + return null; + } + + /** + * Modify the skip list when an image is deleted by finding + * the relevant entry in mSkipList and decrementing the + * counter. This is simple because deletion can never + * cause change the order of images. + */ + public void modifySkipCountForDeletedImage(int index) { + int skipCount = 0; + + for (int i = 0; i < mSkipList.size(); i++) { + long v = mSkipList.get(i); + + int offset = (int) (v & 0xFFFF); + int which = (int) (v >> 32); + + if (skipCount + offset > index) { + mSkipList.set(i, v - 1); + break; + } + + skipCount += offset; + } + } + + public boolean removeImage(IImage image) { + IImageList parent = image.getContainer(); + int pos = -1; + int baseIndex = 0; + while (++pos < mSubList.length) { + IImageList sub = mSubList[pos]; + if (sub == parent) { + if (sub.removeImage(image)) { + modifySkipCountForDeletedImage(baseIndex); + return true; + } else { + break; + } + } + baseIndex += sub.getCount(); + } + return false; + } + + public void removeImageAt(int index) { + IImage img = getImageAt(index); + if (img != null) { + IImageList list = img.getContainer(); + if (list != null) { + list.removeImage(img); + modifySkipCountForDeletedImage(index); + } + } + } + + public void removeOnChangeListener(OnChange changeCallback) { + if (changeCallback == mListener) { + mListener = null; + } + } + + public void setOnChangeListener(OnChange changeCallback, Handler h) { + mListener = changeCallback; + mHandler = h; + } + +} diff --git a/src/com/android/camera/gallery/SimpleBaseImage.java b/src/com/android/camera/gallery/SimpleBaseImage.java new file mode 100644 index 0000000..63e25d7 --- /dev/null +++ b/src/com/android/camera/gallery/SimpleBaseImage.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.net.Uri; + +import java.io.InputStream; + +/** + * A simple version of <code>BaseImage</code>. + */ +public abstract class SimpleBaseImage implements IImage { + public void commitChanges() { + throw new UnsupportedOperationException(); + } + + public InputStream fullSizeImageData() { + throw new UnsupportedOperationException(); + } + + public long fullSizeImageId() { + return 0; + } + + public Uri fullSizeImageUri() { + throw new UnsupportedOperationException(); + } + + public IImageList getContainer() { + return null; + } + + public long getDateTaken() { + return 0; + } + + public String getMimeType() { + throw new UnsupportedOperationException(); + } + + public String getDescription() { + throw new UnsupportedOperationException(); + } + + public boolean getIsPrivate() { + throw new UnsupportedOperationException(); + } + + public double getLatitude() { + return 0D; + } + + public double getLongitude() { + return 0D; + } + + public String getTitle() { + throw new UnsupportedOperationException(); + } + + public String getDisplayName() { + throw new UnsupportedOperationException(); + } + + public String getPicasaId() { + return null; + } + + public int getRow() { + throw new UnsupportedOperationException(); + } + + public int getHeight() { + return 0; + } + + public int getWidth() { + return 0; + } + + public boolean hasLatLong() { + return false; + } + + public boolean isReadonly() { + return true; + } + + public boolean isDrm() { + return false; + } + + public void onRemove() { + throw new UnsupportedOperationException(); + } + + public boolean rotateImageBy(int degrees) { + return false; + } + + public void setDescription(String description) { + throw new UnsupportedOperationException(); + } + + public void setIsPrivate(boolean isPrivate) { + throw new UnsupportedOperationException(); + } + + public void setName(String name) { + throw new UnsupportedOperationException(); + } + + public void setPicasaId(long id) { + } + + public void setPicasaId(String id) { + } + + public Uri thumbUri() { + throw new UnsupportedOperationException(); + } +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/SingleImageList.java b/src/com/android/camera/gallery/SingleImageList.java new file mode 100644 index 0000000..fa7656f --- /dev/null +++ b/src/com/android/camera/gallery/SingleImageList.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import com.android.camera.ImageManager; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.HashMap; + +/** + * An implementation of interface <code>IImageList</code> which contains only + * one image. + */ +public class SingleImageList extends BaseImageList implements IImageList { + private static final String TAG = "SingleImageList"; + private static final boolean VERBOSE = false; + private static final int THUMBNAIL_TARGET_SIZE = 320; + + private IImage mSingleImage; + + private class UriImage extends SimpleBaseImage { + + UriImage() { + } + + public String getDataPath() { + return mUri.getPath(); + } + + InputStream getInputStream() { + try { + if (mUri.getScheme().equals("file")) { + String path = mUri.getPath(); + if (VERBOSE) Log.v(TAG, "path is " + path); + return new java.io.FileInputStream(mUri.getPath()); + } else { + return mContentResolver.openInputStream(mUri); + } + } catch (FileNotFoundException ex) { + return null; + } + } + + ParcelFileDescriptor getPFD() { + try { + if (mUri.getScheme().equals("file")) { + String path = mUri.getPath(); + if (VERBOSE) Log.v(TAG, "path is " + path); + return ParcelFileDescriptor.open(new File(path), + ParcelFileDescriptor.MODE_READ_ONLY); + } else { + return mContentResolver.openFileDescriptor(mUri, "r"); + } + } catch (FileNotFoundException ex) { + return null; + } + } + + public Bitmap fullSizeBitmap(int targetWidthHeight) { + try { + ParcelFileDescriptor pfdInput = getPFD(); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + + if (targetWidthHeight != -1) { + options.inSampleSize = + Util.computeSampleSize(options, targetWidthHeight); + } + + options.inJustDecodeBounds = false; + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + + Bitmap b = BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + if (VERBOSE) { + Log.v(TAG, "B: got bitmap " + b + " with sampleSize " + + options.inSampleSize); + } + pfdInput.close(); + return b; + } catch (Exception ex) { + Log.e(TAG, "got exception decoding bitmap " + ex.toString()); + return null; + } + } + + final class LoadBitmapCancelable extends BaseCancelable + implements IGetBitmapCancelable { + ParcelFileDescriptor mPfdInput; + BitmapFactory.Options mOptions = new BitmapFactory.Options(); + long mCancelInitiationTime; + int mTargetWidthOrHeight; + + public LoadBitmapCancelable( + ParcelFileDescriptor pfd, int targetWidthOrHeight) { + mPfdInput = pfd; + mTargetWidthOrHeight = targetWidthOrHeight; + } + + @Override + public boolean doCancelWork() { + if (VERBOSE) { + Log.v(TAG, "requesting bitmap load cancel"); + } + mCancelInitiationTime = System.currentTimeMillis(); + mOptions.requestCancelDecode(); + return true; + } + + public Bitmap get() { + try { + Bitmap b = makeBitmap(mTargetWidthOrHeight, + fullSizeImageUri(), mPfdInput, mOptions); + if (b == null && mCancelInitiationTime != 0) { + if (VERBOSE) { + Log.v(TAG, "cancel returned null bitmap -- took " + + (System.currentTimeMillis() + - mCancelInitiationTime)); + } + } + if (VERBOSE) Log.v(TAG, "b is " + b); + return b; + } catch (Exception ex) { + return null; + } finally { + acknowledgeCancel(); + } + } + } + + public IGetBitmapCancelable fullSizeBitmapCancelable( + int targetWidthOrHeight) { + try { + ParcelFileDescriptor pfdInput = getPFD(); + if (pfdInput == null) return null; + if (VERBOSE) Log.v(TAG, "inputStream is " + pfdInput); + return new LoadBitmapCancelable(pfdInput, targetWidthOrHeight); + } catch (UnsupportedOperationException ex) { + return null; + } + } + + @Override + public Uri fullSizeImageUri() { + return mUri; + } + + @Override + public InputStream fullSizeImageData() { + return getInputStream(); + } + + public long imageId() { + return 0; + } + + public Bitmap miniThumbBitmap() { + return thumbBitmap(); + } + + @Override + public String getTitle() { + return mUri.toString(); + } + + @Override + public String getDisplayName() { + return getTitle(); + } + + @Override + public String getDescription() { + return ""; + } + + + public Bitmap thumbBitmap() { + Bitmap b = fullSizeBitmap(THUMBNAIL_TARGET_SIZE); + if (b != null) { + Matrix m = new Matrix(); + float scale = Math.min( + 1F, THUMBNAIL_TARGET_SIZE / (float) b.getWidth()); + m.setScale(scale, scale); + Bitmap scaledBitmap = Bitmap.createBitmap( + b, 0, 0, b.getWidth(), b.getHeight(), m, true); + return scaledBitmap; + } else { + return null; + } + } + + private BitmapFactory.Options snifBitmapOptions() { + ParcelFileDescriptor input = getPFD(); + if (input == null) return null; + try { + Uri uri = fullSizeImageUri(); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor( + input.getFileDescriptor(), null, options); + return options; + } finally { + Util.closeSiliently(input); + } + } + + @Override + public String getMimeType() { + BitmapFactory.Options options = snifBitmapOptions(); + return (options != null) ? options.outMimeType : ""; + } + + @Override + public int getHeight() { + BitmapFactory.Options options = snifBitmapOptions(); + return (options != null) ? options.outHeight : 0; + } + + @Override + public int getWidth() { + BitmapFactory.Options options = snifBitmapOptions(); + return (options != null) ? options.outWidth : 0; + } + } + + public SingleImageList(ContentResolver cr, Uri uri) { + super(null, cr, uri, ImageManager.SORT_ASCENDING, null); + mSingleImage = new UriImage(); + } + + public HashMap<String, String> getBucketIds() { + throw new UnsupportedOperationException(); + } + + @Override + public void deactivate() { + // nothing to do here + } + + @Override + public int getCount() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public IImage getImageAt(int i) { + return i == 0 ? mSingleImage : null; + } + + @Override + public IImage getImageForUri(Uri uri) { + return uri.equals(mUri) ? mSingleImage : null; + } + + public IImage getImageWithId(long id) { + throw new UnsupportedOperationException(); + } + + @Override + protected int indexOrientation() { + return -1; + } + + @Override + protected int indexDateTaken() { + return -1; + } + + @Override + protected int indexMimeType() { + return -1; + } + + @Override + protected int indexDescription() { + return -1; + } + + @Override + protected int indexId() { + return -1; + } + + @Override + protected int indexData() { + return -1; + } + + @Override + protected int indexLatitude() { + return -1; + } + + @Override + protected int indexLongitude() { + return -1; + } + + @Override + protected int indexMiniThumbId() { + return -1; + } + + @Override + protected int indexPicasaWeb() { + return -1; + } + + @Override + protected int indexPrivate() { + return -1; + } + + @Override + protected int indexTitle() { + return -1; + } + + @Override + protected int indexDisplayName() { + return -1; + } + + @Override + protected int indexThumbId() { + return -1; + } + + @Override + protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, + ParcelFileDescriptor pfdInput, BitmapFactory.Options options) { + Bitmap b = null; + try { + if (options == null) options = new BitmapFactory.Options(); + options.inSampleSize = 1; + + if (targetWidthHeight != -1) { + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + options.inSampleSize = + Util.computeSampleSize(options, targetWidthHeight); + options.inJustDecodeBounds = false; + } + b = BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + if (VERBOSE) { + Log.v(TAG, "C: got bitmap " + b + " with sampleSize " + + options.inSampleSize); + } + } catch (OutOfMemoryError ex) { + if (VERBOSE) Log.v(TAG, "got oom exception " + ex); + return null; + } finally { + Util.closeSiliently(pfdInput); + } + return b; + } +} diff --git a/src/com/android/camera/gallery/ThreadSafeOutputStream.java b/src/com/android/camera/gallery/ThreadSafeOutputStream.java new file mode 100644 index 0000000..2bd1f96 --- /dev/null +++ b/src/com/android/camera/gallery/ThreadSafeOutputStream.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A wrapper of an <code>OutputStream</code>, so that all the IO operations are + * thread safe. + */ +class ThreadSafeOutputStream extends OutputStream { + private OutputStream mDelegateStream; + boolean mClosed; + + public ThreadSafeOutputStream(OutputStream delegate) { + mDelegateStream = delegate; + } + + @Override + public synchronized void close() { + try { + mClosed = true; + mDelegateStream.close(); + } catch (IOException ex) { + //TODO: this should be thrown out. + } + } + + @Override + public synchronized void flush() throws IOException { + super.flush(); + } + + @Override + public void write(byte[] b, int offset, int length) throws IOException { + while (length > 0) { + synchronized (this) { + if (mClosed) return; + int writeLength = Math.min(8192, length); + mDelegateStream.write(b, offset, writeLength); + offset += writeLength; + length -= writeLength; + } + } + } + + @Override + public synchronized void write(int oneByte) throws IOException { + if (mClosed) return; + mDelegateStream.write(oneByte); + } +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/Util.java b/src/com/android/camera/gallery/Util.java new file mode 100644 index 0000000..04998ef --- /dev/null +++ b/src/com/android/camera/gallery/Util.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.media.MediaMetadataRetriever; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import com.android.camera.ImageLoader; +import com.android.camera.ImageManager; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; + +/** + * Collection of utility functions used in this package. + */ +public class Util { + private static final boolean VERBOSE = false; + private static final String TAG = "db.Util"; + + private Util() { + } + + // Rotates the bitmap by the specified degree. + // If a new bitmap is created, the original bitmap is recycled. + public static Bitmap rotate(Bitmap b, int degrees) { + if (degrees != 0 && b != null) { + Matrix m = new Matrix(); + m.setRotate(degrees, + (float) b.getWidth() / 2, (float) b.getHeight() / 2); + try { + Bitmap b2 = Bitmap.createBitmap( + b, 0, 0, b.getWidth(), b.getHeight(), m, true); + if (b != b2) { + b.recycle(); + b = b2; + } + } catch (OutOfMemoryError ex) { + // We have no memory to rotate. Return the original bitmap. + } + } + return b; + } + + /* + * Compute the sample size as a function of the image size and the target. + * Scale the image down so that both the width and height are just above the + * target. If this means that one of the dimension goes from above the + * target to below the target (e.g. given a width of 480 and an image width + * of 600 but sample size of 2 -- i.e. new width 300 -- bump the sample size + * down by 1. + */ + public static int computeSampleSize( + BitmapFactory.Options options, int target) { + int w = options.outWidth; + int h = options.outHeight; + + int candidateW = w / target; + int candidateH = h / target; + int candidate = Math.max(candidateW, candidateH); + + if (candidate == 0) return 1; + + if (candidate > 1) { + if ((w > target) && (w / candidate) < target) candidate -= 1; + } + + if (candidate > 1) { + if ((h > target) && (h / candidate) < target) candidate -= 1; + } + + if (VERBOSE) { + Log.v(TAG, "for w/h " + w + "/" + h + " returning " + candidate + + "(" + (w / candidate) + " / " + (h / candidate)); + } + + return candidate; + } + + /** + * Creates a centered bitmap of the desired size. Recycles the input. + * @param source + */ + public static Bitmap extractMiniThumb( + Bitmap source, int width, int height) { + return Util.extractMiniThumb(source, width, height, true); + } + + public static Bitmap extractMiniThumb( + Bitmap source, int width, int height, boolean recycle) { + if (source == null) { + return null; + } + + float scale; + if (source.getWidth() < source.getHeight()) { + scale = width / (float) source.getWidth(); + } else { + scale = height / (float) source.getHeight(); + } + Matrix matrix = new Matrix(); + matrix.setScale(scale, scale); + Bitmap miniThumbnail = ImageLoader.transform(matrix, source, + width, height, false); + + if (recycle && miniThumbnail != source) { + source.recycle(); + } + return miniThumbnail; + } + + /** + * Creates a byte[] for a given bitmap of the desired size. Recycles the + * input bitmap. + */ + public static byte[] miniThumbData(Bitmap source) { + if (source == null) return null; + + Bitmap miniThumbnail = extractMiniThumb( + source, ImageManager.MINI_THUMB_TARGET_SIZE, + ImageManager.MINI_THUMB_TARGET_SIZE); + + ByteArrayOutputStream miniOutStream = new ByteArrayOutputStream(); + miniThumbnail.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream); + miniThumbnail.recycle(); + + try { + miniOutStream.close(); + byte [] data = miniOutStream.toByteArray(); + return data; + } catch (java.io.IOException ex) { + Log.e(TAG, "got exception ex " + ex); + } + return null; + } + + /** + * @return true if the mimetype is a video mimetype. + */ + public static boolean isVideoMimeType(String mimeType) { + return mimeType.startsWith("video/"); + } + + /** + * Create a video thumbnail for a video. May return null if the video is + * corrupt. + * + * @param filePath + */ + public static Bitmap createVideoThumbnail(String filePath) { + Bitmap bitmap = null; + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY); + retriever.setDataSource(filePath); + bitmap = retriever.captureFrame(); + } catch (IllegalArgumentException ex) { + // Assume this is a corrupt video file + } catch (RuntimeException ex) { + // Assume this is a corrupt video file. + } finally { + try { + retriever.release(); + } catch (RuntimeException ex) { + // Ignore failures while cleaning up. + } + } + return bitmap; + } + + public static int indexOf(String [] array, String s) { + for (int i = 0; i < array.length; i++) { + if (array[i].equals(s)) { + return i; + } + } + return -1; + } + + public static void closeSiliently(Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (Throwable t) { + // do nothing + } + } + + public static void closeSiliently(ParcelFileDescriptor c) { + if (c == null) return; + try { + c.close(); + } catch (Throwable t) { + // do nothing + } + } + +} diff --git a/src/com/android/camera/gallery/VideoList.java b/src/com/android/camera/gallery/VideoList.java new file mode 100644 index 0000000..d1ec512 --- /dev/null +++ b/src/com/android/camera/gallery/VideoList.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import static com.android.camera.gallery.BaseImageList.MINITHUMB_IS_NULL; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.BaseColumns; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Video; +import android.provider.MediaStore.Video.VideoColumns; +import android.util.Config; +import android.util.Log; + +import com.android.camera.ImageManager; + +import java.io.IOException; +import java.util.HashMap; + +/** + * A collection of all the <code>VideoObject</code> in gallery. + */ +public class VideoList extends BaseImageList implements IImageList { + private static final String TAG = "BaseImageList"; + private static final boolean VERBOSE = false; + + private static final String[] sProjection = new String[] { + Video.Media._ID, + Video.Media.DATA, + Video.Media.DATE_TAKEN, + Video.Media.TITLE, + Video.Media.DISPLAY_NAME, + Video.Media.DESCRIPTION, + Video.Media.IS_PRIVATE, + Video.Media.TAGS, + Video.Media.CATEGORY, + Video.Media.LANGUAGE, + Video.Media.LATITUDE, + Video.Media.LONGITUDE, + Video.Media.MINI_THUMB_MAGIC, + Video.Media.MIME_TYPE}; + + static final int INDEX_ID = indexOf(Video.Media._ID); + static final int INDEX_DATA = indexOf(Video.Media.DATA); + static final int INDEX_DATE_TAKEN = indexOf(Video.Media.DATE_TAKEN); + static final int INDEX_TITLE = indexOf(Video.Media.TITLE); + static final int INDEX_DISPLAY_NAME = + indexOf(Video.Media.DISPLAY_NAME); + static final int INDEX_MIME_TYPE = indexOf(Video.Media.MIME_TYPE); + static final int INDEX_DESCRIPTION = + indexOf(Video.Media.DESCRIPTION); + static final int INDEX_PRIVATE = indexOf(Video.Media.IS_PRIVATE); + static final int INDEX_TAGS = indexOf(Video.Media.TAGS); + static final int INDEX_CATEGORY = indexOf(Video.Media.CATEGORY); + static final int INDEX_LANGUAGE = indexOf(Video.Media.LANGUAGE); + static final int INDEX_LATITUDE = indexOf(Video.Media.LATITUDE); + static final int INDEX_LONGITUDE = indexOf(Video.Media.LONGITUDE); + static final int INDEX_MINI_THUMB_MAGIC = + indexOf(Video.Media.MINI_THUMB_MAGIC); + static final int INDEX_THUMB_ID = indexOf(BaseColumns._ID); + + private static int indexOf(String field) { + return Util.indexOf(sProjection, field); + } + + public VideoList(Context ctx, ContentResolver cr, Uri uri, Uri thumbUri, + int sort, String bucketId) { + super(ctx, cr, uri, sort, bucketId); + + mCursor = createCursor(); + if (mCursor == null) { + Log.e(TAG, "unable to create video cursor for " + mBaseUri); + throw new UnsupportedOperationException(); + } + + if (Config.LOGV) { + Log.v(TAG, "for " + mUri.toString() + " got cursor " + mCursor + + " with length " + + (mCursor != null ? mCursor.getCount() : -1)); + } + + if (mCursor == null) { + throw new UnsupportedOperationException(); + } + if (mCursor != null && mCursor.moveToFirst()) { + int row = 0; + do { + long imageId = mCursor.getLong(indexId()); + long dateTaken = mCursor.getLong(indexDateTaken()); + long miniThumbId = mCursor.getLong(indexMiniThumbId()); + mCache.put(imageId, new VideoObject(imageId, miniThumbId, + mContentResolver, this, dateTaken, row++)); + } while (mCursor.moveToNext()); + } + } + + public HashMap<String, String> getBucketIds() { + Uri uri = mBaseUri.buildUpon() + .appendQueryParameter("distinct", "true").build(); + Cursor c = Images.Media.query( + mContentResolver, uri, + new String[] { + VideoColumns.BUCKET_DISPLAY_NAME, + VideoColumns.BUCKET_ID + }, + whereClause(), whereClauseArgs(), sortOrder()); + HashMap<String, String> hash = new HashMap<String, String>(); + if (c != null && c.moveToFirst()) { + do { + hash.put(c.getString(1), c.getString(0)); + } while (c.moveToNext()); + } + return hash; + } + + protected String whereClause() { + if (mBucketId != null) { + return Images.Media.BUCKET_ID + " = '" + mBucketId + "'"; + } else { + return null; + } + } + + protected String[] whereClauseArgs() { + return null; + } + + @Override + protected String thumbnailWhereClause() { + return MINITHUMB_IS_NULL; + } + + @Override + protected String[] thumbnailWhereClauseArgs() { + return null; + } + + protected Cursor createCursor() { + Cursor c = Images.Media.query( + mContentResolver, mBaseUri, sProjection, + whereClause(), whereClauseArgs(), sortOrder()); + if (VERBOSE) { + Log.v(TAG, "createCursor got cursor with count " + + (c == null ? -1 : c.getCount())); + } + return c; + } + + @Override + protected int indexOrientation() { + return -1; + } + + @Override + protected int indexDateTaken() { + return INDEX_DATE_TAKEN; + } + + @Override + protected int indexDescription() { + return INDEX_DESCRIPTION; + } + + @Override + protected int indexMimeType() { + return INDEX_MIME_TYPE; + } + + @Override + protected int indexData() { + return INDEX_DATA; + } + + @Override + protected int indexId() { + return INDEX_ID; + } + + @Override + protected int indexLatitude() { + return INDEX_LATITUDE; + } + + @Override + protected int indexLongitude() { + return INDEX_LONGITUDE; + } + + @Override + protected int indexMiniThumbId() { + return INDEX_MINI_THUMB_MAGIC; + } + + @Override + protected int indexPicasaWeb() { + return -1; + } + + @Override + protected int indexPrivate() { + return INDEX_PRIVATE; + } + + @Override + protected int indexTitle() { + return INDEX_TITLE; + } + + @Override + protected int indexDisplayName() { + return -1; + } + + @Override + protected int indexThumbId() { + return INDEX_THUMB_ID; + } + + @Override + protected IImage make(long id, long miniThumbId, ContentResolver cr, + IImageList list, long timestamp, int index, int rotation) { + return new VideoObject(id, miniThumbId, mContentResolver, this, + timestamp, index); + } + + @Override + protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, + ParcelFileDescriptor pfdInput, BitmapFactory.Options options) { + MediaPlayer mp = new MediaPlayer(); + Bitmap thumbnail = ImageManager.DEFAULT_THUMBNAIL; + try { + mp.setDataSource(mContext, uri); +// int duration = mp.getDuration(); +// int at = duration > 2000 ? 1000 : duration / 2; + int at = 1000; + thumbnail = mp.getFrameAt(at); + if (Config.LOGV) { + if (thumbnail != null) { + Log.v(TAG, "getFrameAt @ " + at + " returned " + thumbnail + + "; " + thumbnail.getWidth() + " " + + thumbnail.getHeight()); + } else { + Log.v(TAG, "getFrame @ " + at + " failed for " + uri); + } + } + } catch (IOException ex) { + // ignore + } catch (IllegalArgumentException ex) { + // ignore + } catch (SecurityException ex) { + // ignore + } finally { + mp.release(); + } + return thumbnail; + } + + private String sortOrder() { + return Video.Media.DATE_TAKEN + + (mSort == ImageManager.SORT_ASCENDING ? " ASC " : " DESC"); + } +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/VideoObject.java b/src/com/android/camera/gallery/VideoObject.java new file mode 100644 index 0000000..19a17fa --- /dev/null +++ b/src/com/android/camera/gallery/VideoObject.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.graphics.Bitmap; + +import com.android.camera.ImageManager; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Represents a particular video and provides access to the underlying data and + * two thumbnail bitmaps as well as other information such as the id, and the + * path to the actual video data. + */ +public class VideoObject extends BaseImage implements IImage { + + /** + * Constructor. + * + * @param id the image id of the image + * @param cr the content resolver + */ + protected VideoObject(long id, long miniThumbId, ContentResolver cr, + VideoList container, long dateTaken, int row) { + super(id, miniThumbId, cr, container, row); + } + + @Override + protected Bitmap.CompressFormat compressionType() { + return Bitmap.CompressFormat.JPEG; + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof VideoObject)) return false; + return fullSizeImageUri().equals( + ((VideoObject) other).fullSizeImageUri()); + } + + @Override + public int hashCode() { + return fullSizeImageUri().toString().hashCode(); + } + + public String getDataPath() { + String path = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + int column = ((VideoList) getContainer()).indexData(); + if (column >= 0) path = c.getString(column); + } + } + return path; + } + + @Override + public Bitmap fullSizeBitmap(int targetWidthHeight) { + return ImageManager.NO_IMAGE_BITMAP; + } + + @Override + public IGetBitmapCancelable fullSizeBitmapCancelable( + int targetWidthHeight) { + return null; + } + + @Override + public InputStream fullSizeImageData() { + try { + InputStream input = mContentResolver.openInputStream( + fullSizeImageUri()); + return input; + } catch (IOException ex) { + return null; + } + } + + @Override + public long fullSizeImageId() { + return mId; + } + + public String getCategory() { + return getStringEntry(VideoList.INDEX_CATEGORY); + } + + @Override + public int getHeight() { + return 0; + } + + public String getLanguage() { + return getStringEntry(VideoList.INDEX_LANGUAGE); + } + + @Override + public String getPicasaId() { + return null; + } + + private String getStringEntry(int entryName) { + String entry = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + entry = c.getString(entryName); + } + } + return entry; + } + + public String getTags() { + return getStringEntry(VideoList.INDEX_TAGS); + } + + @Override + public int getWidth() { + return 0; + } + + @Override + public long imageId() { + return mId; + } + + public boolean isReadonly() { + return false; + } + + public boolean isDrm() { + return false; + } + + public boolean rotateImageBy(int degrees) { + return false; + } + + public void setCategory(String category) { + setStringEntry(category, VideoList.INDEX_CATEGORY); + } + + public void setLanguage(String language) { + setStringEntry(language, VideoList.INDEX_LANGUAGE); + } + + private void setStringEntry(String entry, int entryName) { + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + c.updateString(entryName, entry); + } + } + } + + public void setTags(String tags) { + setStringEntry(tags, VideoList.INDEX_TAGS); + } + + public Bitmap thumbBitmap() { + return fullSizeBitmap(320); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("" + mId); + return sb.toString(); + } +}
\ No newline at end of file |