Copy Tiling changes from G+

Change-Id: Id229728182a002c29699884289f1354b6cb6e714
diff --git a/src/com/android/photos/BitmapRegionTileSource.java b/src/com/android/photos/BitmapRegionTileSource.java
index 1c71151..d7d52f6 100644
--- a/src/com/android/photos/BitmapRegionTileSource.java
+++ b/src/com/android/photos/BitmapRegionTileSource.java
@@ -16,66 +16,131 @@
 
 package com.android.photos;
 
+import android.content.Context;
 import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
 import android.graphics.Rect;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
 import android.util.Log;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.BitmapTexture;
 import com.android.photos.views.TiledImageRenderer;
 
 import java.io.IOException;
 
+/**
+ * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
+ * {@link BitmapRegionDecoder} to wrap a local file
+ */
 public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
 
+    private static final String TAG = "BitmapRegionTileSource";
+
+    private static final boolean REUSE_BITMAP =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+    private static final int MAX_PREVIEW_SIZE = 1024;
+
     BitmapRegionDecoder mDecoder;
+    int mWidth;
+    int mHeight;
+    int mTileSize;
+    private BasicTexture mPreview;
+    private final int mRotation;
 
+    // For use only by getTile
+    private Rect mWantRegion = new Rect();
+    private Rect mOverlapRegion = new Rect();
+    private BitmapFactory.Options mOptions;
+    private Canvas mCanvas;
 
-    public BitmapRegionTileSource(String path) {
+    public BitmapRegionTileSource(Context context, String path, int previewSize, int rotation) {
+        mTileSize = TiledImageRenderer.suggestedTileSize(context);
+        mRotation = rotation;
         try {
             mDecoder = BitmapRegionDecoder.newInstance(path, true);
+            mWidth = mDecoder.getWidth();
+            mHeight = mDecoder.getHeight();
         } catch (IOException e) {
             Log.w("BitmapRegionTileSource", "ctor failed", e);
         }
+        mOptions = new BitmapFactory.Options();
+        mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        mOptions.inPreferQualityOverSpeed = true;
+        mOptions.inTempStorage = new byte[16 * 1024];
+        if (previewSize != 0) {
+            previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
+            // Although this is the same size as the Bitmap that is likely already
+            // loaded, the lifecycle is different and interactions are on a different
+            // thread. Thus to simplify, this source will decode its own bitmap.
+            int sampleSize = (int) Math.ceil(Math.max(
+                    mWidth / (float) previewSize, mHeight / (float) previewSize));
+            mOptions.inSampleSize = Math.max(sampleSize, 1);
+            Bitmap preview = mDecoder.decodeRegion(
+                    new Rect(0, 0, mWidth, mHeight), mOptions);
+            if (preview.getWidth() <= MAX_PREVIEW_SIZE && preview.getHeight() <= MAX_PREVIEW_SIZE) {
+                mPreview = new BitmapTexture(preview);
+            } else {
+                Log.w(TAG, String.format(
+                        "Failed to create preview of apropriate size! "
+                        + " in: %dx%d, sample: %d, out: %dx%d",
+                        mWidth, mHeight, sampleSize,
+                        preview.getWidth(), preview.getHeight()));
+            }
+        }
     }
 
     @Override
     public int getTileSize() {
-        return 256;
+        return mTileSize;
     }
 
     @Override
     public int getImageWidth() {
-        return mDecoder.getWidth();
+        return mWidth;
     }
 
     @Override
     public int getImageHeight() {
-        return mDecoder.getHeight();
+        return mHeight;
+    }
+
+    @Override
+    public BasicTexture getPreview() {
+        return mPreview;
+    }
+
+    @Override
+    public int getRotation() {
+        return mRotation;
     }
 
     @Override
     public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
         int tileSize = getTileSize();
-        int t = tileSize << level;
+        if (!REUSE_BITMAP) {
+            return getTileWithoutReusingBitmap(level, x, y, tileSize);
+        }
 
-        Rect wantRegion = new Rect(x, y, x + t, y + t);
+        int t = tileSize << level;
+        mWantRegion.set(x, y, x + t, y + t);
 
         if (bitmap == null) {
             bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
         }
 
-        BitmapFactory.Options options = new BitmapFactory.Options();
-        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
-        options.inPreferQualityOverSpeed = true;
-        options.inSampleSize =  (1 << level);
-        options.inBitmap = bitmap;
+        mOptions.inSampleSize = (1 << level);
+        mOptions.inBitmap = bitmap;
 
         try {
-            // In CropImage, we may call the decodeRegion() concurrently.
-            bitmap = mDecoder.decodeRegion(wantRegion, options);
+            bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
         } finally {
-            if (options.inBitmap != bitmap && options.inBitmap != null) {
-                options.inBitmap = null;
+            if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
+                mOptions.inBitmap = null;
             }
         }
 
@@ -84,4 +149,35 @@
         }
         return bitmap;
     }
