Use SurfaceTexture to show Camera preview.

Change-Id: I8bf63dfc5b969ecce51841378b093a650b6f91d8
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
index 8bd2bad..37ddb18 100644
--- a/src/com/android/gallery3d/app/AlbumPage.java
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -51,6 +51,7 @@
 import com.android.gallery3d.ui.PositionProvider;
 import com.android.gallery3d.ui.PositionRepository;
 import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.ui.ScreenNailHolder;
 import com.android.gallery3d.ui.SelectionManager;
 import com.android.gallery3d.ui.SlotView;
 import com.android.gallery3d.util.Future;
@@ -77,6 +78,7 @@
     private static final int BIT_LOADING_SYNC = 2;
 
     private static final float USER_DISTANCE_METER = 0.3f;
+    private static final boolean TEST_CAMERA_PREVIEW = false;
 
     private boolean mIsActive = false;
     private AlbumView mAlbumView;
@@ -196,6 +198,10 @@
                         mMediaSetPath.toString());
                 data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH,
                         item.getPath().toString());
+                if (TEST_CAMERA_PREVIEW) {
+                    ScreenNailHolder holder = new CameraScreenNailHolder(mActivity);
+                    data.putParcelable(PhotoPage.KEY_SCREENNAIL_HOLDER, holder);
+                }
                 mActivity.getStateManager().startStateForResult(
                         PhotoPage.class, REQUEST_PHOTO, data);
             }
