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