Merge "Remove deprecated carousel from frameworks/ex." into lmp-dev
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java
index 65047f1..e675796 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java
@@ -19,7 +19,9 @@
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.graphics.ImageFormat;
+import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraCaptureSession;
@@ -215,6 +217,8 @@
                         mOpenCallback = openCallback;
                         mCameraIndex = cameraIndex;
                         mCameraId = mCameraDevices.get(mCameraIndex);
+                        Log.i(TAG, String.format("Opening camera index %d (id %s) with camera2 API",
+                                cameraIndex, mCameraId));
 
                         if (mCameraId == null) {
                             mOpenCallback.onCameraDisabled(msg.arg1);
@@ -458,7 +462,6 @@
                     }*/
 
                     case CameraActions.SET_DISPLAY_ORIENTATION: {
-                        // TODO: Need to handle preview in addition to capture
                         // Only set the JPEG capture orientation if requested to do so; otherwise,
                         // capture in the sensor's physical orientation
                         mPersistentSettings.set(CaptureRequest.JPEG_ORIENTATION, msg.arg2 > 0 ?
@@ -858,6 +861,9 @@
         public void setPreviewDataCallbackWithBuffer(Handler handler, CameraPreviewDataCallback cb)
                 {}
 
+        // TODO: Implement
+        public void addCallbackBuffer(final byte[] callbackBuffer) {}
+
         @Override
         public void autoFocus(final Handler handler, final CameraAFCallback cb) {
             mDispatchThread.runJob(new Runnable() {
@@ -1139,11 +1145,51 @@
             }
 
             @Override
+            public Matrix getPreviewTransform(int currentDisplayOrientation,
+                                              RectF surfaceDimensions,
+                                              RectF desiredBounds) {
+                if (!orientationIsValid(currentDisplayOrientation)) {
+                    return new Matrix();
+                }
+
+                // The system transparently transforms the image to fill the surface
+                // when the device is in its natural orientation. We rotate the
+                // coordinates of the rectangle's corners to be relative to the
+                // original image, instead of to the current screen orientation.
+                float[] surfacePolygon = rotate(convertRectToPoly(surfaceDimensions),
+                        2 * currentDisplayOrientation / 90);
+                float[] desiredPolygon = convertRectToPoly(desiredBounds);
+
+                Matrix transform = new Matrix();
+                // Use polygons instead of rectangles so that rotation will be
+                // calculated, since that is not done by the new camera API.
+                transform.setPolyToPoly(surfacePolygon, 0, desiredPolygon, 0, 4);
+                return transform;
+            }
+
+            @Override
             public boolean canDisableShutterSound() {
                 // The new API doesn't support this operation, so don't encourage people to try it.
                 // TODO: What kind of assumptions have callers made about this result's meaning?
                 return false;
             }
+
+            private static float[] convertRectToPoly(RectF rf) {
+                return new float[] {rf.left, rf.top, rf.right, rf.top,
+                        rf.right, rf.bottom, rf.left, rf.bottom};
+            }
+
+            private static float[] rotate(float[] arr, int times) {
+                if (times < 0) {
+                    times = times % arr.length + arr.length;
+                }
+
+                float[] res = new float[arr.length];
+                for (int offset = 0; offset < arr.length; ++offset) {
+                    res[offset] = arr[(times + offset) % arr.length];
+                }
+                return res;
+            }
         }
     }
 
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Capabilities.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Capabilities.java
index 4bdbe64..51c1422 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Capabilities.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Capabilities.java
@@ -18,14 +18,14 @@
 
 import static android.hardware.camera2.CameraCharacteristics.*;
 
+import android.graphics.ImageFormat;
 import android.graphics.Point;
+import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.params.StreamConfigurationMap;
-import android.media.ImageReader;
 import android.media.MediaRecorder;
 import android.util.Range;
 import android.util.Rational;
-import android.view.SurfaceHolder;
 
 import com.android.ex.camera2.portability.debug.Log;
 
@@ -47,20 +47,20 @@
             mSupportedPreviewFpsRange.add(new int[] { fpsRange.getLower(), fpsRange.getUpper() });
         }
 
-        // TODO: We only support SurfaceView preview rendering
+        // TODO: We only support TextureView preview rendering
         mSupportedPreviewSizes.addAll(Size.buildListFromAndroidSizes(Arrays.asList(
-                s.getOutputSizes(SurfaceHolder.class))));
+                s.getOutputSizes(SurfaceTexture.class))));
         for (int format : s.getOutputFormats()) {
             mSupportedPreviewFormats.add(format);
         }
 