diff --git a/src/com/android/gallery3d/app/CameraScreenNail.java b/src/com/android/gallery3d/app/CameraScreenNail.java
new file mode 100644
index 0000000..f68ab72
--- /dev/null
+++ b/src/com/android/gallery3d/app/CameraScreenNail.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.app;
+
+import android.app.Activity;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+import android.view.Surface;
+
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.ui.ScreenNailHolder;
+import com.android.gallery3d.ui.SurfaceTextureScreenNail;
+
+// This is a ScreenNail which displays camera preview. This demos the usage of
+// SurfaceTextureScreenNail. It is not intended for production use.
+class CameraScreenNail extends SurfaceTextureScreenNail {
+    private static final String TAG = "CameraScreenNail";
+    private static final int CAMERA_ID = 0;
+    private static final int PREVIEW_WIDTH = 960;
+    private static final int PREVIEW_HEIGHT = 720;
+    private static final int MSG_START_CAMERA = 1;
+    private static final int MSG_STOP_CAMERA = 2;
+
+    public interface Listener {
+        void requestRender();
+    }
+
+    private Activity mActivity;
+    private Listener mListener;
+    private int mOrientation;
+    private Camera mCamera;
+
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private volatile boolean mVisible;
+    private volatile boolean mHasFrame;
+
+    public CameraScreenNail(Activity activity, Listener listener) {
+        mActivity = activity;
+        mListener = listener;
+
+        mOrientation = getCameraDisplayOrientation(mActivity, CAMERA_ID);
+        if (mOrientation % 180 == 0) {
+            setSize(PREVIEW_WIDTH, PREVIEW_HEIGHT);
+        } else {
+            setSize(PREVIEW_HEIGHT, PREVIEW_WIDTH);
+        }
+
+        mHandlerThread = new HandlerThread("Camera");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper()) {
+                public void handleMessage(Message message) {
+                    if (message.what == MSG_START_CAMERA && mCamera == null) {
+                        startCamera();
+                    } else if (message.what == MSG_STOP_CAMERA && mCamera != null) {
+                        stopCamera();
+                    }
+                }
+            };
+        mHandler.sendEmptyMessage(MSG_START_CAMERA);
+    }
+
+    private void startCamera() {
+        try {
+            acquireSurfaceTexture();
+            Camera camera = Camera.open(CAMERA_ID);
+            Camera.Parameters param = camera.getParameters();
+            param.setPreviewSize(PREVIEW_WIDTH, PREVIEW_HEIGHT);
+            camera.setParameters(param);
+            camera.setDisplayOrientation(mOrientation);
+            camera.setPreviewTexture(getSurfaceTexture());
+            camera.startPreview();
+            synchronized (this) {
+                mCamera = camera;
+            }
+        } catch (Throwable th) {
+            Log.e(TAG, "cannot open camera", th);
+        }
+    }
+
+    private void stopCamera() {
+        releaseSurfaceTexture();
+        mCamera.stopPreview();
+        mCamera.release();
+        synchronized (this) {
+            mCamera = null;
+            notifyAll();
+        }
+        mHasFrame = false;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        if (!mVisible) {
+            mVisible = true;
+            // Only send one message when mVisible makes transition from
+            // false to true.
+            mHandler.sendEmptyMessage(MSG_START_CAMERA);
+        }
+
+        if (mVisible && mHasFrame) {
+            super.draw(canvas, x, y, width, height);
+        }
+    }
+
+    @Override
+    public void noDraw() {
+        mVisible = false;
+    }
+
+    @Override
+    public void pauseDraw() {
+        mVisible = false;
+    }
+
+    @Override
+    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+        mHasFrame = true;
+        if (mVisible) {
+            // We need to ask for re-render if the SurfaceTexture receives a new
+            // frame (and we are visible).
+            mListener.requestRender();
+        }
+    }
+
+    public void destroy() {
+        synchronized (this) {
+            mHandler.sendEmptyMessage(MSG_STOP_CAMERA);
+
+            // Wait until camera is closed.
+            while (mCamera != null) {
+                try {
+                    wait();
+                } catch (Exception ex) {
+                    // ignore.
+                }
+            }
+        }
+        mHandlerThread.quit();
+    }
+
+    // The three methods below are copied from Camera.java
+    private static int getCameraDisplayOrientation(
+            Activity activity, int cameraId) {
+        int displayRotation = getDisplayRotation(activity);
+        int displayOrientation = getDisplayOrientation(
+                displayRotation, cameraId);
+        return displayOrientation;
+    }
+
+    private static int getDisplayRotation(Activity activity) {
+        int rotation = activity.getWindowManager().getDefaultDisplay()
+                .getRotation();
+        switch (rotation) {
+            case Surface.ROTATION_0: return 0;
+            case Surface.ROTATION_90: return 90;
+            case Surface.ROTATION_180: return 180;
+            case Surface.ROTATION_270: return 270;
+        }
+        return 0;
+    }
+
+    private static int getDisplayOrientation(int degrees, int cameraId) {
+        // See android.hardware.Camera.setDisplayOrientation for
+        // documentation.
+        Camera.CameraInfo info = new Camera.CameraInfo();
+        Camera.getCameraInfo(cameraId, info);
+        int result;
+        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+            result = (info.orientation + degrees) % 360;
+            result = (360 - result) % 360;  // compensate the mirror
+        } else {  // back-facing
+            result = (info.orientation - degrees + 360) % 360;
+        }
+        return result;
+    }
+}
+
+// This holds a CameraScreenNail, so we can pass it to a PhotoPage.
+class CameraScreenNailHolder extends ScreenNailHolder
+        implements CameraScreenNail.Listener {
+    private static final String TAG = "CameraScreenNailHolder";
+    private GalleryActivity mActivity;
+    private CameraScreenNail mCameraScreenNail;
+
+    public CameraScreenNailHolder(GalleryActivity activity) {
+        mActivity = activity;
+    }
+
+    @Override
+    public void requestRender() {
+        mActivity.getGLRoot().requestRender();
+    }
+
+    @Override
+    public ScreenNail attach() {
+        mCameraScreenNail = new CameraScreenNail((Activity) mActivity, this);
+        return mCameraScreenNail;
+    }
+
+    @Override
+    public void detach() {
+        mCameraScreenNail.destroy();
+        mCameraScreenNail = null;
+    }
+}
diff --git a/src/com/android/gallery3d/app/CameraView.java b/src/com/android/gallery3d/app/CameraView.java
deleted file mode 100644
index a6c2332..0000000
--- a/src/com/android/gallery3d/app/CameraView.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.app;
-
-import android.content.Context;
-import android.graphics.SurfaceTexture;
-import android.hardware.Camera;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.TextureView;
-import android.view.View;
-
-import java.io.IOException;
-
-// This is a sample View which demos the usage of ScreenNailBridge. It
-// is not intended for production use.
-public class CameraView extends TextureView implements
-        TextureView.SurfaceTextureListener, ScreenNailBridge.Listener {
-    private static final String TAG = "CameraView";
-    private static final int PREVIEW_WIDTH = 960;
-    private static final int PREVIEW_HEIGHT = 720;
-    private Camera mCamera;
-    private ScreenNailBridge mScreenNailBridge;
-
-    public CameraView(Context context) {
-        super(context);
-        init();
-    }
-
-    public CameraView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        init();
-    }
-
-    private void init() {
-        setVisibility(View.INVISIBLE);
-        setSurfaceTextureListener(this);
-    }
-
-    public void setScreenNailBridge(ScreenNailBridge s) {
-        mScreenNailBridge = s;
-    }
-
-    @Override
-    public void onMeasure(int widthSpec, int heightSpec) {
-        int width = getDefaultSize(PREVIEW_WIDTH, widthSpec);
-        int height = getDefaultSize(PREVIEW_HEIGHT, heightSpec);
-        // Keep aspect ratio
-        if (width * PREVIEW_HEIGHT > PREVIEW_WIDTH * height) {
-            width = PREVIEW_WIDTH * height / PREVIEW_HEIGHT;
-        } else {
-            height = PREVIEW_HEIGHT * width / PREVIEW_WIDTH;
-        }
-        setMeasuredDimension(width, height);
-    }
-
-    @Override
-    public void onSizeChanged(int w, int h, int oldw, int oldh) {
-        mScreenNailBridge.setSize(w, h);
-    }
-
-    @Override
-    public void updateView(boolean visible, int x, int y, int w, int h) {
-        if (!visible) {
-            setVisibility(View.INVISIBLE);
-        } else {
-            setVisibility(View.VISIBLE);
-            setTranslationX(x);
-            setTranslationY(y);
-        }
-    }
-
-    @Override
-    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
-        try {
-            mCamera = Camera.open();
-
-            Camera.Parameters param = mCamera.getParameters();
-            param.setPreviewSize(PREVIEW_WIDTH, PREVIEW_HEIGHT);
-            mCamera.setParameters(param);
-
-            mCamera.setPreviewTexture(surface);
-            mCamera.startPreview();
-        } catch (Throwable ex) {
-            Log.e(TAG, "failed to open camera", ex);
-        }
-    }
-
-    @Override
-    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
-        mCamera.stopPreview();
-        mCamera.release();
-        return true;
-    }
-
-    @Override
-    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
-    }
-
-    @Override
-    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
-    }
-}
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
index a4fd411..109323c 100644
--- a/src/com/android/gallery3d/app/PhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -221,7 +221,7 @@
         ScreenNail screenNail = future.get();
 
         if (entry == null || entry.screenNailTask != future) {
-            if (screenNail != null) screenNail.recycle();
+            if (screenNail != null) screenNail.pauseDraw();
             return;
         }
 
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
index 3a18dd2..9a1992e 100644
--- a/src/com/android/gallery3d/app/PhotoPage.java
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -55,6 +55,8 @@
 import com.android.gallery3d.ui.GLView;
 import com.android.gallery3d.ui.ImportCompleteListener;
 import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.ui.ScreenNailHolder;
 import com.android.gallery3d.ui.PhotoView;
 import com.android.gallery3d.ui.PositionRepository;
 import com.android.gallery3d.ui.PositionRepository.Position;
