TestingCamera: Add preview callbacks

- Support for NV21, YV12, YUY2 preview formats
- Switch to API level 17 for certain Renderscript features
- Fix assorted SDK warnings/lint issues.

Change-Id: I3a5063065e239308df74c1077c4120429f431ab7
diff --git a/apps/TestingCamera/Android.mk b/apps/TestingCamera/Android.mk
index d7e00d1..cfe77d4 100644
--- a/apps/TestingCamera/Android.mk
+++ b/apps/TestingCamera/Android.mk
@@ -20,7 +20,11 @@
 
 LOCAL_MODULE_TAGS := optional
 
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SDK_VERSION := 17
+
+LOCAL_SRC_FILES := \
+	$(call all-java-files-under, src) \
+	$(call all-renderscript-files-under, src)
 
 LOCAL_PACKAGE_NAME := TestingCamera
 
diff --git a/apps/TestingCamera/AndroidManifest.xml b/apps/TestingCamera/AndroidManifest.xml
index cfed74b..36c21ca 100644
--- a/apps/TestingCamera/AndroidManifest.xml
+++ b/apps/TestingCamera/AndroidManifest.xml
@@ -22,7 +22,7 @@
   <uses-feature android:name="android.hardware.camera" />
   <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
 
-  <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="16"/>
+  <uses-sdk android:minSdkVersion="17" android:targetSdkVersion="17"/>
   <uses-feature android:name="android.hardware.camera.front" android:required="false"/>
   <uses-permission android:name="android.permission.RECORD_AUDIO"/>
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
diff --git a/apps/TestingCamera/project.properties b/apps/TestingCamera/project.properties
index 9b84a6b..a3ee5ab 100644
--- a/apps/TestingCamera/project.properties
+++ b/apps/TestingCamera/project.properties
@@ -11,4 +11,4 @@
 #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
 
 # Project target.
-target=android-16
+target=android-17
diff --git a/apps/TestingCamera/res/layout/main.xml b/apps/TestingCamera/res/layout/main.xml
index cd61805..43f4cc7 100644
--- a/apps/TestingCamera/res/layout/main.xml
+++ b/apps/TestingCamera/res/layout/main.xml
@@ -25,21 +25,33 @@
         android:layout_width="0dp"
         android:layout_height="fill_parent"
         android:layout_weight="6"
-        android:orientation="vertical" >
+        android:animateLayoutChanges="false"
+        android:orientation="vertical"
+        android:visibility="visible" >
 
         <SurfaceView
             android:id="@+id/preview"
             android:layout_width="fill_parent"
             android:layout_height="0dp"
-            android:layout_weight="6"
+            android:layout_weight="@integer/preview_only_weight"
             tools:ignore="NestedWeights" />
 
+        <SurfaceView
+            android:id="@+id/callback_view"
+            android:layout_width="fill_parent"
+            android:layout_height="0dp"
+            android:layout_weight="@integer/preview_with_callback_weight"
+            android:visibility="gone" />
+
         <TextView
             android:id="@+id/log"
             android:layout_width="fill_parent"
-            android:layout_height="0dp"
-            android:layout_weight="1.5"
-            android:freezesText="true" />
+            android:layout_height="10dp"
+            android:layout_weight="1"
+            android:freezesText="true"
+            android:minLines="3"
+            android:typeface="normal" />
+
     </LinearLayout>
 
     <ScrollView
@@ -198,11 +210,11 @@
                 android:background="@color/horiz_rule_color" />
 
             <TextView
-                android:id="@+id/textView1"
+                android:id="@+id/snapshot_size_spinner_label"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_weight="1"
-                android:text="@string/snapshot_size_spinner_label"
+                android:text="@string/snapshot_size_prompt"
                 android:textAppearance="?android:attr/textAppearanceSmall" />
 
             <Spinner
@@ -287,6 +299,38 @@
                 android:textColorLink="@android:color/holo_blue_dark"
                 android:textOff="@string/record_stabilization_off_label"
                 android:textOn="@string/record_stabilization_on_label" />
+
+            <View
+                android:id="@+id/horizontal_rule_5"
+                android:layout_width="fill_parent"
+                android:layout_height="1dip"
+                android:layout_marginBottom="@dimen/horiz_rule_btm_margin"
+                android:layout_marginTop="@dimen/horiz_rule_top_margin"
+                android:layout_weight="1"
+                android:background="@color/horiz_rule_color" />
+
+            <TextView
+                android:id="@+id/callback_format_spinner_label"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:text="@string/callback_format_prompt"
+                android:textAppearance="?android:attr/textAppearanceSmall" />
+
+            <Spinner
+                android:id="@+id/callback_format_spinner"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_weight="1" />
+
+            <ToggleButton
+                android:id="@+id/enable_callbacks"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:textOff="@string/callbacks_off_label"
+                android:textOn="@string/callbacks_on_label" />
+
         </LinearLayout>
     </ScrollView>
 
diff --git a/apps/TestingCamera/res/values/dimens.xml b/apps/TestingCamera/res/values/dimens.xml
index dbea0fa..e996a32 100644
--- a/apps/TestingCamera/res/values/dimens.xml
+++ b/apps/TestingCamera/res/values/dimens.xml
@@ -2,4 +2,6 @@
 <resources>
     <dimen name="horiz_rule_top_margin">8dp</dimen>
     <dimen name="horiz_rule_btm_margin">4dp</dimen>
+    <item format="integer" name="preview_only_weight" type="integer">6</item>
+    <item format="integer" name="preview_with_callback_weight" type="integer">3</item>
 </resources>
diff --git a/apps/TestingCamera/res/values/strings.xml b/apps/TestingCamera/res/values/strings.xml
index 2225152..b8e1b85 100644
--- a/apps/TestingCamera/res/values/strings.xml
+++ b/apps/TestingCamera/res/values/strings.xml
@@ -25,7 +25,7 @@
     <string name="camera_selection_prompt">Active camera</string>
     <string name="default_camera_entry">No cameras found</string>
     <string name="snapshot_text_default">Save to view EXIF.</string>