+
+    private Bitmap getTileWithoutReusingBitmap(
+            int level, int x, int y, int tileSize) {
+
+        int t = tileSize << level;
+        mWantRegion.set(x, y, x + t, y + t);
+
+        mOverlapRegion.set(0, 0, mWidth, mHeight);
+
+        mOptions.inSampleSize = (1 << level);
+        Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
+
+        if (bitmap == null) {
+            Log.w(TAG, "fail in decoding region");
+        }
+
+        if (mWantRegion.equals(mOverlapRegion)) {
+            return bitmap;
+        }
+
+        Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
+        if (mCanvas == null) {
+            mCanvas = new Canvas();
+        }
+        mCanvas.setBitmap(result);
+        mCanvas.drawBitmap(bitmap,
+                (mOverlapRegion.left - mWantRegion.left) >> level,
+                (mOverlapRegion.top - mWantRegion.top) >> level, null);
+        mCanvas.setBitmap(null);
+        return result;
+    }
 }
diff --git a/src/com/android/photos/FullscreenViewer.java b/src/com/android/photos/FullscreenViewer.java
index 50ea1ba..a376139 100644
--- a/src/com/android/photos/FullscreenViewer.java
+++ b/src/com/android/photos/FullscreenViewer.java
@@ -31,7 +31,7 @@
 
         String path = getIntent().getData().toString();
         mTextureView = new TiledImageView(this);
-        mTextureView.setTileSource(new BitmapRegionTileSource(path));
+        mTextureView.setTileSource(new BitmapRegionTileSource(this, path, 0, 0), null);
         setContentView(mTextureView);
     }
 
diff --git a/src/com/android/photos/views/BlockingGLTextureView.java b/src/com/android/photos/views/BlockingGLTextureView.java
index c38f8f7..8a05051 100644
--- a/src/com/android/photos/views/BlockingGLTextureView.java
+++ b/src/com/android/photos/views/BlockingGLTextureView.java
@@ -16,7 +16,6 @@
 
 package com.android.photos.views;
 
-import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.SurfaceTexture;
 import android.opengl.GLSurfaceView.Renderer;
@@ -32,7 +31,9 @@
 import javax.microedition.khronos.egl.EGLSurface;
 import javax.microedition.khronos.opengles.GL10;
 
-
+/**
+ * A TextureView that supports blocking rendering for synchronous drawing
+ */
 public class BlockingGLTextureView extends TextureView
         implements SurfaceTextureListener {
 
@@ -90,7 +91,9 @@
     protected void finalize() throws Throwable {
         try {
             destroy();
-        } catch (Throwable t) {}
+        } catch (Throwable t) {
+            // Ignore
+        }
         super.finalize();
     }
 
@@ -135,8 +138,8 @@
         }
 
         EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) {
-            int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
-            return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
+            int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
+            return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attribList);
         }
 
         /**
@@ -161,7 +164,7 @@
              * We can now initialize EGL for that display
              */
             int[] version = new int[2];
-            if(!mEgl.eglInitialize(mEglDisplay, version)) {
+            if (!mEgl.eglInitialize(mEglDisplay, version)) {
                 throw new RuntimeException("eglInitialize failed");
             }
             mEglConfig = chooseEglConfig();
@@ -251,7 +254,7 @@
          * @return the EGL error code from eglSwapBuffers.
          */
         public int swap() {
-            if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
+            if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
                 return mEgl.eglGetError();
             }
             return EGL10.EGL_SUCCESS;
@@ -368,19 +371,24 @@
             exec(FINISH);
             try {
                 join();
-            } catch (InterruptedException e) {}
+            } catch (InterruptedException e) {
+                // Ignore
+            }
         }
 
         private void exec(int msgid) {
             synchronized (mLock) {
                 if (mExecMsgId != INVALID) {
-                    throw new IllegalArgumentException("Message already set - multithreaded access?");
+                    throw new IllegalArgumentException(
+                            "Message already set - multithreaded access?");
                 }
                 mExecMsgId = msgid;
                 mLock.notify();
                 try {
                     mLock.wait();
-                } catch (InterruptedException e) {}
+                } catch (InterruptedException e) {
+                    // Ignore
+                }
             }
         }
 
