am 2fc87a73: Merge "Clean up stats stubs." into gb-ub-photos-denali

* commit '2fc87a7338d34d8d7383eeea3a367893961a6ac5':
  Clean up stats stubs.
diff --git a/Android.mk b/Android.mk
index 8779081..4f18789 100644
--- a/Android.mk
+++ b/Android.mk
@@ -6,6 +6,7 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES := android-support-v13
 LOCAL_STATIC_JAVA_LIBRARIES += xmp_toolkit
+LOCAL_STATIC_JAVA_LIBRARIES += glide
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_SRC_FILES += $(call all-java-files-under, src_pd)
diff --git a/res/layout/filmstrip_video.xml b/res/layout/filmstrip_video.xml
new file mode 100644
index 0000000..3449174
--- /dev/null
+++ b/res/layout/filmstrip_video.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <ImageView
+        android:id="@+id/video_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center" />
+    <ImageView
+        android:id="@+id/play_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:src="@drawable/ic_control_play" />
+</FrameLayout>
\ No newline at end of file
diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java
index d7f1154..82da41e 100644
--- a/src/com/android/camera/CameraActivity.java
+++ b/src/com/android/camera/CameraActivity.java
@@ -82,6 +82,7 @@
 import com.android.camera.app.OrientationManager;
 import com.android.camera.app.OrientationManagerImpl;
 import com.android.camera.data.CameraDataAdapter;
+import com.android.camera.data.LocalDataViewType;
 import com.android.camera.data.FixedLastDataAdapter;
 import com.android.camera.data.LocalData;
 import com.android.camera.data.LocalDataAdapter;
@@ -126,6 +127,8 @@
 import com.android.camera.widget.FilmstripView;
 import com.android.camera.widget.Preloader;
 import com.android.camera2.R;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.resize.ImageManager;
 import com.google.common.logging.eventprotos;
 import com.google.common.logging.eventprotos.CameraEvent.InteractionCause;
 import com.google.common.logging.eventprotos.NavigationChange;
@@ -136,6 +139,7 @@
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Executors;
 
 public class CameraActivity extends Activity
         implements AppController, CameraManager.CameraOpenCallback,
@@ -1163,6 +1167,12 @@
     @Override
     public void onCreate(Bundle state) {
         super.onCreate(state);
+        final Glide glide = Glide.get();
+        if (!glide.isImageManagerSet()) {
+            // We load exclusively large images, so we want fewer threads to minimize jank.
+            glide.setImageManager(new ImageManager.Builder(getApplicationContext())
+                    .setResizeService(Executors.newSingleThreadExecutor()));
+        }
         CameraPerformanceTracker.onEvent(CameraPerformanceTracker.ACTIVITY_START);
         mOnCreateTime = System.currentTimeMillis();
         mAppContext = getApplicationContext();
@@ -1254,8 +1264,7 @@
         mPanoramaViewHelper = new PanoramaViewHelper(this);
         mPanoramaViewHelper.onCreate();
         // Set up the camera preview first so the preview shows up ASAP.
-        mDataAdapter = new CameraDataAdapter(mAppContext,
-                new ColorDrawable(getResources().getColor(R.color.photo_placeholder)));
+        mDataAdapter = new CameraDataAdapter(mAppContext, R.color.photo_placeholder);
         mDataAdapter.setLocalDataListener(mLocalDataListener);
 
         mPreloader = new Preloader<Integer, AsyncTask>(FILMSTRIP_PRELOAD_AHEAD_ITEMS, mDataAdapter,
@@ -1323,6 +1332,7 @@
                     mDataAdapter,
                     new SimpleViewData(
                             v,
+                            LocalDataViewType.SECURE_ALBUM_PLACEHOLDER,
                             v.getDrawable().getIntrinsicWidth(),
                             v.getDrawable().getIntrinsicHeight(),
                             0, 0));
diff --git a/src/com/android/camera/CameraModule.java b/src/com/android/camera/CameraModule.java
index 41be023..4abfe6b 100644
--- a/src/com/android/camera/CameraModule.java
+++ b/src/com/android/camera/CameraModule.java
@@ -83,8 +83,7 @@
         }
     }
 
-    @Override
-    public void onPreviewInitialDataReceived() {};
+    public void onPreviewInitialDataReceived() {}
 
     /**
      * Releases the back camera through {@link CameraProvider}.
diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java
index c81962d..22dea88 100644
--- a/src/com/android/camera/PhotoModule.java
+++ b/src/com/android/camera/PhotoModule.java
@@ -45,6 +45,7 @@
 import android.view.KeyEvent;
 import android.view.OrientationEventListener;
 import android.view.View;
+import android.view.ViewGroup;
 
 import com.android.camera.PhotoModule.NamedImages.NamedEntity;
 import com.android.camera.app.AppController;
@@ -64,11 +65,13 @@
 import com.android.camera.hardware.HardwareSpec;
 import com.android.camera.hardware.HardwareSpecImpl;
 import com.android.camera.module.ModuleController;
+import com.android.camera.remote.RemoteCameraModule;
 import com.android.camera.settings.SettingsManager;
 import com.android.camera.settings.SettingsUtil;
 import com.android.camera.util.ApiHelper;
 import com.android.camera.util.CameraUtil;
 import com.android.camera.util.GcamHelper;
+import com.android.camera.util.SmartCameraHelper;
 import com.android.camera.util.UsageStatistics;
 import com.android.camera2.R;
 import com.google.common.logging.eventprotos;
@@ -89,7 +92,8 @@
         MemoryListener,
         FocusOverlayManager.Listener,
         SensorEventListener,
-        SettingsManager.OnSettingChangedListener {
+        SettingsManager.OnSettingChangedListener,
+        RemoteCameraModule {
 
     private static final String TAG = "PhotoModule";
 
@@ -376,10 +380,11 @@
         setCameraState(IDLE);
         startFaceDetection();
         locationFirstRun();
-    }
-
-    @Override
-    public void onPreviewInitialDataReceived() {
+        // TODO(teresako): Check with Camera team re: starting the Smart Camera here rather than
+        // in the onPreviewInitialDataReceived() function which is no longer being called in
+        // Denali. The original issue of the blue overlay not going away no longer seems to be
+        // an issue.  Related CL: https://googleplex-android-review.git.corp.google.com/#/c/428719/.
+        startSmartCamera();
     }
 
     // Prompt the user to pick to record location for the very first run of
@@ -611,6 +616,15 @@
         });
     }
 
+    private void startSmartCamera() {
+        SmartCameraHelper.register(mCameraDevice, mParameters.getPreviewSize(), mActivity,
+                (ViewGroup) mActivity.findViewById(R.id.camera_app_root));
+    }
+
+    private void stopSmartCamera() {
+        SmartCameraHelper.tearDown();
+    }
+
     @Override
     public void startFaceDetection() {
         if (mFaceDetectionStarted) {
@@ -789,6 +803,9 @@
                 }
             }
 
+            // Send the taken photo to remote shutter listeners, if any are registered.
+            getServices().getRemoteShutterListener().onPictureTaken(jpegData);
+
             // Check this in advance of each shot so we don't add to shutter
             // latency. It's true that someone else could write to the SD card
             // in the mean time and fill it, but that could have happened
@@ -1234,10 +1251,12 @@
             Log.v(TAG, "On resume.");
             onResumeTasks();
         }
+        getServices().getRemoteShutterListener().onModuleReady(this);
     }
 
     @Override
     public void pause() {
+        getServices().getRemoteShutterListener().onModuleExit();
         mPaused = true;
         Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
         if (gsensor != null) {
@@ -1511,6 +1530,7 @@
         if (mFocusManager != null) {
             mFocusManager.onPreviewStopped();
         }
+        stopSmartCamera();
     }
 
     @Override
@@ -1898,4 +1918,9 @@
             }
         }
     }
+
+    @Override
+    public void onRemoteShutterPress() {
+        capture();
+    }
 }
diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java
index 307b138..a835fab 100644
--- a/src/com/android/camera/VideoModule.java
+++ b/src/com/android/camera/VideoModule.java
@@ -50,6 +50,7 @@
 import android.view.KeyEvent;
 import android.view.OrientationEventListener;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.Toast;
 
 import com.android.camera.app.AppController;
@@ -70,6 +71,7 @@
 import com.android.camera.util.AccessibilityUtils;
 import com.android.camera.util.ApiHelper;
 import com.android.camera.util.CameraUtil;
+import com.android.camera.util.SmartCameraHelper;
 import com.android.camera.util.UsageStatistics;
 import com.android.camera2.R;
 import com.google.common.logging.eventprotos;
@@ -889,6 +891,12 @@
     }
 
     @Override
+    public void onPreviewInitialDataReceived() {
+        SmartCameraHelper.register(mCameraDevice, mParameters.getPreviewSize(), mActivity,
+                (ViewGroup) mActivity.findViewById(R.id.camera_app_root));
+    }
+
+    @Override
     public void stopPreview() {
         if (!mPreviewing) {
             return;
@@ -898,6 +906,7 @@
             mFocusManager.onPreviewStopped();
         }
         mPreviewing = false;
+        SmartCameraHelper.tearDown();
     }
 
     private void closeCamera() {
diff --git a/src/com/android/camera/app/CameraApp.java b/src/com/android/camera/app/CameraApp.java
index 1cfa5ff..3d250bf 100644
--- a/src/com/android/camera/app/CameraApp.java
+++ b/src/com/android/camera/app/CameraApp.java
@@ -22,12 +22,14 @@
 
 import com.android.camera.MediaSaverImpl;
 import com.android.camera.processing.ProcessingServiceManager;
+import com.android.camera.remote.RemoteShutterListener;
 import com.android.camera.session.CaptureSessionManager;
 import com.android.camera.session.CaptureSessionManagerImpl;
 import com.android.camera.session.PlaceholderManager;
 import com.android.camera.session.SessionStorageManager;
 import com.android.camera.session.SessionStorageManagerImpl;
 import com.android.camera.util.CameraUtil;
+import com.android.camera.util.RemoteShutterHelper;
 import com.android.camera.util.UsageStatistics;
 
 /**
@@ -40,6 +42,7 @@
     private SessionStorageManager mSessionStorageManager;
     private MemoryManagerImpl mMemoryManager;
     private PlaceholderManager mPlaceHolderManager;
+    private RemoteShutterListener mRemoteShutterListener;
 
     @Override
     public void onCreate() {
@@ -57,6 +60,7 @@
         mSessionManager = new CaptureSessionManagerImpl(mMediaSaver, getContentResolver(),
                 mPlaceHolderManager, mSessionStorageManager);
         mMemoryManager = MemoryManagerImpl.create(getApplicationContext(), mMediaSaver);
+        mRemoteShutterListener = RemoteShutterHelper.create(this);
 
         clearNotifications();
     }
@@ -77,6 +81,11 @@
         return mMediaSaver;
     }
 
+    @Override
+    public RemoteShutterListener getRemoteShutterListener() {
+        return mRemoteShutterListener;
+    }
+
     /**
      * Clears all notifications. This cleans up notifications that we might have
      * created earlier but remained after a crash.
diff --git a/src/com/android/camera/app/CameraServices.java b/src/com/android/camera/app/CameraServices.java
index 2c0216e..b6c8ec7 100644
--- a/src/com/android/camera/app/CameraServices.java
+++ b/src/com/android/camera/app/CameraServices.java
@@ -16,6 +16,7 @@
 
 package com.android.camera.app;
 
+import com.android.camera.remote.RemoteShutterListener;
 import com.android.camera.session.CaptureSessionManager;
 
 /**
@@ -43,4 +44,10 @@
      */
     @Deprecated
     public MediaSaver getMediaSaver();
