/* * 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); } }