@@ -415,12 +423,15 @@
                     while (mExecMsgId == INVALID) {
                         try {
                             mLock.wait();
-                        } catch (InterruptedException e) {}
+                        } catch (InterruptedException e) {
+                            // Ignore
+                        }
                     }
                     handleMessageLocked(mExecMsgId);
                     mExecMsgId = INVALID;
                     mLock.notify();
                 }
+                mExecMsgId = FINISH;
             }
         }
     }
diff --git a/src/com/android/photos/views/TiledImageRenderer.java b/src/com/android/photos/views/TiledImageRenderer.java
index a1f7107..c4e493b 100644
--- a/src/com/android/photos/views/TiledImageRenderer.java
+++ b/src/com/android/photos/views/TiledImageRenderer.java
@@ -23,14 +23,19 @@
 import android.support.v4.util.LongSparseArray;
 import android.util.DisplayMetrics;
 import android.util.Log;
+import android.util.Pools.Pool;
+import android.util.Pools.SynchronizedPool;
 import android.view.View;
 import android.view.WindowManager;
 
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.BasicTexture;
 import com.android.gallery3d.glrenderer.GLCanvas;
 import com.android.gallery3d.glrenderer.UploadedTexture;
-import com.android.photos.data.GalleryBitmapPool;
 
+/**
+ * Handles laying out, decoding, and drawing of tiles in GL
+ */
 public class TiledImageRenderer {
     public static final int SIZE_UNKNOWN = -1;
 
@@ -62,12 +67,13 @@
     private static final int STATE_RECYCLING = 0x20;
     private static final int STATE_RECYCLED = 0x40;
 
-    private static GalleryBitmapPool sTilePool = GalleryBitmapPool.getInstance();
+    private static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64);
 
     // TILE_SIZE must be 2^N
     private int mTileSize;
 
     private TileSource mModel;
+    private BasicTexture mPreview;
     protected int mLevelCount;  // cache the value of mScaledBitmaps.length
 
     // The mLevel variable indicates which level of bitmap we should use.
@@ -116,22 +122,39 @@
     private int mViewWidth, mViewHeight;
     private View mParent;
 
+    /**
+     * Interface for providing tiles to a {@link TiledImageRenderer}
+     */
     public static interface TileSource {
+
+        /**
+         * If the source does not care about the tile size, it should use
+         * {@link TiledImageRenderer#suggestedTileSize(Context)}
+         */
         public int getTileSize();
         public int getImageWidth();
         public int getImageHeight();
+        public int getRotation();
 
-        // The tile returned by this method can be specified this way: Assuming
-        // the image size is (width, height), first take the intersection of (0,
-        // 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
-        // in extending the region, we found some part of the region is outside
-        // the image, those pixels are filled with black.
-        //
-        // If level > 0, it does the same operation on a down-scaled version of
-        // the original image (down-scaled by a factor of 2^level), but (x, y)
-        // still refers to the coordinate on the original image.
-        //
-        // The method would be called by the decoder thread.
+        /**
+         * Return a Preview image if available. This will be used as the base layer
+         * if higher res tiles are not yet available
+         */
+        public BasicTexture getPreview();
+
+        /**
+         * The tile returned by this method can be specified this way: Assuming
+         * the image size is (width, height), first take the intersection of (0,
+         * 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
+         * in extending the region, we found some part of the region is outside
+         * the image, those pixels are filled with black.
+         *
+         * If level > 0, it does the same operation on a down-scaled version of
+         * the original image (down-scaled by a factor of 2^level), but (x, y)
+         * still refers to the coordinate on the original image.
+         *
+         * The method would be called by the decoder thread.
+         */
         public Bitmap getTile(int level, int x, int y, Bitmap reuse);
     }
 
@@ -173,19 +196,23 @@
         if (mRotation != rotation) {
             mRotation = rotation;
             mLayoutTiles = true;
-            invalidate();
         }
     }
 
