blob: 72228da0598231ca63e5421648c11c7a20c333c8 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.camera;
import com.android.camera.gallery.IImage;
import com.android.camera.gallery.IImageList;
import com.android.camera.gallery.VideoObject;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import android.provider.MediaStore;
/*
* Here's the loading strategy. For any given image, load the thumbnail
* into memory and post a callback to display the resulting bitmap.
*
* Then proceed to load the full image bitmap. Three things can
* happen at this point:
*
* 1. the image fails to load because the UI thread decided
* to move on to a different image. This "cancellation" happens
* by virtue of the UI thread closing the stream containing the
* image being decoded. BitmapFactory.decodeStream returns null
* in this case.
*
* 2. the image loaded successfully. At that point we post
* a callback to the UI thread to actually show the bitmap.
*
* 3. when the post runs it checks to see if the image that was
* loaded is still the one we want. The UI may have moved on
* to some other image and if so we just drop the newly loaded
* bitmap on the floor.
*/
interface ImageGetterCallback {
public void imageLoaded(int pos, int offset, RotateBitmap bitmap,
boolean isThumb);
public boolean wantsThumbnail(int pos, int offset);
public boolean wantsFullImage(int pos, int offset);
public int fullImageSizeToUse(int pos, int offset);
public void completed();
public int [] loadOrder();
}
class ImageGetter {
@SuppressWarnings("unused")
private static final String TAG = "ImageGetter";
// The thread which does the work.
private Thread mGetterThread;
// The current request serial number.
// This is increased by one each time a new job is assigned.
// It is only written in the main thread.
private int mCurrentSerial;
// The base position that's being retrieved. The actual images retrieved
// are this base plus each of the offets. -1 means there is no current
// request needs to be finished.
private int mCurrentPosition = -1;
// The callback to invoke for each image.
private ImageGetterCallback mCB;
// The image list for the images.
private IImageList mImageList;
// The handler to do callback.
private GetterHandler mHandler;
// True if we want to cancel the current loading.
private volatile boolean mCancel = true;
// True if the getter thread is idle waiting.
private boolean mIdle = false;
// True when the getter thread should exit.
private boolean mDone = false;
private ContentResolver mCr;
private class ImageGetterRunnable implements Runnable {
private Runnable callback(final int position, final int offset,
final boolean isThumb,
final RotateBitmap bitmap,
final int requestSerial) {
return new Runnable() {
public void run() {
// check for inflight callbacks that aren't applicable
// any longer before delivering them
if (requestSerial == mCurrentSerial) {
mCB.imageLoaded(position, offset, bitmap, isThumb);
} else if (bitmap != null) {
bitmap.recycle();
}
}
};
}
private Runnable completedCallback(final int requestSerial) {
return new Runnable() {
public void run() {
if (requestSerial == mCurrentSerial) {
mCB.completed();
}
}
};
}
public void run() {
// Lower the priority of this thread to avoid competing with
// the UI thread.
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while (true) {
synchronized (ImageGetter.this) {
while (mCancel || mDone || mCurrentPosition == -1) {
if (mDone) return;
mIdle = true;
ImageGetter.this.notify();
try {
ImageGetter.this.wait();
} catch (InterruptedException ex) {
// ignore
}
mIdle = false;
}
}
executeRequest();
synchronized (ImageGetter.this) {
mCurrentPosition = -1;
}
}
}
private void executeRequest() {
int imageCount = mImageList.getCount();
int [] order = mCB.loadOrder();
for (int i = 0; i < order.length; i++) {
if (mCancel) return;
int offset = order[i];
int imageNumber = mCurrentPosition + offset;
if (imageNumber >= 0 && imageNumber < imageCount) {
if (!mCB.wantsThumbnail(mCurrentPosition, offset)) {
continue;
}
IImage image = mImageList.getImageAt(imageNumber);
if (image == null) continue;
if (mCancel) return;
Bitmap b = image.thumbBitmap(IImage.NO_ROTATE);
if (b == null) continue;
if (mCancel) {
b.recycle();
return;
}
Runnable cb = callback(mCurrentPosition, offset,
true,
new RotateBitmap(b, image.getDegreesRotated()),
mCurrentSerial);
mHandler.postGetterCallback(cb);
}
}
for (int i = 0; i < order.length; i++) {
if (mCancel) return;
int offset = order[i];
int imageNumber = mCurrentPosition + offset;
if (imageNumber >= 0 && imageNumber < imageCount) {
if (!mCB.wantsFullImage(mCurrentPosition, offset)) {
continue;
}
IImage image = mImageList.getImageAt(imageNumber);
if (image == null) continue;
if (image instanceof VideoObject) continue;
if (mCancel) return;
int sizeToUse = mCB.fullImageSizeToUse(
mCurrentPosition, offset);
Bitmap b = image.fullSizeBitmap(sizeToUse, 3 * 1024 * 1024,
IImage.NO_ROTATE, IImage.USE_NATIVE);
if (b == null) continue;
if (mCancel) {
b.recycle();
return;
}
RotateBitmap rb = new RotateBitmap(b,
image.getDegreesRotated());
Runnable cb = callback(mCurrentPosition, offset,
false, rb, mCurrentSerial);
mHandler.postGetterCallback(cb);
}
}
mHandler.postGetterCallback(completedCallback(mCurrentSerial));
}
}
public ImageGetter(ContentResolver cr) {
mCr = cr;
mGetterThread = new Thread(new ImageGetterRunnable());
mGetterThread.setName("ImageGettter");
mGetterThread.start();
}
// Cancels current loading (without waiting).
public synchronized void cancelCurrent() {
Util.Assert(mGetterThread != null);
mCancel = true;
BitmapManager.instance().cancelThreadDecoding(mGetterThread, mCr);
}
// Cancels current loading (with waiting).
private synchronized void cancelCurrentAndWait() {
cancelCurrent();
while (mIdle != true) {
try {
wait();
} catch (InterruptedException ex) {
// ignore.
}
}
}
// Stops this image getter.
public void stop() {
synchronized (this) {
cancelCurrentAndWait();
mDone = true;
notify();
}
try {
mGetterThread.join();
} catch (InterruptedException ex) {
// Ignore the exception
}
mGetterThread = null;
}
public synchronized void setPosition(int position, ImageGetterCallback cb,
IImageList imageList, GetterHandler handler) {
// Cancel the previous request.
cancelCurrentAndWait();
// Set new data.
mCurrentPosition = position;
mCB = cb;
mImageList = imageList;
mHandler = handler;
mCurrentSerial += 1;
// Kick-start the current request.
mCancel = false;
BitmapManager.instance().allowThreadDecoding(mGetterThread);
notify();
}
}
class GetterHandler extends Handler {
private static final int IMAGE_GETTER_CALLBACK = 1;
@Override
public void handleMessage(Message message) {
switch(message.what) {
case IMAGE_GETTER_CALLBACK:
((Runnable) message.obj).run();
break;
}
}
public void postGetterCallback(Runnable callback) {
postDelayedGetterCallback(callback, 0);
}
public void postDelayedGetterCallback(Runnable callback, long delay) {
if (callback == null) {
throw new NullPointerException();
}
Message message = Message.obtain();
message.what = IMAGE_GETTER_CALLBACK;
message.obj = callback;
sendMessageDelayed(message, delay);
}
public void removeAllGetterCallbacks() {
removeMessages(IMAGE_GETTER_CALLBACK);
}
}