summaryrefslogtreecommitdiffstats
path: root/src/com/android/camera/gallery
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/camera/gallery')
-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
20 files changed, 4373 insertions, 0 deletions
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