diff options
Diffstat (limited to 'src/com/android/camera/gallery')
20 files changed, 4373 insertions, 0 deletions
diff --git a/src/com/android/camera/gallery/BaseCancelable.java b/src/com/android/camera/gallery/BaseCancelable.java new file mode 100644 index 0000000..dbc46cc --- /dev/null +++ b/src/com/android/camera/gallery/BaseCancelable.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + + +/** + * A base class for the interface <code>ICancelable</code>. + */ +public abstract class BaseCancelable implements ICancelable { + protected boolean mCancel = false; + protected boolean mFinished = false; + + /* + * Subclasses should call acknowledgeCancel when they're finished with + * their operation. + */ + protected synchronized void acknowledgeCancel() { + mFinished = true; + if (mCancel) { + this.notify(); + } + } + + public synchronized boolean cancel() { + if (mCancel || mFinished) { + return false; + } + mCancel = true; + boolean retVal = doCancelWork(); + try { + this.wait(); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return retVal; + } + + /* + * Subclasses can call this to see if they have been canceled. + * This is the polling model. + */ + protected synchronized void checkCanceled() throws CanceledException { + if (mCancel) { + throw new CanceledException(); + } + } + + /* + * Subclasses implement this method to take whatever action + * is necessary when getting canceled. Sometimes it's not + * possible to do anything in which case the "checkCanceled" + * polling model may be used (or some combination). + */ + protected abstract boolean doCancelWork(); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/BaseImage.java b/src/com/android/camera/gallery/BaseImage.java new file mode 100644 index 0000000..a43064c --- /dev/null +++ b/src/com/android/camera/gallery/BaseImage.java @@ -0,0 +1,602 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore.Images; +import android.util.Log; + + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; + +/** + * Represents a particular image and provides access to the underlying bitmap + * and two thumbnail bitmaps as well as other information such as the id, and + * the path to the actual image data. + */ +public abstract class BaseImage implements IImage { + + private static final boolean VERBOSE = false; + private static final String TAG = "BaseImage"; + + static final int BYTES_PER_MINTHUMB = 10000; + private static final byte [] sMiniThumbData = new byte[BYTES_PER_MINTHUMB]; + + protected ContentResolver mContentResolver; + protected long mId, mMiniThumbMagic; + protected BaseImageList mContainer; + protected HashMap<String, String> mExifData; + protected int mCursorRow; + + protected BaseImage(long id, long miniThumbId, ContentResolver cr, + BaseImageList container, int cursorRow) { + mContentResolver = cr; + mId = id; + mMiniThumbMagic = miniThumbId; + mContainer = container; + mCursorRow = cursorRow; + } + + protected abstract Bitmap.CompressFormat compressionType(); + + public void commitChanges() { + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + c.commitUpdates(); + c.requery(); + } + } + } + + private class CompressImageToFile extends BaseCancelable + implements IGetBooleanCancelable { + private ThreadSafeOutputStream mOutputStream = null; + + private Bitmap mBitmap; + private Uri mUri; + private byte[] mJpegData; + + public CompressImageToFile(Bitmap bitmap, byte[] jpegData, Uri uri) { + mBitmap = bitmap; + mUri = uri; + mJpegData = jpegData; + } + + @Override + public boolean doCancelWork() { + if (mOutputStream != null) { + mOutputStream.close(); + return true; + } + return false; + } + + public boolean get() { + try { + long t1 = System.currentTimeMillis(); + OutputStream delegate = mContentResolver.openOutputStream(mUri); + synchronized (this) { + checkCanceled(); + mOutputStream = new ThreadSafeOutputStream(delegate); + } + long t2 = System.currentTimeMillis(); + if (mBitmap != null) { + mBitmap.compress(compressionType(), 75, mOutputStream); + } else { + long x1 = System.currentTimeMillis(); + mOutputStream.write(mJpegData); + long x2 = System.currentTimeMillis(); + if (VERBOSE) { + Log.v(TAG, "done writing... " + mJpegData.length + + " bytes took " + (x2 - x1)); + } + } + long t3 = System.currentTimeMillis(); + if (VERBOSE) { + Log.v(TAG, String.format( + "CompressImageToFile.get took %d (%d, %d)", + (t3 - t1), (t2 - t1), (t3 - t2))); + } + return true; + } catch (FileNotFoundException ex) { + return false; + } catch (CanceledException ex) { + return false; + } catch (IOException ex) { + return false; + } finally { + Util.closeSiliently(mOutputStream); + acknowledgeCancel(); + } + } + } + + /** + * Take a given bitmap and compress it to a file as described + * by the Uri parameter. + * + * @param bitmap the bitmap to be compressed/stored + * @param uri where to store the bitmap + * @return true if we succeeded + */ + protected IGetBooleanCancelable compressImageToFile( + Bitmap bitmap, byte [] jpegData, Uri uri) { + return new CompressImageToFile(bitmap, jpegData, uri); + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof Image)) return false; + return fullSizeImageUri().equals(((Image) other).fullSizeImageUri()); + } + + @Override + public int hashCode() { + return fullSizeImageUri().toString().hashCode(); + } + + public Bitmap fullSizeBitmap(int targetWidthHeight) { + return fullSizeBitmap(targetWidthHeight, true); + } + + protected Bitmap fullSizeBitmap( + int targetWidthHeight, boolean rotateAsNeeded) { + Uri url = mContainer.contentUri(mId); + if (VERBOSE) Log.v(TAG, "getCreateBitmap for " + url); + if (url == null) return null; + + Bitmap b = makeBitmap(targetWidthHeight, url); + if (b != null && rotateAsNeeded) { + b = Util.rotate(b, getDegreesRotated()); + } + return b; + } + + private class LoadBitmapCancelable extends BaseCancelable + implements IGetBitmapCancelable { + private ParcelFileDescriptor mPFD; + private BitmapFactory.Options mOptions = new BitmapFactory.Options(); + private long mCancelInitiationTime; + private int mTargetWidthHeight; + + public LoadBitmapCancelable( + ParcelFileDescriptor pfdInput, int targetWidthHeight) { + mPFD = pfdInput; + mTargetWidthHeight = targetWidthHeight; + } + + @Override + public boolean doCancelWork() { + if (VERBOSE) Log.v(TAG, "requesting bitmap load cancel"); + mCancelInitiationTime = System.currentTimeMillis(); + mOptions.requestCancelDecode(); + return true; + } + + public Bitmap get() { + try { + Bitmap b = makeBitmap( + mTargetWidthHeight, fullSizeImageUri(), mPFD, mOptions); + if (mCancelInitiationTime != 0 && VERBOSE) { + Log.v(TAG, "cancelation of bitmap load success==" + + (b == null ? "TRUE" : "FALSE") + " -- took " + + (System.currentTimeMillis() + - mCancelInitiationTime)); + } + if (b != null) { + b = Util.rotate(b, getDegreesRotated()); + } + return b; + } catch (RuntimeException ex) { + return null; + } catch (Error e) { + return null; + } finally { + acknowledgeCancel(); + } + } + } + + + public IGetBitmapCancelable fullSizeBitmapCancelable( + int targetWidthHeight) { + try { + ParcelFileDescriptor pfdInput = mContentResolver + .openFileDescriptor(fullSizeImageUri(), "r"); + return new LoadBitmapCancelable(pfdInput, targetWidthHeight); + } catch (FileNotFoundException ex) { + return null; + } catch (UnsupportedOperationException ex) { + return null; + } + } + + public InputStream fullSizeImageData() { + try { + InputStream input = mContentResolver.openInputStream( + fullSizeImageUri()); + return input; + } catch (IOException ex) { + return null; + } + } + + public long fullSizeImageId() { + return mId; + } + + public Uri fullSizeImageUri() { + return mContainer.contentUri(mId); + } + + public IImageList getContainer() { + return mContainer; + } + + Cursor getCursor() { + return mContainer.getCursor(); + } + + public long getDateTaken() { + if (mContainer.indexDateTaken() < 0) return 0; + Cursor c = getCursor(); + synchronized (c) { + c.moveToPosition(getRow()); + return c.getLong(mContainer.indexDateTaken()); + } + } + + protected int getDegreesRotated() { + return 0; + } + + public String getMimeType() { + if (mContainer.indexMimeType() < 0) { + Cursor c = mContentResolver.query(fullSizeImageUri(), + new String[] { "_id", Images.Media.MIME_TYPE }, + null, null, null); + try { + return c.moveToFirst() ? c.getString(1) : ""; + } finally { + c.close(); + } + } else { + String mimeType = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + mimeType = c.getString(mContainer.indexMimeType()); + } + } + return mimeType; + } + } + + public String getDescription() { + if (mContainer.indexDescription() < 0) { + Cursor c = mContentResolver.query(fullSizeImageUri(), + new String[] { "_id", Images.Media.DESCRIPTION }, + null, null, null); + try { + return c.moveToFirst() ? c.getString(1) : ""; + } finally { + c.close(); + } + } else { + String description = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + description = c.getString(mContainer.indexDescription()); + } + } + return description; + } + } + + public boolean getIsPrivate() { + if (mContainer.indexPrivate() < 0) return false; + boolean isPrivate = false; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + isPrivate = c.getInt(mContainer.indexPrivate()) != 0; + } + } + return isPrivate; + } + + public double getLatitude() { + if (mContainer.indexLatitude() < 0) return 0D; + Cursor c = getCursor(); + synchronized (c) { + c.moveToPosition(getRow()); + return c.getDouble(mContainer.indexLatitude()); + } + } + + public double getLongitude() { + if (mContainer.indexLongitude() < 0) return 0D; + Cursor c = getCursor(); + synchronized (c) { + c.moveToPosition(getRow()); + return c.getDouble(mContainer.indexLongitude()); + } + } + + public String getTitle() { + String name = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + if (mContainer.indexTitle() != -1) { + name = c.getString(mContainer.indexTitle()); + } + } + } + return name != null && name.length() > 0 ? name : String.valueOf(mId); + } + + public String getDisplayName() { + if (mContainer.indexDisplayName() < 0) { + Cursor c = mContentResolver.query(fullSizeImageUri(), + new String[] { "_id", Images.Media.DISPLAY_NAME }, + null, null, null); + try { + if (c.moveToFirst()) return c.getString(1); + } finally { + c.close(); + } + } else { + String name = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + name = c.getString(mContainer.indexDisplayName()); + } + } + if (name != null && name.length() > 0) { + return name; + } + } + return String.valueOf(mId); + } + + public String getPicasaId() { + return null; + } + + public int getRow() { + return mCursorRow; + } + + public int getWidth() { + ParcelFileDescriptor input = null; + try { + Uri uri = fullSizeImageUri(); + input = mContentResolver.openFileDescriptor(uri, "r"); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor( + input.getFileDescriptor(), null, options); + return options.outWidth; + } catch (IOException ex) { + return 0; + } finally { + Util.closeSiliently(input); + } + } + + public int getHeight() { + ParcelFileDescriptor input = null; + try { + Uri uri = fullSizeImageUri(); + input = mContentResolver.openFileDescriptor(uri, "r"); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor( + input.getFileDescriptor(), null, options); + return options.outHeight; + } catch (IOException ex) { + return 0; + } finally { + Util.closeSiliently(input); + } + } + + public boolean hasLatLong() { + if (mContainer.indexLatitude() < 0 || mContainer.indexLongitude() < 0) { + return false; + } + Cursor c = getCursor(); + synchronized (c) { + c.moveToPosition(getRow()); + return !c.isNull(mContainer.indexLatitude()) + && !c.isNull(mContainer.indexLongitude()); + } + } + + public long imageId() { + return mId; + } + + /** + * Make a bitmap from a given Uri. + * + * @param uri + */ + private Bitmap makeBitmap(int targetWidthOrHeight, Uri uri) { + ParcelFileDescriptor input = null; + try { + input = mContentResolver.openFileDescriptor(uri, "r"); + return makeBitmap(targetWidthOrHeight, uri, input, null); + } catch (IOException ex) { + return null; + } finally { + Util.closeSiliently(input); + } + } + + protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, + ParcelFileDescriptor pfdInput, BitmapFactory.Options options) { + return mContainer.makeBitmap(targetWidthHeight, uri, pfdInput, options); + } + + public Bitmap miniThumbBitmap() { + try { + long id = mId; + long dbMagic = mMiniThumbMagic; + if (dbMagic == 0 || dbMagic == id) { + dbMagic = ((BaseImageList) getContainer()) + .checkThumbnail(this, getCursor(), getRow()); + if (VERBOSE) { + Log.v(TAG, "after computing thumbnail dbMagic is " + + dbMagic); + } + } + + synchronized (sMiniThumbData) { + dbMagic = mMiniThumbMagic; + byte [] data = mContainer.getMiniThumbFromFile(id, + sMiniThumbData, dbMagic); + if (data == null) { + byte[][] createdThumbData = new byte[1][]; + try { + dbMagic = ((BaseImageList) getContainer()) + .checkThumbnail(this, getCursor(), getRow(), + createdThumbData); + } catch (IOException ex) { + // Typically IOException because the sd card is full. + // But createdThumbData may have been filled in, so + // continue on. + } + data = createdThumbData[0]; + } + if (data == null) { + data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, + dbMagic); + } + if (data == null) { + if (VERBOSE) { + Log.v(TAG, "unable to get miniThumbBitmap," + + " data is null"); + } + } + if (data != null) { + Bitmap b = BitmapFactory.decodeByteArray(data, 0, + data.length); + if (b == null && VERBOSE) { + Log.v(TAG, "couldn't decode byte array, " + + "length was " + data.length); + } + return b; + } + } + return null; + } catch (Throwable ex) { + if (VERBOSE) { + Log.e(TAG, "miniThumbBitmap got exception " + ex.toString()); + for (StackTraceElement s : ex.getStackTrace()) + Log.e(TAG, "... " + s.toString()); + } + return null; + } + } + + public void onRemove() { + mContainer.mCache.remove(mId); + } + + protected void saveMiniThumb(Bitmap source) throws IOException { + mContainer.saveMiniThumbToFile(source, fullSizeImageId(), 0); + } + + public void setDescription(String description) { + if (mContainer.indexDescription() < 0) return; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + c.updateString(mContainer.indexDescription(), description); + } + } + } + + public void setIsPrivate(boolean isPrivate) { + if (mContainer.indexPrivate() < 0) return; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + c.updateInt(mContainer.indexPrivate(), isPrivate ? 1 : 0); + } + } + } + + public void setName(String name) { + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + c.updateString(mContainer.indexTitle(), name); + } + } + } + + public void setPicasaId(String id) { + Cursor c = null; + try { + c = mContentResolver.query(fullSizeImageUri(), + new String[] { "_id", Images.Media.PICASA_ID }, + null, null, null); + if (c != null && c.moveToFirst()) { + if (VERBOSE) { + Log.v(TAG, "storing picasaid " + id + " for " + + fullSizeImageUri()); + } + c.updateString(1, id); + c.commitUpdates(); + if (VERBOSE) { + Log.v(TAG, "updated image with picasa id " + id); + } + } + } finally { + if (c != null) + c.close(); + } + } + + public Uri thumbUri() { + Uri uri = fullSizeImageUri(); + // The value for the query parameter cannot be null :-(, + // so using a dummy "1" + uri = uri.buildUpon().appendQueryParameter("thumb", "1").build(); + return uri; + } + + @Override + public String toString() { + return fullSizeImageUri().toString(); + } +} diff --git a/src/com/android/camera/gallery/BaseImageList.java b/src/com/android/camera/gallery/BaseImageList.java new file mode 100644 index 0000000..388ffe8 --- /dev/null +++ b/src/com/android/camera/gallery/BaseImageList.java @@ -0,0 +1,870 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.os.ParcelFileDescriptor; +import android.provider.BaseColumns; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.MediaColumns; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.Images.Thumbnails; +import android.util.Log; + +import com.android.camera.ExifInterface; +import com.android.camera.ImageManager; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * A collection of <code>BaseImage</code>s. + */ +public abstract class BaseImageList implements IImageList { + private static final boolean VERBOSE = false; + private static final String TAG = "BaseImageList"; + + private static final int MINI_THUMB_TARGET_SIZE = 96; + private static final int THUMBNAIL_TARGET_SIZE = 320; + private static final int MINI_THUMB_DATA_FILE_VERSION = 3; + + private static final String WHERE_CLAUSE = + "(" + Images.Media.MIME_TYPE + " in (?, ?, ?))"; + + static final String[] IMAGE_PROJECTION = new String[] { + "_id", + "_data", + ImageColumns.DATE_TAKEN, + ImageColumns.MINI_THUMB_MAGIC, + ImageColumns.ORIENTATION, + ImageColumns.MIME_TYPE}; + + static final String[] THUMB_PROJECTION = new String[] { + BaseColumns._ID, // 0 + Images.Thumbnails.IMAGE_ID, // 1 + Images.Thumbnails.WIDTH, + Images.Thumbnails.HEIGHT}; + + static final int INDEX_ID = Util.indexOf(IMAGE_PROJECTION, "_id"); + static final int INDEX_DATA = Util.indexOf(IMAGE_PROJECTION, "_data"); + static final int INDEX_MIME_TYPE = + Util.indexOf(IMAGE_PROJECTION, MediaColumns.MIME_TYPE); + static final int INDEX_DATE_TAKEN = + Util.indexOf(IMAGE_PROJECTION, ImageColumns.DATE_TAKEN); + static final int INDEX_MINI_THUMB_MAGIC = + Util.indexOf(IMAGE_PROJECTION, ImageColumns.MINI_THUMB_MAGIC); + static final int INDEX_ORIENTATION = + Util.indexOf(IMAGE_PROJECTION, ImageColumns.ORIENTATION); + static final int INDEX_THUMB_ID = + Util.indexOf(THUMB_PROJECTION, BaseColumns._ID); + static final int INDEX_THUMB_IMAGE_ID = + Util.indexOf(THUMB_PROJECTION, Images.Thumbnails.IMAGE_ID); + static final int INDEX_THUMB_WIDTH = + Util.indexOf(THUMB_PROJECTION, Images.Thumbnails.WIDTH); + static final int INDEX_THUMB_HEIGHT = + Util.indexOf(THUMB_PROJECTION, Images.Thumbnails.HEIGHT); + + protected static final String[] ACCEPTABLE_IMAGE_TYPES = + new String[] { "image/jpeg", "image/png", "image/gif" }; + protected static final String MINITHUMB_IS_NULL = "mini_thumb_magic isnull"; + + protected ContentResolver mContentResolver; + protected int mSort; + protected Uri mBaseUri; + protected Cursor mCursor; + protected IImageList.OnChange mListener = null; + protected boolean mCursorDeactivated; + protected String mBucketId; + protected Context mContext; + protected Uri mUri; + protected HashMap<Long, IImage> mCache = new HashMap<Long, IImage>(); + protected RandomAccessFile mMiniThumbData; + protected Uri mThumbUri; + + public BaseImageList(Context ctx, ContentResolver cr, Uri uri, int sort, + String bucketId) { + mContext = ctx; + mSort = sort; + mUri = uri; + mBaseUri = uri; + mBucketId = bucketId; + mContentResolver = cr; + } + + String randomAccessFilePath(int version) { + String directoryName = + Environment.getExternalStorageDirectory().toString() + + "/DCIM/.thumbnails"; + return directoryName + "/.thumbdata" + version + "-" + mUri.hashCode(); + } + + RandomAccessFile miniThumbDataFile() { + if (mMiniThumbData == null) { + String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION); + File directory = new File(new File(path).getParent()); + if (!directory.isDirectory()) { + if (!directory.mkdirs()) { + Log.e(TAG, "!!!! unable to create .thumbnails directory " + + directory.toString()); + } + } + File f = new File(path); + if (VERBOSE) Log.v(TAG, "file f is " + f.toString()); + try { + mMiniThumbData = new RandomAccessFile(f, "rw"); + } catch (IOException ex) { + // ignore exception + } + } + return mMiniThumbData; + } + + /** + * Store a given thumbnail in the database. + */ + protected Bitmap storeThumbnail(Bitmap thumb, long imageId) { + if (thumb == null) return null; + try { + Uri uri = getThumbnailUri( + imageId, thumb.getWidth(), thumb.getHeight()); + if (uri == null) { + return thumb; + } + OutputStream thumbOut = mContentResolver.openOutputStream(uri); + thumb.compress(Bitmap.CompressFormat.JPEG, 60, thumbOut); + thumbOut.close(); + return thumb; + } catch (Exception ex) { + if (VERBOSE) Log.d(TAG, "unable to store thumbnail: " + ex); + return thumb; + } + } + + /** + * Store a JPEG thumbnail from the EXIF header in the database. + */ + protected boolean storeThumbnail( + byte[] jpegThumbnail, long imageId, int width, int height) { + if (jpegThumbnail == null) return false; + + Uri uri = getThumbnailUri(imageId, width, height); + if (uri == null) { + return false; + } + try { + OutputStream thumbOut = mContentResolver.openOutputStream(uri); + thumbOut.write(jpegThumbnail); + thumbOut.close(); + return true; + } catch (FileNotFoundException ex) { + return false; + } catch (IOException ex) { + return false; + } + } + + private Uri getThumbnailUri(long imageId, int width, int height) { + // we do not store thumbnails for DRM'd images + if (mThumbUri == null) { + return null; + } + + Uri uri = null; + Cursor c = mContentResolver.query(mThumbUri, THUMB_PROJECTION, + Thumbnails.IMAGE_ID + "=?", + new String[]{String.valueOf(imageId)}, null); + try { + if (c.moveToFirst()) { + // If, for some reaosn, we already have a row with a matching + // image id, then just update that row rather than creating a + // new row. + uri = ContentUris.withAppendedId( + mThumbUri, c.getLong(indexThumbId())); + c.commitUpdates(); + } + } finally { + c.close(); + } + if (uri == null) { + ContentValues values = new ContentValues(4); + values.put(Images.Thumbnails.KIND, Images.Thumbnails.MINI_KIND); + values.put(Images.Thumbnails.IMAGE_ID, imageId); + values.put(Images.Thumbnails.HEIGHT, height); + values.put(Images.Thumbnails.WIDTH, width); + uri = mContentResolver.insert(mThumbUri, values); + } + return uri; + } + + private static final java.util.Random sRandom = + new java.util.Random(System.currentTimeMillis()); + + protected SomewhatFairLock mLock = new SomewhatFairLock(); + + private static class SomewhatFairLock { + private boolean mLocked = false; + private ArrayList<Thread> mWaiting = new ArrayList<Thread>(); + + public synchronized void lock() { + while (mLocked) { + try { + mWaiting.add(Thread.currentThread()); + wait(); + if (mWaiting.get(0) == Thread.currentThread()) { + mWaiting.remove(0); + break; + } + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + mLocked = true; + } + + public synchronized void unlock() { + mLocked = false; + notifyAll(); + } + } + + // If the photo has an EXIF thumbnail and it's big enough, extract it and + // save that JPEG as the large thumbnail without re-encoding it. We still + // have to decompress it though, in order to generate the minithumb. + private Bitmap createThumbnailFromEXIF(String filePath, long id) { + if (filePath == null) return null; + + byte [] thumbData = null; + synchronized (ImageManager.instance()) { + thumbData = (new ExifInterface(filePath)).getThumbnail(); + } + if (thumbData == null) return null; + + // Sniff the size of the EXIF thumbnail before decoding it. Photos + // from the device will pass, but images that are side loaded from + // other cameras may not. + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options); + int width = options.outWidth; + int height = options.outHeight; + if (width >= THUMBNAIL_TARGET_SIZE && height >= THUMBNAIL_TARGET_SIZE + && storeThumbnail(thumbData, id, width, height)) { + // this is used for *encoding* the minithumb, so + // we don't want to dither or convert to 565 here. + // + // Decode with a scaling factor + // to match MINI_THUMB_TARGET_SIZE closely + // which will produce much better scaling quality + // and is significantly faster. + options.inSampleSize = + Util.computeSampleSize(options, THUMBNAIL_TARGET_SIZE); + + if (VERBOSE) { + Log.v(TAG, "in createThumbnailFromExif using inSampleSize of " + + options.inSampleSize); + } + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + options.inJustDecodeBounds = false; + return BitmapFactory.decodeByteArray( + thumbData, 0, thumbData.length, options); + } + return null; + } + + // The fallback case is to decode the original photo to thumbnail size, + // then encode it as a JPEG. We return the thumbnail Bitmap in order to + // create the minithumb from it. + private Bitmap createThumbnailFromUri(Cursor c, long id) { + Uri uri = ContentUris.withAppendedId(mBaseUri, id); + Bitmap bitmap = makeBitmap(THUMBNAIL_TARGET_SIZE, uri, null, null); + if (bitmap != null) { + storeThumbnail(bitmap, id); + } else { + uri = ContentUris.withAppendedId(mBaseUri, id); + bitmap = makeBitmap(MINI_THUMB_TARGET_SIZE, uri, null, null); + } + return bitmap; + } + + // returns id + public long checkThumbnail(BaseImage existingImage, Cursor c, int i) + throws IOException { + return checkThumbnail(existingImage, c, i, null); + } + + /** + * Checks to see if a mini thumbnail exists in the cache. If not, tries to + * create it and add it to the cache. + * @param existingImage + * @param c + * @param i + * @param createdThumbnailData if this parameter is non-null, and a new + * mini-thumbnail bitmap is created, the new bitmap's data will be + * stored in createdThumbnailData[0]. Note that if the sdcard is + * full, it's possible that createdThumbnailData[0] will be set + * even if the method throws an IOException. This is actually + * useful, because it allows the caller to use the created + * thumbnail even if the sdcard is full. + * @throws IOException + */ + public long checkThumbnail(BaseImage existingImage, Cursor c, int i, + byte[][] createdThumbnailData) throws IOException { + long magic, fileMagic = 0, id; + mLock.lock(); + try { + if (existingImage == null) { + // if we don't have an Image object then get the id and magic + // from the cursor. Synchronize on the cursor object. + synchronized (c) { + if (!c.moveToPosition(i)) { + return -1; + } + magic = c.getLong(indexMiniThumbId()); + id = c.getLong(indexId()); + } + } else { + // if we have an Image object then ask them for the magic/id + magic = existingImage.mMiniThumbMagic; + id = existingImage.fullSizeImageId(); + } + + if (magic != 0) { + // check the mini thumb file for the right data. Right is + // defined as having the right magic number at the offset + // reserved for this "id". + RandomAccessFile r = miniThumbDataFile(); + if (r != null) { + synchronized (r) { + long pos = id * BaseImage.BYTES_PER_MINTHUMB; + try { + // check that we can read the following 9 bytes + // (1 for the "status" and 8 for the long) + if (r.length() >= pos + 1 + 8) { + r.seek(pos); + if (r.readByte() == 1) { + fileMagic = r.readLong(); + if (fileMagic == magic && magic != 0 + && magic != id) { + return magic; + } + } + } + } catch (IOException ex) { + Log.v(TAG, "got exception checking file magic: " + + ex); + } + } + } + if (VERBOSE) { + Log.v(TAG, "didn't verify... fileMagic: " + fileMagic + + "; magic: " + magic + "; id: " + id + "; "); + } + } + + // If we can't retrieve the thumbnail, first check if there is one + // embedded in the EXIF data. If not, or it's not big enough, + // decompress the full size image. + Bitmap bitmap = null; + String filePath = null; + synchronized (c) { + if (c.moveToPosition(i)) { + filePath = c.getString(indexData()); + } + } + if (filePath != null) { + String mimeType = c.getString(indexMimeType()); + boolean isVideo = Util.isVideoMimeType(mimeType); + if (isVideo) { + bitmap = Util.createVideoThumbnail(filePath); + } else { + bitmap = createThumbnailFromEXIF(filePath, id); + if (bitmap == null) { + bitmap = createThumbnailFromUri(c, id); + } + } + synchronized (c) { + int degrees = 0; + if (c.moveToPosition(i)) { + int column = indexOrientation(); + if (column >= 0) degrees = c.getInt(column); + } + if (degrees != 0) { + bitmap = Util.rotate(bitmap, degrees); + } + } + } + + // make a new magic number since things are out of sync + do { + magic = sRandom.nextLong(); + } while (magic == 0); + if (bitmap != null) { + byte [] data = Util.miniThumbData(bitmap); + if (createdThumbnailData != null) { + createdThumbnailData[0] = data; + } + saveMiniThumbToFile(data, id, magic); + } + + synchronized (c) { + c.moveToPosition(i); + c.updateLong(indexMiniThumbId(), magic); + c.commitUpdates(); + c.requery(); + c.moveToPosition(i); + + if (existingImage != null) { + existingImage.mMiniThumbMagic = magic; + } + return magic; + } + } finally { + mLock.unlock(); + } + } + + public void checkThumbnails(ThumbCheckCallback cb, int totalThumbnails) { + Cursor c = Images.Media.query(mContentResolver, mBaseUri, + new String[] { "_id", "mini_thumb_magic" }, + thumbnailWhereClause(), thumbnailWhereClauseArgs(), + "_id ASC"); + + int count = c.getCount(); + if (VERBOSE) { + Log.v(TAG, ">>>>>>>>>>> need to check " + c.getCount() + " rows"); + } + c.close(); + + if (!ImageManager.hasStorage()) { + if (VERBOSE) { + Log.v(TAG, "bailing from the image checker thread " + + "-- no storage"); + } + return; + } + + String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1); + File oldFile = new File(oldPath); + + if (count == 0) { + // now check that we have the right thumbs file + if (!oldFile.exists()) { + return; + } + } + + c = getCursor(); + try { + if (VERBOSE) Log.v(TAG, "checkThumbnails found " + c.getCount()); + int current = 0; + for (int i = 0; i < c.getCount(); i++) { + try { + checkThumbnail(null, c, i); + } catch (IOException ex) { + Log.e(TAG, "!!!!! failed to check thumbnail..." + + " was the sd card removed? - " + ex.getMessage()); + break; + } + if (cb != null) { + if (!cb.checking(current, totalThumbnails)) { + if (VERBOSE) { + Log.v(TAG, "got false from checking... break"); + } + break; + } + } + current += 1; + } + } finally { + if (VERBOSE) { + Log.v(TAG, "checkThumbnails existing after reaching count " + + c.getCount()); + } + try { + oldFile.delete(); + } catch (SecurityException ex) { + // ignore + } + } + } + + protected String thumbnailWhereClause() { + return MINITHUMB_IS_NULL + " and " + WHERE_CLAUSE; + } + + protected String[] thumbnailWhereClauseArgs() { + return ACCEPTABLE_IMAGE_TYPES; + } + + public void commitChanges() { + synchronized (mCursor) { + mCursor.commitUpdates(); + requery(); + } + } + protected Uri contentUri(long id) { + try { + // does our uri already have an id (single image query)? + // if so just return it + long existingId = ContentUris.parseId(mBaseUri); + if (existingId != id) Log.e(TAG, "id mismatch"); + return mBaseUri; + } catch (NumberFormatException ex) { + // otherwise tack on the id + return ContentUris.withAppendedId(mBaseUri, id); + } + } + + public void deactivate() { + mCursorDeactivated = true; + try { + mCursor.deactivate(); + } catch (IllegalStateException e) { + // IllegalStateException may be thrown if the cursor is stale. + Log.e(TAG, "Caught exception while deactivating cursor.", e); + } + if (mMiniThumbData != null) { + try { + mMiniThumbData.close(); + mMiniThumbData = null; + } catch (IOException ex) { + // ignore exception + } + } + } + + public void dump(String msg) { + int count = getCount(); + if (VERBOSE) { + Log.v(TAG, "dump ImageList (count is " + count + ") " + msg); + } + for (int i = 0; i < count; i++) { + IImage img = getImageAt(i); + if (VERBOSE) Log.v(TAG, " " + i + ": " + img); + } + if (VERBOSE) Log.v(TAG, "end of dump container"); + } + + public int getCount() { + Cursor c = getCursor(); + synchronized (c) { + try { + return c.getCount(); + } catch (RuntimeException ex) { + return 0; + } + } + } + + public boolean isEmpty() { + return getCount() == 0; + } + + protected Cursor getCursor() { + synchronized (mCursor) { + if (mCursorDeactivated) { + activateCursor(); + } + return mCursor; + } + } + + protected void activateCursor() { + requery(); + } + + public IImage getImageAt(int i) { + Cursor c = getCursor(); + synchronized (c) { + boolean moved; + try { + moved = c.moveToPosition(i); + } catch (RuntimeException ex) { + return null; + } + if (moved) { + try { + long id = c.getLong(0); + long miniThumbId = 0; + int rotation = 0; + if (indexMiniThumbId() != -1) { + miniThumbId = c.getLong(indexMiniThumbId()); + } + if (indexOrientation() != -1) { + rotation = c.getInt(indexOrientation()); + } + long timestamp = c.getLong(1); + IImage img = mCache.get(id); + if (img == null) { + img = make(id, miniThumbId, mContentResolver, this, + timestamp, i, rotation); + mCache.put(id, img); + } + return img; + } catch (RuntimeException ex) { + Log.e(TAG, "got this exception trying to create image: " + + ex); + return null; + } + } else { + Log.e(TAG, "unable to moveTo to " + i + "; count is " + + c.getCount()); + return null; + } + } + } + + public IImage getImageForUri(Uri uri) { + // TODO: make this a hash lookup + for (int i = 0; i < getCount(); i++) { + if (getImageAt(i).fullSizeImageUri().equals(uri)) { + return getImageAt(i); + } + } + return null; + } + + byte [] getMiniThumbFromFile(long id, byte [] data, long magicCheck) { + RandomAccessFile r = miniThumbDataFile(); + if (r == null) return null; + + long pos = id * BaseImage.BYTES_PER_MINTHUMB; + synchronized (r) { + try { + r.seek(pos); + if (r.readByte() == 1) { + long magic = r.readLong(); + if (magic != magicCheck) { + if (VERBOSE) { + Log.v(TAG, "for id " + id + "; magic: " + magic + + "; magicCheck: " + magicCheck + + " (fail)"); + } + return null; + } + int length = r.readInt(); + r.read(data, 0, length); + return data; + } else { + return null; + } + } catch (IOException ex) { + long fileLength; + try { + fileLength = r.length(); + } catch (IOException ex1) { + fileLength = -1; + } + if (VERBOSE) { + Log.e(TAG, "couldn't read thumbnail for " + id + "; " + + ex.toString() + "; pos is " + pos + "; length is " + + fileLength); + } + return null; + } + } + } + protected int getRowFor(IImage imageObj) { + Cursor c = getCursor(); + synchronized (c) { + int index = 0; + long targetId = imageObj.fullSizeImageId(); + if (c.moveToFirst()) { + do { + if (c.getLong(0) == targetId) { + return index; + } + index += 1; + } while (c.moveToNext()); + } + return -1; + } + } + + protected abstract int indexOrientation(); + + protected abstract int indexDateTaken(); + + protected abstract int indexDescription(); + + protected abstract int indexMimeType(); + + protected abstract int indexData(); + + protected abstract int indexId(); + + protected abstract int indexLatitude(); + + protected abstract int indexLongitude(); + + protected abstract int indexMiniThumbId(); + + protected abstract int indexPicasaWeb(); + + protected abstract int indexPrivate(); + + protected abstract int indexTitle(); + + protected abstract int indexDisplayName(); + + protected abstract int indexThumbId(); + + protected IImage make(long id, long miniThumbId, ContentResolver cr, + IImageList list, long timestamp, int index, int rotation) { + return null; + } + + protected abstract Bitmap makeBitmap( + int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, + BitmapFactory.Options options); + + public boolean removeImage(IImage image) { + Cursor c = getCursor(); + synchronized (c) { + /* + * TODO: consider putting the image in a holding area so + * we can get it back as needed + * TODO: need to delete the thumbnails as well + */ + boolean moved; + try { + moved = c.moveToPosition(image.getRow()); + } catch (RuntimeException ex) { + Log.e(TAG, "removeImage got exception " + ex.toString()); + return false; + } + if (moved) { + Uri u = image.fullSizeImageUri(); + mContentResolver.delete(u, null, null); + image.onRemove(); + requery(); + } + } + return true; + } + + public void removeImageAt(int i) { + Cursor c = getCursor(); + synchronized (c) { + /* + * TODO: consider putting the image in a holding area so + * we can get it back as needed + * TODO: need to delete the thumbnails as well + */ + dump("before delete"); + IImage image = getImageAt(i); + boolean moved; + try { + moved = c.moveToPosition(i); + } catch (RuntimeException ex) { + Log.e(TAG, "removeImageAt " + i + " get " + ex); + return; + } + if (moved) { + Uri u = image.fullSizeImageUri(); + mContentResolver.delete(u, null, null); + requery(); + image.onRemove(); + } + dump("after delete"); + } + } + + public void removeOnChangeListener(OnChange changeCallback) { + if (changeCallback == mListener) mListener = null; + } + + protected void requery() { + mCache.clear(); + mCursor.requery(); + mCursorDeactivated = false; + } + + protected void saveMiniThumbToFile(Bitmap bitmap, long id, long magic) + throws IOException { + byte[] data = Util.miniThumbData(bitmap); + saveMiniThumbToFile(data, id, magic); + } + + protected void saveMiniThumbToFile(byte[] data, long id, long magic) + throws IOException { + RandomAccessFile r = miniThumbDataFile(); + if (r == null) return; + + long pos = id * BaseImage.BYTES_PER_MINTHUMB; + long t0 = System.currentTimeMillis(); + synchronized (r) { + try { + long t1 = System.currentTimeMillis(); + long t2 = System.currentTimeMillis(); + if (data != null) { + if (data.length > BaseImage.BYTES_PER_MINTHUMB) { + if (VERBOSE) { + Log.v(TAG, "warning: " + data.length + " > " + + BaseImage.BYTES_PER_MINTHUMB); + } + return; + } + r.seek(pos); + r.writeByte(0); // we have no data in this slot + + // if magic is 0 then leave it alone + if (magic == 0) { + r.skipBytes(8); + } else { + r.writeLong(magic); + } + r.writeInt(data.length); + r.write(data); + // f.flush(); + r.seek(pos); + r.writeByte(1); // we have data in this slot + long t3 = System.currentTimeMillis(); + + if (VERBOSE) { + Log.v(TAG, "saveMiniThumbToFile took " + (t3 - t0) + + "; " + (t1 - t0) + " " + (t2 - t1) + " " + + (t3 - t2)); + } + } + } catch (IOException ex) { + Log.e(TAG, "couldn't save mini thumbnail data for " + + id + "; " + ex.toString()); + throw ex; + } + } + } + + public void setOnChangeListener(OnChange changeCallback, Handler h) { + mListener = changeCallback; + } +} diff --git a/src/com/android/camera/gallery/CanceledException.java b/src/com/android/camera/gallery/CanceledException.java new file mode 100644 index 0000000..02a6b31 --- /dev/null +++ b/src/com/android/camera/gallery/CanceledException.java @@ -0,0 +1,9 @@ +// Copyright 2009 Google Inc. All Rights Reserved. + +package com.android.camera.gallery; + +/** + * Exception which will be thrown when the task has been canceled. + */ +public class CanceledException extends Exception { +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/DrmImageList.java b/src/com/android/camera/gallery/DrmImageList.java new file mode 100644 index 0000000..bceb6ab --- /dev/null +++ b/src/com/android/camera/gallery/DrmImageList.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import com.android.camera.ImageManager; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.provider.DrmStore; + +/** + * Represents an ordered collection of Image objects from the DRM provider. + */ +public class DrmImageList extends ImageList implements IImageList { + + private static final String[] DRM_IMAGE_PROJECTION = new String[] { + DrmStore.Audio._ID, + DrmStore.Audio.DATA, + DrmStore.Audio.MIME_TYPE, + }; + + public DrmImageList(Context ctx, ContentResolver cr, Uri imageUri, + int sort, String bucketId) { + super(ctx, cr, imageUri, null, sort, bucketId); + } + + @Override + protected Cursor createCursor() { + return mContentResolver.query( + mBaseUri, DRM_IMAGE_PROJECTION, null, null, sortOrder()); + } + + @Override + public void checkThumbnails( + IImageList.ThumbCheckCallback cb, int totalCount) { + // do nothing + } + + @Override + public long checkThumbnail(BaseImage existingImage, Cursor c, int i) { + return 0; + } + + private class DrmImage extends Image { + + protected DrmImage(long id, ContentResolver cr, + BaseImageList container, int cursorRow) { + super(id, 0, cr, container, cursorRow, 0); + } + + @Override + public boolean isDrm() { + return true; + } + + @Override + public boolean isReadonly() { + return true; + } + + @Override + public Bitmap miniThumbBitmap() { + return fullSizeBitmap(ImageManager.MINI_THUMB_TARGET_SIZE); + } + + @Override + public Bitmap thumbBitmap() { + return fullSizeBitmap(ImageManager.THUMBNAIL_TARGET_SIZE); + } + + @Override + public String getDisplayName() { + return getTitle(); + } + } + + @Override + protected IImage make(long id, long miniThumbId, ContentResolver cr, + IImageList list, long timestamp, int index, int rotation) { + return new DrmImage(id, mContentResolver, this, index); + } + + @Override + protected int indexOrientation() { + return -1; + } + + @Override + protected int indexDateTaken() { + return -1; + } + + @Override + protected int indexDescription() { + return -1; + } + + @Override + protected int indexMimeType() { + return -1; + } + + @Override + protected int indexId() { + return -1; + } + + @Override + protected int indexLatitude() { + return -1; + } + + @Override + protected int indexLongitude() { + return -1; + } + + @Override + protected int indexMiniThumbId() { + return -1; + } + + @Override + protected int indexPicasaWeb() { + return -1; + } + + @Override + protected int indexPrivate() { + return -1; + } + + @Override + protected int indexTitle() { + return -1; + } + + @Override + protected int indexDisplayName() { + return -1; + } + + @Override + protected int indexThumbId() { + return -1; + } + + // TODO: Review this probably should be based on DATE_TAKEN same as images + private String sortOrder() { + String ascending = + mSort == ImageManager.SORT_ASCENDING ? " ASC" : " DESC"; + return DrmStore.Images.TITLE + ascending + "," + DrmStore.Images._ID; + } +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/IAddImageCancelable.java b/src/com/android/camera/gallery/IAddImageCancelable.java new file mode 100644 index 0000000..30a6880 --- /dev/null +++ b/src/com/android/camera/gallery/IAddImageCancelable.java @@ -0,0 +1,10 @@ +// Copyright 2009 Google Inc. All Rights Reserved. + +package com.android.camera.gallery; + +/** + * Cancelable interface for add image task. + */ +public interface IAddImageCancelable extends ICancelable { + public void get(); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/ICancelable.java b/src/com/android/camera/gallery/ICancelable.java new file mode 100644 index 0000000..ca02e79 --- /dev/null +++ b/src/com/android/camera/gallery/ICancelable.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +/** + * The interface for all the tasks that could be canceled. + */ +public interface ICancelable { + /* + * call cancel() when the unit of work in progress needs to be + * canceled. This should return true if it was possible to + * cancel and false otherwise. If this returns false the caller + * may still be able to cleanup and simulate cancelation. + */ + public boolean cancel(); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/IGetBitmapCancelable.java b/src/com/android/camera/gallery/IGetBitmapCancelable.java new file mode 100644 index 0000000..32f8b91 --- /dev/null +++ b/src/com/android/camera/gallery/IGetBitmapCancelable.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.graphics.Bitmap; + +/** + * An <code>ICancelable</code> interface which will return a bitmap. + */ +public interface IGetBitmapCancelable extends ICancelable { + // returns the bitmap or null if there was an error or we were canceled + public Bitmap get(); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/IGetBooleanCancelable.java b/src/com/android/camera/gallery/IGetBooleanCancelable.java new file mode 100644 index 0000000..c85f223 --- /dev/null +++ b/src/com/android/camera/gallery/IGetBooleanCancelable.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +/** + * An <code>ICancelable</code> interface which will return a boolean value. + */ +public interface IGetBooleanCancelable extends ICancelable { + public boolean get(); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/IImage.java b/src/com/android/camera/gallery/IImage.java new file mode 100644 index 0000000..d2cad98 --- /dev/null +++ b/src/com/android/camera/gallery/IImage.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.graphics.Bitmap; +import android.net.Uri; + +import java.io.InputStream; + +/** + * The interface of all images used in gallery. + */ +public interface IImage { + + public abstract void commitChanges(); + + /** + * Get the bitmap for the full size image. + * @return the bitmap for the full size image. + */ + public abstract Bitmap fullSizeBitmap(int targetWidthOrHeight); + + /** + * + * @return an object which can be canceled while the bitmap is loading + */ + public abstract IGetBitmapCancelable fullSizeBitmapCancelable( + int targetWidthOrHeight); + + /** + * Gets the input stream associated with a given full size image. + * This is used, for example, if one wants to email or upload + * the image. + * @return the InputStream associated with the image. + */ + public abstract InputStream fullSizeImageData(); + public abstract long fullSizeImageId(); + public abstract Uri fullSizeImageUri(); + public abstract IImageList getContainer(); + public abstract long getDateTaken(); + + /** + * Gets the description of the image. + * @return the description of the image. + */ + public abstract String getDescription(); + public abstract String getMimeType(); + public abstract int getHeight(); + + /** + * Gets the flag telling whether this video/photo is private or public. + * @return the description of the image. + */ + public abstract boolean getIsPrivate(); + + public abstract double getLatitude(); + + public abstract double getLongitude(); + + /** + * Gets the name of the image. + * @return the name of the image. + */ + public abstract String getTitle(); + + public abstract String getDisplayName(); + + public abstract String getPicasaId(); + + public abstract int getRow(); + + public abstract int getWidth(); + + public abstract boolean hasLatLong(); + + public abstract long imageId(); + + public abstract boolean isReadonly(); + + public abstract boolean isDrm(); + + public abstract Bitmap miniThumbBitmap(); + + public abstract void onRemove(); + + public abstract boolean rotateImageBy(int degrees); + + /** + * Sets the description of the image. + */ + public abstract void setDescription(String description); + + /** + * Sets whether the video/photo is private or public. + */ + public abstract void setIsPrivate(boolean isPrivate); + + /** + * Sets the name of the image. + */ + public abstract void setName(String name); + + public abstract void setPicasaId(String id); + + /** + * Get the bitmap for the medium thumbnail. + * @return the bitmap for the medium thumbnail. + */ + public abstract Bitmap thumbBitmap(); + + public abstract Uri thumbUri(); + + public abstract String getDataPath(); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/IImageList.java b/src/com/android/camera/gallery/IImageList.java new file mode 100644 index 0000000..04faccd --- /dev/null +++ b/src/com/android/camera/gallery/IImageList.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.net.Uri; +import android.os.Handler; + +import java.util.HashMap; + +/** + * The interface of all image collections used in gallery. + */ +public interface IImageList { + public HashMap<String, String> getBucketIds(); + + /** + * Notify interface when the image list has been changed. + */ + public interface OnChange { + public void onChange(IImageList list); + } + + /** + * Notify interface of how many thumbnails are processed. + */ + public interface ThumbCheckCallback { + public boolean checking(int current, int count); + } + + public abstract void checkThumbnails( + IImageList.ThumbCheckCallback cb, int totalCount); + + public abstract void commitChanges(); + + public abstract void deactivate(); + + /** + * Returns the count of image objects. + * + * @return the number of images + */ + public abstract int getCount(); + + /** + * @return true if the count of image objects is zero. + */ + + public abstract boolean isEmpty(); + + /** + * Returns the image at the ith position. + * + * @param i the position + * @return the image at the ith position + */ + public abstract IImage getImageAt(int i); + + /** + * Returns the image with a particular Uri. + * + * @param uri + * @return the image with a particular Uri. + */ + public abstract IImage getImageForUri(Uri uri); + + /** + * + * @param image + * @return true if the image was removed. + */ + public abstract boolean removeImage(IImage image); + + /** + * Removes the image at the ith position. + * @param i the position + */ + public abstract void removeImageAt(int i); + + public abstract void removeOnChangeListener( + IImageList.OnChange changeCallback); + + public abstract void setOnChangeListener( + IImageList.OnChange changeCallback, Handler h); +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/Image.java b/src/com/android/camera/gallery/Image.java new file mode 100644 index 0000000..40d780c --- /dev/null +++ b/src/com/android/camera/gallery/Image.java @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore.Images.Thumbnails; +import android.util.Log; + +import com.android.camera.ExifInterface; +import com.android.camera.ImageManager; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; + +/** + * The class for normal images in gallery. + */ +public class Image extends BaseImage implements IImage { + private static final boolean VERBOSE = false; + private static final String TAG = "BaseImage"; + + private int mRotation; + + public Image(long id, long miniThumbId, ContentResolver cr, + BaseImageList container, int cursorRow, int rotation) { + super(id, miniThumbId, cr, container, cursorRow); + mRotation = rotation; + } + + public String getDataPath() { + String path = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + int column = ((ImageList) getContainer()).indexData(); + if (column >= 0) + path = c.getString(column); + } + } + return path; + } + + @Override + protected int getDegreesRotated() { + return mRotation; + } + + protected void setDegreesRotated(int degrees) { + Cursor c = getCursor(); + mRotation = degrees; + synchronized (c) { + if (c.moveToPosition(getRow())) { + int column = ((ImageList) getContainer()).indexOrientation(); + if (column >= 0) { + c.updateInt(column, degrees); + getContainer().commitChanges(); + } + } + } + } + + @Override + protected Bitmap.CompressFormat compressionType() { + String mimeType = getMimeType(); + if ("image/png".equals(mimeType)) { + return Bitmap.CompressFormat.PNG; + } else if ("image/gif".equals(mimeType)) { + return Bitmap.CompressFormat.PNG; + } + return Bitmap.CompressFormat.JPEG; + } + + /** + * Does not replace the tag if already there. Otherwise, adds to the exif + * tags. + * + * @param tag + * @param value + */ + public void addExifTag(String tag, String value) { + if (mExifData == null) { + mExifData = new HashMap<String, String>(); + } + if (!mExifData.containsKey(tag)) { + mExifData.put(tag, value); + } else { + if (VERBOSE) { + Log.v(TAG, "addExifTag where the key already was there: " + + tag + " = " + value); + } + } + } + + /** + * Return the value of the Exif tag as an int. Returns 0 on any type of + * error. + * + * @param tag + */ + public int getExifTagInt(String tag) { + if (mExifData != null) { + String tagValue = mExifData.get(tag); + if (tagValue != null) { + return Integer.parseInt(tagValue); + } + } + return 0; + } + + public boolean isReadonly() { + String mimeType = getMimeType(); + return !"image/jpeg".equals(mimeType) && !"image/png".equals(mimeType); + } + + public boolean isDrm() { + return false; + } + + /** + * Remove tag if already there. Otherwise, does nothing. + * @param tag + */ + public void removeExifTag(String tag) { + if (mExifData == null) { + mExifData = new HashMap<String, String>(); + } + mExifData.remove(tag); + } + + /** + * Replaces the tag if already there. Otherwise, adds to the exif tags. + * @param tag + * @param value + */ + public void replaceExifTag(String tag, String value) { + if (mExifData == null) { + mExifData = new HashMap<String, String>(); + } + if (!mExifData.containsKey(tag)) { + mExifData.remove(tag); + } + mExifData.put(tag, value); + } + + private class SaveImageContentsCancelable extends BaseCancelable + implements IGetBooleanCancelable { + private Bitmap mImage; + private byte [] mJpegData; + private int mOrientation; + private Cursor mCursor; + IGetBooleanCancelable mCurrentCancelable = null; + + SaveImageContentsCancelable(Bitmap image, byte[] jpegData, + int orientation, Cursor cursor) { + mImage = image; + mJpegData = jpegData; + mOrientation = orientation; + mCursor = cursor; + } + + @Override + public boolean doCancelWork() { + synchronized (this) { + if (mCurrentCancelable != null) mCurrentCancelable.cancel(); + } + return true; + } + + public boolean get() { + try { + Bitmap thumbnail = null; + + long t1 = System.currentTimeMillis(); + Uri uri = mContainer.contentUri(mId); + synchronized (this) { + checkCanceled(); + mCurrentCancelable = + compressImageToFile(mImage, mJpegData, uri); + } + + long t2 = System.currentTimeMillis(); + if (!mCurrentCancelable.get()) return false; + + synchronized (this) { + String filePath; + synchronized (mCursor) { + mCursor.moveToPosition(0); + filePath = mCursor.getString(2); + } + // TODO: If thumbData is present and usable, we should call + // the version of storeThumbnail which takes a byte array, + // rather than re-encoding a new JPEG of the same + // dimensions. + + byte[] thumbData = null; + synchronized (ImageManager.instance()) { + thumbData = + (new ExifInterface(filePath)).getThumbnail(); + } + if (VERBOSE) { + Log.v(TAG, "for file " + filePath + " thumbData is " + + thumbData + "; length " + + (thumbData != null ? thumbData.length : -1)); + } + + if (thumbData != null) { + thumbnail = BitmapFactory.decodeByteArray( + thumbData, 0, thumbData.length); + if (VERBOSE) { + Log.v(TAG, "embedded thumbnail bitmap " + + thumbnail.getWidth() + "/" + + thumbnail.getHeight()); + } + } + if (thumbnail == null && mImage != null) { + thumbnail = mImage; + } + if (thumbnail == null && mJpegData != null) { + thumbnail = BitmapFactory.decodeByteArray( + mJpegData, 0, mJpegData.length); + } + } + + long t3 = System.currentTimeMillis(); + mContainer.storeThumbnail( + thumbnail, Image.this.fullSizeImageId()); + long t4 = System.currentTimeMillis(); + checkCanceled(); + if (VERBOSE) Log.v(TAG, "rotating by " + mOrientation); + try { + thumbnail = Util.rotate(thumbnail, mOrientation); + saveMiniThumb(thumbnail); + } catch (IOException e) { + // Ignore if unable to save thumb. + } + long t5 = System.currentTimeMillis(); + checkCanceled(); + + if (VERBOSE) { + Log.v(TAG, String.format("Timing data %d %d %d %d", + t2 - t1, t3 - t2, t4 - t3, t5 - t4)); + } + return true; + } catch (CanceledException ex) { + if (VERBOSE) Log.v(TAG, "got canceled... need to cleanup"); + return false; + } finally { + /* + * Cursor c = getCursor(); synchronized (c) { if + * (c.moveTo(getRow())) { mContainer.requery(); } } + */ + acknowledgeCancel(); + } + } + } + + public IGetBooleanCancelable saveImageContents(Bitmap image, + byte [] jpegData, int orientation, boolean newFile, Cursor cursor) { + return new SaveImageContentsCancelable( + image, jpegData, orientation, cursor); + } + + private void setExifRotation(int degrees) { + try { + Cursor c = getCursor(); + String filePath; + synchronized (c) { + filePath = c.getString(mContainer.indexData()); + } + synchronized (ImageManager.instance()) { + ExifInterface exif = new ExifInterface(filePath); + if (mExifData == null) { + mExifData = exif.getAttributes(); + } + if (degrees < 0) degrees += 360; + + int orientation = ExifInterface.ORIENTATION_NORMAL; + switch (degrees) { + case 0: + orientation = ExifInterface.ORIENTATION_NORMAL; + break; + case 90: + orientation = ExifInterface.ORIENTATION_ROTATE_90; + break; + case 180: + orientation = ExifInterface.ORIENTATION_ROTATE_180; + break; + case 270: + orientation = ExifInterface.ORIENTATION_ROTATE_270; + break; + } + + replaceExifTag(ExifInterface.TAG_ORIENTATION, + Integer.toString(orientation)); + replaceExifTag("UserComment", + "saveRotatedImage comment orientation: " + orientation); + exif.saveAttributes(mExifData); + exif.commitChanges(); + } + } catch (RuntimeException ex) { + Log.e(TAG, "unable to save exif data with new orientation " + + fullSizeImageUri()); + } + } + + /** + * Save the rotated image by updating the Exif "Orientation" tag. + * @param degrees + */ + public boolean rotateImageBy(int degrees) { + int newDegrees = getDegreesRotated() + degrees; + setExifRotation(newDegrees); + setDegreesRotated(newDegrees); + + // setting this to zero will force the call to checkCursor to generate + // fresh thumbs + mMiniThumbMagic = 0; + try { + mContainer.checkThumbnail( + this, mContainer.getCursor(), this.getRow()); + } catch (IOException e) { + // Ignore inability to store mini thumbnail. + } + return true; + } + + public Bitmap thumbBitmap() { + Bitmap bitmap = null; + if (mContainer.mThumbUri != null) { + Cursor c = mContentResolver.query( + mContainer.mThumbUri, BaseImageList.THUMB_PROJECTION, + Thumbnails.IMAGE_ID + "=?", + new String[] { String.valueOf(fullSizeImageId()) }, + null); + try { + if (c.moveToFirst()) bitmap = decodeCurrentImage(c); + } catch (RuntimeException ex) { + // sdcard removed? + return null; + } finally { + c.close(); + } + } + + if (bitmap == null) { + bitmap = fullSizeBitmap(ImageManager.THUMBNAIL_TARGET_SIZE, false); + if (VERBOSE) { + Log.v(TAG, "no thumbnail found... storing new one for " + + fullSizeImageId()); + } + bitmap = mContainer.storeThumbnail(bitmap, fullSizeImageId()); + } + + if (bitmap != null) { + bitmap = Util.rotate(bitmap, getDegreesRotated()); + } + + long elapsed = System.currentTimeMillis(); + return bitmap; + } + + private Bitmap decodeCurrentImage(Cursor c) { + Uri thumbUri = ContentUris.withAppendedId( + mContainer.mThumbUri, + c.getLong(((ImageList) mContainer).INDEX_THUMB_ID)); + ParcelFileDescriptor pfdInput; + Bitmap bitmap = null; + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + pfdInput = mContentResolver.openFileDescriptor(thumbUri, "r"); + bitmap = BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + pfdInput.close(); + } catch (FileNotFoundException ex) { + Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); + } catch (IOException ex) { + Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); + } catch (NullPointerException ex) { + // we seem to get this if the file doesn't exist anymore + Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); + } catch (OutOfMemoryError ex) { + Log.e(TAG, "failed to allocate memory for thumbnail " + + thumbUri + "; " + ex); + } + return bitmap; + } +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/ImageList.java b/src/com/android/camera/gallery/ImageList.java new file mode 100644 index 0000000..d1c5bff --- /dev/null +++ b/src/com/android/camera/gallery/ImageList.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import com.android.camera.ImageManager; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.util.Log; + +import java.io.IOException; +import java.util.HashMap; + +/** + * Represents an ordered collection of Image objects. Provides an API to add + * and remove an image. + */ +public class ImageList extends BaseImageList implements IImageList { + + private static final String TAG = "ImageList"; + private static final boolean VERBOSE = false; + + boolean mIsRegistered = false; + ContentObserver mContentObserver; + DataSetObserver mDataSetObserver; + + public HashMap<String, String> getBucketIds() { + Uri uri = mBaseUri.buildUpon() + .appendQueryParameter("distinct", "true").build(); + Cursor c = Images.Media.query( + mContentResolver, uri, + new String[] { + ImageColumns.BUCKET_DISPLAY_NAME, + ImageColumns.BUCKET_ID}, + whereClause(), whereClauseArgs(), sortOrder()); + + HashMap<String, String> hash = new HashMap<String, String>(); + if (c != null && c.moveToFirst()) { + do { + hash.put(c.getString(1), c.getString(0)); + } while (c.moveToNext()); + } + return hash; + } + /** + * ImageList constructor. + * @param cr ContentResolver + */ + public ImageList(Context ctx, ContentResolver cr, Uri imageUri, + Uri thumbUri, int sort, String bucketId) { + super(ctx, cr, imageUri, sort, bucketId); + mBaseUri = imageUri; + mThumbUri = thumbUri; + mSort = sort; + + mContentResolver = cr; + + mCursor = createCursor(); + if (mCursor == null) { + Log.e(TAG, "unable to create image cursor for " + mBaseUri); + throw new UnsupportedOperationException(); + } + + if (VERBOSE) { + Log.v(TAG, "for " + mBaseUri.toString() + " got cursor " + + mCursor + " with length " + + (mCursor != null ? mCursor.getCount() : "-1")); + } + + final Runnable updateRunnable = new Runnable() { + public void run() { + + // handling these external updates is causing ANR problems that + // are unresolved. For now ignore them since there shouldn't + // be anyone modifying the database on the fly. + if (true) return; + + synchronized (mCursor) { + requery(); + } + if (mListener != null) mListener.onChange(ImageList.this); + } + }; + + mContentObserver = new ContentObserver(null) { + @Override + public boolean deliverSelfNotifications() { + return false; + } + + @Override + public void onChange(boolean selfChange) { + if (VERBOSE) { + Log.v(TAG, "MyContentObserver.onChange; selfChange == " + + selfChange); + } + updateRunnable.run(); + } + }; + + mDataSetObserver = new DataSetObserver() { + @Override + public void onChanged() { + if (VERBOSE) Log.v(TAG, "MyDataSetObserver.onChanged"); + // handling these external updates is causing ANR problems that + // are unresolved. For now ignore them since there shouldn't + // be anyone modifying the database on the fly. + + // updateRunnable.run(); + } + + @Override + public void onInvalidated() { + if (VERBOSE) { + Log.v(TAG, "MyDataSetObserver.onInvalidated: " + + mCursorDeactivated); + } + } + }; + + registerObservers(); + } + + private void registerObservers() { + if (mIsRegistered) return; + mCursor.registerContentObserver(mContentObserver); + mCursor.registerDataSetObserver(mDataSetObserver); + mIsRegistered = true; + } + + private void unregisterObservers() { + if (!mIsRegistered) return; + mCursor.unregisterContentObserver(mContentObserver); + mCursor.unregisterDataSetObserver(mDataSetObserver); + mIsRegistered = false; + } + + @Override + public void deactivate() { + super.deactivate(); + unregisterObservers(); + } + + @Override + protected void activateCursor() { + super.activateCursor(); + registerObservers(); + } + + private static final String sWhereClause = + "(" + Images.Media.MIME_TYPE + " in (?, ?, ?))"; + + protected String whereClause() { + if (mBucketId != null) { + return sWhereClause + " and " + Images.Media.BUCKET_ID + " = '" + + mBucketId + "'"; + } else { + return sWhereClause; + } + } + + protected String[] whereClauseArgs() { + return ACCEPTABLE_IMAGE_TYPES; + } + + protected Cursor createCursor() { + Cursor c = Images.Media.query( + mContentResolver, mBaseUri, BaseImageList.IMAGE_PROJECTION, + whereClause(), whereClauseArgs(), sortOrder()); + if (VERBOSE) { + Log.v(TAG, "createCursor got cursor with count " + + (c == null ? -1 : c.getCount())); + } + return c; + } + + @Override + protected int indexOrientation() { + return INDEX_ORIENTATION; + } + + @Override + protected int indexDateTaken() { + return INDEX_DATE_TAKEN; + } + + @Override + protected int indexDescription() { + return -1; + } + + @Override + protected int indexMimeType() { + return INDEX_MIME_TYPE; + } + + @Override + protected int indexData() { + return INDEX_DATA; + } + + @Override + protected int indexId() { + return INDEX_ID; + } + + @Override + protected int indexLatitude() { + return -1; + } + + @Override + protected int indexLongitude() { + return -1; + } + + @Override + protected int indexMiniThumbId() { + return INDEX_MINI_THUMB_MAGIC; + } + + @Override + protected int indexPicasaWeb() { + return -1; + } + + @Override + protected int indexPrivate() { + return -1; + } + + @Override + protected int indexTitle() { + return -1; + } + + @Override + protected int indexDisplayName() { + return -1; + } + + @Override + protected int indexThumbId() { + return INDEX_THUMB_ID; + } + + @Override + protected IImage make(long id, long miniThumbId, ContentResolver cr, + IImageList list, long timestamp, int index, int rotation) { + return new Image(id, miniThumbId, mContentResolver, this, index, + rotation); + } + + @Override + protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, + ParcelFileDescriptor pfd, BitmapFactory.Options options) { + Bitmap b = null; + try { + if (pfd == null) pfd = makeInputStream(uri); + if (pfd == null) return null; + if (options == null) options = new BitmapFactory.Options(); + + java.io.FileDescriptor fd = pfd.getFileDescriptor(); + options.inSampleSize = 1; + if (targetWidthHeight != -1) { + options.inJustDecodeBounds = true; + long t1 = System.currentTimeMillis(); + BitmapFactory.decodeFileDescriptor(fd, null, options); + long t2 = System.currentTimeMillis(); + if (options.mCancel || options.outWidth == -1 + || options.outHeight == -1) { + return null; + } + options.inSampleSize = + Util.computeSampleSize(options, targetWidthHeight); + options.inJustDecodeBounds = false; + } + + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + long t1 = System.currentTimeMillis(); + b = BitmapFactory.decodeFileDescriptor(fd, null, options); + long t2 = System.currentTimeMillis(); + if (VERBOSE) { + Log.v(TAG, "A: got bitmap " + b + " with sampleSize " + + options.inSampleSize + " took " + (t2 - t1)); + } + } catch (OutOfMemoryError ex) { + if (VERBOSE) { + Log.v(TAG, "got oom exception " + ex); + } + return null; + } finally { + Util.closeSiliently(pfd); + } + return b; + } + + private ParcelFileDescriptor makeInputStream(Uri uri) { + try { + return mContentResolver.openFileDescriptor(uri, "r"); + } catch (IOException ex) { + return null; + } + } + + private String sortOrder() { + // add id to the end so that we don't ever get random sorting + // which could happen, I suppose, if the first two values were + // duplicated + String ascending = + mSort == ImageManager.SORT_ASCENDING ? " ASC" : " DESC"; + return Images.Media.DATE_TAKEN + ascending + "," + Images.Media._ID + + ascending; + } + +} + diff --git a/src/com/android/camera/gallery/ImageListUber.java b/src/com/android/camera/gallery/ImageListUber.java new file mode 100644 index 0000000..17306f4 --- /dev/null +++ b/src/com/android/camera/gallery/ImageListUber.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import com.android.camera.ImageManager; + +import android.net.Uri; +import android.os.Handler; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * A union of different <code>IImageList</code>. + */ +public class ImageListUber implements IImageList { + private static final boolean VERBOSE = false; + private static final String TAG = "ImageListUber"; + + private IImageList [] mSubList; + private int mSort; + private IImageList.OnChange mListener = null; + private Handler mHandler; + + // This is an array of Longs wherein each Long consists of + // two components. The first component indicates the number of + // consecutive entries that belong to a given sublist. + // The second component indicates which sublist we're referring + // to (an int which is used to index into mSubList). + private ArrayList<Long> mSkipList = null; + private int [] mSkipCounts = null; + + public HashMap<String, String> getBucketIds() { + HashMap<String, String> hashMap = new HashMap<String, String>(); + for (IImageList list : mSubList) { + hashMap.putAll(list.getBucketIds()); + } + return hashMap; + } + + public ImageListUber(IImageList [] sublist, int sort) { + mSubList = sublist.clone(); + mSort = sort; + + if (mListener != null) { + for (IImageList list : sublist) { + list.setOnChangeListener(new OnChange() { + public void onChange(IImageList list) { + if (mListener != null) { + mListener.onChange(ImageListUber.this); + } + } + }, mHandler); + } + } + } + + public void checkThumbnails(ThumbCheckCallback cb, int totalThumbnails) { + for (IImageList i : mSubList) { + int count = i.getCount(); + i.checkThumbnails(cb, totalThumbnails); + totalThumbnails -= count; + } + } + + public void commitChanges() { + final IImageList sublist[] = mSubList; + final int length = sublist.length; + for (int i = 0; i < length; i++) + sublist[i].commitChanges(); + } + + public void deactivate() { + final IImageList sublist[] = mSubList; + final int length = sublist.length; + int pos = -1; + while (++pos < length) { + IImageList sub = sublist[pos]; + sub.deactivate(); + } + } + + public int getCount() { + final IImageList sublist[] = mSubList; + final int length = sublist.length; + int count = 0; + for (int i = 0; i < length; i++) + count += sublist[i].getCount(); + return count; + } + + public boolean isEmpty() { + final IImageList sublist[] = mSubList; + final int length = sublist.length; + for (int i = 0; i < length; i++) { + if (!sublist[i].isEmpty()) { + return false; + } + } + return true; + } + + // mSkipCounts is used to tally the counts as we traverse + // the mSkipList. It's a member variable only so that + // we don't have to allocate each time through. Otherwise + // it could just as easily be a local. + + public synchronized IImage getImageAt(int index) { + if (index < 0 || index > getCount()) { + throw new IndexOutOfBoundsException( + "index " + index + " out of range max is " + getCount()); + } + + // first make sure our allocations are in order + if (mSkipCounts == null || mSubList.length > mSkipCounts.length) { + mSkipCounts = new int[mSubList.length]; + } + + if (mSkipList == null) { + mSkipList = new ArrayList<Long>(); + } + + // zero out the mSkipCounts since that's only used for the + // duration of the function call + for (int i = 0; i < mSubList.length; i++) { + mSkipCounts[i] = 0; + } + + // a counter of how many images we've skipped in + // trying to get to index. alternatively we could + // have decremented index but, alas, I liked this + // way more. + int skipCount = 0; + + // scan the existing mSkipList to see if we've computed + // enough to just return the answer + for (int i = 0; i < mSkipList.size(); i++) { + long v = mSkipList.get(i); + + int offset = (int) (v & 0xFFFF); + int which = (int) (v >> 32); + + if (skipCount + offset > index) { + int subindex = mSkipCounts[which] + (index - skipCount); + IImage img = mSubList[which].getImageAt(subindex); + return img; + } + + skipCount += offset; + mSkipCounts[which] += offset; + } + + // if we get here we haven't computed the answer for + // "index" yet so keep computing. This means running + // through the list of images and either modifying the + // last entry or creating a new one. + long count = 0; + while (true) { + long maxTimestamp = mSort == ImageManager.SORT_ASCENDING + ? Long.MAX_VALUE + : Long.MIN_VALUE; + int which = -1; + for (int i = 0; i < mSubList.length; i++) { + int pos = mSkipCounts[i]; + IImageList list = mSubList[i]; + if (pos < list.getCount()) { + IImage image = list.getImageAt(pos); + // this should never be null but sometimes the database is + // causing problems and it is null + if (image != null) { + long timestamp = image.getDateTaken(); + if (mSort == ImageManager.SORT_ASCENDING + ? (timestamp < maxTimestamp) + : (timestamp > maxTimestamp)) { + maxTimestamp = timestamp; + which = i; + } + } + } + } + + if (which == -1) { + if (VERBOSE) Log.v(TAG, "which is -1, returning null"); + return null; + } + + boolean done = false; + count = 1; + if (mSkipList.size() > 0) { + int pos = mSkipList.size() - 1; + long oldEntry = mSkipList.get(pos); + if ((oldEntry >> 32) == which) { + long newEntry = oldEntry + 1; + mSkipList.set(pos, newEntry); + done = true; + } + } + if (!done) { + long newEntry = ((long) which << 32) | count; + if (VERBOSE) { + Log.v(TAG, "new entry is " + Long.toHexString(newEntry)); + } + mSkipList.add(newEntry); + } + + if (skipCount++ == index) { + return mSubList[which].getImageAt(mSkipCounts[which]); + } + mSkipCounts[which] += 1; + } + } + + public IImage getImageForUri(Uri uri) { + // TODO: perhaps we can preflight the base of the uri + // against each sublist first + for (int i = 0; i < mSubList.length; i++) { + IImage img = mSubList[i].getImageForUri(uri); + if (img != null) return img; + } + return null; + } + + /** + * Modify the skip list when an image is deleted by finding + * the relevant entry in mSkipList and decrementing the + * counter. This is simple because deletion can never + * cause change the order of images. + */ + public void modifySkipCountForDeletedImage(int index) { + int skipCount = 0; + + for (int i = 0; i < mSkipList.size(); i++) { + long v = mSkipList.get(i); + + int offset = (int) (v & 0xFFFF); + int which = (int) (v >> 32); + + if (skipCount + offset > index) { + mSkipList.set(i, v - 1); + break; + } + + skipCount += offset; + } + } + + public boolean removeImage(IImage image) { + IImageList parent = image.getContainer(); + int pos = -1; + int baseIndex = 0; + while (++pos < mSubList.length) { + IImageList sub = mSubList[pos]; + if (sub == parent) { + if (sub.removeImage(image)) { + modifySkipCountForDeletedImage(baseIndex); + return true; + } else { + break; + } + } + baseIndex += sub.getCount(); + } + return false; + } + + public void removeImageAt(int index) { + IImage img = getImageAt(index); + if (img != null) { + IImageList list = img.getContainer(); + if (list != null) { + list.removeImage(img); + modifySkipCountForDeletedImage(index); + } + } + } + + public void removeOnChangeListener(OnChange changeCallback) { + if (changeCallback == mListener) { + mListener = null; + } + } + + public void setOnChangeListener(OnChange changeCallback, Handler h) { + mListener = changeCallback; + mHandler = h; + } + +} diff --git a/src/com/android/camera/gallery/SimpleBaseImage.java b/src/com/android/camera/gallery/SimpleBaseImage.java new file mode 100644 index 0000000..63e25d7 --- /dev/null +++ b/src/com/android/camera/gallery/SimpleBaseImage.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.net.Uri; + +import java.io.InputStream; + +/** + * A simple version of <code>BaseImage</code>. + */ +public abstract class SimpleBaseImage implements IImage { + public void commitChanges() { + throw new UnsupportedOperationException(); + } + + public InputStream fullSizeImageData() { + throw new UnsupportedOperationException(); + } + + public long fullSizeImageId() { + return 0; + } + + public Uri fullSizeImageUri() { + throw new UnsupportedOperationException(); + } + + public IImageList getContainer() { + return null; + } + + public long getDateTaken() { + return 0; + } + + public String getMimeType() { + throw new UnsupportedOperationException(); + } + + public String getDescription() { + throw new UnsupportedOperationException(); + } + + public boolean getIsPrivate() { + throw new UnsupportedOperationException(); + } + + public double getLatitude() { + return 0D; + } + + public double getLongitude() { + return 0D; + } + + public String getTitle() { + throw new UnsupportedOperationException(); + } + + public String getDisplayName() { + throw new UnsupportedOperationException(); + } + + public String getPicasaId() { + return null; + } + + public int getRow() { + throw new UnsupportedOperationException(); + } + + public int getHeight() { + return 0; + } + + public int getWidth() { + return 0; + } + + public boolean hasLatLong() { + return false; + } + + public boolean isReadonly() { + return true; + } + + public boolean isDrm() { + return false; + } + + public void onRemove() { + throw new UnsupportedOperationException(); + } + + public boolean rotateImageBy(int degrees) { + return false; + } + + public void setDescription(String description) { + throw new UnsupportedOperationException(); + } + + public void setIsPrivate(boolean isPrivate) { + throw new UnsupportedOperationException(); + } + + public void setName(String name) { + throw new UnsupportedOperationException(); + } + + public void setPicasaId(long id) { + } + + public void setPicasaId(String id) { + } + + public Uri thumbUri() { + throw new UnsupportedOperationException(); + } +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/SingleImageList.java b/src/com/android/camera/gallery/SingleImageList.java new file mode 100644 index 0000000..fa7656f --- /dev/null +++ b/src/com/android/camera/gallery/SingleImageList.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import com.android.camera.ImageManager; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.HashMap; + +/** + * An implementation of interface <code>IImageList</code> which contains only + * one image. + */ +public class SingleImageList extends BaseImageList implements IImageList { + private static final String TAG = "SingleImageList"; + private static final boolean VERBOSE = false; + private static final int THUMBNAIL_TARGET_SIZE = 320; + + private IImage mSingleImage; + + private class UriImage extends SimpleBaseImage { + + UriImage() { + } + + public String getDataPath() { + return mUri.getPath(); + } + + InputStream getInputStream() { + try { + if (mUri.getScheme().equals("file")) { + String path = mUri.getPath(); + if (VERBOSE) Log.v(TAG, "path is " + path); + return new java.io.FileInputStream(mUri.getPath()); + } else { + return mContentResolver.openInputStream(mUri); + } + } catch (FileNotFoundException ex) { + return null; + } + } + + ParcelFileDescriptor getPFD() { + try { + if (mUri.getScheme().equals("file")) { + String path = mUri.getPath(); + if (VERBOSE) Log.v(TAG, "path is " + path); + return ParcelFileDescriptor.open(new File(path), + ParcelFileDescriptor.MODE_READ_ONLY); + } else { + return mContentResolver.openFileDescriptor(mUri, "r"); + } + } catch (FileNotFoundException ex) { + return null; + } + } + + public Bitmap fullSizeBitmap(int targetWidthHeight) { + try { + ParcelFileDescriptor pfdInput = getPFD(); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + + if (targetWidthHeight != -1) { + options.inSampleSize = + Util.computeSampleSize(options, targetWidthHeight); + } + + options.inJustDecodeBounds = false; + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + + Bitmap b = BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + if (VERBOSE) { + Log.v(TAG, "B: got bitmap " + b + " with sampleSize " + + options.inSampleSize); + } + pfdInput.close(); + return b; + } catch (Exception ex) { + Log.e(TAG, "got exception decoding bitmap " + ex.toString()); + return null; + } + } + + final class LoadBitmapCancelable extends BaseCancelable + implements IGetBitmapCancelable { + ParcelFileDescriptor mPfdInput; + BitmapFactory.Options mOptions = new BitmapFactory.Options(); + long mCancelInitiationTime; + int mTargetWidthOrHeight; + + public LoadBitmapCancelable( + ParcelFileDescriptor pfd, int targetWidthOrHeight) { + mPfdInput = pfd; + mTargetWidthOrHeight = targetWidthOrHeight; + } + + @Override + public boolean doCancelWork() { + if (VERBOSE) { + Log.v(TAG, "requesting bitmap load cancel"); + } + mCancelInitiationTime = System.currentTimeMillis(); + mOptions.requestCancelDecode(); + return true; + } + + public Bitmap get() { + try { + Bitmap b = makeBitmap(mTargetWidthOrHeight, + fullSizeImageUri(), mPfdInput, mOptions); + if (b == null && mCancelInitiationTime != 0) { + if (VERBOSE) { + Log.v(TAG, "cancel returned null bitmap -- took " + + (System.currentTimeMillis() + - mCancelInitiationTime)); + } + } + if (VERBOSE) Log.v(TAG, "b is " + b); + return b; + } catch (Exception ex) { + return null; + } finally { + acknowledgeCancel(); + } + } + } + + public IGetBitmapCancelable fullSizeBitmapCancelable( + int targetWidthOrHeight) { + try { + ParcelFileDescriptor pfdInput = getPFD(); + if (pfdInput == null) return null; + if (VERBOSE) Log.v(TAG, "inputStream is " + pfdInput); + return new LoadBitmapCancelable(pfdInput, targetWidthOrHeight); + } catch (UnsupportedOperationException ex) { + return null; + } + } + + @Override + public Uri fullSizeImageUri() { + return mUri; + } + + @Override + public InputStream fullSizeImageData() { + return getInputStream(); + } + + public long imageId() { + return 0; + } + + public Bitmap miniThumbBitmap() { + return thumbBitmap(); + } + + @Override + public String getTitle() { + return mUri.toString(); + } + + @Override + public String getDisplayName() { + return getTitle(); + } + + @Override + public String getDescription() { + return ""; + } + + + public Bitmap thumbBitmap() { + Bitmap b = fullSizeBitmap(THUMBNAIL_TARGET_SIZE); + if (b != null) { + Matrix m = new Matrix(); + float scale = Math.min( + 1F, THUMBNAIL_TARGET_SIZE / (float) b.getWidth()); + m.setScale(scale, scale); + Bitmap scaledBitmap = Bitmap.createBitmap( + b, 0, 0, b.getWidth(), b.getHeight(), m, true); + return scaledBitmap; + } else { + return null; + } + } + + private BitmapFactory.Options snifBitmapOptions() { + ParcelFileDescriptor input = getPFD(); + if (input == null) return null; + try { + Uri uri = fullSizeImageUri(); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor( + input.getFileDescriptor(), null, options); + return options; + } finally { + Util.closeSiliently(input); + } + } + + @Override + public String getMimeType() { + BitmapFactory.Options options = snifBitmapOptions(); + return (options != null) ? options.outMimeType : ""; + } + + @Override + public int getHeight() { + BitmapFactory.Options options = snifBitmapOptions(); + return (options != null) ? options.outHeight : 0; + } + + @Override + public int getWidth() { + BitmapFactory.Options options = snifBitmapOptions(); + return (options != null) ? options.outWidth : 0; + } + } + + public SingleImageList(ContentResolver cr, Uri uri) { + super(null, cr, uri, ImageManager.SORT_ASCENDING, null); + mSingleImage = new UriImage(); + } + + public HashMap<String, String> getBucketIds() { + throw new UnsupportedOperationException(); + } + + @Override + public void deactivate() { + // nothing to do here + } + + @Override + public int getCount() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public IImage getImageAt(int i) { + return i == 0 ? mSingleImage : null; + } + + @Override + public IImage getImageForUri(Uri uri) { + return uri.equals(mUri) ? mSingleImage : null; + } + + public IImage getImageWithId(long id) { + throw new UnsupportedOperationException(); + } + + @Override + protected int indexOrientation() { + return -1; + } + + @Override + protected int indexDateTaken() { + return -1; + } + + @Override + protected int indexMimeType() { + return -1; + } + + @Override + protected int indexDescription() { + return -1; + } + + @Override + protected int indexId() { + return -1; + } + + @Override + protected int indexData() { + return -1; + } + + @Override + protected int indexLatitude() { + return -1; + } + + @Override + protected int indexLongitude() { + return -1; + } + + @Override + protected int indexMiniThumbId() { + return -1; + } + + @Override + protected int indexPicasaWeb() { + return -1; + } + + @Override + protected int indexPrivate() { + return -1; + } + + @Override + protected int indexTitle() { + return -1; + } + + @Override + protected int indexDisplayName() { + return -1; + } + + @Override + protected int indexThumbId() { + return -1; + } + + @Override + protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, + ParcelFileDescriptor pfdInput, BitmapFactory.Options options) { + Bitmap b = null; + try { + if (options == null) options = new BitmapFactory.Options(); + options.inSampleSize = 1; + + if (targetWidthHeight != -1) { + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + options.inSampleSize = + Util.computeSampleSize(options, targetWidthHeight); + options.inJustDecodeBounds = false; + } + b = BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + if (VERBOSE) { + Log.v(TAG, "C: got bitmap " + b + " with sampleSize " + + options.inSampleSize); + } + } catch (OutOfMemoryError ex) { + if (VERBOSE) Log.v(TAG, "got oom exception " + ex); + return null; + } finally { + Util.closeSiliently(pfdInput); + } + return b; + } +} diff --git a/src/com/android/camera/gallery/ThreadSafeOutputStream.java b/src/com/android/camera/gallery/ThreadSafeOutputStream.java new file mode 100644 index 0000000..2bd1f96 --- /dev/null +++ b/src/com/android/camera/gallery/ThreadSafeOutputStream.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A wrapper of an <code>OutputStream</code>, so that all the IO operations are + * thread safe. + */ +class ThreadSafeOutputStream extends OutputStream { + private OutputStream mDelegateStream; + boolean mClosed; + + public ThreadSafeOutputStream(OutputStream delegate) { + mDelegateStream = delegate; + } + + @Override + public synchronized void close() { + try { + mClosed = true; + mDelegateStream.close(); + } catch (IOException ex) { + //TODO: this should be thrown out. + } + } + + @Override + public synchronized void flush() throws IOException { + super.flush(); + } + + @Override + public void write(byte[] b, int offset, int length) throws IOException { + while (length > 0) { + synchronized (this) { + if (mClosed) return; + int writeLength = Math.min(8192, length); + mDelegateStream.write(b, offset, writeLength); + offset += writeLength; + length -= writeLength; + } + } + } + + @Override + public synchronized void write(int oneByte) throws IOException { + if (mClosed) return; + mDelegateStream.write(oneByte); + } +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/Util.java b/src/com/android/camera/gallery/Util.java new file mode 100644 index 0000000..04998ef --- /dev/null +++ b/src/com/android/camera/gallery/Util.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.media.MediaMetadataRetriever; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import com.android.camera.ImageLoader; +import com.android.camera.ImageManager; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; + +/** + * Collection of utility functions used in this package. + */ +public class Util { + private static final boolean VERBOSE = false; + private static final String TAG = "db.Util"; + + private Util() { + } + + // Rotates the bitmap by the specified degree. + // If a new bitmap is created, the original bitmap is recycled. + public static Bitmap rotate(Bitmap b, int degrees) { + if (degrees != 0 && b != null) { + Matrix m = new Matrix(); + m.setRotate(degrees, + (float) b.getWidth() / 2, (float) b.getHeight() / 2); + try { + Bitmap b2 = Bitmap.createBitmap( + b, 0, 0, b.getWidth(), b.getHeight(), m, true); + if (b != b2) { + b.recycle(); + b = b2; + } + } catch (OutOfMemoryError ex) { + // We have no memory to rotate. Return the original bitmap. + } + } + return b; + } + + /* + * Compute the sample size as a function of the image size and the target. + * Scale the image down so that both the width and height are just above the + * target. If this means that one of the dimension goes from above the + * target to below the target (e.g. given a width of 480 and an image width + * of 600 but sample size of 2 -- i.e. new width 300 -- bump the sample size + * down by 1. + */ + public static int computeSampleSize( + BitmapFactory.Options options, int target) { + int w = options.outWidth; + int h = options.outHeight; + + int candidateW = w / target; + int candidateH = h / target; + int candidate = Math.max(candidateW, candidateH); + + if (candidate == 0) return 1; + + if (candidate > 1) { + if ((w > target) && (w / candidate) < target) candidate -= 1; + } + + if (candidate > 1) { + if ((h > target) && (h / candidate) < target) candidate -= 1; + } + + if (VERBOSE) { + Log.v(TAG, "for w/h " + w + "/" + h + " returning " + candidate + + "(" + (w / candidate) + " / " + (h / candidate)); + } + + return candidate; + } + + /** + * Creates a centered bitmap of the desired size. Recycles the input. + * @param source + */ + public static Bitmap extractMiniThumb( + Bitmap source, int width, int height) { + return Util.extractMiniThumb(source, width, height, true); + } + + public static Bitmap extractMiniThumb( + Bitmap source, int width, int height, boolean recycle) { + if (source == null) { + return null; + } + + float scale; + if (source.getWidth() < source.getHeight()) { + scale = width / (float) source.getWidth(); + } else { + scale = height / (float) source.getHeight(); + } + Matrix matrix = new Matrix(); + matrix.setScale(scale, scale); + Bitmap miniThumbnail = ImageLoader.transform(matrix, source, + width, height, false); + + if (recycle && miniThumbnail != source) { + source.recycle(); + } + return miniThumbnail; + } + + /** + * Creates a byte[] for a given bitmap of the desired size. Recycles the + * input bitmap. + */ + public static byte[] miniThumbData(Bitmap source) { + if (source == null) return null; + + Bitmap miniThumbnail = extractMiniThumb( + source, ImageManager.MINI_THUMB_TARGET_SIZE, + ImageManager.MINI_THUMB_TARGET_SIZE); + + ByteArrayOutputStream miniOutStream = new ByteArrayOutputStream(); + miniThumbnail.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream); + miniThumbnail.recycle(); + + try { + miniOutStream.close(); + byte [] data = miniOutStream.toByteArray(); + return data; + } catch (java.io.IOException ex) { + Log.e(TAG, "got exception ex " + ex); + } + return null; + } + + /** + * @return true if the mimetype is a video mimetype. + */ + public static boolean isVideoMimeType(String mimeType) { + return mimeType.startsWith("video/"); + } + + /** + * Create a video thumbnail for a video. May return null if the video is + * corrupt. + * + * @param filePath + */ + public static Bitmap createVideoThumbnail(String filePath) { + Bitmap bitmap = null; + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY); + retriever.setDataSource(filePath); + bitmap = retriever.captureFrame(); + } catch (IllegalArgumentException ex) { + // Assume this is a corrupt video file + } catch (RuntimeException ex) { + // Assume this is a corrupt video file. + } finally { + try { + retriever.release(); + } catch (RuntimeException ex) { + // Ignore failures while cleaning up. + } + } + return bitmap; + } + + public static int indexOf(String [] array, String s) { + for (int i = 0; i < array.length; i++) { + if (array[i].equals(s)) { + return i; + } + } + return -1; + } + + public static void closeSiliently(Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (Throwable t) { + // do nothing + } + } + + public static void closeSiliently(ParcelFileDescriptor c) { + if (c == null) return; + try { + c.close(); + } catch (Throwable t) { + // do nothing + } + } + +} diff --git a/src/com/android/camera/gallery/VideoList.java b/src/com/android/camera/gallery/VideoList.java new file mode 100644 index 0000000..d1ec512 --- /dev/null +++ b/src/com/android/camera/gallery/VideoList.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import static com.android.camera.gallery.BaseImageList.MINITHUMB_IS_NULL; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.BaseColumns; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Video; +import android.provider.MediaStore.Video.VideoColumns; +import android.util.Config; +import android.util.Log; + +import com.android.camera.ImageManager; + +import java.io.IOException; +import java.util.HashMap; + +/** + * A collection of all the <code>VideoObject</code> in gallery. + */ +public class VideoList extends BaseImageList implements IImageList { + private static final String TAG = "BaseImageList"; + private static final boolean VERBOSE = false; + + private static final String[] sProjection = new String[] { + Video.Media._ID, + Video.Media.DATA, + Video.Media.DATE_TAKEN, + Video.Media.TITLE, + Video.Media.DISPLAY_NAME, + Video.Media.DESCRIPTION, + Video.Media.IS_PRIVATE, + Video.Media.TAGS, + Video.Media.CATEGORY, + Video.Media.LANGUAGE, + Video.Media.LATITUDE, + Video.Media.LONGITUDE, + Video.Media.MINI_THUMB_MAGIC, + Video.Media.MIME_TYPE}; + + static final int INDEX_ID = indexOf(Video.Media._ID); + static final int INDEX_DATA = indexOf(Video.Media.DATA); + static final int INDEX_DATE_TAKEN = indexOf(Video.Media.DATE_TAKEN); + static final int INDEX_TITLE = indexOf(Video.Media.TITLE); + static final int INDEX_DISPLAY_NAME = + indexOf(Video.Media.DISPLAY_NAME); + static final int INDEX_MIME_TYPE = indexOf(Video.Media.MIME_TYPE); + static final int INDEX_DESCRIPTION = + indexOf(Video.Media.DESCRIPTION); + static final int INDEX_PRIVATE = indexOf(Video.Media.IS_PRIVATE); + static final int INDEX_TAGS = indexOf(Video.Media.TAGS); + static final int INDEX_CATEGORY = indexOf(Video.Media.CATEGORY); + static final int INDEX_LANGUAGE = indexOf(Video.Media.LANGUAGE); + static final int INDEX_LATITUDE = indexOf(Video.Media.LATITUDE); + static final int INDEX_LONGITUDE = indexOf(Video.Media.LONGITUDE); + static final int INDEX_MINI_THUMB_MAGIC = + indexOf(Video.Media.MINI_THUMB_MAGIC); + static final int INDEX_THUMB_ID = indexOf(BaseColumns._ID); + + private static int indexOf(String field) { + return Util.indexOf(sProjection, field); + } + + public VideoList(Context ctx, ContentResolver cr, Uri uri, Uri thumbUri, + int sort, String bucketId) { + super(ctx, cr, uri, sort, bucketId); + + mCursor = createCursor(); + if (mCursor == null) { + Log.e(TAG, "unable to create video cursor for " + mBaseUri); + throw new UnsupportedOperationException(); + } + + if (Config.LOGV) { + Log.v(TAG, "for " + mUri.toString() + " got cursor " + mCursor + + " with length " + + (mCursor != null ? mCursor.getCount() : -1)); + } + + if (mCursor == null) { + throw new UnsupportedOperationException(); + } + if (mCursor != null && mCursor.moveToFirst()) { + int row = 0; + do { + long imageId = mCursor.getLong(indexId()); + long dateTaken = mCursor.getLong(indexDateTaken()); + long miniThumbId = mCursor.getLong(indexMiniThumbId()); + mCache.put(imageId, new VideoObject(imageId, miniThumbId, + mContentResolver, this, dateTaken, row++)); + } while (mCursor.moveToNext()); + } + } + + public HashMap<String, String> getBucketIds() { + Uri uri = mBaseUri.buildUpon() + .appendQueryParameter("distinct", "true").build(); + Cursor c = Images.Media.query( + mContentResolver, uri, + new String[] { + VideoColumns.BUCKET_DISPLAY_NAME, + VideoColumns.BUCKET_ID + }, + whereClause(), whereClauseArgs(), sortOrder()); + HashMap<String, String> hash = new HashMap<String, String>(); + if (c != null && c.moveToFirst()) { + do { + hash.put(c.getString(1), c.getString(0)); + } while (c.moveToNext()); + } + return hash; + } + + protected String whereClause() { + if (mBucketId != null) { + return Images.Media.BUCKET_ID + " = '" + mBucketId + "'"; + } else { + return null; + } + } + + protected String[] whereClauseArgs() { + return null; + } + + @Override + protected String thumbnailWhereClause() { + return MINITHUMB_IS_NULL; + } + + @Override + protected String[] thumbnailWhereClauseArgs() { + return null; + } + + protected Cursor createCursor() { + Cursor c = Images.Media.query( + mContentResolver, mBaseUri, sProjection, + whereClause(), whereClauseArgs(), sortOrder()); + if (VERBOSE) { + Log.v(TAG, "createCursor got cursor with count " + + (c == null ? -1 : c.getCount())); + } + return c; + } + + @Override + protected int indexOrientation() { + return -1; + } + + @Override + protected int indexDateTaken() { + return INDEX_DATE_TAKEN; + } + + @Override + protected int indexDescription() { + return INDEX_DESCRIPTION; + } + + @Override + protected int indexMimeType() { + return INDEX_MIME_TYPE; + } + + @Override + protected int indexData() { + return INDEX_DATA; + } + + @Override + protected int indexId() { + return INDEX_ID; + } + + @Override + protected int indexLatitude() { + return INDEX_LATITUDE; + } + + @Override + protected int indexLongitude() { + return INDEX_LONGITUDE; + } + + @Override + protected int indexMiniThumbId() { + return INDEX_MINI_THUMB_MAGIC; + } + + @Override + protected int indexPicasaWeb() { + return -1; + } + + @Override + protected int indexPrivate() { + return INDEX_PRIVATE; + } + + @Override + protected int indexTitle() { + return INDEX_TITLE; + } + + @Override + protected int indexDisplayName() { + return -1; + } + + @Override + protected int indexThumbId() { + return INDEX_THUMB_ID; + } + + @Override + protected IImage make(long id, long miniThumbId, ContentResolver cr, + IImageList list, long timestamp, int index, int rotation) { + return new VideoObject(id, miniThumbId, mContentResolver, this, + timestamp, index); + } + + @Override + protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, + ParcelFileDescriptor pfdInput, BitmapFactory.Options options) { + MediaPlayer mp = new MediaPlayer(); + Bitmap thumbnail = ImageManager.DEFAULT_THUMBNAIL; + try { + mp.setDataSource(mContext, uri); +// int duration = mp.getDuration(); +// int at = duration > 2000 ? 1000 : duration / 2; + int at = 1000; + thumbnail = mp.getFrameAt(at); + if (Config.LOGV) { + if (thumbnail != null) { + Log.v(TAG, "getFrameAt @ " + at + " returned " + thumbnail + + "; " + thumbnail.getWidth() + " " + + thumbnail.getHeight()); + } else { + Log.v(TAG, "getFrame @ " + at + " failed for " + uri); + } + } + } catch (IOException ex) { + // ignore + } catch (IllegalArgumentException ex) { + // ignore + } catch (SecurityException ex) { + // ignore + } finally { + mp.release(); + } + return thumbnail; + } + + private String sortOrder() { + return Video.Media.DATE_TAKEN + + (mSort == ImageManager.SORT_ASCENDING ? " ASC " : " DESC"); + } +}
\ No newline at end of file diff --git a/src/com/android/camera/gallery/VideoObject.java b/src/com/android/camera/gallery/VideoObject.java new file mode 100644 index 0000000..19a17fa --- /dev/null +++ b/src/com/android/camera/gallery/VideoObject.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.gallery; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.graphics.Bitmap; + +import com.android.camera.ImageManager; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Represents a particular video and provides access to the underlying data and + * two thumbnail bitmaps as well as other information such as the id, and the + * path to the actual video data. + */ +public class VideoObject extends BaseImage implements IImage { + + /** + * Constructor. + * + * @param id the image id of the image + * @param cr the content resolver + */ + protected VideoObject(long id, long miniThumbId, ContentResolver cr, + VideoList container, long dateTaken, int row) { + super(id, miniThumbId, cr, container, row); + } + + @Override + protected Bitmap.CompressFormat compressionType() { + return Bitmap.CompressFormat.JPEG; + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof VideoObject)) return false; + return fullSizeImageUri().equals( + ((VideoObject) other).fullSizeImageUri()); + } + + @Override + public int hashCode() { + return fullSizeImageUri().toString().hashCode(); + } + + public String getDataPath() { + String path = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + int column = ((VideoList) getContainer()).indexData(); + if (column >= 0) path = c.getString(column); + } + } + return path; + } + + @Override + public Bitmap fullSizeBitmap(int targetWidthHeight) { + return ImageManager.NO_IMAGE_BITMAP; + } + + @Override + public IGetBitmapCancelable fullSizeBitmapCancelable( + int targetWidthHeight) { + return null; + } + + @Override + public InputStream fullSizeImageData() { + try { + InputStream input = mContentResolver.openInputStream( + fullSizeImageUri()); + return input; + } catch (IOException ex) { + return null; + } + } + + @Override + public long fullSizeImageId() { + return mId; + } + + public String getCategory() { + return getStringEntry(VideoList.INDEX_CATEGORY); + } + + @Override + public int getHeight() { + return 0; + } + + public String getLanguage() { + return getStringEntry(VideoList.INDEX_LANGUAGE); + } + + @Override + public String getPicasaId() { + return null; + } + + private String getStringEntry(int entryName) { + String entry = null; + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + entry = c.getString(entryName); + } + } + return entry; + } + + public String getTags() { + return getStringEntry(VideoList.INDEX_TAGS); + } + + @Override + public int getWidth() { + return 0; + } + + @Override + public long imageId() { + return mId; + } + + public boolean isReadonly() { + return false; + } + + public boolean isDrm() { + return false; + } + + public boolean rotateImageBy(int degrees) { + return false; + } + + public void setCategory(String category) { + setStringEntry(category, VideoList.INDEX_CATEGORY); + } + + public void setLanguage(String language) { + setStringEntry(language, VideoList.INDEX_LANGUAGE); + } + + private void setStringEntry(String entry, int entryName) { + Cursor c = getCursor(); + synchronized (c) { + if (c.moveToPosition(getRow())) { + c.updateString(entryName, entry); + } + } + } + + public void setTags(String tags) { + setStringEntry(tags, VideoList.INDEX_TAGS); + } + + public Bitmap thumbBitmap() { + return fullSizeBitmap(320); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("" + mId); + return sb.toString(); + } +}
\ No newline at end of file |