summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/com/android/camera/Camera.java18
-rw-r--r--src/com/android/camera/CropImage.java10
-rw-r--r--src/com/android/camera/ExifInterface.java23
-rw-r--r--src/com/android/camera/GalleryPicker.java17
-rw-r--r--src/com/android/camera/ImageGallery2.java40
-rw-r--r--src/com/android/camera/ImageLoader.java48
-rwxr-xr-xsrc/com/android/camera/ImageManager.java4158
-rw-r--r--src/com/android/camera/MenuHelper.java27
-rw-r--r--src/com/android/camera/SelectedImageGetter.java4
-rw-r--r--src/com/android/camera/SlideShow.java17
-rw-r--r--src/com/android/camera/ThumbnailController.java4
-rw-r--r--src/com/android/camera/VideoCamera.java12
-rw-r--r--src/com/android/camera/ViewImage.java34
-rw-r--r--src/com/android/camera/gallery/BaseCancelable.java69
-rw-r--r--src/com/android/camera/gallery/BaseImage.java602
-rw-r--r--src/com/android/camera/gallery/BaseImageList.java870
-rw-r--r--src/com/android/camera/gallery/CanceledException.java9
-rw-r--r--src/com/android/camera/gallery/DrmImageList.java171
-rw-r--r--src/com/android/camera/gallery/IAddImageCancelable.java10
-rw-r--r--src/com/android/camera/gallery/ICancelable.java30
-rw-r--r--src/com/android/camera/gallery/IGetBitmapCancelable.java27
-rw-r--r--src/com/android/camera/gallery/IGetBooleanCancelable.java24
-rw-r--r--src/com/android/camera/gallery/IImage.java128
-rw-r--r--src/com/android/camera/gallery/IImageList.java98
-rw-r--r--src/com/android/camera/gallery/Image.java410
-rw-r--r--src/com/android/camera/gallery/ImageList.java341
-rw-r--r--src/com/android/camera/gallery/ImageListUber.java303
-rw-r--r--src/com/android/camera/gallery/SimpleBaseImage.java136
-rw-r--r--src/com/android/camera/gallery/SingleImageList.java391
-rw-r--r--src/com/android/camera/gallery/ThreadSafeOutputStream.java67
-rw-r--r--src/com/android/camera/gallery/Util.java216
-rw-r--r--src/com/android/camera/gallery/VideoList.java283
-rw-r--r--src/com/android/camera/gallery/VideoObject.java188
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