-        // TODO: We only support MediaRecorder videos capture
+        // TODO: We only support MediaRecorder video capture
         mSupportedVideoSizes.addAll(Size.buildListFromAndroidSizes(Arrays.asList(
                 s.getOutputSizes(MediaRecorder.class))));
 
-        // TODO: We only support ImageReader image capture
+        // TODO: We only support JPEG image capture
         mSupportedPhotoSizes.addAll(Size.buildListFromAndroidSizes(Arrays.asList(
-                s.getOutputSizes(ImageReader.class))));
+                s.getOutputSizes(ImageFormat.JPEG))));
         mSupportedPhotoFormats.addAll(mSupportedPreviewFormats);
 
         buildSceneModes(p);
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java
index 288ded7..efa68e8 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java
@@ -84,13 +84,21 @@
         // TODO: mCurrentZoomIndex
         mExposureCompensationIndex =
                 queryTemplateDefaultOrMakeOneUp(CONTROL_AE_EXPOSURE_COMPENSATION, 0);
+
         mCurrentFlashMode = flashModeFromRequest();
-        mCurrentFocusMode = AndroidCamera2Capabilities.focusModeFromInt(
-                mTemplateSettings.get(CONTROL_AF_MODE));
-        mCurrentSceneMode = AndroidCamera2Capabilities.sceneModeFromInt(
-                mTemplateSettings.get(CONTROL_SCENE_MODE));
-        mWhiteBalance = AndroidCamera2Capabilities.whiteBalanceFromInt(
-                mTemplateSettings.get(CONTROL_AWB_MODE));
+        Integer currentFocusMode = mTemplateSettings.get(CONTROL_AF_MODE);
+        if (currentFocusMode != null) {
+            mCurrentFocusMode = AndroidCamera2Capabilities.focusModeFromInt(currentFocusMode);
+        }
+        Integer currentSceneMode = mTemplateSettings.get(CONTROL_SCENE_MODE);
+        if (currentSceneMode != null) {
+            mCurrentSceneMode = AndroidCamera2Capabilities.sceneModeFromInt(currentSceneMode);
+        }
+        Integer whiteBalance = mTemplateSettings.get(CONTROL_AWB_MODE);
+        if (whiteBalance != null) {
+            mWhiteBalance = AndroidCamera2Capabilities.whiteBalanceFromInt(whiteBalance);
+        }
+
         mVideoStabilizationEnabled = queryTemplateDefaultOrMakeOneUp(
                         CONTROL_VIDEO_STABILIZATION_MODE, CONTROL_VIDEO_STABILIZATION_MODE_OFF) ==
                 CONTROL_VIDEO_STABILIZATION_MODE_ON;
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java
index 95f0320..c26a1a3 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java
@@ -46,8 +46,6 @@
 class AndroidCameraAgentImpl extends CameraAgent {
     private static final Log.Tag TAG = new Log.Tag("AndCamAgntImp");
 
-    private Parameters mParameters;
-    private boolean mParametersIsDirty;
     private CameraDeviceInfo.Characteristics mCharacteristics;
     private AndroidCameraCapabilities mCapabilities;
 
@@ -131,6 +129,7 @@
                     Camera.getCameraInfo(i, cameraInfos[i]);
                 }
             } catch (RuntimeException ex) {
+                Log.e(TAG, "Exception while creating CameraDeviceInfo", ex);
                 return null;
             }
 
@@ -204,15 +203,39 @@
         }
     }
 