-    <string name="snapshot_size_spinner_label">Still capture size</string>
+    <string name="snapshot_size_prompt">Still capture size</string>
     <string name="record_on_label">Recording on</string>
     <string name="record_off_label">Recording off</string>
     <string name="record_stabilization_on_label">Video Stabilization on</string>
@@ -47,4 +47,7 @@
     <string name="exposure_label">Exposure</string>
     <string name="exposure_lock_on_label">Auto-Exposure Locked</string>
     <string name="exposure_lock_off_label">Auto-Exposure Unlocked</string>
+    <string name="callback_format_prompt">Preview callback format</string>
+    <string name="callbacks_on_label">Callbacks on</string>
+    <string name="callbacks_off_label">Callbacks off</string>
 </resources>
diff --git a/apps/TestingCamera/src/com/android/testingcamera/CallbackProcessor.java b/apps/TestingCamera/src/com/android/testingcamera/CallbackProcessor.java
new file mode 100644
index 0000000..0dfd1e3
--- /dev/null
+++ b/apps/TestingCamera/src/com/android/testingcamera/CallbackProcessor.java
@@ -0,0 +1,177 @@
+package com.android.testingcamera;
+
+import android.content.res.Resources;
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.os.AsyncTask;
+import android.os.SystemClock;
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.Matrix4f;
+import android.renderscript.RenderScript;
+import android.renderscript.Script;
+import android.renderscript.ScriptGroup;
+import android.renderscript.ScriptIntrinsicColorMatrix;
+import android.renderscript.Type;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceView;
+
+/**
+ *  Process preview callback data for display.
+ *  This is done by constructing a two-step Renderscript group,
+ *  the first of which converts from various YUV formats to 8bpp YUV, and
+ *  the second of which converts from YUV to RGB.
+ *
+ *  The processing is done in a background thread, and the result is produced
+ *  into an Allocation that's backed by a SurfaceView
+ */
+class CallbackProcessor {
+    private SurfaceView mCallbackView;
+    private Surface mCallbackSurface;
+
+    private Object mTaskLock = new Object();
+
+    private RenderScript mRS;
+    private Allocation mAllocationIn;
+    private Allocation mAllocationOut;
+    private ScriptGroup mConverter;
+
+    private int mWidth;
+    private int mHeight;
+    private int mFormat;
+
+    private boolean mDone = false;
+    private boolean mTaskInProgress = false;
+
+     /**
+      * JFIF standard YCbCr <-> RGB conversion matrix,
+      * column-major order.
+      */
+    static final private float[] kJpegYuv2Rgb = new float[] {
+            1.f,     1.f,      1.f,     0.f,
+            0.f,    -0.34414f, 1.772f,  0.f,
+            1.402f, -0.71414f, 0.f,     0.f,
+           -0.701f,  0.529f,  -0.886f, 1.0f
+    };
+
+    static final private int kStopTimeout = 2000; // ms
+
+    private static final String TAG = "CallbackProcessor";
+
+    public CallbackProcessor(int width, int height, int format,
+            Resources res, SurfaceView callbackView,
+            int viewWidth, int viewHeight,
+            RenderScript rs) {
+        mWidth = width;
+        mHeight = height;
+        mFormat = format;
+        mRS = rs;
+        mCallbackView = callbackView;
+
+        int inputSize = TestingCamera.getCallbackBufferSize(mWidth, mHeight,
+                mFormat);
+        mAllocationIn = Allocation.createSized(mRS, Element.U8(mRS), inputSize);
+
+        Type.Builder tb = new Type.Builder(mRS, Element.RGBA_8888(mRS));
+        tb.setX(viewWidth);
+        tb.setY(viewHeight);
+        Type outType = tb.create();
+
+        mAllocationOut = Allocation.createTyped(mRS, outType,
+                Allocation.USAGE_IO_OUTPUT | Allocation.USAGE_SCRIPT);
+
+        ScriptC_callback swizzleScript =
+                new ScriptC_callback(mRS, res, R.raw.callback);
+        swizzleScript.bind_yuv_in(mAllocationIn);
+        swizzleScript.invoke_init_convert(mWidth, mHeight,
+            mFormat, viewWidth, viewHeight);
+        Script.KernelID swizzleId;
+        switch (mFormat) {
+        case ImageFormat.NV21:
+            swizzleId = swizzleScript.getKernelID_convert_semiplanar();
+            break;
+        case ImageFormat.YV12:
+            swizzleId = swizzleScript.getKernelID_convert_planar();
+            break;
+        case ImageFormat.YUY2:
+            swizzleId = swizzleScript.getKernelID_convert_interleaved();
+            break;
+        case ImageFormat.UNKNOWN:
+        default:
+            swizzleId = swizzleScript.getKernelID_convert_unknown();
+        }
+
+        ScriptIntrinsicColorMatrix colorMatrix =
+                ScriptIntrinsicColorMatrix.create(rs, Element.U8_4(mRS));
+
+        Matrix4f yuv2rgb = new Matrix4f(kJpegYuv2Rgb);
+        colorMatrix.setColorMatrix(yuv2rgb);
+
+        ScriptGroup.Builder b = new ScriptGroup.Builder(rs);
+        b.addKernel(swizzleId);
+        b.addKernel(colorMatrix.getKernelID());
+        b.addConnection(outType, swizzleId,
+                colorMatrix.getKernelID());
+        mConverter = b.create();
+
+        mConverter.setOutput(colorMatrix.getKernelID(), mAllocationOut);
+    }
+
+    public boolean stop() {
+        synchronized(mTaskLock) {
+            mDone = true;
+            long startTime = SystemClock.elapsedRealtime();
+            while (mTaskInProgress) {
+                try {
+                    mTaskLock.wait(kStopTimeout);
+                } catch (InterruptedException e) {
+                    // ignored, keep waiting
+                }
+                long endTime = SystemClock.elapsedRealtime();
+                if (endTime - startTime > kStopTimeout) {
+                    return false;
+                }
+            }
+        }
+        mAllocationOut.setSurface(null);
+        return true;
+    }
+
+    public void displayCallback(byte[] data) {
+        synchronized(mTaskLock) {
+            if (mTaskInProgress || mDone) return;
+            mTaskInProgress = true;
+        }
+        if (mCallbackSurface == null) {
+            mCallbackView.getHolder().setFormat(PixelFormat.RGBA_8888);
+            mCallbackSurface = mCallbackView.getHolder().getSurface();
+            if (mCallbackSurface == null) return;
+            mAllocationOut.setSurface(mCallbackSurface);
+        }
+        new ProcessCallbackTask().execute(data);
+    }
+
+    private class ProcessCallbackTask extends AsyncTask<byte[], Void, Boolean> {
+
+        @Override
+        protected Boolean doInBackground(byte[]... datas) {
+            byte[] data = datas[0];
+
+            mAllocationIn.copyFrom(data);
+            mConverter.execute();
+            mAllocationOut.ioSend();
+
+            synchronized(mTaskLock) {
+                mTaskInProgress = false;
+                mTaskLock.notify();
+            }
+            return true;
+        }
+
+        @Override
+        protected void onPostExecute(Boolean result) {
+        }
+    }
+
+}
diff --git a/apps/TestingCamera/src/com/android/testingcamera/InfoDialogFragment.java b/apps/TestingCamera/src/com/android/testingcamera/InfoDialogFragment.java
index da5496a..9499778 100644
--- a/apps/TestingCamera/src/com/android/testingcamera/InfoDialogFragment.java
+++ b/apps/TestingCamera/src/com/android/testingcamera/InfoDialogFragment.java
@@ -37,6 +37,7 @@
         return view;
     }
 
