blob: 1fe93b5b1198c418b0cdb75593c86ff3eec1bd99 [file] [log] [blame]
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera;
18
19import android.content.Context;
20import android.content.ContentResolver;
21import android.content.ContentValues;
22import android.content.ContentUris;
23import android.database.ContentObserver;
24import android.database.Cursor;
25import android.database.DataSetObserver;
26import android.graphics.Bitmap;
27import android.graphics.BitmapFactory;
The Android Open Source Project0eaec582008-12-17 18:05:54 -080028import android.graphics.Canvas;
The Android Open Source Project1d4c7502008-10-21 07:00:00 -070029import android.graphics.Matrix;
30import android.location.Location;
The Android Open Source Project0eaec582008-12-17 18:05:54 -080031import android.media.MediaMetadataRetriever;
32import android.media.MediaPlayer;
The Android Open Source Project1d4c7502008-10-21 07:00:00 -070033import android.net.Uri;
34import android.os.Environment;
35import android.os.Handler;
36import android.os.ParcelFileDescriptor;
37import android.provider.BaseColumns;
38import android.provider.DrmStore;
39import android.provider.MediaStore;
40import android.provider.MediaStore.Images.ImageColumns;
41import android.provider.MediaStore.Images.Thumbnails;
The Android Open Source Project0eaec582008-12-17 18:05:54 -080042import android.provider.MediaStore.Video.VideoColumns;
The Android Open Source Project1d4c7502008-10-21 07:00:00 -070043import android.provider.MediaStore.Images;
44import android.provider.MediaStore.MediaColumns;
The Android Open Source Project0eaec582008-12-17 18:05:54 -080045import android.provider.MediaStore.Video;
The Android Open Source Project1d4c7502008-10-21 07:00:00 -070046import android.util.Config;
47import android.util.Log;
48
49import java.io.File;
50import java.io.FileNotFoundException;
51import java.io.IOException;
52import java.io.InputStream;
53import java.io.OutputStream;
54import java.io.RandomAccessFile;
55import java.util.ArrayList;
56import java.util.HashMap;
57
58/**
59 *
60 * ImageManager is used to retrieve and store images
61 * in the media content provider.
62 *
63 */
64public class ImageManager {
65 public static final String CAMERA_IMAGE_BUCKET_NAME = "/sdcard/dcim/camera";
66 public static final String CAMERA_IMAGE_BUCKET_ID = String.valueOf(CAMERA_IMAGE_BUCKET_NAME.hashCode());
67
68 // To enable verbose logging for this class, change false to true. The other logic ensures that
69 // this logging can be disabled by turned off DEBUG and lower, and that it can be enabled by
70 // "setprop log.tag.ImageManager VERBOSE" if desired.
71 //
72 // IMPORTANT: Never check in this file set to true!
73 private static final boolean VERBOSE = Config.LOGD && (false || Config.LOGV);
74 private static final String TAG = "ImageManager";
75
76 private static final int MINI_THUMB_DATA_FILE_VERSION = 3;
The Android Open Source Project0eaec582008-12-17 18:05:54 -080077
The Android Open Source Project1d4c7502008-10-21 07:00:00 -070078 static public void debug_where(String tag, String msg) {
79 try {
80 throw new Exception();
81 } catch (Exception ex) {
82 if (msg != null) {
83 Log.v(tag, msg);
84 }
85 boolean first = true;
86 for (StackTraceElement s : ex.getStackTrace()) {
87 if (first)
88 first = false;
89 else
90 Log.v(tag, s.toString());
91 }
92 }
93 }
94
95 /*
96 * Compute the sample size as a function of the image size and the target.
97 * Scale the image down so that both the width and height are just above
98 * the target. If this means that one of the dimension goes from above
99 * the target to below the target (e.g. given a width of 480 and an image
100 * width of 600 but sample size of 2 -- i.e. new width 300 -- bump the
101 * sample size down by 1.
102 */
103 private static int computeSampleSize(BitmapFactory.Options options, int target) {
104 int w = options.outWidth;
105 int h = options.outHeight;
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800106
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700107 int candidateW = w / target;
108 int candidateH = h / target;
109 int candidate = Math.max(candidateW, candidateH);
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800110
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700111 if (candidate == 0)
112 return 1;
113
114 if (candidate > 1) {
115 if ((w > target) && (w / candidate) < target)
116 candidate -= 1;
117 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800118
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700119 if (candidate > 1) {
120 if ((h > target) && (h / candidate) < target)
121 candidate -= 1;
122 }
123
124 if (VERBOSE)
125 Log.v(TAG, "for w/h " + w + "/" + h + " returning " + candidate + "(" + (w/candidate) + " / " + (h/candidate));
126
127 return candidate;
128 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800129 /*
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700130 * All implementors of ICancelable should inherit from BaseCancelable
131 * since it provides some convenience methods such as acknowledgeCancel
132 * and checkCancel.
133 */
134 public abstract class BaseCancelable implements ICancelable {
135 boolean mCancel = false;
136 boolean mFinished = false;
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800137
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700138 /*
139 * Subclasses should call acknowledgeCancel when they're finished with
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800140 * their operation.
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700141 */
142 protected void acknowledgeCancel() {
143 synchronized (this) {
144 mFinished = true;
145 if (!mCancel)
146 return;
147 if (mCancel) {
148 this.notify();
149 }
150 }
151 }
152
153 public boolean cancel() {
154 synchronized (this) {
155 if (mCancel) {
156 return false;
157 }
158 if (mFinished) {
159 return false;
160 }
161 mCancel = true;
162 boolean retVal = doCancelWork();
163
164 try {
165 this.wait();
166 } catch (InterruptedException ex) {
167 // now what??? TODO
168 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800169
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700170 return retVal;
171 }
172 }
173
174 /*
175 * Subclasses can call this to see if they have been canceled.
176 * This is the polling model.
177 */
178 protected void checkCanceled() throws CanceledException {
179 synchronized (this) {
180 if (mCancel)
181 throw new CanceledException();
182 }
183 }
184
185 /*
186 * Subclasses implement this method to take whatever action
187 * is necessary when getting canceled. Sometimes it's not
188 * possible to do anything in which case the "checkCanceled"
189 * polling model may be used (or some combination).
190 */
191 public abstract boolean doCancelWork();
192 }
193
194 private static final int sBytesPerMiniThumb = 10000;
195 static final private byte [] sMiniThumbData = new byte[sBytesPerMiniThumb];
196
197 /**
198 * Represents a particular image and provides access
199 * to the underlying bitmap and two thumbnail bitmaps
200 * as well as other information such as the id, and
201 * the path to the actual image data.
202 */
203 abstract class BaseImage implements IImage {
204 protected ContentResolver mContentResolver;
205 protected long mId, mMiniThumbMagic;
206 protected BaseImageList mContainer;
207 protected HashMap<String, String> mExifData;
208 protected int mCursorRow;
209
210 protected BaseImage(long id, long miniThumbId, ContentResolver cr, BaseImageList container, int cursorRow) {
211 mContentResolver = cr;
212 mId = id;
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800213 mMiniThumbMagic = miniThumbId;
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700214 mContainer = container;
215 mCursorRow = cursorRow;
216 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800217
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700218 abstract Bitmap.CompressFormat compressionType();
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800219
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700220 public void commitChanges() {
221 Cursor c = getCursor();
222 synchronized (c) {
223 if (c.moveToPosition(getRow())) {
224 c.commitUpdates();
225 c.requery();
226 }
227 }
228 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800229
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700230 /**
231 * Take a given bitmap and compress it to a file as described
232 * by the Uri parameter.
233 *
234 * @param bitmap the bitmap to be compressed/stored
235 * @param uri where to store the bitmap
236 * @return true if we succeeded
237 */
238 protected IGetBoolean_cancelable compressImageToFile(
239 final Bitmap bitmap,
240 final byte [] jpegData,
241 final Uri uri) {
242 class CompressImageToFile extends BaseCancelable implements IGetBoolean_cancelable {
243 ThreadSafeOutputStream mOutputStream = null;
244
245 public boolean doCancelWork() {
246 if (mOutputStream != null) {
247 try {
248 mOutputStream.close();
249 return true;
250 } catch (IOException ex) {
251 // TODO what to do here
252 }
253 }
254 return false;
255 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800256
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700257 public boolean get() {
258 try {
259 long t1 = System.currentTimeMillis();
260 OutputStream delegate = mContentResolver.openOutputStream(uri);
261 synchronized (this) {
262 checkCanceled();
263 mOutputStream = new ThreadSafeOutputStream(delegate);
264 }
265 long t2 = System.currentTimeMillis();
266 if (bitmap != null) {
267 bitmap.compress(compressionType(), 75, mOutputStream);
268 } else {
269 long x1 = System.currentTimeMillis();
270 mOutputStream.write(jpegData);
271 long x2 = System.currentTimeMillis();
272 if (VERBOSE) Log.v(TAG, "done writing... " + jpegData.length + " bytes took " + (x2-x1));
273 }
274 long t3 = System.currentTimeMillis();
275 if (VERBOSE) Log.v(TAG, String.format("CompressImageToFile.get took %d (%d, %d)",(t3-t1),(t2-t1),(t3-t2)));
276 return true;
277 } catch (FileNotFoundException ex) {
278 return false;
279 } catch (CanceledException ex) {
280 return false;
281 } catch (IOException ex) {
282 return false;
283 }
284 finally {
285 if (mOutputStream != null) {
286 try {
287 mOutputStream.close();
288 } catch (IOException ex) {
289 // not much we can do here so ignore
290 }
291 }
292 acknowledgeCancel();
293 }
294 }
295 }
296 return new CompressImageToFile();
297 }
298
299 @Override
300 public boolean equals(Object other) {
301 if (other == null)
302 return false;
303 if (!(other instanceof Image))
304 return false;
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800305
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700306 return fullSizeImageUri().equals(((Image)other).fullSizeImageUri());
307 }
308
309 public Bitmap fullSizeBitmap(int targetWidthHeight) {
310 return fullSizeBitmap(targetWidthHeight, true);
311 }
312
313 protected Bitmap fullSizeBitmap(int targetWidthHeight, boolean rotateAsNeeded) {
314 Uri url = mContainer.contentUri(mId);
315 if (VERBOSE) Log.v(TAG, "getCreateBitmap for " + url);
316 if (url == null)
317 return null;
318
319 Bitmap b = null;
320 if (b == null) {
321 b = makeBitmap(targetWidthHeight, url);
322 if (b != null && rotateAsNeeded) {
323 b = rotate(b, getDegreesRotated());
324 }
325 }
326 return b;
327 }
328
329
330 public IGetBitmap_cancelable fullSizeBitmap_cancelable(final int targetWidthHeight) {
331 final class LoadBitmapCancelable extends BaseCancelable implements IGetBitmap_cancelable {
332 ParcelFileDescriptor mPFD;
333 BitmapFactory.Options mOptions = new BitmapFactory.Options();
334 long mCancelInitiationTime;
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800335
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700336 public LoadBitmapCancelable(ParcelFileDescriptor pfdInput) {
337 mPFD = pfdInput;
338 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800339
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700340 public boolean doCancelWork() {
341 if (VERBOSE)
342 Log.v(TAG, "requesting bitmap load cancel");
343 mCancelInitiationTime = System.currentTimeMillis();
344 mOptions.requestCancelDecode();
345 return true;
346 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800347
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700348 public Bitmap get() {
349 try {
350 Bitmap b = makeBitmap(targetWidthHeight, fullSizeImageUri(), mPFD, mOptions);
351 if (mCancelInitiationTime != 0) {
352 if (VERBOSE)
353 Log.v(TAG, "cancelation of bitmap load success==" + (b == null ? "TRUE" : "FALSE") + " -- took " + (System.currentTimeMillis() - mCancelInitiationTime));
354 }
355 if (b != null) {
356 int degrees = getDegreesRotated();
357 if (degrees != 0) {
358 Matrix m = new Matrix();
359 m.setRotate(degrees, (float) b.getWidth() / 2, (float) b.getHeight() / 2);
360 Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
361 if (b != b2)
362 b.recycle();
363 b = b2;
364 }
365 }
366 return b;
367 } catch (Exception ex) {
368 return null;
369 } finally {
370 acknowledgeCancel();
371 }
372 }
373 }
374
375 try {
376 ParcelFileDescriptor pfdInput = mContentResolver.openFileDescriptor(fullSizeImageUri(), "r");
377 return new LoadBitmapCancelable(pfdInput);
378 } catch (FileNotFoundException ex) {
379 return null;
380 } catch (UnsupportedOperationException ex) {
381 return null;
382 }
383 }
384
385 public InputStream fullSizeImageData() {
386 try {
387 InputStream input = mContentResolver.openInputStream(
388 fullSizeImageUri());
389 return input;
390 } catch (IOException ex) {
391 return null;
392 }
393 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800394
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700395 public long fullSizeImageId() {
396 return mId;
397 }
398
399 public Uri fullSizeImageUri() {
400 return mContainer.contentUri(mId);
401 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800402
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700403 public IImageList getContainer() {
404 return mContainer;
405 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800406
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700407 Cursor getCursor() {
408 return mContainer.getCursor();
409 }
410
411 public long getDateTaken() {
412 if (mContainer.indexDateTaken() < 0) return 0;
413 Cursor c = getCursor();
414 synchronized (c) {
415 c.moveToPosition(getRow());
416 return c.getLong(mContainer.indexDateTaken());
417 }
418 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800419
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700420 protected int getDegreesRotated() {
421 return 0;
422 }
423
424 public String getMimeType() {
425 if (mContainer.indexMimeType() < 0) {
426 Cursor c = null;
427 try {
428 c = mContentResolver.query(
429 fullSizeImageUri(),
430 new String[] { "_id", Images.Media.MIME_TYPE },
431 null,
432 null, null);
433 if (c != null && c.moveToFirst()) {
434 return c.getString(1);
435 } else {
436 return "";
437 }
438 } finally {
439 if (c != null)
440 c.close();
441 }
442 } else {
443 String mimeType = null;
444 Cursor c = getCursor();
445 synchronized(c) {
446 if (c.moveToPosition(getRow())) {
447 mimeType = c.getString(mContainer.indexMimeType());
448 }
449 }
450 return mimeType;
451 }
452 }
453
454 /* (non-Javadoc)
455 * @see com.android.camera.IImage#getDescription()
456 */
457 public String getDescription() {
458 if (mContainer.indexDescription() < 0) {
459 Cursor c = null;
460 try {
461 c = mContentResolver.query(
462 fullSizeImageUri(),
463 new String[] { "_id", Images.Media.DESCRIPTION },
464 null,
465 null, null);
466 if (c != null && c.moveToFirst()) {
467 return c.getString(1);
468 } else {
469 return "";
470 }
471 } finally {
472 if (c != null)
473 c.close();
474 }
475 } else {
476 String description = null;
477 Cursor c = getCursor();
478 synchronized(c) {
479 if (c.moveToPosition(getRow())) {
480 description = c.getString(mContainer.indexDescription());
481 }
482 }
483 return description;
484 }
485 }
486
487 /* (non-Javadoc)
488 * @see com.android.camera.IImage#getIsPrivate()
489 */
490 public boolean getIsPrivate() {
491 if (mContainer.indexPrivate() < 0) return false;
492 boolean isPrivate = false;
493 Cursor c = getCursor();
494 synchronized(c) {
495 if (c.moveToPosition(getRow())) {
496 isPrivate = c.getInt(mContainer.indexPrivate()) != 0;
497 }
498 }
499 return isPrivate;
500 }
501
502 public double getLatitude() {
503 if (mContainer.indexLatitude() < 0) return 0D;
504 Cursor c = getCursor();
505 synchronized (c) {
506 c.moveToPosition(getRow());
507 return c.getDouble(mContainer.indexLatitude());
508 }
509 }
510
511 public double getLongitude() {
512 if (mContainer.indexLongitude() < 0) return 0D;
513 Cursor c = getCursor();
514 synchronized (c) {
515 c.moveToPosition(getRow());
516 return c.getDouble(mContainer.indexLongitude());
517 }
518 }
519
520 /* (non-Javadoc)
521 * @see com.android.camera.IImage#getTitle()
522 */
523 public String getTitle() {
524 String name = null;
525 Cursor c = getCursor();
526 synchronized(c) {
527 if (c.moveToPosition(getRow())) {
528 if (mContainer.indexTitle() != -1) {
529 name = c.getString(mContainer.indexTitle());
530 }
531 }
532 }
533 return name != null && name.length() > 0 ? name : String.valueOf(mId);
534 }
535
536 /* (non-Javadoc)
537 * @see com.android.camera.IImage#getDisplayName()
538 */
539 public String getDisplayName() {
540 if (mContainer.indexDisplayName() < 0) {
541 Cursor c = null;
542 try {
543 c = mContentResolver.query(
544 fullSizeImageUri(),
545 new String[] { "_id", Images.Media.DISPLAY_NAME },
546 null,
547 null, null);
548 if (c != null && c.moveToFirst()) {
549 return c.getString(1);
550 }
551 } finally {
552 if (c != null)
553 c.close();
554 }
555 } else {
556 String name = null;
557 Cursor c = getCursor();
558 synchronized(c) {
559 if (c.moveToPosition(getRow())) {
560 name = c.getString(mContainer.indexDisplayName());
561 }
562 }
563 if (name != null && name.length() > 0)
564 return name;
565 }
566 return String.valueOf(mId);
567 }
568
569 public String getPicasaId() {
570 /*
571 if (mContainer.indexPicasaWeb() < 0) return null;
572 Cursor c = getCursor();
573 synchronized (c) {
574 c.moveTo(getRow());
575 return c.getString(mContainer.indexPicasaWeb());
576 }
577 */
578 return null;
579 }
580
581 public int getRow() {
582 return mCursorRow;
583 }
584
585 public int getWidth() {
586 ParcelFileDescriptor input = null;
587 try {
588 Uri uri = fullSizeImageUri();
589 input = mContentResolver.openFileDescriptor(uri, "r");
590 BitmapFactory.Options options = new BitmapFactory.Options();
591 options.inJustDecodeBounds = true;
592 BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options);
593 return options.outWidth;
594 } catch (IOException ex) {
595 return 0;
596 } finally {
597 try {
598 if (input != null) {
599 input.close();
600 }
601 } catch (IOException ex) {
602 }
603 }
604 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800605
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700606 public int getHeight() {
607 ParcelFileDescriptor input = null;
608 try {
609 Uri uri = fullSizeImageUri();
610 input = mContentResolver.openFileDescriptor(uri, "r");
611 BitmapFactory.Options options = new BitmapFactory.Options();
612 options.inJustDecodeBounds = true;
613 BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options);
614 return options.outHeight;
615 } catch (IOException ex) {
616 return 0;
617 } finally {
618 try {
619 if (input != null) {
620 input.close();
621 }
622 } catch (IOException ex) {
623 }
624 }
625 }
626
627 public boolean hasLatLong() {
628 if (mContainer.indexLatitude() < 0 || mContainer.indexLongitude() < 0) return false;
629 Cursor c = getCursor();
630 synchronized (c) {
631 c.moveToPosition(getRow());
632 return !c.isNull(mContainer.indexLatitude()) && !c.isNull(mContainer.indexLongitude());
633 }
634 }
635
636 /* (non-Javadoc)
637 * @see com.android.camera.IImage#imageId()
638 */
639 public long imageId() {
640 return mId;
641 }
642
643 /**
644 * Make a bitmap from a given Uri.
645 *
646 * @param uri
647 */
648 private Bitmap makeBitmap(int targetWidthOrHeight, Uri uri) {
649 ParcelFileDescriptor input = null;
650 try {
651 input = mContentResolver.openFileDescriptor(uri, "r");
652 return makeBitmap(targetWidthOrHeight, uri, input, null);
653 } catch (IOException ex) {
654 return null;
655 } finally {
656 try {
657 if (input != null) {
658 input.close();
659 }
660 } catch (IOException ex) {
661 }
662 }
663 }
664
665 protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options) {
666 return mContainer.makeBitmap(targetWidthHeight, uri, pfdInput, options);
667 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800668
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700669 /* (non-Javadoc)
670 * @see com.android.camera.IImage#thumb1()
671 */
672 public Bitmap miniThumbBitmap() {
673 try {
674 long id = mId;
675 long dbMagic = mMiniThumbMagic;
676 if (dbMagic == 0 || dbMagic == id) {
677 dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), getRow());
678 if (VERBOSE) Log.v(TAG, "after computing thumbnail dbMagic is " + dbMagic);
679 }
680
681 synchronized(sMiniThumbData) {
682 dbMagic = mMiniThumbMagic;
683 byte [] data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic);
684 if (data == null) {
685 dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), getRow());
686 data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic);
687 }
688 if (data == null) {
689 if (VERBOSE)
690 Log.v(TAG, "unable to get miniThumbBitmap, data is null");
691 }
692 if (data != null) {
693 Bitmap b = BitmapFactory.decodeByteArray(data, 0, data.length);
694 if (b == null) {
695 if (VERBOSE) {
696 Log.v(TAG, "couldn't decode byte array for mini thumb, length was " + data.length);
697 }
698 }
699 return b;
700 }
701 }
702 return null;
703 } catch (Exception ex) {
704 Log.e(TAG, "miniThumbBitmap got exception " + ex.toString());
705 for (StackTraceElement s : ex.getStackTrace())
706 Log.e(TAG, "... " + s.toString());
707 return null;
708 }
709 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800710
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700711 public void onRemove() {
712 mContainer.mCache.remove(mId);
713 }
714
715 protected void saveMiniThumb(Bitmap source) {
716 mContainer.saveMiniThumbToFile(source, fullSizeImageId(), 0);
717 }
718
719 /* (non-Javadoc)
720 * @see com.android.camera.IImage#setName()
721 */
722 public void setDescription(String description) {
723 if (mContainer.indexDescription() < 0) return;
724 Cursor c = getCursor();
725 synchronized (c) {
726 if (c.moveToPosition(getRow())) {
727 c.updateString(mContainer.indexDescription(), description);
728 }
729 }
730 }
731
732 /* (non-Javadoc)
733 * @see com.android.camera.IImage#setIsPrivate()
734 */
735 public void setIsPrivate(boolean isPrivate) {
736 if (mContainer.indexPrivate() < 0) return;
737 Cursor c = getCursor();
738 synchronized (c) {
739 if (c.moveToPosition(getRow())) {
740 c.updateInt(mContainer.indexPrivate(), isPrivate ? 1 : 0);
741 }
742 }
743 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800744
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700745 /* (non-Javadoc)
746 * @see com.android.camera.IImage#setName()
747 */
748 public void setName(String name) {
749 Cursor c = getCursor();
750 synchronized (c) {
751 if (c.moveToPosition(getRow())) {
752 c.updateString(mContainer.indexTitle(), name);
753 }
754 }
755 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800756
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700757 public void setPicasaId(String id) {
758 Cursor c = null;
759 try {
760 c = mContentResolver.query(
761 fullSizeImageUri(),
762 new String[] { "_id", Images.Media.PICASA_ID },
763 null,
764 null, null);
765 if (c != null && c.moveToFirst()) {
766 if (VERBOSE) {
767 Log.v(TAG, "storing picasaid " + id + " for " + fullSizeImageUri());
768 }
769 c.updateString(1, id);
770 c.commitUpdates();
771 if (VERBOSE) {
772 Log.v(TAG, "updated image with picasa id " + id);
773 }
774 }
775 } finally {
776 if (c != null)
777 c.close();
778 }
779 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800780
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700781 /* (non-Javadoc)
782 * @see com.android.camera.IImage#thumbUri()
783 */
784 public Uri thumbUri() {
785 Uri uri = fullSizeImageUri();
786 // The value for the query parameter cannot be null :-(, so using a dummy "1"
787 uri = uri.buildUpon().appendQueryParameter("thumb", "1").build();
788 return uri;
789 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800790
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700791 @Override
792 public String toString() {
793 return fullSizeImageUri().toString();
794 }
795 }
796
797 abstract static class BaseImageList implements IImageList {
798 Context mContext;
799 ContentResolver mContentResolver;
800 Uri mBaseUri, mUri;
801 int mSort;
802 String mBucketId;
803 boolean mDistinct;
804 Cursor mCursor;
805 boolean mCursorDeactivated;
806 protected HashMap<Long, IImage> mCache = new HashMap<Long, IImage>();
807
808 IImageList.OnChange mListener = null;
809 Handler mHandler;
810 protected RandomAccessFile mMiniThumbData;
811 protected Uri mThumbUri;
812
813 public BaseImageList(Context ctx, ContentResolver cr, Uri uri, int sort, String bucketId) {
814 mContext = ctx;
815 mSort = sort;
816 mUri = uri;
817 mBaseUri = uri;
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800818 mBucketId = bucketId;
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700819
820 mContentResolver = cr;
821 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800822
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700823 String randomAccessFilePath(int version) {
824 String directoryName = Environment.getExternalStorageDirectory().toString() + "/dcim/.thumbnails";
825 String path = directoryName + "/.thumbdata" + version + "-" + mUri.hashCode();
826 return path;
827 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800828
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700829 RandomAccessFile miniThumbDataFile() {
830 if (mMiniThumbData == null) {
831 String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION);
832 File directory = new File(new File(path).getParent());
833 if (!directory.isDirectory()) {
834 if (!directory.mkdirs()) {
835 Log.e(TAG, "!!!! unable to create .thumbnails directory " + directory.toString());
836 }
837 }
838 File f = new File(path);
839 if (VERBOSE) Log.v(TAG, "file f is " + f.toString());
840 try {
841 mMiniThumbData = new RandomAccessFile(f, "rw");
842 } catch (IOException ex) {
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800843
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700844 }
845 }
846 return mMiniThumbData;
847 }
848
849 /**
850 * Store a given thumbnail in the database.
851 */
852 protected Bitmap storeThumbnail(Bitmap thumb, long imageId) {
853 if (thumb == null)
854 return null;
855
856 try {
857 Uri uri = getThumbnailUri(imageId, thumb.getWidth(), thumb.getHeight());
858 if (uri == null) {
859 return thumb;
860 }
861 OutputStream thumbOut = mContentResolver.openOutputStream(uri);
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800862 thumb.compress(Bitmap.CompressFormat.JPEG, 60, thumbOut);
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700863 thumbOut.close();
864 return thumb;
865 }
866 catch (Exception ex) {
867 Log.d(TAG, "unable to store thumbnail: " + ex);
868 return thumb;
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800869 }
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700870 }
871
872 /**
873 * Store a JPEG thumbnail from the EXIF header in the database.
874 */
875 protected boolean storeThumbnail(byte[] jpegThumbnail, long imageId, int width, int height) {
876 if (jpegThumbnail == null)
877 return false;
878
879 Uri uri = getThumbnailUri(imageId, width, height);
880 if (uri == null) {
881 return false;
882 }
883 try {
884 OutputStream thumbOut = mContentResolver.openOutputStream(uri);
885 thumbOut.write(jpegThumbnail);
886 thumbOut.close();
887 return true;
888 }
889 catch (FileNotFoundException ex) {
890 return false;
891 }
892 catch (IOException ex) {
893 return false;
894 }
895 }
896
897 private Uri getThumbnailUri(long imageId, int width, int height) {
898 // we do not store thumbnails for DRM'd images
899 if (mThumbUri == null) {
900 return null;
901 }
902
903 Uri uri = null;
904 Cursor c = null;
905 try {
906 c = mContentResolver.query(
907 mThumbUri,
908 THUMB_PROJECTION,
909 Thumbnails.IMAGE_ID + "=?",
910 new String[]{String.valueOf(imageId)},
911 null);
912 if (c != null && c.moveToFirst()) {
913 // If, for some reaosn, we already have a row with a matching
914 // image id, then just update that row rather than creating a
915 // new row.
916 uri = ContentUris.withAppendedId(mThumbUri, c.getLong(indexThumbId()));
917 c.commitUpdates();
918 }
919 } finally {
920 if (c != null)
921 c.close();
922 }
923 if (uri == null) {
924 ContentValues values = new ContentValues(4);
925 values.put(Images.Thumbnails.KIND, Images.Thumbnails.MINI_KIND);
926 values.put(Images.Thumbnails.IMAGE_ID, imageId);
927 values.put(Images.Thumbnails.HEIGHT, height);
928 values.put(Images.Thumbnails.WIDTH, width);
929 uri = mContentResolver.insert(mThumbUri, values);
930 }
931 return uri;
932 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800933
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700934 java.util.Random mRandom = new java.util.Random(System.currentTimeMillis());
935
936 protected SomewhatFairLock mLock = new SomewhatFairLock();
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800937
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700938 class SomewhatFairLock {
939 private Object mSync = new Object();
940 private boolean mLocked = false;
941 private ArrayList<Thread> mWaiting = new ArrayList<Thread>();
942
943 void lock() {
944// if (VERBOSE) Log.v(TAG, "lock... thread " + Thread.currentThread().getId());
945 synchronized (mSync) {
946 while (mLocked) {
947 try {
948// if (VERBOSE) Log.v(TAG, "waiting... thread " + Thread.currentThread().getId());
949 mWaiting.add(Thread.currentThread());
950 mSync.wait();
951 if (mWaiting.get(0) == Thread.currentThread()) {
952 mWaiting.remove(0);
953 break;
954 }
955 } catch (InterruptedException ex) {
956 //
957 }
958 }
959// if (VERBOSE) Log.v(TAG, "locked... thread " + Thread.currentThread().getId());
960 mLocked = true;
961 }
962 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -0800963
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700964 void unlock() {
965// if (VERBOSE) Log.v(TAG, "unlocking... thread " + Thread.currentThread().getId());
966 synchronized (mSync) {
967 mLocked = false;
968 mSync.notifyAll();
969 }
970 }
971 }
972
973 // If the photo has an EXIF thumbnail and it's big enough, extract it and save that JPEG as
974 // the large thumbnail without re-encoding it. We still have to decompress it though, in
975 // order to generate the minithumb.
976 private Bitmap createThumbnailFromEXIF(String filePath, long id) {
977 if (filePath != null) {
978 byte [] thumbData = null;
979 synchronized (ImageManager.instance()) {
980 thumbData = (new ExifInterface(filePath)).getThumbnail();
981 }
982 if (thumbData != null) {
983 // Sniff the size of the EXIF thumbnail before decoding it. Photos from the
984 // device will pass, but images that are side loaded from other cameras may not.
985 BitmapFactory.Options options = new BitmapFactory.Options();
986 options.inJustDecodeBounds = true;
987 BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options);
988 int width = options.outWidth;
989 int height = options.outHeight;
990 if (width >= THUMBNAIL_TARGET_SIZE && height >= THUMBNAIL_TARGET_SIZE) {
991 if (storeThumbnail(thumbData, id, width, height)) {
992 // this is used for *encoding* the minithumb, so
993 // we don't want to dither or convert to 565 here.
994 //
995 // Decode with a scaling factor
996 // to match MINI_THUMB_TARGET_SIZE closely
997 // which will produce much better scaling quality
998 // and is significantly faster.
999 options.inSampleSize = computeSampleSize(options, THUMBNAIL_TARGET_SIZE);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001000
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001001 if (VERBOSE) {
1002 Log.v(TAG, "in createThumbnailFromExif using inSampleSize of " + options.inSampleSize);
1003 }
1004 options.inDither = false;
1005 options.inPreferredConfig = Bitmap.Config.ARGB_8888;
1006 options.inJustDecodeBounds = false;
1007 return BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options);
1008 }
1009 }
1010 }
1011 }
1012 return null;
1013 }
1014
1015 // The fallback case is to decode the original photo to thumbnail size, then encode it as a
1016 // JPEG. We return the thumbnail Bitmap in order to create the minithumb from it.
1017 private Bitmap createThumbnailFromUri(Cursor c, long id) {
1018 Uri uri = ContentUris.withAppendedId(mBaseUri, id);
1019 Bitmap bitmap = makeBitmap(THUMBNAIL_TARGET_SIZE, uri, null, null);
1020 if (bitmap != null) {
1021 storeThumbnail(bitmap, id);
1022 } else {
1023 uri = ContentUris.withAppendedId(mBaseUri, id);
1024 bitmap = makeBitmap(MINI_THUMB_TARGET_SIZE, uri, null, null);
1025 }
1026 return bitmap;
1027 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001028
1029 private Bitmap createVideoThumbnail(String filePath) {
1030 Bitmap bitmap = null;
1031 MediaMetadataRetriever retriever = new MediaMetadataRetriever();
1032 try {
1033 retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY);
1034 retriever.setDataSource(filePath);
1035 bitmap = retriever.captureFrame();
1036 } finally {
1037 retriever.release();
1038 }
1039 return bitmap;
1040 }
1041
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001042 // returns id
1043 public long checkThumbnail(BaseImage existingImage, Cursor c, int i) {
1044 long magic, fileMagic = 0, id;
1045 try {
1046 mLock.lock();
1047 if (existingImage == null) {
1048 // if we don't have an Image object then get the id and magic from
1049 // the cursor. Synchonize on the cursor object.
1050 synchronized (c) {
1051 if (!c.moveToPosition(i)) {
1052 return -1;
1053 }
1054 magic = c.getLong(indexMiniThumbId());
1055 id = c.getLong(indexId());
1056 }
1057 } else {
1058 // if we have an Image object then ask them for the magic/id
1059 magic = existingImage.mMiniThumbMagic;
1060 id = existingImage.fullSizeImageId();
1061 }
1062
1063 if (magic != 0) {
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001064 // check the mini thumb file for the right data. Right is defined as
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001065 // having the right magic number at the offset reserved for this "id".
1066 RandomAccessFile r = miniThumbDataFile();
1067 if (r != null) {
1068 synchronized (r) {
1069 long pos = id * sBytesPerMiniThumb;
1070 try {
1071 // check that we can read the following 9 bytes (1 for the "status" and 8 for the long)
1072 if (r.length() >= pos + 1 + 8) {
1073 r.seek(pos);
1074 if (r.readByte() == 1) {
1075 fileMagic = r.readLong();
1076 if (fileMagic == magic && magic != 0 && magic != id) {
1077 return magic;
1078 }
1079 }
1080 }
1081 } catch (IOException ex) {
1082 Log.v(TAG, "got exception checking file magic: " + ex);
1083 }
1084 }
1085 }
1086 if (VERBOSE) {
1087 Log.v(TAG, "didn't verify... fileMagic: " + fileMagic + "; magic: " + magic + "; id: " + id + "; ");
1088 }
1089 }
1090
1091 // If we can't retrieve the thumbnail, first check if there is one embedded in the
1092 // EXIF data. If not, or it's not big enough, decompress the full size image.
1093 Bitmap bitmap = null;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001094
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001095 String filePath = null;
1096 synchronized (c) {
1097 if (c.moveToPosition(i)) {
1098 filePath = c.getString(indexData());
1099 }
1100 }
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001101 if (filePath != null) {
1102 bitmap = createThumbnailFromEXIF(filePath, id);
1103 if (bitmap == null) {
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001104 String mimeType = c.getString(indexMimeType());
1105 boolean isVideo = isVideoMimeType(mimeType);
1106 if (isVideo) {
1107 bitmap = createVideoThumbnail(filePath);
1108 } else {
1109 bitmap = createThumbnailFromUri(c, id);
1110 }
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001111 }
1112 synchronized (c) {
1113 int degrees = 0;
1114 if (c.moveToPosition(i)) {
1115 int column = indexOrientation();
1116 if (column >= 0)
1117 degrees = c.getInt(column);
1118 }
1119 if (degrees != 0) {
1120 Bitmap b2 = rotate(bitmap, degrees);
1121 if (b2 != bitmap)
1122 bitmap.recycle();
1123 bitmap = b2;
1124 }
1125 }
1126 }
1127
1128 // make a new magic number since things are out of sync
1129 do {
1130 magic = mRandom.nextLong();
1131 } while (magic == 0);
1132 if (bitmap != null) {
1133 saveMiniThumbToFile(bitmap, id, magic);
1134 bitmap.recycle();
1135 }
1136
1137 synchronized (c) {
1138 c.moveToPosition(i);
1139 c.updateLong(indexMiniThumbId(), magic);
1140 c.commitUpdates();
1141 c.requery();
1142 c.moveToPosition(i);
1143
1144 if (existingImage != null) {
1145 existingImage.mMiniThumbMagic = magic;
1146 }
1147 return magic;
1148 }
1149 } finally {
1150 mLock.unlock();
1151 }
1152 }
1153
1154 public void checkThumbnails(ThumbCheckCallback cb) {
1155 Cursor c = Images.Media.query(
1156 mContentResolver,
1157 mBaseUri,
1158 new String[] { "_id", "mini_thumb_magic" },
1159 "mini_thumb_magic isnull and " + sWhereClause,
1160 sAcceptableImageTypes,
1161 "_id ASC");
1162
1163 int count = c.getCount();
1164 if (VERBOSE)
1165 Log.v(TAG, ">>>>>>>>>>> need to check " + c.getCount() + " rows");
1166
1167 c.close();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001168
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001169 if (!ImageManager.hasStorage()) {
1170 if (VERBOSE)
1171 Log.v(TAG, "bailing from the image checker thread -- no storage");
1172 return;
1173 }
1174
1175 String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1);
1176 File oldFile = new File(oldPath);
1177
1178 if (count == 0) {
1179 // now check that we have the right thumbs file
1180// Log.v(TAG, "count is zero but oldFile.exists() is " + oldFile.exists());
1181 if (!oldFile.exists()) {
1182 return;
1183 }
1184 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001185
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001186 c = getCursor();
1187 try {
1188 if (VERBOSE) Log.v(TAG, "checkThumbnails found " + c.getCount());
1189 int max = c.getCount();
1190 int current = 0;
1191 for (int i = 0; i < c.getCount(); i++) {
1192 try {
1193 checkThumbnail(null, c, i);
1194 } catch (Exception ex) {
1195 Log.e(TAG, "!!!!! failed to check thumbnail... was the sd card removed?");
1196 break;
1197 }
1198 if (cb != null) {
1199 if (!cb.checking(current, max)) {
1200 if (VERBOSE) Log.v(TAG, "got false from checking... break <<<<<<<<<<<<<<<<<<<<<<<<");
1201 break;
1202 }
1203 }
1204 current += 1;
1205 }
1206 } finally {
1207 if (VERBOSE) Log.v(TAG, "checkThumbnails existing after reaching count " + c.getCount());
1208 try {
1209 oldFile.delete();
1210 } catch (Exception ex) {
1211 // ignore
1212 }
1213 }
1214 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001215
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001216 public void commitChanges() {
1217 synchronized (mCursor) {
1218 mCursor.commitUpdates();
1219 requery();
1220 }
1221 }
1222 protected Uri contentUri(long id) {
1223 try {
1224 // does our uri already have an id (single image query)?
1225 // if so just return it
1226 long existingId = ContentUris.parseId(mBaseUri);
1227 if (existingId != id)
1228 Log.e(TAG, "id mismatch");
1229 return mBaseUri;
1230 } catch (NumberFormatException ex) {
1231 // otherwise tack on the id
1232 return ContentUris.withAppendedId(mBaseUri, id);
1233 }
1234 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001235
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001236 public void deactivate() {
1237 mCursorDeactivated = true;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001238 try {
1239 mCursor.deactivate();
1240 } catch (IllegalStateException e) {
1241 // IllegalStateException may be thrown if the cursor is stale.
1242 Log.e(TAG, "Caught exception while deactivating cursor.", e);
1243 }
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001244 if (mMiniThumbData != null) {
1245 try {
1246 mMiniThumbData.close();
1247 mMiniThumbData = null;
1248 } catch (IOException ex) {
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001249
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001250 }
1251 }
1252 }
1253
1254 public void dump(String msg) {
1255 int count = getCount();
1256 if (VERBOSE) Log.v(TAG, "dump ImageList (count is " + count + ") " + msg);
1257 for (int i = 0; i < count; i++) {
1258 IImage img = getImageAt(i);
1259 if (img == null)
1260 if (VERBOSE) Log.v(TAG, " " + i + ": " + "null");
1261 else
1262 if (VERBOSE) Log.v(TAG, " " + i + ": " + img.toString());
1263 }
1264 if (VERBOSE) Log.v(TAG, "end of dump container");
1265 }
1266 public int getCount() {
1267 Cursor c = getCursor();
1268 synchronized (c) {
1269 try {
1270 return c.getCount();
1271 } catch (Exception ex) {
1272 }
1273 return 0;
1274 }
1275 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001276
1277 public boolean isEmpty() {
1278 return getCount() == 0;
1279 }
1280
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001281 protected Cursor getCursor() {
1282 synchronized (mCursor) {
1283 if (mCursorDeactivated) {
1284 activateCursor();
1285 }
1286 return mCursor;
1287 }
1288 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001289
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001290 protected void activateCursor() {
1291 requery();
1292 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001293
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001294 public IImage getImageAt(int i) {
1295 Cursor c = getCursor();
1296 synchronized (c) {
1297 boolean moved;
1298 try {
1299 moved = c.moveToPosition(i);
1300 } catch (Exception ex) {
1301 return null;
1302 }
1303 if (moved) {
1304 try {
1305 long id = c.getLong(0);
1306 long miniThumbId = 0;
1307 int rotation = 0;
1308 if (indexMiniThumbId() != -1) {
1309 miniThumbId = c.getLong(indexMiniThumbId());
1310 }
1311 if (indexOrientation() != -1) {
1312 rotation = c.getInt(indexOrientation());
1313 }
1314 long timestamp = c.getLong(1);
1315 IImage img = mCache.get(id);
1316 if (img == null) {
1317 img = make(id, miniThumbId, mContentResolver, this, timestamp, i, rotation);
1318 mCache.put(id, img);
1319 }
1320 return img;
1321 } catch (Exception ex) {
1322 Log.e(TAG, "got this exception trying to create image object: " + ex);
1323 return null;
1324 }
1325 } else {
1326 Log.e(TAG, "unable to moveTo to " + i + "; count is " + c.getCount());
1327 return null;
1328 }
1329 }
1330 }
1331 public IImage getImageForUri(Uri uri) {
1332 // TODO make this a hash lookup
1333 for (int i = 0; i < getCount(); i++) {
1334 if (getImageAt(i).fullSizeImageUri().equals(uri)) {
1335 return getImageAt(i);
1336 }
1337 }
1338 return null;
1339 }
1340 private byte [] getMiniThumbFromFile(long id, byte [] data, long magicCheck) {
1341 RandomAccessFile r = miniThumbDataFile();
1342 if (r == null)
1343 return null;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001344
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001345 long pos = id * sBytesPerMiniThumb;
1346 RandomAccessFile f = r;
1347 synchronized (f) {
1348 try {
1349 f.seek(pos);
1350 if (f.readByte() == 1) {
1351 long magic = f.readLong();
1352 if (magic != magicCheck) {
1353 if (VERBOSE) Log.v(TAG, "for id " + id + "; magic: " + magic + "; magicCheck: " + magicCheck + " (fail)");
1354 return null;
1355 }
1356 int length = f.readInt();
1357 f.read(data, 0, length);
1358 return data;
1359 } else {
1360 return null;
1361 }
1362 } catch (IOException ex) {
1363 long fileLength;
1364 try {
1365 fileLength = f.length();
1366 } catch (IOException ex1) {
1367 fileLength = -1;
1368 }
1369 Log.e(TAG, "couldn't read thumbnail for " + id + "; " + ex.toString() + "; pos is " + pos + "; length is " + fileLength);
1370 return null;
1371 }
1372 }
1373 }
1374 protected int getRowFor(IImage imageObj) {
1375 Cursor c = getCursor();
1376 synchronized (c) {
1377 int index = 0;
1378 long targetId = imageObj.fullSizeImageId();
1379 if (c.moveToFirst()) {
1380 do {
1381 if (c.getLong(0) == targetId) {
1382 return index;
1383 }
1384 index += 1;
1385 } while (c.moveToNext());
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001386 }
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001387 return -1;
1388 }
1389 }
1390
1391 protected abstract int indexOrientation();
1392 protected abstract int indexDateTaken();
1393 protected abstract int indexDescription();
1394 protected abstract int indexMimeType();
1395 protected abstract int indexData();
1396 protected abstract int indexId();
1397 protected abstract int indexLatitude();
1398 protected abstract int indexLongitude();
1399 protected abstract int indexMiniThumbId();
1400 protected abstract int indexPicasaWeb();
1401 protected abstract int indexPrivate();
1402 protected abstract int indexTitle();
1403 protected abstract int indexDisplayName();
1404 protected abstract int indexThumbId();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001405
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001406 protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) {
1407 return null;
1408 }
1409
1410 protected abstract Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options);
1411
1412 public boolean removeImage(IImage image) {
1413 Cursor c = getCursor();
1414 synchronized (c) {
1415 /*
1416 * TODO: consider putting the image in a holding area so
1417 * we can get it back as needed
1418 * TODO: need to delete the thumbnails as well
1419 */
1420 boolean moved;
1421 try {
1422 moved = c.moveToPosition(image.getRow());
1423 } catch (Exception ex) {
1424 Log.e(TAG, "removeImage got exception " + ex.toString());
1425 return false;
1426 }
1427 if (moved) {
1428 Uri u = image.fullSizeImageUri();
1429 mContentResolver.delete(u, null, null);
1430 image.onRemove();
1431 requery();
1432 }
1433 }
1434 return false;
1435 }
1436
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001437
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001438 /* (non-Javadoc)
1439 * @see com.android.camera.IImageList#removeImageAt(int)
1440 */
1441 public void removeImageAt(int i) {
1442 Cursor c = getCursor();
1443 synchronized (c) {
1444 /*
1445 * TODO: consider putting the image in a holding area so
1446 * we can get it back as needed
1447 * TODO: need to delete the thumbnails as well
1448 */
1449 dump("before delete");
1450 IImage image = getImageAt(i);
1451 boolean moved;
1452 try {
1453 moved = c.moveToPosition(i);
1454 } catch (Exception ex) {
1455 return;
1456 }
1457 if (moved) {
1458 Uri u = image.fullSizeImageUri();
1459 mContentResolver.delete(u, null, null);
1460 requery();
1461 image.onRemove();
1462 }
1463 dump("after delete");
1464 }
1465 }
1466
1467 public void removeOnChangeListener(OnChange changeCallback) {
1468 if (changeCallback == mListener)
1469 mListener = null;
1470 }
1471
1472 protected void requery() {
1473 mCache.clear();
1474 mCursor.requery();
1475 mCursorDeactivated = false;
1476 }
1477
1478 protected void saveMiniThumbToFile(Bitmap source, long id, long magic) {
1479 RandomAccessFile r = miniThumbDataFile();
1480 if (r == null)
1481 return;
1482
1483 long pos = id * sBytesPerMiniThumb;
1484 long t0 = System.currentTimeMillis();
1485 synchronized (r) {
1486 try {
1487 long t1 = System.currentTimeMillis();
1488 byte [] data = miniThumbData(source);
1489 long t2 = System.currentTimeMillis();
1490 if (data != null) {
1491 if (data.length > sBytesPerMiniThumb) {
1492 if (VERBOSE) Log.v(TAG, "!!!!!!!!!!!!!!!!!!!!!!!!!!! " + data.length + " > " + sBytesPerMiniThumb);
1493 return;
1494 }
1495 r.seek(pos);
1496 r.writeByte(0); // we have no data in this slot
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001497
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001498 // if magic is 0 then leave it alone
1499 if (magic == 0)
1500 r.skipBytes(8);
1501 else
1502 r.writeLong(magic);
1503 r.writeInt(data.length);
1504 r.write(data);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001505 // f.flush();
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001506 r.seek(pos);
1507 r.writeByte(1); // we have data in this slot
1508 long t3 = System.currentTimeMillis();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001509
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001510 if (VERBOSE) Log.v(TAG, "saveMiniThumbToFile took " + (t3-t0) + "; " + (t1-t0) + " " + (t2-t1) + " " + (t3-t2));
1511 }
1512 } catch (IOException ex) {
1513 Log.e(TAG, "couldn't save mini thumbnail data for " + id + "; " + ex.toString());
1514 }
1515 }
1516 }
1517
1518 public void setOnChangeListener(OnChange changeCallback, Handler h) {
1519 mListener = changeCallback;
1520 mHandler = h;
1521 }
1522 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001523
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001524 public class CanceledException extends Exception {
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001525
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001526 }
1527 public enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL }
1528
1529 public interface IAddImage_cancelable extends ICancelable {
1530 public void get();
1531 }
1532
1533 /*
1534 * The model for canceling an in-progress image save is this. For any
1535 * given part of the task of saving return an ICancelable. The "result"
1536 * from an ICancelable can be retrieved using the get* method. If the
1537 * operation was canceled then null is returned. The act of canceling
1538 * is to call "cancel" -- from another thread.
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001539 *
1540 * In general an object which implements ICancelable will need to
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001541 * check, periodically, whether they are canceled or not. This works
1542 * well for some things and less well for others.
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001543 *
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001544 * Right now the actual jpeg encode does not check cancelation but
1545 * the part of encoding which writes the data to disk does. Note,
1546 * though, that there is what appears to be a bug in the jpeg encoder
1547 * in that if the stream that's being written is closed it crashes
1548 * rather than returning an error. TODO fix that.
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001549 *
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001550 * When an object detects that it is canceling it must, before exiting,
1551 * call acknowledgeCancel. This is necessary because the caller of
1552 * cancel() will block until acknowledgeCancel is called.
1553 */
1554 public interface ICancelable {
1555 /*
1556 * call cancel() when the unit of work in progress needs to be
1557 * canceled. This should return true if it was possible to
1558 * cancel and false otherwise. If this returns false the caller
1559 * may still be able to cleanup and simulate cancelation.
1560 */
1561 public boolean cancel();
1562 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001563
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001564 public interface IGetBitmap_cancelable extends ICancelable {
1565 // returns the bitmap or null if there was an error or we were canceled
1566 public Bitmap get();
1567 };
1568 public interface IGetBoolean_cancelable extends ICancelable {
1569 public boolean get();
1570 }
1571 public interface IImage {
1572
1573 public abstract void commitChanges();
1574
1575 /**
1576 * Get the bitmap for the full size image.
1577 * @return the bitmap for the full size image.
1578 */
1579 public abstract Bitmap fullSizeBitmap(int targetWidthOrHeight);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001580
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001581 /**
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001582 *
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001583 * @return an object which can be canceled while the bitmap is loading
1584 */
1585 public abstract IGetBitmap_cancelable fullSizeBitmap_cancelable(int targetWidthOrHeight);
1586
1587 /**
1588 * Gets the input stream associated with a given full size image.
1589 * This is used, for example, if one wants to email or upload
1590 * the image.
1591 * @return the InputStream associated with the image.
1592 */
1593 public abstract InputStream fullSizeImageData();
1594 public abstract long fullSizeImageId();
1595 public abstract Uri fullSizeImageUri();
1596 public abstract IImageList getContainer();
1597 public abstract long getDateTaken();
1598
1599 /**
1600 * Gets the description of the image.
1601 * @return the description of the image.
1602 */
1603 public abstract String getDescription();
1604 public abstract String getMimeType();
1605 public abstract int getHeight();
1606
1607 /**
1608 * Gets the flag telling whether this video/photo is private or public.
1609 * @return the description of the image.
1610 */
1611 public abstract boolean getIsPrivate();
1612
1613 public abstract double getLatitude();
1614
1615 public abstract double getLongitude();
1616
1617 /**
1618 * Gets the name of the image.
1619 * @return the name of the image.
1620 */
1621 public abstract String getTitle();
1622
1623 public abstract String getDisplayName();
1624
1625 public abstract String getPicasaId();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001626
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001627 public abstract int getRow();
1628
1629 public abstract int getWidth();
1630
1631 public abstract boolean hasLatLong();
1632
1633 public abstract long imageId();
1634
1635 public abstract boolean isReadonly();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001636
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001637 public abstract boolean isDrm();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001638
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001639 public abstract Bitmap miniThumbBitmap();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001640
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001641 public abstract void onRemove();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001642
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001643 public abstract boolean rotateImageBy(int degrees);
1644
1645 /**
1646 * Sets the description of the image.
1647 */
1648 public abstract void setDescription(String description);
1649
1650 /**
1651 * Sets whether the video/photo is private or public.
1652 */
1653 public abstract void setIsPrivate(boolean isPrivate);
1654
1655 /**
1656 * Sets the name of the image.
1657 */
1658 public abstract void setName(String name);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001659
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001660 public abstract void setPicasaId(String id);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001661
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001662 /**
1663 * Get the bitmap for the medium thumbnail.
1664 * @return the bitmap for the medium thumbnail.
1665 */
1666 public abstract Bitmap thumbBitmap();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001667
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001668 public abstract Uri thumbUri();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001669
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001670 public abstract String getDataPath();
1671 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001672
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001673 public interface IImageList {
1674 public HashMap<String, String> getBucketIds();
1675
1676 public interface OnChange {
1677 public void onChange(IImageList list);
1678 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001679
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001680 public interface ThumbCheckCallback {
1681 public boolean checking(int current, int count);
1682 }
1683
1684 public abstract void checkThumbnails(ThumbCheckCallback cb);
1685
1686 public abstract void commitChanges();
1687
1688 public abstract void deactivate();
1689
1690 /**
1691 * Returns the count of image objects.
1692 *
1693 * @return the number of images
1694 */
1695 public abstract int getCount();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001696
1697 /**
1698 * @return true if the count of image objects is zero.
1699 */
1700
1701 public abstract boolean isEmpty();
1702
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001703 /**
1704 * Returns the image at the ith position.
1705 *
1706 * @param i the position
1707 * @return the image at the ith position
1708 */
1709 public abstract IImage getImageAt(int i);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001710
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001711 /**
1712 * Returns the image with a particular Uri.
1713 *
1714 * @param uri
1715 * @return the image with a particular Uri.
1716 */
1717 public abstract IImage getImageForUri(Uri uri);;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001718
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001719 public abstract boolean removeImage(IImage image);
1720 /**
1721 * Removes the image at the ith position.
1722 * @param i the position
1723 */
1724 public abstract void removeImageAt(int i);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001725
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001726 public abstract void removeOnChangeListener(OnChange changeCallback);
1727 public abstract void setOnChangeListener(OnChange changeCallback, Handler h);
1728 }
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001729
1730 class Image extends BaseImage implements IImage {
1731 int mRotation;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001732
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001733 protected Image(long id, long miniThumbId, ContentResolver cr, BaseImageList container, int cursorRow, int rotation) {
1734 super(id, miniThumbId, cr, container, cursorRow);
1735 mRotation = rotation;
1736 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001737
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001738 public String getDataPath() {
1739 String path = null;
1740 Cursor c = getCursor();
1741 synchronized (c) {
1742 if (c.moveToPosition(getRow())) {
1743 int column = ((ImageList)getContainer()).indexData();
1744 if (column >= 0)
1745 path = c.getString(column);
1746 }
1747 }
1748 return path;
1749 }
1750
1751 protected int getDegreesRotated() {
1752 return mRotation;
1753 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001754
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001755 protected void setDegreesRotated(int degrees) {
1756 Cursor c = getCursor();
1757 mRotation = degrees;
1758 synchronized (c) {
1759 if (c.moveToPosition(getRow())) {
1760 int column = ((ImageList)getContainer()).indexOrientation();
1761 if (column >= 0) {
1762 c.updateInt(column, degrees);
1763 getContainer().commitChanges();
1764 }
1765 }
1766 }
1767 }
1768
1769 protected Bitmap.CompressFormat compressionType() {
1770 String mimeType = getMimeType();
1771 if (mimeType == null)
1772 return Bitmap.CompressFormat.JPEG;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001773
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001774 if (mimeType.equals("image/png"))
1775 return Bitmap.CompressFormat.PNG;
1776 else if (mimeType.equals("image/png"))
1777 return Bitmap.CompressFormat.PNG;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001778
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001779 return Bitmap.CompressFormat.JPEG;
1780 }
1781
1782 /**
1783 * Does not replace the tag if already there. Otherwise, adds to the exif tags.
1784 * @param tag
1785 * @param value
1786 */
1787 public void addExifTag(String tag, String value) {
1788 if (mExifData == null) {
1789 mExifData = new HashMap<String, String>();
1790 }
1791 if (!mExifData.containsKey(tag)) {
1792 mExifData.put(tag, value);
1793 } else {
1794 if (VERBOSE) Log.v(TAG, "addExifTag where the key already was there: " + tag + " = " + value);
1795 }
1796 }
1797
1798 /**
1799 * Return the value of the Exif tag as an int. Returns 0 on any type of error.
1800 * @param tag
1801 * @return
1802 */
1803 public int getExifTagInt(String tag) {
1804 if (mExifData != null) {
1805 String tagValue = mExifData.get(tag);
1806 if (tagValue != null) {
1807 return Integer.parseInt(tagValue);
1808 }
1809 }
1810 return 0;
1811 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001812
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001813 public boolean isReadonly() {
1814 String mimeType = getMimeType();
1815 return !"image/jpeg".equals(mimeType) && !"image/png".equals(mimeType);
1816 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001817
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001818 public boolean isDrm() {
1819 return false;
1820 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001821
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001822 /**
1823 * Remove tag if already there. Otherwise, does nothing.
1824 * @param tag
1825 */
1826 public void removeExifTag(String tag) {
1827 if (mExifData == null) {
1828 mExifData = new HashMap<String, String>();
1829 }
1830 mExifData.remove(tag);
1831 }
1832
1833 /**
1834 * Replaces the tag if already there. Otherwise, adds to the exif tags.
1835 * @param tag
1836 * @param value
1837 */
1838 public void replaceExifTag(String tag, String value) {
1839 if (mExifData == null) {
1840 mExifData = new HashMap<String, String>();
1841 }
1842 if (!mExifData.containsKey(tag)) {
1843 mExifData.remove(tag);
1844 }
1845 mExifData.put(tag, value);
1846 }
1847
1848 /* (non-Javadoc)
1849 * @see com.android.camera.IImage#saveModifiedImage(android.graphics.Bitmap)
1850 */
1851 public IGetBoolean_cancelable saveImageContents(
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001852 final Bitmap image,
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001853 final byte [] jpegData,
1854 final int orientation,
1855 final boolean newFile,
1856 final Cursor cursor) {
1857 final class SaveImageContentsCancelable extends BaseCancelable implements IGetBoolean_cancelable {
1858 IGetBoolean_cancelable mCurrentCancelable = null;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001859
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001860 SaveImageContentsCancelable() {
1861 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001862
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001863 public boolean doCancelWork() {
1864 synchronized (this) {
1865 if (mCurrentCancelable != null)
1866 mCurrentCancelable.cancel();
1867 }
1868 return true;
1869 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001870
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001871 public boolean get() {
1872 try {
1873 Bitmap thumbnail = null;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001874
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001875 long t1 = System.currentTimeMillis();
1876 Uri uri = mContainer.contentUri(mId);
1877 synchronized (this) {
1878 checkCanceled();
1879 mCurrentCancelable = compressImageToFile(image, jpegData, uri);
1880 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001881
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001882 long t2 = System.currentTimeMillis();
1883 if (!mCurrentCancelable.get())
1884 return false;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001885
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001886 synchronized (this) {
1887 String filePath;
1888 synchronized (cursor) {
1889 cursor.moveToPosition(0);
1890 filePath = cursor.getString(2);
1891 }
1892 // TODO: If thumbData is present and usable, we should call the version
1893 // of storeThumbnail which takes a byte array, rather than re-encoding
1894 // a new JPEG of the same dimensions.
1895 byte [] thumbData = null;
1896 synchronized (ImageManager.instance()) {
1897 thumbData = (new ExifInterface(filePath)).getThumbnail();
1898 }
1899 if (VERBOSE) Log.v(TAG, "for file " + filePath + " thumbData is " + thumbData + "; length " + (thumbData!=null ? thumbData.length : -1));
1900 if (thumbData != null) {
1901 thumbnail = BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length);
1902 if (VERBOSE) Log.v(TAG, "embedded thumbnail bitmap " + thumbnail.getWidth() + "/" + thumbnail.getHeight());
1903 }
1904 if (thumbnail == null && image != null) {
1905 thumbnail = image;
1906 }
1907 if (thumbnail == null && jpegData != null) {
1908 thumbnail = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
1909 }
1910 }
1911
1912 long t3 = System.currentTimeMillis();
1913 mContainer.storeThumbnail(thumbnail, Image.this.fullSizeImageId());
1914 long t4 = System.currentTimeMillis();
1915 checkCanceled();
1916 if (VERBOSE) Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>> rotating by " + orientation);
1917 saveMiniThumb(rotate(thumbnail, orientation));
1918 long t5 = System.currentTimeMillis();
1919 checkCanceled();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001920
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001921 if (VERBOSE) Log.v(TAG, String.format("Timing data %d %d %d %d", t2-t1, t3-t2, t4-t3, t5-t4));
1922 return true;
1923 } catch (CanceledException ex) {
1924 if (VERBOSE) Log.v(TAG, "got canceled... need to cleanup");
1925 return false;
1926 } finally {
1927 /*
1928 Cursor c = getCursor();
1929 synchronized (c) {
1930 if (c.moveTo(getRow())) {
1931 mContainer.requery();
1932 }
1933 }
1934 */
1935 acknowledgeCancel();
1936 }
1937 }
1938 }
1939 return new SaveImageContentsCancelable();
1940 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001941
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001942 private void setExifRotation(int degrees) {
1943 try {
1944 Cursor c = getCursor();
1945 String filePath;
1946 synchronized (c) {
1947 filePath = c.getString(mContainer.indexData());
1948 }
1949 synchronized (ImageManager.instance()) {
1950 ExifInterface exif = new ExifInterface(filePath);
1951 if (mExifData == null) {
1952 mExifData = exif.getAttributes();
1953 }
1954 if (degrees < 0)
1955 degrees += 360;
1956
1957 int orientation = ExifInterface.ORIENTATION_NORMAL;
1958 switch (degrees) {
1959 case 0:
1960 orientation = ExifInterface.ORIENTATION_NORMAL;
1961 break;
1962 case 90:
1963 orientation = ExifInterface.ORIENTATION_ROTATE_90;
1964 break;
1965 case 180:
1966 orientation = ExifInterface.ORIENTATION_ROTATE_180;
1967 break;
1968 case 270:
1969 orientation = ExifInterface.ORIENTATION_ROTATE_270;
1970 break;
1971 }
1972
1973 replaceExifTag(ExifInterface.TAG_ORIENTATION, Integer.toString(orientation));
1974 replaceExifTag("UserComment", "saveRotatedImage comment orientation: " + orientation);
1975 exif.saveAttributes(mExifData);
1976 exif.commitChanges();
1977 }
1978 } catch (Exception ex) {
1979 Log.e(TAG, "unable to save exif data with new orientation " + fullSizeImageUri());
1980 }
1981 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08001982
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001983 /**
1984 * Save the rotated image by updating the Exif "Orientation" tag.
1985 * @param degrees
1986 * @return
1987 */
1988 public boolean rotateImageBy(int degrees) {
1989 int newDegrees = getDegreesRotated() + degrees;
1990 setExifRotation(newDegrees);
1991 setDegreesRotated(newDegrees);
1992
1993 // setting this to zero will force the call to checkCursor to generate fresh thumbs
1994 mMiniThumbMagic = 0;
1995 Cursor c = mContainer.getCursor();
1996 synchronized (c) {
1997 mContainer.checkThumbnail(this, mContainer.getCursor(), this.getRow());
1998 }
1999
2000 return true;
2001 }
2002
2003 public Bitmap thumbBitmap() {
2004 Bitmap bitmap = null;
2005 Cursor c = null;
2006 if (mContainer.mThumbUri != null) {
2007 try {
2008 c = mContentResolver.query(
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002009 mContainer.mThumbUri,
2010 THUMB_PROJECTION,
2011 Thumbnails.IMAGE_ID + "=?",
2012 new String[] { String.valueOf(fullSizeImageId()) },
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002013 null);
2014 if (c != null && c.moveToFirst()) {
2015 Uri thumbUri = ContentUris.withAppendedId(mContainer.mThumbUri, c.getLong(((ImageList)mContainer).INDEX_THUMB_ID));
2016 ParcelFileDescriptor pfdInput;
2017 try {
2018 BitmapFactory.Options options = new BitmapFactory.Options();
2019 options.inDither = false;
2020 options.inPreferredConfig = Bitmap.Config.ARGB_8888;
2021 pfdInput = mContentResolver.openFileDescriptor(thumbUri, "r");
2022 bitmap = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
2023 pfdInput.close();
2024 } catch (FileNotFoundException ex) {
2025 Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
2026 } catch (IOException ex) {
2027 Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
2028 } catch (NullPointerException ex) {
2029 // we seem to get this if the file doesn't exist anymore
2030 Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
2031 }
2032 }
2033 } catch (Exception ex) {
2034 // sdcard removed?
2035 return null;
2036 } finally {
2037 if (c != null)
2038 c.close();
2039 }
2040 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002041
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002042 if (bitmap == null) {
2043 bitmap = fullSizeBitmap(THUMBNAIL_TARGET_SIZE, false);
2044 if (VERBOSE) {
2045 Log.v(TAG, "no thumbnail found... storing new one for " + fullSizeImageId());
2046 }
2047 bitmap = mContainer.storeThumbnail(bitmap, fullSizeImageId());
2048 }
2049
2050 if (bitmap != null) {
2051 int degrees = getDegreesRotated();
2052 if (degrees != 0) {
2053 Matrix m = new Matrix();
2054 m.setRotate(degrees, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2);
2055 bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),
2056 m, true);
2057 }
2058 }
2059
2060 long elapsed = System.currentTimeMillis();
2061 return bitmap;
2062 }
2063
2064 }
2065
2066 final static private String sWhereClause = "(" + Images.Media.MIME_TYPE + "=? or " + Images.Media.MIME_TYPE + "=?" + ")";
2067 final static private String[] sAcceptableImageTypes = new String[] { "image/jpeg", "image/png" };
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002068
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002069 private static final String[] IMAGE_PROJECTION = new String[] {
2070 "_id",
2071 "_data",
2072 ImageColumns.DATE_TAKEN,
2073 ImageColumns.MINI_THUMB_MAGIC,
2074 ImageColumns.ORIENTATION,
2075 ImageColumns.MIME_TYPE
2076 };
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002077
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002078 /**
2079 * Represents an ordered collection of Image objects.
2080 * Provides an api to add and remove an image.
2081 */
2082 class ImageList extends BaseImageList implements IImageList {
2083 final int INDEX_ID = indexOf(IMAGE_PROJECTION, "_id");
2084 final int INDEX_DATA = indexOf(IMAGE_PROJECTION, "_data");
2085 final int INDEX_MIME_TYPE = indexOf(IMAGE_PROJECTION, MediaColumns.MIME_TYPE);
2086 final int INDEX_DATE_TAKEN = indexOf(IMAGE_PROJECTION, ImageColumns.DATE_TAKEN);
2087 final int INDEX_MINI_THUMB_MAGIC = indexOf(IMAGE_PROJECTION, ImageColumns.MINI_THUMB_MAGIC);
2088 final int INDEX_ORIENTATION = indexOf(IMAGE_PROJECTION, ImageColumns.ORIENTATION);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002089
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002090 final int INDEX_THUMB_ID = indexOf(THUMB_PROJECTION, BaseColumns._ID);
2091 final int INDEX_THUMB_IMAGE_ID = indexOf(THUMB_PROJECTION, Images.Thumbnails.IMAGE_ID);
2092 final int INDEX_THUMB_WIDTH = indexOf(THUMB_PROJECTION, Images.Thumbnails.WIDTH);
2093 final int INDEX_THUMB_HEIGHT = indexOf(THUMB_PROJECTION, Images.Thumbnails.HEIGHT);
2094
2095 boolean mIsRegistered = false;
2096 ContentObserver mContentObserver;
2097 DataSetObserver mDataSetObserver;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002098
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002099 public HashMap<String, String> getBucketIds() {
2100 Cursor c = Images.Media.query(
2101 mContentResolver,
2102 mBaseUri.buildUpon().appendQueryParameter("distinct", "true").build(),
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002103 new String[] {
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002104 ImageColumns.BUCKET_DISPLAY_NAME,
2105 ImageColumns.BUCKET_ID
2106 },
2107 whereClause(),
2108 whereClauseArgs(),
2109 sortOrder());
2110
2111 HashMap<String, String> hash = new HashMap<String, String>();
2112 if (c != null && c.moveToFirst()) {
2113 do {
2114 hash.put(c.getString(1), c.getString(0));
2115 } while (c.moveToNext());
2116 }
2117 return hash;
2118 }
2119 /**
2120 * ImageList constructor.
2121 * @param cr ContentResolver
2122 */
2123 public ImageList(Context ctx, ContentResolver cr, Uri imageUri, Uri thumbUri, int sort, String bucketId) {
2124 super(ctx, cr, imageUri, sort, bucketId);
2125 mBaseUri = imageUri;
2126 mThumbUri = thumbUri;
2127 mSort = sort;
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002128
2129 mContentResolver = cr;
2130
2131 mCursor = createCursor();
2132 if (mCursor == null) {
2133 Log.e(TAG, "unable to create image cursor for " + mBaseUri);
2134 throw new UnsupportedOperationException();
2135 }
2136
2137 if (VERBOSE) {
2138 Log.v(TAG, "for " + mBaseUri.toString() + " got cursor " + mCursor + " with length " + (mCursor != null ? mCursor.getCount() : "-1"));
2139 }
2140
2141 final Runnable updateRunnable = new Runnable() {
2142 public void run() {
2143 // handling these external updates is causing ANR problems that are unresolved.
2144 // For now ignore them since there shouldn't be anyone modifying the database on the fly.
2145 if (true)
2146 return;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002147
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002148 synchronized (mCursor) {
2149 requery();
2150 }
2151 if (mListener != null)
2152 mListener.onChange(ImageList.this);
2153 }
2154 };
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002155
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002156 mContentObserver = new ContentObserver(null) {
2157 @Override
2158 public boolean deliverSelfNotifications() {
2159 return false;
2160 }
2161
2162 @Override
2163 public void onChange(boolean selfChange) {
2164 if (VERBOSE) Log.v(TAG, "MyContentObserver.onChange; selfChange == " + selfChange);
2165 updateRunnable.run();
2166 }
2167 };
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002168
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002169 mDataSetObserver = new DataSetObserver() {
2170 @Override
2171 public void onChanged() {
2172 if (VERBOSE) Log.v(TAG, "MyDataSetObserver.onChanged");
2173// updateRunnable.run();
2174 }
2175
2176 @Override
2177 public void onInvalidated() {
2178 if (VERBOSE) Log.v(TAG, "MyDataSetObserver.onInvalidated: " + mCursorDeactivated);
2179 }
2180 };
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002181
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002182 registerObservers();
2183 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002184
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002185 private void registerObservers() {
2186 if (mIsRegistered)
2187 return;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002188
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002189 mCursor.registerContentObserver(mContentObserver);
2190 mCursor.registerDataSetObserver(mDataSetObserver);
2191 mIsRegistered = true;
2192 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002193
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002194 private void unregisterObservers() {
2195 if (!mIsRegistered)
2196 return;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002197
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002198 mCursor.unregisterContentObserver(mContentObserver);
2199 mCursor.unregisterDataSetObserver(mDataSetObserver);
2200 mIsRegistered = false;
2201 }
2202
2203 public void deactivate() {
2204 super.deactivate();
2205 unregisterObservers();
2206 }
2207
2208 protected void activateCursor() {
2209 super.activateCursor();
2210 registerObservers();
2211 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002212
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002213 protected String whereClause() {
2214 if (mBucketId != null) {
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002215 return sWhereClause + " and " + Images.Media.BUCKET_ID + " = '" + mBucketId + "'";
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002216 } else {
2217 return sWhereClause;
2218 }
2219 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002220
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002221 protected String[] whereClauseArgs() {
2222 return sAcceptableImageTypes;
2223 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002224
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002225 protected Cursor createCursor() {
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002226 Cursor c =
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002227 Images.Media.query(
2228 mContentResolver,
2229 mBaseUri,
2230 IMAGE_PROJECTION,
2231 whereClause(),
2232 whereClauseArgs(),
2233 sortOrder());
2234 if (VERBOSE)
2235 Log.v(TAG, "createCursor got cursor with count " + (c == null ? -1 : c.getCount()));
2236 return c;
2237 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002238
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002239 protected int indexOrientation() { return INDEX_ORIENTATION; }
2240 protected int indexDateTaken() { return INDEX_DATE_TAKEN; }
2241 protected int indexDescription() { return -1; }
2242 protected int indexMimeType() { return INDEX_MIME_TYPE; }
2243 protected int indexData() { return INDEX_DATA; }
2244 protected int indexId() { return INDEX_ID; }
2245 protected int indexLatitude() { return -1; }
2246 protected int indexLongitude() { return -1; }
2247 protected int indexMiniThumbId() { return INDEX_MINI_THUMB_MAGIC; }
2248
2249 protected int indexPicasaWeb() { return -1; }
2250 protected int indexPrivate() { return -1; }
2251 protected int indexTitle() { return -1; }
2252 protected int indexDisplayName() { return -1; }
2253 protected int indexThumbId() { return INDEX_THUMB_ID; }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002254
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002255 protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) {
2256 return new Image(id, miniThumbId, mContentResolver, this, index, rotation);
2257 }
2258
2259 protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfd, BitmapFactory.Options options) {
2260 Bitmap b = null;
2261
2262 try {
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002263 if (pfd == null)
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002264 pfd = makeInputStream(uri);
2265
2266 if (pfd == null)
2267 return null;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002268
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002269 if (options == null)
2270 options = new BitmapFactory.Options();
2271
2272 java.io.FileDescriptor fd = pfd.getFileDescriptor();
2273 options.inSampleSize = 1;
2274 if (targetWidthHeight != -1) {
2275 options.inJustDecodeBounds = true;
2276 long t1 = System.currentTimeMillis();
2277 BitmapFactory.decodeFileDescriptor(fd, null, options);
2278 long t2 = System.currentTimeMillis();
2279 if (options.mCancel || options.outWidth == -1 || options.outHeight == -1) {
2280 return null;
2281 }
2282 options.inSampleSize = computeSampleSize(options, targetWidthHeight);
2283 options.inJustDecodeBounds = false;
2284 }
2285
2286 options.inDither = false;
2287 options.inPreferredConfig = Bitmap.Config.ARGB_8888;
2288 long t1 = System.currentTimeMillis();
2289 b = BitmapFactory.decodeFileDescriptor(fd, null, options);
2290 long t2 = System.currentTimeMillis();
2291 if (VERBOSE) {
2292 Log.v(TAG, "A: got bitmap " + b + " with sampleSize " + options.inSampleSize + " took " + (t2-t1));
2293 }
2294 pfd.close();
2295 } catch (IOException ex) {
2296 if (VERBOSE) Log.v(TAG, "got io exception " + ex);
2297 return null;
2298 }
2299 return b;
2300 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002301
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002302 private ParcelFileDescriptor makeInputStream(Uri uri) {
2303 try {
2304 return mContentResolver.openFileDescriptor(uri, "r");
2305 } catch (IOException ex) {
2306 return null;
2307 }
2308 }
2309
2310 private String sortOrder() {
2311 // add id to the end so that we don't ever get random sorting
2312 // which could happen, I suppose, if the first two values were
2313 // duplicated
2314 String ascending = (mSort == SORT_ASCENDING ? " ASC" : " DESC");
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002315 return
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002316 Images.Media.DATE_TAKEN + ascending + "," +
2317 Images.Media._ID + ascending;
2318 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002319
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002320 }
2321
2322 /**
2323 * Represents an ordered collection of Image objects from the DRM provider.
2324 */
2325 class DrmImageList extends ImageList implements IImageList {
2326 private final String[] DRM_IMAGE_PROJECTION = new String[] {
2327 DrmStore.Audio._ID,
2328 DrmStore.Audio.DATA,
2329 DrmStore.Audio.MIME_TYPE,
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002330 };
2331
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002332 final int INDEX_ID = indexOf(DRM_IMAGE_PROJECTION, DrmStore.Audio._ID);
2333 final int INDEX_MIME_TYPE = indexOf(DRM_IMAGE_PROJECTION, DrmStore.Audio.MIME_TYPE);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002334
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002335 public DrmImageList(Context ctx, ContentResolver cr, Uri imageUri, int sort, String bucketId) {
2336 super(ctx, cr, imageUri, null, sort, bucketId);
2337 }
2338
2339 protected Cursor createCursor() {
2340 return mContentResolver.query(mBaseUri, DRM_IMAGE_PROJECTION, null, null, sortOrder());
2341 }
2342
2343 @Override
2344 public void checkThumbnails(ThumbCheckCallback cb) {
2345 // do nothing
2346 }
2347
2348 @Override
2349 public long checkThumbnail(BaseImage existingImage, Cursor c, int i) {
2350 return 0;
2351 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002352
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002353 class DrmImage extends Image {
2354 protected DrmImage(long id, ContentResolver cr, BaseImageList container, int cursorRow) {
2355 super(id, 0, cr, container, cursorRow, 0);
2356 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002357
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002358 public boolean isDrm() {
2359 return true;
2360 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002361
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002362 public boolean isReadonly() {
2363 return true;
2364 }
2365
2366 public Bitmap miniThumbBitmap() {
2367 return fullSizeBitmap(MINI_THUMB_TARGET_SIZE);
2368 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002369
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002370 public Bitmap thumbBitmap() {
2371 return fullSizeBitmap(THUMBNAIL_TARGET_SIZE);
2372 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002373
2374 public String getDisplayName() {
2375 return getTitle();
2376 }
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002377 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002378
2379 @Override
2380 protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) {
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002381 return new DrmImage(id, mContentResolver, this, index);
2382 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002383
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002384 protected int indexOrientation() { return -1; }
2385 protected int indexDateTaken() { return -1; }
2386 protected int indexDescription() { return -1; }
2387 protected int indexMimeType() { return -1; }
2388 protected int indexId() { return -1; }
2389 protected int indexLatitude() { return -1; }
2390 protected int indexLongitude() { return -1; }
2391 protected int indexMiniThumbId() { return -1; }
2392 protected int indexPicasaWeb() { return -1; }
2393 protected int indexPrivate() { return -1; }
2394 protected int indexTitle() { return -1; }
2395 protected int indexDisplayName() { return -1; }
2396 protected int indexThumbId() { return -1; }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002397
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002398 // TODO review this probably should be based on DATE_TAKEN same as images
2399 private String sortOrder() {
2400 String ascending = (mSort == SORT_ASCENDING ? " ASC" : " DESC");
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002401 return
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002402 DrmStore.Images.TITLE + ascending + "," +
2403 DrmStore.Images._ID;
2404 }
2405 }
2406
2407 class ImageListUber implements IImageList {
2408 private IImageList [] mSubList;
2409 private int mSort;
2410 private IImageList.OnChange mListener = null;
2411 Handler mHandler;
2412
2413 // This is an array of Longs wherein each Long consists of
2414 // two components. The first component indicates the number of
2415 // consecutive entries that belong to a given sublist.
2416 // The second component indicates which sublist we're referring
2417 // to (an int which is used to index into mSubList).
2418 ArrayList<Long> mSkipList = null;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002419
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002420 int [] mSkipCounts = null;
2421
2422 public HashMap<String, String> getBucketIds() {
2423 HashMap<String, String> hashMap = new HashMap<String, String>();
2424 for (IImageList list: mSubList) {
2425 hashMap.putAll(list.getBucketIds());
2426 }
2427 return hashMap;
2428 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002429
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002430 public ImageListUber(IImageList [] sublist, int sort) {
2431 mSubList = sublist.clone();
2432 mSort = sort;
2433
2434 if (mListener != null) {
2435 for (IImageList list: sublist) {
2436 list.setOnChangeListener(new OnChange() {
2437 public void onChange(IImageList list) {
2438 if (mListener != null) {
2439 mListener.onChange(ImageListUber.this);
2440 }
2441 }
2442 }, mHandler);
2443 }
2444 }
2445 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002446
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002447 public void checkThumbnails(ThumbCheckCallback cb) {
2448 // TODO this isn't quite right because we need to get the
2449 // total from each sub item and provide that in the callback
2450 final IImageList sublist[] = mSubList;
2451 final int length = sublist.length;
2452 for (int i = 0; i < length; i++)
2453 sublist[i].checkThumbnails(cb);
2454 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002455
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002456 public void commitChanges() {
2457 final IImageList sublist[] = mSubList;
2458 final int length = sublist.length;
2459 for (int i = 0; i < length; i++)
2460 sublist[i].commitChanges();
2461 }
2462
2463 public void deactivate() {
2464 final IImageList sublist[] = mSubList;
2465 final int length = sublist.length;
2466 int pos = -1;
2467 while (++pos < length) {
2468 IImageList sub = sublist[pos];
2469 sub.deactivate();
2470 }
2471 }
2472
2473 public int getCount() {
2474 final IImageList sublist[] = mSubList;
2475 final int length = sublist.length;
2476 int count = 0;
2477 for (int i = 0; i < length; i++)
2478 count += sublist[i].getCount();
2479 return count;
2480 }
2481
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002482 public boolean isEmpty() {
2483 final IImageList sublist[] = mSubList;
2484 final int length = sublist.length;
2485 for (int i = 0; i < length; i++) {
2486 if (! sublist[i].isEmpty()) {
2487 return false;
2488 }
2489 }
2490 return true;
2491 }
2492
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002493 // mSkipCounts is used to tally the counts as we traverse
2494 // the mSkipList. It's a member variable only so that
2495 // we don't have to allocate each time through. Otherwise
2496 // it could just as easily be a local.
2497
2498 public synchronized IImage getImageAt(int index) {
2499 if (index < 0 || index > getCount())
2500 throw new IndexOutOfBoundsException("index " + index + " out of range max is " + getCount());
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002501
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002502 // first make sure our allocations are in order
2503 if (mSkipCounts == null || mSubList.length > mSkipCounts.length)
2504 mSkipCounts = new int[mSubList.length];
2505
2506 if (mSkipList == null)
2507 mSkipList = new ArrayList<Long>();
2508
2509 // zero out the mSkipCounts since that's only used for the
2510 // duration of the function call
2511 for (int i = 0; i < mSubList.length; i++)
2512 mSkipCounts[i] = 0;
2513
2514 // a counter of how many images we've skipped in
2515 // trying to get to index. alternatively we could
2516 // have decremented index but, alas, I liked this
2517 // way more.
2518 int skipCount = 0;
2519
2520 // scan the existing mSkipList to see if we've computed
2521 // enough to just return the answer
2522 for (int i = 0; i < mSkipList.size(); i++) {
2523 long v = mSkipList.get(i);
2524
2525 int offset = (int) (v & 0xFFFF);
2526 int which = (int) (v >> 32);
2527
2528 if (skipCount + offset > index) {
2529 int subindex = mSkipCounts[which] + (index - skipCount);
2530 IImage img = mSubList[which].getImageAt(subindex);
2531 return img;
2532 }
2533
2534 skipCount += offset;
2535 mSkipCounts[which] += offset;
2536 }
2537
2538 // if we get here we haven't computed the answer for
2539 // "index" yet so keep computing. This means running
2540 // through the list of images and either modifying the
2541 // last entry or creating a new one.
2542 long count = 0;
2543 while (true) {
2544 long maxTimestamp = mSort == SORT_ASCENDING ? Long.MAX_VALUE : Long.MIN_VALUE;
2545 int which = -1;
2546 for (int i = 0; i < mSubList.length; i++) {
2547 int pos = mSkipCounts[i];
2548 IImageList list = mSubList[i];
2549 if (pos < list.getCount()) {
2550 IImage image = list.getImageAt(pos);
2551 // this should never be null but sometimes the database is
2552 // causing problems and it is null
2553 if (image != null) {
2554 long timestamp = image.getDateTaken();
2555 if (mSort == SORT_ASCENDING ? (timestamp < maxTimestamp) : (timestamp > maxTimestamp)) {
2556 maxTimestamp = timestamp;
2557 which = i;
2558 }
2559 }
2560 }
2561 }
2562
2563 if (which == -1) {
2564 if (VERBOSE) Log.v(TAG, "which is -1, returning null");
2565 return null;
2566 }
2567
2568 boolean done = false;
2569 count = 1;
2570 if (mSkipList.size() > 0) {
2571 int pos = mSkipList.size() - 1;
2572 long oldEntry = mSkipList.get(pos);
2573 if ((oldEntry >> 32) == which) {
2574 long newEntry = oldEntry + 1;
2575 mSkipList.set(pos, newEntry);
2576 done = true;
2577 }
2578 }
2579 if (!done) {
2580 long newEntry = ((long)which << 32) | count;
2581 if (VERBOSE) {
2582 Log.v(TAG, "new entry is " + Long.toHexString(newEntry));
2583 }
2584 mSkipList.add(newEntry);
2585 }
2586
2587 if (skipCount++ == index) {
2588 return mSubList[which].getImageAt(mSkipCounts[which]);
2589 }
2590 mSkipCounts[which] += 1;
2591 }
2592 }
2593
2594 public IImage getImageForUri(Uri uri) {
2595 // TODO perhaps we can preflight the base of the uri
2596 // against each sublist first
2597 for (int i = 0; i < mSubList.length; i++) {
2598 IImage img = mSubList[i].getImageForUri(uri);
2599 if (img != null)
2600 return img;
2601 }
2602 return null;
2603 }
2604
2605 /**
2606 * Modify the skip list when an image is deleted by finding
2607 * the relevant entry in mSkipList and decrementing the
2608 * counter. This is simple because deletion can never
2609 * cause change the order of images.
2610 */
2611 public void modifySkipCountForDeletedImage(int index) {
2612 int skipCount = 0;
2613
2614 for (int i = 0; i < mSkipList.size(); i++) {
2615 long v = mSkipList.get(i);
2616
2617 int offset = (int) (v & 0xFFFF);
2618 int which = (int) (v >> 32);
2619
2620 if (skipCount + offset > index) {
2621 mSkipList.set(i, v-1);
2622 break;
2623 }
2624
2625 skipCount += offset;
2626 }
2627 }
2628
2629 public boolean removeImage(IImage image) {
2630 int pos = -1;
2631 while (++pos < mSubList.length) {
2632 IImageList sub = mSubList[pos];
2633 if (sub.removeImage(image)) {
2634 return true;
2635 }
2636 }
2637 return false;
2638 }
2639
2640 public void removeImageAt(int index) {
2641 IImage img = getImageAt(index);
2642 if (img != null) {
2643 IImageList list = img.getContainer();
2644 if (list != null) {
2645 list.removeImage(img);
2646 modifySkipCountForDeletedImage(index);
2647 }
2648 }
2649 }
2650
2651 public void removeOnChangeListener(OnChange changeCallback) {
2652 if (changeCallback == mListener)
2653 mListener = null;
2654 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002655
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002656 public void setOnChangeListener(OnChange changeCallback, Handler h) {
2657 mListener = changeCallback;
2658 mHandler = h;
2659 }
2660
2661 }
2662
2663 public static abstract class SimpleBaseImage implements IImage {
2664 public void commitChanges() {
2665 throw new UnsupportedOperationException();
2666 }
2667
2668 public InputStream fullSizeImageData() {
2669 throw new UnsupportedOperationException();
2670 }
2671
2672 public long fullSizeImageId() {
2673 return 0;
2674 }
2675
2676 public Uri fullSizeImageUri() {
2677 throw new UnsupportedOperationException();
2678 }
2679
2680 public IImageList getContainer() {
2681 return null;
2682 }
2683
2684 public long getDateTaken() {
2685 return 0;
2686 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002687
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002688 public String getMimeType() {
2689 throw new UnsupportedOperationException();
2690 }
2691
2692 public String getDescription() {
2693 throw new UnsupportedOperationException();
2694 }
2695
2696 public boolean getIsPrivate() {
2697 throw new UnsupportedOperationException();
2698 }
2699
2700 public double getLatitude() {
2701 return 0D;
2702 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002703
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002704 public double getLongitude() {
2705 return 0D;
2706 }
2707
2708 public String getTitle() {
2709 throw new UnsupportedOperationException();
2710 }
2711
2712 public String getDisplayName() {
2713 throw new UnsupportedOperationException();
2714 }
2715
2716 public String getPicasaId() {
2717 return null;
2718 }
2719
2720 public int getRow() {
2721 throw new UnsupportedOperationException();
2722 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002723
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002724 public int getHeight() {
2725 return 0;
2726 }
2727
2728 public int getWidth() {
2729 return 0;
2730 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002731
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002732 public boolean hasLatLong() {
2733 return false;
2734 }
2735
2736 public boolean isReadonly() {
2737 return true;
2738 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002739
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002740 public boolean isDrm() {
2741 return false;
2742 }
2743
2744 public void onRemove() {
2745 throw new UnsupportedOperationException();
2746 }
2747
2748 public boolean rotateImageBy(int degrees) {
2749 return false;
2750 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002751
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002752 public void setDescription(String description) {
2753 throw new UnsupportedOperationException();
2754 }
2755
2756 public void setIsPrivate(boolean isPrivate) {
2757 throw new UnsupportedOperationException();
2758 }
2759
2760 public void setName(String name) {
2761 throw new UnsupportedOperationException();
2762 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002763
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002764 public void setPicasaId(long id) {
2765 }
2766
2767 public void setPicasaId(String id) {
2768 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002769
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002770 public Uri thumbUri() {
2771 throw new UnsupportedOperationException();
2772 }
2773 }
2774
2775 class SingleImageList extends BaseImageList implements IImageList {
2776 private IImage mSingleImage;
2777 private ContentResolver mContentResolver;
2778 private Uri mUri;
2779
2780 class UriImage extends SimpleBaseImage {
2781
2782 UriImage() {
2783 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002784
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002785 public String getDataPath() {
2786 return mUri.getPath();
2787 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002788
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002789 InputStream getInputStream() {
2790 try {
2791 if (mUri.getScheme().equals("file")) {
2792 String path = mUri.getPath();
2793 if (VERBOSE)
2794 Log.v(TAG, "path is " + path);
2795 return new java.io.FileInputStream(mUri.getPath());
2796 } else {
2797 return mContentResolver.openInputStream(mUri);
2798 }
2799 } catch (FileNotFoundException ex) {
2800 return null;
2801 }
2802 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002803
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002804 ParcelFileDescriptor getPFD() {
2805 try {
2806 if (mUri.getScheme().equals("file")) {
2807 String path = mUri.getPath();
2808 if (VERBOSE)
2809 Log.v(TAG, "path is " + path);
2810 return ParcelFileDescriptor.open(new File(path), ParcelFileDescriptor.MODE_READ_ONLY);
2811 } else {
2812 return mContentResolver.openFileDescriptor(mUri, "r");
2813 }
2814 } catch (FileNotFoundException ex) {
2815 return null;
2816 }
2817 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002818
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002819 /* (non-Javadoc)
2820 * @see com.android.camera.ImageManager.IImage#fullSizeBitmap(int)
2821 */
2822 public Bitmap fullSizeBitmap(int targetWidthHeight) {
2823 try {
2824 ParcelFileDescriptor pfdInput = getPFD();
2825 BitmapFactory.Options options = new BitmapFactory.Options();
2826 options.inJustDecodeBounds = true;
2827 BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
2828
2829 if (targetWidthHeight != -1)
2830 options.inSampleSize = computeSampleSize(options, targetWidthHeight);
2831
2832 options.inJustDecodeBounds = false;
2833 options.inDither = false;
2834 options.inPreferredConfig = Bitmap.Config.ARGB_8888;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002835
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002836 Bitmap b = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
2837 if (VERBOSE) {
2838 Log.v(TAG, "B: got bitmap " + b + " with sampleSize " + options.inSampleSize);
2839 }
2840 pfdInput.close();
2841 return b;
2842 } catch (Exception ex) {
2843 Log.e(TAG, "got exception decoding bitmap " + ex.toString());
2844 return null;
2845 }
2846 }
2847
2848 public IGetBitmap_cancelable fullSizeBitmap_cancelable(final int targetWidthOrHeight) {
2849 final class LoadBitmapCancelable extends BaseCancelable implements IGetBitmap_cancelable {
2850 ParcelFileDescriptor pfdInput;
2851 BitmapFactory.Options mOptions = new BitmapFactory.Options();
2852 long mCancelInitiationTime;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002853
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002854 public LoadBitmapCancelable(ParcelFileDescriptor pfd) {
2855 pfdInput = pfd;
2856 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002857
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002858 public boolean doCancelWork() {
2859 if (VERBOSE)
2860 Log.v(TAG, "requesting bitmap load cancel");
2861 mCancelInitiationTime = System.currentTimeMillis();
2862 mOptions.requestCancelDecode();
2863 return true;
2864 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002865
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002866 public Bitmap get() {
2867 try {
2868 Bitmap b = makeBitmap(targetWidthOrHeight, fullSizeImageUri(), pfdInput, mOptions);
2869 if (b == null && mCancelInitiationTime != 0) {
2870 if (VERBOSE)
2871 Log.v(TAG, "cancel returned null bitmap -- took " + (System.currentTimeMillis()-mCancelInitiationTime));
2872 }
2873 if (VERBOSE) Log.v(TAG, "b is " + b);
2874 return b;
2875 } catch (Exception ex) {
2876 return null;
2877 } finally {
2878 acknowledgeCancel();
2879 }
2880 }
2881 }
2882
2883 try {
2884 ParcelFileDescriptor pfdInput = getPFD();
2885 if (pfdInput == null)
2886 return null;
2887 if (VERBOSE) Log.v(TAG, "inputStream is " + pfdInput);
2888 return new LoadBitmapCancelable(pfdInput);
2889 } catch (UnsupportedOperationException ex) {
2890 return null;
2891 }
2892 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002893
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002894 @Override
2895 public Uri fullSizeImageUri() {
2896 return mUri;
2897 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002898
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002899 @Override
2900 public InputStream fullSizeImageData() {
2901 return getInputStream();
2902 }
2903
2904 public long imageId() {
2905 return 0;
2906 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002907
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002908 public Bitmap miniThumbBitmap() {
2909 return thumbBitmap();
2910 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002911
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002912 @Override
2913 public String getTitle() {
2914 return mUri.toString();
2915 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002916
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002917 @Override
2918 public String getDisplayName() {
2919 return getTitle();
2920 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002921
2922 @Override
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002923 public String getDescription() {
2924 return "";
2925 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002926
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002927 public Bitmap thumbBitmap() {
2928 Bitmap b = fullSizeBitmap(THUMBNAIL_TARGET_SIZE);
2929 if (b != null) {
2930 Matrix m = new Matrix();
2931 float scale = Math.min(1F, THUMBNAIL_TARGET_SIZE / (float) b.getWidth());
2932 m.setScale(scale, scale);
2933 Bitmap scaledBitmap = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
2934 return scaledBitmap;
2935 } else {
2936 return null;
2937 }
2938 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002939
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002940 private BitmapFactory.Options snifBitmapOptions() {
2941 ParcelFileDescriptor input = getPFD();
2942 if (input == null)
2943 return null;
2944 try {
2945 Uri uri = fullSizeImageUri();
2946 BitmapFactory.Options options = new BitmapFactory.Options();
2947 options.inJustDecodeBounds = true;
2948 BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options);
2949 return options;
2950 } finally {
2951 try {
2952 if (input != null) {
2953 input.close();
2954 }
2955 } catch (IOException ex) {
2956 }
2957 }
2958 }
2959
2960 @Override
2961 public String getMimeType() {
2962 BitmapFactory.Options options = snifBitmapOptions();
2963 return (options!=null) ? options.outMimeType : "";
2964 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002965
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002966 @Override
2967 public int getHeight() {
2968 BitmapFactory.Options options = snifBitmapOptions();
2969 return (options!=null) ? options.outHeight : 0;
2970 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002971
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002972 @Override
2973 public int getWidth() {
2974 BitmapFactory.Options options = snifBitmapOptions();
2975 return (options!=null) ? options.outWidth : 0;
2976 }
2977 }
2978
2979 public SingleImageList(ContentResolver cr, Uri uri) {
2980 super(null, cr, uri, ImageManager.SORT_ASCENDING, null);
2981 mContentResolver = cr;
2982 mUri = uri;
2983 mSingleImage = new UriImage();
2984 }
2985
2986 public HashMap<String, String> getBucketIds() {
2987 throw new UnsupportedOperationException();
2988 }
2989
2990 public void deactivate() {
2991 // nothing to do here
2992 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002993
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07002994 public int getCount() {
2995 return 1;
2996 }
2997
The Android Open Source Project0eaec582008-12-17 18:05:54 -08002998 public boolean isEmpty() {
2999 return false;
3000 }
3001
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003002 public IImage getImageAt(int i) {
3003 if (i == 0)
3004 return mSingleImage;
3005
3006 return null;
3007 }
3008
3009 public IImage getImageForUri(Uri uri) {
3010 if (uri.equals(mUri))
3011 return mSingleImage;
3012 else
3013 return null;
3014 }
3015
3016 public IImage getImageWithId(long id) {
3017 throw new UnsupportedOperationException();
3018 }
3019
3020 @Override
3021 protected int indexOrientation() {
3022 return -1;
3023 }
3024
3025 @Override
3026 protected int indexDateTaken() {
3027 return -1;
3028 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003029
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003030 @Override
3031 protected int indexMimeType() {
3032 return -1;
3033 }
3034
3035 @Override
3036 protected int indexDescription() {
3037 return -1;
3038 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003039
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003040 @Override
3041 protected int indexId() {
3042 return -1;
3043 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003044
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003045 @Override
3046 protected int indexData() {
3047 return -1;
3048 }
3049
3050 @Override
3051 protected int indexLatitude() {
3052 return -1;
3053 }
3054
3055 @Override
3056 protected int indexLongitude() {
3057 return -1;
3058 }
3059
3060 @Override
3061 protected int indexMiniThumbId() {
3062 return -1;
3063 }
3064
3065 @Override
3066 protected int indexPicasaWeb() {
3067 return -1;
3068 }
3069
3070 @Override
3071 protected int indexPrivate() {
3072 return -1;
3073 }
3074
3075 @Override
3076 protected int indexTitle() {
3077 return -1;
3078 }
3079
3080 @Override
3081 protected int indexDisplayName() {
3082 return -1;
3083 }
3084
3085 @Override
3086 protected int indexThumbId() {
3087 return -1;
3088 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003089
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003090 private InputStream makeInputStream(Uri uri) {
3091 InputStream input = null;
3092 try {
3093 input = mContentResolver.openInputStream(uri);
3094 return input;
3095 } catch (IOException ex) {
3096 return null;
3097 }
3098 }
3099
3100 @Override
3101 protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options) {
3102 Bitmap b = null;
3103
3104 try {
3105 if (options == null)
3106 options = new BitmapFactory.Options();
3107 options.inSampleSize = 1;
3108
3109 if (targetWidthHeight != -1) {
3110 options.inJustDecodeBounds = true;
3111 BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
3112
3113 options.inSampleSize = computeSampleSize(options, targetWidthHeight);
3114 options.inJustDecodeBounds = false;
3115 }
3116 b = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
3117 if (VERBOSE) {
3118 Log.v(TAG, "C: got bitmap " + b + " with sampleSize " + options.inSampleSize);
3119 }
3120 pfdInput.close();
3121 } catch (IOException ex) {
3122 if (VERBOSE) Log.v(TAG, "got io exception " + ex);
3123 return null;
3124 }
3125 return b;
3126 }
3127 }
3128
3129 class ThreadSafeOutputStream extends OutputStream {
3130 java.io.OutputStream mDelegateStream;
3131 boolean mClosed;
3132
3133 public ThreadSafeOutputStream(OutputStream delegate) {
3134 mDelegateStream = delegate;
3135 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003136
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003137 @Override
3138 synchronized public void close() throws IOException {
3139 try {
3140 mClosed = true;
3141 mDelegateStream.close();
3142 } catch (IOException ex) {
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003143
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003144 }
3145 }
3146
3147 @Override
3148 synchronized public void flush() throws IOException {
3149 super.flush();
3150 }
3151
3152 @Override
3153 public void write(byte[] b, int offset, int length) throws IOException {
3154 /*
3155 mDelegateStream.write(b, offset, length);
3156 return;
3157 */
3158 while (length > 0) {
3159 synchronized (this) {
3160 if (mClosed)
3161 return;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003162
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003163 int writeLength = Math.min(8192, length);
3164 mDelegateStream.write(b, offset, writeLength);
3165 offset += writeLength;
3166 length -= writeLength;
3167 }
3168 }
3169 }
3170
3171 @Override
3172 synchronized public void write(int oneByte) throws IOException {
3173 if (mClosed)
3174 return;
3175 mDelegateStream.write(oneByte);
3176 }
3177 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003178
3179 class VideoList extends BaseImageList implements IImageList {
3180 private final String[] sProjection = new String[] {
3181 Video.Media._ID,
3182 Video.Media.DATA,
3183 Video.Media.DATE_TAKEN,
3184 Video.Media.TITLE,
3185 Video.Media.DISPLAY_NAME,
3186 Video.Media.DESCRIPTION,
3187 Video.Media.IS_PRIVATE,
3188 Video.Media.TAGS,
3189 Video.Media.CATEGORY,
3190 Video.Media.LANGUAGE,
3191 Video.Media.LATITUDE,
3192 Video.Media.LONGITUDE,
3193 Video.Media.MINI_THUMB_MAGIC,
3194 Video.Media.MIME_TYPE,
3195 };
3196
3197 final int INDEX_ID = indexOf(sProjection, Video.Media._ID);
3198 final int INDEX_DATA = indexOf(sProjection, Video.Media.DATA);
3199 final int INDEX_DATE_TAKEN = indexOf(sProjection, Video.Media.DATE_TAKEN);
3200 final int INDEX_TITLE = indexOf(sProjection, Video.Media.TITLE);
3201 final int INDEX_DISPLAY_NAME = indexOf(sProjection, Video.Media.DISPLAY_NAME);
3202 final int INDEX_MIME_TYPE = indexOf(sProjection, Video.Media.MIME_TYPE);
3203 final int INDEX_DESCRIPTION = indexOf(sProjection, Video.Media.DESCRIPTION);
3204 final int INDEX_PRIVATE = indexOf(sProjection, Video.Media.IS_PRIVATE);
3205 final int INDEX_TAGS = indexOf(sProjection, Video.Media.TAGS);
3206 final int INDEX_CATEGORY = indexOf(sProjection, Video.Media.CATEGORY);
3207 final int INDEX_LANGUAGE = indexOf(sProjection, Video.Media.LANGUAGE);
3208 final int INDEX_LATITUDE = indexOf(sProjection, Video.Media.LATITUDE);
3209 final int INDEX_LONGITUDE = indexOf(sProjection, Video.Media.LONGITUDE);
3210 final int INDEX_MINI_THUMB_MAGIC = indexOf(sProjection, Video.Media.MINI_THUMB_MAGIC);
3211 final int INDEX_THUMB_ID = indexOf(sProjection, BaseColumns._ID);
3212
3213 public VideoList(Context ctx, ContentResolver cr, Uri uri, Uri thumbUri,
3214 int sort, String bucketId) {
3215 super(ctx, cr, uri, sort, bucketId);
3216
3217 mCursor = createCursor();
3218 if (mCursor == null) {
3219 Log.e(TAG, "unable to create video cursor for " + mBaseUri);
3220 throw new UnsupportedOperationException();
3221 }
3222
3223 if (Config.LOGV) {
3224 Log.v(TAG, "for " + mUri.toString() + " got cursor " + mCursor + " with length "
3225 + (mCursor != null ? mCursor.getCount() : -1));
3226 }
3227
3228 if (mCursor == null) {
3229 throw new UnsupportedOperationException();
3230 }
3231 if (mCursor != null && mCursor.moveToFirst()) {
3232 int row = 0;
3233 do {
3234 long imageId = mCursor.getLong(indexId());
3235 long dateTaken = mCursor.getLong(indexDateTaken());
3236 long miniThumbId = mCursor.getLong(indexMiniThumbId());
3237 mCache.put(imageId, new VideoObject(imageId, miniThumbId, mContentResolver,
3238 this, dateTaken, row++));
3239 } while (mCursor.moveToNext());
3240 }
3241 }
3242
3243 public HashMap<String, String> getBucketIds() {
3244 Cursor c = Images.Media.query(
3245 mContentResolver,
3246 mBaseUri.buildUpon().appendQueryParameter("distinct", "true").build(),
3247 new String[] {
3248 VideoColumns.BUCKET_DISPLAY_NAME,
3249 VideoColumns.BUCKET_ID
3250 },
3251 whereClause(),
3252 whereClauseArgs(),
3253 sortOrder());
3254
3255 HashMap<String, String> hash = new HashMap<String, String>();
3256 if (c != null && c.moveToFirst()) {
3257 do {
3258 Log.e(TAG, "id: " + c.getString(1) + " display_name: " + c.getString(0));
3259 hash.put(c.getString(1), c.getString(0));
3260 } while (c.moveToNext());
3261 }
3262 return hash;
3263 }
3264
3265 protected String whereClause() {
3266 if (mBucketId != null) {
3267 return Images.Media.BUCKET_ID + " = '" + mBucketId + "'";
3268 } else {
3269 return null;
3270 }
3271 }
3272
3273 protected String[] whereClauseArgs() {
3274 return null;
3275 }
3276
3277 protected Cursor createCursor() {
3278 Cursor c =
3279 Images.Media.query(
3280 mContentResolver,
3281 mBaseUri,
3282 sProjection,
3283 whereClause(),
3284 whereClauseArgs(),
3285 sortOrder());
3286 if (VERBOSE)
3287 Log.v(TAG, "createCursor got cursor with count " + (c == null ? -1 : c.getCount()));
3288 return c;
3289 }
3290
3291 protected int indexOrientation() { return -1; }
3292 protected int indexDateTaken() { return INDEX_DATE_TAKEN; }
3293 protected int indexDescription() { return INDEX_DESCRIPTION; }
3294 protected int indexMimeType() { return INDEX_MIME_TYPE; }
3295 protected int indexData() { return INDEX_DATA; }
3296 protected int indexId() { return INDEX_ID; }
3297 protected int indexLatitude() { return INDEX_LATITUDE; }
3298 protected int indexLongitude() { return INDEX_LONGITUDE; }
3299 protected int indexMiniThumbId() { return INDEX_MINI_THUMB_MAGIC; }
3300 protected int indexPicasaWeb() { return -1; }
3301 protected int indexPrivate() { return INDEX_PRIVATE; }
3302 protected int indexTitle() { return INDEX_TITLE; }
3303 protected int indexDisplayName() { return -1; }
3304 protected int indexThumbId() { return INDEX_THUMB_ID; }
3305
3306 protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list,
3307 long timestamp, int index) {
3308 return new VideoObject(id, miniThumbId, mContentResolver, this, timestamp, index);
3309 }
3310
3311 @Override
3312 protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput,
3313 BitmapFactory.Options options) {
3314 MediaPlayer mp = new MediaPlayer();
3315 Bitmap thumbnail = sDefaultThumbnail;
3316 try {
3317 mp.setDataSource(mContext, uri);
3318// int duration = mp.getDuration();
3319// int at = duration > 2000 ? 1000 : duration / 2;
3320 int at = 1000;
3321 thumbnail = mp.getFrameAt(at);
3322 if (Config.LOGV) {
3323 if ( thumbnail != null) {
3324 Log.v(TAG, "getFrameAt @ " + at + " returned " + thumbnail + "; " +
3325 thumbnail.getWidth() + " " + thumbnail.getHeight());
3326 } else {
3327 Log.v(TAG, "getFrame @ " + at + " failed for " + uri);
3328 }
3329 }
3330 } catch (IOException ex) {
3331 } catch (IllegalArgumentException ex) {
3332 } catch (SecurityException ex) {
3333 } finally {
3334 mp.release();
3335 }
3336 return thumbnail;
3337 }
3338
3339 private final Bitmap sDefaultThumbnail = Bitmap.createBitmap(32, 32, Bitmap.Config.RGB_565);
3340
3341 private String sortOrder() {
3342 return Video.Media.DATE_MODIFIED + (mSort == SORT_ASCENDING ? " ASC " : " DESC");
3343 }
3344 }
3345
3346 /**
3347 * Represents a particular video and provides access
3348 * to the underlying data and two thumbnail bitmaps
3349 * as well as other information such as the id, and
3350 * the path to the actual video data.
3351 */
3352 class VideoObject extends BaseImage implements IImage {
3353 /**
3354 * Constructor.
3355 *
3356 * @param id the image id of the image
3357 * @param cr the content resolver
3358 */
3359 protected VideoObject(long id, long miniThumbId, ContentResolver cr, VideoList container,
3360 long dateTaken, int row) {
3361 super(id, miniThumbId, cr, container, row);
3362 }
3363
3364 protected Bitmap.CompressFormat compressionType() {
3365 return Bitmap.CompressFormat.JPEG;
3366 }
3367
3368 @Override
3369 public boolean equals(Object other) {
3370 if (other == null)
3371 return false;
3372 if (!(other instanceof VideoObject))
3373 return false;
3374
3375 return fullSizeImageUri().equals(((VideoObject)other).fullSizeImageUri());
3376 }
3377
3378 public String getDataPath() {
3379 String path = null;
3380 Cursor c = getCursor();
3381 synchronized (c) {
3382 if (c.moveToPosition(getRow())) {
3383 int column = ((VideoList)getContainer()).indexData();
3384 if (column >= 0)
3385 path = c.getString(column);
3386 }
3387 }
3388 return path;
3389 }
3390
3391 /* (non-Javadoc)
3392 * @see com.android.camera.IImage#fullSizeBitmap()
3393 */
3394 public Bitmap fullSizeBitmap(int targetWidthHeight) {
3395 return sNoImageBitmap;
3396 }
3397
3398 public IGetBitmap_cancelable fullSizeBitmap_cancelable(int targetWidthHeight) {
3399 return null;
3400 }
3401
3402 /* (non-Javadoc)
3403 * @see com.android.camera.IImage#fullSizeImageData()
3404 */
3405 public InputStream fullSizeImageData() {
3406 try {
3407 InputStream input = mContentResolver.openInputStream(
3408 fullSizeImageUri());
3409 return input;
3410 } catch (IOException ex) {
3411 return null;
3412 }
3413 }
3414
3415 /* (non-Javadoc)
3416 * @see com.android.camera.IImage#fullSizeImageId()
3417 */
3418 public long fullSizeImageId() {
3419 return mId;
3420 }
3421
3422 public String getCategory() {
3423 return getStringEntry(((VideoList)mContainer).INDEX_CATEGORY);
3424 }
3425
3426 public int getHeight() {
3427 return 0;
3428 }
3429
3430 public String getLanguage() {
3431 return getStringEntry(((VideoList)mContainer).INDEX_LANGUAGE);
3432 }
3433
3434 public String getPicasaId() {
3435 return null;
3436 }
3437
3438 private String getStringEntry(int entryName) {
3439 String entry = null;
3440 Cursor c = getCursor();
3441 synchronized(c) {
3442 if (c.moveToPosition(getRow())) {
3443 entry = c.getString(entryName);
3444 }
3445 }
3446 return entry;
3447 }
3448
3449 public String getTags() {
3450 return getStringEntry(((VideoList)mContainer).INDEX_TAGS);
3451 }
3452
3453 public int getWidth() {
3454 return 0;
3455 }
3456
3457 /* (non-Javadoc)
3458 * @see com.android.camera.IImage#imageId()
3459 */
3460 public long imageId() {
3461 return mId;
3462 }
3463
3464 public boolean isReadonly() {
3465 return false;
3466 }
3467
3468 public boolean isDrm() {
3469 return false;
3470 }
3471
3472 public boolean rotateImageBy(int degrees) {
3473 return false;
3474 }
3475
3476 public void setCategory(String category) {
3477 setStringEntry(category, ((VideoList)mContainer).INDEX_CATEGORY);
3478 }
3479
3480 public void setLanguage(String language) {
3481 setStringEntry(language, ((VideoList)mContainer).INDEX_LANGUAGE);
3482 }
3483
3484 private void setStringEntry(String entry, int entryName) {
3485 Cursor c = getCursor();
3486 synchronized (c) {
3487 if (c.moveToPosition(getRow())) {
3488 c.updateString(entryName, entry);
3489 }
3490 }
3491 }
3492
3493 public void setTags(String tags) {
3494 setStringEntry(tags, ((VideoList)mContainer).INDEX_TAGS);
3495 }
3496
3497 /* (non-Javadoc)
3498 * @see com.android.camera.IImage#thumb1()
3499 */
3500 public Bitmap thumbBitmap() {
3501 return fullSizeBitmap(320);
3502 }
3503
3504 @Override
3505 public String toString() {
3506 StringBuilder sb = new StringBuilder();
3507 sb.append("" + mId);
3508 return sb.toString();
3509 }
3510
3511 private final Bitmap sNoImageBitmap = Bitmap.createBitmap(128, 128, Bitmap.Config.RGB_565);
3512 }
3513
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003514 /*
3515 * How much quality to use when storing the thumbnail.
3516 */
3517 private static ImageManager sInstance = null;
3518 private static final int MINI_THUMB_TARGET_SIZE = 96;
3519 private static final int THUMBNAIL_TARGET_SIZE = 320;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003520
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003521 private static final String[] THUMB_PROJECTION = new String[] {
3522 BaseColumns._ID, // 0
3523 Images.Thumbnails.IMAGE_ID, // 1
3524 Images.Thumbnails.WIDTH,
3525 Images.Thumbnails.HEIGHT
3526 };
3527
3528 private static Uri sStorageURI = Images.Media.EXTERNAL_CONTENT_URI;
3529
3530 private static Uri sThumbURI = Images.Thumbnails.EXTERNAL_CONTENT_URI;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003531
3532 private static Uri sVideoStorageURI = Uri.parse("content://media/external/video/media");
3533
3534 private static Uri sVideoThumbURI = Uri.parse("content://media/external/video/thumbnails");
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003535 /**
3536 * Returns an ImageList object that contains
3537 * all of the images.
3538 * @param cr
3539 * @param location
3540 * @param includeImages
3541 * @param includeVideo
3542 * @return the singleton ImageList
3543 */
3544 static final public int SORT_ASCENDING = 1;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003545
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003546 static final public int SORT_DESCENDING = 2;
3547
3548 static final public int INCLUDE_IMAGES = (1 << 0);
3549 static final public int INCLUDE_DRM_IMAGES = (1 << 1);
3550 static final public int INCLUDE_VIDEOS = (1 << 2);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003551
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003552 static public DataLocation getDefaultDataLocation() {
3553 return DataLocation.EXTERNAL;
3554 }
3555 private static int indexOf(String [] array, String s) {
3556 for (int i = 0; i < array.length; i++) {
3557 if (array[i].equals(s)) {
3558 return i;
3559 }
3560 }
3561 return -1;
3562 }
3563
3564 /**
3565 * Returns the singleton instance of the ImageManager.
3566 * @return the ImageManager instance.
3567 */
3568 public static ImageManager instance() {
3569 if (sInstance == null) {
3570 sInstance = new ImageManager();
3571 }
3572 return sInstance;
3573 }
3574
3575 static public byte [] miniThumbData(Bitmap source) {
3576 if (source == null)
3577 return null;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003578
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003579 float scale;
3580 if (source.getWidth() < source.getHeight()) {
3581 scale = MINI_THUMB_TARGET_SIZE / (float)source.getWidth();
3582 } else {
3583 scale = MINI_THUMB_TARGET_SIZE / (float)source.getHeight();
3584 }
3585 Matrix matrix = new Matrix();
3586 matrix.setScale(scale, scale);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003587 Bitmap miniThumbnail = ImageLoader.transform(matrix, source,
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003588 MINI_THUMB_TARGET_SIZE, MINI_THUMB_TARGET_SIZE, false);
3589
3590 if (miniThumbnail != source) {
3591 source.recycle();
3592 }
3593 java.io.ByteArrayOutputStream miniOutStream = new java.io.ByteArrayOutputStream();
3594 miniThumbnail.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream);
3595 miniThumbnail.recycle();
3596
3597 try {
3598 miniOutStream.close();
3599 byte [] data = miniOutStream.toByteArray();
3600 return data;
3601 } catch (java.io.IOException ex) {
3602 Log.e(TAG, "got exception ex " + ex);
3603 }
3604 return null;
3605 }
3606
3607 static Bitmap rotate(Bitmap b, int degrees) {
3608 if (degrees != 0 && b != null) {
3609 Matrix m = new Matrix();
3610 m.setRotate(degrees, (float) b.getWidth() / 2, (float) b.getHeight() / 2);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003611
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003612 Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
3613 // TODO should recycle here but that needs more testing/verification
3614// b.recycle();
3615 b = b2;
3616 }
3617 return b;
3618 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003619
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003620 public static int roundOrientation(int orientationInput) {
3621 int orientation = orientationInput;
3622 if (orientation == -1)
3623 orientation = 0;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003624
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003625 orientation = orientation % 360;
3626 int retVal;
3627 if (orientation < (0*90) + 45) {
3628 retVal = 0;
3629 } else if (orientation < (1*90) + 45) {
3630 retVal = 90;
3631 } else if (orientation < (2*90) + 45) {
3632 retVal = 180;
3633 } else if (orientation < (3*90) + 45) {
3634 retVal = 270;
3635 } else {
3636 retVal = 0;
3637 }
3638
3639 if (VERBOSE) Log.v(TAG, "map orientation " + orientationInput + " to " + retVal);
3640 return retVal;
3641 }
3642
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003643
3644 /**
3645 * @return true if the mimetype is an image mimetype.
3646 */
3647 public static boolean isImageMimeType(String mimeType) {
3648 return mimeType.startsWith("image/");
3649 }
3650
3651 /**
3652 * @return true if the mimetype is a video mimetype.
3653 */
3654 public static boolean isVideoMimeType(String mimeType) {
3655 return mimeType.startsWith("video/");
3656 }
3657
3658 /**
3659 * @return true if the image is an image.
3660 */
3661 public static boolean isImage(IImage image) {
3662 return isImageMimeType(image.getMimeType());
3663 }
3664
3665 /**
3666 * @return true if the image is a video.
3667 */
3668 public static boolean isVideo(IImage image) {
3669 return isVideoMimeType(image.getMimeType());
3670 }
3671
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003672 public Uri addImage(
3673 final Context ctx,
3674 final ContentResolver cr,
3675 final String imageName,
3676 final String description,
3677 final long dateTaken,
3678 final Location location,
3679 final int orientation,
3680 final String directory,
3681 final String filename) {
3682 ContentValues values = new ContentValues(7);
3683 values.put(Images.Media.TITLE, imageName);
3684 values.put(Images.Media.DISPLAY_NAME, imageName);
3685 values.put(Images.Media.DESCRIPTION, description);
3686 values.put(Images.Media.DATE_TAKEN, dateTaken);
3687 values.put(Images.Media.MIME_TYPE, "image/jpeg");
3688 values.put(Images.Media.ORIENTATION, orientation);
3689
3690 File parentFile = new File(directory);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003691 // Lowercase the path for hashing. This avoids duplicate buckets if the filepath
3692 // case is changed externally.
3693 // Keep the original case for display.
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003694 String path = parentFile.toString().toLowerCase();
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003695 String name = parentFile.getName();
3696
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003697 if (VERBOSE) Log.v(TAG, "addImage id is " + path.hashCode() + "; name " + name + "; path is " + path);
3698
3699 if (location != null) {
3700 if (VERBOSE) {
3701 Log.v(TAG, "lat long " + location.getLatitude() + " / " + location.getLongitude());
3702 }
3703 values.put(Images.Media.LATITUDE, location.getLatitude());
3704 values.put(Images.Media.LONGITUDE, location.getLongitude());
3705 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003706
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003707 if (directory != null && filename != null) {
3708 String value = directory + "/" + filename;
3709 values.put("_data", value);
3710 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003711
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003712 long t3 = System.currentTimeMillis();
3713 Uri uri = cr.insert(sStorageURI, values);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003714
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003715 // The line above will create a filename that ends in .jpg
3716 // That filename is what will be handed to gmail when a user shares a photo.
3717 // Gmail gets the name of the picture attachment from the "DISPLAY_NAME" field.
3718 // Extract the filename and jam it into the display name.
3719 Cursor c = cr.query(
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003720 uri,
3721 new String [] { ImageColumns._ID, Images.Media.DISPLAY_NAME, "_data" },
3722 null,
3723 null,
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003724 null);
3725 if (c.moveToFirst()) {
3726 String filePath = c.getString(2);
3727 if (filePath != null) {
3728 int pos = filePath.lastIndexOf("/");
3729 if (pos >= 0) {
3730 filePath = filePath.substring(pos + 1); // pick off the filename
3731 c.updateString(1, filePath);
3732 c.commitUpdates();
3733 }
3734 }
3735 }
3736 c.close();
3737 return uri;
3738 }
3739
3740 public IAddImage_cancelable storeImage(
3741 final Uri uri,
3742 final Context ctx,
3743 final ContentResolver cr,
3744 final int orientation,
3745 final Bitmap source,
3746 final byte [] jpegData) {
3747 class AddImageCancelable extends BaseCancelable implements IAddImage_cancelable {
3748 private IGetBoolean_cancelable mSaveImageCancelable;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003749
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003750 public boolean doCancelWork() {
3751 if (VERBOSE) {
3752 Log.v(TAG, "calling AddImageCancelable.cancel() " + mSaveImageCancelable);
3753 }
3754
3755 if (mSaveImageCancelable != null) {
3756 mSaveImageCancelable.cancel();
3757 }
3758 return true;
3759 }
3760
3761 public void get() {
3762 if (source == null && jpegData == null) {
3763 throw new IllegalArgumentException("source cannot be null");
3764 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003765
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003766 try {
3767 long t1 = System.currentTimeMillis();
3768 synchronized (this) {
3769 if (mCancel) {
3770 throw new CanceledException();
3771 }
3772 }
3773 long id = ContentUris.parseId(uri);
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003774
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003775 BaseImageList il = new ImageList(ctx, cr, sStorageURI, sThumbURI, SORT_ASCENDING, null);
3776 ImageManager.Image image = new Image(id, 0, cr, il, il.getCount(), 0);
3777 long t5 = System.currentTimeMillis();
3778 Cursor c = cr.query(
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003779 uri,
3780 new String [] { ImageColumns._ID, ImageColumns.MINI_THUMB_MAGIC, "_data" },
3781 null,
3782 null,
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003783 null);
3784 c.moveToPosition(0);
3785
3786 synchronized (this) {
3787 checkCanceled();
3788 mSaveImageCancelable = image.saveImageContents(source, jpegData, orientation, true, c);
3789 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003790
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003791 if (mSaveImageCancelable.get()) {
3792 long t6 = System.currentTimeMillis();
3793 if (VERBOSE) Log.v(TAG, "saveImageContents took " + (t6-t5));
3794 if (VERBOSE) Log.v(TAG, "updating new picture with id " + id);
3795 c.updateLong(1, id);
3796 c.commitUpdates();
3797 c.close();
3798 long t7 = System.currentTimeMillis();
3799 if (VERBOSE) Log.v(TAG, "commit updates to save mini thumb took " + (t7-t6));
3800 }
3801 else {
3802 c.close();
3803 throw new CanceledException();
3804 }
3805 } catch (CanceledException ex) {
3806 if (VERBOSE) {
3807 Log.v(TAG, "caught CanceledException");
3808 }
3809 if (uri != null) {
3810 if (VERBOSE) {
3811 Log.v(TAG, "canceled... cleaning up this uri: " + uri);
3812 }
3813 cr.delete(uri, null, null);
3814 }
3815 acknowledgeCancel();
3816 }
3817 }
3818 }
3819 return new AddImageCancelable();
3820 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003821
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003822 static public IImageList makeImageList(Uri uri, Context ctx, int sort) {
3823 ContentResolver cr = ctx.getContentResolver();
3824 String uriString = (uri != null) ? uri.toString() : "";
3825 // TODO we need to figure out whether we're viewing
3826 // DRM images in a better way. Is there a constant
3827 // for content://drm somewhere??
3828 IImageList imageList;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003829
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003830 if (uriString.startsWith("content://drm")) {
3831 imageList = ImageManager.instance().allImages(
3832 ctx,
3833 cr,
3834 ImageManager.DataLocation.ALL,
3835 ImageManager.INCLUDE_DRM_IMAGES,
3836 sort);
3837 } else if (!uriString.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
3838 && !uriString.startsWith(MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString())) {
3839 imageList = ImageManager.instance().new SingleImageList(cr, uri);
3840 } else {
3841 String bucketId = uri.getQueryParameter("bucketId");
3842 if (VERBOSE) Log.v(TAG, "bucketId is " + bucketId);
3843 imageList = ImageManager.instance().allImages(
3844 ctx,
3845 cr,
3846 ImageManager.DataLocation.ALL,
3847 ImageManager.INCLUDE_IMAGES,
3848 sort,
3849 bucketId);
3850 }
3851 return imageList;
3852 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003853
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003854 public IImageList emptyImageList() {
3855 return
3856 new IImageList() {
3857 public void checkThumbnails(com.android.camera.ImageManager.IImageList.ThumbCheckCallback cb) {
3858 }
3859
3860 public void commitChanges() {
3861 }
3862
3863 public void deactivate() {
3864 }
3865
3866 public HashMap<String, String> getBucketIds() {
3867 return new HashMap<String,String>();
3868 }
3869
3870 public int getCount() {
3871 return 0;
3872 }
3873
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003874 public boolean isEmpty() {
3875 return true;
3876 }
3877
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003878 public IImage getImageAt(int i) {
3879 return null;
3880 }
3881
3882 public IImage getImageForUri(Uri uri) {
3883 return null;
3884 }
3885
3886 public boolean removeImage(IImage image) {
3887 return false;
3888 }
3889
3890 public void removeImageAt(int i) {
3891 }
3892
3893 public void removeOnChangeListener(com.android.camera.ImageManager.IImageList.OnChange changeCallback) {
3894 }
3895
3896 public void setOnChangeListener(com.android.camera.ImageManager.IImageList.OnChange changeCallback, Handler h) {
3897 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003898
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003899 };
3900 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003901
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003902 public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort) {
3903 return allImages(ctx, cr, location, inclusion, sort, null, null);
3904 }
3905
3906 public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId) {
3907 return allImages(ctx, cr, location, inclusion, sort, bucketId, null);
3908 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003909
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003910 public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId, Uri specificImageUri) {
3911 if (VERBOSE) {
3912 Log.v(TAG, "allImages " + location + " " + ((inclusion&INCLUDE_IMAGES)!=0) + " + v=" + ((inclusion&INCLUDE_VIDEOS)!=0));
3913 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003914
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003915 if (cr == null) {
3916 return null;
3917 } else {
3918 // false ==> don't require write access
3919 boolean haveSdCard = hasStorage(false);
3920
3921 if (true) {
3922 // use this code to merge videos and stills into the same list
3923 ArrayList<IImageList> l = new ArrayList<IImageList>();
3924
3925 if (VERBOSE) {
3926 Log.v(TAG, "initializing ... haveSdCard == " + haveSdCard + "; inclusion is " + String.format("%x", inclusion));
3927 }
3928 if (specificImageUri != null) {
3929 try {
3930 if (specificImageUri.getScheme().equalsIgnoreCase("content"))
3931 l.add(new ImageList(ctx, cr, specificImageUri, sThumbURI, sort, bucketId));
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003932 else
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003933 l.add(new SingleImageList(cr, specificImageUri));
3934 } catch (UnsupportedOperationException ex) {
3935 }
3936 } else {
3937 if (haveSdCard && location != DataLocation.INTERNAL) {
3938 if ((inclusion & INCLUDE_IMAGES) != 0) {
3939 try {
3940 l.add(new ImageList(ctx, cr, sStorageURI, sThumbURI, sort, bucketId));
3941 } catch (UnsupportedOperationException ex) {
3942 }
3943 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003944 if ((inclusion & INCLUDE_VIDEOS) != 0) {
3945 try {
3946 l.add(new VideoList(ctx, cr, sVideoStorageURI, sVideoThumbURI, sort, bucketId));
3947 } catch (UnsupportedOperationException ex) {
3948 }
3949 }
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003950 }
3951 if (location == DataLocation.INTERNAL || location == DataLocation.ALL) {
3952 if ((inclusion & INCLUDE_IMAGES) != 0) {
3953 try {
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003954 l.add(new ImageList(ctx, cr, Images.Media.INTERNAL_CONTENT_URI,
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003955 Images.Thumbnails.INTERNAL_CONTENT_URI, sort, bucketId));
3956 } catch (UnsupportedOperationException ex) {
3957 }
3958 }
3959 if ((inclusion & INCLUDE_DRM_IMAGES) != 0) {
3960 try {
3961 l.add(new DrmImageList(ctx, cr, DrmStore.Images.CONTENT_URI, sort, bucketId));
3962 } catch (UnsupportedOperationException ex) {
3963 }
3964 }
3965 }
3966 }
3967
3968 IImageList [] imageList = l.toArray(new IImageList[l.size()]);
3969 return new ImageListUber(imageList, sort);
3970 } else {
3971 if (haveSdCard && location != DataLocation.INTERNAL) {
3972 return new ImageList(ctx, cr, sStorageURI, sThumbURI, sort, bucketId);
3973 } else {
The Android Open Source Project0eaec582008-12-17 18:05:54 -08003974 return new ImageList(ctx, cr, Images.Media.INTERNAL_CONTENT_URI,
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07003975 Images.Thumbnails.INTERNAL_CONTENT_URI, sort, bucketId);
3976 }
3977 }
3978 }
3979 }
3980
3981 // Create a temporary file to see whether a volume is really writeable. It's important not to
3982 // put it in the root directory which may have a limit on the number of files.
3983 static private boolean checkFsWritable() {
3984 String directoryName = Environment.getExternalStorageDirectory().toString() + "/dcim";
3985 File directory = new File(directoryName);
3986 if (!directory.isDirectory()) {
3987 if (!directory.mkdirs()) {
3988 return false;
3989 }
3990 }
3991 File f = new File(directoryName, ".probe");
3992 try {
3993 // Remove stale file if any
3994 if (f.exists()) {
3995 f.delete();
3996 }
3997 if (!f.createNewFile())
3998 return false;
3999 f.delete();
4000 return true;
4001 } catch (IOException ex) {
4002 return false;
4003 }
4004 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08004005
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07004006 static public boolean hasStorage() {
4007 return hasStorage(true);
4008 }
4009
4010 static public boolean hasStorage(boolean requireWriteAccess) {
4011 String state = Environment.getExternalStorageState();
4012 if (VERBOSE) Log.v(TAG, "state is " + state);
4013 if (Environment.MEDIA_MOUNTED.equals(state)) {
4014 if (requireWriteAccess) {
4015 boolean writable = checkFsWritable();
4016 if (VERBOSE) Log.v(TAG, "writable is " + writable);
4017 return writable;
4018 } else {
4019 return true;
4020 }
4021 } else if (!requireWriteAccess && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
4022 return true;
4023 }
4024 return false;
4025 }
4026
4027 public static Cursor query(Context context, Uri uri, String[] projection,
4028 String selection, String[] selectionArgs, String sortOrder) {
4029 try {
4030 ContentResolver resolver = context.getContentResolver();
4031 if (resolver == null) {
4032 return null;
4033 }
4034 return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
4035 } catch (UnsupportedOperationException ex) {
4036 return null;
4037 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08004038
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07004039 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08004040
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07004041 public static boolean isMediaScannerScanning(Context context) {
4042 boolean result = false;
The Android Open Source Project0eaec582008-12-17 18:05:54 -08004043 Cursor cursor = query(context, MediaStore.getMediaScannerUri(),
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07004044 new String [] { MediaStore.MEDIA_SCANNER_VOLUME }, null, null, null);
4045 if (cursor != null) {
4046 if (cursor.getCount() == 1) {
4047 cursor.moveToFirst();
4048 result = "external".equals(cursor.getString(0));
4049 }
The Android Open Source Project0eaec582008-12-17 18:05:54 -08004050 cursor.close();
4051 }
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07004052
4053 if (VERBOSE)
4054 Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>>>>>> isMediaScannerScanning returning " + result);
4055 return result;
4056 }
4057}