+    private static class ParametersCache {
+        private Parameters mParameters;
+        private Camera mCamera;
+
+        public ParametersCache(Camera camera) {
+            mCamera = camera;
+        }
+
+        public synchronized void invalidate() {
+            mParameters = null;
+        }
+
+        /**
+         * Access parameters from the cache. If cache is empty, block by
+         * retrieving parameters directly from Camera, but if cache is present,
+         * returns immediately.
+         */
+        public synchronized Parameters getBlocking() {
+            if (mParameters == null) {
+                mParameters = mCamera.getParameters();
+            }
+            return mParameters;
+        }
+    }
+
     /**
      * The handler on which the actual camera operations happen.
      */
     private class CameraHandler extends HistoryHandler {
 
-        // Used to retain a copy of Parameters for setting parameters.
-        private Parameters mParamsToSet;
         private Camera mCamera;
         private int mCameraId;
+        private ParametersCache mParameterCache;
 
         private class CaptureCallbacks {
             public final ShutterCallback mShutter;
@@ -304,16 +327,16 @@
                             break;
                         }
 
+                        Log.i(TAG, "Opening camera " + cameraId + " with camera1 API");
                         mCamera = android.hardware.Camera.open(cameraId);
                         if (mCamera != null) {
                             mCameraId = cameraId;
-                            mParametersIsDirty = true;
+                            mParameterCache = new ParametersCache(mCamera);
 
-                            // Get an instance of Camera.Parameters for later use.
-                            mParamsToSet = mCamera.getParameters();
                             mCharacteristics =
                                     AndroidCameraDeviceInfo.create().getCharacteristics(cameraId);
-                            mCapabilities = new AndroidCameraCapabilities(mParamsToSet);
+                            mCapabilities = new AndroidCameraCapabilities(
+                                    mParameterCache.getBlocking());
 
                             mCameraState.setState(AndroidCameraStateHolder.CAMERA_IDLE);
                             if (openCallback != null) {
@@ -442,9 +465,10 @@
                                 mCharacteristics.getPreviewOrientation(msg.arg1));
                         // Only set the JPEG capture orientation if requested to do so; otherwise,
                         // capture in the sensor's physical orientation
-                        mParamsToSet.setRotation(
+                        Parameters parameters = mParameterCache.getBlocking();
+                        parameters.setRotation(
                                 msg.arg2 > 0 ? mCharacteristics.getJpegOrientation(msg.arg1) : 0);
-                        mCamera.setParameters(mParamsToSet);
+                        mCamera.setParameters(parameters);
                         break;
                     }
 
@@ -474,25 +498,26 @@
                     }
 
                     case CameraActions.APPLY_SETTINGS: {
-                        mParametersIsDirty = true;
+                        Parameters parameters = mParameterCache.getBlocking();
                         CameraSettings settings = (CameraSettings) msg.obj;
-                        applyToParameters(settings);
-                        mCamera.setParameters(mParamsToSet);
+                        applySettingsToParameters(settings, parameters);
+                        mCamera.setParameters(parameters);
+                        mParameterCache.invalidate();
                         break;
                     }
 
                     case CameraActions.SET_PARAMETERS: {
-                        mParametersIsDirty = true;
-                        mParamsToSet.unflatten((String) msg.obj);
-                        mCamera.setParameters(mParamsToSet);
+                        Parameters parameters = mParameterCache.getBlocking();
+                        parameters.unflatten((String) msg.obj);
+                        mCamera.setParameters(parameters);
+                        mParameterCache.invalidate();
                         break;
                     }
 
                     case CameraActions.GET_PARAMETERS: {
-                        if (mParametersIsDirty) {
-                            mParameters = mCamera.getParameters();
-                            mParametersIsDirty = false;
-                        }
+                        Parameters[] parametersHolder = (Parameters[]) msg.obj;
+                        Parameters parameters = mParameterCache.getBlocking();
+                        parametersHolder[0] = parameters;
                         break;
                     }
 
@@ -507,7 +532,7 @@
                     }
 
                     case CameraActions.REFRESH_PARAMETERS: {
-                        mParametersIsDirty = true;
+                        mParameterCache.invalidate();;
                         break;
                     }
 
@@ -555,69 +580,70 @@
             }
         }
 