+
+    /**
+     * @return A listener to be informed by events interesting for remote
+     *         capture apps. Will never return null.
+     */
+    public RemoteShutterListener getRemoteShutterListener();
 }
diff --git a/src/com/android/camera/data/CameraDataAdapter.java b/src/com/android/camera/data/CameraDataAdapter.java
index 5e4c860..67a9a0e 100644
--- a/src/com/android/camera/data/CameraDataAdapter.java
+++ b/src/com/android/camera/data/CameraDataAdapter.java
@@ -18,7 +18,6 @@
 
 import android.content.ContentResolver;
 import android.content.Context;
-import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.util.Log;
@@ -44,7 +43,7 @@
 
     private Listener mListener;
     private LocalDataListener mLocalDataListener;
-    private final Drawable mPlaceHolder;
+    private final int mPlaceHolderResourceId;
 
     private int mSuggestedWidth = DEFAULT_DECODE_SIZE;
     private int mSuggestedHeight = DEFAULT_DECODE_SIZE;
@@ -52,10 +51,10 @@
 
     private LocalData mLocalDataToDelete;
 
-    public CameraDataAdapter(Context context, Drawable placeHolder) {
+    public CameraDataAdapter(Context context, int placeholderResource) {
         mContext = context;
         mImages = new LocalDataList();
-        mPlaceHolder = placeHolder;
+        mPlaceHolderResourceId = placeholderResource;
     }
 
     @Override
@@ -91,6 +90,15 @@
     }
 
     @Override
+    public int getItemViewType(int dataId) {
+        if (dataId > mImages.size() || dataId < 0) {
+            return -1;
+        }
+
+        return mImages.get(dataId).getItemViewType().ordinal();
+    }
+
+    @Override
     public LocalData getLocalData(int dataID) {
         if (dataID < 0 || dataID >= mImages.size()) {
             return null;
@@ -116,14 +124,14 @@
     }
 
     @Override
-    public View getView(Context context, int dataID) {
+    public View getView(Context context, View recycled, int dataID) {
         if (dataID >= mImages.size() || dataID < 0) {
             return null;
         }
 
         return mImages.get(dataID).getView(
-                context, mSuggestedWidth, mSuggestedHeight,
-                mPlaceHolder.getConstantState().newDrawable(), this, /* inProgress */ false);
+                context, recycled, mSuggestedWidth, mSuggestedHeight,
+                mPlaceHolderResourceId, this, /* inProgress */ false);
     }
 
     @Override
@@ -131,7 +139,7 @@
         if (dataID >= mImages.size() || dataID < 0) {
             return;
         }
-        mImages.get(dataID).resizeView(context, w, h, view, this);
+        mImages.get(dataID).loadFullImage(context, w, h, view, this);
     }
 
     @Override
diff --git a/src/com/android/camera/data/CameraPreviewData.java b/src/com/android/camera/data/CameraPreviewData.java
index 350ee8d..efbbe35 100644
--- a/src/com/android/camera/data/CameraPreviewData.java
+++ b/src/com/android/camera/data/CameraPreviewData.java
@@ -35,7 +35,7 @@
      * @param height The height of the camera preview.
      */
     public CameraPreviewData(View v, int width, int height) {
-        super(v, width, height, -1, -1);
+        super(v, LocalDataViewType.CAMERA_PREVIEW, width, height, -1, -1);
         mPreviewLocked = true;
     }
 
diff --git a/src/com/android/camera/data/FixedFirstDataAdapter.java b/src/com/android/camera/data/FixedFirstDataAdapter.java
index 71ee632..05e5274 100644
--- a/src/com/android/camera/data/FixedFirstDataAdapter.java
+++ b/src/com/android/camera/data/FixedFirstDataAdapter.java
@@ -109,12 +109,20 @@
     }
 
     @Override
-    public View getView(Context context, int dataID) {
+    public View getView(Context context, View recycled, int dataID) {
         if (dataID == 0) {
             return mFirstData.getView(
-                    context, mSuggestedWidth, mSuggestedHeight, null, null, false);
+                    context, recycled, mSuggestedWidth, mSuggestedHeight, 0, null, false);
         }
-        return mAdapter.getView(context, dataID - 1);
+        return mAdapter.getView(context, recycled, dataID - 1);
+    }
+
+    @Override
+    public int getItemViewType(int dataId) {
+        if (dataId == 0) {
+            return mFirstData.getItemViewType().ordinal();
+        }
+        return mAdapter.getItemViewType(dataId);
     }
 
     @Override