-    private static int calulateLevelCount(TileSource source) {
-        int levels = 1;
-        int maxDim = Math.max(source.getImageWidth(), source.getImageHeight());
-        int t = source.getTileSize();
-        while (t < maxDim) {
-            t <<= 1;
-            levels++;
+    private void calculateLevelCount() {
+        if (mPreview != null) {
+            mLevelCount = Math.max(0, Utils.ceilLog2(
+                mImageWidth / (float) mPreview.getWidth()));
+        } else {
+            int levels = 1;
+            int maxDim = Math.max(mImageWidth, mImageHeight);
+            int t = mTileSize;
+            while (t < maxDim) {
+                t <<= 1;
+                levels++;
+            }
+            mLevelCount = levels;
         }
-        return levels;
     }
 
     public void notifyModelInvalidated() {
@@ -194,14 +221,15 @@
             mImageWidth = 0;
             mImageHeight = 0;
             mLevelCount = 0;
+            mPreview = null;
         } else {
             mImageWidth = mModel.getImageWidth();
             mImageHeight = mModel.getImageHeight();
-            mLevelCount = calulateLevelCount(mModel);
+            mPreview = mModel.getPreview();
             mTileSize = mModel.getTileSize();
+            calculateLevelCount();
         }
         mLayoutTiles = true;
-        invalidate();
     }
 
     public void setViewSize(int width, int height) {
@@ -211,12 +239,13 @@
 
     public void setPosition(int centerX, int centerY, float scale) {
         if (mCenterX == centerX && mCenterY == centerY
-                && mScale == scale) return;
+                && mScale == scale) {
+            return;
+        }
         mCenterX = centerX;
         mCenterY = centerY;
         mScale = scale;
         mLayoutTiles = true;
-        invalidate();
     }
 
     // Prepare the tiles we want to use for display.
@@ -265,7 +294,9 @@
         }
 
         // If rotation is transient, don't update the tile.
-        if (mRotation % 90 != 0) return;
+        if (mRotation % 90 != 0) {
+            return;
+        }
 
         synchronized (mQueueLock) {
             mDecodeQueue.clean();
@@ -305,7 +336,7 @@
             mDecodeQueue.clean();
             mUploadQueue.clean();
 
-            // TODO disable decoder
+            // TODO(xx): disable decoder
             int n = mActiveTiles.size();
             for (int i = 0; i < n; i++) {
                 Tile tile = mActiveTiles.valueAt(i);
@@ -357,6 +388,7 @@
     public void freeTextures() {
         mLayoutTiles = true;
 
+        mTileDecoder.finishAndWait();
         synchronized (mQueueLock) {
             mUploadQueue.clean();
             mDecodeQueue.clean();
@@ -375,10 +407,10 @@
         mActiveTiles.clear();
         mTileRange.set(0, 0, 0, 0);
 
-        if (sTilePool != null) sTilePool.clear();
+        while (sTilePool.acquire() != null) {}
     }
 
-    public void draw(GLCanvas canvas) {
+    public boolean draw(GLCanvas canvas) {
         layoutTiles();
         uploadTiles(canvas);
 
@@ -388,7 +420,9 @@
         int level = mLevel;
         int rotation = mRotation;
         int flags = 0;
-        if (rotation != 0) flags |= GLCanvas.SAVE_FLAG_MATRIX;
+        if (rotation != 0) {
+            flags |= GLCanvas.SAVE_FLAG_MATRIX;
+        }
 
         if (flags != 0) {
             canvas.save(flags);
@@ -412,9 +446,15 @@
                         drawTile(canvas, tx, ty, level, x, y, length);
                     }
                 }
+            } else if (mPreview != null) {
+                mPreview.draw(canvas, mOffsetX, mOffsetY,
+                        Math.round(mImageWidth * mScale),
+                        Math.round(mImageHeight * mScale));
             }
         } finally {
-            if (flags != 0) canvas.restore();
+            if (flags != 0) {
+                canvas.restore();
+            }
         }
 
         if (mRenderComplete) {
@@ -424,6 +464,7 @@
         } else {
             invalidate();
         }
+        return mRenderComplete || mPreview != null;
     }
 
     private void uploadBackgroundTiles(GLCanvas canvas) {
@@ -437,17 +478,6 @@
         }
     }
 
