1/*
2 * Copyright (C) 2009 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 com.android.camera.gallery.IImage;
20import com.android.camera.gallery.IImageList;
21import com.android.camera.gallery.VideoObject;
22
23import android.content.ContentResolver;
24import android.graphics.Bitmap;
25import android.os.Handler;
26import android.os.Message;
27import android.os.Process;
28import android.provider.MediaStore;
29
30/*
31 * Here's the loading strategy.  For any given image, load the thumbnail
32 * into memory and post a callback to display the resulting bitmap.
33 *
34 * Then proceed to load the full image bitmap.   Three things can
35 * happen at this point:
36 *
37 * 1.  the image fails to load because the UI thread decided
38 * to move on to a different image.  This "cancellation" happens
39 * by virtue of the UI thread closing the stream containing the
40 * image being decoded.  BitmapFactory.decodeStream returns null
41 * in this case.
42 *
43 * 2.  the image loaded successfully.  At that point we post
44 * a callback to the UI thread to actually show the bitmap.
45 *
46 * 3.  when the post runs it checks to see if the image that was
47 * loaded is still the one we want.  The UI may have moved on
48 * to some other image and if so we just drop the newly loaded
49 * bitmap on the floor.
50 */
51
52interface ImageGetterCallback {
53    public void imageLoaded(int pos, int offset, RotateBitmap bitmap,
54                            boolean isThumb);
55    public boolean wantsThumbnail(int pos, int offset);
56    public boolean wantsFullImage(int pos, int offset);
57    public int fullImageSizeToUse(int pos, int offset);
58    public void completed();
59    public int [] loadOrder();
60}
61
62class ImageGetter {
63
64    @SuppressWarnings("unused")
65    private static final String TAG = "ImageGetter";
66
67    // The thread which does the work.
68    private Thread mGetterThread;
69
70    // The current request serial number.
71    // This is increased by one each time a new job is assigned.
72    // It is only written in the main thread.
73    private int mCurrentSerial;
74
75    // The base position that's being retrieved.  The actual images retrieved
76    // are this base plus each of the offets. -1 means there is no current
77    // request needs to be finished.
78    private int mCurrentPosition = -1;
79
80    // The callback to invoke for each image.
81    private ImageGetterCallback mCB;
82
83    // The image list for the images.
84    private IImageList mImageList;
85
86    // The handler to do callback.
87    private GetterHandler mHandler;
88
89    // True if we want to cancel the current loading.
90    private volatile boolean mCancel = true;
91
92    // True if the getter thread is idle waiting.
93    private boolean mIdle = false;
94
95    // True when the getter thread should exit.
96    private boolean mDone = false;
97
98    private ContentResolver mCr;
99
100    private class ImageGetterRunnable implements Runnable {
101
102        private Runnable callback(final int position, final int offset,
103                                  final boolean isThumb,
104                                  final RotateBitmap bitmap,
105                                  final int requestSerial) {
106            return new Runnable() {
107                public void run() {
108                    // check for inflight callbacks that aren't applicable
109                    // any longer before delivering them
110                    if (requestSerial == mCurrentSerial) {
111                        mCB.imageLoaded(position, offset, bitmap, isThumb);
112                    } else if (bitmap != null) {
113                        bitmap.recycle();
114                    }
115                }
116            };
117        }
118
119        private Runnable completedCallback(final int requestSerial) {
120            return new Runnable() {
121                public void run() {
122                    if (requestSerial == mCurrentSerial) {
123                        mCB.completed();
124                    }
125                }
126            };
127        }
128
129        public void run() {
130            // Lower the priority of this thread to avoid competing with
131            // the UI thread.
132            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
133
134            while (true) {
135                synchronized (ImageGetter.this) {
136                    while (mCancel || mDone || mCurrentPosition == -1) {
137                        if (mDone) return;
138                        mIdle = true;
139                        ImageGetter.this.notify();
140                        try {
141                            ImageGetter.this.wait();
142                        } catch (InterruptedException ex) {
143                            // ignore
144                        }
145                        mIdle = false;
146                    }
147                }
148
149                executeRequest();
150
151                synchronized (ImageGetter.this) {
152                    mCurrentPosition = -1;
153                }
154            }
155        }
156        private void executeRequest() {
157            int imageCount = mImageList.getCount();
158
159            int [] order = mCB.loadOrder();
160            for (int i = 0; i < order.length; i++) {
161                if (mCancel) return;
162                int offset = order[i];
163                int imageNumber = mCurrentPosition + offset;
164                if (imageNumber >= 0 && imageNumber < imageCount) {
165                    if (!mCB.wantsThumbnail(mCurrentPosition, offset)) {
166                        continue;
167                    }
168
169                    IImage image = mImageList.getImageAt(imageNumber);
170                    if (image == null) continue;
171                    if (mCancel) return;
172
173                    Bitmap b = image.thumbBitmap(IImage.NO_ROTATE);
174                    if (b == null) continue;
175                    if (mCancel) {
176                        b.recycle();
177                        return;
178                    }
179
180                    Runnable cb = callback(mCurrentPosition, offset,
181                            true,
182                            new RotateBitmap(b, image.getDegreesRotated()),
183                            mCurrentSerial);
184                    mHandler.postGetterCallback(cb);
185                }
186            }
187
188            for (int i = 0; i < order.length; i++) {
189                if (mCancel) return;
190                int offset = order[i];
191                int imageNumber = mCurrentPosition + offset;
192                if (imageNumber >= 0 && imageNumber < imageCount) {
193                    if (!mCB.wantsFullImage(mCurrentPosition, offset)) {
194                        continue;
195                    }
196
197                    IImage image = mImageList.getImageAt(imageNumber);
198                    if (image == null) continue;
199                    if (image instanceof VideoObject) continue;
200                    if (mCancel) return;
201
202                    int sizeToUse = mCB.fullImageSizeToUse(
203                            mCurrentPosition, offset);
204                    Bitmap b = image.fullSizeBitmap(sizeToUse, 3 * 1024 * 1024,
205                            IImage.NO_ROTATE, IImage.USE_NATIVE);
206
207                    if (b == null) continue;
208                    if (mCancel) {
209                        b.recycle();
210                        return;
211                    }
212
213                    RotateBitmap rb = new RotateBitmap(b,
214                            image.getDegreesRotated());
215
216                    Runnable cb = callback(mCurrentPosition, offset,
217                            false, rb, mCurrentSerial);
218                    mHandler.postGetterCallback(cb);
219                }
220            }
221
222            mHandler.postGetterCallback(completedCallback(mCurrentSerial));
223        }
224    }
225
226    public ImageGetter(ContentResolver cr) {
227        mCr = cr;
228        mGetterThread = new Thread(new ImageGetterRunnable());
229        mGetterThread.setName("ImageGettter");
230        mGetterThread.start();
231    }
232
233    // Cancels current loading (without waiting).
234    public synchronized void cancelCurrent() {
235        Util.Assert(mGetterThread != null);
236        mCancel = true;
237        BitmapManager.instance().cancelThreadDecoding(mGetterThread, mCr);
238    }
239
240    // Cancels current loading (with waiting).
241    private synchronized void cancelCurrentAndWait() {
242        cancelCurrent();
243        while (mIdle != true) {
244            try {
245                wait();
246            } catch (InterruptedException ex) {
247                // ignore.
248            }
249        }
250    }
251
252    // Stops this image getter.
253    public void stop() {
254        synchronized (this) {
255            cancelCurrentAndWait();
256            mDone = true;
257            notify();
258        }
259        try {
260            mGetterThread.join();
261        } catch (InterruptedException ex) {
262            // Ignore the exception
263        }
264        mGetterThread = null;
265    }
266
267    public synchronized void setPosition(int position, ImageGetterCallback cb,
268            IImageList imageList, GetterHandler handler) {
269        // Cancel the previous request.
270        cancelCurrentAndWait();
271
272        // Set new data.
273        mCurrentPosition = position;
274        mCB = cb;
275        mImageList = imageList;
276        mHandler = handler;
277        mCurrentSerial += 1;
278
279        // Kick-start the current request.
280        mCancel = false;
281        BitmapManager.instance().allowThreadDecoding(mGetterThread);
282        notify();
283    }
284}
285
286class GetterHandler extends Handler {
287    private static final int IMAGE_GETTER_CALLBACK = 1;
288
289    @Override
290    public void handleMessage(Message message) {
291        switch(message.what) {
292            case IMAGE_GETTER_CALLBACK:
293                ((Runnable) message.obj).run();
294                break;
295        }
296    }
297
298    public void postGetterCallback(Runnable callback) {
299       postDelayedGetterCallback(callback, 0);
300    }
301
302    public void postDelayedGetterCallback(Runnable callback, long delay) {
303        if (callback == null) {
304            throw new NullPointerException();
305        }
306        Message message = Message.obtain();
307        message.what = IMAGE_GETTER_CALLBACK;
308        message.obj = callback;
309        sendMessageDelayed(message, delay);
310    }
311
312    public void removeAllGetterCallbacks() {
313        removeMessages(IMAGE_GETTER_CALLBACK);
314    }
315}
316