-        private void applyToParameters(final CameraSettings settings) {
+        private void applySettingsToParameters(final CameraSettings settings,
+                final Parameters parameters) {
             final CameraCapabilities.Stringifier stringifier = mCapabilities.getStringifier();
             Size photoSize = settings.getCurrentPhotoSize();
-            mParamsToSet.setPictureSize(photoSize.width(), photoSize.height());
+            parameters.setPictureSize(photoSize.width(), photoSize.height());
             Size previewSize = settings.getCurrentPreviewSize();
-            mParamsToSet.setPreviewSize(previewSize.width(), previewSize.height());
+            parameters.setPreviewSize(previewSize.width(), previewSize.height());
             if (settings.getPreviewFrameRate() == -1) {
-                mParamsToSet.setPreviewFpsRange(settings.getPreviewFpsRangeMin(),
+                parameters.setPreviewFpsRange(settings.getPreviewFpsRangeMin(),
                         settings.getPreviewFpsRangeMax());
             } else {
-                mParamsToSet.setPreviewFrameRate(settings.getPreviewFrameRate());
+                parameters.setPreviewFrameRate(settings.getPreviewFrameRate());
             }
-            mParamsToSet.setPreviewFormat(settings.getCurrentPreviewFormat());
-            mParamsToSet.setJpegQuality(settings.getPhotoJpegCompressionQuality());
+            parameters.setPreviewFormat(settings.getCurrentPreviewFormat());
+            parameters.setJpegQuality(settings.getPhotoJpegCompressionQuality());
             if (mCapabilities.supports(CameraCapabilities.Feature.ZOOM)) {
                 // Should use settings.getCurrentZoomRatio() instead here.
-                mParamsToSet.setZoom(settings.getCurrentZoomIndex());
+                parameters.setZoom(settings.getCurrentZoomIndex());
             }
-            mParamsToSet.setExposureCompensation(settings.getExposureCompensationIndex());
+            parameters.setExposureCompensation(settings.getExposureCompensationIndex());
             if (mCapabilities.supports(CameraCapabilities.Feature.AUTO_EXPOSURE_LOCK)) {
-                mParamsToSet.setAutoExposureLock(settings.isAutoExposureLocked());
+                parameters.setAutoExposureLock(settings.isAutoExposureLocked());
             }
-            mParamsToSet.setFocusMode(stringifier.stringify(settings.getCurrentFocusMode()));
+            parameters.setFocusMode(stringifier.stringify(settings.getCurrentFocusMode()));
             if (mCapabilities.supports(CameraCapabilities.Feature.AUTO_WHITE_BALANCE_LOCK)) {
-                mParamsToSet.setAutoWhiteBalanceLock(settings.isAutoWhiteBalanceLocked());
+                parameters.setAutoWhiteBalanceLock(settings.isAutoWhiteBalanceLocked());
             }
             if (mCapabilities.supports(CameraCapabilities.Feature.FOCUS_AREA)) {
                 if (settings.getFocusAreas().size() != 0) {
-                    mParamsToSet.setFocusAreas(settings.getFocusAreas());
+                    parameters.setFocusAreas(settings.getFocusAreas());
                 }
             }
             if (mCapabilities.supports(CameraCapabilities.Feature.METERING_AREA)) {
                 if (settings.getMeteringAreas().size() != 0) {
-                    mParamsToSet.setMeteringAreas(settings.getMeteringAreas());
+                    parameters.setMeteringAreas(settings.getMeteringAreas());
                 }
             }
             if (settings.getCurrentFlashMode() != CameraCapabilities.FlashMode.NO_FLASH) {
-                mParamsToSet.setFlashMode(stringifier.stringify(settings.getCurrentFlashMode()));
+                parameters.setFlashMode(stringifier.stringify(settings.getCurrentFlashMode()));
             }
             if (settings.getCurrentSceneMode() != CameraCapabilities.SceneMode.NO_SCENE_MODE) {
                 if (settings.getCurrentSceneMode() != null) {
-                    mParamsToSet
+                    parameters
                             .setSceneMode(stringifier.stringify(settings.getCurrentSceneMode()));
                 }
             }
-            mParamsToSet.setRecordingHint(settings.isRecordingHintEnabled());
+            parameters.setRecordingHint(settings.isRecordingHintEnabled());
             Size jpegThumbSize = settings.getExifThumbnailSize();
-            mParamsToSet.setJpegThumbnailSize(jpegThumbSize.width(), jpegThumbSize.height());
-            mParamsToSet.setPictureFormat(settings.getCurrentPhotoFormat());
+            parameters.setJpegThumbnailSize(jpegThumbSize.width(), jpegThumbSize.height());
+            parameters.setPictureFormat(settings.getCurrentPhotoFormat());
 
             CameraSettings.GpsData gpsData = settings.getGpsData();
             if (gpsData == null) {
-                mParamsToSet.removeGpsData();
+                parameters.removeGpsData();
             } else {
-                mParamsToSet.setGpsTimestamp(gpsData.timeStamp);
+                parameters.setGpsTimestamp(gpsData.timeStamp);
                 if (gpsData.processingMethod != null) {
                     // It's a hack since we always use GPS time stamp but does
                     // not use other fields sometimes. Setting processing
                     // method to null means the other fields should not be used.
-                    mParamsToSet.setGpsAltitude(gpsData.altitude);
-                    mParamsToSet.setGpsLatitude(gpsData.latitude);
-                    mParamsToSet.setGpsLongitude(gpsData.longitude);
-                    mParamsToSet.setGpsProcessingMethod(gpsData.processingMethod);
+                    parameters.setGpsAltitude(gpsData.altitude);
+                    parameters.setGpsLatitude(gpsData.latitude);
+                    parameters.setGpsLongitude(gpsData.longitude);
+                    parameters.setGpsProcessingMethod(gpsData.processingMethod);
                 }
             }
 
@@ -644,6 +670,7 @@
             mCapabilities = capabilities;
         }
 
+        @Deprecated
         @Override
         public android.hardware.Camera getCamera() {
             return mCamera;
@@ -826,6 +853,7 @@
             });
         }
 