-    private void queueForUpload(Tile tile) {
-        synchronized (mQueueLock) {
-            mUploadQueue.push(tile);
-        }
-        invalidate();
-        // TODO
-//        if (mTileUploader.mActive.compareAndSet(false, true)) {
-//            getGLRoot().addOnGLIdleListener(mTileUploader);
-//        }
-    }
-
    private void queueForDecode(Tile tile) {
        synchronized (mQueueLock) {
            if (tile.mTileState == STATE_ACTIVATED) {
@@ -459,9 +489,11 @@
        }
     }
 
-    private boolean decodeTile(Tile tile) {
+    private void decodeTile(Tile tile) {
         synchronized (mQueueLock) {
-            if (tile.mTileState != STATE_IN_QUEUE) return false;
+            if (tile.mTileState != STATE_IN_QUEUE) {
+                return;
+            }
             tile.mTileState = STATE_DECODING;
         }
         boolean decodeComplete = tile.decode();
@@ -469,15 +501,19 @@
             if (tile.mTileState == STATE_RECYCLING) {
                 tile.mTileState = STATE_RECYCLED;
                 if (tile.mDecodedTile != null) {
-                    if (sTilePool != null) sTilePool.put(tile.mDecodedTile);
+                    sTilePool.release(tile.mDecodedTile);
                     tile.mDecodedTile = null;
                 }
                 mRecycledQueue.push(tile);
-                return false;
+                return;
             }
             tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL;
-            return decodeComplete;
+            if (!decodeComplete) {
+                return;
+            }
+            mUploadQueue.push(tile);
         }
+        invalidate();
     }
 
     private Tile obtainTile(int x, int y, int level) {
@@ -500,7 +536,7 @@
             }
             tile.mTileState = STATE_RECYCLED;
             if (tile.mDecodedTile != null) {
-                if (sTilePool != null) sTilePool.put(tile.mDecodedTile);
+                sTilePool.release(tile.mDecodedTile);
                 tile.mDecodedTile = null;
             }
             mRecycledQueue.push(tile);
@@ -538,11 +574,16 @@
             synchronized (mQueueLock) {
                 tile = mUploadQueue.pop();
             }
-            if (tile == null) break;
+            if (tile == null) {
+                break;
+            }
             if (!tile.isContentValid()) {
-                Utils.assertTrue(tile.mTileState == STATE_DECODED);
-                tile.updateContent(canvas);
-                --quota;
+                if (tile.mTileState == STATE_DECODED) {
+                    tile.updateContent(canvas);
+                    --quota;
+                } else {
+                    Log.w(TAG, "Tile in upload queue has invalid state: " + tile.mTileState);
+                }
             }
         }
         if (tile != null) {
@@ -574,7 +615,17 @@
                     queueForDecode(tile);
                 }
             }
-            drawTile(tile, canvas, source, target);
+            if (drawTile(tile, canvas, source, target)) {
+                return;
+            }
+        }
+        if (mPreview != null) {
+            int size = mTileSize << level;
+            float scaleX = (float) mPreview.getWidth() / mImageWidth;
+            float scaleY = (float) mPreview.getHeight() / mImageHeight;
+            source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
+                    (ty + size) * scaleY);
+            canvas.drawTexture(mPreview, source, target);
         }
     }
 
@@ -588,7 +639,9 @@
 
             // Parent can be divided to four quads and tile is one of the four.
             Tile parent = tile.getParentTile();