+    @Override
     public void onClick(View v) {
         this.dismiss();
     }
diff --git a/apps/TestingCamera/src/com/android/testingcamera/SnapshotDialogFragment.java b/apps/TestingCamera/src/com/android/testingcamera/SnapshotDialogFragment.java
index d22f751..6995320 100644
--- a/apps/TestingCamera/src/com/android/testingcamera/SnapshotDialogFragment.java
+++ b/apps/TestingCamera/src/com/android/testingcamera/SnapshotDialogFragment.java
@@ -22,7 +22,7 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
-public class SnapshotDialogFragment extends DialogFragment
+class SnapshotDialogFragment extends DialogFragment
                 implements OnScanCompletedListener{
 
     private ImageView mInfoImage;
@@ -81,6 +81,7 @@
                     return img;
                 }
 
+                @Override
                 protected void onPostExecute(Bitmap img) {
                     mInfoImage.setImageBitmap(img);
                 }
@@ -92,18 +93,21 @@
     }
 
     public OnClickListener mOkButtonListener = new OnClickListener() {
+        @Override
         public void onClick(View v) {
             dismiss();
         }
     };
 
     public OnClickListener mSaveButtonListener = new OnClickListener() {
+        @Override
         public void onClick(View v) {
             saveFile();
         }
     };
 
     public OnClickListener mSaveAndViewButtonListener = new OnClickListener() {
+        @Override
         public void onClick(View v) {
             saveFile();
             viewFile();
@@ -246,6 +250,7 @@
         }
     }
 