+        @Deprecated
         @Override
         public void setParameters(final Parameters params) {
             if (params == null) {
@@ -844,17 +872,21 @@
             });
         }
 
+        @Deprecated
         @Override
         public Parameters getParameters() {
             final WaitDoneBundle bundle = new WaitDoneBundle();
+            final Parameters[] parametersHolder = new Parameters[1];
             mDispatchThread.runJobSync(new Runnable() {
                 @Override
                 public void run() {
-                    mCameraHandler.sendEmptyMessage(CameraActions.GET_PARAMETERS);
+                    Message getParametersMessage = mCameraHandler.obtainMessage(
+                            CameraActions.GET_PARAMETERS, parametersHolder);
+                    mCameraHandler.sendMessage(getParametersMessage);
                     mCameraHandler.post(bundle.mUnlockRunnable);
                 }
             }, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "get parameters");
-            return mParameters;
+            return parametersHolder[0];
         }
 
         @Override
@@ -870,14 +902,19 @@
 
         @Override
         public String dumpDeviceSettings() {
-            String flattened = mParameters.flatten();
-            StringTokenizer tokenizer = new StringTokenizer(flattened, ";");
-            String dumpedSettings = new String();
-            while (tokenizer.hasMoreElements()) {
-                dumpedSettings += tokenizer.nextToken() + '\n';
-            }
+            Parameters parameters = getParameters();
+            if (parameters != null) {
+                String flattened = getParameters().flatten();
+                StringTokenizer tokenizer = new StringTokenizer(flattened, ";");
+                String dumpedSettings = new String();
+                while (tokenizer.hasMoreElements()) {
+                    dumpedSettings += tokenizer.nextToken() + '\n';
+                }
 
-            return dumpedSettings;
+                return dumpedSettings;
+            } else {
+                return "[no parameters retrieved]";
+            }
         }
 
         @Override
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraAgentFactory.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraAgentFactory.java
index 00dc280..77ad0c3 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/CameraAgentFactory.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraAgentFactory.java
@@ -17,42 +17,150 @@
 package com.android.ex.camera2.portability;
 
 import android.content.Context;
