TileImageView.java revision 6dc3866ac10cc6bf38e73f098fe1988d1a6eecd4
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.gallery3d.ui;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.Point;
22import android.graphics.Rect;
23import android.graphics.RectF;
24import android.support.v4.util.LongSparseArray;
25import android.util.DisplayMetrics;
26import android.util.FloatMath;
27import android.view.WindowManager;
28
29import com.android.gallery3d.app.GalleryContext;
30import com.android.gallery3d.common.ApiHelper;
31import com.android.gallery3d.common.Utils;
32import com.android.gallery3d.data.BitmapPool;
33import com.android.gallery3d.data.DecodeUtils;
34import com.android.gallery3d.util.Future;
35import com.android.gallery3d.util.ThreadPool;
36import com.android.gallery3d.util.ThreadPool.CancelListener;
37import com.android.gallery3d.util.ThreadPool.JobContext;
38
39import java.util.concurrent.atomic.AtomicBoolean;
40
41public class TileImageView extends GLView {
42    public static final int SIZE_UNKNOWN = -1;
43
44    @SuppressWarnings("unused")
45    private static final String TAG = "TileImageView";
46    private static final int UPLOAD_LIMIT = 1;
47
48    // TILE_SIZE must be 2^N
49    private static int sTileSize;
50
51    private static BitmapPool sTilePool;
52
53    /*
54     *  This is the tile state in the CPU side.
55     *  Life of a Tile:
56     *      ACTIVATED (initial state)
57     *              --> IN_QUEUE - by queueForDecode()
58     *              --> RECYCLED - by recycleTile()
59     *      IN_QUEUE --> DECODING - by decodeTile()
60     *               --> RECYCLED - by recycleTile)
61     *      DECODING --> RECYCLING - by recycleTile()
62     *               --> DECODED  - by decodeTile()
63     *               --> DECODE_FAIL - by decodeTile()
64     *      RECYCLING --> RECYCLED - by decodeTile()
65     *      DECODED --> ACTIVATED - (after the decoded bitmap is uploaded)
66     *      DECODED --> RECYCLED - by recycleTile()
67     *      DECODE_FAIL -> RECYCLED - by recycleTile()
68     *      RECYCLED --> ACTIVATED - by obtainTile()
69     */
70    private static final int STATE_ACTIVATED = 0x01;
71    private static final int STATE_IN_QUEUE = 0x02;
72    private static final int STATE_DECODING = 0x04;
73    private static final int STATE_DECODED = 0x08;
74    private static final int STATE_DECODE_FAIL = 0x10;
75    private static final int STATE_RECYCLING = 0x20;
76    private static final int STATE_RECYCLED = 0x40;
77
78    private TileSource mModel;
79    private ScreenNail mScreenNail;
80    protected int mLevelCount;  // cache the value of mScaledBitmaps.length
81
82    // The mLevel variable indicates which level of bitmap we should use.
83    // Level 0 means the original full-sized bitmap, and a larger value means
84    // a smaller scaled bitmap (The width and height of each scaled bitmap is
85    // half size of the previous one). If the value is in [0, mLevelCount), we
86    // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value
87    // is mLevelCount, and that means we use mScreenNail for display.
88    private int mLevel = 0;
89
90    // The offsets of the (left, top) of the upper-left tile to the (left, top)
91    // of the view.
92    private int mOffsetX;
93    private int mOffsetY;
94
95    private int mUploadQuota;
96    private boolean mRenderComplete;
97
98    private final RectF mSourceRect = new RectF();
99    private final RectF mTargetRect = new RectF();
100
101    private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>();
102
103    // The following three queue is guarded by TileImageView.this
104    private final TileQueue mRecycledQueue = new TileQueue();
105    private final TileQueue mUploadQueue = new TileQueue();
106    private final TileQueue mDecodeQueue = new TileQueue();
107
108    // The width and height of the full-sized bitmap
109    protected int mImageWidth = SIZE_UNKNOWN;
110    protected int mImageHeight = SIZE_UNKNOWN;
111
112    protected int mCenterX;
113    protected int mCenterY;
114    protected float mScale;
115    protected int mRotation;
116
117    // Temp variables to avoid memory allocation
118    private final Rect mTileRange = new Rect();
119    private final Rect mActiveRange[] = {new Rect(), new Rect()};
120
121    private final TileUploader mTileUploader = new TileUploader();
122    private boolean mIsTextureFreed;
123    private Future<Void> mTileDecoder;
124    private final ThreadPool mThreadPool;
125    private boolean mBackgroundTileUploaded;
126
127    public static interface TileSource {
128        public int getLevelCount();
129        public ScreenNail getScreenNail();
130        public int getImageWidth();
131        public int getImageHeight();
132
133        // The tile returned by this method can be specified this way: Assuming
134        // the image size is (width, height), first take the intersection of (0,
135        // 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
136        // in extending the region, we found some part of the region are outside
137        // the image, those pixels are filled with black.
138        //
139        // If level > 0, it does the same operation on a down-scaled version of
140        // the original image (down-scaled by a factor of 2^level), but (x, y)
141        // still refers to the coordinate on the original image.
142        //
143        // The method would be called in another thread.
144        public Bitmap getTile(int level, int x, int y, int tileSize,
145                BitmapPool pool);
146    }
147
148    public static boolean isHighResolution(Context context) {
149        DisplayMetrics metrics = new DisplayMetrics();
150        WindowManager wm = (WindowManager)
151                context.getSystemService(Context.WINDOW_SERVICE);
152        wm.getDefaultDisplay().getMetrics(metrics);
153        return metrics.heightPixels > 2048 ||  metrics.widthPixels > 2048;
154    }
155
156    public TileImageView(GalleryContext context) {
157        mThreadPool = context.getThreadPool();
158        mTileDecoder = mThreadPool.submit(new TileDecoder());
159        if (sTileSize == 0) {
160            if (isHighResolution(context.getAndroidContext())) {
161                sTileSize = 512 ;
162            } else {
163                sTileSize = 256;
164            }
165            sTilePool =
166                    ApiHelper.HAS_REUSING_BITMAP_IN_BITMAP_REGION_DECODER
167                    ? new BitmapPool(sTileSize, sTileSize, 128)
168                    : null;
169        }
170    }
171
172    public void setModel(TileSource model) {
173        mModel = model;
174        if (model != null) notifyModelInvalidated();
175    }
176
177    public void setScreenNail(ScreenNail s) {
178        mScreenNail = s;
179    }
180
181    public void notifyModelInvalidated() {
182        invalidateTiles();
183        if (mModel == null) {
184            mScreenNail = null;
185            mImageWidth = 0;
186            mImageHeight = 0;
187            mLevelCount = 0;
188        } else {
189            setScreenNail(mModel.getScreenNail());
190            mImageWidth = mModel.getImageWidth();
191            mImageHeight = mModel.getImageHeight();
192            mLevelCount = mModel.getLevelCount();
193        }
194        layoutTiles(mCenterX, mCenterY, mScale, mRotation);
195        invalidate();
196    }
197
198    @Override
199    protected void onLayout(
200            boolean changeSize, int left, int top, int right, int bottom) {
201        super.onLayout(changeSize, left, top, right, bottom);
202        if (changeSize) layoutTiles(mCenterX, mCenterY, mScale, mRotation);
203    }
204
205    // Prepare the tiles we want to use for display.
206    //
207    // 1. Decide the tile level we want to use for display.
208    // 2. Decide the tile levels we want to keep as texture (in addition to
209    //    the one we use for display).
210    // 3. Recycle unused tiles.
211    // 4. Activate the tiles we want.
212    private void layoutTiles(int centerX, int centerY, float scale, int rotation) {
213        // The width and height of this view.
214        int width = getWidth();
215        int height = getHeight();
216
217        // The tile levels we want to keep as texture is in the range
218        // [fromLevel, endLevel).
219        int fromLevel;
220        int endLevel;
221
222        // We want to use a texture larger than or equal to the display size.
223        mLevel = Utils.clamp(Utils.floorLog2(1f / scale), 0, mLevelCount);
224
225        // We want to keep one more tile level as texture in addition to what
226        // we use for display. So it can be faster when the scale moves to the
227        // next level. We choose a level closer to the current scale.
228        if (mLevel != mLevelCount) {
229            Rect range = mTileRange;
230            getRange(range, centerX, centerY, mLevel, scale, rotation);
231            mOffsetX = Math.round(width / 2f + (range.left - centerX) * scale);
232            mOffsetY = Math.round(height / 2f + (range.top - centerY) * scale);
233            fromLevel = scale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel;
234        } else {
235            // Activate the tiles of the smallest two levels.
236            fromLevel = mLevel - 2;
237            mOffsetX = Math.round(width / 2f - centerX * scale);
238            mOffsetY = Math.round(height / 2f - centerY * scale);
239        }
240
241        fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2));
242        endLevel = Math.min(fromLevel + 2, mLevelCount);
243
244        Rect range[] = mActiveRange;
245        for (int i = fromLevel; i < endLevel; ++i) {
246            getRange(range[i - fromLevel], centerX, centerY, i, rotation);
247        }
248
249        // If rotation is transient, don't update the tile.
250        if (rotation % 90 != 0) return;
251
252        synchronized (this) {
253            mDecodeQueue.clean();
254            mUploadQueue.clean();
255            mBackgroundTileUploaded = false;
256
257            // Recycle unused tiles: if the level of the active tile is outside the
258            // range [fromLevel, endLevel) or not in the visible range.
259            int n = mActiveTiles.size();
260            for (int i = 0; i < n; i++) {
261                Tile tile = mActiveTiles.valueAt(i);
262                int level = tile.mTileLevel;
263                if (level < fromLevel || level >= endLevel
264                        || !range[level - fromLevel].contains(tile.mX, tile.mY)) {
265                    mActiveTiles.removeAt(i);
266                    i--;
267                    n--;
268                    recycleTile(tile);
269                }
270            }
271        }
272
273        for (int i = fromLevel; i < endLevel; ++i) {
274            int size = sTileSize << i;
275            Rect r = range[i - fromLevel];
276            for (int y = r.top, bottom = r.bottom; y < bottom; y += size) {
277                for (int x = r.left, right = r.right; x < right; x += size) {
278                    activateTile(x, y, i);
279                }
280            }
281        }
282        invalidate();
283    }
284
285    protected synchronized void invalidateTiles() {
286        mDecodeQueue.clean();
287        mUploadQueue.clean();
288
289        // TODO disable decoder
290        int n = mActiveTiles.size();
291        for (int i = 0; i < n; i++) {
292            Tile tile = mActiveTiles.valueAt(i);
293            recycleTile(tile);
294        }
295        mActiveTiles.clear();
296    }
297
298    private void getRange(Rect out, int cX, int cY, int level, int rotation) {
299        getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation);
300    }
301
302    // If the bitmap is scaled by the given factor "scale", return the
303    // rectangle containing visible range. The left-top coordinate returned is
304    // aligned to the tile boundary.
305    //
306    // (cX, cY) is the point on the original bitmap which will be put in the
307    // center of the ImageViewer.
308    private void getRange(Rect out,
309            int cX, int cY, int level, float scale, int rotation) {
310
311        double radians = Math.toRadians(-rotation);
312        double w = getWidth();
313        double h = getHeight();
314
315        double cos = Math.cos(radians);
316        double sin = Math.sin(radians);
317        int width = (int) Math.ceil(Math.max(
318                Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h)));
319        int height = (int) Math.ceil(Math.max(
320                Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h)));
321
322        int left = (int) FloatMath.floor(cX - width / (2f * scale));
323        int top = (int) FloatMath.floor(cY - height / (2f * scale));
324        int right = (int) FloatMath.ceil(left + width / scale);
325        int bottom = (int) FloatMath.ceil(top + height / scale);
326
327        // align the rectangle to tile boundary
328        int size = sTileSize << level;
329        left = Math.max(0, size * (left / size));
330        top = Math.max(0, size * (top / size));
331        right = Math.min(mImageWidth, right);
332        bottom = Math.min(mImageHeight, bottom);
333
334        out.set(left, top, right, bottom);
335    }
336
337    // Calculate where the center of the image is, in the view coordinates.
338    public void getImageCenter(Point center) {
339        // The width and height of this view.
340        int viewW = getWidth();
341        int viewH = getHeight();
342
343        // The distance between the center of the view to the center of the
344        // bitmap, in bitmap units. (mCenterX and mCenterY are the bitmap
345        // coordinates correspond to the center of view)
346        int distW, distH;
347        if (mRotation % 180 == 0) {
348            distW = mImageWidth / 2 - mCenterX;
349            distH = mImageHeight / 2 - mCenterY;
350        } else {
351            distW = mImageHeight / 2 - mCenterY;
352            distH = mImageWidth / 2 - mCenterX;
353        }
354
355        // Convert to view coordinates. mScale translates from bitmap units to
356        // view units.
357        center.x = Math.round(viewW / 2f + distW * mScale);
358        center.y = Math.round(viewH / 2f + distH * mScale);
359    }
360
361    public boolean setPosition(int centerX, int centerY, float scale, int rotation) {
362        if (mCenterX == centerX && mCenterY == centerY
363                && mScale == scale && mRotation == rotation) return false;
364        mCenterX = centerX;
365        mCenterY = centerY;
366        mScale = scale;
367        mRotation = rotation;
368        layoutTiles(centerX, centerY, scale, rotation);
369        invalidate();
370        return true;
371    }
372
373    public void freeTextures() {
374        mIsTextureFreed = true;
375
376        if (mTileDecoder != null) {
377            mTileDecoder.cancel();
378            mTileDecoder.get();
379            mTileDecoder = null;
380        }
381
382        int n = mActiveTiles.size();
383        for (int i = 0; i < n; i++) {
384            Tile texture = mActiveTiles.valueAt(i);
385            texture.recycle();
386        }
387        mActiveTiles.clear();
388        mTileRange.set(0, 0, 0, 0);
389
390        synchronized (this) {
391            mUploadQueue.clean();
392            mDecodeQueue.clean();
393            Tile tile = mRecycledQueue.pop();
394            while (tile != null) {
395                tile.recycle();
396                tile = mRecycledQueue.pop();
397            }
398        }
399        setScreenNail(null);
400        if (sTilePool != null) sTilePool.clear();
401    }
402
403    public void prepareTextures() {
404        if (mTileDecoder == null) {
405            mTileDecoder = mThreadPool.submit(new TileDecoder());
406        }
407        if (mIsTextureFreed) {
408            layoutTiles(mCenterX, mCenterY, mScale, mRotation);
409            mIsTextureFreed = false;
410            setScreenNail(mModel == null ? null : mModel.getScreenNail());
411        }
412    }
413
414    @Override
415    protected void render(GLCanvas canvas) {
416        mUploadQuota = UPLOAD_LIMIT;
417        mRenderComplete = true;
418
419        int level = mLevel;
420        int rotation = mRotation;
421        int flags = 0;
422        if (rotation != 0) flags |= GLCanvas.SAVE_FLAG_MATRIX;
423
424        if (flags != 0) {
425            canvas.save(flags);
426            if (rotation != 0) {
427                int centerX = getWidth() / 2, centerY = getHeight() / 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 && !isScreenNailAnimating()) {
435                if (mScreenNail != null) {
436                    mScreenNail.noDraw();
437                }
438
439                int size = (sTileSize << 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 (mScreenNail != null) {
451                mScreenNail.draw(canvas, mOffsetX, mOffsetY,
452                        Math.round(mImageWidth * mScale),
453                        Math.round(mImageHeight * mScale));
454                if (isScreenNailAnimating()) {
455                    invalidate();
456                }
457            }
458        } finally {
459            if (flags != 0) canvas.restore();
460        }
461
462        if (mRenderComplete) {
463            if (!mBackgroundTileUploaded) uploadBackgroundTiles(canvas);
464        } else {
465            invalidate();
466        }
467    }
468
469    private boolean isScreenNailAnimating() {
470        return (mScreenNail instanceof TiledScreenNail)
471                && ((TiledScreenNail) mScreenNail).isAnimating();
472    }
473
474    private void uploadBackgroundTiles(GLCanvas canvas) {
475        mBackgroundTileUploaded = true;
476        int n = mActiveTiles.size();
477        for (int i = 0; i < n; i++) {
478            Tile tile = mActiveTiles.valueAt(i);
479            if (!tile.isContentValid()) queueForDecode(tile);
480        }
481    }
482
483    void queueForUpload(Tile tile) {
484        synchronized (this) {
485            mUploadQueue.push(tile);
486        }
487        if (mTileUploader.mActive.compareAndSet(false, true)) {
488            getGLRoot().addOnGLIdleListener(mTileUploader);
489        }
490    }
491
492    synchronized void queueForDecode(Tile tile) {
493        if (tile.mTileState == STATE_ACTIVATED) {
494            tile.mTileState = STATE_IN_QUEUE;
495            if (mDecodeQueue.push(tile)) notifyAll();
496        }
497    }
498
499    boolean decodeTile(Tile tile) {
500        synchronized (this) {
501            if (tile.mTileState != STATE_IN_QUEUE) return false;
502            tile.mTileState = STATE_DECODING;
503        }
504        boolean decodeComplete = tile.decode();
505        synchronized (this) {
506            if (tile.mTileState == STATE_RECYCLING) {
507                tile.mTileState = STATE_RECYCLED;
508                if (tile.mDecodedTile != null) {
509                    if (sTilePool != null) sTilePool.recycle(tile.mDecodedTile);
510                    tile.mDecodedTile = null;
511                }
512                mRecycledQueue.push(tile);
513                return false;
514            }
515            tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL;
516            return decodeComplete;
517        }
518    }
519
520    private synchronized Tile obtainTile(int x, int y, int level) {
521        Tile tile = mRecycledQueue.pop();
522        if (tile != null) {
523            tile.mTileState = STATE_ACTIVATED;
524            tile.update(x, y, level);
525            return tile;
526        }
527        return new Tile(x, y, level);
528    }
529
530    synchronized void recycleTile(Tile tile) {
531        if (tile.mTileState == STATE_DECODING) {
532            tile.mTileState = STATE_RECYCLING;
533            return;
534        }
535        tile.mTileState = STATE_RECYCLED;
536        if (tile.mDecodedTile != null) {
537            if (sTilePool != null) sTilePool.recycle(tile.mDecodedTile);
538            tile.mDecodedTile = null;
539        }
540        mRecycledQueue.push(tile);
541    }
542
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
556    private Tile getTile(int x, int y, int level) {
557        return mActiveTiles.get(makeTileKey(x, y, level));
558    }
559
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
567    private class TileUploader implements GLRoot.OnGLIdleListener {
568        AtomicBoolean mActive = new AtomicBoolean(false);
569
570        @Override
571        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
572            // Skips uploading if there is a pending rendering request.
573            // Returns true to keep uploading in next rendering loop.
574            if (renderRequested) return true;
575            int quota = UPLOAD_LIMIT;
576            Tile tile = null;
577            while (quota > 0) {
578                synchronized (TileImageView.this) {
579                    tile = mUploadQueue.pop();
580                }
581                if (tile == null) break;
582                if (!tile.isContentValid()) {
583                    boolean hasBeenLoaded = tile.isLoaded();
584                    Utils.assertTrue(tile.mTileState == STATE_DECODED);
585                    tile.updateContent(canvas);
586                    if (!hasBeenLoaded) tile.draw(canvas, 0, 0);
587                    --quota;
588                }
589            }
590            if (tile == null) mActive.set(false);
591            return tile != null;
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.
597    public 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, sTileSize, sTileSize);
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)) return;
620        }
621        if (mScreenNail != null) {
622            int size = sTileSize << level;
623            float scaleX = (float) mScreenNail.getWidth() / mImageWidth;
624            float scaleY = (float) mScreenNail.getHeight() / mImageHeight;
625            source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
626                    (ty + size) * scaleY);
627            mScreenNail.draw(canvas, source, target);
628        }
629    }
630
631    static boolean drawTile(
632            Tile tile, GLCanvas canvas, RectF source, RectF target) {
633        while (true) {
634            if (tile.isContentValid()) {
635                canvas.drawTexture(tile, source, target);
636                return true;
637            }
638
639            // Parent can be divided to four quads and tile is one of the four.
640            Tile parent = tile.getParentTile();
641            if (parent == null) return false;
642            if (tile.mX == parent.mX) {
643                source.left /= 2f;
644                source.right /= 2f;
645            } else {
646                source.left = (sTileSize + source.left) / 2f;
647                source.right = (sTileSize + source.right) / 2f;
648            }
649            if (tile.mY == parent.mY) {
650                source.top /= 2f;
651                source.bottom /= 2f;
652            } else {
653                source.top = (sTileSize + source.top) / 2f;
654                source.bottom = (sTileSize + 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
668        public Tile(int x, int y, int level) {
669            mX = x;
670            mY = y;
671            mTileLevel = level;
672        }
673
674        @Override
675        protected void onFreeBitmap(Bitmap bitmap) {
676            if (sTilePool != null) sTilePool.recycle(bitmap);
677        }
678
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                mDecodedTile = DecodeUtils.ensureGLCompatibleBitmap(mModel.getTile(
684                        mTileLevel, mX, mY, sTileSize, sTilePool));
685            } catch (Throwable t) {
686                Log.w(TAG, "fail to decode tile", t);
687            }
688            return mDecodedTile != null;
689        }
690
691        @Override
692        protected Bitmap onGetBitmap() {
693            Utils.assertTrue(mTileState == STATE_DECODED);
694
695            // We need to override the width and height, so that we won't
696            // draw beyond the boundaries.
697            int rightEdge = ((mImageWidth - mX) >> mTileLevel);
698            int bottomEdge = ((mImageHeight - mY) >> mTileLevel);
699            setSize(Math.min(sTileSize, rightEdge), Math.min(sTileSize, bottomEdge));
700
701            Bitmap bitmap = mDecodedTile;
702            mDecodedTile = null;
703            mTileState = STATE_ACTIVATED;
704            return bitmap;
705        }
706
707        // We override getTextureWidth() and getTextureHeight() here, so the
708        // texture can be re-used for different tiles regardless of the actual
709        // size of the tile (which may be small because it is a tile at the
710        // boundary).
711        @Override
712        public int getTextureWidth() {
713            return sTileSize;
714        }
715
716        @Override
717        public int getTextureHeight() {
718            return sTileSize;
719        }
720
721        public void update(int x, int y, int level) {
722            mX = x;
723            mY = y;
724            mTileLevel = level;
725            invalidateContent();
726        }
727
728        public Tile getParentTile() {
729            if (mTileLevel + 1 == mLevelCount) return null;
730            int size = sTileSize << (mTileLevel + 1);
731            int x = size * (mX / size);
732            int y = size * (mY / size);
733            return getTile(x, y, mTileLevel + 1);
734        }
735
736        @Override
737        public String toString() {
738            return String.format("tile(%s, %s, %s / %s)",
739                    mX / sTileSize, mY / sTileSize, mLevel, mLevelCount);
740        }
741    }
742
743    private static class TileQueue {
744        private Tile mHead;
745
746        public Tile pop() {
747            Tile tile = mHead;
748            if (tile != null) mHead = tile.mNext;
749            return tile;
750        }
751
752        public boolean push(Tile tile) {
753            boolean wasEmpty = mHead == null;
754            tile.mNext = mHead;
755            mHead = tile;
756            return wasEmpty;
757        }
758
759        public void clean() {
760            mHead = null;
761        }
762    }
763
764    private class TileDecoder implements ThreadPool.Job<Void> {
765
766        private CancelListener mNotifier = new CancelListener() {
767            @Override
768            public void onCancel() {
769                synchronized (TileImageView.this) {
770                    TileImageView.this.notifyAll();
771                }
772            }
773        };
774
775        @Override
776        public Void run(JobContext jc) {
777            jc.setMode(ThreadPool.MODE_NONE);
778            jc.setCancelListener(mNotifier);
779            while (!jc.isCancelled()) {
780                Tile tile = null;
781                synchronized(TileImageView.this) {
782                    tile = mDecodeQueue.pop();
783                    if (tile == null && !jc.isCancelled()) {
784                        Utils.waitWithoutInterrupt(TileImageView.this);
785                    }
786                }
787                if (tile == null) continue;
788                if (decodeTile(tile)) queueForUpload(tile);
789            }
790            return null;
791        }
792    }
793}
794