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