+    @Override
     public synchronized void onScanCompleted(String path, Uri uri) {
         mSavedUri = uri;
         if (mViewWhenReady) viewFile();
diff --git a/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java b/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java
index d24b3f3..a60b188 100644
--- a/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java
+++ b/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java
@@ -16,8 +16,11 @@
 
 package com.android.testingcamera;
 
+import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.FragmentManager;
+import android.content.res.Resources;
+import android.graphics.ImageFormat;
 import android.hardware.Camera;
 import android.hardware.Camera.Parameters;
 import android.media.CamcorderProfile;
@@ -27,6 +30,7 @@
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.view.View;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
@@ -35,13 +39,16 @@
 import android.widget.AdapterView.OnItemSelectedListener;
 import android.widget.ArrayAdapter;
 import android.widget.Button;
+import android.widget.LinearLayout;
 import android.widget.LinearLayout.LayoutParams;
 import android.widget.Spinner;
 import android.widget.TextView;
 import android.widget.ToggleButton;
+import android.renderscript.RenderScript;
 import android.text.Layout;
 import android.text.method.ScrollingMovementMethod;
 import android.util.Log;
+import android.util.SparseArray;
 
 import java.io.File;
 import java.io.IOException;
@@ -60,11 +67,16 @@
  * The goal of this application is to allow all camera API features to be
  * exercised, and all information provided by the API to be shown.
  */
-public class TestingCamera extends Activity implements SurfaceHolder.Callback {
+public class TestingCamera extends Activity
+    implements SurfaceHolder.Callback, Camera.PreviewCallback {
 
     /** UI elements */
     private SurfaceView mPreviewView;
     private SurfaceHolder mPreviewHolder;
+    private LinearLayout mPreviewColumn;
+
+    private SurfaceView mCallbackView;
+    private SurfaceHolder mCallbackHolder;
 
     private Spinner mCameraSpinner;
     private Button mInfoButton;
@@ -82,17 +94,23 @@
     private Spinner mVideoFrameRateSpinner;
     private ToggleButton mRecordToggle;
     private ToggleButton mRecordStabilizationToggle;
+    private Spinner mCallbackFormatSpinner;
+    private ToggleButton mCallbackToggle;
 
     private TextView mLogView;
 
     private Set<View> mPreviewOnlyControls = new HashSet<View>();
 
+    private SparseArray<String> mFormatNames;
+
     /** Camera state */
     private int mCameraId = 0;
     private Camera mCamera;
     private Camera.Parameters mParams;
     private List<Camera.Size> mPreviewSizes;
     private int mPreviewSize = 0;
+    private List<Integer> mPreviewFormats;
+    private int mPreviewFormat = 0;
     private List<String> mAfModes;
     private int mAfMode = 0;
     private List<String> mFlashModes;
@@ -109,6 +127,18 @@
     private MediaRecorder mRecorder;
     private File mRecordingFile;
 
+    private RenderScript mRS;
+
+    private boolean mCallbacksEnabled = false;
+    private CallbackProcessor mCallbackProcessor = null;
+    long mLastCallbackTimestamp = -1;
+    float mCallbackAvgFrameDuration = 30;
+    int mCallbackFrameCount = 0;
+    private static final float MEAN_FPS_HISTORY_COEFF = 0.9f;
+    private static final float MEAN_FPS_MEASUREMENT_COEFF = 0.1f;
+    private static final int   FPS_REPORTING_PERIOD = 200; // frames
+    private static final int CALLBACK_BUFFER_COUNT = 3;
+
     private static final int CAMERA_UNINITIALIZED = 0;
     private static final int CAMERA_OPEN = 1;
     private static final int CAMERA_PREVIEW = 2;
@@ -116,6 +146,8 @@
     private static final int CAMERA_RECORD = 4;
     private int mState = CAMERA_UNINITIALIZED;
 
+
+
     /** Misc variables */
 
     private static final String TAG = "TestingCamera";
@@ -128,9 +160,13 @@
 
         setContentView(R.layout.main);
 
-        mPreviewView = (SurfaceView)findViewById(R.id.preview);
+        mPreviewColumn = (LinearLayout) findViewById(R.id.preview_column);
+
+        mPreviewView = (SurfaceView) findViewById(R.id.preview);
         mPreviewView.getHolder().addCallback(this);
 
+        mCallbackView = (SurfaceView)findViewById(R.id.callback_view);
+
         mCameraSpinner = (Spinner) findViewById(R.id.camera_spinner);
         mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener);
 
@@ -183,9 +219,24 @@
         mRecordStabilizationToggle = (ToggleButton) findViewById(R.id.record_stabilization);
         mRecordStabilizationToggle.setOnClickListener(mRecordStabilizationToggleListener);
 
+        mCallbackFormatSpinner = (Spinner) findViewById(R.id.callback_format_spinner);
+        mCallbackFormatSpinner.setOnItemSelectedListener(mCallbackFormatListener);
+
+        mCallbackToggle = (ToggleButton) findViewById(R.id.enable_callbacks);
+        mCallbackToggle.setOnClickListener(mCallbackToggleListener);
+
         mLogView = (TextView) findViewById(R.id.log);
         mLogView.setMovementMethod(new ScrollingMovementMethod());
 
+        mFormatNames = new SparseArray<String>(7);
+        mFormatNames.append(ImageFormat.JPEG, "JPEG");
+        mFormatNames.append(ImageFormat.NV16, "NV16");
+        mFormatNames.append(ImageFormat.NV21, "NV21");
+        mFormatNames.append(ImageFormat.RGB_565, "RGB_565");
+        mFormatNames.append(ImageFormat.UNKNOWN, "UNKNOWN");
+        mFormatNames.append(ImageFormat.YUY2, "YUY2");
+        mFormatNames.append(ImageFormat.YV12, "YV12");
+
         int numCameras = Camera.getNumberOfCameras();
         String[] cameraNames = new String[numCameras];
         for (int i = 0; i < numCameras; i++) {
@@ -195,6 +246,8 @@
         mCameraSpinner.setAdapter(
                 new ArrayAdapter<String>(this,
                         R.layout.spinner_item, cameraNames));
+
+        mRS = RenderScript.create(this);
     }
 
     @Override
@@ -214,25 +267,53 @@
     }
 
     /** SurfaceHolder.Callback methods */
+    @Override
     public void surfaceChanged(SurfaceHolder holder,
             int format,
             int width,
             int height) {
-        if (mPreviewHolder != null) return;
+        if (holder == mPreviewView.getHolder()) {
+            if (mState >= CAMERA_OPEN) {
+                final int previewWidth =
+                        mPreviewSizes.get(mPreviewSize).width;
+                final int previewHeight =
+                        mPreviewSizes.get(mPreviewSize).height;
 
-        log("Surface holder available: " + width + " x " + height);
-        mPreviewHolder = holder;
-        try {
-            mCamera.setPreviewDisplay(holder);
-        } catch (IOException e) {
-            logE("Unable to set up preview!");
+                if ( Math.abs((float)previewWidth / previewHeight -
+                        (float)width/height) > 0.01f) {
+                    Handler h = new Handler();
+                    h.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            layoutPreview();
+                        }
+                    });
+                }
+            }
+
+            if (mPreviewHolder != null) {
+                return;
+            }
+            log("Surface holder available: " + width + " x " + height);
+            mPreviewHolder = holder;
+            try {
+                if (mCamera != null) {
+                    mCamera.setPreviewDisplay(holder);
+                }
+            } catch (IOException e) {
+                logE("Unable to set up preview!");
+            }
+        } else if (holder == mCallbackView.getHolder()) {
+            mCallbackHolder = holder;
         }
     }
 
+    @Override
     public void surfaceCreated(SurfaceHolder holder) {
 
     }
 
+    @Override
     public void surfaceDestroyed(SurfaceHolder holder) {
         mPreviewHolder = null;
     }
