| /* |
| * 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<String, String> mExifData; |
| protected int mCursorRow; |
| |
| protected BaseImage(long id, long miniThumbId, ContentResolver cr, BaseImageList container, int cursorRow) { |
| mContentResolver = cr; |
| mId = id; |
| mMiniThumbMagic = miniThumbId; |
| mContainer = container; |
| mCursorRow = cursorRow; |
| } |
| |
| abstract Bitmap.CompressFormat compressionType(); |
| |
| public void commitChanges() { |
| Cursor c = getCursor(); |
| synchronized (c) { |
| if (c.moveToPosition(getRow())) { |
| c.commitUpdates(); |
| c.requery(); |
| } |
| } |
| } |
| |
| /** |
| * Take a given bitmap and compress it to a file as described |
| * by the Uri parameter. |
| * |
| * @param bitmap the bitmap to be compressed/stored |
| * @param uri where to store the bitmap |
| * @return true if we succeeded |
| */ |
| protected IGetBoolean_cancelable compressImageToFile( |
| final Bitmap bitmap, |
| final byte [] jpegData, |
| final Uri uri) { |
| class CompressImageToFile extends BaseCancelable implements IGetBoolean_cancelable { |
| ThreadSafeOutputStream mOutputStream = null; |
| |
| public boolean doCancelWork() { |
| if (mOutputStream != null) { |
| try { |
| mOutputStream.close(); |
| return true; |
| } catch (IOException ex) { |
| // TODO what to do here |
| } |
| } |
| return false; |
| } |
| |
| public boolean get() { |
| try { |
| long t1 = System.currentTimeMillis(); |
| OutputStream delegate = mContentResolver.openOutputStream(uri); |
| synchronized (this) { |
| checkCanceled(); |
| mOutputStream = new ThreadSafeOutputStream(delegate); |
| } |
| long t2 = System.currentTimeMillis(); |
| if (bitmap != null) { |
| bitmap.compress(compressionType(), 75, mOutputStream); |
| } else { |
| long x1 = System.currentTimeMillis(); |
| mOutputStream.write(jpegData); |
| long x2 = System.currentTimeMillis(); |
| if (VERBOSE) Log.v(TAG, "done writing... " + jpegData.length + " bytes took " + (x2-x1)); |
| } |
| long t3 = System.currentTimeMillis(); |
| if (VERBOSE) Log.v(TAG, String.format("CompressImageToFile.get took %d (%d, %d)",(t3-t1),(t2-t1),(t3-t2))); |
| return true; |
| } catch (FileNotFoundException ex) { |
| return false; |
| } catch (CanceledException ex) { |
| return false; |
| } catch (IOException ex) { |
| return false; |
| } |
| finally { |
| if (mOutputStream != null) { |
| try { |
| mOutputStream.close(); |
| } catch (IOException ex) { |
| // not much we can do here so ignore |
| } |
| } |
| acknowledgeCancel(); |
| } |
| } |
| } |
| return new CompressImageToFile(); |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (other == null) |
| return false; |
| if (!(other instanceof Image)) |
| return false; |
| |
| return fullSizeImageUri().equals(((Image)other).fullSizeImageUri()); |
| } |
| |
| public Bitmap fullSizeBitmap(int targetWidthHeight) { |
| return fullSizeBitmap(targetWidthHeight, true); |
| } |
| |
| protected Bitmap fullSizeBitmap(int targetWidthHeight, boolean rotateAsNeeded) { |
| Uri url = mContainer.contentUri(mId); |
| if (VERBOSE) Log.v(TAG, "getCreateBitmap for " + url); |
| if (url == null) |
| return null; |
| |
| Bitmap b = null; |
| if (b == null) { |
| b = makeBitmap(targetWidthHeight, url); |
| if (b != null && rotateAsNeeded) { |
| b = rotate(b, getDegreesRotated()); |
| } |
| } |
| return b; |
| } |
| |
| |
| public IGetBitmap_cancelable fullSizeBitmap_cancelable(final int targetWidthHeight) { |
| final class LoadBitmapCancelable extends BaseCancelable implements IGetBitmap_cancelable { |
| ParcelFileDescriptor mPFD; |
| BitmapFactory.Options mOptions = new BitmapFactory.Options(); |
| long mCancelInitiationTime; |
| |
| public LoadBitmapCancelable(ParcelFileDescriptor pfdInput) { |
| mPFD = pfdInput; |
| } |
| |
| public boolean doCancelWork() { |
| if (VERBOSE) |
| Log.v(TAG, "requesting bitmap load cancel"); |
| mCancelInitiationTime = System.currentTimeMillis(); |
| mOptions.requestCancelDecode(); |
| return true; |
| } |
| |
| public Bitmap get() { |
| try { |
| Bitmap b = makeBitmap(targetWidthHeight, fullSizeImageUri(), mPFD, mOptions); |
| if (mCancelInitiationTime != 0) { |
| if (VERBOSE) |
| Log.v(TAG, "cancelation of bitmap load success==" + (b == null ? "TRUE" : "FALSE") + " -- took " + (System.currentTimeMillis() - mCancelInitiationTime)); |
| } |
| if (b != null) { |
| int degrees = getDegreesRotated(); |
| if (degrees != 0) { |
| Matrix m = new Matrix(); |
| m.setRotate(degrees, (float) b.getWidth() / 2, (float) b.getHeight() / 2); |
| Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true); |
| if (b != b2) |
| b.recycle(); |
| b = b2; |
| } |
| } |
| return b; |
| } catch (Exception ex) { |
| return null; |
| } finally { |
| acknowledgeCancel(); |
| } |
| } |
| } |
| |
| try { |
| ParcelFileDescriptor pfdInput = mContentResolver.openFileDescriptor(fullSizeImageUri(), "r"); |
| return new LoadBitmapCancelable(pfdInput); |
| } catch (FileNotFoundException ex) { |
| return null; |
| } catch (UnsupportedOperationException ex) { |
| return null; |
| } |
| } |
| |
| public InputStream fullSizeImageData() { |
| try { |
| InputStream input = mContentResolver.openInputStream( |
| fullSizeImageUri()); |
| return input; |
| } catch (IOException ex) { |
| return null; |
| } |
| } |
| |
| public long fullSizeImageId() { |
| return mId; |
| } |
| |
| public Uri fullSizeImageUri() { |
| return mContainer.contentUri(mId); |
| } |
| |
| public IImageList getContainer() { |
| return mContainer; |
| } |
| |
| Cursor getCursor() { |
| return mContainer.getCursor(); |
| } |
| |
| public long getDateTaken() { |
| if (mContainer.indexDateTaken() < 0) return 0; |
| Cursor c = getCursor(); |
| synchronized (c) { |
| c.moveToPosition(getRow()); |
| return c.getLong(mContainer.indexDateTaken()); |
| } |
| } |
| |
| protected int getDegreesRotated() { |
| return 0; |
| } |
| |
| public String getMimeType() { |
| if (mContainer.indexMimeType() < 0) { |
| Cursor c = null; |
| try { |
| c = mContentResolver.query( |
| fullSizeImageUri(), |
| new String[] { "_id", Images.Media.MIME_TYPE }, |
| null, |
| null, null); |
| if (c != null && c.moveToFirst()) { |
| return c.getString(1); |
| } else { |
| return ""; |
| } |
| } finally { |
| if (c != null) |
| c.close(); |
| } |
| } else { |
| String mimeType = null; |
| Cursor c = getCursor(); |
| synchronized(c) { |
| if (c.moveToPosition(getRow())) { |
| mimeType = c.getString(mContainer.indexMimeType()); |
| } |
| } |
| return mimeType; |
| } |
| } |
| |
| /* (non-Javadoc) |
| * @see com.android.camera.IImage#getDescription() |
| */ |
| public String getDescription() { |
| if (mContainer.indexDescription() < 0) { |
| Cursor c = null; |
| try { |
| c = mContentResolver.query( |
| fullSizeImageUri(), |
| new String[] { "_id", Images.Media.DESCRIPTION }, |
| null, |
| null, null); |
| if (c != null && c.moveToFirst()) { |
| return c.getString(1); |
| } else { |
| return ""; |
| } |
| } finally { |
| if (c != null) |
| c.close(); |
| } |
| } else { |
| String description = null; |
| Cursor c = getCursor(); |
| synchronized(c) { |
| if (c.moveToPosition(getRow())) { |
| description = c.getString(mContainer.indexDescription()); |
| } |
| } |
| return description; |
| } |
| } |
| |
| /* (non-Javadoc) |
| * @see com.android.camera.IImage#getIsPrivate() |
| */ |
| public boolean getIsPrivate() { |
| if (mContainer.indexPrivate() < 0) return false; |
| boolean isPrivate = false; |
| Cursor c = getCursor(); |
| synchronized(c) { |
| if (c.moveToPosition(getRow())) { |
| isPrivate = c.getInt(mContainer.indexPrivate()) != 0; |
| } |
| } |
| return isPrivate; |
| } |
| |
| public double getLatitude() { |
| if (mContainer.indexLatitude() < 0) return 0D; |
| Cursor c = getCursor(); |
| synchronized (c) { |
| c.moveToPosition(getRow()); |
| return c.getDouble(mContainer.indexLatitude()); |
| } |
| } |
| |
| public double getLongitude() { |
| if (mContainer.indexLongitude() < 0) return 0D; |
| Cursor c = getCursor(); |
| synchronized (c) { |
| c.moveToPosition(getRow()); |
| return c.getDouble(mContainer.indexLongitude()); |
| } |
| } |
| |
| /* (non-Javadoc) |
| * @see com.android.camera.IImage#getTitle() |
| */ |
| public String getTitle() { |
| String name = null; |
| Cursor c = getCursor(); |
| synchronized(c) { |
| if (c.moveToPosition(getRow())) { |
| if (mContainer.indexTitle() != -1) { |
| name = c.getString(mContainer.indexTitle()); |
| } |
| } |
| } |
| return name != null && name.length() > 0 ? name : String.valueOf(mId); |
| } |
| |
| /* (non-Javadoc) |
| * @see com.android.camera.IImage#getDisplayName() |
| */ |
| public String getDisplayName() { |
| if (mContainer.indexDisplayName() < 0) { |
| Cursor c = null; |
| try { |
| c = mContentResolver.query( |
| fullSizeImageUri(), |
| new String[] { "_id", Images.Media.DISPLAY_NAME }, |
| null, |
| null, null); |
| if (c != null && c.moveToFirst()) { |
| return c.getString(1); |
| } |
| } finally { |
| if (c != null) |
| c.close(); |
| } |
| } else { |
| String name = null; |
| Cursor c = getCursor(); |
| synchronized(c) { |
| if (c.moveToPosition(getRow())) { |
| name = c.getString(mContainer.indexDisplayName()); |
| } |
| } |
| if (name != null && name.length() > 0) |
| return name; |
| } |
| return String.valueOf(mId); |
| } |
| |
| public String getPicasaId() { |
| /* |
| if (mContainer.indexPicasaWeb() < 0) return null; |
| Cursor c = getCursor(); |
| synchronized (c) { |
| c.moveTo(getRow()); |
| return c.getString(mContainer.indexPicasaWeb()); |
| } |
| */ |
| return null; |
| } |
| |
| public int getRow() { |
| return mCursorRow; |
| } |
| |
| public int getWidth() { |
| ParcelFileDescriptor input = null; |
| try { |
| Uri uri = fullSizeImageUri(); |
| input = mContentResolver.openFileDescriptor(uri, "r"); |
| BitmapFactory.Options options = new BitmapFactory.Options(); |
| options.inJustDecodeBounds = true; |
| BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options); |
| return options.outWidth; |
| } catch (IOException ex) { |
| return 0; |
| } finally { |
| try { |
| if (input != null) { |
| input.close(); |
| } |
| } catch (IOException ex) { |
| } |
| } |
| } |
| |
| public int getHeight() { |
| ParcelFileDescriptor input = null; |
| try { |
| Uri uri = fullSizeImageUri(); |
| input = mContentResolver.openFileDescriptor(uri, "r"); |
| BitmapFactory.Options options = new BitmapFactory.Options(); |
| options.inJustDecodeBounds = true; |
| BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options); |
| return options.outHeight; |
| } catch (IOException ex) { |
| return 0; |
| } finally { |
| try { |
| if (input != null) { |
| input.close(); |
| } |
| } catch (IOException ex) { |
| } |
| } |
| } |
| |
| public boolean hasLatLong() { |
| if (mContainer.indexLatitude() < 0 || mContainer.indexLongitude() < 0) return false; |
| Cursor c = getCursor(); |
| synchronized (c) { |
| c.moveToPosition(getRow()); |
| return !c.isNull(mContainer.indexLatitude()) && !c.isNull(mContainer.indexLongitude()); |
| } |
| } |
| |
| /* (non-Javadoc) |
| * @see com.android.camera.IImage#imageId() |
| */ |
| public long imageId() { |
| return mId; |
| } |
| |
| /** |
| * Make a bitmap from a given Uri. |
| * |
| * @param uri |
| */ |
| private Bitmap makeBitmap(int targetWidthOrHeight, Uri uri) { |
| ParcelFileDescriptor input = null; |
| try { |
| input = mContentResolver.openFileDescriptor(uri, "r"); |
| return makeBitmap(targetWidthOrHeight, uri, input, null); |
| } catch (IOException ex) { |
| return null; |
| } finally { |
| try { |
| if (input != null) { |
| input.close(); |
| } |
| } catch (IOException ex) { |
| } |
| } |
| } |
| |
| protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options) { |
| return mContainer.makeBitmap(targetWidthHeight, uri, pfdInput, options); |
| } |
| |
| /* (non-Javadoc) |
| * @see com.android.camera.IImage#thumb1() |
| */ |
| public Bitmap miniThumbBitmap() { |
| try { |
| long id = mId; |
| long dbMagic = mMiniThumbMagic; |
| if (dbMagic == 0 || dbMagic == id) { |
| dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), getRow()); |
| if (VERBOSE) Log.v(TAG, "after computing thumbnail dbMagic is " + dbMagic); |
| } |
| |
| synchronized(sMiniThumbData) { |
| dbMagic = mMiniThumbMagic; |
| byte [] data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic); |
| if (data == null) { |
| dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), getRow()); |
| data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic); |
| } |
| if (data == null) { |
| if (VERBOSE) |
| Log.v(TAG, "unable to get miniThumbBitmap, data is null"); |
| } |
| if (data != null) { |
| Bitmap b = BitmapFactory.decodeByteArray(data, 0, data.length); |
| if (b == null) { |
| if (VERBOSE) { |
| Log.v(TAG, "couldn't decode byte array for mini thumb, length was " + data.length); |
| } |
| } |
| return b; |
| } |
| } |
| return null; |
| } catch (Exception ex) { |
| Log.e(TAG, "miniThumbBitmap got exception " + ex.toString()); |
| for (StackTraceElement s : ex.getStackTrace()) |
| Log.e(TAG, "... " + s.toString()); |
| return null; |
| } |
| } |
| |
| public void onRemove() { |
| mContainer.mCache.remove(mId); |
| } |
| |
| protected void saveMiniThumb(Bitmap source) { |
| mContainer.saveMiniThumbToFile(source, fullSizeImageId(), 0); |
| } |
| |
| /* (non-Javadoc) |
| * @see com.android.camera.IImage#setName() |
| */ |
| public void setDescription(String description) { |
| if (mContainer.indexDescription() < 0) return; |
| Cursor c = getCursor(); |
| synchronized (c) { |
| if (c.moveToPosition(getRow())) { |
| c.updateString(mContainer.indexDescription(), description); |
| } |
| } |
| } |
| |
| /* (non-Javadoc) |
| * @see com.android.camera.IImage#setIsPrivate() |
| */ |
| public void setIsPrivate(boolean isPrivate) { |
| if (mContainer.indexPrivate() < 0) return; |
| Cursor c = getCursor(); |
| synchronized (c) { |
| if (c.moveToPosition(getRow())) { |
| c.updateInt(mContainer.indexPrivate(), isPrivate ? 1 : 0); |
| } |
| } |
| } |
| |
| /* (non-Javadoc) |
| * @see com.android.camera.IImage#setName() |
| */ |
| public void setName(String name) { |
| Cursor c = getCursor(); |
| synchronized (c) { |
| if (c.moveToPosition(getRow())) { |
| c.updateString(mContainer.indexTitle(), name); |
| } |
| } |
| } |
| |
| public void setPicasaId(String id) { |
| Cursor c = null; |
| try { |
| c = mContentResolver.query( |
| fullSizeImageUri(), |
| new String[] { "_id", Images.Media.PICASA_ID }, |
| null, |
| null, null); |
| if (c != null && c.moveToFirst()) { |
| if (VERBOSE) { |
| Log.v(TAG, "storing picasaid " + id + " for " + fullSizeImageUri()); |
| } |
| c.updateString(1, id); |
| c.commitUpdates(); |
| if (VERBOSE) { |
| Log.v(TAG, "updated image with picasa id " + id); |
| } |
| } |
| } finally { |
| if (c != null) |
| c.close(); |
| } |
| } |
| |
| /* (non-Javadoc) |
| * @see com.android.camera.IImage#thumbUri() |
| */ |
| public Uri thumbUri() { |
| Uri uri = fullSizeImageUri(); |
| // The value for the query parameter cannot be null :-(, so using a dummy "1" |
| uri = uri.buildUpon().appendQueryParameter("thumb", "1").build(); |
| return uri; |
| } |
| |
| @Override |
| public String toString() { |
| return fullSizeImageUri().toString(); |
| } |
| } |
| |
| abstract static class BaseImageList implements IImageList { |
| Context mContext; |
| ContentResolver mContentResolver; |
| Uri mBaseUri, mUri; |
| int mSort; |
| String mBucketId; |
| boolean mDistinct; |
| Cursor mCursor; |
| boolean mCursorDeactivated; |
| protected HashMap<Long, IImage> mCache = new HashMap<Long, IImage>(); |
| |
| IImageList.OnChange mListener = null; |
| Handler mHandler; |
| protected RandomAccessFile mMiniThumbData; |
| protected Uri mThumbUri; |
| |
| public BaseImageList(Context ctx, ContentResolver cr, Uri uri, int sort, String bucketId) { |
| mContext = ctx; |
| mSort = sort; |
| mUri = uri; |
| mBaseUri = uri; |
| 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<Thread> mWaiting = new ArrayList<Thread>(); |
| |
| void lock() { |
| // if (VERBOSE) Log.v(TAG, "lock... thread " + Thread.currentThread().getId()); |
| synchronized (mSync) { |
| while (mLocked) { |
| try { |
| // if (VERBOSE) Log.v(TAG, "waiting... thread " + Thread.currentThread().getId()); |
| mWaiting.add(Thread.currentThread()); |
| mSync.wait(); |
| if (mWaiting.get(0) == Thread.currentThread()) { |
| mWaiting.remove(0); |
| break; |
| } |
| } catch (InterruptedException ex) { |
| // |
| } |
| } |
| // if (VERBOSE) Log.v(TAG, "locked... thread " + Thread.currentThread().getId()); |
| mLocked = true; |
| } |
| } |
| |
| void unlock() { |
| // if (VERBOSE) Log.v(TAG, "unlocking... thread " + Thread.currentThread().getId()); |
| synchronized (mSync) { |
| mLocked = false; |
| mSync.notifyAll(); |
| } |
| } |
| } |
| |
| // If the photo has an EXIF thumbnail and it's big enough, extract it and save that JPEG as |
| // the large thumbnail without re-encoding it. We still have to decompress it though, in |
| // order to generate the minithumb. |
| private Bitmap createThumbnailFromEXIF(String filePath, long id) { |
| if (filePath != null) { |
| byte [] thumbData = null; |
| synchronized (ImageManager.instance()) { |
| thumbData = (new ExifInterface(filePath)).getThumbnail(); |
| } |
| if (thumbData != null) { |
| // Sniff the size of the EXIF thumbnail before decoding it. Photos from the |
| // device will pass, but images that are side loaded from other cameras may not. |
| BitmapFactory.Options options = new BitmapFactory.Options(); |
| options.inJustDecodeBounds = true; |
| BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options); |
| int width = options.outWidth; |
| int height = options.outHeight; |
| if (width >= THUMBNAIL_TARGET_SIZE && height >= THUMBNAIL_TARGET_SIZE) { |
| if (storeThumbnail(thumbData, id, width, height)) { |
| // this is used for *encoding* the minithumb, so |
| // we don't want to dither or convert to 565 here. |
| // |
| // Decode with a scaling factor |
| // to match MINI_THUMB_TARGET_SIZE closely |
| // which will produce much better scaling quality |
| // and is significantly faster. |
| options.inSampleSize = computeSampleSize(options, THUMBNAIL_TARGET_SIZE); |
| |
| if (VERBOSE) { |
| Log.v(TAG, "in createThumbnailFromExif using inSampleSize of " + options.inSampleSize); |
| } |
| options.inDither = false; |
| options.inPreferredConfig = Bitmap.Config.ARGB_8888; |
| options.inJustDecodeBounds = false; |
| return BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| // The fallback case is to decode the original photo to thumbnail size, then encode it as a |
| // JPEG. We return the thumbnail Bitmap in order to create the minithumb from it. |
| private Bitmap createThumbnailFromUri(Cursor c, long id) { |
| Uri uri = ContentUris.withAppendedId(mBaseUri, id); |
| Bitmap bitmap = makeBitmap(THUMBNAIL_TARGET_SIZE, uri, null, null); |
| if (bitmap != null) { |
| storeThumbnail(bitmap, id); |
| } else { |
| uri = ContentUris.withAppendedId(mBaseUri, id); |
| bitmap = makeBitmap(MINI_THUMB_TARGET_SIZE, uri, null, null); |
| } |
| return bitmap; |
| } |
| |
| private 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(); |
| } finally { |
| retriever.release(); |
| } |
| return bitmap; |
| } |
| |
| // returns id |
| public long checkThumbnail(BaseImage existingImage, Cursor c, int i) { |
| long magic, fileMagic = 0, id; |
| try { |
| mLock.lock(); |
| if (existingImage == null) { |
| // if we don't have an Image object then get the id and magic from |
| // the cursor. Synchonize on the cursor object. |
| synchronized (c) { |
| if (!c.moveToPosition(i)) { |
| return -1; |
| } |
| magic = c.getLong(indexMiniThumbId()); |
| id = c.getLong(indexId()); |
| } |
| } else { |
| // if we have an Image object then ask them for the magic/id |
| magic = existingImage.mMiniThumbMagic; |
| id = existingImage.fullSizeImageId(); |
| } |
| |
| if (magic != 0) { |
| // check the mini thumb file for the right data. Right is defined as |
| // having the right magic number at the offset reserved for this "id". |
| RandomAccessFile r = miniThumbDataFile(); |
| if (r != null) { |
| synchronized (r) { |
| long pos = id * sBytesPerMiniThumb; |
| try { |
| // check that we can read the following 9 bytes (1 for the "status" and 8 for the long) |
| if (r.length() >= pos + 1 + 8) { |
| r.seek(pos); |
| if (r.readByte() == 1) { |
| fileMagic = r.readLong(); |
| if (fileMagic == magic && magic != 0 && magic != id) { |
| return magic; |
| } |
| } |
| } |
| } catch (IOException ex) { |
| Log.v(TAG, "got exception checking file magic: " + ex); |
| } |
| } |
| } |
| if (VERBOSE) { |
| Log.v(TAG, "didn't verify... fileMagic: " + fileMagic + "; magic: " + magic + "; id: " + id + "; "); |
| } |
| } |
| |
| // If we can't retrieve the thumbnail, first check if there is one embedded in the |
| // EXIF data. If not, or it's not big enough, decompress the full size image. |
| Bitmap bitmap = null; |
| |
| String filePath = null; |
| synchronized (c) { |
| if (c.moveToPosition(i)) { |
| filePath = c.getString(indexData()); |
| } |
| } |
| if (filePath != null) { |
| bitmap = createThumbnailFromEXIF(filePath, id); |
| if (bitmap == null) { |
| String mimeType = c.getString(indexMimeType()); |
| boolean isVideo = isVideoMimeType(mimeType); |
| if (isVideo) { |
| bitmap = createVideoThumbnail(filePath); |
| } else { |
| bitmap = createThumbnailFromUri(c, id); |
| } |
| } |
| synchronized (c) { |
| int degrees = 0; |
| if (c.moveToPosition(i)) { |
| int column = indexOrientation(); |
| if (column >= 0) |
| degrees = c.getInt(column); |
| } |
| if (degrees != 0) { |
| Bitmap b2 = rotate(bitmap, degrees); |
| if (b2 != bitmap) |
| bitmap.recycle(); |
| bitmap = b2; |
| } |
| } |
| } |
| |
| // make a new magic number since things are out of sync |
| do { |
| magic = mRandom.nextLong(); |
| } while (magic == 0); |
| if (bitmap != null) { |
| saveMiniThumbToFile(bitmap, id, magic); |
| bitmap.recycle(); |
| } |
| |
| synchronized (c) { |
| c.moveToPosition(i); |
| c.updateLong(indexMiniThumbId(), magic); |
| c.commitUpdates(); |
| c.requery(); |
| c.moveToPosition(i); |
| |
| if (existingImage != null) { |
| existingImage.mMiniThumbMagic = magic; |
| } |
| return magic; |
| } |
| } finally { |
| mLock.unlock(); |
| } |
| } |
| |
| public void checkThumbnails(ThumbCheckCallback cb) { |
| Cursor c = Images.Media.query( |
| mContentResolver, |
| mBaseUri, |
| new String[] { "_id", "mini_thumb_magic" }, |
| "mini_thumb_magic isnull and " + sWhereClause, |
| sAcceptableImageTypes, |
| "_id ASC"); |
| |
| int count = c.getCount(); |
| if (VERBOSE) |
| Log.v(TAG, ">>>>>>>>>>> need to check " + c.getCount() + " rows"); |
| |
| c.close(); |
| |
| if (!ImageManager.hasStorage()) { |
| if (VERBOSE) |
| Log.v(TAG, "bailing from the image checker thread -- no storage"); |
| return; |
| } |
| |
| String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1); |
| File oldFile = new File(oldPath); |
| |
| if (count == 0) { |
| // now check that we have the right thumbs file |
| // Log.v(TAG, "count is zero but oldFile.exists() is " + oldFile.exists()); |
| if (!oldFile.exists()) { |
| return; |
| } |
| } |
| |
| c = getCursor(); |
| try { |
| if (VERBOSE) Log.v(TAG, "checkThumbnails found " + c.getCount()); |
| int max = c.getCount(); |
| int current = 0; |
| for (int i = 0; i < c.getCount(); i++) { |
| try { |
| checkThumbnail(null, c, i); |
| } catch (Exception ex) { |
| Log.e(TAG, "!!!!! failed to check thumbnail... was the sd card removed?"); |
| break; |
| } |
| if (cb != null) { |
| if (!cb.checking(current, max)) { |
| if (VERBOSE) Log.v(TAG, "got false from checking... break <<<<<<<<<<<<<<<<<<<<<<<<"); |
| break; |
| } |
| } |
| current += 1; |
| } |
| } finally { |
| if (VERBOSE) Log.v(TAG, "checkThumbnails existing after reaching count " + c.getCount()); |
| try { |
| oldFile.delete(); |
| } catch (Exception ex) { |
| // ignore |
| } |
| } |
| } |
| |
| public void commitChanges() { |
| synchronized (mCursor) { |
| mCursor.commitUpdates(); |
| requery(); |
| } |
| } |
| protected Uri contentUri(long id) { |
| try { |
| // does our uri already have an id (single image query)? |
| // if so just return it |
| long existingId = ContentUris.parseId(mBaseUri); |
| if (existingId != id) |
| Log.e(TAG, "id mismatch"); |
| return mBaseUri; |
| } catch (NumberFormatException ex) { |
| // otherwise tack on the id |
| return ContentUris.withAppendedId(mBaseUri, id); |
| } |
| } |
| |
| public void deactivate() { |
| mCursorDeactivated = true; |
| 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<String, String> getBucketIds(); |
| |
| public interface OnChange { |
| public void onChange(IImageList list); |
| } |
| |
| public interface ThumbCheckCallback { |
| public boolean checking(int current, int count); |
| } |
| |
| public abstract void checkThumbnails(ThumbCheckCallback cb); |
| |
| public abstract void commitChanges(); |
| |
| public abstract void deactivate(); |
| |
| /** |
| * Returns the count of image objects. |
| * |
| * @return the number of images |
| */ |
| public abstract int getCount(); |
| |
| /** |
| * @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<String, String>(); |
| } |
| if (!mExifData.containsKey(tag)) { |
| mExifData.put(tag, value); |
| } else { |
| if (VERBOSE) Log.v(TAG, "addExifTag where the key already was there: " + tag + " = " + value); |
| } |
| } |
| |
| /** |
| * Return the value of the Exif tag as an int. Returns 0 on any type of error. |
| * @param tag |
| * @return |
| */ |
| public int getExifTagInt(String tag) { |
| if (mExifData != null) { |
| String tagValue = mExifData.get(tag); |
| if (tagValue != null) { |
| return Integer.parseInt(tagValue); |
| } |
| } |
| return 0; |
| } |
| |
| public boolean isReadonly() { |
| String mimeType = getMimeType(); |
| return !"image/jpeg".equals(mimeType) && !"image/png".equals(mimeType); |
| } |
| |
| public boolean isDrm() { |
| return false; |
| } |
| |
| /** |
| * Remove tag if already there. Otherwise, does nothing. |
| * @param tag |
| */ |
| public void removeExifTag(String tag) { |
| if (mExifData == null) { |
| mExifData = new HashMap<String, String>(); |
| } |
| mExifData.remove(tag); |
| } |
| |
| /** |
| * Replaces the tag if already there. Otherwise, adds to the exif tags. |
| * @param tag |
| * @param value |
| */ |
| public void replaceExifTag(String tag, String value) { |
| if (mExifData == null) { |
| mExifData = new HashMap<String, String>(); |
| } |
| if (!mExifData.containsKey(tag)) { |
| mExifData.remove(tag); |
| } |
| mExifData.put(tag, value); |
| } |
| |
| /* (non-Javadoc) |
| * @see com.android.camera.IImage#saveModifiedImage(android.graphics.Bitmap) |
| */ |
| public IGetBoolean_cancelable saveImageContents( |
| final Bitmap image, |
| final byte [] jpegData, |
| final int orientation, |
| final boolean newFile, |
| final Cursor cursor) { |
| final class SaveImageContentsCancelable extends BaseCancelable implements IGetBoolean_cancelable { |
| IGetBoolean_cancelable mCurrentCancelable = null; |
| |
| SaveImageContentsCancelable() { |
| } |
| |
| public boolean doCancelWork() { |
| synchronized (this) { |
| if (mCurrentCancelable != null) |
| mCurrentCancelable.cancel(); |
| } |
| return true; |
| } |
| |
| public boolean get() { |
| try { |
| Bitmap thumbnail = null; |
| |
| long t1 = System.currentTimeMillis(); |
| Uri uri = mContainer.contentUri(mId); |
| synchronized (this) { |
| checkCanceled(); |
| mCurrentCancelable = compressImageToFile(image, jpegData, uri); |
| } |
| |
| long t2 = System.currentTimeMillis(); |
| if (!mCurrentCancelable.get()) |
| return false; |
| |
| synchronized (this) { |
| String filePath; |
| synchronized (cursor) { |
| cursor.moveToPosition(0); |
| filePath = cursor.getString(2); |
| } |
| // TODO: If thumbData is present and usable, we should call the version |
| // of storeThumbnail which takes a byte array, rather than re-encoding |
| // a new JPEG of the same dimensions. |
| byte [] thumbData = null; |
| synchronized (ImageManager.instance()) { |
| thumbData = (new ExifInterface(filePath)).getThumbnail(); |
| } |
| if (VERBOSE) Log.v(TAG, "for file " + filePath + " thumbData is " + thumbData + "; length " + (thumbData!=null ? thumbData.length : -1)); |
| if (thumbData != null) { |
| thumbnail = BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length); |
| if (VERBOSE) Log.v(TAG, "embedded thumbnail bitmap " + thumbnail.getWidth() + "/" + thumbnail.getHeight()); |
| } |
| if (thumbnail == null && image != null) { |
| thumbnail = image; |
| } |
| if (thumbnail == null && jpegData != null) { |
| thumbnail = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length); |
| } |
| } |
| |
| long t3 = System.currentTimeMillis(); |
| mContainer.storeThumbnail(thumbnail, Image.this.fullSizeImageId()); |
| long t4 = System.currentTimeMillis(); |
| checkCanceled(); |
| if (VERBOSE) Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>> rotating by " + orientation); |
| saveMiniThumb(rotate(thumbnail, orientation)); |
| long t5 = System.currentTimeMillis(); |
| checkCanceled(); |
| |
| if (VERBOSE) Log.v(TAG, String.format("Timing data %d %d %d %d", t2-t1, t3-t2, t4-t3, t5-t4)); |
| return true; |
| } catch (CanceledException ex) { |
| if (VERBOSE) Log.v(TAG, "got canceled... need to cleanup"); |
| return false; |
| } finally { |
| /* |
| Cursor c = getCursor(); |
| synchronized (c) { |
| if (c.moveTo(getRow())) { |
| mContainer.requery(); |
| } |
| } |
| */ |
| acknowledgeCancel(); |
| } |
| } |
| } |
| return new SaveImageContentsCancelable(); |
| } |
| |
| private void setExifRotation(int degrees) { |
| try { |
| Cursor c = getCursor(); |
| String filePath; |
| synchronized (c) { |
| filePath = c.getString(mContainer.indexData()); |
| } |
| synchronized (ImageManager.instance()) { |
| ExifInterface exif = new ExifInterface(filePath); |
| if (mExifData == null) { |
| mExifData = exif.getAttributes(); |
| } |
| if (degrees < 0) |
| degrees += 360; |
| |
| int orientation = ExifInterface.ORIENTATION_NORMAL; |
| switch (degrees) { |
| case 0: |
| orientation = ExifInterface.ORIENTATION_NORMAL; |
| break; |
| case 90: |
| orientation = ExifInterface.ORIENTATION_ROTATE_90; |
| break; |
| case 180: |
| orientation = ExifInterface.ORIENTATION_ROTATE_180; |
| break; |
| case 270: |
| orientation = ExifInterface.ORIENTATION_ROTATE_270; |
| break; |
| } |
| |
| replaceExifTag(ExifInterface.TAG_ORIENTATION, Integer.toString(orientation)); |
| replaceExifTag("UserComment", "saveRotatedImage comment orientation: " + orientation); |
| exif.saveAttributes(mExifData); |
| exif.commitChanges(); |
| } |
| } catch (Exception ex) { |
| Log.e(TAG, "unable to save exif data with new orientation " + fullSizeImageUri()); |
| } |
| } |
| |
| /** |
| * Save the rotated image by updating the Exif "Orientation" tag. |
| * @param degrees |
| * @return |
| */ |
| public boolean rotateImageBy(int degrees) { |
| int newDegrees = getDegreesRotated() + degrees; |
| setExifRotation(newDegrees); |
| setDegreesRotated(newDegrees); |
| |
| // setting this to zero will force the call to checkCursor to generate fresh thumbs |
| mMiniThumbMagic = 0; |
| Cursor c = mContainer.getCursor(); |
| synchronized (c) { |
| mContainer.checkThumbnail(this, mContainer.getCursor(), this.getRow()); |
| } |
| |
| return true; |
| } |
| |
| public Bitmap thumbBitmap() { |
| Bitmap bitmap = null; |
| Cursor c = null; |
| if (mContainer.mThumbUri != null) { |
| try { |
| c = mContentResolver.query( |
| mContainer.mThumbUri, |
| THUMB_PROJECTION, |
| Thumbnails.IMAGE_ID + "=?", |
| new String[] { String.valueOf(fullSizeImageId()) }, |
| null); |
| if (c != null && c.moveToFirst()) { |
| Uri thumbUri = ContentUris.withAppendedId(mContainer.mThumbUri, c.getLong(((ImageList)mContainer).INDEX_THUMB_ID)); |
| ParcelFileDescriptor pfdInput; |
| try { |
| BitmapFactory.Options options = new BitmapFactory.Options(); |
| options.inDither = false; |
| options.inPreferredConfig = Bitmap.Config.ARGB_8888; |
| pfdInput = mContentResolver.openFileDescriptor(thumbUri, "r"); |
| bitmap = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options); |
| pfdInput.close(); |
| } catch (FileNotFoundException ex) { |
| Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); |
| } catch (IOException ex) { |
| Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); |
| } catch (NullPointerException ex) { |
| // we seem to get this if the file doesn't exist anymore |
| Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); |
| } |
| } |
| } catch (Exception ex) { |
| // sdcard removed? |
| return null; |
| } finally { |
| if (c != null) |
| c.close(); |
| } |
| } |
| |
| if (bitmap == null) { |
| bitmap = fullSizeBitmap(THUMBNAIL_TARGET_SIZE, false); |
| if (VERBOSE) { |
| Log.v(TAG, "no thumbnail found... storing new one for " + fullSizeImageId()); |
| } |
| bitmap = mContainer.storeThumbnail(bitmap, fullSizeImageId()); |
| } |
| |
| if (bitmap != null) { |
| int degrees = getDegreesRotated(); |
| if (degrees != 0) { |
| Matrix m = new Matrix(); |
| m.setRotate(degrees, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2); |
| bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), |
| m, true); |
| } |
| } |
| |
| long elapsed = System.currentTimeMillis(); |
| return bitmap; |
| } |
| |
| } |
| |
| final static private String sWhereClause = "(" + Images.Media.MIME_TYPE + "=? or " + Images.Media.MIME_TYPE + "=?" + ")"; |
| final static private String[] sAcceptableImageTypes = new String[] { "image/jpeg", "image/png" }; |
| |
| private static final String[] IMAGE_PROJECTION = new String[] { |
| "_id", |
| "_data", |
| ImageColumns.DATE_TAKEN, |
| ImageColumns.MINI_THUMB_MAGIC, |
| ImageColumns.ORIENTATION, |
| ImageColumns.MIME_TYPE |
| }; |
| |
| /** |
| * Represents an ordered collection of Image objects. |
| * Provides an api to add and remove an image. |
| */ |
| class ImageList extends BaseImageList implements IImageList { |
| final int INDEX_ID = indexOf(IMAGE_PROJECTION, "_id"); |
| final int INDEX_DATA = indexOf(IMAGE_PROJECTION, "_data"); |
| final int INDEX_MIME_TYPE = indexOf(IMAGE_PROJECTION, MediaColumns.MIME_TYPE); |
| final int INDEX_DATE_TAKEN = indexOf(IMAGE_PROJECTION, ImageColumns.DATE_TAKEN); |
| final int INDEX_MINI_THUMB_MAGIC = indexOf(IMAGE_PROJECTION, ImageColumns.MINI_THUMB_MAGIC); |
| final int INDEX_ORIENTATION = indexOf(IMAGE_PROJECTION, ImageColumns.ORIENTATION); |
| |
| final int INDEX_THUMB_ID = indexOf(THUMB_PROJECTION, BaseColumns._ID); |
| final int INDEX_THUMB_IMAGE_ID = indexOf(THUMB_PROJECTION, Images.Thumbnails.IMAGE_ID); |
| final int INDEX_THUMB_WIDTH = indexOf(THUMB_PROJECTION, Images.Thumbnails.WIDTH); |
| final int INDEX_THUMB_HEIGHT = indexOf(THUMB_PROJECTION, Images.Thumbnails.HEIGHT); |
| |
| boolean mIsRegistered = false; |
| ContentObserver mContentObserver; |
| DataSetObserver mDataSetObserver; |
| |
| public HashMap<String, String> getBucketIds() { |
| Cursor c = Images.Media.query( |
| mContentResolver, |
| mBaseUri.buildUpon().appendQueryParameter("distinct", "true").build(), |
| new String[] { |
| ImageColumns.BUCKET_DISPLAY_NAME, |
| ImageColumns.BUCKET_ID |
| }, |
| whereClause(), |
| whereClauseArgs(), |
| sortOrder()); |
| |
| HashMap<String, String> hash = new HashMap<String, String>(); |
| if (c != null && c.moveToFirst()) { |
| do { |
| hash.put(c.getString(1), c.getString(0)); |
| } while (c.moveToNext()); |
| } |
| return hash; |
| } |
| /** |
| * ImageList constructor. |
| * @param cr ContentResolver |
| */ |
| public ImageList(Context ctx, ContentResolver cr, Uri imageUri, Uri thumbUri, int sort, String bucketId) { |
| super(ctx, cr, imageUri, sort, bucketId); |
| mBaseUri = imageUri; |
| mThumbUri = thumbUri; |
| mSort = sort; |
| |
| 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) { |
| // 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<Long> mSkipList = null; |
| |
| int [] mSkipCounts = null; |
| |
| public HashMap<String, String> getBucketIds() { |
| HashMap<String, String> hashMap = new HashMap<String, String>(); |
| for (IImageList list: mSubList) { |
| hashMap.putAll(list.getBucketIds()); |
| } |
| return hashMap; |
| } |
| |
| public ImageListUber(IImageList [] sublist, int sort) { |
| mSubList = sublist.clone(); |
| mSort = sort; |
| |
| if (mListener != null) { |
| for (IImageList list: sublist) { |
| list.setOnChangeListener(new OnChange() { |
| public void onChange(IImageList list) { |
| if (mListener != null) { |
| mListener.onChange(ImageListUber.this); |
| } |
| } |
| }, mHandler); |
| } |
| } |
| } |
| |
| public void checkThumbnails(ThumbCheckCallback cb) { |
| // TODO this isn't quite right because we need to get the |
| // total from each sub item and provide that in the callback |
| final IImageList sublist[] = mSubList; |
| final int length = sublist.length; |
| for (int i = 0; i < length; i++) |
| sublist[i].checkThumbnails(cb); |
| } |
| |
| public void commitChanges() { |
| final IImageList sublist[] = mSubList; |
| final int length = sublist.length; |
| for (int i = 0; i < length; i++) |
| sublist[i].commitChanges(); |
| } |
| |
| public void deactivate() { |
| final IImageList sublist[] = mSubList; |
| final int length = sublist.length; |
| int pos = -1; |
| while (++pos < length) { |
| IImageList sub = sublist[pos]; |
| sub.deactivate(); |
| } |
| } |
| |
| public int getCount() { |
| final IImageList sublist[] = mSubList; |
| final int length = sublist.length; |
| int count = 0; |
| for (int i = 0; i < length; i++) |
| count += sublist[i].getCount(); |
| return count; |
| } |
| |
| public boolean isEmpty() { |
| final IImageList sublist[] = mSubList; |
| final int length = sublist.length; |
| for (int i = 0; i < length; i++) { |
| if (! sublist[i].isEmpty()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // mSkipCounts is used to tally the counts as we traverse |
| // the mSkipList. It's a member variable only so that |
| // we don't have to allocate each time through. Otherwise |
| // it could just as easily be a local. |
| |
| public synchronized IImage getImageAt(int index) { |
| if (index < 0 || index > getCount()) |
| throw new IndexOutOfBoundsException("index " + index + " out of range max is " + getCount()); |
| |
| // first make sure our allocations are in order |
| if (mSkipCounts == null || mSubList.length > mSkipCounts.length) |
| mSkipCounts = new int[mSubList.length]; |
| |
| if (mSkipList == null) |
| mSkipList = new ArrayList<Long>(); |
| |
| // zero out the mSkipCounts since that's only used for the |
| // duration of the function call |
| for (int i = 0; i < mSubList.length; i++) |
| mSkipCounts[i] = 0; |
| |
| // a counter of how many images we've skipped in |
| // trying to get to index. alternatively we could |
| // have decremented index but, alas, I liked this |
| // way more. |
| int skipCount = 0; |
| |
| // scan the existing mSkipList to see if we've computed |
| // enough to just return the answer |
| for (int i = 0; i < mSkipList.size(); i++) { |
| long v = mSkipList.get(i); |
| |
| int offset = (int) (v & 0xFFFF); |
| int which = (int) (v >> 32); |
| |
| if (skipCount + offset > index) { |
| int subindex = mSkipCounts[which] + (index - skipCount); |
| IImage img = mSubList[which].getImageAt(subindex); |
| return img; |
| } |
| |
| skipCount += offset; |
| mSkipCounts[which] += offset; |
| } |
| |
| // if we get here we haven't computed the answer for |
| // "index" yet so keep computing. This means running |
| // through the list of images and either modifying the |
| // last entry or creating a new one. |
| long count = 0; |
| while (true) { |
| long maxTimestamp = mSort == 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<String, String> 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<String, String> 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<String, String> hash = new HashMap<String, String>(); |
| if (c != null && c.moveToFirst()) { |
| do { |
| Log.e(TAG, "id: " + c.getString(1) + " display_name: " + c.getString(0)); |
| 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; |
| } |
| |
| 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 final Bitmap sDefaultThumbnail = Bitmap.createBitmap(32, 32, Bitmap.Config.RGB_565); |
| |
| private String sortOrder() { |
| return Video.Media.DATE_MODIFIED + (mSort == SORT_ASCENDING ? " ASC " : " DESC"); |
| } |
| } |
| |
| /** |
| * 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 Bitmap sNoImageBitmap = Bitmap.createBitmap(128, 128, 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(com.android.camera.ImageManager.IImageList.ThumbCheckCallback cb) { |
| } |
| |
| public void commitChanges() { |
| } |
| |
| public void deactivate() { |
| } |
| |
| public HashMap<String, String> getBucketIds() { |
| return new HashMap<String,String>(); |
| } |
| |
| public int getCount() { |
| return 0; |
| } |
| |
| public 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(com.android.camera.ImageManager.IImageList.OnChange changeCallback) { |
| } |
| |
| public void setOnChangeListener(com.android.camera.ImageManager.IImageList.OnChange changeCallback, Handler h) { |
| } |
| |
| }; |
| } |
| |
| public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort) { |
| return allImages(ctx, cr, location, inclusion, sort, null, null); |
| } |
| |
| public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId) { |
| return allImages(ctx, cr, location, inclusion, sort, bucketId, null); |
| } |
| |
| public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId, Uri specificImageUri) { |
| if (VERBOSE) { |
| Log.v(TAG, "allImages " + location + " " + ((inclusion&INCLUDE_IMAGES)!=0) + " + v=" + ((inclusion&INCLUDE_VIDEOS)!=0)); |
| } |
| |
| if (cr == null) { |
| return null; |
| } else { |
| // false ==> don't require write access |
| boolean haveSdCard = hasStorage(false); |
| |
| if (true) { |
| // use this code to merge videos and stills into the same list |
| ArrayList<IImageList> l = new ArrayList<IImageList>(); |
| |
| if (VERBOSE) { |
| Log.v(TAG, "initializing ... haveSdCard == " + haveSdCard + "; inclusion is " + String.format("%x", inclusion)); |
| } |
| if (specificImageUri != null) { |
| try { |
| if (specificImageUri.getScheme().equalsIgnoreCase("content")) |
| l.add(new ImageList(ctx, cr, specificImageUri, sThumbURI, sort, bucketId)); |
| else |
| l.add(new SingleImageList(cr, specificImageUri)); |
| } catch (UnsupportedOperationException ex) { |
| } |
| } else { |
| if (haveSdCard && location != DataLocation.INTERNAL) { |
| if ((inclusion & INCLUDE_IMAGES) != 0) { |
| try { |
| l.add(new ImageList(ctx, cr, sStorageURI, sThumbURI, sort, bucketId)); |
| } catch (UnsupportedOperationException ex) { |
| } |
| } |
| if ((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; |
| } |
| } |