diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | 1d4c75065966c4f6f56900e31f655bfd1b334435 (patch) | |
tree | 5d4526db3153daa63087fcb9384f8bc0659fbd18 /src/com/android/camera/ImageManager.java | |
download | LegacyCamera-1d4c75065966c4f6f56900e31f655bfd1b334435.zip LegacyCamera-1d4c75065966c4f6f56900e31f655bfd1b334435.tar.gz LegacyCamera-1d4c75065966c4f6f56900e31f655bfd1b334435.tar.bz2 |
Initial Contribution
Diffstat (limited to 'src/com/android/camera/ImageManager.java')
-rwxr-xr-x | src/com/android/camera/ImageManager.java | 3632 |
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; + } +} |