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