blob: cc76e83f87b764eaf2ee4b995860ce10d9672f95 [file] [log] [blame]
/*
* 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());
}
/**
* OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be imported.
* This is a temporary fix for bug#1655552.
*/
public static void ensureOSXCompatibleFolder() {
File nnnAAAAA = new File(
Environment.getExternalStorageDirectory().toString() + "/DCIM/100ANDRO");
if ((!nnnAAAAA.exists()) && (!nnnAAAAA.mkdir())) {
Log.e(TAG, "create NNNAAAAA file: "+ nnnAAAAA.getPath()+" failed");
}
}
// 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) {
byte[][] createdThumbData = new byte[1][];
try {
dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(),
getRow(), createdThumbData);
} catch (IOException ex) {
// Typically IOException because the sd card is full.
// But createdThumbData may have been filled in, so continue on.
}
data = createdThumbData[0];
}
if (data == null) {
data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic);
}
if (data == null) {
if (VERBOSE)
Log.v(TAG, "unable to get miniThumbBitmap, data is null");
}
if (data != null) {
Bitmap b = BitmapFactory.decodeByteArray(data, 0, data.length);
if (b == null) {
if (VERBOSE) {
Log.v(TAG, "couldn't decode byte array for mini thumb, length was " + data.length);
}
}
return b;
}
}
return null;
} catch (Exception ex) {
// Typically IOException because the sd card is full.
if (VERBOSE) {
Log.e(TAG, "miniThumbBitmap got exception " + ex.toString());
for (StackTraceElement s : ex.getStackTrace())
Log.e(TAG, "... " + s.toString());
}
return null;
}
}
public void onRemove() {
mContainer.mCache.remove(mId);
}
protected void saveMiniThumb(Bitmap source) throws IOException {
mContainer.saveMiniThumbToFile(source, fullSizeImageId(), 0);
}
/* (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) {
if (VERBOSE) Log.d(TAG, "unable to store thumbnail: " + ex);
return thumb;
}
}
/**
* Store a JPEG thumbnail from the EXIF header in the database.
*/
protected boolean storeThumbnail(byte[] jpegThumbnail, long imageId, int width, int height) {
if (jpegThumbnail == null)
return false;
Uri uri = getThumbnailUri(imageId, width, height);
if (uri == null) {
return false;
}
try {
OutputStream thumbOut = mContentResolver.openOutputStream(uri);
thumbOut.write(jpegThumbnail);
thumbOut.close();
return true;
}
catch (FileNotFoundException ex) {
return false;
}
catch (IOException ex) {
return false;
}
}
private Uri getThumbnailUri(long imageId, int width, int height) {
// we do not store thumbnails for DRM'd images
if (mThumbUri == null) {
return null;
}
Uri uri = null;
Cursor c = null;
try {
c = mContentResolver.query(
mThumbUri,
THUMB_PROJECTION,
Thumbnails.IMAGE_ID + "=?",
new String[]{String.valueOf(imageId)},
null);
if (c != null && c.moveToFirst()) {
// If, for some reaosn, we already have a row with a matching
// image id, then just update that row rather than creating a
// new row.
uri = ContentUris.withAppendedId(mThumbUri, c.getLong(indexThumbId()));
c.commitUpdates();
}
} finally {
if (c != null)
c.close();
}
if (uri == null) {
ContentValues values = new ContentValues(4);
values.put(Images.Thumbnails.KIND, Images.Thumbnails.MINI_KIND);
values.put(Images.Thumbnails.IMAGE_ID, imageId);
values.put(Images.Thumbnails.HEIGHT, height);
values.put(Images.Thumbnails.WIDTH, width);
uri = mContentResolver.insert(mThumbUri, values);
}
return uri;
}
java.util.Random mRandom = new java.util.Random(System.currentTimeMillis());
protected SomewhatFairLock mLock = new SomewhatFairLock();
class SomewhatFairLock {
private Object mSync = new Object();
private boolean mLocked = false;
private ArrayList<Thread> mWaiting = new ArrayList<Thread>();
void lock() {
// if (VERBOSE) Log.v(TAG, "lock... thread " + Thread.currentThread().getId());
synchronized (mSync) {
while (mLocked) {
try {
// if (VERBOSE) Log.v(TAG, "waiting... thread " + Thread.currentThread().getId());
mWaiting.add(Thread.currentThread());
mSync.wait();
if (mWaiting.get(0) == Thread.currentThread()) {
mWaiting.remove(0);
break;
}
} catch (InterruptedException ex) {
//
}
}
// if (VERBOSE) Log.v(TAG, "locked... thread " + Thread.currentThread().getId());
mLocked = true;
}
}
void unlock() {
// if (VERBOSE) Log.v(TAG, "unlocking... thread " + Thread.currentThread().getId());
synchronized (mSync) {
mLocked = false;
mSync.notifyAll();
}
}
}
// If the photo has an EXIF thumbnail and it's big enough, extract it and save that JPEG as
// the large thumbnail without re-encoding it. We still have to decompress it though, in
// order to generate the minithumb.
private Bitmap createThumbnailFromEXIF(String filePath, long id) {
if (filePath != null) {
byte [] thumbData = null;
synchronized (ImageManager.instance()) {
thumbData = (new ExifInterface(filePath)).getThumbnail();
}
if (thumbData != null) {
// Sniff the size of the EXIF thumbnail before decoding it. Photos from the
// device will pass, but images that are side loaded from other cameras may not.
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options);
int width = options.outWidth;
int height = options.outHeight;
if (width >= THUMBNAIL_TARGET_SIZE && height >= THUMBNAIL_TARGET_SIZE) {
if (storeThumbnail(thumbData, id, width, height)) {
// this is used for *encoding* the minithumb, so
// we don't want to dither or convert to 565 here.
//
// Decode with a scaling factor
// to match MINI_THUMB_TARGET_SIZE closely
// which will produce much better scaling quality
// and is significantly faster.
options.inSampleSize = computeSampleSize(options, THUMBNAIL_TARGET_SIZE);
if (VERBOSE) {
Log.v(TAG, "in createThumbnailFromExif using inSampleSize of " + options.inSampleSize);
}
options.inDither = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
options.inJustDecodeBounds = false;
return BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options);
}
}
}
}
return null;
}
// The fallback case is to decode the original photo to thumbnail size, then encode it as a
// JPEG. We return the thumbnail Bitmap in order to create the minithumb from it.
private Bitmap createThumbnailFromUri(Cursor c, long id) {
Uri uri = ContentUris.withAppendedId(mBaseUri, id);
Bitmap bitmap = makeBitmap(THUMBNAIL_TARGET_SIZE, uri, null, null);
if (bitmap != null) {
storeThumbnail(bitmap, id);
} else {
uri = ContentUris.withAppendedId(mBaseUri, id);
bitmap = makeBitmap(MINI_THUMB_TARGET_SIZE, uri, null, null);
}
return bitmap;
}
// returns id
public long checkThumbnail(BaseImage existingImage, Cursor c, int i) throws IOException {
return checkThumbnail(existingImage, c, i, null);
}
/**
* Checks to see if a mini thumbnail exists in the cache. If not, tries to create it and
* add it to the cache.
* @param existingImage
* @param c
* @param i
* @param createdThumbnailData if this parameter is non-null, and a new mini-thumbnail
* bitmap is created, the new bitmap's data will be stored in createdThumbnailData[0].
* Note that if the sdcard is full, it's possible that
* createdThumbnailData[0] will be set even if the method throws an IOException. This is
* actually useful, because it allows the caller to use the created thumbnail even if
* the sdcard is full.
* @return
* @throws IOException
*/
public long checkThumbnail(BaseImage existingImage, Cursor c, int i,
byte[][] createdThumbnailData) throws IOException {
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. Synchronize on the cursor object.
synchronized (c) {
if (!c.moveToPosition(i)) {
return -1;
}
magic = c.getLong(indexMiniThumbId());
id = c.getLong(indexId());
}
} else {
// if we have an Image object then ask them for the magic/id
magic = existingImage.mMiniThumbMagic;
id = existingImage.fullSizeImageId();
}
if (magic != 0) {
// check the mini thumb file for the right data. Right is defined as
// having the right magic number at the offset reserved for this "id".
RandomAccessFile r = miniThumbDataFile();
if (r != null) {
synchronized (r) {
long pos = id * sBytesPerMiniThumb;
try {
// check that we can read the following 9 bytes (1 for the "status" and 8 for the long)
if (r.length() >= pos + 1 + 8) {
r.seek(pos);
if (r.readByte() == 1) {
fileMagic = r.readLong();
if (fileMagic == magic && magic != 0 && magic != id) {
return magic;
}
}
}
} catch (IOException ex) {
Log.v(TAG, "got exception checking file magic: " + ex);
}
}
}
if (VERBOSE) {
Log.v(TAG, "didn't verify... fileMagic: " + fileMagic + "; magic: " + magic + "; id: " + id + "; ");
}
}
// If we can't retrieve the thumbnail, first check if there is one embedded in the
// EXIF data. If not, or it's not big enough, decompress the full size image.
Bitmap bitmap = null;
String filePath = null;
synchronized (c) {
if (c.moveToPosition(i)) {
filePath = c.getString(indexData());
}
}
if (filePath != null) {
String mimeType = c.getString(indexMimeType());
boolean isVideo = isVideoMimeType(mimeType);
if (isVideo) {
bitmap = createVideoThumbnail(filePath);
} else {
bitmap = createThumbnailFromEXIF(filePath, id);
if (bitmap == null) {
bitmap = createThumbnailFromUri(c, id);
}
}
synchronized (c) {
int degrees = 0;
if (c.moveToPosition(i)) {
int column = indexOrientation();
if (column >= 0)
degrees = c.getInt(column);
}
if (degrees != 0) {
Bitmap b2 = rotate(bitmap, degrees);
if (b2 != bitmap)
bitmap.recycle();
bitmap = b2;
}
}
}
// make a new magic number since things are out of sync
do {
magic = mRandom.nextLong();
} while (magic == 0);
if (bitmap != null) {
byte [] data = miniThumbData(bitmap);
if (createdThumbnailData != null) {
createdThumbnailData[0] = data;
}
saveMiniThumbToFile(data, id, magic);
}
synchronized (c) {
c.moveToPosition(i);
c.updateLong(indexMiniThumbId(), magic);
c.commitUpdates();
c.requery();
c.moveToPosition(i);
if (existingImage != null) {
existingImage.mMiniThumbMagic = magic;
}
return magic;
}
} finally {
mLock.unlock();
}
}
public void checkThumbnails(ThumbCheckCallback cb, int totalThumbnails) {
Cursor c = Images.Media.query(
mContentResolver,
mBaseUri,
new String[] { "_id", "mini_thumb_magic" },
thumbnailWhereClause(),
thumbnailWhereClauseArgs(),
"_id ASC");
int count = c.getCount();
if (VERBOSE)
Log.v(TAG, ">>>>>>>>>>> need to check " + c.getCount() + " rows");
c.close();
if (!ImageManager.hasStorage()) {
if (VERBOSE)
Log.v(TAG, "bailing from the image checker thread -- no storage");
return;
}
String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1);
File oldFile = new File(oldPath);
if (count == 0) {
// now check that we have the right thumbs file
// Log.v(TAG, "count is zero but oldFile.exists() is " + oldFile.exists());
if (!oldFile.exists()) {
return;
}
}
c = getCursor();
try {
if (VERBOSE) Log.v(TAG, "checkThumbnails found " + c.getCount());
int current = 0;
for (int i = 0; i < c.getCount(); i++) {
try {
checkThumbnail(null, c, i);
} catch (Exception ex) {
Log.e(TAG, "!!!!! failed to check thumbnail... was the sd card removed?");
break;
}
if (cb != null) {
if (!cb.checking(current, totalThumbnails)) {
if (VERBOSE) Log.v(TAG, "got false from checking... break <<<<<<<<<<<<<<<<<<<<<<<<");
break;
}
}
current += 1;
}
} finally {
if (VERBOSE) Log.v(TAG, "checkThumbnails existing after reaching count " + c.getCount());
try {
oldFile.delete();
} catch (Exception ex) {
// ignore
}
}
}
protected String thumbnailWhereClause() {
return sMiniThumbIsNull + " and " + sWhereClause;
}
protected String[] thumbnailWhereClauseArgs() {
return sAcceptableImageTypes;
}
public void commitChanges() {
synchronized (mCursor) {
mCursor.commitUpdates();
requery();
}
}
protected Uri contentUri(long id) {
try {
// does our uri already have an id (single image query)?
// if so just return it
long existingId = ContentUris.parseId(mBaseUri);
if (existingId != id)
Log.e(TAG, "id mismatch");
return mBaseUri;
} catch (NumberFormatException ex) {
// otherwise tack on the id
return ContentUris.withAppendedId(mBaseUri, id);
}
}
public void deactivate() {
mCursorDeactivated = true;
try {
mCursor.deactivate();
} catch (IllegalStateException e) {
// IllegalStateException may be thrown if the cursor is stale.
Log.e(TAG, "Caught exception while deactivating cursor.", e);
}
if (mMiniThumbData != null) {
try {
mMiniThumbData.close();
mMiniThumbData = null;
} catch (IOException ex) {
}
}
}
public void dump(String msg) {
int count = getCount();
if (VERBOSE) Log.v(TAG, "dump ImageList (count is " + count + ") " + msg);
for (int i = 0; i < count; i++) {
IImage img = getImageAt(i);
if (img == null)
if (VERBOSE) Log.v(TAG, " " + i + ": " + "null");
else
if (VERBOSE) Log.v(TAG, " " + i + ": " + img.toString());
}
if (VERBOSE) Log.v(TAG, "end of dump container");
}
public int getCount() {
Cursor c = getCursor();
synchronized (c) {
try {
return c.getCount();
} catch (Exception ex) {
}
return 0;
}
}
public boolean isEmpty() {
return getCount() == 0;
}
protected Cursor getCursor() {
synchronized (mCursor) {
if (mCursorDeactivated) {
activateCursor();
}
return mCursor;
}
}
protected void activateCursor() {
requery();
}
public IImage getImageAt(int i) {
Cursor c = getCursor();
synchronized (c) {
boolean moved;
try {
moved = c.moveToPosition(i);
} catch (Exception ex) {
return null;
}
if (moved) {
try {
long id = c.getLong(0);
long miniThumbId = 0;
int rotation = 0;
if (indexMiniThumbId() != -1) {
miniThumbId = c.getLong(indexMiniThumbId());
}
if (indexOrientation() != -1) {
rotation = c.getInt(indexOrientation());
}
long timestamp = c.getLong(1);
IImage img = mCache.get(id);
if (img == null) {
img = make(id, miniThumbId, mContentResolver, this, timestamp, i, rotation);
mCache.put(id, img);
}
return img;
} catch (Exception ex) {
Log.e(TAG, "got this exception trying to create image object: " + ex);
return null;
}
} else {
Log.e(TAG, "unable to moveTo to " + i + "; count is " + c.getCount());
return null;
}
}
}
public IImage getImageForUri(Uri uri) {
// TODO make this a hash lookup
for (int i = 0; i < getCount(); i++) {
if (getImageAt(i).fullSizeImageUri().equals(uri)) {
return getImageAt(i);
}
}
return null;
}
private byte [] getMiniThumbFromFile(long id, byte [] data, long magicCheck) {
RandomAccessFile r = miniThumbDataFile();
if (r == null)
return null;
long pos = id * sBytesPerMiniThumb;
RandomAccessFile f = r;
synchronized (f) {
try {
f.seek(pos);
if (f.readByte() == 1) {
long magic = f.readLong();
if (magic != magicCheck) {
if (VERBOSE) Log.v(TAG, "for id " + id + "; magic: " + magic + "; magicCheck: " + magicCheck + " (fail)");
return null;
}
int length = f.readInt();
f.read(data, 0, length);
return data;
} else {
return null;
}
} catch (IOException ex) {
long fileLength;
try {
fileLength = f.length();
} catch (IOException ex1) {
fileLength = -1;
}
if (VERBOSE) {
Log.e(TAG, "couldn't read thumbnail for " + id + "; " + ex.toString() + "; pos is " + pos + "; length is " + fileLength);
}
return null;
}
}
}
protected int getRowFor(IImage imageObj) {
Cursor c = getCursor();
synchronized (c) {
int index = 0;
long targetId = imageObj.fullSizeImageId();
if (c.moveToFirst()) {
do {
if (c.getLong(0) == targetId) {
return index;
}
index += 1;
} while (c.moveToNext());
}
return -1;
}
}
protected abstract int indexOrientation();
protected abstract int indexDateTaken();
protected abstract int indexDescription();
protected abstract int indexMimeType();
protected abstract int indexData();
protected abstract int indexId();
protected abstract int indexLatitude();
protected abstract int indexLongitude();
protected abstract int indexMiniThumbId();
protected abstract int indexPicasaWeb();
protected abstract int indexPrivate();
protected abstract int indexTitle();
protected abstract int indexDisplayName();
protected abstract int indexThumbId();
protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) {
return null;
}
protected abstract Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options);
public boolean removeImage(IImage image) {
Cursor c = getCursor();
synchronized (c) {
/*
* TODO: consider putting the image in a holding area so
* we can get it back as needed
* TODO: need to delete the thumbnails as well
*/
boolean moved;
try {
moved = c.moveToPosition(image.getRow());
} catch (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 bitmap, long id, long magic) throws IOException {
byte[] data = miniThumbData(bitmap);
saveMiniThumbToFile(data, id, magic);
}
protected void saveMiniThumbToFile(byte[] data, long id, long magic) throws IOException {
RandomAccessFile r = miniThumbDataFile();
if (r == null)
return;
long pos = id * sBytesPerMiniThumb;
long t0 = System.currentTimeMillis();
synchronized (r) {
try {
long t1 = System.currentTimeMillis();
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());
throw ex;
}
}
}
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, int totalCount);
public abstract void commitChanges();
public abstract void deactivate();
/**
* Returns the count of image objects.
*
* @return the number of images
*/
public abstract int getCount();
/**
* @return true if the count of image objects is zero.
*/
public abstract boolean isEmpty();
/**
* Returns the image at the ith position.
*
* @param i the position
* @return the image at the ith position
*/
public abstract IImage getImageAt(int i);
/**
* Returns the image with a particular Uri.
*
* @param uri
* @return the image with a particular Uri.
*/
public abstract IImage getImageForUri(Uri uri);;
/**
*
* @param image
* @return true if the image was removed.
*/
public abstract boolean removeImage(IImage image);
/**
* Removes the image at the ith position.
* @param i the position
*/
public abstract void removeImageAt(int i);
public abstract void removeOnChangeListener(OnChange changeCallback);
public abstract void setOnChangeListener(OnChange changeCallback, Handler h);
}
class Image extends BaseImage implements IImage {
int mRotation;
protected Image(long id, long miniThumbId, ContentResolver cr, BaseImageList container, int cursorRow, int rotation) {
super(id, miniThumbId, cr, container, cursorRow);
mRotation = rotation;
}
public String getDataPath() {
String path = null;
Cursor c = getCursor();
synchronized (c) {
if (c.moveToPosition(getRow())) {
int column = ((ImageList)getContainer()).indexData();
if (column >= 0)
path = c.getString(column);
}
}
return path;
}
protected int getDegreesRotated() {
return mRotation;
}
protected void setDegreesRotated(int degrees) {
Cursor c = getCursor();
mRotation = degrees;
synchronized (c) {
if (c.moveToPosition(getRow())) {
int column = ((ImageList)getContainer()).indexOrientation();
if (column >= 0) {
c.updateInt(column, degrees);
getContainer().commitChanges();
}
}
}
}
protected Bitmap.CompressFormat compressionType() {
String mimeType = getMimeType();
if (mimeType == null)
return Bitmap.CompressFormat.JPEG;
if (mimeType.equals("image/png"))
return Bitmap.CompressFormat.PNG;
else if (mimeType.equals("image/png"))
return Bitmap.CompressFormat.PNG;
return Bitmap.CompressFormat.JPEG;
}
/**
* Does not replace the tag if already there. Otherwise, adds to the exif tags.
* @param tag
* @param value
*/
public void addExifTag(String tag, String value) {
if (mExifData == null) {
mExifData = new HashMap<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);
try {
saveMiniThumb(rotate(thumbnail, orientation));
} catch (IOException e) {
// Ignore if unable to save thumb.
}
long t5 = System.currentTimeMillis();
checkCanceled();
if (VERBOSE) Log.v(TAG, String.format("Timing data %d %d %d %d", t2-t1, t3-t2, t4-t3, t5-t4));
return true;
} catch (CanceledException ex) {
if (VERBOSE) Log.v(TAG, "got canceled... need to cleanup");
return false;
} finally {
/*
Cursor c = getCursor();
synchronized (c) {
if (c.moveTo(getRow())) {
mContainer.requery();
}
}
*/
acknowledgeCancel();
}
}
}
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;
try {
mContainer.checkThumbnail(this, mContainer.getCursor(), this.getRow());
} catch (IOException e) {
// Ignore inability to store mini thumbnail.
}
return true;
}
public Bitmap thumbBitmap() {
Bitmap bitmap = null;
Cursor c = null;
if (mContainer.mThumbUri != null) {
try {
c = mContentResolver.query(
mContainer.mThumbUri,
THUMB_PROJECTION,
Thumbnails.IMAGE_ID + "=?",
new String[] { String.valueOf(fullSizeImageId()) },
null);
if (c != null && c.moveToFirst()) {
Uri thumbUri = ContentUris.withAppendedId(mContainer.mThumbUri, c.getLong(((ImageList)mContainer).INDEX_THUMB_ID));
ParcelFileDescriptor pfdInput;
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDither = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
pfdInput = mContentResolver.openFileDescriptor(thumbUri, "r");
bitmap = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
pfdInput.close();
} catch (FileNotFoundException ex) {
Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
} catch (IOException ex) {
Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
} catch (NullPointerException ex) {
// we seem to get this if the file doesn't exist anymore
Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
}
}
} catch (Exception ex) {
// sdcard removed?
return null;
} finally {
if (c != null)
c.close();
}
}
if (bitmap == null) {
bitmap = fullSizeBitmap(THUMBNAIL_TARGET_SIZE, false);
if (VERBOSE) {
Log.v(TAG, "no thumbnail found... storing new one for " + fullSizeImageId());
}
bitmap = mContainer.storeThumbnail(bitmap, fullSizeImageId());
}
if (bitmap != null) {
int degrees = getDegreesRotated();
if (degrees != 0) {
Matrix m = new Matrix();
m.setRotate(degrees, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),
m, true);
}
}
long elapsed = System.currentTimeMillis();
return bitmap;
}
}
final static private String sWhereClause = "(" + Images.Media.MIME_TYPE + "=? or " + Images.Media.MIME_TYPE + "=?" + ")";
final static private String[] sAcceptableImageTypes = new String[] { "image/jpeg", "image/png" };
final static private String sMiniThumbIsNull = "mini_thumb_magic isnull";
private static final String[] IMAGE_PROJECTION = new String[] {
"_id",
"_data",
ImageColumns.DATE_TAKEN,
ImageColumns.MINI_THUMB_MAGIC,
ImageColumns.ORIENTATION,
ImageColumns.MIME_TYPE
};
/**
* Represents an ordered collection of Image objects.
* Provides an api to add and remove an image.
*/
class ImageList extends BaseImageList implements IImageList {
final int INDEX_ID = indexOf(IMAGE_PROJECTION, "_id");
final int INDEX_DATA = indexOf(IMAGE_PROJECTION, "_data");
final int INDEX_MIME_TYPE = indexOf(IMAGE_PROJECTION, MediaColumns.MIME_TYPE);
final int INDEX_DATE_TAKEN = indexOf(IMAGE_PROJECTION, ImageColumns.DATE_TAKEN);
final int INDEX_MINI_THUMB_MAGIC = indexOf(IMAGE_PROJECTION, ImageColumns.MINI_THUMB_MAGIC);
final int INDEX_ORIENTATION = indexOf(IMAGE_PROJECTION, ImageColumns.ORIENTATION);
final int INDEX_THUMB_ID = indexOf(THUMB_PROJECTION, BaseColumns._ID);
final int INDEX_THUMB_IMAGE_ID = indexOf(THUMB_PROJECTION, Images.Thumbnails.IMAGE_ID);
final int INDEX_THUMB_WIDTH = indexOf(THUMB_PROJECTION, Images.Thumbnails.WIDTH);
final int INDEX_THUMB_HEIGHT = indexOf(THUMB_PROJECTION, Images.Thumbnails.HEIGHT);
boolean mIsRegistered = false;
ContentObserver mContentObserver;
DataSetObserver mDataSetObserver;
public HashMap<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, int totalCount) {
// do nothing
}
@Override
public long checkThumbnail(BaseImage existingImage, Cursor c, int i) {
return 0;
}
class DrmImage extends Image {
protected DrmImage(long id, ContentResolver cr, BaseImageList container, int cursorRow) {
super(id, 0, cr, container, cursorRow, 0);
}
public boolean isDrm() {
return true;
}
public boolean isReadonly() {
return true;
}
public Bitmap miniThumbBitmap() {
return fullSizeBitmap(MINI_THUMB_TARGET_SIZE);
}
public Bitmap thumbBitmap() {
return fullSizeBitmap(THUMBNAIL_TARGET_SIZE);
}
public String getDisplayName() {
return getTitle();
}
}
@Override
protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) {
return new DrmImage(id, mContentResolver, this, index);
}
protected int indexOrientation() { return -1; }
protected int indexDateTaken() { return -1; }
protected int indexDescription() { return -1; }
protected int indexMimeType() { return -1; }
protected int indexId() { return -1; }
protected int indexLatitude() { return -1; }
protected int indexLongitude() { return -1; }
protected int indexMiniThumbId() { return -1; }
protected int indexPicasaWeb() { return -1; }
protected int indexPrivate() { return -1; }
protected int indexTitle() { return -1; }
protected int indexDisplayName() { return -1; }
protected int indexThumbId() { return -1; }
// TODO review this probably should be based on DATE_TAKEN same as images
private String sortOrder() {
String ascending = (mSort == SORT_ASCENDING ? " ASC" : " DESC");
return
DrmStore.Images.TITLE + ascending + "," +
DrmStore.Images._ID;
}
}
class ImageListUber implements IImageList {
private IImageList [] mSubList;
private int mSort;
private IImageList.OnChange mListener = null;
Handler mHandler;
// This is an array of Longs wherein each Long consists of
// two components. The first component indicates the number of
// consecutive entries that belong to a given sublist.
// The second component indicates which sublist we're referring
// to (an int which is used to index into mSubList).
ArrayList<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, int totalThumbnails) {
for (IImageList i : mSubList) {
int count = i.getCount();
i.checkThumbnails(cb, totalThumbnails);
totalThumbnails -= count;
}
}
public void commitChanges() {
final IImageList sublist[] = mSubList;
final int length = sublist.length;
for (int i = 0; i < length; i++)
sublist[i].commitChanges();
}
public void deactivate() {
final IImageList sublist[] = mSubList;
final int length = sublist.length;
int pos = -1;
while (++pos < length) {
IImageList sub = sublist[pos];
sub.deactivate();
}
}
public int getCount() {
final IImageList sublist[] = mSubList;
final int length = sublist.length;
int count = 0;
for (int i = 0; i < length; i++)
count += sublist[i].getCount();
return count;
}
public boolean isEmpty() {
final IImageList sublist[] = mSubList;
final int length = sublist.length;
for (int i = 0; i < length; i++) {
if (! sublist[i].isEmpty()) {
return false;
}
}
return true;
}
// mSkipCounts is used to tally the counts as we traverse
// the mSkipList. It's a member variable only so that
// we don't have to allocate each time through. Otherwise
// it could just as easily be a local.
public synchronized IImage getImageAt(int index) {
if (index < 0 || index > getCount())
throw new IndexOutOfBoundsException("index " + index + " out of range max is " + getCount());
// first make sure our allocations are in order
if (mSkipCounts == null || mSubList.length > mSkipCounts.length)
mSkipCounts = new int[mSubList.length];
if (mSkipList == null)
mSkipList = new ArrayList<Long>();
// zero out the mSkipCounts since that's only used for the
// duration of the function call
for (int i = 0; i < mSubList.length; i++)
mSkipCounts[i] = 0;
// a counter of how many images we've skipped in
// trying to get to index. alternatively we could
// have decremented index but, alas, I liked this
// way more.
int skipCount = 0;
// scan the existing mSkipList to see if we've computed
// enough to just return the answer
for (int i = 0; i < mSkipList.size(); i++) {
long v = mSkipList.get(i);
int offset = (int) (v & 0xFFFF);
int which = (int) (v >> 32);
if (skipCount + offset > index) {
int subindex = mSkipCounts[which] + (index - skipCount);
IImage img = mSubList[which].getImageAt(subindex);
return img;
}
skipCount += offset;
mSkipCounts[which] += offset;
}
// if we get here we haven't computed the answer for
// "index" yet so keep computing. This means running
// through the list of images and either modifying the
// last entry or creating a new one.
long count = 0;
while (true) {
long maxTimestamp = mSort == 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 {
hash.put(c.getString(1), c.getString(0));
} while (c.moveToNext());
}
return hash;
}
protected String whereClause() {
if (mBucketId != null) {
return Images.Media.BUCKET_ID + " = '" + mBucketId + "'";
} else {
return null;
}
}
protected String[] whereClauseArgs() {
return null;
}
@Override
protected String thumbnailWhereClause() {
return sMiniThumbIsNull;
}
@Override
protected String[] thumbnailWhereClauseArgs() {
return null;
}
protected Cursor createCursor() {
Cursor c =
Images.Media.query(
mContentResolver,
mBaseUri,
sProjection,
whereClause(),
whereClauseArgs(),
sortOrder());
if (VERBOSE)
Log.v(TAG, "createCursor got cursor with count " + (c == null ? -1 : c.getCount()));
return c;
}
protected int indexOrientation() { return -1; }
protected int indexDateTaken() { return INDEX_DATE_TAKEN; }
protected int indexDescription() { return INDEX_DESCRIPTION; }
protected int indexMimeType() { return INDEX_MIME_TYPE; }
protected int indexData() { return INDEX_DATA; }
protected int indexId() { return INDEX_ID; }
protected int indexLatitude() { return INDEX_LATITUDE; }
protected int indexLongitude() { return INDEX_LONGITUDE; }
protected int indexMiniThumbId() { return INDEX_MINI_THUMB_MAGIC; }
protected int indexPicasaWeb() { return -1; }
protected int indexPrivate() { return INDEX_PRIVATE; }
protected int indexTitle() { return INDEX_TITLE; }
protected int indexDisplayName() { return -1; }
protected int indexThumbId() { return INDEX_THUMB_ID; }
@Override
protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list,
long timestamp, int index, int rotation) {
return new VideoObject(id, miniThumbId, mContentResolver, this, timestamp, index);
}
@Override
protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput,
BitmapFactory.Options options) {
MediaPlayer mp = new MediaPlayer();
Bitmap thumbnail = sDefaultThumbnail;
try {
mp.setDataSource(mContext, uri);
// int duration = mp.getDuration();
// int at = duration > 2000 ? 1000 : duration / 2;
int at = 1000;
thumbnail = mp.getFrameAt(at);
if (Config.LOGV) {
if ( thumbnail != null) {
Log.v(TAG, "getFrameAt @ " + at + " returned " + thumbnail + "; " +
thumbnail.getWidth() + " " + thumbnail.getHeight());
} else {
Log.v(TAG, "getFrame @ " + at + " failed for " + uri);
}
}
} catch (IOException ex) {
} catch (IllegalArgumentException ex) {
} catch (SecurityException ex) {
} finally {
mp.release();
}
return thumbnail;
}
private String sortOrder() {
return Video.Media.DATE_TAKEN + (mSort == SORT_ASCENDING ? " ASC " : " DESC");
}
}
private final static Bitmap sDefaultThumbnail = Bitmap.createBitmap(32, 32, Bitmap.Config.RGB_565);
/**
* Represents a particular video and provides access
* to the underlying data and two thumbnail bitmaps
* as well as other information such as the id, and
* the path to the actual video data.
*/
class VideoObject extends BaseImage implements IImage {
/**
* Constructor.
*
* @param id the image id of the image
* @param cr the content resolver
*/
protected VideoObject(long id, long miniThumbId, ContentResolver cr, VideoList container,
long dateTaken, int row) {
super(id, miniThumbId, cr, container, row);
}
protected Bitmap.CompressFormat compressionType() {
return Bitmap.CompressFormat.JPEG;
}
@Override
public boolean equals(Object other) {
if (other == null)
return false;
if (!(other instanceof VideoObject))
return false;
return fullSizeImageUri().equals(((VideoObject)other).fullSizeImageUri());
}
public String getDataPath() {
String path = null;
Cursor c = getCursor();
synchronized (c) {
if (c.moveToPosition(getRow())) {
int column = ((VideoList)getContainer()).indexData();
if (column >= 0)
path = c.getString(column);
}
}
return path;
}
/* (non-Javadoc)
* @see com.android.camera.IImage#fullSizeBitmap()
*/
public Bitmap fullSizeBitmap(int targetWidthHeight) {
return sNoImageBitmap;
}
public IGetBitmap_cancelable fullSizeBitmap_cancelable(int targetWidthHeight) {
return null;
}
/* (non-Javadoc)
* @see com.android.camera.IImage#fullSizeImageData()
*/
public InputStream fullSizeImageData() {
try {
InputStream input = mContentResolver.openInputStream(
fullSizeImageUri());
return input;
} catch (IOException ex) {
return null;
}
}
/* (non-Javadoc)
* @see com.android.camera.IImage#fullSizeImageId()
*/
public long fullSizeImageId() {
return mId;
}
public String getCategory() {
return getStringEntry(((VideoList)mContainer).INDEX_CATEGORY);
}
public int getHeight() {
return 0;
}
public String getLanguage() {
return getStringEntry(((VideoList)mContainer).INDEX_LANGUAGE);
}
public String getPicasaId() {
return null;
}
private String getStringEntry(int entryName) {
String entry = null;
Cursor c = getCursor();
synchronized(c) {
if (c.moveToPosition(getRow())) {
entry = c.getString(entryName);
}
}
return entry;
}
public String getTags() {
return getStringEntry(((VideoList)mContainer).INDEX_TAGS);
}
public int getWidth() {
return 0;
}
/* (non-Javadoc)
* @see com.android.camera.IImage#imageId()
*/
public long imageId() {
return mId;
}
public boolean isReadonly() {
return false;
}
public boolean isDrm() {
return false;
}
public boolean rotateImageBy(int degrees) {
return false;
}
public void setCategory(String category) {
setStringEntry(category, ((VideoList)mContainer).INDEX_CATEGORY);
}
public void setLanguage(String language) {
setStringEntry(language, ((VideoList)mContainer).INDEX_LANGUAGE);
}
private void setStringEntry(String entry, int entryName) {
Cursor c = getCursor();
synchronized (c) {
if (c.moveToPosition(getRow())) {
c.updateString(entryName, entry);
}
}
}
public void setTags(String tags) {
setStringEntry(tags, ((VideoList)mContainer).INDEX_TAGS);
}
/* (non-Javadoc)
* @see com.android.camera.IImage#thumb1()
*/
public Bitmap thumbBitmap() {
return fullSizeBitmap(320);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("" + mId);
return sb.toString();
}
}
private final static Bitmap sNoImageBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565);
/*
* How much quality to use when storing the thumbnail.
*/
private static ImageManager sInstance = null;
private static final int MINI_THUMB_TARGET_SIZE = 96;
private static final int THUMBNAIL_TARGET_SIZE = 320;
private static final String[] THUMB_PROJECTION = new String[] {
BaseColumns._ID, // 0
Images.Thumbnails.IMAGE_ID, // 1
Images.Thumbnails.WIDTH,
Images.Thumbnails.HEIGHT
};
private static Uri sStorageURI = Images.Media.EXTERNAL_CONTENT_URI;
private static Uri sThumbURI = Images.Thumbnails.EXTERNAL_CONTENT_URI;
private static Uri sVideoStorageURI = Uri.parse("content://media/external/video/media");
private static Uri sVideoThumbURI = Uri.parse("content://media/external/video/thumbnails");
/**
* Returns an ImageList object that contains
* all of the images.
* @param cr
* @param location
* @param includeImages
* @param includeVideo
* @return the singleton ImageList
*/
static final public int SORT_ASCENDING = 1;
static final public int SORT_DESCENDING = 2;
static final public int INCLUDE_IMAGES = (1 << 0);
static final public int INCLUDE_DRM_IMAGES = (1 << 1);
static final public int INCLUDE_VIDEOS = (1 << 2);
static public DataLocation getDefaultDataLocation() {
return DataLocation.EXTERNAL;
}
private static int indexOf(String [] array, String s) {
for (int i = 0; i < array.length; i++) {
if (array[i].equals(s)) {
return i;
}
}
return -1;
}
/**
* Returns the singleton instance of the ImageManager.
* @return the ImageManager instance.
*/
public static ImageManager instance() {
if (sInstance == null) {
sInstance = new ImageManager();
}
return sInstance;
}
/**
* Creates a byte[] for a given bitmap of the desired size. Recycles the input bitmap.
*/
static public byte[] miniThumbData(Bitmap source) {
if (source == null)
return null;
Bitmap miniThumbnail = extractMiniThumb(source, MINI_THUMB_TARGET_SIZE,
MINI_THUMB_TARGET_SIZE);
java.io.ByteArrayOutputStream miniOutStream = new java.io.ByteArrayOutputStream();
miniThumbnail.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream);
miniThumbnail.recycle();
try {
miniOutStream.close();
byte [] data = miniOutStream.toByteArray();
return data;
} catch (java.io.IOException ex) {
Log.e(TAG, "got exception ex " + ex);
}
return null;
}
/**
* Creates a centered bitmap of the desired size. Recycles the input.
* @param source
* @return
*/
static public Bitmap extractMiniThumb(Bitmap source, int width, int height) {
if (source == null) {
return null;
}
float scale;
if (source.getWidth() < source.getHeight()) {
scale = width / (float)source.getWidth();
} else {
scale = height / (float)source.getHeight();
}
Matrix matrix = new Matrix();
matrix.setScale(scale, scale);
Bitmap miniThumbnail = ImageLoader.transform(matrix, source,
width, height, false);
if (miniThumbnail != source) {
source.recycle();
}
return miniThumbnail;
}
static Bitmap rotate(Bitmap b, int degrees) {
if (degrees != 0 && b != null) {
Matrix m = new Matrix();
m.setRotate(degrees, (float) b.getWidth() / 2, (float) b.getHeight() / 2);
Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
// TODO should recycle here but that needs more testing/verification
// b.recycle();
b = b2;
}
return b;
}
public static int roundOrientation(int orientationInput) {
int orientation = orientationInput;
if (orientation == -1)
orientation = 0;
orientation = orientation % 360;
int retVal;
if (orientation < (0*90) + 45) {
retVal = 0;
} else if (orientation < (1*90) + 45) {
retVal = 90;
} else if (orientation < (2*90) + 45) {
retVal = 180;
} else if (orientation < (3*90) + 45) {
retVal = 270;
} else {
retVal = 0;
}
if (VERBOSE) Log.v(TAG, "map orientation " + orientationInput + " to " + retVal);
return retVal;
}
/**
* @return true if the mimetype is an image mimetype.
*/
public static boolean isImageMimeType(String mimeType) {
return mimeType.startsWith("image/");
}
/**
* @return true if the mimetype is a video mimetype.
*/
public static boolean isVideoMimeType(String mimeType) {
return mimeType.startsWith("video/");
}
/**
* @return true if the image is an image.
*/
public static boolean isImage(IImage image) {
return isImageMimeType(image.getMimeType());
}
/**
* @return true if the image is a video.
*/
public static boolean isVideo(IImage image) {
return isVideoMimeType(image.getMimeType());
}
public Uri addImage(
final Context ctx,
final ContentResolver cr,
final String imageName,
final String description,
final long dateTaken,
final Location location,
final int orientation,
final String directory,
final String filename) {
ContentValues values = new ContentValues(7);
values.put(Images.Media.TITLE, imageName);
values.put(Images.Media.DISPLAY_NAME, imageName);
values.put(Images.Media.DESCRIPTION, description);
values.put(Images.Media.DATE_TAKEN, dateTaken);
values.put(Images.Media.MIME_TYPE, "image/jpeg");
values.put(Images.Media.ORIENTATION, orientation);
File parentFile = new File(directory);
// Lowercase the path for hashing. This avoids duplicate buckets if the filepath
// case is changed externally.
// Keep the original case for display.
String path = parentFile.toString().toLowerCase();
String name = parentFile.getName();
if (VERBOSE) Log.v(TAG, "addImage id is " + path.hashCode() + "; name " + name + "; path is " + path);
if (location != null) {
if (VERBOSE) {
Log.v(TAG, "lat long " + location.getLatitude() + " / " + location.getLongitude());
}
values.put(Images.Media.LATITUDE, location.getLatitude());
values.put(Images.Media.LONGITUDE, location.getLongitude());
}
if (directory != null && filename != null) {
String value = directory + "/" + filename;
values.put("_data", value);
}
long t3 = System.currentTimeMillis();
Uri uri = cr.insert(sStorageURI, values);
// The line above will create a filename that ends in .jpg
// That filename is what will be handed to gmail when a user shares a photo.
// Gmail gets the name of the picture attachment from the "DISPLAY_NAME" field.
// Extract the filename and jam it into the display name.
Cursor c = cr.query(
uri,
new String [] { ImageColumns._ID, Images.Media.DISPLAY_NAME, "_data" },
null,
null,
null);
if (c.moveToFirst()) {
String filePath = c.getString(2);
if (filePath != null) {
int pos = filePath.lastIndexOf("/");
if (pos >= 0) {
filePath = filePath.substring(pos + 1); // pick off the filename
c.updateString(1, filePath);
c.commitUpdates();
}
}
}
c.close();
return uri;
}
public IAddImage_cancelable storeImage(
final Uri uri,
final Context ctx,
final ContentResolver cr,
final int orientation,
final Bitmap source,
final byte [] jpegData) {
class AddImageCancelable extends BaseCancelable implements IAddImage_cancelable {
private IGetBoolean_cancelable mSaveImageCancelable;
public boolean doCancelWork() {
if (VERBOSE) {
Log.v(TAG, "calling AddImageCancelable.cancel() " + mSaveImageCancelable);
}
if (mSaveImageCancelable != null) {
mSaveImageCancelable.cancel();
}
return true;
}
public void get() {
if (source == null && jpegData == null) {
throw new IllegalArgumentException("source cannot be null");
}
try {
long t1 = System.currentTimeMillis();
synchronized (this) {
if (mCancel) {
throw new CanceledException();
}
}
long id = ContentUris.parseId(uri);
BaseImageList il = new ImageList(ctx, cr, sStorageURI, sThumbURI, SORT_ASCENDING, null);
ImageManager.Image image = new Image(id, 0, cr, il, il.getCount(), 0);
long t5 = System.currentTimeMillis();
Cursor c = cr.query(
uri,
new String [] { ImageColumns._ID, ImageColumns.MINI_THUMB_MAGIC, "_data" },
null,
null,
null);
c.moveToPosition(0);
synchronized (this) {
checkCanceled();
mSaveImageCancelable = image.saveImageContents(source, jpegData, orientation, true, c);
}
if (mSaveImageCancelable.get()) {
long t6 = System.currentTimeMillis();
if (VERBOSE) Log.v(TAG, "saveImageContents took " + (t6-t5));
if (VERBOSE) Log.v(TAG, "updating new picture with id " + id);
c.updateLong(1, id);
c.commitUpdates();
c.close();
long t7 = System.currentTimeMillis();
if (VERBOSE) Log.v(TAG, "commit updates to save mini thumb took " + (t7-t6));
}
else {
c.close();
throw new CanceledException();
}
} catch (CanceledException ex) {
if (VERBOSE) {
Log.v(TAG, "caught CanceledException");
}
if (uri != null) {
if (VERBOSE) {
Log.v(TAG, "canceled... cleaning up this uri: " + uri);
}
cr.delete(uri, null, null);
}
acknowledgeCancel();
}
}
}
return new AddImageCancelable();
}
static public IImageList makeImageList(Uri uri, Context ctx, int sort) {
ContentResolver cr = ctx.getContentResolver();
String uriString = (uri != null) ? uri.toString() : "";
// TODO we need to figure out whether we're viewing
// DRM images in a better way. Is there a constant
// for content://drm somewhere??
IImageList imageList;
if (uriString.startsWith("content://drm")) {
imageList = ImageManager.instance().allImages(
ctx,
cr,
ImageManager.DataLocation.ALL,
ImageManager.INCLUDE_DRM_IMAGES,
sort);
} else if (!uriString.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
&& !uriString.startsWith(MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString())) {
imageList = ImageManager.instance().new SingleImageList(cr, uri);
} else {
String bucketId = uri.getQueryParameter("bucketId");
if (VERBOSE) Log.v(TAG, "bucketId is " + bucketId);
imageList = ImageManager.instance().allImages(
ctx,
cr,
ImageManager.DataLocation.ALL,
ImageManager.INCLUDE_IMAGES,
sort,
bucketId);
}
return imageList;
}
public IImageList emptyImageList() {
return
new IImageList() {
public void checkThumbnails(ImageManager.IImageList.ThumbCheckCallback cb,
int totalThumbnails) {
}
public void commitChanges() {
}
public void deactivate() {
}
public HashMap<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(ImageManager.IImageList.OnChange changeCallback) {
}
public void setOnChangeListener(ImageManager.IImageList.OnChange changeCallback,
Handler h) {
}
};
}
public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort) {
return allImages(ctx, cr, location, inclusion, sort, null, null);
}
public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId) {
return allImages(ctx, cr, location, inclusion, sort, bucketId, null);
}
public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId, Uri specificImageUri) {
if (VERBOSE) {
Log.v(TAG, "allImages " + location + " " + ((inclusion&INCLUDE_IMAGES)!=0) + " + v=" + ((inclusion&INCLUDE_VIDEOS)!=0));
}
if (cr == null) {
return null;
} else {
// false ==> don't require write access
boolean haveSdCard = hasStorage(false);
if (true) {
// use this code to merge videos and stills into the same list
ArrayList<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;
}
/**
* Create a video thumbnail for a video. May return null if the video is corrupt.
* @param filePath
* @return
*/
public static Bitmap createVideoThumbnail(String filePath) {
Bitmap bitmap = null;
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY);
retriever.setDataSource(filePath);
bitmap = retriever.captureFrame();
} catch(IllegalArgumentException ex) {
// Assume this is a corrupt video file
} catch (RuntimeException ex) {
// Assume this is a corrupt video file.
} finally {
try {
retriever.release();
} catch (RuntimeException ex) {
// Ignore failures while cleaning up.
}
}
return bitmap;
}
public static String getLastThumbPath() {
return Environment.getExternalStorageDirectory().toString() +
"/DCIM/.thumbnails/camera_last_thumb";
}
}