@@ -80,6 +82,7 @@
     public static final String KEY_MEDIA_ITEM_PATH = "media-item-path";
     public static final String KEY_INDEX_HINT = "index-hint";
     public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect";
+    public static final String KEY_SCREENNAIL_HOLDER = "screennail-holder";
 
     private GalleryApp mApplication;
     private SelectionManager mSelectionManager;
@@ -109,11 +112,8 @@
     private boolean mIsActive;
     private ShareActionProvider mShareActionProvider;
     private String mSetPathString;
-
-    // This is for testing only. It should be removed once we have the real
-    // Camera view.
-    private CameraView mCameraView;
-    private ScreenNailBridge mScreenNail;
+    private ScreenNailHolder mScreenNailHolder;
+    private ScreenNail mScreenNail;
 
     public static interface Model extends PhotoView.Model {
         public void resume();
@@ -190,14 +190,24 @@
         Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH));
 
         if (mSetPathString != null) {
-            // Uncomment the block below to test camera screennail.
-            /*
-            Path cameraScreenNailSetPath = addCameraScreenNail();
+            mScreenNailHolder =
+                (ScreenNailHolder) data.getParcelable(KEY_SCREENNAIL_HOLDER);
+            if (mScreenNailHolder != null) {
+                mScreenNail = mScreenNailHolder.attach();
 
-            // Combine the original MediaSet with the one for camera ScreenNail.
-            mSetPathString = "/combo/item/{" + cameraScreenNailSetPath + "," +
-                    mSetPathString + "}";
-            */
+                // Get the ScreenNail from ScreenNailHolder and register it.
+                int id = SnailSource.registerScreenNail(mScreenNail);
+                Path screenNailSetPath = SnailSource.getSetPath(id);
+                Path screenNailItemPath = SnailSource.getItemPath(id);
+
+                // Combine the original MediaSet with the one for CameraScreenNail.
+                mSetPathString = "/combo/item/{" + screenNailSetPath +
+                        "," + mSetPathString + "}";
+
+                // Start from the screen nail.
+                itemPath = screenNailItemPath;
+            }
+
             mMediaSet = mActivity.getDataManager().getMediaSet(mSetPathString);
             mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
             mMediaSet = (MediaSet)
@@ -279,34 +289,6 @@
         }
     }
 