diff --git a/src/com/android/camera/data/FixedLastDataAdapter.java b/src/com/android/camera/data/FixedLastDataAdapter.java
index c94c1b4..35978d8 100644
--- a/src/com/android/camera/data/FixedLastDataAdapter.java
+++ b/src/com/android/camera/data/FixedLastDataAdapter.java
@@ -112,20 +112,33 @@
     }
 
     @Override
-    public View getView(Context context, int dataID) {
+    public View getView(Context context, View recycled, int dataID) {
         int totalNumber = mAdapter.getTotalNumber();
 
         if (dataID < totalNumber) {
-            return mAdapter.getView(context, dataID);
+            return mAdapter.getView(context, recycled, dataID);
         } else if (dataID == totalNumber) {
-            return mLastData.getView(context,
-                    mSuggestedWidth, mSuggestedHeight, null, null, false);
+            return mLastData.getView(context, recycled,
+                    mSuggestedWidth, mSuggestedHeight, 0, null, false);
         }
 
         return null;
     }
 
     @Override
+    public int getItemViewType(int dataId) {
+        int totalNumber = mAdapter.getTotalNumber();
+
+        if (dataId < totalNumber) {
+            return mAdapter.getItemViewType(dataId);
+        } else if (dataId == totalNumber) {
+            return mLastData.getItemViewType().ordinal();
+        }
+
+        return -1;
+   }
+
+    @Override
     public void resizeView(Context context, int dataID, View view, int w, int h) {
         // Do nothing.
     }
diff --git a/src/com/android/camera/data/ImageModelLoader.java b/src/com/android/camera/data/ImageModelLoader.java
new file mode 100644
index 0000000..9eea4d3
--- /dev/null
+++ b/src/com/android/camera/data/ImageModelLoader.java
@@ -0,0 +1,31 @@
+package com.android.camera.data;
+
+import android.content.Context;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.loader.bitmap.model.ModelLoader;
+import com.bumptech.glide.loader.bitmap.model.stream.StreamModelLoader;
+import com.bumptech.glide.loader.bitmap.resource.ResourceFetcher;
+
+import java.io.InputStream;
+
+/**
+ * Translates a local data representing an image into an InputStream that can be loaded by the
+ * Glide library.
+ */
+public class ImageModelLoader implements StreamModelLoader<LocalData> {
+    private final ModelLoader<String, InputStream> mPathLoader;
+
+    public ImageModelLoader(Context context) {
+        mPathLoader = Glide.buildStreamModelLoader(String.class, context);
+    }
+
+    @Override
+    public ResourceFetcher<InputStream> getResourceFetcher(LocalData model, int width, int height) {
+        return mPathLoader.getResourceFetcher(model.getPath(), width, height);
+    }
+
+    @Override
+    public String getId(LocalData model) {
+        return mPathLoader.getId(model.getPath()) + model.getSignature();
+    }
+}
diff --git a/src/com/android/camera/data/LocalData.java b/src/com/android/camera/data/LocalData.java
index 31ecee4..b434f70 100644
--- a/src/com/android/camera/data/LocalData.java
+++ b/src/com/android/camera/data/LocalData.java
@@ -79,9 +79,16 @@
      * @param height Height in pixels of rendered view.
      * @param adapter Data adapter for this data item.
      */
-    View getView(Context context, int width, int height, Drawable placeHolder,
+    View getView(Context context, View recycled, int width, int height, int placeHolderResourceId,
             LocalDataAdapter adapter, boolean isInProgress);
 
+    /** Returns a unique identifier for the view created by this data so that the view
+     * can be reused.
+     *
+     * @see android.widget.BaseAdapter#getItemViewType(int)
+     */
+    LocalDataViewType getItemViewType();
+
    /**
      * Request resize of View created by getView().
      *
@@ -91,7 +98,7 @@
      * @param view View created by getView();
      * @param adapter Data adapter for this data item.
      */
-    public void resizeView(Context context, int width, int height, View view, LocalDataAdapter adapter);
+    public void loadFullImage(Context context, int width, int height, View view, LocalDataAdapter adapter);
 
     /**
      * Gets the date when this data is created. The returned date is also used
@@ -125,19 +132,6 @@
     /** Removes the data from the storage if possible. */
     boolean delete(Context c);
 
-    /**
-     * Rotate the image in 90 degrees. This is a no-op for non-image.
-     *
-     * @param context Used to update the content provider when rotation is done.
-     * @param adapter Used to update the view.
-     * @param currentDataId Used to update the view.
-     * @param clockwise True if the rotation goes clockwise.
-     *
-     * @return Whether the rotation is supported.
-     */
-    boolean rotate90Degrees(Context context, LocalDataAdapter adapter,
-            int currentDataId, boolean clockwise);
-
     void onFullScreen(boolean fullScreen);
 
     /** Returns {@code true} if it allows swipe to filmstrip in full screen. */
@@ -196,6 +190,15 @@
     Bundle getMetadata();
 
     /**
+     * Any media store attribute that can potentially change the local data
+     * should be included in this signature, primarily oriented at detecting
+     * edits.
+     *
+     * @return A string identifying the set of changeable attributes.
+     */
+    String getSignature();
+
+    /**
      * @return whether the metadata is updated.
      */
     public boolean isMetadataUpdated();
diff --git a/src/com/android/camera/data/LocalDataViewType.java b/src/com/android/camera/data/LocalDataViewType.java
new file mode 100644
index 0000000..39a0697
--- /dev/null
+++ b/src/com/android/camera/data/LocalDataViewType.java
@@ -0,0 +1,13 @@
+package com.android.camera.data;
+
+/**
+ * The set of all unique identifiers for all different views that may be shown
+ * in the Filmstrip.
+ */
+public enum LocalDataViewType {
+    CAMERA_PREVIEW,
+    PHOTO,
+    VIDEO,
+    SESSION,
+    SECURE_ALBUM_PLACEHOLDER,
+}
diff --git a/src/com/android/camera/data/LocalMediaData.java b/src/com/android/camera/data/LocalMediaData.java
index 0471eb8..19f972b 100644
--- a/src/com/android/camera/data/LocalMediaData.java
+++ b/src/com/android/camera/data/LocalMediaData.java
@@ -18,36 +18,28 @@
 
 import android.app.Activity;
 import android.content.ContentResolver;
-import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
-import android.graphics.Point;
-import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.media.CamcorderProfile;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.provider.MediaStore;
-import android.provider.MediaStore.Images;
 import android.util.Log;
-import android.view.Gravity;
+import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
 import android.widget.ImageView;
 
 import com.android.camera.Storage;
 import com.android.camera.util.CameraUtil;
 import com.android.camera2.R;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.presenter.target.ImageViewTarget;
+import com.bumptech.glide.presenter.target.Target;
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
 import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.Date;
@@ -91,11 +83,11 @@
             int width, int height, long sizeInBytes, double latitude,
             double longitude) {
         mContentId = contentId;
-        mTitle = new String(title);
-        mMimeType = new String(mimeType);
+        mTitle = title;
+        mMimeType = mimeType;
         mDateTakenInSeconds = dateTakenInSeconds;
         mDateModifiedInSeconds = dateModifiedInSeconds;
-        mPath = new String(path);
+        mPath = path;
         mWidth = width;
         mHeight = height;
         mSizeInBytes = sizeInBytes;
@@ -151,7 +143,7 @@
 
     @Override
     public String getTitle() {
-        return new String(mTitle);
+        return mTitle;
     }
 
     @Override
@@ -206,31 +198,41 @@
     }
 
     protected ImageView fillImageView(Context context, ImageView v,
-            int decodeWidth, int decodeHeight, Drawable placeHolder,
+            int decodeWidth, int decodeHeight, int placeHolderResourceId,
             LocalDataAdapter adapter, boolean isInProgress) {
-        v.setScaleType(ImageView.ScaleType.FIT_XY);
-        if (placeHolder != null) {
-            v.setImageDrawable(placeHolder);
+
+        SizedImageViewTarget target = (SizedImageViewTarget) v.getTag();
+        if (target != null) {
+            target = new SizedImageViewTarget(v);
+            v.setTag(target);
         }
 
-        // TODO: Load MediaStore or embedded-in-JPEG-stream thumbnail.
+        Glide.with(context)
+                .load(mPath)
+                .fitCenter()
+                .placeholder(placeHolderResourceId)
+                .into(target);
 
-        BitmapLoadTask task = getBitmapLoadTask(context, v, decodeWidth, decodeHeight,
-                context.getContentResolver(), adapter);
-        task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
         return v;
     }
 
     @Override
-    public View getView(Context context, int decodeWidth, int decodeHeight, Drawable placeHolder,
-            LocalDataAdapter adapter, boolean isInProgress) {
-        return fillImageView(context, new ImageView(context), decodeWidth, decodeHeight,
-                placeHolder, adapter, isInProgress);
+    public View getView(Context context, View recycled, int decodeWidth, int decodeHeight,
+            int placeHolderResourceId, LocalDataAdapter adapter, boolean isInProgress) {
+        final ImageView imageView;
+        if (recycled != null) {
+            imageView = (ImageView) recycled;
+        } else {
+            imageView = new ImageView(context);
+        }
+
+        return fillImageView(context, imageView, decodeWidth, decodeHeight,
+                placeHolderResourceId, adapter, isInProgress);
     }
 
     @Override
-    public void resizeView(Context context, int width, int height, View view,
-                           LocalDataAdapter adapter) {
+    public void loadFullImage(Context context, int width, int height, View view,
+            LocalDataAdapter adapter) {
         // Default is do nothing.
         // Can be implemented by sub-classes.
     }
@@ -243,7 +245,7 @@
     }
 
     @Override
-    public void recycle() {
+    public void recycle(View view) {
         synchronized (mUsing) {
             mUsing = false;
         }
@@ -304,15 +306,6 @@
         return MetadataLoader.isMetadataCached(this);
     }
 
-    /**
-     * A background task that loads the provided ImageView with a Bitmap.
-     * A Bitmap of maximum size that fits into a decodeWidth x decodeHeight
-     * box will be decoded.
-     */
-    protected abstract BitmapLoadTask getBitmapLoadTask(
-            Context context, ImageView v, int decodeWidth, int decodeHeight,
-            ContentResolver resolver, LocalDataAdapter adapter);
-
     public static final class PhotoData extends LocalMediaData {
         private static final String TAG = "PhotoData";
 
@@ -331,8 +324,6 @@
 
         // GL max texture size: keep bitmaps below this value.
         private static final int MAXIMUM_TEXTURE_SIZE = 2048;
-        // Maximum pixel count for Bitmaps.  To limit RAM consumption.
-        private static final int MAXIMUM_DECODE_PIXELS = 4000000;
 
         static final Uri CONTENT_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
 
@@ -362,6 +353,8 @@
 
         /** from MediaStore, can only be 0, 90, 180, 270 */
         private final int mOrientation;
+        /** @see #getSignature() */
+        private final String mSignature;
 
         public static LocalData fromContentUri(ContentResolver cr, Uri contentUri) {
             List<LocalData> newPhotos = query(cr, contentUri, QUERY_ALL_MEDIA_ID);
@@ -378,7 +371,7 @@
             super(id, title, mimeType, dateTakenInSeconds, dateModifiedInSeconds,
                     path, width, height, sizeInBytes, latitude, longitude);
             mOrientation = orientation;
-
+            mSignature = mimeType + orientation + dateModifiedInSeconds;
         }
 
         static List<LocalData> query(ContentResolver cr, Uri uri, long lastId) {
@@ -500,93 +493,58 @@
         }
 
         @Override
-        public void resizeView(Context context, int w, int h, View v, LocalDataAdapter adapter)
+        public String getSignature() {
+            return mSignature;
+        }
+
+        @Override
+        protected ImageView fillImageView(Context context, final ImageView v, final int decodeWidth,
+                final int decodeHeight, int placeHolderResourceId, LocalDataAdapter adapter,
+                boolean isInProgress) {
+            loadImage(context, v, decodeWidth, decodeHeight, placeHolderResourceId, false);
+            return v;
+        }
+
+        private void loadImage(Context context, ImageView imageView, int decodeWidth,
+                int decodeHeight, int placeHolderResourceId, boolean full) {
+            ThumbTarget thumbTarget = (ThumbTarget) imageView.getTag();
+            if (thumbTarget == null) {
+                thumbTarget = new ThumbTarget(imageView);
+                imageView.setTag(thumbTarget);
+            }
+            // Make sure we've reset all state related to showing thumb and full whenever
+            // we load a thumbnail because loading a thumbnail only happens when we're changing
+            // images.
+            if (!full) {
+                thumbTarget.clearTargets();
+            }
+
+            Glide.with(context)
+                    .using(new ImageModelLoader(context))
+                    .load(this)
+                    .placeholder(placeHolderResourceId)
+                    .fitCenter()
+                    .into(thumbTarget.getTarget(decodeWidth, decodeHeight, full));
+        }
+
+        @Override
+        public void recycle(View view) {
+            super.recycle(view);
+            if (view != null) {
+                ThumbTarget thumbTarget = (ThumbTarget) view.getTag();
+                thumbTarget.clearTargets();
+            }
+        }
+
+        @Override
+        public LocalDataViewType getItemViewType() {
+            return LocalDataViewType.PHOTO;
+        }
+
+        @Override
+        public void loadFullImage(Context context, int w, int h, View v, LocalDataAdapter adapter)
         {
-            // This will call PhotoBitmapLoadTask.
-            fillImageView(context, (ImageView) v, w, h, null, adapter, false);
-        }
-
-        @Override
-        protected BitmapLoadTask getBitmapLoadTask(Context context, ImageView v, int decodeWidth,
-                int decodeHeight, ContentResolver resolver, LocalDataAdapter adapter) {
-            return new PhotoBitmapLoadTask(context, v, decodeWidth, decodeHeight, resolver,
-                    adapter);
-        }
-
-        @Override
-        public boolean rotate90Degrees(Context context, LocalDataAdapter adapter,
-                int currentDataId, boolean clockwise) {
-            RotationTask task = new RotationTask(context, adapter,
-                    currentDataId, clockwise);
-            task.execute(this);
-            return true;
-        }
-
-        private final class PhotoBitmapLoadTask extends BitmapLoadTask {
-            private final int mDecodeWidth;
-            private final int mDecodeHeight;
-            private final Context mContext;
-            private final LocalDataAdapter mAdapter;
-
-            private boolean mNeedsRefresh;
-
-            public PhotoBitmapLoadTask(Context context, ImageView v, int decodeWidth,
-                    int decodeHeight, ContentResolver resolver, LocalDataAdapter adapter) {
-                super(context, v);
-                mDecodeWidth = decodeWidth;
-                mDecodeHeight = decodeHeight;
-                mContext = context;
-                mAdapter = adapter;
-            }
-
-            @Override
-            protected Bitmap doInBackground(Void... v) {
-                // TODO: Implement image cache, which can verify image dims.
-
-                // For correctness, double check image size here.
-                // This only takes 1% of full decode time.
-                Point decodedSize = LocalDataUtil.decodeBitmapDimension(mPath);
-
-                // If the width and height are valid and not matching the values
-                // from MediaStore, then update the MediaStore. This only
-                // happens when the MediaStore has been told incorrect values.
-                if (decodedSize != null && (decodedSize.x != mWidth || decodedSize.y != mHeight)) {
-                    ContentValues values = new ContentValues();
-                    values.put(Images.Media.WIDTH, decodedSize.x);
-                    values.put(Images.Media.HEIGHT, decodedSize.y);
-                    mContext.getContentResolver().update(getUri(), values, null, null);
-                    mNeedsRefresh = true;
-                    Log.w(TAG, "Uri " + getUri() + " has been updated with" +
-                            " the correct size!");
-                    return null;
-                }
-
-                InputStream stream;
-                Bitmap bitmap;
-                try {
-                    stream = new FileInputStream(mPath);
-                    bitmap = LocalDataUtil
-                            .loadImageThumbnailFromStream(stream, mWidth, mHeight, mDecodeWidth,
-                                    mDecodeHeight, mOrientation, MAXIMUM_DECODE_PIXELS);
-                    stream.close();
-                } catch (FileNotFoundException e) {
-                    Log.v(TAG, "File not found:" + mPath);
-                    bitmap = null;
-                } catch (IOException e) {
-                    Log.v(TAG, "IOException for " + mPath, e);
-                    bitmap = null;
-                }
-
-                return bitmap;
-            }
-
-            @Override
-            protected void onPostExecute(Bitmap bitmap) {
-                super.onPostExecute(bitmap);
-                if (mNeedsRefresh && mAdapter != null) {
-                    mAdapter.refresh(getUri());
-                }
-            }
+            loadImage(context, (ImageView) v, w, h, 0, true);
         }
 
         private static class PhotoDataBuilder implements CursorToLocalData {
@@ -595,6 +553,67 @@
                 return LocalMediaData.PhotoData.buildFromCursor(cursor);
             }
         }
+
+        /**
+         * In the filmstrip we want to load a small version of an image and then load a
+         * larger version when we switch to full screen mode. This class manages
+         * that transition and loading process by keeping one target for the smaller thumbnail
+         * and a second target for the larger version.
+         */
+        private static class ThumbTarget {
+            private final SizedImageViewTarget mThumbTarget;
+            private final SizedImageViewTarget mFullTarget;
+            private boolean fullLoaded = false;
+
+            public ThumbTarget(ImageView imageView) {
+                mThumbTarget = new SizedImageViewTarget(imageView) {
+                    @Override
+                    public void onImageReady(Bitmap bitmap) {
+                        // If we manage to load the thumb after the full, we don't
+                        // want to replace the higher quality full with the thumb.
+                        if (!fullLoaded) {
+                            super.onImageReady(bitmap);
+                        }
+                    }
+                };
+
+                mFullTarget = new SizedImageViewTarget(imageView) {
+
+                    @Override
+                    public void onImageReady(Bitmap bitmap) {
+                        // When the full is loaded, we no longer need the thumb.
+                        fullLoaded = true;
+                        Glide.cancel(mThumbTarget);
+                        super.onImageReady(bitmap);
+                    }
+
+                    @Override
+                    public void setPlaceholder(Drawable placeholder) {
+                        // We always load the thumb first which will set the placeholder.
+                        // If we were to set the placeholder here too, instead of showing
+                        // the thumb while we load the full, we will instead revert back
+                        // to the placeholder.
+                    }
+                };
+            }
+
+            public Target getTarget(int width, int height, boolean full) {
+                final SizedImageViewTarget result = full ? mFullTarget : mThumbTarget;
+                // Limit the target size so we don't load a bitmap larger than the max size we
+                // can display.
+                width = Math.min(width, MAXIMUM_TEXTURE_SIZE);
+                height = Math.min(height, MAXIMUM_TEXTURE_SIZE);
+
+                result.setSize(width, height);
+                return result;
+            }
+
+            public void clearTargets() {
+                fullLoaded = false;
+                Glide.cancel(mThumbTarget);
+                Glide.cancel(mFullTarget);
+            }
+        }
     }
 
     public static final class VideoData extends LocalMediaData {
@@ -606,11 +625,10 @@
         public static final int COL_DATA = 5;
         public static final int COL_WIDTH = 6;
         public static final int COL_HEIGHT = 7;
-        public static final int COL_RESOLUTION = 8;
-        public static final int COL_SIZE = 9;
-        public static final int COL_LATITUDE = 10;
-        public static final int COL_LONGITUDE = 11;
-        public static final int COL_DURATION = 12;
+        public static final int COL_SIZE = 8;
+        public static final int COL_LATITUDE = 9;
+        public static final int COL_LONGITUDE = 10;
+        public static final int COL_DURATION = 11;
 
         static final Uri CONTENT_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
 
@@ -632,15 +650,15 @@
                 MediaStore.Video.VideoColumns.DATA,          // 5, string
                 MediaStore.Video.VideoColumns.WIDTH,         // 6, int
                 MediaStore.Video.VideoColumns.HEIGHT,        // 7, int
-                MediaStore.Video.VideoColumns.RESOLUTION,    // 8 string
-                MediaStore.Video.VideoColumns.SIZE,          // 9 long
-                MediaStore.Video.VideoColumns.LATITUDE,      // 10 double
-                MediaStore.Video.VideoColumns.LONGITUDE,     // 11 double
-                MediaStore.Video.VideoColumns.DURATION       // 12 long
+                MediaStore.Video.VideoColumns.SIZE,          // 8 long
+                MediaStore.Video.VideoColumns.LATITUDE,      // 9 double
+                MediaStore.Video.VideoColumns.LONGITUDE,     // 10 double
+                MediaStore.Video.VideoColumns.DURATION       // 11 long
         };
 
         /** The duration in milliseconds. */
         private final long mDurationInSeconds;
+        private final String mSignature;
 
         public VideoData(long id, String title, String mimeType,
                 long dateTakenInSeconds, long dateModifiedInSeconds,
@@ -649,6 +667,7 @@
             super(id, title, mimeType, dateTakenInSeconds, dateModifiedInSeconds,
                     path, width, height, sizeInBytes, latitude, longitude);
             mDurationInSeconds = durationInSeconds;
+            mSignature = mimeType + dateModifiedInSeconds;
         }
 
         public static LocalData fromContentUri(ContentResolver cr, Uri contentUri) {
@@ -792,26 +811,54 @@
         }
 
         @Override
-        public View getView(final Context context,
-                int decodeWidth, int decodeHeight, Drawable placeHolder,
+        public String getSignature() {
+            return mSignature;
+        }
+
+        @Override
+        protected ImageView fillImageView(Context context, final ImageView v, final int decodeWidth,
+                final int decodeHeight, int placeHolderResourceId, LocalDataAdapter adapter,
+                boolean isInProgress) {
+            SizedImageViewTarget target = (SizedImageViewTarget) v.getTag();
+            if (target == null) {
+                target = new SizedImageViewTarget(v);
+                v.setTag(target);
+            }
+            target.setSize(decodeWidth, decodeHeight);
+
+            Glide.with(context)
+                    .using(new VideoModelLoader(context))
+                    .loadFromVideo(this)
+                    .placeholder(placeHolderResourceId)
+                    .fitCenter()
+                    .into(target);
+
+            return v;
+        }
+
+        @Override
+        public View getView(final Context context, View recycled,
+                int decodeWidth, int decodeHeight, int placeHolderResourceId,
                 LocalDataAdapter adapter, boolean isInProgress) {
 
-            // ImageView for the bitmap.
-            ImageView iv = new ImageView(context);
-            iv.setLayoutParams(new FrameLayout.LayoutParams(
-                    ViewGroup.LayoutParams.MATCH_PARENT,
-                    ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
-            fillImageView(context, iv, decodeWidth, decodeHeight, placeHolder,
-                    adapter, isInProgress);
+            final VideoViewHolder viewHolder;
+            final View result;
+            if (recycled != null) {
+                result = recycled;
+                viewHolder = (VideoViewHolder) recycled.getTag();
+            } else {
+                result = LayoutInflater.from(context).inflate(R.layout.filmstrip_video, null);
+                ImageView videoView = (ImageView) result.findViewById(R.id.video_view);
+                ImageView playButton = (ImageView) result.findViewById(R.id.play_button);
+                viewHolder = new VideoViewHolder(videoView, playButton);
+                result.setTag(viewHolder);
+            }
+
+            fillImageView(context, viewHolder.mVideoView, decodeWidth, decodeHeight,
+                    placeHolderResourceId, adapter, isInProgress);
 
             // ImageView for the play icon.
-            ImageView icon = new ImageView(context);
-            icon.setImageResource(R.drawable.ic_control_play);
-            icon.setScaleType(ImageView.ScaleType.CENTER);
-            icon.setLayoutParams(new FrameLayout.LayoutParams(
-                    ViewGroup.LayoutParams.WRAP_CONTENT,
-                    ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
-            icon.setOnClickListener(new View.OnClickListener() {
+            viewHolder.mPlayButton.setOnClickListener(new View.OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     // TODO: refactor this into activities to avoid this class
@@ -820,77 +867,20 @@
                 }
             });
 
-            FrameLayout f = new FrameLayout(context);
-            f.addView(iv);
-            f.addView(icon);
-            return f;
+            return result;
         }
 
         @Override
-        protected BitmapLoadTask getBitmapLoadTask(
-                Context context, ImageView v, int decodeWidth, int decodeHeight,
-                ContentResolver resolver, LocalDataAdapter adapter) {
-            // TODO: Support isInProgressSession for videos when we need it.
-            return new VideoBitmapLoadTask(context, v);
-        }
-
-        private final class VideoBitmapLoadTask extends BitmapLoadTask {
-
-            public VideoBitmapLoadTask(Context context, ImageView v) {
-                super(context, v);
-            }
-
-            @Override
-            protected Bitmap doInBackground(Void... v) {
-                if (isCancelled() || !isUsing()) {
-                    return null;
-                }
-                Bitmap bitmap = null;
-                bitmap = LocalDataUtil.loadVideoThumbnail(mPath);
-
-                if (isCancelled() || !isUsing()) {
-                    return null;
-                }
-                return bitmap;
-            }
+        public void recycle(View view) {
+            super.recycle(view);
+            VideoViewHolder videoViewHolder = (VideoViewHolder) view.getTag();
+            Target target = (Target) videoViewHolder.mVideoView.getTag();
+            Glide.cancel(target);
         }
 
         @Override
-        public boolean rotate90Degrees(Context context, LocalDataAdapter adapter,
-                int currentDataId, boolean clockwise) {
-            // We don't support rotation for video data.
-            Log.e(TAG, "Unexpected call in rotate90Degrees()");
-            return false;
-        }
-    }
-
-    /**
-     * An {@link AsyncTask} class that loads the bitmap in the background
-     * thread. Sub-classes should implement their own
-     * {@code BitmapLoadTask#doInBackground(Void...)}."
-     */
-    protected abstract class BitmapLoadTask extends AsyncTask<Void, Void, Bitmap> {
-        protected final Context mContext;
-        protected ImageView mView;
-
-        protected BitmapLoadTask(Context context, ImageView v) {
-            mContext = context;
-            mView = v;
-        }
-
-        @Override
-        protected void onPostExecute(Bitmap bitmap) {
-            if (!isUsing()) {
-                return;
-            }
-            if (bitmap == null) {
-                Log.e(TAG, "Failed decoding bitmap for file:" + mPath);
-                return;
-            }
-            BitmapDrawable d = new BitmapDrawable(mContext.getResources(), bitmap);
-            mView.setScaleType(ImageView.ScaleType.FIT_XY);
-            mView.setImageDrawable(d);
-            Log.v(TAG, "Created bitmap: " + bitmap.getWidth() + " x " + bitmap.getHeight());
+        public LocalDataViewType getItemViewType() {
+            return LocalDataViewType.VIDEO;
         }
     }
 
@@ -902,4 +892,39 @@
         }
     }
 
+     private static class VideoViewHolder {
+        private final ImageView mVideoView;
+        private final ImageView mPlayButton;
+
+        public VideoViewHolder(ImageView videoView, ImageView playButton) {
+            mVideoView = videoView;
+            mPlayButton = playButton;
+        }
+    }
+
+    /**
+     * Normally Glide will figure out the necessary size based on the view
+     * the image is being loaded into. In filmstrip the view isn't immediately
+     * laid out after being requested from the data, which can cause Glide to give
+     * up on obtaining the view dimensions. To avoid that, we manually set the
+     * dimensions.
+     */
+    private static class SizedImageViewTarget extends ImageViewTarget {
+        private int mWidth;
+        private int mHeight;
+
+        public SizedImageViewTarget(ImageView imageView) {
+            super(imageView);
+        }
+
+        public void setSize(int width, int height) {
+            mWidth = width;
+            mHeight = height;
+        }
+
+        @Override
+        public void getSize(final SizeReadyCallback cb) {
+            cb.onSizeReady(mWidth, mHeight);
+        }
+    }
 }
diff --git a/src/com/android/camera/data/LocalSessionData.java b/src/com/android/camera/data/LocalSessionData.java
index 8e82a76..43227d2 100644
--- a/src/com/android/camera/data/LocalSessionData.java
+++ b/src/com/android/camera/data/LocalSessionData.java
@@ -52,19 +52,30 @@
     }
 
     @Override
-    public View getView(Context context, int width, int height, Drawable placeHolder,
-           LocalDataAdapter adapter, boolean isInProgress) {
-        //TODO do this on a background thread
+    public View getView(Context context, View recycled, int width, int height,
+            int placeholderResourcedId, LocalDataAdapter adapter, boolean isInProgress) {
+        final ImageView imageView;
+        if (recycled != null) {
+            imageView = (ImageView) recycled;
+        } else {
+            imageView = new ImageView(context);
+        }
+
         byte[] jpegData = Storage.getJpegForSession(mUri);
+        //TODO do this on a background thread
         Bitmap bmp = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
-        ImageView imageView = new ImageView(context);
         imageView.setImageBitmap(bmp);
         return imageView;
     }
 
     @Override
-    public void resizeView(Context context, int width, int height, View view,
-           LocalDataAdapter adapter) {
+    public LocalDataViewType getItemViewType() {
+        return LocalDataViewType.SESSION;
+    }
+
+    @Override
+    public void loadFullImage(Context context, int width, int height, View view,
+            LocalDataAdapter adapter) {
 
     }
 
@@ -94,11 +105,6 @@
     }
 
     @Override
-    public boolean rotate90Degrees(Context context, LocalDataAdapter adapter, int currentDataId, boolean clockwise) {
-        return false;
-    }
-
-    @Override
     public void onFullScreen(boolean fullScreen) {
 
     }
@@ -149,6 +155,11 @@
     }
 
     @Override
+    public String getSignature() {
+        return "";
+    }
+
+    @Override
     public boolean isMetadataUpdated() {
         return true;
     }
@@ -189,7 +200,7 @@
     }
 
     @Override
-    public void recycle() {
+    public void recycle(View view) {
 
     }
 
diff --git a/src/com/android/camera/data/RotationTask.java b/src/com/android/camera/data/RotationTask.java
deleted file mode 100644
index 41495b8..0000000
--- a/src/com/android/camera/data/RotationTask.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2013 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.camera.data;
-
-import android.app.ProgressDialog;
-import android.content.ContentValues;
-import android.content.Context;
-import android.os.AsyncTask;
-import android.provider.MediaStore.Images;
-import android.util.Log;
-
-import com.android.camera.data.LocalMediaData.PhotoData;
-import com.android.camera.exif.ExifInterface;
-import com.android.camera.exif.ExifTag;
-import com.android.camera2.R;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-
-/**
- * RotationTask can be used to rotate a {@link LocalData} by updating the exif
- * data from jpeg file. Note that only {@link PhotoData}  can be rotated.
- */
-public class RotationTask extends AsyncTask<LocalData, Void, LocalData> {
-    private static final String TAG = "RotationTask";
-    private final Context mContext;
-    private final LocalDataAdapter mAdapter;
-    private final int mCurrentDataId;
-    private final boolean mClockwise;
-    private ProgressDialog mProgress;
-
-    public RotationTask(Context context, LocalDataAdapter adapter,
-            int currentDataId, boolean clockwise) {
-        mContext = context;
-        mAdapter = adapter;
-        mCurrentDataId = currentDataId;
-        mClockwise = clockwise;
-    }
-
-    @Override
-    protected void onPreExecute() {
-        // Show a progress bar since the rotation could take long.
-        mProgress = new ProgressDialog(mContext);
-        int titleStringId = mClockwise ? R.string.rotate_right : R.string.rotate_left;
-        mProgress.setTitle(mContext.getString(titleStringId));
-        mProgress.setMessage(mContext.getString(R.string.please_wait));
-        mProgress.setCancelable(false);
-        mProgress.show();
-    }
-
-    @Override
-    protected LocalData doInBackground(LocalData... data) {
-        return rotateInJpegExif(data[0]);
-    }
-
-    /**
-     * Rotates the image by updating the exif. Done in background thread.
-     * The worst case is the whole file needed to be re-written with
-     * modified exif data.
-     *
-     * @return A new {@link LocalData} object which containing the new info.
-     */
-    private LocalData rotateInJpegExif(LocalData data) {
-        if (!(data instanceof PhotoData)) {
-            Log.w(TAG, "Rotation can only happen on PhotoData.");
-            return null;
-        }
-
-        PhotoData imageData = (PhotoData) data;
-        int originRotation = imageData.getRotation();
-        int finalRotationDegrees;
-        if (mClockwise) {
-            finalRotationDegrees = (originRotation + 90) % 360;
-        } else {
-            finalRotationDegrees = (originRotation + 270) % 360;
-        }
-
-        String filePath = imageData.getPath();
-        ContentValues values = new ContentValues();
-        boolean success = false;
-        int newOrientation = 0;
-        if (imageData.getMimeType().equalsIgnoreCase(LocalData.MIME_TYPE_JPEG)) {
-            ExifInterface exifInterface = new ExifInterface();
-            ExifTag tag = exifInterface.buildTag(
-                    ExifInterface.TAG_ORIENTATION,
-                    ExifInterface.getOrientationValueForRotation(
-                            finalRotationDegrees));
-            if (tag != null) {
-                exifInterface.setTag(tag);
-                try {
-                    // Note: This only works if the file already has some EXIF.
-                    exifInterface.forceRewriteExif(filePath);
-                    long fileSize = new File(filePath).length();
-                    values.put(Images.Media.SIZE, fileSize);
-                    newOrientation = finalRotationDegrees;
-                    success = true;
-                } catch (FileNotFoundException e) {
-                    Log.w(TAG, "Cannot find file to set exif: " + filePath);
-                } catch (IOException e) {
-                    Log.w(TAG, "Cannot set exif data: " + filePath);
-                }
-            } else {
-                Log.w(TAG, "Cannot build tag: " + ExifInterface.TAG_ORIENTATION);
-            }
-        }
-
-        PhotoData result = null;
-        if (success) {
-            // MediaStore using SQLite is thread safe.
-            values.put(Images.Media.ORIENTATION, finalRotationDegrees);
-            mContext.getContentResolver().update(imageData.getUri(),
-                    values, null, null);
-            double[] latLong = data.getLatLong();
-            double latitude = 0;
-            double longitude = 0;
-            if (latLong != null) {
-                latitude = latLong[0];
-                longitude = latLong[1];
-            }
-
-            result = new PhotoData(data.getContentId(), data.getTitle(),
-                    data.getMimeType(), data.getDateTaken(), data.getDateModified(),
-                    data.getPath(), newOrientation, imageData.getWidth(),
-                    imageData.getHeight(), data.getSizeInBytes(), latitude, longitude);
-        }
-
-        return result;
-    }
-
-    @Override
-    protected void onPostExecute(LocalData result) {
-        mProgress.dismiss();
-        if (result != null) {
-            mAdapter.updateData(mCurrentDataId, result);
-        }
-    }
-}
diff --git a/src/com/android/camera/data/SimpleViewData.java b/src/com/android/camera/data/SimpleViewData.java
index a2091c3..3692e2c 100644
--- a/src/com/android/camera/data/SimpleViewData.java
+++ b/src/com/android/camera/data/SimpleViewData.java
@@ -41,11 +41,13 @@
     private final long mDateModified;
     private final Bundle mMetaData;
     private final Uri mUri;
+    private final LocalDataViewType mItemViewType;
 
     public SimpleViewData(
-            View v, int width, int height,
+            View v, LocalDataViewType viewType, int width, int height,
             int dateTaken, int dateModified) {
         mView = v;
+        mItemViewType = viewType;
         mWidth = width;
         mHeight = height;
         mDateTaken = dateTaken;
@@ -93,6 +95,11 @@
     }
 
     @Override
+    public LocalDataViewType getItemViewType() {
+        return mItemViewType;
+    }
+
+    @Override
     public String getPath() {
         return "";
     }
@@ -128,13 +135,13 @@
     }
 
     @Override
-    public View getView(Context context, int width, int height, Drawable placeHolder,
+    public View getView(Context context, View recycled, int width, int height, int placeHolderResourceId,
             LocalDataAdapter adapter, boolean isInProgressSession) {
         return mView;
     }
 
     @Override
-    public void resizeView(Context context, int w, int h, View view, LocalDataAdapter adapter) {
+    public void loadFullImage(Context context, int w, int h, View view, LocalDataAdapter adapter) {
         // do nothing.
     }
 
@@ -144,8 +151,8 @@
     }
 
     @Override
-    public void recycle() {
-        // do nothing.
+    public void recycle(View view) {
+        // Do nothing.
     }
 
     @Override
@@ -174,14 +181,6 @@
     }
 
     @Override
-    public boolean rotate90Degrees(Context context, LocalDataAdapter adapter,
-            int currentDataId, boolean clockwise) {
-        // We don't support rotation for SimpleViewData.
-        Log.w(TAG, "Unexpected call in rotate90Degrees()");
-        return false;
-    }
-
-    @Override
     public long getSizeInBytes() {
         return 0;
     }
@@ -197,6 +196,11 @@
     }
 
     @Override
+    public String getSignature() {
+        return "";
+    }
+
+    @Override
     public boolean isMetadataUpdated() {
         return true;
     }
diff --git a/src/com/android/camera/data/VideoModelLoader.java b/src/com/android/camera/data/VideoModelLoader.java
new file mode 100644
index 0000000..b140037
--- /dev/null
+++ b/src/com/android/camera/data/VideoModelLoader.java
@@ -0,0 +1,29 @@
+package com.android.camera.data;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.loader.bitmap.model.ModelLoader;
+import com.bumptech.glide.loader.bitmap.model.file_descriptor.FileDescriptorModelLoader;
+import com.bumptech.glide.loader.bitmap.resource.ResourceFetcher;
+
+/**
+ * Translates a video data into an InputStream for the Glide library.
+ */
+public class VideoModelLoader implements FileDescriptorModelLoader<LocalMediaData.VideoData> {
+    private final ModelLoader<String, ParcelFileDescriptor> mPathLoader;
+
+    public VideoModelLoader(Context context) {
+        mPathLoader = Glide.buildFileDescriptorModelLoader(String.class, context);
+    }
+
+    @Override
+    public ResourceFetcher<ParcelFileDescriptor> getResourceFetcher(LocalMediaData.VideoData model,
+            int width, int height) {
+        return mPathLoader.getResourceFetcher(model.getPath(), width, height);
+    }
+
+    public String getId(LocalMediaData.VideoData model) {
+        return mPathLoader.getId(model.getPath()) + model.getSignature();
+    }
+}
diff --git a/src/com/android/camera/filmstrip/DataAdapter.java b/src/com/android/camera/filmstrip/DataAdapter.java
index e896908..e1ca61d 100644
--- a/src/com/android/camera/filmstrip/DataAdapter.java
+++ b/src/com/android/camera/filmstrip/DataAdapter.java
@@ -80,11 +80,19 @@
      * Returns the view to visually present the image data.
      *
      * @param context The {@link android.content.Context} to create the view.
+     * @param recycled A view that can be reused if one is available, or null.
      * @param dataID The ID of the image data to be presented.
      * @return The view representing the image data. Null if unavailable or
      *         the {@code dataID} is out of range.
      */
-    public View getView(Context context, int dataID);
+    public View getView(Context context, View recycled, int dataID);
+
+    /** Returns a unique identifier for the view created by this data so that the view
+     * can be reused.
+     *
+     * @see android.widget.BaseAdapter#getItemViewType(int)
+     */
+    public int getItemViewType(int dataId);
 
     /**
      * Resizes the view used to visually present the image data.  This is
diff --git a/src/com/android/camera/filmstrip/ImageData.java b/src/com/android/camera/filmstrip/ImageData.java
index 1bbea11..f080f8e 100644
--- a/src/com/android/camera/filmstrip/ImageData.java
+++ b/src/com/android/camera/filmstrip/ImageData.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.net.Uri;
+import android.view.View;
 
 /**
  * Common interface for all images in the filmstrip.
@@ -111,7 +112,7 @@
      * function after its corresponding view is removed from the view
      * hierarchy.
      */
-    public void recycle();
+    public void recycle(View view);
 
     /**
      * @return The URI of this data. Must be a unique one and not null.
diff --git a/src/com/android/camera/module/ModuleController.java b/src/com/android/camera/module/ModuleController.java
index a61c590..d0ad456 100644
--- a/src/com/android/camera/module/ModuleController.java
+++ b/src/com/android/camera/module/ModuleController.java
@@ -74,11 +74,6 @@
     public void onPreviewVisibilityChanged(int visibility);
 
     /**
-     * Called when the first preview data is received.
-     */
-    public void onPreviewInitialDataReceived();
-
-    /**
      * Called when the framework layout orientation changed.
      *
      * @param isLandscape Whether the new orientation is landscape or portrait.
diff --git a/src/com/android/camera/remote/RemoteCameraModule.java b/src/com/android/camera/remote/RemoteCameraModule.java
new file mode 100644
index 0000000..d094224
--- /dev/null
+++ b/src/com/android/camera/remote/RemoteCameraModule.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2014 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.camera.remote;
+
+/**
+ * Modules implementing this interface signal that they do support remote
+ * shutter. Such modules need to signal to the remote interface that they are
+ * available or unavailable through calling
+ * {@link RemoteShutterListener#onModuleReady(RemoteCameraModule)} and
+ * {@link RemoteShutterListener#onModuleExit()}.
+ */
+public interface RemoteCameraModule {
+    /**
+     * Called when a remote client wants the module to take a picture. The
+     * module should trigger a capture and send the result via
+     * {@link RemoteShutterListener#onPictureTaken(byte[])}.
+     */
+    void onRemoteShutterPress();
+}
diff --git a/src/com/android/camera/remote/RemoteShutterListener.java b/src/com/android/camera/remote/RemoteShutterListener.java
new file mode 100644
index 0000000..b24a4a1
--- /dev/null
+++ b/src/com/android/camera/remote/RemoteShutterListener.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2014 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.camera.remote;
+
+/**
+ * Classes implementing this interface can be informed when events relevant to
+ * remote shutter apps are occurring.
+ */
+public interface RemoteShutterListener {
+    /**
+     * Called when the module is active and ready for shutter presses.
+     */
+    void onModuleReady(RemoteCameraModule module);
+
+    /**
+     * Called when module is no longer ready for shutter presses.
+     */
+    void onModuleExit();
+
+    /**
+     * Called when a picture is taken.
+     */
+    void onPictureTaken(byte[] photoData);
+}
diff --git a/src/com/android/camera/widget/FilmstripView.java b/src/com/android/camera/widget/FilmstripView.java
index 012408a..c49a813 100644
--- a/src/com/android/camera/widget/FilmstripView.java
+++ b/src/com/android/camera/widget/FilmstripView.java
@@ -31,6 +31,7 @@
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -46,7 +47,11 @@
 import com.android.camera.util.CameraUtil;
 import com.android.camera2.R;
 
+import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
+import java.util.Queue;
 
 public class FilmstripView extends ViewGroup {
     private static final String TAG = "FilmStripView";
@@ -105,6 +110,8 @@
     private float mOverScaleFactor = 1f;
 
     private boolean mFullScreenUIHidden = false;
+    private SparseArray<Queue<View>> recycledViews = new SparseArray<Queue<View>>();
+
 
     /**
      * A helper class to tract and calculate the view coordination.
@@ -353,7 +360,8 @@
         public void removeViewFromHierarchy(boolean force) {
             if (force || mData.getViewType() != ImageData.VIEW_TYPE_STICKY) {
                 removeView(mView);
-                mData.recycle();
+                mData.recycle(mView);
+                recycleView(mView, mDataId);
             } else {
                 setVisibility(View.INVISIBLE);
             }
@@ -595,6 +603,26 @@
         }
     }
 
+    private void recycleView(View view, int dataId) {
+        final int viewType = mDataAdapter.getItemViewType(dataId);
+        Queue<View> recycledViewsForType = recycledViews.get(viewType);
+        if (recycledViewsForType == null) {
+            recycledViewsForType = new ArrayDeque<View>();
+            recycledViews.put(viewType, recycledViewsForType);
+        }
+        recycledViewsForType.offer(view);
+    }
+
+    private View getRecycledView(int dataId) {
+        final int viewType = mDataAdapter.getItemViewType(dataId);
+        Queue<View> recycledViewsForType = recycledViews.get(viewType);
+        View result = null;
+        if (recycledViewsForType != null) {
+            result = recycledViewsForType.poll();
+        }
+        return result;
+    }
+
     /**
      * Returns the controller.
      *
@@ -709,11 +737,13 @@
             return null;
         }
 
-        int maxEdge = (int) ((float) Math.max(this.getHeight(), this.getWidth())
-                * FILM_STRIP_SCALE);
-        mDataAdapter.suggestViewSizeBound(maxEdge, maxEdge);
+        int width = Math.round(mScale * getWidth());
+        int height = Math.round(mScale * getHeight());
+        mDataAdapter.suggestViewSizeBound(width, height);
+
         data.prepare();
-        View v = mDataAdapter.getView(mActivity, dataID);
+        View recycled = getRecycledView(dataID);
+        View v = mDataAdapter.getView(mActivity, recycled, dataID);
         if (v == null) {
             return null;
         }
@@ -925,6 +955,7 @@
 
         curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale);
         curr.setAlpha(1f);
+        curr.setVisibility(VISIBLE);
 
         if (inFullScreen()) {
             curr.setTranslationX(translate * (mCenterX - currCenterX) / (nextCenterX - currCenterX));
@@ -1409,7 +1440,9 @@
 
     private void setDataAdapter(DataAdapter adapter) {
         mDataAdapter = adapter;
-        mDataAdapter.suggestViewSizeBound(getMeasuredWidth(), getMeasuredHeight());
+        int maxEdge = (int) ((float) Math.max(this.getHeight(), this.getWidth())
+                * FILM_STRIP_SCALE);
+        mDataAdapter.suggestViewSizeBound(maxEdge, maxEdge);
         mDataAdapter.setListener(new DataAdapter.Listener() {
             @Override
             public void onDataLoaded() {
diff --git a/src_pd/com/android/camera/util/RemoteShutterHelper.java b/src_pd/com/android/camera/util/RemoteShutterHelper.java
new file mode 100644
index 0000000..78028ad
--- /dev/null
+++ b/src_pd/com/android/camera/util/RemoteShutterHelper.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014 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.camera.util;
+
+import android.content.Context;
+
+import com.android.camera.remote.RemoteCameraModule;
+import com.android.camera.remote.RemoteShutterListener;
+
+public class RemoteShutterHelper {
+    public static RemoteShutterListener create(Context context) {
+        return new RemoteShutterListener() {
+            @Override
+            public void onPictureTaken(byte[] photoData) {
+            }
+
+            @Override
+            public void onModuleReady(RemoteCameraModule module) {
+            }
+
+            @Override
+            public void onModuleExit() {
+            }
+        };
+    }
+}
diff --git a/src_pd/com/android/camera/util/SmartCameraHelper.java b/src_pd/com/android/camera/util/SmartCameraHelper.java
new file mode 100644
index 0000000..43c0e83
--- /dev/null
+++ b/src_pd/com/android/camera/util/SmartCameraHelper.java
@@ -0,0 +1,32 @@
+/*
+ * 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.camera.util;
+
+import android.app.Activity;
+import android.hardware.Camera;
+import android.view.ViewGroup;
+
+import com.android.camera.app.CameraManager.CameraProxy;
+
+public class SmartCameraHelper {
+    public static void register(CameraProxy camera, Camera.Size previewSize, Activity activity,
+            ViewGroup parentView) {
+    }
+
+    public static void tearDown() {
+    }
+}