-            if (parent == null) return false;
+            if (parent == null) {
+                return false;
+            }
             if (tile.mX == parent.mX) {
                 source.left /= 2f;
                 source.right /= 2f;
@@ -623,14 +676,17 @@
 
         @Override
         protected void onFreeBitmap(Bitmap bitmap) {
-            if (sTilePool != null) sTilePool.put(bitmap);
+            sTilePool.release(bitmap);
         }
 
         boolean decode() {
             // Get a tile from the original image. The tile is down-scaled
             // by (1 << mTilelevel) from a region in the original image.
             try {
-                Bitmap reuse = sTilePool.get(mTileSize, mTileSize);
+                Bitmap reuse = sTilePool.acquire();
+                if (reuse != null && reuse.getWidth() != mTileSize) {
+                    reuse = null;
+                }
                 mDecodedTile = mModel.getTile(mTileLevel, mX, mY, reuse);
             } catch (Throwable t) {
                 Log.w(TAG, "fail to decode tile", t);
@@ -676,7 +732,9 @@
         }
 
         public Tile getParentTile() {
-            if (mTileLevel + 1 == mLevelCount) return null;
+            if (mTileLevel + 1 == mLevelCount) {
+                return null;
+            }
             int size = mTileSize << (mTileLevel + 1);
             int x = size * (mX / size);
             int y = size * (mY / size);
@@ -695,17 +753,34 @@
 
         public Tile pop() {
             Tile tile = mHead;
-            if (tile != null) mHead = tile.mNext;
+            if (tile != null) {
+                mHead = tile.mNext;
+            }
             return tile;
         }
 
         public boolean push(Tile tile) {
+            if (contains(tile)) {
+                Log.w(TAG, "Attempting to add a tile already in the queue!");
+                return false;
+            }
             boolean wasEmpty = mHead == null;
             tile.mNext = mHead;
             mHead = tile;
             return wasEmpty;
         }
 
+        private boolean contains(Tile tile) {
+            Tile other = mHead;
+            while (other != null) {
+                if (other == tile) {
+                    return true;
+                }
+                other = other.mNext;
+            }
+            return false;
+        }
+
         public void clean() {
             mHead = null;
         }
@@ -723,7 +798,7 @@
         }
 
         private Tile waitForTile() throws InterruptedException {
-            synchronized(mQueueLock) {
+            synchronized (mQueueLock) {
                 while (true) {
                     Tile tile = mDecodeQueue.pop();
                     if (tile != null) {
@@ -739,11 +814,10 @@
             try {
                 while (!isInterrupted()) {
                     Tile tile = waitForTile();
-                    if (decodeTile(tile)) {
-                        queueForUpload(tile);
-                    }
+                    decodeTile(tile);
                 }
             } catch (InterruptedException ex) {
+                // We were finished
             }
         }
 
diff --git a/src/com/android/photos/views/TiledImageView.java b/src/com/android/photos/views/TiledImageView.java
index 6fe030d..8bc07c0 100644
--- a/src/com/android/photos/views/TiledImageView.java
+++ b/src/com/android/photos/views/TiledImageView.java
@@ -16,29 +16,48 @@
 
 package com.android.photos.views;
 
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Paint.Align;
+import android.graphics.RectF;
+import android.opengl.GLSurfaceView;
 import android.opengl.GLSurfaceView.Renderer;
+import android.os.Build;
 import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.ScaleGestureDetector;
-import android.view.ScaleGestureDetector.OnScaleGestureListener;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.view.View;
 import android.widget.FrameLayout;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
 import com.android.gallery3d.glrenderer.GLES20Canvas;
 import com.android.photos.views.TiledImageRenderer.TileSource;
 
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
 
+/**
+ * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView}
+ * or {@link BlockingGLTextureView}.
+ */
+public class TiledImageView extends FrameLayout {
 
-public class TiledImageView extends FrameLayout implements OnScaleGestureListener {
+    private static final boolean USE_TEXTURE_VIEW = false;
+    private static final boolean IS_SUPPORTED =
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
+    private static final boolean USE_CHOREOGRAPHER =
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
 
     private BlockingGLTextureView mTextureView;
-    private float mLastX, mLastY;
+    private GLSurfaceView mGLSurfaceView;
+    private boolean mInvalPending = false;
+    private FrameCallback mFrameCallback;
 
     private static class ImageRendererWrapper {
         // Guarded by locks
@@ -46,20 +65,19 @@
         int centerX, centerY;
         int rotation;
         TileSource source;
+        Runnable isReadyCallback;
 
         // GL thread only
         TiledImageRenderer image;
     }
 
-    // TODO: left/right paging
-    private ImageRendererWrapper mRenderers[] = new ImageRendererWrapper[1];
-    private ImageRendererWrapper mFocusedRenderer;
+    private float[] mValues = new float[9];
 
     // -------------------------
     // Guarded by mLock
     // -------------------------
     private Object mLock = new Object();
-    private ScaleGestureDetector mScaleGestureDetector;
+    private ImageRendererWrapper mRenderer;
 
     public TiledImageView(Context context) {
         this(context, null);
@@ -67,102 +85,99 @@
 
     public TiledImageView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mTextureView = new BlockingGLTextureView(context);
-        addView(mTextureView, new LayoutParams(
+        if (!IS_SUPPORTED) {
+            return;
+        }
+
+        mRenderer = new ImageRendererWrapper();
+        mRenderer.image = new TiledImageRenderer(this);
+        View view;
+        if (USE_TEXTURE_VIEW) {
+            mTextureView = new BlockingGLTextureView(context);
+            mTextureView.setRenderer(new TileRenderer());
+            view = mTextureView;
+        } else {
+            mGLSurfaceView = new GLSurfaceView(context);
+            mGLSurfaceView.setEGLContextClientVersion(2);
+            mGLSurfaceView.setRenderer(new TileRenderer());
+            mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+            view = mGLSurfaceView;
+        }
+        addView(view, new LayoutParams(
                 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
-        mTextureView.setRenderer(new TileRenderer());
-        setTileSource(new ColoredTiles());
-        mScaleGestureDetector = new ScaleGestureDetector(context, this);
+        //setTileSource(new ColoredTiles());
     }
 
     public void destroy() {
-        mTextureView.destroy();
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        if (USE_TEXTURE_VIEW) {
+            mTextureView.destroy();
+        } else {
+            mGLSurfaceView.queueEvent(mFreeTextures);
+        }
     }
 
-    public void setTileSource(TileSource source) {
+    private Runnable mFreeTextures = new Runnable() {
+
+        @Override
+        public void run() {
+            mRenderer.image.freeTextures();
+        }
+    };
+
+    public void onPause() {
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        if (!USE_TEXTURE_VIEW) {
+            mGLSurfaceView.onPause();
+        }
+    }
+
+    public void onResume() {
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        if (!USE_TEXTURE_VIEW) {
+            mGLSurfaceView.onResume();
+        }
+    }
+
+    public void setTileSource(TileSource source, Runnable isReadyCallback) {
+        if (!IS_SUPPORTED) {
+            return;
+        }
         synchronized (mLock) {
-            for (int i = 0; i < mRenderers.length; i++) {
-                ImageRendererWrapper renderer = mRenderers[i];
-                if (renderer == null) {
-                    renderer = mRenderers[i] = new ImageRendererWrapper();
-                }
-                renderer.source = source;
-                renderer.centerX = renderer.source.getImageWidth() / 2;
-                renderer.centerY = renderer.source.getImageHeight() / 2;
-                renderer.rotation = 0;
-                renderer.scale = 0;
-                renderer.image = new TiledImageRenderer(this);
-                updateScaleIfNecessaryLocked(renderer);
-            }
+            mRenderer.source = source;
+            mRenderer.isReadyCallback = isReadyCallback;
+            mRenderer.centerX = source != null ? source.getImageWidth() / 2 : 0;
+            mRenderer.centerY = source != null ? source.getImageHeight() / 2 : 0;
+            mRenderer.rotation = source != null ? source.getRotation() : 0;
+            mRenderer.scale = 0;
+            updateScaleIfNecessaryLocked(mRenderer);
         }
-        mFocusedRenderer = mRenderers[0];
         invalidate();
     }
 
     @Override
-    public boolean onScaleBegin(ScaleGestureDetector detector) {
-        return true;
-    }
-
-    @Override
-    public boolean onScale(ScaleGestureDetector detector) {
-        // Don't need the lock because this will only fire inside of onTouchEvent
-        mFocusedRenderer.scale *= detector.getScaleFactor();
-        invalidate();
-        return true;
-    }
-
-    @Override
-    public void onScaleEnd(ScaleGestureDetector detector) {
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        int action = event.getActionMasked();
-        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
-        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
-
-        // Determine focal point
-        float sumX = 0, sumY = 0;
-        final int count = event.getPointerCount();
-        for (int i = 0; i < count; i++) {
-            if (skipIndex == i) continue;
-            sumX += event.getX(i);
-            sumY += event.getY(i);
-        }
-        final int div = pointerUp ? count - 1 : count;
-        float x = sumX / div;
-        float y = sumY / div;
-
-        synchronized (mLock) {
-            mScaleGestureDetector.onTouchEvent(event);
-            switch (action) {
-            case MotionEvent.ACTION_MOVE:
-                mFocusedRenderer.centerX += (mLastX - x) / mFocusedRenderer.scale;
-                mFocusedRenderer.centerY += (mLastY - y) / mFocusedRenderer.scale;
-                invalidate();
-                break;
-            }
-        }
-
-        mLastX = x;
-        mLastY = y;
-        return true;
-    }
-
-    @Override
     protected void onLayout(boolean changed, int left, int top, int right,
             int bottom) {
         super.onLayout(changed, left, top, right, bottom);
+        if (!IS_SUPPORTED) {
+            return;
+        }
         synchronized (mLock) {
-            for (ImageRendererWrapper renderer : mRenderers) {
-                updateScaleIfNecessaryLocked(renderer);
-            }
+            updateScaleIfNecessaryLocked(mRenderer);
         }
     }
 
     private void updateScaleIfNecessaryLocked(ImageRendererWrapper renderer) {
-        if (renderer.scale > 0 || getWidth() == 0) return;
+        if (renderer == null || renderer.source == null
+                || renderer.scale > 0 || getWidth() == 0) {
+            return;
+        }
         renderer.scale = Math.min(
                 (float) getWidth() / (float) renderer.source.getImageWidth(),
                 (float) getHeight() / (float) renderer.source.getImageHeight());
@@ -170,14 +185,93 @@
 
     @Override
     protected void dispatchDraw(Canvas canvas) {
-        mTextureView.render();
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        if (USE_TEXTURE_VIEW) {
+            mTextureView.render();
+        }
         super.dispatchDraw(canvas);
     }
 
+    @SuppressLint("NewApi")
+    @Override
+    public void setTranslationX(float translationX) {
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        super.setTranslationX(translationX);
+    }
+
     @Override
     public void invalidate() {
-        super.invalidate();
-        mTextureView.invalidate();
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        if (USE_TEXTURE_VIEW) {
+            super.invalidate();
+            mTextureView.invalidate();
+        } else {
+            if (USE_CHOREOGRAPHER) {
+                invalOnVsync();
+            } else {
+                mGLSurfaceView.requestRender();
+            }
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    private void invalOnVsync() {
+        if (!mInvalPending) {
+            mInvalPending = true;
+            if (mFrameCallback == null) {
+                mFrameCallback = new FrameCallback() {
+                    @Override
+                    public void doFrame(long frameTimeNanos) {
+                        mInvalPending = false;
+                        mGLSurfaceView.requestRender();
+                    }
+                };
+            }
+            Choreographer.getInstance().postFrameCallback(mFrameCallback);
+        }
+    }
+
+    private RectF mTempRectF = new RectF();
+    public void positionFromMatrix(Matrix matrix) {
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        if (mRenderer.source != null) {
+            final int rotation = mRenderer.source.getRotation();
+            final boolean swap = !(rotation % 180 == 0);
+            final int width = swap ? mRenderer.source.getImageHeight()
+                    : mRenderer.source.getImageWidth();
+            final int height = swap ? mRenderer.source.getImageWidth()
+                    : mRenderer.source.getImageHeight();
+            mTempRectF.set(0, 0, width, height);
+            matrix.mapRect(mTempRectF);
+            matrix.getValues(mValues);
+            int cx = width / 2;
+            int cy = height / 2;
+            float scale = mValues[Matrix.MSCALE_X];
+            int xoffset = Math.round((getWidth() - mTempRectF.width()) / 2 / scale);
+            int yoffset = Math.round((getHeight() - mTempRectF.height()) / 2 / scale);
+            if (rotation == 90 || rotation == 180) {
+                cx += (mTempRectF.left / scale) - xoffset;
+            } else {
+                cx -= (mTempRectF.left / scale) - xoffset;
+            }
+            if (rotation == 180 || rotation == 270) {
+                cy += (mTempRectF.top / scale) - yoffset;
+            } else {
+                cy -= (mTempRectF.top / scale) - yoffset;
+            }
+            mRenderer.scale = scale;
+            mRenderer.centerX = swap ? cy : cx;
+            mRenderer.centerY = swap ? cx : cy;
+            invalidate();
+        }
     }
 
     private class TileRenderer implements Renderer {
@@ -187,37 +281,46 @@
         @Override
         public void onSurfaceCreated(GL10 gl, EGLConfig config) {
             mCanvas = new GLES20Canvas();
-            for (ImageRendererWrapper renderer : mRenderers) {
-                renderer.image.setModel(renderer.source, renderer.rotation);
-            }
+            BasicTexture.invalidateAllTextures();
+            mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
         }
 
         @Override
         public void onSurfaceChanged(GL10 gl, int width, int height) {
             mCanvas.setSize(width, height);
-            for (ImageRendererWrapper renderer : mRenderers) {
-                renderer.image.setViewSize(width, height);
-            }
+            mRenderer.image.setViewSize(width, height);
         }
 
         @Override
         public void onDrawFrame(GL10 gl) {
             mCanvas.clearBuffer();
+            Runnable readyCallback;
             synchronized (mLock) {
-                for (ImageRendererWrapper renderer : mRenderers) {
-                    renderer.image.setModel(renderer.source, renderer.rotation);
-                    renderer.image.setPosition(renderer.centerX, renderer.centerY, renderer.scale);
-                }
+                readyCallback = mRenderer.isReadyCallback;
+                mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
+                mRenderer.image.setPosition(mRenderer.centerX, mRenderer.centerY,
+                        mRenderer.scale);
             }
-            for (ImageRendererWrapper renderer : mRenderers) {
-                renderer.image.draw(mCanvas);
+            boolean complete = mRenderer.image.draw(mCanvas);
+            if (complete && readyCallback != null) {
+                synchronized (mLock) {
+                    // Make sure we don't trample on a newly set callback/source
+                    // if it changed while we were rendering
+                    if (mRenderer.isReadyCallback == readyCallback) {
+                        mRenderer.isReadyCallback = null;
+                    }
+                }
+                if (readyCallback != null) {
+                    post(readyCallback);
+                }
             }
         }
 
     }
 
+    @SuppressWarnings("unused")
     private static class ColoredTiles implements TileSource {
-        private static int[] COLORS = new int[] {
+        private static final int[] COLORS = new int[] {
             Color.RED,
             Color.BLUE,
             Color.YELLOW,
@@ -246,6 +349,11 @@
         }
 
         @Override
+        public int getRotation() {
+            return 0;
+        }
+
+        @Override
         public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
             int tileSize = getTileSize();
             if (bitmap == null) {
@@ -265,5 +373,10 @@
             mCanvas.setBitmap(null);
             return bitmap;
         }
+
+        @Override
+        public BasicTexture getPreview() {
+            return null;
+        }
     }
 }