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