@@ -248,6 +329,7 @@
 
     private AdapterView.OnItemSelectedListener mCameraSpinnerListener =
                 new AdapterView.OnItemSelectedListener() {
+        @Override
         public void onItemSelected(AdapterView<?> parent,
                         View view, int pos, long id) {
             if (mCameraId != pos) {
@@ -256,12 +338,14 @@
             }
         }
 
+        @Override
         public void onNothingSelected(AdapterView<?> parent) {
 
         }
     };
 
     private OnClickListener mInfoButtonListener = new OnClickListener() {
+        @Override
         public void onClick(View v) {
             FragmentManager fm = getFragmentManager();
             InfoDialogFragment infoDialog = new InfoDialogFragment();
@@ -272,11 +356,13 @@
 
     private AdapterView.OnItemSelectedListener mPreviewSizeListener =
         new AdapterView.OnItemSelectedListener() {
+        @Override
         public void onItemSelected(AdapterView<?> parent,
                 View view, int pos, long id) {
             if (pos == mPreviewSize) return;
             if (mState == CAMERA_PREVIEW) {
-                log("Stopping preview to switch resolutions");
+                log("Stopping preview and callbacks to switch resolutions");
+                stopCallbacks();
                 mCamera.stopPreview();
             }
 
@@ -288,14 +374,15 @@
             log("Setting preview size to " + width + "x" + height);
 
             mCamera.setParameters(mParams);
+            resizePreview();
 
             if (mState == CAMERA_PREVIEW) {
                 log("Restarting preview");
-                resizePreview(width, height);
                 mCamera.startPreview();
             }
         }
 
+        @Override
         public void onNothingSelected(AdapterView<?> parent) {
 
         }
@@ -303,6 +390,7 @@
 
     private View.OnClickListener mPreviewToggleListener =
             new View.OnClickListener() {
+        @Override
         public void onClick(View v) {
             if (mState == CAMERA_TAKE_PICTURE) {
                 logE("Can't change preview state while taking picture!");
@@ -310,8 +398,6 @@
             }
             if (mPreviewToggle.isChecked()) {
                 log("Starting preview");
-                resizePreview(mPreviewSizes.get(mPreviewSize).width,
-                        mPreviewSizes.get(mPreviewSize).height);
                 mCamera.startPreview();
                 mState = CAMERA_PREVIEW;
                 enablePreviewOnlyControls(true);
@@ -327,6 +413,7 @@
 
     private OnItemSelectedListener mAutofocusModeListener =
                 new OnItemSelectedListener() {
+        @Override
         public void onItemSelected(AdapterView<?> parent,
                         View view, int pos, long id) {
             if (pos == mAfMode) return;
@@ -343,6 +430,7 @@
             mCamera.setParameters(mParams);
         }
 
+        @Override
         public void onNothingSelected(AdapterView<?> arg0) {
 
         }
@@ -350,6 +438,7 @@
 
     private OnClickListener mAutofocusButtonListener =
             new View.OnClickListener() {
+        @Override
         public void onClick(View v) {
             log("Triggering autofocus");
             mCamera.autoFocus(mAutofocusCallback);
@@ -358,6 +447,7 @@
 
     private OnClickListener mCancelAutofocusButtonListener =
             new View.OnClickListener() {
+        @Override
         public void onClick(View v) {
             log("Cancelling autofocus");
             mCamera.cancelAutoFocus();
@@ -366,6 +456,7 @@
 
     private Camera.AutoFocusCallback mAutofocusCallback =
             new Camera.AutoFocusCallback() {
+        @Override
         public void onAutoFocus(boolean success, Camera camera) {
             log("Autofocus completed: " + (success ? "success" : "failure") );
         }
@@ -373,6 +464,7 @@
 
     private Camera.AutoFocusMoveCallback mAutofocusMoveCallback =
             new Camera.AutoFocusMoveCallback() {
+        @Override
         public void onAutoFocusMoving(boolean start, Camera camera) {
             log("Autofocus movement: " + (start ? "starting" : "stopped") );
         }
@@ -380,6 +472,7 @@
 
     private OnItemSelectedListener mFlashModeListener =
                 new OnItemSelectedListener() {
+        @Override
         public void onItemSelected(AdapterView<?> parent,
                         View view, int pos, long id) {
             if (pos == mFlashMode) return;
@@ -391,6 +484,7 @@
             mCamera.setParameters(mParams);
         }
 
+        @Override
         public void onNothingSelected(AdapterView<?> arg0) {
 
         }
@@ -399,6 +493,7 @@
 
     private AdapterView.OnItemSelectedListener mSnapshotSizeListener =
             new AdapterView.OnItemSelectedListener() {
+        @Override
         public void onItemSelected(AdapterView<?> parent,
                 View view, int pos, long id) {
             if (pos == mSnapshotSize) return;
@@ -413,6 +508,7 @@
             mCamera.setParameters(mParams);
         }
 
+        @Override
         public void onNothingSelected(AdapterView<?> parent) {
 
         }
@@ -420,6 +516,7 @@
 
     private View.OnClickListener mTakePictureListener =
             new View.OnClickListener() {
+        @Override
         public void onClick(View v) {
             log("Taking picture");
             if (mState == CAMERA_PREVIEW) {
@@ -436,6 +533,7 @@
 
     private AdapterView.OnItemSelectedListener mCamcorderProfileListener =
                 new AdapterView.OnItemSelectedListener() {
+        @Override
         public void onItemSelected(AdapterView<?> parent,
                         View view, int pos, long id) {
             if (pos != mCamcorderProfile) {
@@ -458,6 +556,7 @@
             mVideoRecordSizeSpinner.setSelection(mVideoRecordSize);
         }
 
+        @Override
         public void onNothingSelected(AdapterView<?> parent) {
 
         }
@@ -465,6 +564,7 @@
 
     private AdapterView.OnItemSelectedListener mVideoRecordSizeListener =
                 new AdapterView.OnItemSelectedListener() {
+        @Override
         public void onItemSelected(AdapterView<?> parent,
                         View view, int pos, long id) {
             if (pos == mVideoRecordSize) return;
@@ -473,6 +573,7 @@
             mVideoRecordSize = pos;
         }
 
+        @Override
         public void onNothingSelected(AdapterView<?> parent) {
 
         }
@@ -480,6 +581,7 @@
 
     private AdapterView.OnItemSelectedListener mVideoFrameRateListener =
                 new AdapterView.OnItemSelectedListener() {
+        @Override
         public void onItemSelected(AdapterView<?> parent,
                         View view, int pos, long id) {
             if (pos == mVideoFrameRate) return;
@@ -488,6 +590,7 @@
             mVideoFrameRate = pos;
         }
 
+        @Override
         public void onNothingSelected(AdapterView<?> parent) {
 
         }
@@ -495,6 +598,7 @@
 
     private View.OnClickListener mRecordToggleListener =
             new View.OnClickListener() {
+        @Override
         public void onClick(View v) {
             mPreviewToggle.setEnabled(false);
             if (mState == CAMERA_PREVIEW) {
@@ -510,31 +614,38 @@
 
     private View.OnClickListener mRecordStabilizationToggleListener =
             new View.OnClickListener() {
+        @Override
         public void onClick(View v) {
             boolean on = ((ToggleButton) v).isChecked();
             mParams.setVideoStabilization(on);
+
+            mCamera.setParameters(mParams);
         }
     };
 
     private Camera.ShutterCallback mShutterCb = new Camera.ShutterCallback() {
+        @Override
         public void onShutter() {
             log("Shutter callback received");
         }
     };
 
     private Camera.PictureCallback mRawCb = new Camera.PictureCallback() {
+        @Override
         public void onPictureTaken(byte[] data, Camera camera) {
             log("Raw callback received");
         }
     };
 
     private Camera.PictureCallback mPostviewCb = new Camera.PictureCallback() {
+        @Override
         public void onPictureTaken(byte[] data, Camera camera) {
             log("Postview callback received");
         }
     };
 
     private Camera.PictureCallback mJpegCb = new Camera.PictureCallback() {
+        @Override
         public void onPictureTaken(byte[] data, Camera camera) {
             log("JPEG picture callback received");
             FragmentManager fm = getFragmentManager();
@@ -549,6 +660,70 @@
         }
     };
 
+    private AdapterView.OnItemSelectedListener mCallbackFormatListener =
+            new AdapterView.OnItemSelectedListener() {
+        public void onItemSelected(AdapterView<?> parent,
+                        View view, int pos, long id) {
+            mPreviewFormat = pos;
+
+            log("Setting preview format to " +
+                    mFormatNames.get(mPreviewFormats.get(mPreviewFormat)));
+
+            switch (mState) {
+            case CAMERA_UNINITIALIZED:
+                return;
+            case CAMERA_OPEN:
+                break;
+            case CAMERA_PREVIEW:
+                if (mCallbacksEnabled) {
+                    log("Stopping preview and callbacks to switch formats");
+                    stopCallbacks();
+                    mCamera.stopPreview();
+                }
+                break;
+            case CAMERA_RECORD:
+                logE("Can't update format while recording active");
+                return;
+            }
+
+            mParams.setPreviewFormat(mPreviewFormats.get(mPreviewFormat));
+
+            if (mCallbacksEnabled) {
+                mCamera.setParameters(mParams);
+
+                if (mState == CAMERA_PREVIEW) {
+                    mCamera.startPreview();
+                }
+            }
+
+            configureCallbacks(mCallbackView.getWidth(), mCallbackView.getHeight());
+        }
+
+        public void onNothingSelected(AdapterView<?> parent) {
+
+        }
+    };
+
+    private View.OnClickListener mCallbackToggleListener =
+                new View.OnClickListener() {
+        public void onClick(View v) {
+            if (mCallbacksEnabled) {
+                log("Disabling preview callbacks");
+                stopCallbacks();
+                mCallbacksEnabled = false;
+                resizePreview();
+                mCallbackView.setVisibility(View.GONE);
+
+            } else {
+                log("Enabling preview callbacks");
+                mCallbacksEnabled = true;
+                resizePreview();
+                mCallbackView.setVisibility(View.VISIBLE);
+            }
+        }
+    };
+
+
     // Internal methods
 
     void setUpCamera() {
@@ -571,6 +746,7 @@
         logIndent(1);
 
         updatePreviewSizes(mParams);
+        updatePreviewFormats(mParams);
         updateAfModes(mParams);
         updateFlashModes(mParams);
         updateSnapshotSizes(mParams);
@@ -615,9 +791,7 @@
         mPreviewToggle.setChecked(false);
         enablePreviewOnlyControls(false);
 
-        int width = mPreviewSizes.get(mPreviewSize).width;
-        int height = mPreviewSizes.get(mPreviewSize).height;
-        resizePreview(width, height);
+        resizePreview();
         if (mPreviewToggle.isChecked()) {
             log("Starting preview" );
             mCamera.startPreview();
@@ -688,6 +862,28 @@
         log("Setting preview size to " + width + " x " + height);
     }
 
+    private void updatePreviewFormats(Camera.Parameters params) {
+        mPreviewFormats = params.getSupportedPreviewFormats();
+
+        String[] availableFormatNames = new String[mPreviewFormats.size()];
+        int i = 0;
+        for (Integer previewFormat: mPreviewFormats) {
+            availableFormatNames[i++] = mFormatNames.get(previewFormat);
+        }
+        mCallbackFormatSpinner.setAdapter(
+                new ArrayAdapter<String>(
+                        this, R.layout.spinner_item, availableFormatNames));
+
+        mPreviewFormat = 0;
+        mCallbacksEnabled = false;
+        mCallbackToggle.setChecked(false);
+        mCallbackView.setVisibility(View.GONE);
+
+        params.setPreviewFormat(mPreviewFormats.get(mPreviewFormat));
+        log("Setting preview format to " +
+                mFormatNames.get(mPreviewFormats.get(mPreviewFormat)));
+    }
+
     private void updateSnapshotSizes(Camera.Parameters params) {
         String[] availableSizeNames;
         mSnapshotSizes = params.getSupportedPictureSizes();
@@ -816,22 +1012,114 @@
                         this, R.layout.spinner_item, nameArray));
 
         mVideoFrameRate = 0;
-        log("Setting frame rate to " + nameArray[mVideoFrameRate]);
+        log("Setting recording frame rate to " + nameArray[mVideoFrameRate]);
     }
 
-    void resizePreview(int width, int height) {
-        if (mPreviewHolder != null) {
-            int viewHeight = mPreviewView.getHeight();
-            int viewWidth = (int)(((double)width)/height * viewHeight);
+    void resizePreview() {
+        // Reset preview layout parameters, to trigger layout pass
+        // This will eventually call layoutPreview below
+        Resources res = getResources();
+        mPreviewView.setLayoutParams(
+                new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 0,
+                        mCallbacksEnabled ?
+                        res.getInteger(R.integer.preview_with_callback_weight):
+                        res.getInteger(R.integer.preview_only_weight) ));
+    }
 
-            mPreviewView.setLayoutParams(
-                new LayoutParams(viewWidth, viewHeight));
+    void layoutPreview() {
+        int width = mPreviewSizes.get(mPreviewSize).width;
+        int height = mPreviewSizes.get(mPreviewSize).height;
+        float previewAspect = ((float) width) / height;
+
+        int viewHeight = mPreviewView.getHeight();
+        int viewWidth = mPreviewView.getWidth();
+        float viewAspect = ((float) viewWidth) / viewHeight;
+        if ( previewAspect > viewAspect) {
+            viewHeight = (int) (viewWidth / previewAspect);
+        } else {
+            viewWidth = (int) (viewHeight * previewAspect);
         }
+        mPreviewView.setLayoutParams(
+                new LayoutParams(viewWidth, viewHeight));
 
+        if (mCallbacksEnabled) {
+            int callbackHeight = mCallbackView.getHeight();
+            int callbackWidth = mCallbackView.getWidth();
+            float callbackAspect = ((float) callbackWidth) / callbackHeight;
+            if ( previewAspect > callbackAspect) {
+                callbackHeight = (int) (callbackWidth / previewAspect);
+            } else {
+                callbackWidth = (int) (callbackHeight * previewAspect);
+            }
+            mCallbackView.setLayoutParams(
+                    new LayoutParams(callbackWidth, callbackHeight));
+            configureCallbacks(callbackWidth, callbackHeight);
+        }
+    }
+
+
+    private void configureCallbacks(int callbackWidth, int callbackHeight) {
+        if (mState >= CAMERA_OPEN && mCallbacksEnabled) {
+            mCamera.setPreviewCallbackWithBuffer(null);
+            int width = mPreviewSizes.get(mPreviewSize).width;
+            int height = mPreviewSizes.get(mPreviewSize).height;
+            int format = mPreviewFormats.get(mPreviewFormat);
+
+            mCallbackProcessor = new CallbackProcessor(width, height, format,
+                    getResources(), mCallbackView,
+                    callbackWidth, callbackHeight, mRS);
+
+            int size = getCallbackBufferSize(width, height, format);
+            log("Configuring callbacks:" + width + " x " + height +
+                    " , format " + format);
+            for (int i = 0; i < CALLBACK_BUFFER_COUNT; i++) {
+                mCamera.addCallbackBuffer(new byte[size]);
+            }
+            mCamera.setPreviewCallbackWithBuffer(this);
+        }
+        mLastCallbackTimestamp = -1;
+        mCallbackFrameCount = 0;
+        mCallbackAvgFrameDuration = 30;
+    }
+
+    private void stopCallbacks() {
+        if (mState >= CAMERA_OPEN) {
+            mCamera.setPreviewCallbackWithBuffer(null);
+            if (mCallbackProcessor != null) {
+                if (!mCallbackProcessor.stop()) {
+                    logE("Can't stop preview callback processing!");
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onPreviewFrame(byte[] data, Camera camera) {
+        long timestamp = SystemClock.elapsedRealtime();
+        if (mLastCallbackTimestamp != -1) {
+            long frameDuration = timestamp - mLastCallbackTimestamp;
+            mCallbackAvgFrameDuration =
+                    mCallbackAvgFrameDuration * MEAN_FPS_HISTORY_COEFF +
+                    frameDuration * MEAN_FPS_MEASUREMENT_COEFF;
+        }
+        mLastCallbackTimestamp = timestamp;
+        if (mState < CAMERA_PREVIEW || !mCallbacksEnabled) {
+            mCamera.addCallbackBuffer(data);
+            return;
+        }
+        mCallbackFrameCount++;
+        if (mCallbackFrameCount % FPS_REPORTING_PERIOD == 0) {
+            log("Got " + FPS_REPORTING_PERIOD + " callback frames, fps "
+                    + 1e3/mCallbackAvgFrameDuration);
+        }
+        mCallbackProcessor.displayCallback(data);
+
+        mCamera.addCallbackBuffer(data);
     }
 
     static final int MEDIA_TYPE_IMAGE = 0;
     static final int MEDIA_TYPE_VIDEO = 1;
+    @SuppressLint("SimpleDateFormat")
     File getOutputMediaFile(int type){
         // To be safe, you should check that the SDCard is mounted
         // using Environment.getExternalStorageState() before doing this.
@@ -877,8 +1165,10 @@
                 new String[] { newFile.toString() },
                 null,
                 new MediaScannerConnection.OnScanCompletedListener() {
+                    @Override
                     public void onScanCompleted(final String path, final Uri uri) {
                         h.post(new Runnable() {
+                            @Override
                             public void run() {
                                 log("MediaScanner notified: " +
                                         path + " -> " + uri);
@@ -955,6 +1245,7 @@
 
     private MediaRecorder.OnErrorListener mRecordingErrorListener =
             new MediaRecorder.OnErrorListener() {
+        @Override
         public void onError(MediaRecorder mr, int what, int extra) {
             logE("MediaRecorder reports error: " + what + ", extra "
                     + extra);
@@ -966,6 +1257,7 @@
 
     private MediaRecorder.OnInfoListener mRecordingInfoListener =
             new MediaRecorder.OnInfoListener() {
+        @Override
         public void onInfo(MediaRecorder mr, int what, int extra) {
             log("MediaRecorder reports info: " + what + ", extra "
                     + extra);
@@ -989,6 +1281,36 @@
         }
     }
 
+    static int getCallbackBufferSize(int width, int height, int format) {
+        int size = -1;
+        switch (format) {
+        case ImageFormat.NV21:
+            size = width * height * 3 / 2;
+            break;
+        case ImageFormat.YV12:
+            int y_stride = (int) (Math.ceil( width / 16.) * 16);
+            int y_size = y_stride * height;
+            int c_stride = (int) (Math.ceil(y_stride / 32.) * 16);
+            int c_size = c_stride * height/2;
+            size = y_size + c_size * 2;
+            break;
+        case ImageFormat.NV16:
+        case ImageFormat.RGB_565:
+        case ImageFormat.YUY2:
+            size = 2 * width * height;
+            break;
+        case ImageFormat.JPEG:
+            Log.e(TAG, "JPEG callback buffers not supported!");
+            size = 0;
+            break;
+        case ImageFormat.UNKNOWN:
+            Log.e(TAG, "Unknown-format callback buffers not supported!");
+            size = 0;
+            break;
+        }
+        return size;
+    }
+
     private int mLogIndentLevel = 0;
     private String mLogIndent = "\t";
     /** Increment or decrement log indentation level */
@@ -1002,6 +1324,7 @@
         mLogIndent = new String(mLogIndentArray);
     }
 
+    @SuppressLint("SimpleDateFormat")
     SimpleDateFormat mDateFormatter = new SimpleDateFormat("HH:mm:ss.SSS");
     /** Log both to log text view and to device logcat */
     void log(String logLine) {
diff --git a/apps/TestingCamera/src/com/android/testingcamera/callback.rs b/apps/TestingCamera/src/com/android/testingcamera/callback.rs
new file mode 100644
index 0000000..ba7b891
--- /dev/null
+++ b/apps/TestingCamera/src/com/android/testingcamera/callback.rs
@@ -0,0 +1,132 @@
+#pragma version(1)
+#pragma rs java_package_name(com.android.testingcamera)
+#pragma rs_fp_relaxed
+
+uchar *yuv_in;
+
+// Input globals
+uint32_t yuv_height;
+uint32_t yuv_width;
+uint32_t out_width;
+uint32_t out_height;
+// Derived globals
+uint32_t y_stride;
+uint32_t uv_stride;
+uint32_t u_start;
+uint32_t v_start;
+float x_scale;
+float y_scale;
+
+enum ImageFormat {
+    NV16 = 16,
+    NV21 = 17,
+    RGB_565 = 4,
+    UNKNOWN = 0,
+    YUY2 = 20,
+    YV12 = 0x32315659
+};
+
+// Must be called before using any conversion methods
+void init_convert(uint32_t yw, uint32_t yh, uint32_t format,
+        uint32_t ow, uint32_t oh) {
+    yuv_height = yh;
+    yuv_width = yw;
+    out_width = ow;
+    out_height = oh;
+
+    x_scale = (float)yuv_width / out_width;
+    y_scale = (float)yuv_height / out_height;
+
+    switch (format) {
+    case NV16:
+    case NV21:
+        y_stride = yuv_width;
+        uv_stride = yuv_width;
+        v_start = y_stride * yuv_height;
+        u_start = v_start + 1;
+        break;
+    case YV12:
+        // Minimum align-16 stride
+        y_stride = (yuv_width + 0xF) & ~0xF;
+        uv_stride = (y_stride / 2 + 0xF) & ~0xF;
+        v_start = y_stride * yuv_height;
+        u_start = v_start + uv_stride * (yuv_height / 2);
+        break;
+    case YUY2:
+        y_stride = yuv_width * 2;
+        uv_stride = y_stride;
+        u_start = 1;
+        v_start = 3;
+        break;
+    case RGB_565:
+    case UNKNOWN:
+    default:
+        y_stride = yuv_width;
+        uv_stride = yuv_width;
+        v_start = 0;
+        u_start = 0;
+    }
+}
+
+// Makes up a conversion for unknown YUV types to try to display something
+// Asssumes that there's at least 1bpp in input YUV data
+uchar4 __attribute__((kernel)) convert_unknown(uint32_t x, uint32_t y) {
+    uint32_t x_scaled = x * x_scale;
+    uint32_t y_scaled = y * y_scale;
+
+    uchar4 out;
+    out.r = yuv_in[y_stride * y_scaled + x_scaled];
+    out.g = 128;
+    out.b = 128;
+    out.a = 255; // For affine transform later
+    return out;
+}
+
+// Converts semiplanar YVU to interleaved YUV, nearest neighbor
+uchar4 __attribute__((kernel)) convert_semiplanar(uint32_t x, uint32_t y) {
+    uint32_t x_scaled = x * x_scale;
+    uint32_t y_scaled = y * y_scale;
+
+    uint32_t uv_row = y_scaled / 2; // truncation is important here
+    uint32_t uv_col = x_scaled & ~0x1;
+    uint32_t vu_pixel = uv_row * uv_stride + uv_col;
+
+    uchar4 out;
+    out.r = yuv_in[y_stride * y_scaled + x_scaled];
+    out.g = yuv_in[u_start + vu_pixel];
+    out.b = yuv_in[v_start + vu_pixel];
+    out.a = 255; // For affine transform later
+    return out;
+}
+
+// Converts planar YVU to interleaved YUV, nearest neighbor
+uchar4 __attribute__((kernel)) convert_planar(uint32_t x, uint32_t y) {
+    uint32_t x_scaled = x * x_scale;
+    uint32_t y_scaled = y * y_scale;
+
+    uint32_t uv_row = y_scaled / 2; // truncation is important here
+    uint32_t vu_pixel = uv_stride * uv_row + x_scaled / 2;
+
+    uchar4 out;
+    out.r = yuv_in[y_stride * y_scaled + x_scaled];
+    out.g = yuv_in[u_start + vu_pixel];
+    out.b = yuv_in[v_start + vu_pixel];
+    out.a = 255; // For affine transform later
+    return out;
+}
+
+// Converts interleaved 4:2:2 YUV to interleaved YUV, nearest neighbor
+uchar4 __attribute__((kernel)) convert_interleaved(uint32_t x, uint32_t y) {
+    uint32_t x_scaled = x * x_scale;
+    uint32_t y_scaled = y * y_scale;
+
+    uint32_t uv_col = 2 * (x_scaled & ~0x1);
+    uint32_t vu_pixel = y_stride * y_scaled + uv_col;
+
+    uchar4 out;
+    out.r = yuv_in[y_stride * y_scaled + x_scaled * 2];
+    out.g = yuv_in[u_start + vu_pixel];
+    out.b = yuv_in[v_start + vu_pixel];
+    out.a = 255; // For affine transform later
+    return out;
+}