-    // We create a Camera View and a ScreenNail. The two work together
-    // to present the view together with other pictures. Returns the
-    // Path of the MediaItem hosting the ScreenNail.
-    private Path addCameraScreenNail() {
-        // Create a camera view and add it to the root.
-        Activity activity = (Activity) mActivity;
-        mCameraView = new CameraView(activity);
-        ViewGroup galleryRoot = (ViewGroup) activity.findViewById(R.id.gallery_root);
-        galleryRoot.addView(mCameraView);
-
-        // Create a ScreenNail and register it.
-        mScreenNail = new ScreenNailBridge(mCameraView);
-        mCameraView.setScreenNailBridge(mScreenNail);
-        return SnailSource.registerScreenNail(mScreenNail);
-    }
-
-    private void removeCameraScreenNail() {
-        if (mCameraView == null) return;
-
-        // Remove the camera view.
-        ((ViewGroup) mCameraView.getParent()).removeView(mCameraView);
-        mCameraView = null;
-
-        // Unregister the ScreenNail.
-        SnailSource.unregisterScreenNail(mScreenNail);
-        mScreenNail = null;
-    }
-
     private void updateShareURI(Path path) {
         if (mShareActionProvider != null) {
             DataManager manager = mActivity.getDataManager();
@@ -690,7 +672,13 @@
 
     @Override
     protected void onDestroy() {
-        removeCameraScreenNail();
+        if (mScreenNailHolder != null) {
+            // Unregister the ScreenNail and notify mScreenNailHolder.
+            SnailSource.unregisterScreenNail(mScreenNail);
+            mScreenNailHolder.detach();
+            mScreenNailHolder = null;
+            mScreenNail = null;
+        }
         super.onDestroy();
     }
 
diff --git a/src/com/android/gallery3d/app/ScreenNailBridge.java b/src/com/android/gallery3d/app/ScreenNailBridge.java
deleted file mode 100644
index 9da197d..0000000
--- a/src/com/android/gallery3d/app/ScreenNailBridge.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.app;
-
-import android.graphics.RectF;
-import android.os.Handler;
-import android.util.Log;
-
-import com.android.gallery3d.ui.GLCanvas;
-import com.android.gallery3d.ui.ScreenNail;
-
-// This is a ScreenNail whose actually display is done by an foreign component.
-// The foreign component tells the ScreenNail its size by setSize(). The
-// ScreenNail tells the foreign component the position to display by
-// updateView().
-class ScreenNailBridge implements ScreenNail {
-    private static final String TAG = "ScreenNailBridge";
-    private int mWidth, mHeight;
-    private boolean mVisible = false;
-    private int mDrawX, mDrawY, mDrawWidth, mDrawHeight;
-    private Listener mListener;
-    private Handler mMainHandler;
-
-    public interface Listener {
-        // This is called from the main thread.
-        void updateView(boolean visible, int x, int y, int width, int height);
-    };
-
-    // The constructor should be called from the main thread.
-    public ScreenNailBridge(Listener listener) {
-        mListener = listener;
-        mMainHandler = new Handler();
-    }
-
-    // This can be called from any thread.  (We expect it to be called from the
-    // main thread).
-    public synchronized void setSize(int w, int h) {
-        mWidth = w;
-        mHeight = h;
-    }
-
-    // This can be called from any thread. (We expect it to be called from GL
-    // thread)
-    @Override
-    public synchronized int getWidth() {
-        return mWidth;
-    }
-
-    // This can be called from any thread. (We expect it to be called from GL
-    // thread)
-    @Override
-    public synchronized int getHeight() {
-        return mHeight;
-    }
-
-    @Override
-    public int getRotation() {
-        return 0;
-    }
-
-    // This is run in the main thread.
-    private Runnable mUpdateViewRunnable = new Runnable() {
-            public void run() {
-                boolean v;
-                int x, y, width, height;
-                synchronized (ScreenNailBridge.this) {
-                    v = mVisible;
-                    x = mDrawX;
-                    y = mDrawY;
-                    width = mDrawWidth;
-                    height = mDrawHeight;
-                }
-                mListener.updateView(v, x, y, width, height);
-            }
-        };
-
-    @Override
-    public synchronized void draw(GLCanvas canvas, int x, int y, int width, int height) {
-        if (mVisible && mDrawX == x && mDrawY == y && mDrawWidth == width &&
-                mDrawHeight == height) {
-            return;
-        }
-        mVisible = true;
-        mDrawX = x;
-        mDrawY = y;
-        mDrawWidth = width;
-        mDrawHeight = height;
-        mMainHandler.post(mUpdateViewRunnable);
-    }
-
-    @Override
-    public synchronized void disableDraw() {
-        if (!mVisible) return;
-        mVisible = false;
-        mMainHandler.post(mUpdateViewRunnable);
-    }
-
-    @Override
-    public void recycle() {
-        // Make sure we will not draw anymore.
-        disableDraw();
-    }
-
-    @Override
-    public void draw(GLCanvas canvas, RectF source, RectF dest) {
-        throw new UnsupportedOperationException();
-    }
-}
diff --git a/src/com/android/gallery3d/data/SnailSource.java b/src/com/android/gallery3d/data/SnailSource.java
index 17b899d..e74a8bb 100644
--- a/src/com/android/gallery3d/data/SnailSource.java
+++ b/src/com/android/gallery3d/data/SnailSource.java
@@ -55,14 +55,23 @@
         return null;
     }
 
-    // Register a ScreenNail. Returns the Path of the MediaSet
-    // containing the MediaItem associated with the ScreenNail.
-    public static synchronized Path registerScreenNail(ScreenNail s) {
+    // Registers a ScreenNail and returns the id of it. You can obtain the Path
+    // of the MediaItem associated with the ScreenNail by getItemPath(), and the
+    // Path of the MediaSet containing that MediaItem by getSetPath().
+    public static synchronized int registerScreenNail(ScreenNail s) {
         int id = sNextId++;
         sRegistry.put(id, s);
+        return id;
+    }
+
+    public static Path getSetPath(int id) {
         return Path.fromString("/snail/set").getChild(id);
     }
 
+    public static Path getItemPath(int id) {
+        return Path.fromString("/snail/item").getChild(id);
+    }
+
     public static synchronized void unregisterScreenNail(ScreenNail s) {
         int index = sRegistry.indexOfValue(s);
         sRegistry.removeAt(index);
diff --git a/src/com/android/gallery3d/ui/BasicTexture.java b/src/com/android/gallery3d/ui/BasicTexture.java
index d3dfd74..6a9a17d 100644
--- a/src/com/android/gallery3d/ui/BasicTexture.java
+++ b/src/com/android/gallery3d/ui/BasicTexture.java
@@ -130,6 +130,9 @@
     // It should make sure the data is uploaded to GL memory.
     abstract protected boolean onBind(GLCanvas canvas);
 
+    // Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D).
+    abstract protected int getTarget();
+
     public boolean isLoaded(GLCanvas canvas) {
         return mState == STATE_LOADED;
     }
diff --git a/src/com/android/gallery3d/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java
index 117a9ac..5a10068 100644
--- a/src/com/android/gallery3d/ui/BitmapScreenNail.java
+++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java
@@ -56,7 +56,11 @@
     }
 
     @Override
-    public void recycle() {
+    public void noDraw() {
+    }
+
+    @Override
+    public void pauseDraw() {
         if (mTexture != null) {
             mTexture.recycle();
         }
@@ -71,10 +75,6 @@
     }
 
     @Override
-    public void disableDraw() {
-    }
-
-    @Override
     public void draw(GLCanvas canvas, RectF source, RectF dest) {
         if (mTexture == null) {
             mTexture = new BitmapTexture(mBitmap);
diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java
index 99b64d4..1e78cfd 100644
--- a/src/com/android/gallery3d/ui/BitmapTileProvider.java
+++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java
@@ -96,7 +96,7 @@
             BitmapUtils.recycleSilently(bitmap);
         }
         if (mScreenNail != null) {
-            mScreenNail.recycle();
+            mScreenNail.pauseDraw();
         }
     }
 
diff --git a/src/com/android/gallery3d/ui/ExtTexture.java b/src/com/android/gallery3d/ui/ExtTexture.java
new file mode 100644
index 0000000..e39f317
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ExtTexture.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.SurfaceTexture;
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+
+// ExtTexture is a texture whose content comes from a external texture.
+// Before drawing, setSize() should be called.
+public class ExtTexture extends BasicTexture {
+
+    private static int[] sTextureId = new int[1];
+    private static float[] sCropRect = new float[4];
+    private int mTarget;
+
+    public ExtTexture(int target) {
+        GLId.glGenTextures(1, sTextureId, 0);
+        mId = sTextureId[0];
+        mTarget = target;
+    }
+
+    private void uploadToCanvas(GLCanvas canvas) {
+        GL11 gl = canvas.getGLInstance();
+
+        int width = getWidth();
+        int height = getHeight();
+        // Define a vertically flipped crop rectangle for OES_draw_texture.
+        // The four values in sCropRect are: left, bottom, width, and
+        // height. Negative value of width or height means flip.
+        sCropRect[0] = 0;
+        sCropRect[1] = height;
+        sCropRect[2] = width;
+        sCropRect[3] = -height;
+
+        // Set texture parameters.
+        gl.glBindTexture(mTarget, mId);
+        gl.glTexParameterfv(mTarget,
+                GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0);
+        gl.glTexParameteri(mTarget,
+                GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+        gl.glTexParameteri(mTarget,
+                GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+        gl.glTexParameterf(mTarget,
+                GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+        gl.glTexParameterf(mTarget,
+                GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+
+        setAssociatedCanvas(canvas);
+        mState = UploadedTexture.STATE_LOADED;
+    }
+
+    @Override
+    protected boolean onBind(GLCanvas canvas) {
+        if (!isLoaded(canvas)) {
+            uploadToCanvas(canvas);
+        }
+
+        return true;
+    }
+
+    @Override
+    public int getTarget() {
+        return mTarget;
+    }
+
+    public boolean isOpaque() {
+        return true;
+    }
+
+    @Override
+    public void yield() {
+        // we cannot free the texture because we have no backup.
+    }
+}
diff --git a/src/com/android/gallery3d/ui/GLCanvas.java b/src/com/android/gallery3d/ui/GLCanvas.java
index 1359115..9b8053f 100644
--- a/src/com/android/gallery3d/ui/GLCanvas.java
+++ b/src/com/android/gallery3d/ui/GLCanvas.java
@@ -85,9 +85,13 @@
     public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
             int uvBuffer, int indexBuffer, int indexCount);
 
-    // Draws a the source rectangle part of the texture to the target rectangle.
+    // Draws the source rectangle part of the texture to the target rectangle.
     public void drawTexture(BasicTexture texture, RectF source, RectF target);
 
+    // Draw a texture with a specified texture transform.
+    public void drawTexture(BasicTexture texture, float[] mTextureTransform,
+                int x, int y, int w, int h);
+
     // Draw two textures to the specified rectangle. The actual texture used is
     // from * (1 - ratio) + to * ratio
     // The two textures must have the same size.
diff --git a/src/com/android/gallery3d/ui/GLCanvasImpl.java b/src/com/android/gallery3d/ui/GLCanvasImpl.java
index 68f5636..686e712 100644
--- a/src/com/android/gallery3d/ui/GLCanvasImpl.java
+++ b/src/com/android/gallery3d/ui/GLCanvasImpl.java
@@ -136,7 +136,7 @@
         xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0);
 
         int[] name = new int[1];
-        gl.glGenBuffers(1, name, 0);
+        GLId.glGenBuffers(1, name, 0);
         mBoxCoords = name[0];
 
         gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
@@ -377,6 +377,16 @@
         textureRect(target.left, target.top, target.width(), target.height());
     }
 
+    public void drawTexture(BasicTexture texture, float[] mTextureTransform,
+            int x, int y, int w, int h) {
+        mGLState.setBlendEnabled(mBlendEnabled
+                && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
+        if (!bindTexture(texture)) return;
+        setTextureCoords(mTextureTransform);
+        mGLState.setTextureAlpha(mAlpha);
+        textureRect(x, y, w, h);
+    }
+
     // This function changes the source coordinate to the texture coordinates.
     // It also clips the source and target coordinates if it is beyond the
     // bound of the texture.
@@ -415,8 +425,9 @@
 
     private boolean bindTexture(BasicTexture texture) {
         if (!texture.onBind(this)) return false;
-        mGLState.setTexture2DEnabled(true);
-        mGL.glBindTexture(GL11.GL_TEXTURE_2D, texture.getId());
+        int target = texture.getTarget();
+        mGLState.setTextureTarget(target);
+        mGL.glBindTexture(target, texture.getId());
         return true;
     }
 
@@ -513,7 +524,7 @@
 
         private int mTexEnvMode = GL11.GL_REPLACE;
         private float mTextureAlpha = 1.0f;
-        private boolean mTexture2DEnabled = true;
+        private int mTextureTarget = 0;
         private boolean mBlendEnabled = true;
         private float mLineWidth = 1.0f;
         private boolean mLineSmooth = false;
@@ -578,7 +589,7 @@
             // again in setTextureAlpha(float) later.
             mTextureAlpha = -1.0f;
 
-            setTexture2DEnabled(false);
+            setTextureTarget(0);
 
             float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f;
             mGL.glColor4x(
@@ -588,13 +599,15 @@
                     Math.round(255 * prealpha));
         }
 
-        public void setTexture2DEnabled(boolean enabled) {
-            if (mTexture2DEnabled == enabled) return;
-            mTexture2DEnabled = enabled;
-            if (enabled) {
-                mGL.glEnable(GL11.GL_TEXTURE_2D);
-            } else {
-                mGL.glDisable(GL11.GL_TEXTURE_2D);
+        // target is a value like GL_TEXTURE_2D. If target = 0, texturing is disabled.
+        public void setTextureTarget(int target) {
+            if (mTextureTarget == target) return;
+            if (mTextureTarget != 0) {
+                mGL.glDisable(mTextureTarget);
+            }
+            mTextureTarget = target;
+            if (mTextureTarget != 0) {
+                mGL.glEnable(mTextureTarget);
             }
         }
 
@@ -634,6 +647,12 @@
         mGL.glMatrixMode(GL11.GL_MODELVIEW);
     }
 
+    private void setTextureCoords(float[] mTextureTransform) {
+        mGL.glMatrixMode(GL11.GL_TEXTURE);
+        mGL.glLoadMatrixf(mTextureTransform, 0);
+        mGL.glMatrixMode(GL11.GL_MODELVIEW);
+    }
+
     // unloadTexture and deleteBuffer can be called from the finalizer thread,
     // so we synchronized on the mUnboundTextures object.
     public boolean unloadTexture(BasicTexture t) {
@@ -654,13 +673,13 @@
         synchronized (mUnboundTextures) {
             IntArray ids = mUnboundTextures;
             if (ids.size() > 0) {
-                mGL.glDeleteTextures(ids.size(), ids.getInternalArray(), 0);
+                GLId.glDeleteTextures(ids.size(), ids.getInternalArray(), 0);
                 ids.clear();
             }
 
             ids = mDeleteBuffers;
             if (ids.size() > 0) {
-                mGL.glDeleteBuffers(ids.size(), ids.getInternalArray(), 0);
+                GLId.glDeleteBuffers(ids.size(), ids.getInternalArray(), 0);
                 ids.clear();
             }
         }
diff --git a/src/com/android/gallery3d/ui/GLId.java b/src/com/android/gallery3d/ui/GLId.java
new file mode 100644
index 0000000..c228c35
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLId.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+// This mimics corresponding GL functions.
+public class GLId {
+    static int sNextId = 1;
+
+    public synchronized static void glGenTextures(int n, int[] textures, int offset) {
+        while (n-- > 0) {
+            textures[offset + n] = sNextId++;
+        }
+    }
+
+    public synchronized static void glGenBuffers(int n, int[] buffers, int offset) {
+        while (n-- > 0) {
+            buffers[offset + n] = sNextId++;
+        }
+    }
+
+    public synchronized static void glDeleteTextures(int n, int[] textures, int offset) {
+    }
+
+    public synchronized static void glDeleteBuffers(int n, int[] buffers, int offset) {
+    }
+}
diff --git a/src/com/android/gallery3d/ui/NinePatchTexture.java b/src/com/android/gallery3d/ui/NinePatchTexture.java
index 6a2ba00..957229e 100644
--- a/src/com/android/gallery3d/ui/NinePatchTexture.java
+++ b/src/com/android/gallery3d/ui/NinePatchTexture.java
@@ -398,7 +398,7 @@
     private void prepareBuffers(GLCanvas canvas) {
         mBufferNames = new int[3];
         GL11 gl = canvas.getGLInstance();
-        gl.glGenBuffers(3, mBufferNames, 0);
+        GLId.glGenBuffers(3, mBufferNames, 0);
 
         gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBufferNames[0]);
         gl.glBufferData(GL11.GL_ARRAY_BUFFER,
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
index 252f348..77e3d99 100644
--- a/src/com/android/gallery3d/ui/PhotoView.java
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -225,7 +225,7 @@
             return;
         }
         ScreenNailEntry entry = mScreenNails[which];
-        entry.set(screenNail);
+        entry.updateScreenNail(screenNail);
     }
 
     // -1 previous, 0 current, 1 next
@@ -650,10 +650,8 @@
         ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
         ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
         mTileView.invalidateTiles();
-        if (prevNail.mScreenNail != null) prevNail.mScreenNail.recycle();
-        prevNail.set(mTileView.mScreenNail);
-        mTileView.updateScreenNail(nextNail.mScreenNail);
-        nextNail.set(null);
+        prevNail.updateScreenNail(mTileView.releaseScreenNail());
+        mTileView.updateScreenNail(nextNail.releaseScreenNail());
         mModel.next();
     }
 
@@ -662,10 +660,8 @@
         ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
         ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
         mTileView.invalidateTiles();
-        if (nextNail.mScreenNail != null) nextNail.mScreenNail.recycle();
-        nextNail.set(mTileView.mScreenNail);
-        mTileView.updateScreenNail(prevNail.mScreenNail);
-        nextNail.set(null);
+        nextNail.updateScreenNail(mTileView.releaseScreenNail());
+        mTileView.updateScreenNail(prevNail.releaseScreenNail());
         mModel.previous();
     }
 
@@ -715,10 +711,10 @@
 
         private ScreenNail mScreenNail;
 
-        public void set(ScreenNail screenNail) {
+        public void updateScreenNail(ScreenNail screenNail) {
             mEnabled = (screenNail != null);
             if (mScreenNail == screenNail) return;
-            if (mScreenNail != null) mScreenNail.recycle();
+            if (mScreenNail != null) mScreenNail.pauseDraw();
             mScreenNail = screenNail;
             if (mScreenNail != null) {
                 mRotation = mScreenNail.getRotation();
@@ -726,6 +722,13 @@
             }
         }
 
+        // Release the ownership of the ScreenNail from this entry.
+        public ScreenNail releaseScreenNail() {
+            ScreenNail s = mScreenNail;
+            mScreenNail = null;
+            return s;
+        }
+
         public void layoutRightEdgeAt(int x) {
             mVisible = x > 0;
             mOffsetX = x - getRotated(
@@ -767,7 +770,7 @@
         public void draw(GLCanvas canvas, boolean applyFadingAnimation) {
             if (mScreenNail == null) return;
             if (!mVisible) {
-                mScreenNail.disableDraw();
+                mScreenNail.noDraw();
                 return;
             }
 
@@ -877,7 +880,7 @@
         mTransitionMode = TRANS_NONE;
         mTileView.freeTextures();
         for (ScreenNailEntry entry : mScreenNails) {
-            entry.set(null);
+            entry.updateScreenNail(null);
         }
     }
 
diff --git a/src/com/android/gallery3d/ui/ScreenNail.java b/src/com/android/gallery3d/ui/ScreenNail.java
index a2377fe..58ae8c9 100644
--- a/src/com/android/gallery3d/ui/ScreenNail.java
+++ b/src/com/android/gallery3d/ui/ScreenNail.java
@@ -21,11 +21,9 @@
     public int getWidth();
     public int getHeight();
     public int getRotation();
-    public void recycle();
     public void draw(GLCanvas canvas, int x, int y, int width, int height);
-
-    // We need this method to tell ScreenNail to stop displaying.
-    public void disableDraw();
+    public void noDraw();  // we do not need to draw this ScreenNail in this frame.
+    public void pauseDraw();  // we do not expect to draw this ScreenNail for some time.
 
     // This is only used by TileImageView to back up the tiles not yet loaded.
     public void draw(GLCanvas canvas, RectF source, RectF dest);
diff --git a/src/com/android/gallery3d/ui/ScreenNailHolder.java b/src/com/android/gallery3d/ui/ScreenNailHolder.java
new file mode 100644
index 0000000..a7d5417
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScreenNailHolder.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.gallery3d.ui;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public abstract class ScreenNailHolder implements Parcelable {
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel dest, int flags) {
+    }
+
+    public abstract ScreenNail attach();
+    public abstract void detach();
+}
diff --git a/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
new file mode 100644
index 0000000..3a8f2b0
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
+import android.opengl.GLES11Ext;
+import android.util.Log;
+
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.ui.ExtTexture;
+
+public abstract class SurfaceTextureScreenNail implements ScreenNail,
+        SurfaceTexture.OnFrameAvailableListener {
+    private static final String TAG = "SurfaceTextureScreenNail";
+    private ExtTexture mExtTexture;
+    private SurfaceTexture mSurfaceTexture;
+    private int mWidth, mHeight;
+    private float[] mTransform = new float[16];
+    private boolean mHasTexture = false;
+
+    public SurfaceTextureScreenNail() {
+    }
+
+    public void acquireSurfaceTexture() {
+        mExtTexture = new ExtTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
+        mExtTexture.setSize(mWidth, mHeight);
+        mSurfaceTexture = new SurfaceTexture(mExtTexture.getId());
+        mSurfaceTexture.setOnFrameAvailableListener(this);
+        synchronized (this) {
+            mHasTexture = true;
+        }
+    }
+
+    public SurfaceTexture getSurfaceTexture() {
+        return mSurfaceTexture;
+    }
+
+    public void releaseSurfaceTexture() {
+        synchronized (this) {
+            mHasTexture = false;
+        }
+        mExtTexture.recycle();
+        mExtTexture = null;
+        mSurfaceTexture.release();
+        mSurfaceTexture = null;
+    }
+
+    public void setSize(int width, int height) {
+        mWidth = width;
+        mHeight = height;
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public int getRotation() {
+        return 0;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        synchronized (this) {
+            if (!mHasTexture) return;
+            mSurfaceTexture.updateTexImage();
+            mSurfaceTexture.getTransformMatrix(mTransform);
+
+            // Flip vertically.
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+            int cx = x + width / 2;
+            int cy = y + height / 2;
+            canvas.translate(cx, cy);
+            canvas.scale(1, -1, 1);
+            canvas.translate(-cx, -cy);
+            canvas.drawTexture(mExtTexture, mTransform, x, y, width, height);
+            canvas.restore();
+        }
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, RectF source, RectF dest) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    abstract public void noDraw();
+
+    @Override
+    abstract public void pauseDraw();
+
+    @Override
+    abstract public void onFrameAvailable(SurfaceTexture surfaceTexture);
+}
diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java
index 638a7f9..d2ce167 100644
--- a/src/com/android/gallery3d/ui/TileImageView.java
+++ b/src/com/android/gallery3d/ui/TileImageView.java
@@ -71,7 +71,7 @@
     private static final int STATE_RECYCLED = 0x40;
 
     private Model mModel;
-    protected ScreenNail mScreenNail;
+    private ScreenNail mScreenNail;
     protected int mLevelCount;  // cache the value of mScaledBitmaps.length
 
     // The mLevel variable indicates which level of bitmap we should use.
@@ -155,10 +155,16 @@
 
     public void updateScreenNail(ScreenNail s) {
         if (mScreenNail == s) return;
-        if (mScreenNail != null) mScreenNail.recycle();
+        if (mScreenNail != null) mScreenNail.pauseDraw();
         mScreenNail = s;
     }
 
+    public ScreenNail releaseScreenNail() {
+        ScreenNail s = mScreenNail;
+        mScreenNail = null;
+        return s;
+    }
+
     public void notifyModelInvalidated() {
         invalidateTiles();
         if (mModel == null) {
@@ -421,7 +427,7 @@
         try {
             if (level != mLevelCount) {
                 if (mScreenNail != null) {
-                    mScreenNail.disableDraw();
+                    mScreenNail.noDraw();
                 }
 
                 int size = (TILE_SIZE << level);
diff --git a/src/com/android/gallery3d/ui/UploadedTexture.java b/src/com/android/gallery3d/ui/UploadedTexture.java
index c4d8d82..85aa1c4 100644
--- a/src/com/android/gallery3d/ui/UploadedTexture.java
+++ b/src/com/android/gallery3d/ui/UploadedTexture.java
@@ -228,7 +228,7 @@
                 sCropRect[3] = -bHeight;
 
                 // Upload the bitmap to a new texture.
-                gl.glGenTextures(1, sTextureId, 0);
+                GLId.glGenTextures(1, sTextureId, 0);
                 gl.glBindTexture(GL11.GL_TEXTURE_2D, sTextureId[0]);
                 gl.glTexParameterfv(GL11.GL_TEXTURE_2D,
                         GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0);
@@ -299,6 +299,11 @@
         return isContentValid(canvas);
     }
 
+    @Override
+    protected int getTarget() {
+        return GL11.GL_TEXTURE_2D;
+    }
+
     public void setOpaque(boolean isOpaque) {
         mOpaque = isOpaque;
     }
diff --git a/tests/src/com/android/gallery3d/ui/GLCanvasStub.java b/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
index 70fef1a..ee43cb9 100644
--- a/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
+++ b/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
@@ -57,6 +57,8 @@
     public void drawTexture(BasicTexture texture,
             int x, int y, int width, int height, float alpha) {}
     public void drawTexture(BasicTexture texture, RectF source, RectF target) {}
+    public void drawTexture(BasicTexture texture, float[] mTextureTransform,
+            int x, int y, int w, int h) {}
     public void drawMixed(BasicTexture from, BasicTexture to,
             float ratio, int x, int y, int w, int h) {}
     public void drawMixed(BasicTexture from, int to,
diff --git a/tests/src/com/android/gallery3d/ui/TextureTest.java b/tests/src/com/android/gallery3d/ui/TextureTest.java
index 821d49b..be2356c 100644
--- a/tests/src/com/android/gallery3d/ui/TextureTest.java
+++ b/tests/src/com/android/gallery3d/ui/TextureTest.java
@@ -43,6 +43,11 @@
             return true;
         }
 
+        @Override
+        protected int getTarget() {
+            return GL11.GL_TEXTURE_2D;
+        }
+
         public boolean isOpaque() {
             mOpaqueCalled++;
             return true;
@@ -171,6 +176,11 @@
             return true;
         }
 
+        @Override
+        protected int getTarget() {
+            return GL11.GL_TEXTURE_2D;
+        }
+
         public boolean isOpaque() {
             return true;
         }