+import android.os.Build;
+
+import com.android.ex.camera2.portability.debug.Log;
+import com.android.ex.camera2.portability.util.SystemProperties;
 
 /**
  * A factory class for {@link CameraAgent}.
+ *
+ * <p>The choice of framework API to use can be made automatically based on the
+ * system API level, explicitly forced by the client app, or overridden entirely
+ * by setting the system property com.camera2.portability.fwk_api to 1 or 2.</p>
  */
 public class CameraAgentFactory {
+    private static final Log.Tag TAG = new Log.Tag("CamAgntFact");
+
+    /** Android release replacing the Camera class with the camera2 package. */
+    private static final int FIRST_SDK_WITH_API_2 = 21;
+
+    // The debugging override, which overrides *all* API level selections if set
+    // to API_LEVEL_OVERRIDE_API{1,2}; otherwise, this has no effect. Note that
+    // we check this once when the library is first loaded so that #recycle()
+    // doesn't try to clean up the wrong type of CameraAgent.
+    private static final String API_LEVEL_OVERRIDE_KEY = "camera2.portability.force_api";
+    private static final String API_LEVEL_OVERRIDE_DEFAULT = "0";
+    private static final String API_LEVEL_OVERRIDE_API1 = "1";
+    private static final String API_LEVEL_OVERRIDE_API2 = "2";
+    private static final String API_LEVEL_OVERRIDE_VALUE =
+            SystemProperties.get(API_LEVEL_OVERRIDE_KEY, API_LEVEL_OVERRIDE_DEFAULT);
 
     private static CameraAgent sAndroidCameraAgent;
+    private static CameraAgent sAndroidCamera2Agent;
     private static int sAndroidCameraAgentClientCount;
+    private static int sAndroidCamera2AgentClientCount;
 
     /**
-     * Returns the android camera implementation of {@link com.android.camera.cameradevice.CameraAgent}.
-     *
-     * @return The {@link CameraAgent} to control the camera device.
+     * Used to indicate which camera framework should be used.
      */
-    public static synchronized CameraAgent getAndroidCameraAgent(Context context) {
-        if (sAndroidCameraAgent == null) {
-            if (false) {
-                sAndroidCameraAgent = new AndroidCamera2AgentImpl(context);
-            } else {
-                sAndroidCameraAgent = new AndroidCameraAgentImpl();
-            }
-            sAndroidCameraAgentClientCount = 1;
+    public static enum CameraApi {
+        /** Automatically select based on the device's SDK level. */
+        AUTO,
+
+        /** Use the {@link android.hardware.Camera} class. */
+        API_1,
+
+        /** Use the {@link android.hardware.camera2} package. */
+        API_2
+    };
+
+    private static CameraApi highestSupportedApi() {
+        // TODO: Check SDK_INT instead of RELEASE before L launch
+        if (Build.VERSION.SDK_INT >= FIRST_SDK_WITH_API_2 || Build.VERSION.CODENAME.equals("L")) {
+            return CameraApi.API_2;
         } else {
-            ++sAndroidCameraAgentClientCount;
+            return CameraApi.API_1;
         }
-        return sAndroidCameraAgent;
+    }
+
+    private static CameraApi validateApiChoice(CameraApi choice) {
+        if (API_LEVEL_OVERRIDE_VALUE.equals(API_LEVEL_OVERRIDE_API1)) {
+            Log.d(TAG, "API level overridden by system property: forced to 1");
+            return CameraApi.API_1;
+        } else if (API_LEVEL_OVERRIDE_VALUE.equals(API_LEVEL_OVERRIDE_API2)) {
+            Log.d(TAG, "API level overridden by system property: forced to 2");
+            return CameraApi.API_2;
+        }
+
+        if (choice == null) {
+            Log.w(TAG, "null API level request, so assuming AUTO");
+            choice = CameraApi.AUTO;
+        }
+        if (choice == CameraApi.AUTO) {
+            choice = highestSupportedApi();
+        }
+
+        return choice;
+    }
+
+    /**
+     * Returns the android camera implementation of
+     * {@link com.android.camera.cameradevice.CameraAgent}.
+     *
+     * <p>To clean up the resources allocated by this call, be sure to invoke
+     * {@link #recycle(boolean)} with the same {@code api} value provided
+     * here.</p>
+     *
+     * @param context The application context.
+     * @param api Which camera framework to use.
+     * @return The {@link CameraAgent} to control the camera device.
+     *
+     * @throws UnsupportedOperationException If {@code CameraApi.API_2} was
+     *                                       requested on an unsupported device.
+     */
+    public static synchronized CameraAgent getAndroidCameraAgent(Context context, CameraApi api) {
+        api = validateApiChoice(api);
+
+        if (api == CameraApi.API_1) {
+            if (sAndroidCameraAgent == null) {
+                sAndroidCameraAgent = new AndroidCameraAgentImpl();
+                sAndroidCameraAgentClientCount = 1;
+            } else {
+                ++sAndroidCameraAgentClientCount;
+            }
+            return sAndroidCameraAgent;
+        } else { // API_2
+            if (highestSupportedApi() == CameraApi.API_1) {
+                throw new UnsupportedOperationException("Camera API_2 unavailable on this device");
+            }
+
+            if (sAndroidCamera2Agent == null) {
+                sAndroidCamera2Agent = new AndroidCamera2AgentImpl(context);
+                sAndroidCamera2AgentClientCount = 1;
+            } else {
+                ++sAndroidCamera2AgentClientCount;
+            }
+            return sAndroidCamera2Agent;
+        }
     }
 
     /**
      * Recycles the resources. Always call this method when the activity is
      * stopped.
+     *
+     * @param api Which camera framework handle to recycle.
+     *
+     * @throws UnsupportedOperationException If {@code CameraApi.API_2} was
+     *                                       requested on an unsupported device.
      */
-    public static synchronized void recycle() {
-        if (--sAndroidCameraAgentClientCount == 0 && sAndroidCameraAgent != null) {
-            sAndroidCameraAgent.recycle();
-            sAndroidCameraAgent = null;
+    public static synchronized void recycle(CameraApi api) {
+        api = validateApiChoice(api);
+
+        if (api == CameraApi.API_1) {
+            if (--sAndroidCameraAgentClientCount == 0 && sAndroidCameraAgent != null) {
+                sAndroidCameraAgent.recycle();
+                sAndroidCameraAgent = null;
+            }
+        } else { // API_2
+            if (highestSupportedApi() == CameraApi.API_1) {
+                throw new UnsupportedOperationException("Camera API_2 unavailable on this device");
+            }
+
+            if (--sAndroidCamera2AgentClientCount == 0 && sAndroidCamera2Agent != null) {
+                sAndroidCamera2Agent.recycle();
+                sAndroidCamera2Agent = null;
+            }
         }
     }
 }
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraDeviceInfo.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraDeviceInfo.java
index a657170..72a641e 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/CameraDeviceInfo.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraDeviceInfo.java
@@ -16,7 +16,8 @@
 
 package com.android.ex.camera2.portability;
 
-import android.hardware.Camera;
+import android.graphics.Matrix;
+import android.graphics.RectF;
 
 import com.android.ex.camera2.portability.debug.Log;
 
@@ -67,17 +68,18 @@
         public abstract boolean isFacingFront();
 
         /**
-         * @return The camera image orientation, or the clockwise rotation angle
-         *         that must be applied to display it in its natural orientation
-         *         (in degrees, always a multiple of 90, and between [90,270]).
+         * @return The camera sensor orientation, or the counterclockwise angle
+         *          from its natural position that the device must be held at
+         *          for the sensor to be right side up (in degrees, always a
+         *          multiple of 90, and between 0 and 270, inclusive).
          */
         public abstract int getSensorOrientation();
 
         /**
          * @param currentDisplayOrientation
-         *          The current display orientation, as measured clockwise from
-         *          the device's natural orientation (in degrees, always a
-         *          multiple of 90, and between 0 and 270, inclusive).
+         *          The current display orientation, measured counterclockwise
+         *          from to the device's natural orientation (in degrees, always
+         *          a multiple of 90, and between 0 and 270, inclusive).
          * @return
          *          The relative preview image orientation, or the clockwise
          *          rotation angle that must be applied to display preview
@@ -92,9 +94,9 @@
 
         /**
          * @param currentDisplayOrientation
-         *          The current display orientation, as measured clockwise from
-         *          the device's natural orientation (in degrees, always a
-         *          multiple of 90, and between 0 and 270, inclusive).
+         *          The current display orientation, measured counterclockwise
+         *          from to the device's natural orientation (in degrees, always
+         *          a multiple of 90, and between 0 and 270, inclusive).
          * @return
          *          The relative capture image orientation, or the clockwise
          *          rotation angle that must be applied to display these frames
@@ -120,11 +122,8 @@
          */
         protected int getRelativeImageOrientation(int currentDisplayOrientation,
                                                   boolean compensateForMirroring) {
-            if (currentDisplayOrientation % 90 != 0) {
-                Log.e(TAG, "Provided display orientation is not divisible by 90");
-            }
-            if (currentDisplayOrientation < 0 || currentDisplayOrientation > 270) {
-                Log.e(TAG, "Provided display orientation is outside expected range");
+            if (!orientationIsValid(currentDisplayOrientation)) {
+                return 0;
             }
 
             int result = 0;
@@ -142,8 +141,70 @@
         }
 
         /**
+         * @param currentDisplayOrientation
+         *          The current display orientation, measured counterclockwise
+         *          from to the device's natural orientation (in degrees, always
+         *          a multiple of 90, and between 0 and 270, inclusive).
+         * @param surfaceDimensions
+         *          The dimensions of the {@link android.view.Surface} on which
+         *          the preview image is being rendered. It usually only makes
+         *          sense for the upper-left corner to be at the origin.
+         * @return
+         *          The transform matrix that should be applied to the
+         *          {@link android.view.Surface} in order for the image to
+         *          display properly in the device's current orientation.
+         */
+        public Matrix getPreviewTransform(int currentDisplayOrientation, RectF surfaceDimensions) {
+            return getPreviewTransform(currentDisplayOrientation, surfaceDimensions,
+                    new RectF(surfaceDimensions));
+        }
+
+        /**
+         * @param currentDisplayOrientation
+         *          The current display orientation, measured counterclockwise
+         *          from to the device's natural orientation (in degrees, always
+         *          a multiple of 90, and between 0 and 270, inclusive).
+         * @param surfaceDimensions
+         *          The dimensions of the {@link android.view.Surface} on which
+         *          the preview image is being rendered. It usually only makes
+         *          sense for the upper-left corner to be at the origin.
+         * @param desiredBounds
+         *          The boundaries within the {@link android.view.Surface} where
+         *          the final image should appear. These can be used to
+         *          translate and scale the output, but note that the image will
+         *          be stretched to fit, possibly changing its aspect ratio.
+         * @return
+         *          The transform matrix that should be applied to the
+         *          {@link android.view.Surface} in order for the image to
+         *          display properly in the device's current orientation.
+         */
+        public Matrix getPreviewTransform(int currentDisplayOrientation, RectF surfaceDimensions,
+                                          RectF desiredBounds) {
+            if (!orientationIsValid(currentDisplayOrientation) ||
+                    surfaceDimensions.equals(desiredBounds)) {
+                return new Matrix();
+            }
+
+            Matrix transform = new Matrix();
+            transform.setRectToRect(surfaceDimensions, desiredBounds, Matrix.ScaleToFit.FILL);
+            return transform;
+        }
+
+        /**
          * @return Whether the shutter sound can be disabled.
          */
         public abstract boolean canDisableShutterSound();
+
+        protected static boolean orientationIsValid(int angle) {
+            if (angle % 90 != 0) {
+                Log.e(TAG, "Provided display orientation is not divisible by 90");
+                return false;
+            }
+            if (angle < 0 || angle > 270) {
+                Log.e(TAG, "Provided display orientation is outside expected range");
+                return false;
+            }
+            return true;
+        }
     }
 }
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/util/SystemProperties.java b/camera2/portability/src/com/android/ex/camera2/portability/util/SystemProperties.java
new file mode 100644
index 0000000..81493f3
--- /dev/null
+++ b/camera2/portability/src/com/android/ex/camera2/portability/util/SystemProperties.java
@@ -0,0 +1,51 @@
+/*
+ * 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.ex.camera2.portability.util;
+
+import com.android.ex.camera2.portability.debug.Log;
+
+import java.lang.reflect.Method;
+
+/**
+ * Mirrors hidden class {@link android.os.SystemProperties} (available since API Level 1).
+ */
+public final class SystemProperties {
+    private static final Log.Tag TAG = new Log.Tag("SysProps");
+
+    /**
+     * Gets system properties set by <code>adb shell setprop <em>key</em> <em>value</em></code>
+     *
+     * @param key the property key.
+     * @param defaultValue the value to return if the property is undefined or empty (this parameter
+     *            may be {@code null}).
+     * @return the system property value or the default value.
+     */
+    public static String get(String key, String defaultValue) {
+        try {
+            final Class<?> systemProperties = Class.forName("android.os.SystemProperties");
+            final Method get = systemProperties.getMethod("get", String.class, String.class);
+            return (String) get.invoke(null, key, defaultValue);
+        } catch (Exception e) {
+            // This should never happen
+            Log.e(TAG, "Exception while getting system property: ", e);
+            return defaultValue;
+        }
+    }
+
+    private SystemProperties() {
+    }
+}
diff --git a/common/java/com/android/common/widget/CompositeCursorAdapter.java b/common/java/com/android/common/widget/CompositeCursorAdapter.java
index 114065f..dddbcf6 100644
--- a/common/java/com/android/common/widget/CompositeCursorAdapter.java
+++ b/common/java/com/android/common/widget/CompositeCursorAdapter.java
@@ -54,6 +54,10 @@
         public boolean getHasHeader() {
             return hasHeader;
         }
+
+        public boolean isEmpty() {
+            return count == 0;
+        }
     }
 
     private final Context mContext;
@@ -110,7 +114,7 @@
     /**
      * Removes cursors for all partitions.
      */
-    // TODO: Is this really what this is supposed to do? Just remove the cursors? Not close them? 
+    // TODO: Is this really what this is supposed to do? Just remove the cursors? Not close them?
     // Not remove the partitions themselves? Isn't this leaking?
 
     public void clearPartitions() {