summaryrefslogtreecommitdiffstats
path: root/src/com/android/camera/ImageManager.java
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
committerThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
commit1d4c75065966c4f6f56900e31f655bfd1b334435 (patch)
tree5d4526db3153daa63087fcb9384f8bc0659fbd18 /src/com/android/camera/ImageManager.java
downloadLegacyCamera-1d4c75065966c4f6f56900e31f655bfd1b334435.zip
LegacyCamera-1d4c75065966c4f6f56900e31f655bfd1b334435.tar.gz
LegacyCamera-1d4c75065966c4f6f56900e31f655bfd1b334435.tar.bz2
Initial Contribution
Diffstat (limited to 'src/com/android/camera/ImageManager.java')
-rwxr-xr-xsrc/com/android/camera/ImageManager.java3632
1 files changed, 3632 insertions, 0 deletions
diff --git a/src/com/android/camera/ImageManager.java b/src/com/android/camera/ImageManager.java
new file mode 100755
index 0000000..8d3f90a
--- /dev/null
+++ b/src/com/android/camera/ImageManager.java
@@ -0,0 +1,3632 @@
+/*
+ * Copyright (C) 2007 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;
+
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.ContentUris;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.location.Location;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
+import android.provider.DrmStore;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Images.Thumbnails;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.MediaColumns;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ *
+ * ImageManager is used to retrieve and store images
+ * in the media content provider.
+ *
+ */
+public class ImageManager {
+ public static final String CAMERA_IMAGE_BUCKET_NAME = "/sdcard/dcim/camera";
+ public static final String CAMERA_IMAGE_BUCKET_ID = String.valueOf(CAMERA_IMAGE_BUCKET_NAME.hashCode());
+
+ // To enable verbose logging for this class, change false to true. The other logic ensures that
+ // this logging can be disabled by turned off DEBUG and lower, and that it can be enabled by
+ // "setprop log.tag.ImageManager VERBOSE" if desired.
+ //
+ // IMPORTANT: Never check in this file set to true!
+ private static final boolean VERBOSE = Config.LOGD && (false || Config.LOGV);
+ private static final String TAG = "ImageManager";
+
+ private static final int MINI_THUMB_DATA_FILE_VERSION = 3;
+
+ static public void debug_where(String tag, String msg) {
+ try {
+ throw new Exception();
+ } catch (Exception ex) {
+ if (msg != null) {
+ Log.v(tag, msg);
+ }
+ boolean first = true;
+ for (StackTraceElement s : ex.getStackTrace()) {
+ if (first)
+ first = false;
+ else
+ Log.v(tag, s.toString());
+ }
+ }
+ }
+
+ /*
+ * Compute the sample size as a function of the image size and the target.
+ * Scale the image down so that both the width and height are just above
+ * the target. If this means that one of the dimension goes from above
+ * the target to below the target (e.g. given a width of 480 and an image
+ * width of 600 but sample size of 2 -- i.e. new width 300 -- bump the
+ * sample size down by 1.
+ */
+ private static int computeSampleSize(BitmapFactory.Options options, int target) {
+ int w = options.outWidth;
+ int h = options.outHeight;
+
+ int candidateW = w / target;
+ int candidateH = h / target;
+ int candidate = Math.max(candidateW, candidateH);
+
+ if (candidate == 0)
+ return 1;
+
+ if (candidate > 1) {
+ if ((w > target) && (w / candidate) < target)
+ candidate -= 1;
+ }
+
+ if (candidate > 1) {
+ if ((h > target) && (h / candidate) < target)
+ candidate -= 1;
+ }
+
+ if (VERBOSE)
+ Log.v(TAG, "for w/h " + w + "/" + h + " returning " + candidate + "(" + (w/candidate) + " / " + (h/candidate));
+
+ return candidate;
+ }
+ /*
+ * All implementors of ICancelable should inherit from BaseCancelable
+ * since it provides some convenience methods such as acknowledgeCancel
+ * and checkCancel.
+ */
+ public abstract class BaseCancelable implements ICancelable {
+ boolean mCancel = false;
+ boolean mFinished = false;
+
+ /*
+ * Subclasses should call acknowledgeCancel when they're finished with
+ * their operation.
+ */
+ protected void acknowledgeCancel() {
+ synchronized (this) {
+ mFinished = true;
+ if (!mCancel)
+ return;
+ if (mCancel) {
+ this.notify();
+ }
+ }
+ }
+
+ public boolean cancel() {
+ synchronized (this) {
+ if (mCancel) {
+ return false;
+ }
+ if (mFinished) {
+ return false;
+ }
+ mCancel = true;
+ boolean retVal = doCancelWork();
+
+ try {
+ this.wait();
+ } catch (InterruptedException ex) {
+ // now what??? TODO
+ }
+
+ return retVal;
+ }
+ }
+
+ /*
+ * Subclasses can call this to see if they have been canceled.
+ * This is the polling model.
+ */
+ protected void checkCanceled() throws CanceledException {
+ synchronized (this) {
+ if (mCancel)
+ throw new CanceledException();
+ }
+ }
+
+ /*
+ * Subclasses implement this method to take whatever action
+ * is necessary when getting canceled. Sometimes it's not
+ * possible to do anything in which case the "checkCanceled"
+ * polling model may be used (or some combination).
+ */
+ public abstract boolean doCancelWork();
+ }
+
+ private static final int sBytesPerMiniThumb = 10000;
+ static final private byte [] sMiniThumbData = new byte[sBytesPerMiniThumb];
+
+ /**
+ * Represents a particular image and provides access
+ * to the underlying bitmap and two thumbnail bitmaps
+ * as well as other information such as the id, and
+ * the path to the actual image data.
+ */
+ abstract class BaseImage implements IImage {
+ protected ContentResolver mContentResolver;
+ protected long mId, mMiniThumbMagic;
+ protected BaseImageList mContainer;
+ protected HashMap<String, String> mExifData;
+ protected int mCursorRow;
+
+ protected BaseImage(long id, long miniThumbId, ContentResolver cr, BaseImageList container, int cursorRow) {
+ mContentResolver = cr;
+ mId = id;
+ mMiniThumbMagic = miniThumbId;
+ mContainer = container;
+ mCursorRow = cursorRow;
+ }
+
+ abstract Bitmap.CompressFormat compressionType();
+
+ public void commitChanges() {
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.commitUpdates();
+ c.requery();
+ }
+ }
+ }
+
+ /**
+ * Take a given bitmap and compress it to a file as described
+ * by the Uri parameter.
+ *
+ * @param bitmap the bitmap to be compressed/stored
+ * @param uri where to store the bitmap
+ * @return true if we succeeded
+ */
+ protected IGetBoolean_cancelable compressImageToFile(
+ final Bitmap bitmap,
+ final byte [] jpegData,
+ final Uri uri) {
+ class CompressImageToFile extends BaseCancelable implements IGetBoolean_cancelable {
+ ThreadSafeOutputStream mOutputStream = null;
+
+ public boolean doCancelWork() {
+ if (mOutputStream != null) {
+ try {
+ mOutputStream.close();
+ return true;
+ } catch (IOException ex) {
+ // TODO what to do here
+ }
+ }
+ return false;
+ }
+
+ public boolean get() {
+ try {
+ long t1 = System.currentTimeMillis();
+ OutputStream delegate = mContentResolver.openOutputStream(uri);
+ synchronized (this) {
+ checkCanceled();
+ mOutputStream = new ThreadSafeOutputStream(delegate);
+ }
+ long t2 = System.currentTimeMillis();
+ if (bitmap != null) {
+ bitmap.compress(compressionType(), 75, mOutputStream);
+ } else {
+ long x1 = System.currentTimeMillis();
+ mOutputStream.write(jpegData);
+ long x2 = System.currentTimeMillis();
+ if (VERBOSE) Log.v(TAG, "done writing... " + jpegData.length + " bytes took " + (x2-x1));
+ }
+ long t3 = System.currentTimeMillis();
+ if (VERBOSE) Log.v(TAG, String.format("CompressImageToFile.get took %d (%d, %d)",(t3-t1),(t2-t1),(t3-t2)));
+ return true;
+ } catch (FileNotFoundException ex) {
+ return false;
+ } catch (CanceledException ex) {
+ return false;
+ } catch (IOException ex) {
+ return false;
+ }
+ finally {
+ if (mOutputStream != null) {
+ try {
+ mOutputStream.close();
+ } catch (IOException ex) {
+ // not much we can do here so ignore
+ }
+ }
+ acknowledgeCancel();
+ }
+ }
+ }
+ return new CompressImageToFile();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null)
+ return false;
+ if (!(other instanceof Image))
+ return false;
+
+ return fullSizeImageUri().equals(((Image)other).fullSizeImageUri());
+ }
+
+ public Bitmap fullSizeBitmap(int targetWidthHeight) {
+ return fullSizeBitmap(targetWidthHeight, true);
+ }
+
+ protected Bitmap fullSizeBitmap(int targetWidthHeight, boolean rotateAsNeeded) {
+ Uri url = mContainer.contentUri(mId);
+ if (VERBOSE) Log.v(TAG, "getCreateBitmap for " + url);
+ if (url == null)
+ return null;
+
+ Bitmap b = null;
+ if (b == null) {
+ b = makeBitmap(targetWidthHeight, url);
+ if (b != null && rotateAsNeeded) {
+ b = rotate(b, getDegreesRotated());
+ }
+ }
+ return b;
+ }
+
+
+ public IGetBitmap_cancelable fullSizeBitmap_cancelable(final int targetWidthHeight) {
+ final class LoadBitmapCancelable extends BaseCancelable implements IGetBitmap_cancelable {
+ ParcelFileDescriptor mPFD;
+ BitmapFactory.Options mOptions = new BitmapFactory.Options();
+ long mCancelInitiationTime;
+
+ public LoadBitmapCancelable(ParcelFileDescriptor pfdInput) {
+ mPFD = pfdInput;
+ }
+
+ public boolean doCancelWork() {
+ if (VERBOSE)
+ Log.v(TAG, "requesting bitmap load cancel");
+ mCancelInitiationTime = System.currentTimeMillis();
+ mOptions.requestCancelDecode();
+ return true;
+ }
+
+ public Bitmap get() {
+ try {
+ Bitmap b = makeBitmap(targetWidthHeight, fullSizeImageUri(), mPFD, mOptions);
+ if (mCancelInitiationTime != 0) {
+ if (VERBOSE)
+ Log.v(TAG, "cancelation of bitmap load success==" + (b == null ? "TRUE" : "FALSE") + " -- took " + (System.currentTimeMillis() - mCancelInitiationTime));
+ }
+ if (b != null) {
+ int degrees = getDegreesRotated();
+ if (degrees != 0) {
+ Matrix m = new Matrix();
+ m.setRotate(degrees, (float) b.getWidth() / 2, (float) b.getHeight() / 2);
+ Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+ if (b != b2)
+ b.recycle();
+ b = b2;
+ }
+ }
+ return b;
+ } catch (Exception ex) {
+ return null;
+ } finally {
+ acknowledgeCancel();
+ }
+ }
+ }
+
+ try {
+ ParcelFileDescriptor pfdInput = mContentResolver.openFileDescriptor(fullSizeImageUri(), "r");
+ return new LoadBitmapCancelable(pfdInput);
+ } catch (FileNotFoundException ex) {
+ return null;
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ }
+ }
+
+ public InputStream fullSizeImageData() {
+ try {
+ InputStream input = mContentResolver.openInputStream(
+ fullSizeImageUri());
+ return input;
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
+ public long fullSizeImageId() {
+ return mId;
+ }
+
+ public Uri fullSizeImageUri() {
+ return mContainer.contentUri(mId);
+ }
+
+ public IImageList getContainer() {
+ return mContainer;
+ }
+
+ Cursor getCursor() {
+ return mContainer.getCursor();
+ }
+
+ public long getDateTaken() {
+ if (mContainer.indexDateTaken() < 0) return 0;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveToPosition(getRow());
+ return c.getLong(mContainer.indexDateTaken());
+ }
+ }
+
+ protected int getDegreesRotated() {
+ return 0;
+ }
+
+ public String getMimeType() {
+ if (mContainer.indexMimeType() < 0) {
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ fullSizeImageUri(),
+ new String[] { "_id", Images.Media.MIME_TYPE },
+ null,
+ null, null);
+ if (c != null && c.moveToFirst()) {
+ return c.getString(1);
+ } else {
+ return "";
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ } else {
+ String mimeType = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ mimeType = c.getString(mContainer.indexMimeType());
+ }
+ }
+ return mimeType;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#getDescription()
+ */
+ public String getDescription() {
+ if (mContainer.indexDescription() < 0) {
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ fullSizeImageUri(),
+ new String[] { "_id", Images.Media.DESCRIPTION },
+ null,
+ null, null);
+ if (c != null && c.moveToFirst()) {
+ return c.getString(1);
+ } else {
+ return "";
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ } else {
+ String description = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ description = c.getString(mContainer.indexDescription());
+ }
+ }
+ return description;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#getIsPrivate()
+ */
+ public boolean getIsPrivate() {
+ if (mContainer.indexPrivate() < 0) return false;
+ boolean isPrivate = false;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ isPrivate = c.getInt(mContainer.indexPrivate()) != 0;
+ }
+ }
+ return isPrivate;
+ }
+
+ public double getLatitude() {
+ if (mContainer.indexLatitude() < 0) return 0D;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveToPosition(getRow());
+ return c.getDouble(mContainer.indexLatitude());
+ }
+ }
+
+ public double getLongitude() {
+ if (mContainer.indexLongitude() < 0) return 0D;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveToPosition(getRow());
+ return c.getDouble(mContainer.indexLongitude());
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#getTitle()
+ */
+ public String getTitle() {
+ String name = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ if (mContainer.indexTitle() != -1) {
+ name = c.getString(mContainer.indexTitle());
+ }
+ }
+ }
+ return name != null && name.length() > 0 ? name : String.valueOf(mId);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#getDisplayName()
+ */
+ public String getDisplayName() {
+ if (mContainer.indexDisplayName() < 0) {
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ fullSizeImageUri(),
+ new String[] { "_id", Images.Media.DISPLAY_NAME },
+ null,
+ null, null);
+ if (c != null && c.moveToFirst()) {
+ return c.getString(1);
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ } else {
+ String name = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ name = c.getString(mContainer.indexDisplayName());
+ }
+ }
+ if (name != null && name.length() > 0)
+ return name;
+ }
+ return String.valueOf(mId);
+ }
+
+ public String getPicasaId() {
+ /*
+ if (mContainer.indexPicasaWeb() < 0) return null;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveTo(getRow());
+ return c.getString(mContainer.indexPicasaWeb());
+ }
+ */
+ return null;
+ }
+
+ public int getRow() {
+ return mCursorRow;
+ }
+
+ public int getWidth() {
+ ParcelFileDescriptor input = null;
+ try {
+ Uri uri = fullSizeImageUri();
+ input = mContentResolver.openFileDescriptor(uri, "r");
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options);
+ return options.outWidth;
+ } catch (IOException ex) {
+ return 0;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ public int getHeight() {
+ ParcelFileDescriptor input = null;
+ try {
+ Uri uri = fullSizeImageUri();
+ input = mContentResolver.openFileDescriptor(uri, "r");
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options);
+ return options.outHeight;
+ } catch (IOException ex) {
+ return 0;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ public boolean hasLatLong() {
+ if (mContainer.indexLatitude() < 0 || mContainer.indexLongitude() < 0) return false;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveToPosition(getRow());
+ return !c.isNull(mContainer.indexLatitude()) && !c.isNull(mContainer.indexLongitude());
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#imageId()
+ */
+ public long imageId() {
+ return mId;
+ }
+
+ /**
+ * Make a bitmap from a given Uri.
+ *
+ * @param uri
+ */
+ private Bitmap makeBitmap(int targetWidthOrHeight, Uri uri) {
+ ParcelFileDescriptor input = null;
+ try {
+ input = mContentResolver.openFileDescriptor(uri, "r");
+ return makeBitmap(targetWidthOrHeight, uri, input, null);
+ } catch (IOException ex) {
+ return null;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options) {
+ return mContainer.makeBitmap(targetWidthHeight, uri, pfdInput, options);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#thumb1()
+ */
+ public Bitmap miniThumbBitmap() {
+ try {
+ long id = mId;
+ long dbMagic = mMiniThumbMagic;
+ if (dbMagic == 0 || dbMagic == id) {
+ dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), getRow());
+ if (VERBOSE) Log.v(TAG, "after computing thumbnail dbMagic is " + dbMagic);
+ }
+
+ synchronized(sMiniThumbData) {
+ dbMagic = mMiniThumbMagic;
+ byte [] data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic);
+ if (data == null) {
+ dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), getRow());
+ data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic);
+ }
+ if (data == null) {
+ if (VERBOSE)
+ Log.v(TAG, "unable to get miniThumbBitmap, data is null");
+ }
+ if (data != null) {
+ Bitmap b = BitmapFactory.decodeByteArray(data, 0, data.length);
+ if (b == null) {
+ if (VERBOSE) {
+ Log.v(TAG, "couldn't decode byte array for mini thumb, length was " + data.length);
+ }
+ }
+ return b;
+ }
+ }
+ return null;
+ } catch (Exception ex) {
+ 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) {
+ mContainer.saveMiniThumbToFile(source, fullSizeImageId(), 0);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#setName()
+ */
+ public void setDescription(String description) {
+ if (mContainer.indexDescription() < 0) return;
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.updateString(mContainer.indexDescription(), description);
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#setIsPrivate()
+ */
+ public void setIsPrivate(boolean isPrivate) {
+ if (mContainer.indexPrivate() < 0) return;
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.updateInt(mContainer.indexPrivate(), isPrivate ? 1 : 0);
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#setName()
+ */
+ public void setName(String name) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.updateString(mContainer.indexTitle(), name);
+ }
+ }
+ }
+
+ public void setPicasaId(String id) {
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ fullSizeImageUri(),
+ new String[] { "_id", Images.Media.PICASA_ID },
+ null,
+ null, null);
+ if (c != null && c.moveToFirst()) {
+ if (VERBOSE) {
+ Log.v(TAG, "storing picasaid " + id + " for " + fullSizeImageUri());
+ }
+ c.updateString(1, id);
+ c.commitUpdates();
+ if (VERBOSE) {
+ Log.v(TAG, "updated image with picasa id " + id);
+ }
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#thumbUri()
+ */
+ public Uri thumbUri() {
+ Uri uri = fullSizeImageUri();
+ // The value for the query parameter cannot be null :-(, so using a dummy "1"
+ uri = uri.buildUpon().appendQueryParameter("thumb", "1").build();
+ return uri;
+ }
+
+ @Override
+ public String toString() {
+ return fullSizeImageUri().toString();
+ }
+ }
+
+ abstract static class BaseImageList implements IImageList {
+ Context mContext;
+ ContentResolver mContentResolver;
+ Uri mBaseUri, mUri;
+ int mSort;
+ String mBucketId;
+ boolean mDistinct;
+ Cursor mCursor;
+ boolean mCursorDeactivated;
+ protected HashMap<Long, IImage> mCache = new HashMap<Long, IImage>();
+
+ IImageList.OnChange mListener = null;
+ Handler mHandler;
+ protected RandomAccessFile mMiniThumbData;
+ protected Uri mThumbUri;
+
+ public BaseImageList(Context ctx, ContentResolver cr, Uri uri, int sort, String bucketId) {
+ mContext = ctx;
+ mSort = sort;
+ mUri = uri;
+ mBaseUri = uri;
+
+ mContentResolver = cr;
+ }
+
+ String randomAccessFilePath(int version) {
+ String directoryName = Environment.getExternalStorageDirectory().toString() + "/dcim/.thumbnails";
+ String path = directoryName + "/.thumbdata" + version + "-" + mUri.hashCode();
+ return path;
+ }
+
+ RandomAccessFile miniThumbDataFile() {
+ if (mMiniThumbData == null) {
+ String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION);
+ File directory = new File(new File(path).getParent());
+ if (!directory.isDirectory()) {
+ if (!directory.mkdirs()) {
+ Log.e(TAG, "!!!! unable to create .thumbnails directory " + directory.toString());
+ }
+ }
+ File f = new File(path);
+ if (VERBOSE) Log.v(TAG, "file f is " + f.toString());
+ try {
+ mMiniThumbData = new RandomAccessFile(f, "rw");
+ } catch (IOException ex) {
+
+ }
+ }
+ return mMiniThumbData;
+ }
+
+ /**
+ * Store a given thumbnail in the database.
+ */
+ protected Bitmap storeThumbnail(Bitmap thumb, long imageId) {
+ if (thumb == null)
+ return null;
+
+ try {
+ Uri uri = getThumbnailUri(imageId, thumb.getWidth(), thumb.getHeight());
+ if (uri == null) {
+ return thumb;
+ }
+ OutputStream thumbOut = mContentResolver.openOutputStream(uri);
+ thumb.compress(Bitmap.CompressFormat.JPEG, 60, thumbOut);
+ thumbOut.close();
+ return thumb;
+ }
+ catch (Exception ex) {
+ Log.d(TAG, "unable to store thumbnail: " + ex);
+ return thumb;
+ }
+ }
+
+ /**
+ * Store a JPEG thumbnail from the EXIF header in the database.
+ */
+ protected boolean storeThumbnail(byte[] jpegThumbnail, long imageId, int width, int height) {
+ if (jpegThumbnail == null)
+ return false;
+
+ Uri uri = getThumbnailUri(imageId, width, height);
+ if (uri == null) {
+ return false;
+ }
+ try {
+ OutputStream thumbOut = mContentResolver.openOutputStream(uri);
+ thumbOut.write(jpegThumbnail);
+ thumbOut.close();
+ return true;
+ }
+ catch (FileNotFoundException ex) {
+ return false;
+ }
+ catch (IOException ex) {
+ return false;
+ }
+ }
+
+ private Uri getThumbnailUri(long imageId, int width, int height) {
+ // we do not store thumbnails for DRM'd images
+ if (mThumbUri == null) {
+ return null;
+ }
+
+ Uri uri = null;
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ mThumbUri,
+ THUMB_PROJECTION,
+ Thumbnails.IMAGE_ID + "=?",
+ new String[]{String.valueOf(imageId)},
+ null);
+ if (c != null && c.moveToFirst()) {
+ // If, for some reaosn, we already have a row with a matching
+ // image id, then just update that row rather than creating a
+ // new row.
+ uri = ContentUris.withAppendedId(mThumbUri, c.getLong(indexThumbId()));
+ c.commitUpdates();
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ if (uri == null) {
+ ContentValues values = new ContentValues(4);
+ values.put(Images.Thumbnails.KIND, Images.Thumbnails.MINI_KIND);
+ values.put(Images.Thumbnails.IMAGE_ID, imageId);
+ values.put(Images.Thumbnails.HEIGHT, height);
+ values.put(Images.Thumbnails.WIDTH, width);
+ uri = mContentResolver.insert(mThumbUri, values);
+ }
+ return uri;
+ }
+
+ java.util.Random mRandom = new java.util.Random(System.currentTimeMillis());
+
+ protected SomewhatFairLock mLock = new SomewhatFairLock();
+
+ class SomewhatFairLock {
+ private Object mSync = new Object();
+ private boolean mLocked = false;
+ private ArrayList<Thread> mWaiting = new ArrayList<Thread>();
+
+ void lock() {
+// if (VERBOSE) Log.v(TAG, "lock... thread " + Thread.currentThread().getId());
+ synchronized (mSync) {
+ while (mLocked) {
+ try {
+// if (VERBOSE) Log.v(TAG, "waiting... thread " + Thread.currentThread().getId());
+ mWaiting.add(Thread.currentThread());
+ mSync.wait();
+ if (mWaiting.get(0) == Thread.currentThread()) {
+ mWaiting.remove(0);
+ break;
+ }
+ } catch (InterruptedException ex) {
+ //
+ }
+ }
+// if (VERBOSE) Log.v(TAG, "locked... thread " + Thread.currentThread().getId());
+ mLocked = true;
+ }
+ }
+
+ void unlock() {
+// if (VERBOSE) Log.v(TAG, "unlocking... thread " + Thread.currentThread().getId());
+ synchronized (mSync) {
+ mLocked = false;
+ mSync.notifyAll();
+ }
+ }
+ }
+
+ // If the photo has an EXIF thumbnail and it's big enough, extract it and save that JPEG as
+ // the large thumbnail without re-encoding it. We still have to decompress it though, in
+ // order to generate the minithumb.
+ private Bitmap createThumbnailFromEXIF(String filePath, long id) {
+ if (filePath != null) {
+ byte [] thumbData = null;
+ synchronized (ImageManager.instance()) {
+ thumbData = (new ExifInterface(filePath)).getThumbnail();
+ }
+ if (thumbData != null) {
+ // Sniff the size of the EXIF thumbnail before decoding it. Photos from the
+ // device will pass, but images that are side loaded from other cameras may not.
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options);
+ int width = options.outWidth;
+ int height = options.outHeight;
+ if (width >= THUMBNAIL_TARGET_SIZE && height >= THUMBNAIL_TARGET_SIZE) {
+ if (storeThumbnail(thumbData, id, width, height)) {
+ // this is used for *encoding* the minithumb, so
+ // we don't want to dither or convert to 565 here.
+ //
+ // Decode with a scaling factor
+ // to match MINI_THUMB_TARGET_SIZE closely
+ // which will produce much better scaling quality
+ // and is significantly faster.
+ options.inSampleSize = computeSampleSize(options, THUMBNAIL_TARGET_SIZE);
+
+ if (VERBOSE) {
+ Log.v(TAG, "in createThumbnailFromExif using inSampleSize of " + options.inSampleSize);
+ }
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ // The fallback case is to decode the original photo to thumbnail size, then encode it as a
+ // JPEG. We return the thumbnail Bitmap in order to create the minithumb from it.
+ private Bitmap createThumbnailFromUri(Cursor c, long id) {
+ Uri uri = ContentUris.withAppendedId(mBaseUri, id);
+ Bitmap bitmap = makeBitmap(THUMBNAIL_TARGET_SIZE, uri, null, null);
+ if (bitmap != null) {
+ storeThumbnail(bitmap, id);
+ } else {
+ uri = ContentUris.withAppendedId(mBaseUri, id);
+ bitmap = makeBitmap(MINI_THUMB_TARGET_SIZE, uri, null, null);
+ }
+ return bitmap;
+ }
+
+ // returns id
+ public long checkThumbnail(BaseImage existingImage, Cursor c, int i) {
+ long magic, fileMagic = 0, id;
+ try {
+ mLock.lock();
+ if (existingImage == null) {
+ // if we don't have an Image object then get the id and magic from
+ // the cursor. Synchonize on the cursor object.
+ synchronized (c) {
+ if (!c.moveToPosition(i)) {
+ return -1;
+ }
+ magic = c.getLong(indexMiniThumbId());
+ id = c.getLong(indexId());
+ }
+ } else {
+ // if we have an Image object then ask them for the magic/id
+ magic = existingImage.mMiniThumbMagic;
+ id = existingImage.fullSizeImageId();
+ }
+
+ if (magic != 0) {
+ // check the mini thumb file for the right data. Right is defined as
+ // having the right magic number at the offset reserved for this "id".
+ RandomAccessFile r = miniThumbDataFile();
+ if (r != null) {
+ synchronized (r) {
+ long pos = id * sBytesPerMiniThumb;
+ try {
+ // check that we can read the following 9 bytes (1 for the "status" and 8 for the long)
+ if (r.length() >= pos + 1 + 8) {
+ r.seek(pos);
+ if (r.readByte() == 1) {
+ fileMagic = r.readLong();
+ if (fileMagic == magic && magic != 0 && magic != id) {
+ return magic;
+ }
+ }
+ }
+ } catch (IOException ex) {
+ Log.v(TAG, "got exception checking file magic: " + ex);
+ }
+ }
+ }
+ if (VERBOSE) {
+ Log.v(TAG, "didn't verify... fileMagic: " + fileMagic + "; magic: " + magic + "; id: " + id + "; ");
+ }
+ }
+
+ // If we can't retrieve the thumbnail, first check if there is one embedded in the
+ // EXIF data. If not, or it's not big enough, decompress the full size image.
+ Bitmap bitmap = null;
+
+ String filePath = null;
+ synchronized (c) {
+ if (c.moveToPosition(i)) {
+ filePath = c.getString(indexData());
+ }
+ }
+
+ if (filePath != null) {
+ 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 b2 = rotate(bitmap, degrees);
+ if (b2 != bitmap)
+ bitmap.recycle();
+ bitmap = b2;
+ }
+ }
+ }
+
+ // make a new magic number since things are out of sync
+ do {
+ magic = mRandom.nextLong();
+ } while (magic == 0);
+ if (bitmap != null) {
+ saveMiniThumbToFile(bitmap, id, magic);
+ bitmap.recycle();
+ }
+
+ 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) {
+ Cursor c = Images.Media.query(
+ mContentResolver,
+ mBaseUri,
+ new String[] { "_id", "mini_thumb_magic" },
+ "mini_thumb_magic isnull and " + sWhereClause,
+ sAcceptableImageTypes,
+ "_id ASC");
+
+ int count = c.getCount();
+ if (VERBOSE)
+ Log.v(TAG, ">>>>>>>>>>> need to check " + c.getCount() + " rows");
+
+ c.close();
+
+ if (!ImageManager.hasStorage()) {
+ if (VERBOSE)
+ Log.v(TAG, "bailing from the image checker thread -- no storage");
+ return;
+ }
+
+ String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1);
+ File oldFile = new File(oldPath);
+
+ if (count == 0) {
+ // now check that we have the right thumbs file
+// Log.v(TAG, "count is zero but oldFile.exists() is " + oldFile.exists());
+ if (!oldFile.exists()) {
+ return;
+ }
+ }
+
+ c = getCursor();
+ try {
+ if (VERBOSE) Log.v(TAG, "checkThumbnails found " + c.getCount());
+ int max = c.getCount();
+ int current = 0;
+ for (int i = 0; i < c.getCount(); i++) {
+ try {
+ checkThumbnail(null, c, i);
+ } catch (Exception ex) {
+ Log.e(TAG, "!!!!! failed to check thumbnail... was the sd card removed?");
+ break;
+ }
+ if (cb != null) {
+ if (!cb.checking(current, max)) {
+ if (VERBOSE) Log.v(TAG, "got false from checking... break <<<<<<<<<<<<<<<<<<<<<<<<");
+ break;
+ }
+ }
+ current += 1;
+ }
+ } finally {
+ if (VERBOSE) Log.v(TAG, "checkThumbnails existing after reaching count " + c.getCount());
+ try {
+ oldFile.delete();
+ } catch (Exception ex) {
+ // ignore
+ }
+ }
+ }
+
+ 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;
+ mCursor.deactivate();
+ if (mMiniThumbData != null) {
+ try {
+ mMiniThumbData.close();
+ mMiniThumbData = null;
+ } catch (IOException ex) {
+
+ }
+ }
+ }
+
+ public void dump(String msg) {
+ int count = getCount();
+ if (VERBOSE) Log.v(TAG, "dump ImageList (count is " + count + ") " + msg);
+ for (int i = 0; i < count; i++) {
+ IImage img = getImageAt(i);
+ if (img == null)
+ if (VERBOSE) Log.v(TAG, " " + i + ": " + "null");
+ else
+ if (VERBOSE) Log.v(TAG, " " + i + ": " + img.toString());
+ }
+ if (VERBOSE) Log.v(TAG, "end of dump container");
+ }
+ public int getCount() {
+ Cursor c = getCursor();
+ synchronized (c) {
+ try {
+ return c.getCount();
+ } catch (Exception ex) {
+ }
+ return 0;
+ }
+ }
+ protected Cursor getCursor() {
+ synchronized (mCursor) {
+ if (mCursorDeactivated) {
+ activateCursor();
+ }
+ return mCursor;
+ }
+ }
+
+ protected void activateCursor() {
+ requery();
+ }
+
+ public IImage getImageAt(int i) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ boolean moved;
+ try {
+ moved = c.moveToPosition(i);
+ } catch (Exception ex) {
+ return null;
+ }
+ if (moved) {
+ try {
+ long id = c.getLong(0);
+ long miniThumbId = 0;
+ int rotation = 0;
+ if (indexMiniThumbId() != -1) {
+ miniThumbId = c.getLong(indexMiniThumbId());
+ }
+ if (indexOrientation() != -1) {
+ rotation = c.getInt(indexOrientation());
+ }
+ long timestamp = c.getLong(1);
+ IImage img = mCache.get(id);
+ if (img == null) {
+ img = make(id, miniThumbId, mContentResolver, this, timestamp, i, rotation);
+ mCache.put(id, img);
+ }
+ return img;
+ } catch (Exception ex) {
+ Log.e(TAG, "got this exception trying to create image object: " + ex);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "unable to moveTo to " + i + "; count is " + c.getCount());
+ return null;
+ }
+ }
+ }
+ public IImage getImageForUri(Uri uri) {
+ // TODO make this a hash lookup
+ for (int i = 0; i < getCount(); i++) {
+ if (getImageAt(i).fullSizeImageUri().equals(uri)) {
+ return getImageAt(i);
+ }
+ }
+ return null;
+ }
+ private byte [] getMiniThumbFromFile(long id, byte [] data, long magicCheck) {
+ RandomAccessFile r = miniThumbDataFile();
+ if (r == null)
+ return null;
+
+ long pos = id * sBytesPerMiniThumb;
+ RandomAccessFile f = r;
+ synchronized (f) {
+ try {
+ f.seek(pos);
+ if (f.readByte() == 1) {
+ long magic = f.readLong();
+ if (magic != magicCheck) {
+ if (VERBOSE) Log.v(TAG, "for id " + id + "; magic: " + magic + "; magicCheck: " + magicCheck + " (fail)");
+ return null;
+ }
+ int length = f.readInt();
+ f.read(data, 0, length);
+ return data;
+ } else {
+ return null;
+ }
+ } catch (IOException ex) {
+ long fileLength;
+ try {
+ fileLength = f.length();
+ } catch (IOException ex1) {
+ fileLength = -1;
+ }
+ Log.e(TAG, "couldn't read thumbnail for " + id + "; " + ex.toString() + "; pos is " + pos + "; length is " + fileLength);
+ return null;
+ }
+ }
+ }
+ protected int getRowFor(IImage imageObj) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ int index = 0;
+ long targetId = imageObj.fullSizeImageId();
+ if (c.moveToFirst()) {
+ do {
+ if (c.getLong(0) == targetId) {
+ return index;
+ }
+ index += 1;
+ } while (c.moveToNext());
+ }
+ return -1;
+ }
+ }
+
+ protected abstract int indexOrientation();
+ protected abstract int indexDateTaken();
+ protected abstract int indexDescription();
+ protected abstract int indexMimeType();
+ protected abstract int indexData();
+ protected abstract int indexId();
+ protected abstract int indexLatitude();
+ protected abstract int indexLongitude();
+ protected abstract int indexMiniThumbId();
+ protected abstract int indexPicasaWeb();
+ protected abstract int indexPrivate();
+ protected abstract int indexTitle();
+ protected abstract int indexDisplayName();
+ protected abstract int indexThumbId();
+
+ protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) {
+ return null;
+ }
+
+ protected abstract Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options);
+
+ public boolean removeImage(IImage image) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ /*
+ * TODO: consider putting the image in a holding area so
+ * we can get it back as needed
+ * TODO: need to delete the thumbnails as well
+ */
+ boolean moved;
+ try {
+ moved = c.moveToPosition(image.getRow());
+ } catch (Exception ex) {
+ Log.e(TAG, "removeImage got exception " + ex.toString());
+ return false;
+ }
+ if (moved) {
+ Uri u = image.fullSizeImageUri();
+ mContentResolver.delete(u, null, null);
+ image.onRemove();
+ requery();
+ }
+ }
+ return false;
+ }
+
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImageList#removeImageAt(int)
+ */
+ public void removeImageAt(int i) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ /*
+ * TODO: consider putting the image in a holding area so
+ * we can get it back as needed
+ * TODO: need to delete the thumbnails as well
+ */
+ dump("before delete");
+ IImage image = getImageAt(i);
+ boolean moved;
+ try {
+ moved = c.moveToPosition(i);
+ } catch (Exception ex) {
+ return;
+ }
+ if (moved) {
+ Uri u = image.fullSizeImageUri();
+ mContentResolver.delete(u, null, null);
+ requery();
+ image.onRemove();
+ }
+ dump("after delete");
+ }
+ }
+
+ public void removeOnChangeListener(OnChange changeCallback) {
+ if (changeCallback == mListener)
+ mListener = null;
+ }
+
+ protected void requery() {
+ mCache.clear();
+ mCursor.requery();
+ mCursorDeactivated = false;
+ }
+
+ protected void saveMiniThumbToFile(Bitmap source, long id, long magic) {
+ RandomAccessFile r = miniThumbDataFile();
+ if (r == null)
+ return;
+
+ long pos = id * sBytesPerMiniThumb;
+ long t0 = System.currentTimeMillis();
+ synchronized (r) {
+ try {
+ long t1 = System.currentTimeMillis();
+ byte [] data = miniThumbData(source);
+ long t2 = System.currentTimeMillis();
+ if (data != null) {
+ if (data.length > sBytesPerMiniThumb) {
+ if (VERBOSE) Log.v(TAG, "!!!!!!!!!!!!!!!!!!!!!!!!!!! " + data.length + " > " + sBytesPerMiniThumb);
+ return;
+ }
+ r.seek(pos);
+ r.writeByte(0); // we have no data in this slot
+
+ // if magic is 0 then leave it alone
+ if (magic == 0)
+ r.skipBytes(8);
+ else
+ r.writeLong(magic);
+ r.writeInt(data.length);
+ r.write(data);
+ // f.flush();
+ r.seek(pos);
+ r.writeByte(1); // we have data in this slot
+ long t3 = System.currentTimeMillis();
+
+ if (VERBOSE) Log.v(TAG, "saveMiniThumbToFile took " + (t3-t0) + "; " + (t1-t0) + " " + (t2-t1) + " " + (t3-t2));
+ }
+ } catch (IOException ex) {
+ Log.e(TAG, "couldn't save mini thumbnail data for " + id + "; " + ex.toString());
+ }
+ }
+ }
+
+ public void setOnChangeListener(OnChange changeCallback, Handler h) {
+ mListener = changeCallback;
+ mHandler = h;
+ }
+ }
+
+ public class CanceledException extends Exception {
+
+ }
+ public enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL }
+
+ public interface IAddImage_cancelable extends ICancelable {
+ public void get();
+ }
+
+ /*
+ * The model for canceling an in-progress image save is this. For any
+ * given part of the task of saving return an ICancelable. The "result"
+ * from an ICancelable can be retrieved using the get* method. If the
+ * operation was canceled then null is returned. The act of canceling
+ * is to call "cancel" -- from another thread.
+ *
+ * In general an object which implements ICancelable will need to
+ * check, periodically, whether they are canceled or not. This works
+ * well for some things and less well for others.
+ *
+ * Right now the actual jpeg encode does not check cancelation but
+ * the part of encoding which writes the data to disk does. Note,
+ * though, that there is what appears to be a bug in the jpeg encoder
+ * in that if the stream that's being written is closed it crashes
+ * rather than returning an error. TODO fix that.
+ *
+ * When an object detects that it is canceling it must, before exiting,
+ * call acknowledgeCancel. This is necessary because the caller of
+ * cancel() will block until acknowledgeCancel is called.
+ */
+ public interface ICancelable {
+ /*
+ * call cancel() when the unit of work in progress needs to be
+ * canceled. This should return true if it was possible to
+ * cancel and false otherwise. If this returns false the caller
+ * may still be able to cleanup and simulate cancelation.
+ */
+ public boolean cancel();
+ }
+
+ public interface IGetBitmap_cancelable extends ICancelable {
+ // returns the bitmap or null if there was an error or we were canceled
+ public Bitmap get();
+ };
+ public interface IGetBoolean_cancelable extends ICancelable {
+ public boolean get();
+ }
+ public interface IImage {
+
+ public abstract void commitChanges();
+
+ /**
+ * Get the bitmap for the full size image.
+ * @return the bitmap for the full size image.
+ */
+ public abstract Bitmap fullSizeBitmap(int targetWidthOrHeight);
+
+ /**
+ *
+ * @return an object which can be canceled while the bitmap is loading
+ */
+ public abstract IGetBitmap_cancelable fullSizeBitmap_cancelable(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();
+ }
+ public interface IImageList {
+ public HashMap<String, String> getBucketIds();
+
+ public interface OnChange {
+ public void onChange(IImageList list);
+ }
+
+ public interface ThumbCheckCallback {
+ public boolean checking(int current, int count);
+ }
+
+ public abstract void checkThumbnails(ThumbCheckCallback cb);
+
+ public abstract void commitChanges();
+
+ public abstract void deactivate();
+
+ /**
+ * Returns the count of image objects.
+ *
+ * @return the number of images
+ */
+ public abstract int getCount();
+
+ /**
+ * 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);;
+
+ public abstract boolean removeImage(IImage image);
+ /**
+ * Removes the image at the ith position.
+ * @param i the position
+ */
+ public abstract void removeImageAt(int i);
+
+ public abstract void removeOnChangeListener(OnChange changeCallback);
+ public abstract void setOnChangeListener(OnChange changeCallback, Handler h);
+ }
+
+ class VideoObject extends Image {
+ public VideoObject() {
+ super(0, 0, null, null, 0, 0);
+ }
+
+ public String getTags() {
+ return null;
+ }
+
+ public String setTags(String tags) {
+ return null;
+ }
+ }
+
+ class Image extends BaseImage implements IImage {
+ int mRotation;
+
+ protected Image(long id, long miniThumbId, ContentResolver cr, BaseImageList container, int cursorRow, int rotation) {
+ super(id, miniThumbId, cr, container, cursorRow);
+ mRotation = rotation;
+ }
+
+ public String getDataPath() {
+ String path = null;
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ int column = ((ImageList)getContainer()).indexData();
+ if (column >= 0)
+ path = c.getString(column);
+ }
+ }
+ return path;
+ }
+
+ protected int getDegreesRotated() {
+ return mRotation;
+ }
+
+ protected void setDegreesRotated(int degrees) {
+ Cursor c = getCursor();
+ mRotation = degrees;
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ int column = ((ImageList)getContainer()).indexOrientation();
+ if (column >= 0) {
+ c.updateInt(column, degrees);
+ getContainer().commitChanges();
+ }
+ }
+ }
+ }
+
+ protected Bitmap.CompressFormat compressionType() {
+ String mimeType = getMimeType();
+ if (mimeType == null)
+ return Bitmap.CompressFormat.JPEG;
+
+ if (mimeType.equals("image/png"))
+ return Bitmap.CompressFormat.PNG;
+ else if (mimeType.equals("image/png"))
+ return Bitmap.CompressFormat.PNG;
+
+ return Bitmap.CompressFormat.JPEG;
+ }
+
+ /**
+ * Does not replace the tag if already there. Otherwise, adds to the exif tags.
+ * @param tag
+ * @param value
+ */
+ public void addExifTag(String tag, String value) {
+ if (mExifData == null) {
+ mExifData = new HashMap<String, String>();
+ }
+ if (!mExifData.containsKey(tag)) {
+ mExifData.put(tag, value);
+ } else {
+ if (VERBOSE) Log.v(TAG, "addExifTag where the key already was there: " + tag + " = " + value);
+ }
+ }
+
+ /**
+ * Return the value of the Exif tag as an int. Returns 0 on any type of error.
+ * @param tag
+ * @return
+ */
+ public int getExifTagInt(String tag) {
+ if (mExifData != null) {
+ String tagValue = mExifData.get(tag);
+ if (tagValue != null) {
+ return Integer.parseInt(tagValue);
+ }
+ }
+ return 0;
+ }
+
+ public boolean isReadonly() {
+ String mimeType = getMimeType();
+ return !"image/jpeg".equals(mimeType) && !"image/png".equals(mimeType);
+ }
+
+ public boolean isDrm() {
+ return false;
+ }
+
+ /**
+ * Remove tag if already there. Otherwise, does nothing.
+ * @param tag
+ */
+ public void removeExifTag(String tag) {
+ if (mExifData == null) {
+ mExifData = new HashMap<String, String>();
+ }
+ mExifData.remove(tag);
+ }
+
+ /**
+ * Replaces the tag if already there. Otherwise, adds to the exif tags.
+ * @param tag
+ * @param value
+ */
+ public void replaceExifTag(String tag, String value) {
+ if (mExifData == null) {
+ mExifData = new HashMap<String, String>();
+ }
+ if (!mExifData.containsKey(tag)) {
+ mExifData.remove(tag);
+ }
+ mExifData.put(tag, value);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#saveModifiedImage(android.graphics.Bitmap)
+ */
+ public IGetBoolean_cancelable saveImageContents(
+ final Bitmap image,
+ final byte [] jpegData,
+ final int orientation,
+ final boolean newFile,
+ final Cursor cursor) {
+ final class SaveImageContentsCancelable extends BaseCancelable implements IGetBoolean_cancelable {
+ IGetBoolean_cancelable mCurrentCancelable = null;
+
+ SaveImageContentsCancelable() {
+ }
+
+ public boolean doCancelWork() {
+ synchronized (this) {
+ if (mCurrentCancelable != null)
+ mCurrentCancelable.cancel();
+ }
+ return true;
+ }
+
+ public boolean get() {
+ try {
+ Bitmap thumbnail = null;
+
+ long t1 = System.currentTimeMillis();
+ Uri uri = mContainer.contentUri(mId);
+ synchronized (this) {
+ checkCanceled();
+ mCurrentCancelable = compressImageToFile(image, jpegData, uri);
+ }
+
+ long t2 = System.currentTimeMillis();
+ if (!mCurrentCancelable.get())
+ return false;
+
+ synchronized (this) {
+ String filePath;
+ synchronized (cursor) {
+ cursor.moveToPosition(0);
+ filePath = cursor.getString(2);
+ }
+ // TODO: If thumbData is present and usable, we should call the version
+ // of storeThumbnail which takes a byte array, rather than re-encoding
+ // a new JPEG of the same dimensions.
+ byte [] thumbData = null;
+ synchronized (ImageManager.instance()) {
+ thumbData = (new ExifInterface(filePath)).getThumbnail();
+ }
+ if (VERBOSE) Log.v(TAG, "for file " + filePath + " thumbData is " + thumbData + "; length " + (thumbData!=null ? thumbData.length : -1));
+ if (thumbData != null) {
+ thumbnail = BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length);
+ if (VERBOSE) Log.v(TAG, "embedded thumbnail bitmap " + thumbnail.getWidth() + "/" + thumbnail.getHeight());
+ }
+ if (thumbnail == null && image != null) {
+ thumbnail = image;
+ }
+ if (thumbnail == null && jpegData != null) {
+ thumbnail = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
+ }
+ }
+
+ long t3 = System.currentTimeMillis();
+ mContainer.storeThumbnail(thumbnail, Image.this.fullSizeImageId());
+ long t4 = System.currentTimeMillis();
+ checkCanceled();
+ if (VERBOSE) Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>> rotating by " + orientation);
+ saveMiniThumb(rotate(thumbnail, orientation));
+ long t5 = System.currentTimeMillis();
+ checkCanceled();
+
+ if (VERBOSE) Log.v(TAG, String.format("Timing data %d %d %d %d", t2-t1, t3-t2, t4-t3, t5-t4));
+ return true;
+ } catch (CanceledException ex) {
+ if (VERBOSE) Log.v(TAG, "got canceled... need to cleanup");
+ return false;
+ } finally {
+ /*
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveTo(getRow())) {
+ mContainer.requery();
+ }
+ }
+ */
+ acknowledgeCancel();
+ }
+ }
+ }
+ return new SaveImageContentsCancelable();
+ }
+
+ private void setExifRotation(int degrees) {
+ try {
+ Cursor c = getCursor();
+ String filePath;
+ synchronized (c) {
+ filePath = c.getString(mContainer.indexData());
+ }
+ synchronized (ImageManager.instance()) {
+ ExifInterface exif = new ExifInterface(filePath);
+ if (mExifData == null) {
+ mExifData = exif.getAttributes();
+ }
+ if (degrees < 0)
+ degrees += 360;
+
+ int orientation = ExifInterface.ORIENTATION_NORMAL;
+ switch (degrees) {
+ case 0:
+ orientation = ExifInterface.ORIENTATION_NORMAL;
+ break;
+ case 90:
+ orientation = ExifInterface.ORIENTATION_ROTATE_90;
+ break;
+ case 180:
+ orientation = ExifInterface.ORIENTATION_ROTATE_180;
+ break;
+ case 270:
+ orientation = ExifInterface.ORIENTATION_ROTATE_270;
+ break;
+ }
+
+ replaceExifTag(ExifInterface.TAG_ORIENTATION, Integer.toString(orientation));
+ replaceExifTag("UserComment", "saveRotatedImage comment orientation: " + orientation);
+ exif.saveAttributes(mExifData);
+ exif.commitChanges();
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "unable to save exif data with new orientation " + fullSizeImageUri());
+ }
+ }
+
+ /**
+ * Save the rotated image by updating the Exif "Orientation" tag.
+ * @param degrees
+ * @return
+ */
+ public boolean rotateImageBy(int degrees) {
+ int newDegrees = getDegreesRotated() + degrees;
+ setExifRotation(newDegrees);
+ setDegreesRotated(newDegrees);
+
+ // setting this to zero will force the call to checkCursor to generate fresh thumbs
+ mMiniThumbMagic = 0;
+ Cursor c = mContainer.getCursor();
+ synchronized (c) {
+ mContainer.checkThumbnail(this, mContainer.getCursor(), this.getRow());
+ }
+
+ return true;
+ }
+
+ public Bitmap thumbBitmap() {
+ Bitmap bitmap = null;
+ Cursor c = null;
+ if (mContainer.mThumbUri != null) {
+ try {
+ c = mContentResolver.query(
+ mContainer.mThumbUri,
+ THUMB_PROJECTION,
+ Thumbnails.IMAGE_ID + "=?",
+ new String[] { String.valueOf(fullSizeImageId()) },
+ null);
+ if (c != null && c.moveToFirst()) {
+ Uri thumbUri = ContentUris.withAppendedId(mContainer.mThumbUri, c.getLong(((ImageList)mContainer).INDEX_THUMB_ID));
+ ParcelFileDescriptor pfdInput;
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ pfdInput = mContentResolver.openFileDescriptor(thumbUri, "r");
+ bitmap = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+ pfdInput.close();
+ } catch (FileNotFoundException ex) {
+ Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
+ } catch (IOException ex) {
+ Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
+ } catch (NullPointerException ex) {
+ // we seem to get this if the file doesn't exist anymore
+ Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
+ }
+ }
+ } catch (Exception ex) {
+ // sdcard removed?
+ return null;
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ }
+
+ if (bitmap == null) {
+ bitmap = fullSizeBitmap(THUMBNAIL_TARGET_SIZE, false);
+ if (VERBOSE) {
+ Log.v(TAG, "no thumbnail found... storing new one for " + fullSizeImageId());
+ }
+ bitmap = mContainer.storeThumbnail(bitmap, fullSizeImageId());
+ }
+
+ if (bitmap != null) {
+ int degrees = getDegreesRotated();
+ if (degrees != 0) {
+ Matrix m = new Matrix();
+ m.setRotate(degrees, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2);
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),
+ m, true);
+ }
+ }
+
+ long elapsed = System.currentTimeMillis();
+ return bitmap;
+ }
+
+ }
+
+ final static private String sWhereClause = "(" + Images.Media.MIME_TYPE + "=? or " + Images.Media.MIME_TYPE + "=?" + ")";
+ final static private String[] sAcceptableImageTypes = new String[] { "image/jpeg", "image/png" };
+
+ private static final String[] IMAGE_PROJECTION = new String[] {
+ "_id",
+ "_data",
+ ImageColumns.DATE_TAKEN,
+ ImageColumns.MINI_THUMB_MAGIC,
+ ImageColumns.ORIENTATION,
+ ImageColumns.MIME_TYPE
+ };
+
+ /**
+ * Represents an ordered collection of Image objects.
+ * Provides an api to add and remove an image.
+ */
+ class ImageList extends BaseImageList implements IImageList {
+ final int INDEX_ID = indexOf(IMAGE_PROJECTION, "_id");
+ final int INDEX_DATA = indexOf(IMAGE_PROJECTION, "_data");
+ final int INDEX_MIME_TYPE = indexOf(IMAGE_PROJECTION, MediaColumns.MIME_TYPE);
+ final int INDEX_DATE_TAKEN = indexOf(IMAGE_PROJECTION, ImageColumns.DATE_TAKEN);
+ final int INDEX_MINI_THUMB_MAGIC = indexOf(IMAGE_PROJECTION, ImageColumns.MINI_THUMB_MAGIC);
+ final int INDEX_ORIENTATION = indexOf(IMAGE_PROJECTION, ImageColumns.ORIENTATION);
+
+ final int INDEX_THUMB_ID = indexOf(THUMB_PROJECTION, BaseColumns._ID);
+ final int INDEX_THUMB_IMAGE_ID = indexOf(THUMB_PROJECTION, Images.Thumbnails.IMAGE_ID);
+ final int INDEX_THUMB_WIDTH = indexOf(THUMB_PROJECTION, Images.Thumbnails.WIDTH);
+ final int INDEX_THUMB_HEIGHT = indexOf(THUMB_PROJECTION, Images.Thumbnails.HEIGHT);
+
+ boolean mIsRegistered = false;
+ ContentObserver mContentObserver;
+ DataSetObserver mDataSetObserver;
+
+ public HashMap<String, String> getBucketIds() {
+ Cursor c = Images.Media.query(
+ mContentResolver,
+ mBaseUri.buildUpon().appendQueryParameter("distinct", "true").build(),
+ new String[] {
+ ImageColumns.BUCKET_DISPLAY_NAME,
+ ImageColumns.BUCKET_ID
+ },
+ whereClause(),
+ whereClauseArgs(),
+ sortOrder());
+
+ HashMap<String, String> hash = new HashMap<String, String>();
+ if (c != null && c.moveToFirst()) {
+ do {
+ hash.put(c.getString(1), c.getString(0));
+ } while (c.moveToNext());
+ }
+ return hash;
+ }
+ /**
+ * ImageList constructor.
+ * @param cr ContentResolver
+ */
+ public ImageList(Context ctx, ContentResolver cr, Uri imageUri, Uri thumbUri, int sort, String bucketId) {
+ super(ctx, cr, imageUri, sort, bucketId);
+ mBaseUri = imageUri;
+ mThumbUri = thumbUri;
+ mSort = sort;
+ mBucketId = bucketId;
+
+ mContentResolver = cr;
+
+ mCursor = createCursor();
+ if (mCursor == null) {
+ Log.e(TAG, "unable to create image cursor for " + mBaseUri);
+ throw new UnsupportedOperationException();
+ }
+
+ if (VERBOSE) {
+ Log.v(TAG, "for " + mBaseUri.toString() + " got cursor " + mCursor + " with length " + (mCursor != null ? mCursor.getCount() : "-1"));
+ }
+
+ final Runnable updateRunnable = new Runnable() {
+ public void run() {
+ // handling these external updates is causing ANR problems that are unresolved.
+ // For now ignore them since there shouldn't be anyone modifying the database on the fly.
+ if (true)
+ return;
+
+ synchronized (mCursor) {
+ requery();
+ }
+ if (mListener != null)
+ mListener.onChange(ImageList.this);
+ }
+ };
+
+ mContentObserver = new ContentObserver(null) {
+ @Override
+ public boolean deliverSelfNotifications() {
+ return false;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ if (VERBOSE) Log.v(TAG, "MyContentObserver.onChange; selfChange == " + selfChange);
+ updateRunnable.run();
+ }
+ };
+
+ mDataSetObserver = new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ if (VERBOSE) Log.v(TAG, "MyDataSetObserver.onChanged");
+// updateRunnable.run();
+ }
+
+ @Override
+ public void onInvalidated() {
+ if (VERBOSE) Log.v(TAG, "MyDataSetObserver.onInvalidated: " + mCursorDeactivated);
+ }
+ };
+
+ registerObservers();
+ }
+
+ private void registerObservers() {
+ if (mIsRegistered)
+ return;
+
+ mCursor.registerContentObserver(mContentObserver);
+ mCursor.registerDataSetObserver(mDataSetObserver);
+ mIsRegistered = true;
+ }
+
+ private void unregisterObservers() {
+ if (!mIsRegistered)
+ return;
+
+ mCursor.unregisterContentObserver(mContentObserver);
+ mCursor.unregisterDataSetObserver(mDataSetObserver);
+ mIsRegistered = false;
+ }
+
+ public void deactivate() {
+ super.deactivate();
+ unregisterObservers();
+ }
+
+ protected void activateCursor() {
+ super.activateCursor();
+ registerObservers();
+ }
+
+ protected String whereClause() {
+ if (mBucketId != null) {
+ return sWhereClause + " and " + Images.Media.BUCKET_ID + " = " + mBucketId;
+ } else {
+ return sWhereClause;
+ }
+ }
+
+ protected String[] whereClauseArgs() {
+ return sAcceptableImageTypes;
+ }
+
+ protected Cursor createCursor() {
+ Cursor c =
+ Images.Media.query(
+ mContentResolver,
+ mBaseUri,
+ IMAGE_PROJECTION,
+ whereClause(),
+ whereClauseArgs(),
+ sortOrder());
+ if (VERBOSE)
+ Log.v(TAG, "createCursor got cursor with count " + (c == null ? -1 : c.getCount()));
+ return c;
+ }
+
+ protected int indexOrientation() { return INDEX_ORIENTATION; }
+ protected int indexDateTaken() { return INDEX_DATE_TAKEN; }
+ protected int indexDescription() { return -1; }
+ protected int indexMimeType() { return INDEX_MIME_TYPE; }
+ protected int indexData() { return INDEX_DATA; }
+ protected int indexId() { return INDEX_ID; }
+ protected int indexLatitude() { return -1; }
+ protected int indexLongitude() { return -1; }
+ protected int indexMiniThumbId() { return INDEX_MINI_THUMB_MAGIC; }
+
+ protected int indexPicasaWeb() { return -1; }
+ protected int indexPrivate() { return -1; }
+ protected int indexTitle() { return -1; }
+ protected int indexDisplayName() { return -1; }
+ protected int indexThumbId() { return INDEX_THUMB_ID; }
+
+ protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) {
+ return new Image(id, miniThumbId, mContentResolver, this, index, rotation);
+ }
+
+ protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfd, BitmapFactory.Options options) {
+ Bitmap b = null;
+
+ try {
+ if (pfd == null)
+ pfd = makeInputStream(uri);
+
+ if (pfd == null)
+ return null;
+
+ if (options == null)
+ options = new BitmapFactory.Options();
+
+ java.io.FileDescriptor fd = pfd.getFileDescriptor();
+ options.inSampleSize = 1;
+ if (targetWidthHeight != -1) {
+ options.inJustDecodeBounds = true;
+ long t1 = System.currentTimeMillis();
+ BitmapFactory.decodeFileDescriptor(fd, null, options);
+ long t2 = System.currentTimeMillis();
+ if (options.mCancel || options.outWidth == -1 || options.outHeight == -1) {
+ return null;
+ }
+ options.inSampleSize = computeSampleSize(options, targetWidthHeight);
+ options.inJustDecodeBounds = false;
+ }
+
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ long t1 = System.currentTimeMillis();
+ b = BitmapFactory.decodeFileDescriptor(fd, null, options);
+ long t2 = System.currentTimeMillis();
+ if (VERBOSE) {
+ Log.v(TAG, "A: got bitmap " + b + " with sampleSize " + options.inSampleSize + " took " + (t2-t1));
+ }
+ pfd.close();
+ } catch (IOException ex) {
+ if (VERBOSE) Log.v(TAG, "got io exception " + ex);
+ return null;
+ }
+ return b;
+ }
+
+ private ParcelFileDescriptor makeInputStream(Uri uri) {
+ try {
+ return mContentResolver.openFileDescriptor(uri, "r");
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
+ private String sortOrder() {
+ // add id to the end so that we don't ever get random sorting
+ // which could happen, I suppose, if the first two values were
+ // duplicated
+ String ascending = (mSort == SORT_ASCENDING ? " ASC" : " DESC");
+ return
+ Images.Media.DATE_TAKEN + ascending + "," +
+ Images.Media._ID + ascending;
+ }
+
+ }
+
+ /**
+ * Represents an ordered collection of Image objects from the DRM provider.
+ */
+ class DrmImageList extends ImageList implements IImageList {
+ private final String[] DRM_IMAGE_PROJECTION = new String[] {
+ DrmStore.Audio._ID,
+ DrmStore.Audio.DATA,
+ DrmStore.Audio.MIME_TYPE,
+ };
+
+ final int INDEX_ID = indexOf(DRM_IMAGE_PROJECTION, DrmStore.Audio._ID);
+ final int INDEX_MIME_TYPE = indexOf(DRM_IMAGE_PROJECTION, DrmStore.Audio.MIME_TYPE);
+
+ public DrmImageList(Context ctx, ContentResolver cr, Uri imageUri, int sort, String bucketId) {
+ super(ctx, cr, imageUri, null, sort, bucketId);
+ }
+
+ protected Cursor createCursor() {
+ return mContentResolver.query(mBaseUri, DRM_IMAGE_PROJECTION, null, null, sortOrder());
+ }
+
+ @Override
+ public void checkThumbnails(ThumbCheckCallback cb) {
+ // do nothing
+ }
+
+ @Override
+ public long checkThumbnail(BaseImage existingImage, Cursor c, int i) {
+ return 0;
+ }
+
+ class DrmImage extends Image {
+ protected DrmImage(long id, ContentResolver cr, BaseImageList container, int cursorRow) {
+ super(id, 0, cr, container, cursorRow, 0);
+ }
+
+ public boolean isDrm() {
+ return true;
+ }
+
+ public boolean isReadonly() {
+ return true;
+ }
+
+ public Bitmap miniThumbBitmap() {
+ return fullSizeBitmap(MINI_THUMB_TARGET_SIZE);
+ }
+
+ public Bitmap thumbBitmap() {
+ return fullSizeBitmap(THUMBNAIL_TARGET_SIZE);
+ }
+ }
+
+ protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index) {
+ return new DrmImage(id, mContentResolver, this, index);
+ }
+
+ protected int indexOrientation() { return -1; }
+ protected int indexDateTaken() { return -1; }
+ protected int indexDescription() { return -1; }
+ protected int indexMimeType() { return -1; }
+ protected int indexId() { return -1; }
+ protected int indexLatitude() { return -1; }
+ protected int indexLongitude() { return -1; }
+ protected int indexMiniThumbId() { return -1; }
+ protected int indexPicasaWeb() { return -1; }
+ protected int indexPrivate() { return -1; }
+ protected int indexTitle() { return -1; }
+ protected int indexDisplayName() { return -1; }
+ protected int indexThumbId() { return -1; }
+
+ // TODO review this probably should be based on DATE_TAKEN same as images
+ private String sortOrder() {
+ String ascending = (mSort == SORT_ASCENDING ? " ASC" : " DESC");
+ return
+ DrmStore.Images.TITLE + ascending + "," +
+ DrmStore.Images._ID;
+ }
+ }
+
+ class ImageListUber implements IImageList {
+ private IImageList [] mSubList;
+ private int mSort;
+ private IImageList.OnChange mListener = null;
+ Handler mHandler;
+
+ // This is an array of Longs wherein each Long consists of
+ // two components. The first component indicates the number of
+ // consecutive entries that belong to a given sublist.
+ // The second component indicates which sublist we're referring
+ // to (an int which is used to index into mSubList).
+ ArrayList<Long> mSkipList = null;
+
+ int [] mSkipCounts = null;
+
+ public HashMap<String, String> getBucketIds() {
+ HashMap<String, String> hashMap = new HashMap<String, String>();
+ for (IImageList list: mSubList) {
+ hashMap.putAll(list.getBucketIds());
+ }
+ return hashMap;
+ }
+
+ public ImageListUber(IImageList [] sublist, int sort) {
+ mSubList = sublist.clone();
+ mSort = sort;
+
+ if (mListener != null) {
+ for (IImageList list: sublist) {
+ list.setOnChangeListener(new OnChange() {
+ public void onChange(IImageList list) {
+ if (mListener != null) {
+ mListener.onChange(ImageListUber.this);
+ }
+ }
+ }, mHandler);
+ }
+ }
+ }
+
+ public void checkThumbnails(ThumbCheckCallback cb) {
+ // TODO this isn't quite right because we need to get the
+ // total from each sub item and provide that in the callback
+ final IImageList sublist[] = mSubList;
+ final int length = sublist.length;
+ for (int i = 0; i < length; i++)
+ sublist[i].checkThumbnails(cb);
+ }
+
+ 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;
+ }
+
+ // mSkipCounts is used to tally the counts as we traverse
+ // the mSkipList. It's a member variable only so that
+ // we don't have to allocate each time through. Otherwise
+ // it could just as easily be a local.
+
+ public synchronized IImage getImageAt(int index) {
+ if (index < 0 || index > getCount())
+ throw new IndexOutOfBoundsException("index " + index + " out of range max is " + getCount());
+
+ // first make sure our allocations are in order
+ if (mSkipCounts == null || mSubList.length > mSkipCounts.length)
+ mSkipCounts = new int[mSubList.length];
+
+ if (mSkipList == null)
+ mSkipList = new ArrayList<Long>();
+
+ // zero out the mSkipCounts since that's only used for the
+ // duration of the function call
+ for (int i = 0; i < mSubList.length; i++)
+ mSkipCounts[i] = 0;
+
+ // a counter of how many images we've skipped in
+ // trying to get to index. alternatively we could
+ // have decremented index but, alas, I liked this
+ // way more.
+ int skipCount = 0;
+
+ // scan the existing mSkipList to see if we've computed
+ // enough to just return the answer
+ for (int i = 0; i < mSkipList.size(); i++) {
+ long v = mSkipList.get(i);
+
+ int offset = (int) (v & 0xFFFF);
+ int which = (int) (v >> 32);
+
+ if (skipCount + offset > index) {
+ int subindex = mSkipCounts[which] + (index - skipCount);
+ IImage img = mSubList[which].getImageAt(subindex);
+ return img;
+ }
+
+ skipCount += offset;
+ mSkipCounts[which] += offset;
+ }
+
+ // if we get here we haven't computed the answer for
+ // "index" yet so keep computing. This means running
+ // through the list of images and either modifying the
+ // last entry or creating a new one.
+ long count = 0;
+ while (true) {
+ long maxTimestamp = mSort == SORT_ASCENDING ? Long.MAX_VALUE : Long.MIN_VALUE;
+ int which = -1;
+ for (int i = 0; i < mSubList.length; i++) {
+ int pos = mSkipCounts[i];
+ IImageList list = mSubList[i];
+ if (pos < list.getCount()) {
+ IImage image = list.getImageAt(pos);
+ // this should never be null but sometimes the database is
+ // causing problems and it is null
+ if (image != null) {
+ long timestamp = image.getDateTaken();
+ if (mSort == SORT_ASCENDING ? (timestamp < maxTimestamp) : (timestamp > maxTimestamp)) {
+ maxTimestamp = timestamp;
+ which = i;
+ }
+ }
+ }
+ }
+
+ if (which == -1) {
+ if (VERBOSE) Log.v(TAG, "which is -1, returning null");
+ return null;
+ }
+
+ boolean done = false;
+ count = 1;
+ if (mSkipList.size() > 0) {
+ int pos = mSkipList.size() - 1;
+ long oldEntry = mSkipList.get(pos);
+ if ((oldEntry >> 32) == which) {
+ long newEntry = oldEntry + 1;
+ mSkipList.set(pos, newEntry);
+ done = true;
+ }
+ }
+ if (!done) {
+ long newEntry = ((long)which << 32) | count;
+ if (VERBOSE) {
+ Log.v(TAG, "new entry is " + Long.toHexString(newEntry));
+ }
+ mSkipList.add(newEntry);
+ }
+
+ if (skipCount++ == index) {
+ return mSubList[which].getImageAt(mSkipCounts[which]);
+ }
+ mSkipCounts[which] += 1;
+ }
+ }
+
+ public IImage getImageForUri(Uri uri) {
+ // TODO perhaps we can preflight the base of the uri
+ // against each sublist first
+ for (int i = 0; i < mSubList.length; i++) {
+ IImage img = mSubList[i].getImageForUri(uri);
+ if (img != null)
+ return img;
+ }
+ return null;
+ }
+
+ /**
+ * Modify the skip list when an image is deleted by finding
+ * the relevant entry in mSkipList and decrementing the
+ * counter. This is simple because deletion can never
+ * cause change the order of images.
+ */
+ public void modifySkipCountForDeletedImage(int index) {
+ int skipCount = 0;
+
+ for (int i = 0; i < mSkipList.size(); i++) {
+ long v = mSkipList.get(i);
+
+ int offset = (int) (v & 0xFFFF);
+ int which = (int) (v >> 32);
+
+ if (skipCount + offset > index) {
+ mSkipList.set(i, v-1);
+ break;
+ }
+
+ skipCount += offset;
+ }
+ }
+
+ public boolean removeImage(IImage image) {
+ int pos = -1;
+ while (++pos < mSubList.length) {
+ IImageList sub = mSubList[pos];
+ if (sub.removeImage(image)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void removeImageAt(int index) {
+ IImage img = getImageAt(index);
+ if (img != null) {
+ IImageList list = img.getContainer();
+ if (list != null) {
+ list.removeImage(img);
+ modifySkipCountForDeletedImage(index);
+ }
+ }
+ }
+
+ public void removeOnChangeListener(OnChange changeCallback) {
+ if (changeCallback == mListener)
+ mListener = null;
+ }
+
+ public void setOnChangeListener(OnChange changeCallback, Handler h) {
+ mListener = changeCallback;
+ mHandler = h;
+ }
+
+ }
+
+ public static abstract class SimpleBaseImage implements IImage {
+ public void commitChanges() {
+ throw new UnsupportedOperationException();
+ }
+
+ public InputStream fullSizeImageData() {
+ throw new UnsupportedOperationException();
+ }
+
+ public long fullSizeImageId() {
+ return 0;
+ }
+
+ public Uri fullSizeImageUri() {
+ throw new UnsupportedOperationException();
+ }
+
+ public IImageList getContainer() {
+ return null;
+ }
+
+ public long getDateTaken() {
+ return 0;
+ }
+
+ public String getMimeType() {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getDescription() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean getIsPrivate() {
+ throw new UnsupportedOperationException();
+ }
+
+ public double getLatitude() {
+ return 0D;
+ }
+
+ public double getLongitude() {
+ return 0D;
+ }
+
+ public String getTitle() {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getDisplayName() {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getPicasaId() {
+ return null;
+ }
+
+ public int getRow() {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getHeight() {
+ return 0;
+ }
+
+ public int getWidth() {
+ return 0;
+ }
+
+ public boolean hasLatLong() {
+ return false;
+ }
+
+ public boolean isReadonly() {
+ return true;
+ }
+
+ public boolean isDrm() {
+ return false;
+ }
+
+ public void onRemove() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean rotateImageBy(int degrees) {
+ return false;
+ }
+
+ public void setDescription(String description) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setIsPrivate(boolean isPrivate) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setName(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setPicasaId(long id) {
+ }
+
+ public void setPicasaId(String id) {
+ }
+
+ public Uri thumbUri() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ class SingleImageList extends BaseImageList implements IImageList {
+ private IImage mSingleImage;
+ private ContentResolver mContentResolver;
+ private Uri mUri;
+
+ class UriImage extends SimpleBaseImage {
+
+ UriImage() {
+ }
+
+ public String getDataPath() {
+ return mUri.getPath();
+ }
+
+ InputStream getInputStream() {
+ try {
+ if (mUri.getScheme().equals("file")) {
+ String path = mUri.getPath();
+ if (VERBOSE)
+ Log.v(TAG, "path is " + path);
+ return new java.io.FileInputStream(mUri.getPath());
+ } else {
+ return mContentResolver.openInputStream(mUri);
+ }
+ } catch (FileNotFoundException ex) {
+ return null;
+ }
+ }
+
+ ParcelFileDescriptor getPFD() {
+ try {
+ if (mUri.getScheme().equals("file")) {
+ String path = mUri.getPath();
+ if (VERBOSE)
+ Log.v(TAG, "path is " + path);
+ return ParcelFileDescriptor.open(new File(path), ParcelFileDescriptor.MODE_READ_ONLY);
+ } else {
+ return mContentResolver.openFileDescriptor(mUri, "r");
+ }
+ } catch (FileNotFoundException ex) {
+ return null;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.ImageManager.IImage#fullSizeBitmap(int)
+ */
+ public Bitmap fullSizeBitmap(int targetWidthHeight) {
+ try {
+ ParcelFileDescriptor pfdInput = getPFD();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+
+ if (targetWidthHeight != -1)
+ options.inSampleSize = computeSampleSize(options, targetWidthHeight);
+
+ options.inJustDecodeBounds = false;
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+
+ Bitmap b = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+ if (VERBOSE) {
+ Log.v(TAG, "B: got bitmap " + b + " with sampleSize " + options.inSampleSize);
+ }
+ pfdInput.close();
+ return b;
+ } catch (Exception ex) {
+ Log.e(TAG, "got exception decoding bitmap " + ex.toString());
+ return null;
+ }
+ }
+
+ public IGetBitmap_cancelable fullSizeBitmap_cancelable(final int targetWidthOrHeight) {
+ final class LoadBitmapCancelable extends BaseCancelable implements IGetBitmap_cancelable {
+ ParcelFileDescriptor pfdInput;
+ BitmapFactory.Options mOptions = new BitmapFactory.Options();
+ long mCancelInitiationTime;
+
+ public LoadBitmapCancelable(ParcelFileDescriptor pfd) {
+ pfdInput = pfd;
+ }
+
+ public boolean doCancelWork() {
+ if (VERBOSE)
+ Log.v(TAG, "requesting bitmap load cancel");
+ mCancelInitiationTime = System.currentTimeMillis();
+ mOptions.requestCancelDecode();
+ return true;
+ }
+
+ public Bitmap get() {
+ try {
+ Bitmap b = makeBitmap(targetWidthOrHeight, fullSizeImageUri(), pfdInput, mOptions);
+ if (b == null && mCancelInitiationTime != 0) {
+ if (VERBOSE)
+ Log.v(TAG, "cancel returned null bitmap -- took " + (System.currentTimeMillis()-mCancelInitiationTime));
+ }
+ if (VERBOSE) Log.v(TAG, "b is " + b);
+ return b;
+ } catch (Exception ex) {
+ return null;
+ } finally {
+ acknowledgeCancel();
+ }
+ }
+ }
+
+ try {
+ ParcelFileDescriptor pfdInput = getPFD();
+ if (pfdInput == null)
+ return null;
+ if (VERBOSE) Log.v(TAG, "inputStream is " + pfdInput);
+ return new LoadBitmapCancelable(pfdInput);
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ }
+ }
+
+ @Override
+ public Uri fullSizeImageUri() {
+ return mUri;
+ }
+
+ @Override
+ public InputStream fullSizeImageData() {
+ return getInputStream();
+ }
+
+ public long imageId() {
+ return 0;
+ }
+
+ public Bitmap miniThumbBitmap() {
+ return thumbBitmap();
+ }
+
+ @Override
+ public String getTitle() {
+ return mUri.toString();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return getTitle();
+ }
+
+ @Override
+ public String getDescription() {
+ return "";
+ }
+
+ public Bitmap thumbBitmap() {
+ Bitmap b = fullSizeBitmap(THUMBNAIL_TARGET_SIZE);
+ if (b != null) {
+ Matrix m = new Matrix();
+ float scale = Math.min(1F, THUMBNAIL_TARGET_SIZE / (float) b.getWidth());
+ m.setScale(scale, scale);
+ Bitmap scaledBitmap = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+ return scaledBitmap;
+ } else {
+ return null;
+ }
+ }
+
+ private BitmapFactory.Options snifBitmapOptions() {
+ ParcelFileDescriptor input = getPFD();
+ if (input == null)
+ return null;
+ try {
+ Uri uri = fullSizeImageUri();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options);
+ return options;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ @Override
+ public String getMimeType() {
+ BitmapFactory.Options options = snifBitmapOptions();
+ return (options!=null) ? options.outMimeType : "";
+ }
+
+ @Override
+ public int getHeight() {
+ BitmapFactory.Options options = snifBitmapOptions();
+ return (options!=null) ? options.outHeight : 0;
+ }
+
+ @Override
+ public int getWidth() {
+ BitmapFactory.Options options = snifBitmapOptions();
+ return (options!=null) ? options.outWidth : 0;
+ }
+ }
+
+ public SingleImageList(ContentResolver cr, Uri uri) {
+ super(null, cr, uri, ImageManager.SORT_ASCENDING, null);
+ mContentResolver = cr;
+ mUri = uri;
+ mSingleImage = new UriImage();
+ }
+
+ public HashMap<String, String> getBucketIds() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void deactivate() {
+ // nothing to do here
+ }
+
+ public int getCount() {
+ return 1;
+ }
+
+ public IImage getImageAt(int i) {
+ if (i == 0)
+ return mSingleImage;
+
+ return null;
+ }
+
+ public IImage getImageForUri(Uri uri) {
+ if (uri.equals(mUri))
+ return mSingleImage;
+ else
+ return null;
+ }
+
+ public IImage getImageWithId(long id) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected int indexOrientation() {
+ return -1;
+ }
+
+ @Override
+ protected int indexDateTaken() {
+ return -1;
+ }
+
+ @Override
+ protected int indexMimeType() {
+ return -1;
+ }
+
+ @Override
+ protected int indexDescription() {
+ return -1;
+ }
+
+ @Override
+ protected int indexId() {
+ return -1;
+ }
+
+ @Override
+ protected int indexData() {
+ return -1;
+ }
+
+ @Override
+ protected int indexLatitude() {
+ return -1;
+ }
+
+ @Override
+ protected int indexLongitude() {
+ return -1;
+ }
+
+ @Override
+ protected int indexMiniThumbId() {
+ return -1;
+ }
+
+ @Override
+ protected int indexPicasaWeb() {
+ return -1;
+ }
+
+ @Override
+ protected int indexPrivate() {
+ return -1;
+ }
+
+ @Override
+ protected int indexTitle() {
+ return -1;
+ }
+
+ @Override
+ protected int indexDisplayName() {
+ return -1;
+ }
+
+ @Override
+ protected int indexThumbId() {
+ return -1;
+ }
+
+ private InputStream makeInputStream(Uri uri) {
+ InputStream input = null;
+ try {
+ input = mContentResolver.openInputStream(uri);
+ return input;
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
+ @Override
+ protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options) {
+ Bitmap b = null;
+
+ try {
+ if (options == null)
+ options = new BitmapFactory.Options();
+ options.inSampleSize = 1;
+
+ if (targetWidthHeight != -1) {
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+
+ options.inSampleSize = computeSampleSize(options, targetWidthHeight);
+ options.inJustDecodeBounds = false;
+ }
+ b = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+ if (VERBOSE) {
+ Log.v(TAG, "C: got bitmap " + b + " with sampleSize " + options.inSampleSize);
+ }
+ pfdInput.close();
+ } catch (IOException ex) {
+ if (VERBOSE) Log.v(TAG, "got io exception " + ex);
+ return null;
+ }
+ return b;
+ }
+ }
+
+ class ThreadSafeOutputStream extends OutputStream {
+ java.io.OutputStream mDelegateStream;
+ boolean mClosed;
+
+ public ThreadSafeOutputStream(OutputStream delegate) {
+ mDelegateStream = delegate;
+ }
+
+ @Override
+ synchronized public void close() throws IOException {
+ try {
+ mClosed = true;
+ mDelegateStream.close();
+ } catch (IOException ex) {
+
+ }
+ }
+
+ @Override
+ synchronized public void flush() throws IOException {
+ super.flush();
+ }
+
+ @Override
+ public void write(byte[] b, int offset, int length) throws IOException {
+ /*
+ mDelegateStream.write(b, offset, length);
+ return;
+ */
+ while (length > 0) {
+ synchronized (this) {
+ if (mClosed)
+ return;
+
+ int writeLength = Math.min(8192, length);
+ mDelegateStream.write(b, offset, writeLength);
+ offset += writeLength;
+ length -= writeLength;
+ }
+ }
+ }
+
+ @Override
+ synchronized public void write(int oneByte) throws IOException {
+ if (mClosed)
+ return;
+ mDelegateStream.write(oneByte);
+ }
+ }
+
+ /*
+ * How much quality to use when storing the thumbnail.
+ */
+ private static ImageManager sInstance = null;
+ private static final int MINI_THUMB_TARGET_SIZE = 96;
+ private static final int THUMBNAIL_TARGET_SIZE = 320;
+
+ private static final String[] THUMB_PROJECTION = new String[] {
+ BaseColumns._ID, // 0
+ Images.Thumbnails.IMAGE_ID, // 1
+ Images.Thumbnails.WIDTH,
+ Images.Thumbnails.HEIGHT
+ };
+
+ private static Uri sStorageURI = Images.Media.EXTERNAL_CONTENT_URI;
+
+ private static Uri sThumbURI = Images.Thumbnails.EXTERNAL_CONTENT_URI;
+ /**
+ * Returns an ImageList object that contains
+ * all of the images.
+ * @param cr
+ * @param location
+ * @param includeImages
+ * @param includeVideo
+ * @return the singleton ImageList
+ */
+ static final public int SORT_ASCENDING = 1;
+
+ static final public int SORT_DESCENDING = 2;
+
+ static final public int INCLUDE_IMAGES = (1 << 0);
+ static final public int INCLUDE_DRM_IMAGES = (1 << 1);
+ static final public int INCLUDE_VIDEOS = (1 << 2);
+
+ static public DataLocation getDefaultDataLocation() {
+ return DataLocation.EXTERNAL;
+ }
+ private static int indexOf(String [] array, String s) {
+ for (int i = 0; i < array.length; i++) {
+ if (array[i].equals(s)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the singleton instance of the ImageManager.
+ * @return the ImageManager instance.
+ */
+ public static ImageManager instance() {
+ if (sInstance == null) {
+ sInstance = new ImageManager();
+ }
+ return sInstance;
+ }
+
+ static public byte [] miniThumbData(Bitmap source) {
+ if (source == null)
+ return null;
+
+ float scale;
+ if (source.getWidth() < source.getHeight()) {
+ scale = MINI_THUMB_TARGET_SIZE / (float)source.getWidth();
+ } else {
+ scale = MINI_THUMB_TARGET_SIZE / (float)source.getHeight();
+ }
+ Matrix matrix = new Matrix();
+ matrix.setScale(scale, scale);
+ Bitmap miniThumbnail = ImageLoader.transform(matrix, source,
+ MINI_THUMB_TARGET_SIZE, MINI_THUMB_TARGET_SIZE, false);
+
+ if (miniThumbnail != source) {
+ source.recycle();
+ }
+ java.io.ByteArrayOutputStream miniOutStream = new java.io.ByteArrayOutputStream();
+ miniThumbnail.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream);
+ miniThumbnail.recycle();
+
+ try {
+ miniOutStream.close();
+ byte [] data = miniOutStream.toByteArray();
+ return data;
+ } catch (java.io.IOException ex) {
+ Log.e(TAG, "got exception ex " + ex);
+ }
+ return null;
+ }
+
+ 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);
+
+ Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+ // TODO should recycle here but that needs more testing/verification
+// b.recycle();
+ b = b2;
+ }
+ return b;
+ }
+
+ public static int roundOrientation(int orientationInput) {
+ int orientation = orientationInput;
+ if (orientation == -1)
+ orientation = 0;
+
+ orientation = orientation % 360;
+ int retVal;
+ if (orientation < (0*90) + 45) {
+ retVal = 0;
+ } else if (orientation < (1*90) + 45) {
+ retVal = 90;
+ } else if (orientation < (2*90) + 45) {
+ retVal = 180;
+ } else if (orientation < (3*90) + 45) {
+ retVal = 270;
+ } else {
+ retVal = 0;
+ }
+
+ if (VERBOSE) Log.v(TAG, "map orientation " + orientationInput + " to " + retVal);
+ return retVal;
+ }
+
+ public Uri addImage(
+ final Context ctx,
+ final ContentResolver cr,
+ final String imageName,
+ final String description,
+ final long dateTaken,
+ final Location location,
+ final int orientation,
+ final String directory,
+ final String filename) {
+ ContentValues values = new ContentValues(7);
+ values.put(Images.Media.TITLE, imageName);
+ values.put(Images.Media.DISPLAY_NAME, imageName);
+ values.put(Images.Media.DESCRIPTION, description);
+ values.put(Images.Media.DATE_TAKEN, dateTaken);
+ values.put(Images.Media.MIME_TYPE, "image/jpeg");
+ values.put(Images.Media.ORIENTATION, orientation);
+
+ File parentFile = new File(directory);
+ String path = parentFile.toString().toLowerCase();
+ String name = parentFile.getName().toLowerCase();
+
+ values.put(Images.ImageColumns.BUCKET_ID, path.hashCode());
+ values.put(Images.ImageColumns.BUCKET_DISPLAY_NAME, name);
+ if (VERBOSE) Log.v(TAG, "addImage id is " + path.hashCode() + "; name " + name + "; path is " + path);
+
+ if (location != null) {
+ if (VERBOSE) {
+ Log.v(TAG, "lat long " + location.getLatitude() + " / " + location.getLongitude());
+ }
+ values.put(Images.Media.LATITUDE, location.getLatitude());
+ values.put(Images.Media.LONGITUDE, location.getLongitude());
+ }
+
+ if (directory != null && filename != null) {
+ String value = directory + "/" + filename;
+ values.put("_data", value);
+ }
+
+ long t3 = System.currentTimeMillis();
+ Uri uri = cr.insert(sStorageURI, values);
+
+ // The line above will create a filename that ends in .jpg
+ // That filename is what will be handed to gmail when a user shares a photo.
+ // Gmail gets the name of the picture attachment from the "DISPLAY_NAME" field.
+ // Extract the filename and jam it into the display name.
+ Cursor c = cr.query(
+ uri,
+ new String [] { ImageColumns._ID, Images.Media.DISPLAY_NAME, "_data" },
+ null,
+ null,
+ null);
+ if (c.moveToFirst()) {
+ String filePath = c.getString(2);
+ if (filePath != null) {
+ int pos = filePath.lastIndexOf("/");
+ if (pos >= 0) {
+ filePath = filePath.substring(pos + 1); // pick off the filename
+ c.updateString(1, filePath);
+ c.commitUpdates();
+ }
+ }
+ }
+ c.close();
+ return uri;
+ }
+
+ public IAddImage_cancelable storeImage(
+ final Uri uri,
+ final Context ctx,
+ final ContentResolver cr,
+ final int orientation,
+ final Bitmap source,
+ final byte [] jpegData) {
+ class AddImageCancelable extends BaseCancelable implements IAddImage_cancelable {
+ private IGetBoolean_cancelable mSaveImageCancelable;
+
+ public boolean doCancelWork() {
+ if (VERBOSE) {
+ Log.v(TAG, "calling AddImageCancelable.cancel() " + mSaveImageCancelable);
+ }
+
+ if (mSaveImageCancelable != null) {
+ mSaveImageCancelable.cancel();
+ }
+ return true;
+ }
+
+ public void get() {
+ if (source == null && jpegData == null) {
+ throw new IllegalArgumentException("source cannot be null");
+ }
+
+ try {
+ long t1 = System.currentTimeMillis();
+ synchronized (this) {
+ if (mCancel) {
+ throw new CanceledException();
+ }
+ }
+ long id = ContentUris.parseId(uri);
+
+ BaseImageList il = new ImageList(ctx, cr, sStorageURI, sThumbURI, SORT_ASCENDING, null);
+ ImageManager.Image image = new Image(id, 0, cr, il, il.getCount(), 0);
+ long t5 = System.currentTimeMillis();
+ Cursor c = cr.query(
+ uri,
+ new String [] { ImageColumns._ID, ImageColumns.MINI_THUMB_MAGIC, "_data" },
+ null,
+ null,
+ null);
+ c.moveToPosition(0);
+
+ synchronized (this) {
+ checkCanceled();
+ mSaveImageCancelable = image.saveImageContents(source, jpegData, orientation, true, c);
+ }
+
+ if (mSaveImageCancelable.get()) {
+ long t6 = System.currentTimeMillis();
+ if (VERBOSE) Log.v(TAG, "saveImageContents took " + (t6-t5));
+ if (VERBOSE) Log.v(TAG, "updating new picture with id " + id);
+ c.updateLong(1, id);
+ c.commitUpdates();
+ c.close();
+ long t7 = System.currentTimeMillis();
+ if (VERBOSE) Log.v(TAG, "commit updates to save mini thumb took " + (t7-t6));
+ }
+ else {
+ c.close();
+ throw new CanceledException();
+ }
+ } catch (CanceledException ex) {
+ if (VERBOSE) {
+ Log.v(TAG, "caught CanceledException");
+ }
+ if (uri != null) {
+ if (VERBOSE) {
+ Log.v(TAG, "canceled... cleaning up this uri: " + uri);
+ }
+ cr.delete(uri, null, null);
+ }
+ acknowledgeCancel();
+ }
+ }
+ }
+ return new AddImageCancelable();
+ }
+
+ static public IImageList makeImageList(Uri uri, Context ctx, int sort) {
+ ContentResolver cr = ctx.getContentResolver();
+ String uriString = (uri != null) ? uri.toString() : "";
+ // TODO we need to figure out whether we're viewing
+ // DRM images in a better way. Is there a constant
+ // for content://drm somewhere??
+ IImageList imageList;
+
+ if (uriString.startsWith("content://drm")) {
+ imageList = ImageManager.instance().allImages(
+ ctx,
+ cr,
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_DRM_IMAGES,
+ sort);
+ } else if (!uriString.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
+ && !uriString.startsWith(MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString())) {
+ imageList = ImageManager.instance().new SingleImageList(cr, uri);
+ } else {
+ String bucketId = uri.getQueryParameter("bucketId");
+ if (VERBOSE) Log.v(TAG, "bucketId is " + bucketId);
+ imageList = ImageManager.instance().allImages(
+ ctx,
+ cr,
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES,
+ sort,
+ bucketId);
+ }
+ return imageList;
+ }
+
+ public IImageList emptyImageList() {
+ return
+ new IImageList() {
+ public void checkThumbnails(com.android.camera.ImageManager.IImageList.ThumbCheckCallback cb) {
+ }
+
+ public void commitChanges() {
+ }
+
+ public void deactivate() {
+ }
+
+ public HashMap<String, String> getBucketIds() {
+ return new HashMap<String,String>();
+ }
+
+ public int getCount() {
+ return 0;
+ }
+
+ public IImage getImageAt(int i) {
+ return null;
+ }
+
+ public IImage getImageForUri(Uri uri) {
+ return null;
+ }
+
+ public boolean removeImage(IImage image) {
+ return false;
+ }
+
+ public void removeImageAt(int i) {
+ }
+
+ public void removeOnChangeListener(com.android.camera.ImageManager.IImageList.OnChange changeCallback) {
+ }
+
+ public void setOnChangeListener(com.android.camera.ImageManager.IImageList.OnChange changeCallback, Handler h) {
+ }
+
+ };
+ }
+
+ public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort) {
+ return allImages(ctx, cr, location, inclusion, sort, null, null);
+ }
+
+ public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId) {
+ return allImages(ctx, cr, location, inclusion, sort, bucketId, null);
+ }
+
+ public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId, Uri specificImageUri) {
+ if (VERBOSE) {
+ Log.v(TAG, "allImages " + location + " " + ((inclusion&INCLUDE_IMAGES)!=0) + " + v=" + ((inclusion&INCLUDE_VIDEOS)!=0));
+ }
+
+ if (cr == null) {
+ return null;
+ } else {
+ // false ==> don't require write access
+ boolean haveSdCard = hasStorage(false);
+
+ if (true) {
+ // use this code to merge videos and stills into the same list
+ ArrayList<IImageList> l = new ArrayList<IImageList>();
+
+ if (VERBOSE) {
+ Log.v(TAG, "initializing ... haveSdCard == " + haveSdCard + "; inclusion is " + String.format("%x", inclusion));
+ }
+ if (specificImageUri != null) {
+ try {
+ if (specificImageUri.getScheme().equalsIgnoreCase("content"))
+ l.add(new ImageList(ctx, cr, specificImageUri, sThumbURI, sort, bucketId));
+ else
+ l.add(new SingleImageList(cr, specificImageUri));
+ } catch (UnsupportedOperationException ex) {
+ }
+ } else {
+ if (haveSdCard && location != DataLocation.INTERNAL) {
+ if ((inclusion & INCLUDE_IMAGES) != 0) {
+ try {
+ l.add(new ImageList(ctx, cr, sStorageURI, sThumbURI, sort, bucketId));
+ } catch (UnsupportedOperationException ex) {
+ }
+ }
+ }
+ if (location == DataLocation.INTERNAL || location == DataLocation.ALL) {
+ if ((inclusion & INCLUDE_IMAGES) != 0) {
+ try {
+ l.add(new ImageList(ctx, cr, Images.Media.INTERNAL_CONTENT_URI,
+ Images.Thumbnails.INTERNAL_CONTENT_URI, sort, bucketId));
+ } catch (UnsupportedOperationException ex) {
+ }
+ }
+ if ((inclusion & INCLUDE_DRM_IMAGES) != 0) {
+ try {
+ l.add(new DrmImageList(ctx, cr, DrmStore.Images.CONTENT_URI, sort, bucketId));
+ } catch (UnsupportedOperationException ex) {
+ }
+ }
+ }
+ }
+
+ IImageList [] imageList = l.toArray(new IImageList[l.size()]);
+ return new ImageListUber(imageList, sort);
+ } else {
+ if (haveSdCard && location != DataLocation.INTERNAL) {
+ return new ImageList(ctx, cr, sStorageURI, sThumbURI, sort, bucketId);
+ } else {
+ return new ImageList(ctx, cr, Images.Media.INTERNAL_CONTENT_URI,
+ Images.Thumbnails.INTERNAL_CONTENT_URI, sort, bucketId);
+ }
+ }
+ }
+ }
+
+ // Create a temporary file to see whether a volume is really writeable. It's important not to
+ // put it in the root directory which may have a limit on the number of files.
+ static private boolean checkFsWritable() {
+ String directoryName = Environment.getExternalStorageDirectory().toString() + "/dcim";
+ File directory = new File(directoryName);
+ if (!directory.isDirectory()) {
+ if (!directory.mkdirs()) {
+ return false;
+ }
+ }
+ File f = new File(directoryName, ".probe");
+ try {
+ // Remove stale file if any
+ if (f.exists()) {
+ f.delete();
+ }
+ if (!f.createNewFile())
+ return false;
+ f.delete();
+ return true;
+ } catch (IOException ex) {
+ return false;
+ }
+ }
+
+ static public boolean hasStorage() {
+ return hasStorage(true);
+ }
+
+ static public boolean hasStorage(boolean requireWriteAccess) {
+ String state = Environment.getExternalStorageState();
+ if (VERBOSE) Log.v(TAG, "state is " + state);
+ if (Environment.MEDIA_MOUNTED.equals(state)) {
+ if (requireWriteAccess) {
+ boolean writable = checkFsWritable();
+ if (VERBOSE) Log.v(TAG, "writable is " + writable);
+ return writable;
+ } else {
+ return true;
+ }
+ } else if (!requireWriteAccess && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
+ return true;
+ }
+ return false;
+ }
+
+ public static Cursor query(Context context, Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) {
+ try {
+ ContentResolver resolver = context.getContentResolver();
+ if (resolver == null) {
+ return null;
+ }
+ return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ }
+
+ }
+
+ public static boolean isMediaScannerScanning(Context context) {
+ boolean result = false;
+ Cursor cursor = query(context, MediaStore.getMediaScannerUri(),
+ new String [] { MediaStore.MEDIA_SCANNER_VOLUME }, null, null, null);
+ if (cursor != null) {
+ if (cursor.getCount() == 1) {
+ cursor.moveToFirst();
+ result = "external".equals(cursor.getString(0));
+ }
+ cursor.close();
+ }
+
+ if (VERBOSE)
+ Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>>>>>> isMediaScannerScanning returning " + result);
+ return result;
+ }
+}