OSDN Git Service

Merge "Refactor AutoThumbnailDrawable, fix race conditions" into gb-ub-photos-bryce
[android-x86/packages-apps-Gallery2.git] / src / com / android / photos / views / TiledImageRenderer.java
1 /*
2  * Copyright (C) 2013 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
17 package com.android.photos.views;
18
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.graphics.Rect;
22 import android.graphics.RectF;
23 import android.support.v4.util.LongSparseArray;
24 import android.util.DisplayMetrics;
25 import android.util.Log;
26 import android.view.View;
27 import android.view.WindowManager;
28
29 import com.android.gallery3d.common.Utils;
30 import com.android.gallery3d.glrenderer.GLCanvas;
31 import com.android.gallery3d.glrenderer.UploadedTexture;
32 import com.android.photos.data.GalleryBitmapPool;
33
34 public class TiledImageRenderer {
35     public static final int SIZE_UNKNOWN = -1;
36
37     private static final String TAG = "TiledImageRenderer";
38     private static final int UPLOAD_LIMIT = 1;
39
40     /*
41      *  This is the tile state in the CPU side.
42      *  Life of a Tile:
43      *      ACTIVATED (initial state)
44      *              --> IN_QUEUE - by queueForDecode()
45      *              --> RECYCLED - by recycleTile()
46      *      IN_QUEUE --> DECODING - by decodeTile()
47      *               --> RECYCLED - by recycleTile)
48      *      DECODING --> RECYCLING - by recycleTile()
49      *               --> DECODED  - by decodeTile()
50      *               --> DECODE_FAIL - by decodeTile()
51      *      RECYCLING --> RECYCLED - by decodeTile()
52      *      DECODED --> ACTIVATED - (after the decoded bitmap is uploaded)
53      *      DECODED --> RECYCLED - by recycleTile()
54      *      DECODE_FAIL -> RECYCLED - by recycleTile()
55      *      RECYCLED --> ACTIVATED - by obtainTile()
56      */
57     private static final int STATE_ACTIVATED = 0x01;
58     private static final int STATE_IN_QUEUE = 0x02;
59     private static final int STATE_DECODING = 0x04;
60     private static final int STATE_DECODED = 0x08;
61     private static final int STATE_DECODE_FAIL = 0x10;
62     private static final int STATE_RECYCLING = 0x20;
63     private static final int STATE_RECYCLED = 0x40;
64
65     private static GalleryBitmapPool sTilePool = GalleryBitmapPool.getInstance();
66
67     // TILE_SIZE must be 2^N
68     private int mTileSize;
69
70     private TileSource mModel;
71     protected int mLevelCount;  // cache the value of mScaledBitmaps.length
72
73     // The mLevel variable indicates which level of bitmap we should use.
74     // Level 0 means the original full-sized bitmap, and a larger value means
75     // a smaller scaled bitmap (The width and height of each scaled bitmap is
76     // half size of the previous one). If the value is in [0, mLevelCount), we
77     // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value
78     // is mLevelCount
79     private int mLevel = 0;
80
81     private int mOffsetX;
82     private int mOffsetY;
83
84     private int mUploadQuota;
85     private boolean mRenderComplete;
86
87     private final RectF mSourceRect = new RectF();
88     private final RectF mTargetRect = new RectF();
89
90     private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>();
91
92     // The following three queue are guarded by mQueueLock
93     private final Object mQueueLock = new Object();
94     private final TileQueue mRecycledQueue = new TileQueue();
95     private final TileQueue mUploadQueue = new TileQueue();
96     private final TileQueue mDecodeQueue = new TileQueue();
97
98     // The width and height of the full-sized bitmap
99     protected int mImageWidth = SIZE_UNKNOWN;
100     protected int mImageHeight = SIZE_UNKNOWN;
101
102     protected int mCenterX;
103     protected int mCenterY;
104     protected float mScale;
105     protected int mRotation;
106
107     private boolean mLayoutTiles;
108
109     // Temp variables to avoid memory allocation
110     private final Rect mTileRange = new Rect();
111     private final Rect mActiveRange[] = {new Rect(), new Rect()};
112
113     private TileDecoder mTileDecoder;
114     private boolean mBackgroundTileUploaded;
115
116     private int mViewWidth, mViewHeight;
117     private View mParent;
118
119     public static interface TileSource {
120         public int getTileSize();
121         public int getImageWidth();
122         public int getImageHeight();
123
124         // The tile returned by this method can be specified this way: Assuming
125         // the image size is (width, height), first take the intersection of (0,
126         // 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
127         // in extending the region, we found some part of the region is outside
128         // the image, those pixels are filled with black.
129         //
130         // If level > 0, it does the same operation on a down-scaled version of
131         // the original image (down-scaled by a factor of 2^level), but (x, y)
132         // still refers to the coordinate on the original image.
133         //
134         // The method would be called by the decoder thread.
135         public Bitmap getTile(int level, int x, int y, Bitmap reuse);
136     }
137
138     public static int suggestedTileSize(Context context) {
139         return isHighResolution(context) ? 512 : 256;
140     }
141
142     private static boolean isHighResolution(Context context) {
143         DisplayMetrics metrics = new DisplayMetrics();
144         WindowManager wm = (WindowManager)
145                 context.getSystemService(Context.WINDOW_SERVICE);
146         wm.getDefaultDisplay().getMetrics(metrics);
147         return metrics.heightPixels > 2048 ||  metrics.widthPixels > 2048;
148     }
149
150     public TiledImageRenderer(View parent) {
151         mParent = parent;
152         mTileDecoder = new TileDecoder();
153         mTileDecoder.start();
154     }
155
156     public int getViewWidth() {
157         return mViewWidth;
158     }
159
160     public int getViewHeight() {
161         return mViewHeight;
162     }
163
164     private void invalidate() {
165         mParent.postInvalidate();
166     }
167
168     public void setModel(TileSource model, int rotation) {
169         if (mModel != model) {
170             mModel = model;
171             notifyModelInvalidated();
172         }
173         if (mRotation != rotation) {
174             mRotation = rotation;
175             mLayoutTiles = true;
176             invalidate();
177         }
178     }
179
180     private static int calulateLevelCount(TileSource source) {
181         int levels = 1;
182         int maxDim = Math.max(source.getImageWidth(), source.getImageHeight());
183         int t = source.getTileSize();
184         while (t < maxDim) {
185             t <<= 1;
186             levels++;
187         }
188         return levels;
189     }
190
191     public void notifyModelInvalidated() {
192         invalidateTiles();
193         if (mModel == null) {
194             mImageWidth = 0;
195             mImageHeight = 0;
196             mLevelCount = 0;
197         } else {
198             mImageWidth = mModel.getImageWidth();
199             mImageHeight = mModel.getImageHeight();
200             mLevelCount = calulateLevelCount(mModel);
201             mTileSize = mModel.getTileSize();
202         }
203         mLayoutTiles = true;
204         invalidate();
205     }
206
207     public void setViewSize(int width, int height) {
208         mViewWidth = width;
209         mViewHeight = height;
210     }
211
212     public void setPosition(int centerX, int centerY, float scale) {
213         if (mCenterX == centerX && mCenterY == centerY
214                 && mScale == scale) return;
215         mCenterX = centerX;
216         mCenterY = centerY;
217         mScale = scale;
218         mLayoutTiles = true;
219         invalidate();
220     }
221
222     // Prepare the tiles we want to use for display.
223     //
224     // 1. Decide the tile level we want to use for display.
225     // 2. Decide the tile levels we want to keep as texture (in addition to
226     //    the one we use for display).
227     // 3. Recycle unused tiles.
228     // 4. Activate the tiles we want.
229     private void layoutTiles() {
230         if (mViewWidth == 0 || mViewHeight == 0 || !mLayoutTiles) {
231             return;
232         }
233         mLayoutTiles = false;
234
235         // The tile levels we want to keep as texture is in the range
236         // [fromLevel, endLevel).
237         int fromLevel;
238         int endLevel;
239
240         // We want to use a texture larger than or equal to the display size.
241         mLevel = Utils.clamp(Utils.floorLog2(1f / mScale), 0, mLevelCount);
242
243         // We want to keep one more tile level as texture in addition to what
244         // we use for display. So it can be faster when the scale moves to the
245         // next level. We choose the level closest to the current scale.
246         if (mLevel != mLevelCount) {
247             Rect range = mTileRange;
248             getRange(range, mCenterX, mCenterY, mLevel, mScale, mRotation);
249             mOffsetX = Math.round(mViewWidth / 2f + (range.left - mCenterX) * mScale);
250             mOffsetY = Math.round(mViewHeight / 2f + (range.top - mCenterY) * mScale);
251             fromLevel = mScale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel;
252         } else {
253             // Activate the tiles of the smallest two levels.
254             fromLevel = mLevel - 2;
255             mOffsetX = Math.round(mViewWidth / 2f - mCenterX * mScale);
256             mOffsetY = Math.round(mViewHeight / 2f - mCenterY * mScale);
257         }
258
259         fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2));
260         endLevel = Math.min(fromLevel + 2, mLevelCount);
261
262         Rect range[] = mActiveRange;
263         for (int i = fromLevel; i < endLevel; ++i) {
264             getRange(range[i - fromLevel], mCenterX, mCenterY, i, mRotation);
265         }
266
267         // If rotation is transient, don't update the tile.
268         if (mRotation % 90 != 0) return;
269
270         synchronized (mQueueLock) {
271             mDecodeQueue.clean();
272             mUploadQueue.clean();
273             mBackgroundTileUploaded = false;
274
275             // Recycle unused tiles: if the level of the active tile is outside the
276             // range [fromLevel, endLevel) or not in the visible range.
277             int n = mActiveTiles.size();
278             for (int i = 0; i < n; i++) {
279                 Tile tile = mActiveTiles.valueAt(i);
280                 int level = tile.mTileLevel;
281                 if (level < fromLevel || level >= endLevel
282                         || !range[level - fromLevel].contains(tile.mX, tile.mY)) {
283                     mActiveTiles.removeAt(i);
284                     i--;
285                     n--;
286                     recycleTile(tile);
287                 }
288             }
289         }
290
291         for (int i = fromLevel; i < endLevel; ++i) {
292             int size = mTileSize << i;
293             Rect r = range[i - fromLevel];
294             for (int y = r.top, bottom = r.bottom; y < bottom; y += size) {
295                 for (int x = r.left, right = r.right; x < right; x += size) {
296                     activateTile(x, y, i);
297                 }
298             }
299         }
300         invalidate();
301     }
302
303     private void invalidateTiles() {
304         synchronized (mQueueLock) {
305             mDecodeQueue.clean();
306             mUploadQueue.clean();
307
308             // TODO disable decoder
309             int n = mActiveTiles.size();
310             for (int i = 0; i < n; i++) {
311                 Tile tile = mActiveTiles.valueAt(i);
312                 recycleTile(tile);
313             }
314             mActiveTiles.clear();
315         }
316     }
317
318     private void getRange(Rect out, int cX, int cY, int level, int rotation) {
319         getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation);
320     }
321
322     // If the bitmap is scaled by the given factor "scale", return the
323     // rectangle containing visible range. The left-top coordinate returned is
324     // aligned to the tile boundary.
325     //
326     // (cX, cY) is the point on the original bitmap which will be put in the
327     // center of the ImageViewer.
328     private void getRange(Rect out,
329             int cX, int cY, int level, float scale, int rotation) {
330
331         double radians = Math.toRadians(-rotation);
332         double w = mViewWidth;
333         double h = mViewHeight;
334
335         double cos = Math.cos(radians);
336         double sin = Math.sin(radians);
337         int width = (int) Math.ceil(Math.max(
338                 Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h)));
339         int height = (int) Math.ceil(Math.max(
340                 Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h)));
341
342         int left = (int) Math.floor(cX - width / (2f * scale));
343         int top = (int) Math.floor(cY - height / (2f * scale));
344         int right = (int) Math.ceil(left + width / scale);
345         int bottom = (int) Math.ceil(top + height / scale);
346
347         // align the rectangle to tile boundary
348         int size = mTileSize << level;
349         left = Math.max(0, size * (left / size));
350         top = Math.max(0, size * (top / size));
351         right = Math.min(mImageWidth, right);
352         bottom = Math.min(mImageHeight, bottom);
353
354         out.set(left, top, right, bottom);
355     }
356
357     public void freeTextures() {
358         mLayoutTiles = true;
359
360         synchronized (mQueueLock) {
361             mUploadQueue.clean();
362             mDecodeQueue.clean();
363             Tile tile = mRecycledQueue.pop();
364             while (tile != null) {
365                 tile.recycle();
366                 tile = mRecycledQueue.pop();
367             }
368         }
369
370         int n = mActiveTiles.size();
371         for (int i = 0; i < n; i++) {
372             Tile texture = mActiveTiles.valueAt(i);
373             texture.recycle();
374         }
375         mActiveTiles.clear();
376         mTileRange.set(0, 0, 0, 0);
377
378         if (sTilePool != null) sTilePool.clear();
379     }
380
381     public void draw(GLCanvas canvas) {
382         layoutTiles();
383         uploadTiles(canvas);
384
385         mUploadQuota = UPLOAD_LIMIT;
386         mRenderComplete = true;
387
388         int level = mLevel;
389         int rotation = mRotation;
390         int flags = 0;
391         if (rotation != 0) flags |= GLCanvas.SAVE_FLAG_MATRIX;
392
393         if (flags != 0) {
394             canvas.save(flags);
395             if (rotation != 0) {
396                 int centerX = mViewWidth / 2, centerY = mViewHeight / 2;
397                 canvas.translate(centerX, centerY);
398                 canvas.rotate(rotation, 0, 0, 1);
399                 canvas.translate(-centerX, -centerY);
400             }
401         }
402         try {
403             if (level != mLevelCount) {
404                 int size = (mTileSize << level);
405                 float length = size * mScale;
406                 Rect r = mTileRange;
407
408                 for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) {
409                     float y = mOffsetY + i * length;
410                     for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) {
411                         float x = mOffsetX + j * length;
412                         drawTile(canvas, tx, ty, level, x, y, length);
413                     }
414                 }
415             }
416         } finally {
417             if (flags != 0) canvas.restore();
418         }
419
420         if (mRenderComplete) {
421             if (!mBackgroundTileUploaded) {
422                 uploadBackgroundTiles(canvas);
423             }
424         } else {
425             invalidate();
426         }
427     }
428
429     private void uploadBackgroundTiles(GLCanvas canvas) {
430         mBackgroundTileUploaded = true;
431         int n = mActiveTiles.size();
432         for (int i = 0; i < n; i++) {
433             Tile tile = mActiveTiles.valueAt(i);
434             if (!tile.isContentValid()) {
435                 queueForDecode(tile);
436             }
437         }
438     }
439
440     private void queueForUpload(Tile tile) {
441         synchronized (mQueueLock) {
442             mUploadQueue.push(tile);
443         }
444         invalidate();
445         // TODO
446 //        if (mTileUploader.mActive.compareAndSet(false, true)) {
447 //            getGLRoot().addOnGLIdleListener(mTileUploader);
448 //        }
449     }
450
451    private void queueForDecode(Tile tile) {
452        synchronized (mQueueLock) {
453            if (tile.mTileState == STATE_ACTIVATED) {
454                tile.mTileState = STATE_IN_QUEUE;
455                if (mDecodeQueue.push(tile)) {
456                    mQueueLock.notifyAll();
457                }
458            }
459        }
460     }
461
462     private boolean decodeTile(Tile tile) {
463         synchronized (mQueueLock) {
464             if (tile.mTileState != STATE_IN_QUEUE) return false;
465             tile.mTileState = STATE_DECODING;
466         }
467         boolean decodeComplete = tile.decode();
468         synchronized (mQueueLock) {
469             if (tile.mTileState == STATE_RECYCLING) {
470                 tile.mTileState = STATE_RECYCLED;
471                 if (tile.mDecodedTile != null) {
472                     if (sTilePool != null) sTilePool.put(tile.mDecodedTile);
473                     tile.mDecodedTile = null;
474                 }
475                 mRecycledQueue.push(tile);
476                 return false;
477             }
478             tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL;
479             return decodeComplete;
480         }
481     }
482
483     private Tile obtainTile(int x, int y, int level) {
484         synchronized (mQueueLock) {
485             Tile tile = mRecycledQueue.pop();
486             if (tile != null) {
487                 tile.mTileState = STATE_ACTIVATED;
488                 tile.update(x, y, level);
489                 return tile;
490             }
491             return new Tile(x, y, level);
492         }
493     }
494
495     private void recycleTile(Tile tile) {
496         synchronized (mQueueLock) {
497             if (tile.mTileState == STATE_DECODING) {
498                 tile.mTileState = STATE_RECYCLING;
499                 return;
500             }
501             tile.mTileState = STATE_RECYCLED;
502             if (tile.mDecodedTile != null) {
503                 if (sTilePool != null) sTilePool.put(tile.mDecodedTile);
504                 tile.mDecodedTile = null;
505             }
506             mRecycledQueue.push(tile);
507         }
508     }
509
510     private void activateTile(int x, int y, int level) {
511         long key = makeTileKey(x, y, level);
512         Tile tile = mActiveTiles.get(key);
513         if (tile != null) {
514             if (tile.mTileState == STATE_IN_QUEUE) {
515                 tile.mTileState = STATE_ACTIVATED;
516             }
517             return;
518         }
519         tile = obtainTile(x, y, level);
520         mActiveTiles.put(key, tile);
521     }
522
523     private Tile getTile(int x, int y, int level) {
524         return mActiveTiles.get(makeTileKey(x, y, level));
525     }
526
527     private static long makeTileKey(int x, int y, int level) {
528         long result = x;
529         result = (result << 16) | y;
530         result = (result << 16) | level;
531         return result;
532     }
533
534     private void uploadTiles(GLCanvas canvas) {
535         int quota = UPLOAD_LIMIT;
536         Tile tile = null;
537         while (quota > 0) {
538             synchronized (mQueueLock) {
539                 tile = mUploadQueue.pop();
540             }
541             if (tile == null) break;
542             if (!tile.isContentValid()) {
543                 Utils.assertTrue(tile.mTileState == STATE_DECODED);
544                 tile.updateContent(canvas);
545                 --quota;
546             }
547         }
548         if (tile != null) {
549             invalidate();
550         }
551     }
552
553     // Draw the tile to a square at canvas that locates at (x, y) and
554     // has a side length of length.
555     private void drawTile(GLCanvas canvas,
556             int tx, int ty, int level, float x, float y, float length) {
557         RectF source = mSourceRect;
558         RectF target = mTargetRect;
559         target.set(x, y, x + length, y + length);
560         source.set(0, 0, mTileSize, mTileSize);
561
562         Tile tile = getTile(tx, ty, level);
563         if (tile != null) {
564             if (!tile.isContentValid()) {
565                 if (tile.mTileState == STATE_DECODED) {
566                     if (mUploadQuota > 0) {
567                         --mUploadQuota;
568                         tile.updateContent(canvas);
569                     } else {
570                         mRenderComplete = false;
571                     }
572                 } else if (tile.mTileState != STATE_DECODE_FAIL){
573                     mRenderComplete = false;
574                     queueForDecode(tile);
575                 }
576             }
577             drawTile(tile, canvas, source, target);
578         }
579     }
580
581     private boolean drawTile(
582             Tile tile, GLCanvas canvas, RectF source, RectF target) {
583         while (true) {
584             if (tile.isContentValid()) {
585                 canvas.drawTexture(tile, source, target);
586                 return true;
587             }
588
589             // Parent can be divided to four quads and tile is one of the four.
590             Tile parent = tile.getParentTile();
591             if (parent == null) return false;
592             if (tile.mX == parent.mX) {
593                 source.left /= 2f;
594                 source.right /= 2f;
595             } else {
596                 source.left = (mTileSize + source.left) / 2f;
597                 source.right = (mTileSize + source.right) / 2f;
598             }
599             if (tile.mY == parent.mY) {
600                 source.top /= 2f;
601                 source.bottom /= 2f;
602             } else {
603                 source.top = (mTileSize + source.top) / 2f;
604                 source.bottom = (mTileSize + source.bottom) / 2f;
605             }
606             tile = parent;
607         }
608     }
609
610     private class Tile extends UploadedTexture {
611         public int mX;
612         public int mY;
613         public int mTileLevel;
614         public Tile mNext;
615         public Bitmap mDecodedTile;
616         public volatile int mTileState = STATE_ACTIVATED;
617
618         public Tile(int x, int y, int level) {
619             mX = x;
620             mY = y;
621             mTileLevel = level;
622         }
623
624         @Override
625         protected void onFreeBitmap(Bitmap bitmap) {
626             if (sTilePool != null) sTilePool.put(bitmap);
627         }
628
629         boolean decode() {
630             // Get a tile from the original image. The tile is down-scaled
631             // by (1 << mTilelevel) from a region in the original image.
632             try {
633                 Bitmap reuse = sTilePool.get(mTileSize, mTileSize);
634                 mDecodedTile = mModel.getTile(mTileLevel, mX, mY, reuse);
635             } catch (Throwable t) {
636                 Log.w(TAG, "fail to decode tile", t);
637             }
638             return mDecodedTile != null;
639         }
640
641         @Override
642         protected Bitmap onGetBitmap() {
643             Utils.assertTrue(mTileState == STATE_DECODED);
644
645             // We need to override the width and height, so that we won't
646             // draw beyond the boundaries.
647             int rightEdge = ((mImageWidth - mX) >> mTileLevel);
648             int bottomEdge = ((mImageHeight - mY) >> mTileLevel);
649             setSize(Math.min(mTileSize, rightEdge), Math.min(mTileSize, bottomEdge));
650
651             Bitmap bitmap = mDecodedTile;
652             mDecodedTile = null;
653             mTileState = STATE_ACTIVATED;
654             return bitmap;
655         }
656
657         // We override getTextureWidth() and getTextureHeight() here, so the
658         // texture can be re-used for different tiles regardless of the actual
659         // size of the tile (which may be small because it is a tile at the
660         // boundary).
661         @Override
662         public int getTextureWidth() {
663             return mTileSize;
664         }
665
666         @Override
667         public int getTextureHeight() {
668             return mTileSize;
669         }
670
671         public void update(int x, int y, int level) {
672             mX = x;
673             mY = y;
674             mTileLevel = level;
675             invalidateContent();
676         }
677
678         public Tile getParentTile() {
679             if (mTileLevel + 1 == mLevelCount) return null;
680             int size = mTileSize << (mTileLevel + 1);
681             int x = size * (mX / size);
682             int y = size * (mY / size);
683             return getTile(x, y, mTileLevel + 1);
684         }
685
686         @Override
687         public String toString() {
688             return String.format("tile(%s, %s, %s / %s)",
689                     mX / mTileSize, mY / mTileSize, mLevel, mLevelCount);
690         }
691     }
692
693     private static class TileQueue {
694         private Tile mHead;
695
696         public Tile pop() {
697             Tile tile = mHead;
698             if (tile != null) mHead = tile.mNext;
699             return tile;
700         }
701
702         public boolean push(Tile tile) {
703             boolean wasEmpty = mHead == null;
704             tile.mNext = mHead;
705             mHead = tile;
706             return wasEmpty;
707         }
708
709         public void clean() {
710             mHead = null;
711         }
712     }
713
714     private class TileDecoder extends Thread {
715
716         public void finishAndWait() {
717             interrupt();
718             try {
719                 join();
720             } catch (InterruptedException e) {
721                 Log.w(TAG, "Interrupted while waiting for TileDecoder thread to finish!");
722             }
723         }
724
725         private Tile waitForTile() throws InterruptedException {
726             synchronized(mQueueLock) {
727                 while (true) {
728                     Tile tile = mDecodeQueue.pop();
729                     if (tile != null) {
730                         return tile;
731                     }
732                     mQueueLock.wait();
733                 }
734             }
735         }
736
737         @Override
738         public void run() {
739             try {
740                 while (!isInterrupted()) {
741                     Tile tile = waitForTile();
742                     if (decodeTile(tile)) {
743                         queueForUpload(tile);
744                     }
745                 }
746             } catch (InterruptedException ex) {
747             }
748         }
749
750     }
751 }