Merge "Fix test break due to API not working as advertised." into lmp-sprout-dev
diff --git a/apps/CameraITS/tests/scene1/test_crop_region_raw.py b/apps/CameraITS/tests/scene1/test_crop_region_raw.py
index 9fc52cb..189e987 100644
--- a/apps/CameraITS/tests/scene1/test_crop_region_raw.py
+++ b/apps/CameraITS/tests/scene1/test_crop_region_raw.py
@@ -20,25 +20,6 @@
 import numpy
 import os.path
 
-
-def check_crop_region(expected, reported, active, err_threshold):
-    """Check if the reported region is within the tolerance.
-
-    Args:
-        expected: expected crop region
-        reported: reported crop region
-        active: active resolution
-        err_threshold: error threshold for the active resolution
-    """
-
-    ex = (active["right"] - active["left"]) * err_threshold
-    ey = (active["bottom"] - active["top"]) * err_threshold
-
-    assert ((abs(expected["left"] - reported["left"]) <= ex) and
-            (abs(expected["right"] - reported["right"]) <= ex) and
-            (abs(expected["top"] - reported["top"]) <= ey) and
-            (abs(expected["bottom"] - reported["bottom"]) <= ey))
-
 def main():
     """Test that raw streams are not croppable.
     """
@@ -53,6 +34,7 @@
                              its.caps.raw16(props) and
                              its.caps.per_frame_control(props))
 
+        # Calculate the active sensor region for a full (non-cropped) image.
         a = props['android.sensor.info.activeArraySize']
         ax, ay = a["left"], a["top"]
         aw, ah = a["right"] - a["left"], a["bottom"] - a["top"]
@@ -65,6 +47,19 @@
             "bottom": ah
         }
 
+        # Calculate a center crop region.
+        zoom = min(3.0, its.objects.get_max_digital_zoom(props))
+        assert(zoom >= 1)
+        cropw = aw / zoom
+        croph = ah / zoom
+
+        crop_region = {
+            "left": aw / 2 - cropw / 2,
+            "top": ah / 2 - croph / 2,
+            "right": aw / 2 + cropw / 2,
+            "bottom": ah / 2 + croph / 2
+        }
+
         # Capture without a crop region.
         # Use a manual request with a linear tonemap so that the YUV and RAW
         # should look the same (once converted by the its.image module).
@@ -72,59 +67,57 @@
         req = its.objects.manual_capture_request(s,e, True)
         cap1_raw, cap1_yuv = cam.do_capture(req, cam.CAP_RAW_YUV)
 
-        # Calculate a center crop region.
-        zoom = min(3.0, its.objects.get_max_digital_zoom(props))
-        assert(zoom >= 1)
-        cropw = aw / zoom
-        croph = ah / zoom
-
-        req["android.scaler.cropRegion"] = {
-            "left": aw / 2 - cropw / 2,
-            "top": ah / 2 - croph / 2,
-            "right": aw / 2 + cropw / 2,
-            "bottom": ah / 2 + croph / 2
-        }
-
-        # when both YUV and RAW are requested, the crop region that's
-        # applied to YUV should be reported.
-        crop_region = req["android.scaler.cropRegion"]
-        if crop_region == full_region:
-            crop_region_err_thresh = 0.0
-        else:
-            crop_region_err_thresh = CROP_REGION_ERROR_THRESHOLD
-
+        # Capture with a crop region.
+        req["android.scaler.cropRegion"] = crop_region
         cap2_raw, cap2_yuv = cam.do_capture(req, cam.CAP_RAW_YUV)
 
+        # Check the metadata related to crop regions.
+        # When both YUV and RAW are requested, the crop region that's
+        # applied to YUV should be reported.
+        # Note that the crop region returned by the cropped captures doesn't
+        # need to perfectly match the one that was requested.
         imgs = {}
-        for s, cap, cr, err_delta in [("yuv_full", cap1_yuv, full_region, 0),
-                      ("raw_full", cap1_raw, full_region, 0),
-                      ("yuv_crop", cap2_yuv, crop_region, crop_region_err_thresh),
-                      ("raw_crop", cap2_raw, crop_region, crop_region_err_thresh)]:
+        for s, cap, cr_expected, err_delta in [
+                ("yuv_full",cap1_yuv,full_region,0),
+                ("raw_full",cap1_raw,full_region,0),
+                ("yuv_crop",cap2_yuv,crop_region,CROP_REGION_ERROR_THRESHOLD),
+                ("raw_crop",cap2_raw,crop_region,CROP_REGION_ERROR_THRESHOLD)]:
+
+            # Convert the capture to RGB and dump to a file.
             img = its.image.convert_capture_to_rgb_image(cap, props=props)
             its.image.write_image(img, "%s_%s.jpg" % (NAME, s))
-            r = cap["metadata"]["android.scaler.cropRegion"]
-            x, y = r["left"], r["top"]
-            w, h = r["right"] - r["left"], r["bottom"] - r["top"]
             imgs[s] = img
-            print "Crop on %s: (%d,%d %dx%d)" % (s, x, y, w, h)
-            check_crop_region(cr, r, a, err_delta)
+
+            # Get the crop region that is reported in the capture result.
+            cr_reported = cap["metadata"]["android.scaler.cropRegion"]
+            x, y = cr_reported["left"], cr_reported["top"]
+            w = cr_reported["right"] - cr_reported["left"]
+            h = cr_reported["bottom"] - cr_reported["top"]
+            print "Crop reported on %s: (%d,%d %dx%d)" % (s, x, y, w, h)
+
+            # Test that the reported crop region is the same as the expected
+            # one, for a non-cropped capture, and is close to the expected one,
+            # for a cropped capture.
+            ex = aw * err_delta
+            ey = ah * err_delta
+            assert ((abs(cr_expected["left"] - cr_reported["left"]) <= ex) and
+                    (abs(cr_expected["right"] - cr_reported["right"]) <= ex) and
+                    (abs(cr_expected["top"] - cr_reported["top"]) <= ey) and
+                    (abs(cr_expected["bottom"] - cr_reported["bottom"]) <= ey))
 
         # Also check the image content; 3 of the 4 shots should match.
         # Note that all the shots are RGB below; the variable names correspond
         # to what was captured.
-        # Average the images down 4x4 -> 1 prior to comparison to smooth out
-        # noise.
-        # Shrink the YUV images an additional 2x2 -> 1 to account for the size
-        # reduction that the raw images went through in the RGB conversion.
+
+        # Shrink the YUV images 2x2 -> 1 to account for the size reduction that
+        # the raw images went through in the RGB conversion.
         imgs2 = {}
         for s,img in imgs.iteritems():
             h,w,ch = img.shape
-            m = 4
             if s in ["yuv_full", "yuv_crop"]:
-                m = 8
-            img = img.reshape(h/m,m,w/m,m,3).mean(3).mean(1).reshape(h/m,w/m,3)
+                img = img.reshape(h/2,2,w/2,2,3).mean(3).mean(1)
+                img = img.reshape(h/2,w/2,3)
             imgs2[s] = img
-            print s, img.shape
 
         # Strip any border pixels from the raw shots (since the raw images may
         # be larger than the YUV images). Assume a symmetric padded border.
@@ -139,7 +132,10 @@
         for s,img in imgs2.iteritems():
             its.image.write_image(img, "%s_comp_%s.jpg" % (NAME, s))
 
-        # Compute image diffs.
+        # Compute diffs between images of the same type.
+        # The raw_crop and raw_full shots should be identical (since the crop
+        # doesn't apply to raw images), and the yuv_crop and yuv_full shots
+        # should be different.
         diff_yuv = numpy.fabs((imgs2["yuv_full"] - imgs2["yuv_crop"])).mean()
         diff_raw = numpy.fabs((imgs2["raw_full"] - imgs2["raw_crop"])).mean()
         print "YUV diff (crop vs. non-crop):", diff_yuv
diff --git a/apps/CtsVerifier/Android.mk b/apps/CtsVerifier/Android.mk
index e370c81..87f962f 100644
--- a/apps/CtsVerifier/Android.mk
+++ b/apps/CtsVerifier/Android.mk
@@ -25,7 +25,10 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-Iaidl-files-under, src)
 
-LOCAL_STATIC_JAVA_LIBRARIES := cts-sensors-tests ctstestrunner android-ex-camera2
+LOCAL_STATIC_JAVA_LIBRARIES := android-ex-camera2 \
+                               compatibility-common-util-devicesidelib_v2 \
+                               cts-sensors-tests \
+                               ctstestrunner \
 
 LOCAL_PACKAGE_NAME := CtsVerifier
 
@@ -40,10 +43,13 @@
 
 include $(BUILD_PACKAGE)
 
+notification-bot := $(call intermediates-dir-for,APPS,NotificationBot)/package.apk
+
 # Builds and launches CTS Verifier on a device.
 .PHONY: cts-verifier
-cts-verifier: CtsVerifier adb
+cts-verifier: CtsVerifier adb NotificationBot
 	adb install -r $(PRODUCT_OUT)/data/app/CtsVerifier/CtsVerifier.apk \
+		&& adb install -r $(notification-bot) \
 		&& adb shell "am start -n com.android.cts.verifier/.CtsVerifierActivity"
 
 #
@@ -79,10 +85,11 @@
 $(verifier-zip) : $(HOST_OUT)/bin/cts-usb-accessory
 endif
 $(verifier-zip) : $(HOST_OUT)/CameraITS
-
+$(verifier-zip) : $(notification-bot)
 $(verifier-zip) : $(call intermediates-dir-for,APPS,CtsVerifier)/package.apk | $(ACP)
 		$(hide) mkdir -p $(verifier-dir)
 		$(hide) $(ACP) -fp $< $(verifier-dir)/CtsVerifier.apk
+		$(ACP) -fp $(notification-bot) $(verifier-dir)/NotificationBot.apk
 ifeq ($(HOST_OS),linux)
 		$(hide) $(ACP) -fp $(HOST_OUT)/bin/cts-usb-accessory $(verifier-dir)/cts-usb-accessory
 endif
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/PassFailButtons.java b/apps/CtsVerifier/src/com/android/cts/verifier/PassFailButtons.java
index ab119bd..5a08558 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/PassFailButtons.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/PassFailButtons.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.verifier;
 
+import com.android.compatibility.common.util.ReportLog;
+
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.content.ContentResolver;
@@ -94,10 +96,18 @@
          * @param passed Whether or not the test passed.
          */
         void setTestResultAndFinish(boolean passed);
+
+        /** @return A {@link ReportLog} that is used to record test metric data. */
+        ReportLog getReportLog();
     }
 
     public static class Activity extends android.app.Activity implements PassFailActivity {
         private WakeLock mWakeLock;
+        private final ReportLog reportLog;
+
+        public Activity() {
+           this.reportLog = new CtsVerifierReportLog();
+        }
 
         @Override
         protected void onResume() {
@@ -149,13 +159,22 @@
 
         @Override
         public void setTestResultAndFinish(boolean passed) {
-            PassFailButtons.setTestResultAndFinishHelper(this, getTestId(), getTestDetails(),
-                    passed);
+            PassFailButtons.setTestResultAndFinishHelper(
+                    this, getTestId(), getTestDetails(), passed, getReportLog());
         }
+
+        @Override
+        public ReportLog getReportLog() { return reportLog; }
     }
 
     public static class ListActivity extends android.app.ListActivity implements PassFailActivity {
 
+        private final ReportLog reportLog;
+
+        public ListActivity() {
+            this.reportLog = new CtsVerifierReportLog();
+        }
+
         @Override
         public void setPassFailButtonClickListeners() {
             setPassFailClickListeners(this);
@@ -188,14 +207,23 @@
 
         @Override
         public void setTestResultAndFinish(boolean passed) {
-            PassFailButtons.setTestResultAndFinishHelper(this, getTestId(), getTestDetails(),
-                    passed);
+            PassFailButtons.setTestResultAndFinishHelper(
+                    this, getTestId(), getTestDetails(), passed, getReportLog());
         }
+
+        @Override
+        public ReportLog getReportLog() { return reportLog; }
     }
 
     public static class TestListActivity extends AbstractTestListActivity
             implements PassFailActivity {
 
+        private final ReportLog reportLog;
+
+        public TestListActivity() {
+            this.reportLog = new CtsVerifierReportLog();
+        }
+
         @Override
         public void setPassFailButtonClickListeners() {
             setPassFailClickListeners(this);
@@ -228,9 +256,12 @@
 
         @Override
         public void setTestResultAndFinish(boolean passed) {
-            PassFailButtons.setTestResultAndFinishHelper(this, getTestId(), getTestDetails(),
-                    passed);
+            PassFailButtons.setTestResultAndFinishHelper(
+                    this, getTestId(), getTestDetails(), passed, getReportLog());
         }
+
+        @Override
+        public ReportLog getReportLog() { return reportLog; }
     }
 
     private static <T extends android.app.Activity & PassFailActivity>
@@ -239,7 +270,7 @@
             @Override
             public void onClick(View target) {
                 setTestResultAndFinish(activity, activity.getTestId(), activity.getTestDetails(),
-                        target);
+                        activity.getReportLog(), target);
             }
         };
 
@@ -366,7 +397,7 @@
 
     /** Set the test result corresponding to the button clicked and finish the activity. */
     private static void setTestResultAndFinish(android.app.Activity activity, String testId,
-            String testDetails, View target) {
+            String testDetails, ReportLog reportLog, View target) {
         boolean passed;
         switch (target.getId()) {
             case R.id.pass_button:
@@ -378,16 +409,16 @@
             default:
                 throw new IllegalArgumentException("Unknown id: " + target.getId());
         }
-        setTestResultAndFinishHelper(activity, testId, testDetails, passed);
+        setTestResultAndFinishHelper(activity, testId, testDetails, passed, reportLog);
     }
 
     /** Set the test result and finish the activity. */
     private static void setTestResultAndFinishHelper(android.app.Activity activity, String testId,
-            String testDetails, boolean passed) {
+            String testDetails, boolean passed, ReportLog reportLog) {
         if (passed) {
-            TestResult.setPassedResult(activity, testId, testDetails);
+            TestResult.setPassedResult(activity, testId, testDetails, reportLog);
         } else {
-            TestResult.setFailedResult(activity, testId, testDetails);
+            TestResult.setFailedResult(activity, testId, testDetails, reportLog);
         }
 
         activity.finish();
@@ -396,4 +427,8 @@
     private static ImageButton getPassButtonView(android.app.Activity activity) {
         return (ImageButton) activity.findViewById(R.id.pass_button);
     }
+
+    public static class CtsVerifierReportLog extends ReportLog {
+
+    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
index afe3a73..2160902 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.verifier;
 
+import com.android.compatibility.common.util.ReportLog;
+
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -30,6 +32,9 @@
 import android.widget.ListView;
 import android.widget.TextView;
 
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -66,6 +71,9 @@
     /** Map from test name to test details. */
     private final Map<String, String> mTestDetails = new HashMap<String, String>();
 
+    /** Map from test name to {@link ReportLog}. */
+    private final Map<String, ReportLog> mReportLogs = new HashMap<String, ReportLog>();
+
     private final LayoutInflater mLayoutInflater;
 
     /** {@link ListView} row that is either a test category header or a test. */
@@ -168,7 +176,7 @@
 
     public void setTestResult(TestResult testResult) {
         new SetTestResultTask(testResult.getName(), testResult.getResult(),
-                testResult.getDetails()).execute();
+                testResult.getDetails(), testResult.getReportLog()).execute();
     }
 
     class RefreshTestResultsTask extends AsyncTask<Void, Void, RefreshResult> {
@@ -187,6 +195,8 @@
             mTestResults.putAll(result.mResults);
             mTestDetails.clear();
             mTestDetails.putAll(result.mDetails);
+            mReportLogs.clear();
+            mReportLogs.putAll(result.mReportLogs);
             notifyDataSetChanged();
         }
     }
@@ -195,12 +205,17 @@
         List<TestListItem> mItems;
         Map<String, Integer> mResults;
         Map<String, String> mDetails;
+        Map<String, ReportLog> mReportLogs;
 
-        RefreshResult(List<TestListItem> items, Map<String, Integer> results,
-                Map<String, String> details) {
+        RefreshResult(
+                List<TestListItem> items,
+                Map<String, Integer> results,
+                Map<String, String> details,
+                Map<String, ReportLog> reportLogs) {
             mItems = items;
             mResults = results;
             mDetails = details;
+            mReportLogs = reportLogs;
         }
     }
 
@@ -211,11 +226,13 @@
         TestResultsProvider.COLUMN_TEST_NAME,
         TestResultsProvider.COLUMN_TEST_RESULT,
         TestResultsProvider.COLUMN_TEST_DETAILS,
+        TestResultsProvider.COLUMN_TEST_METRICS,
     };
 
     RefreshResult getRefreshResults(List<TestListItem> items) {
         Map<String, Integer> results = new HashMap<String, Integer>();
         Map<String, String> details = new HashMap<String, String>();
+        Map<String, ReportLog> reportLogs = new HashMap<String, ReportLog>();
         ContentResolver resolver = mContext.getContentResolver();
         Cursor cursor = null;
         try {
@@ -226,8 +243,10 @@
                     String testName = cursor.getString(1);
                     int testResult = cursor.getInt(2);
                     String testDetails = cursor.getString(3);
+                    ReportLog reportLog = (ReportLog) deserialize(cursor.getBlob(4));
                     results.put(testName, testResult);
                     details.put(testName, testDetails);
+                    reportLogs.put(testName, reportLog);
                 } while (cursor.moveToNext());
             }
         } finally {
@@ -235,7 +254,7 @@
                 cursor.close();
             }
         }
-        return new RefreshResult(items, results, details);
+        return new RefreshResult(items, results, details, reportLogs);
     }
 
     class ClearTestResultsTask extends AsyncTask<Void, Void, Void> {
@@ -256,15 +275,22 @@
 
         private final String mDetails;
 
-        SetTestResultTask(String testName, int result, String details) {
+        private final ReportLog mReportLog;
+
+        SetTestResultTask(
+                String testName,
+                int result,
+                String details,
+                ReportLog reportLog) {
             mTestName = testName;
             mResult = result;
             mDetails = details;
+            mReportLog = reportLog;
         }
 
         @Override
         protected Void doInBackground(Void... params) {
-            TestResultsProvider.setTestResult(mContext, mTestName, mResult, mDetails);
+            TestResultsProvider.setTestResult(mContext, mTestName, mResult, mDetails, mReportLog);
             return null;
         }
     }
@@ -332,6 +358,13 @@
                 : null;
     }
 
+    public ReportLog getReportLog(int position) {
+        TestListItem item = getItem(position);
+        return mReportLogs.containsKey(item.testName)
+                ? mReportLogs.get(item.testName)
+                : null;
+    }
+
     public boolean allTestsPassed() {
         for (TestListItem item : mRows) {
             if (item.isTest() && (!mTestResults.containsKey(item.testName)
@@ -400,4 +433,29 @@
 
         }
     }
+
+    private static Object deserialize(byte[] bytes) {
+        if (bytes == null || bytes.length == 0) {
+            return null;
+        }
+        ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes);
+        ObjectInputStream objectInput = null;
+        try {
+            objectInput = new ObjectInputStream(byteStream);
+            return objectInput.readObject();
+        } catch (IOException e) {
+            return null;
+        } catch (ClassNotFoundException e) {
+            return null;
+        } finally {
+            try {
+                if (objectInput != null) {
+                    objectInput.close();
+                }
+                byteStream.close();
+            } catch (IOException e) {
+                // Ignore close exception.
+            }
+        }
+    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestResult.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestResult.java
index 68513ac..d8a675c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestResult.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestResult.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.verifier;
 
+import com.android.compatibility.common.util.ReportLog;
+
 import android.app.Activity;
 import android.content.Intent;
 
@@ -35,29 +37,44 @@
     private static final String TEST_NAME = "name";
     private static final String TEST_RESULT = "result";
     private static final String TEST_DETAILS = "details";
+    private static final String TEST_METRICS = "metrics";
 
     private final String mName;
     private final int mResult;
     private final String mDetails;
+    private final ReportLog mReportLog;
 
     /** Sets the test activity's result to pass. */
     public static void setPassedResult(Activity activity, String testId, String testDetails) {
+        setPassedResult(activity, testId, testDetails, null /*reportLog*/);
+    }
+
+    /** Sets the test activity's result to pass including a test report log result. */
+    public static void setPassedResult(Activity activity, String testId, String testDetails,
+            ReportLog reportLog) {
         activity.setResult(Activity.RESULT_OK, createResult(activity, TEST_RESULT_PASSED, testId,
-                testDetails));
+                testDetails, reportLog));
     }
 
     /** Sets the test activity's result to failed. */
     public static void setFailedResult(Activity activity, String testId, String testDetails) {
+        setFailedResult(activity, testId, testDetails, null /*reportLog*/);
+    }
+
+    /** Sets the test activity's result to failed including a test report log result. */
+    public static void setFailedResult(Activity activity, String testId, String testDetails,
+            ReportLog reportLog) {
         activity.setResult(Activity.RESULT_OK, createResult(activity, TEST_RESULT_FAILED, testId,
-                testDetails));
+                testDetails, reportLog));
     }
 
     private static Intent createResult(Activity activity, int testResult, String testName,
-            String testDetails) {
+            String testDetails, ReportLog reportLog) {
         Intent data = new Intent(activity, activity.getClass());
         data.putExtra(TEST_NAME, testName);
         data.putExtra(TEST_RESULT, testResult);
         data.putExtra(TEST_DETAILS, testDetails);
+        data.putExtra(TEST_METRICS, reportLog);
         return data;
     }
 
@@ -69,13 +86,16 @@
         String name = data.getStringExtra(TEST_NAME);
         int result = data.getIntExtra(TEST_RESULT, TEST_RESULT_NOT_EXECUTED);
         String details = data.getStringExtra(TEST_DETAILS);
-        return new TestResult(name, result, details);
+        ReportLog reportLog = (ReportLog) data.getSerializableExtra(TEST_METRICS);
+        return new TestResult(name, result, details, reportLog);
     }
 
-    private TestResult(String name, int result, String details) {
+    private TestResult(
+            String name, int result, String details, ReportLog reportLog) {
         this.mName = name;
         this.mResult = result;
         this.mDetails = details;
+        this.mReportLog = reportLog;
     }
 
     /** Return the name of the test like "com.android.cts.verifier.foo.FooTest" */
@@ -92,4 +112,9 @@
     public String getDetails() {
         return mDetails;
     }
+
+    /** @return the {@link ReportLog} or null if not set */
+    public ReportLog getReportLog() {
+        return mReportLog;
+    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsBackupHelper.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsBackupHelper.java
index e4cd24a..45e528f 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsBackupHelper.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsBackupHelper.java
@@ -59,6 +59,7 @@
             int resultIndex = cursor.getColumnIndex(TestResultsProvider.COLUMN_TEST_RESULT);
             int infoSeenIndex = cursor.getColumnIndex(TestResultsProvider.COLUMN_TEST_INFO_SEEN);
             int detailsIndex = cursor.getColumnIndex(TestResultsProvider.COLUMN_TEST_DETAILS);
+            int metricsIndex = cursor.getColumnIndex(TestResultsProvider.COLUMN_TEST_METRICS);
 
             ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
             DataOutputStream dataOutput = new DataOutputStream(byteOutput);
@@ -69,11 +70,16 @@
                 int result = cursor.getInt(resultIndex);
                 int infoSeen = cursor.getInt(infoSeenIndex);
                 String details = cursor.getString(detailsIndex);
+                byte[] metricsData = cursor.getBlob(metricsIndex);
 
                 dataOutput.writeUTF(name);
                 dataOutput.writeInt(result);
                 dataOutput.writeInt(infoSeen);
                 dataOutput.writeUTF(details != null ? details : "");
+                dataOutput.writeInt(metricsData.length);
+                if (metricsData.length > 0) {
+                    dataOutput.write(metricsData);
+                }
             }
 
             byte[] rawBytes = byteOutput.toByteArray();
@@ -106,12 +112,19 @@
                     int result = dataInput.readInt();
                     int infoSeen = dataInput.readInt();
                     String details = dataInput.readUTF();
+                    int metricsDataSize = dataInput.readInt();
 
                     values[i] = new ContentValues();
                     values[i].put(TestResultsProvider.COLUMN_TEST_NAME, name);
                     values[i].put(TestResultsProvider.COLUMN_TEST_RESULT, result);
                     values[i].put(TestResultsProvider.COLUMN_TEST_INFO_SEEN, infoSeen);
                     values[i].put(TestResultsProvider.COLUMN_TEST_DETAILS, details);
+
+                    if (metricsDataSize > 0) {
+                        byte[] metrics = new byte[metricsDataSize];
+                        dataInput.readFully(metrics);
+                        values[i].put(TestResultsProvider.COLUMN_TEST_METRICS, metrics);
+                    }
                 }
 
                 ContentResolver resolver = mContext.getContentResolver();
@@ -127,7 +140,7 @@
 
     private void failBackupTest() {
         TestResultsProvider.setTestResult(mContext, BackupTestActivity.class.getName(),
-                TestResult.TEST_RESULT_FAILED, null);
+                TestResult.TEST_RESULT_FAILED, null /*testDetails*/, null /*testMetrics*/);
     }
 
     @Override
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsProvider.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsProvider.java
index df05519..a9f672e 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsProvider.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsProvider.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.verifier;
 
+import com.android.compatibility.common.util.ReportLog;
+
 import android.app.backup.BackupManager;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
@@ -28,6 +30,10 @@
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+
 /** {@link ContentProvider} that provides read and write access to the test results. */
 public class TestResultsProvider extends ContentProvider {
 
@@ -56,6 +62,9 @@
     /** String containing the test's details. */
     static final String COLUMN_TEST_DETAILS = "testdetails";
 
+    /** ReportLog containing the test result metrics. */
+    static final String COLUMN_TEST_METRICS = "testmetrics";
+
     private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
     private static final int RESULTS_ALL = 1;
     private static final int RESULTS_ID = 2;
@@ -96,7 +105,8 @@
                     + COLUMN_TEST_NAME + " TEXT, "
                     + COLUMN_TEST_RESULT + " INTEGER,"
                     + COLUMN_TEST_INFO_SEEN + " INTEGER DEFAULT 0,"
-                    + COLUMN_TEST_DETAILS + " TEXT);");
+                    + COLUMN_TEST_DETAILS + " TEXT,"
+                    + COLUMN_TEST_METRICS + " BLOB);");
         }
 
         @Override
@@ -202,11 +212,12 @@
     }
 
     static void setTestResult(Context context, String testName, int testResult,
-            String testDetails) {
+            String testDetails, ReportLog reportLog) {
         ContentValues values = new ContentValues(2);
         values.put(TestResultsProvider.COLUMN_TEST_RESULT, testResult);
         values.put(TestResultsProvider.COLUMN_TEST_NAME, testName);
         values.put(TestResultsProvider.COLUMN_TEST_DETAILS, testDetails);
+        values.put(TestResultsProvider.COLUMN_TEST_METRICS, serialize(reportLog));
 
         ContentResolver resolver = context.getContentResolver();
         int numUpdated = resolver.update(TestResultsProvider.RESULTS_CONTENT_URI, values,
@@ -218,4 +229,24 @@
         }
     }
 
+    private static byte[] serialize(Object o) {
+        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+        ObjectOutputStream objectOutput = null;
+        try {
+            objectOutput = new ObjectOutputStream(byteStream);
+            objectOutput.writeObject(o);
+            return byteStream.toByteArray();
+        } catch (IOException e) {
+            return null;
+        } finally {
+            try {
+                if (objectOutput != null) {
+                    objectOutput.close();
+                }
+                byteStream.close();
+            } catch (IOException e) {
+                // Ignore close exception.
+            }
+        }
+    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsReport.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsReport.java
index e40b428..dc2502c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsReport.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsReport.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.verifier;
 
+import com.android.compatibility.common.util.MetricsXmlSerializer;
+import com.android.compatibility.common.util.ReportLog;
 import com.android.cts.verifier.TestListAdapter.TestListItem;
 
 import org.xmlpull.v1.XmlSerializer;
@@ -128,6 +130,12 @@
                     xml.endTag(null, TEST_DETAILS_TAG);
                 }
 
+                ReportLog reportLog = mAdapter.getReportLog(i);
+                if (reportLog != null) {
+                    MetricsXmlSerializer metricsXmlSerializer = new MetricsXmlSerializer(xml);
+                    metricsXmlSerializer.serialize(reportLog);
+                }
+
                 xml.endTag(null, TEST_TAG);
             }
         }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/projection/offscreen/ProjectionOffscreenActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/projection/offscreen/ProjectionOffscreenActivity.java
index 510a03b..c202bb1 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/projection/offscreen/ProjectionOffscreenActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/projection/offscreen/ProjectionOffscreenActivity.java
@@ -35,6 +35,7 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.Vibrator;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -89,6 +90,7 @@
             Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
             Ringtone r = RingtoneManager.getRingtone(getApplicationContext(), notification);
             r.play();
+            ((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(1000);
         }
     };
 
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sample/SampleTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sample/SampleTestActivity.java
index 25f90d9..41bc303 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sample/SampleTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sample/SampleTestActivity.java
@@ -16,8 +16,12 @@
 
 package com.android.cts.verifier.sample;
 
+import com.android.compatibility.common.util.ReportLog;
+import com.android.compatibility.common.util.ResultType;
+import com.android.compatibility.common.util.ResultUnit;
 import com.android.cts.verifier.PassFailButtons;
 import com.android.cts.verifier.R;
+import com.android.cts.verifier.TestResult;
 
 import android.content.Intent;
 import android.net.Uri;
@@ -61,6 +65,7 @@
             public void onClick(View v) {
                 try {
                     createFileAndShare();
+                    recordMetricsExample();
                 } catch (Exception e) {
                     e.printStackTrace();
                 }
@@ -68,6 +73,21 @@
         });
     }
 
+    private void recordMetricsExample() {
+        double[] metricValues = new double[] {1, 11, 21, 1211, 111221};
+
+        // Record metric results
+        getReportLog().setSummary(
+                "Sample Summary", 1.0, ResultType.HIGHER_BETTER, ResultUnit.BYTE);
+        getReportLog().addValues("Sample Values", metricValues, ResultType.NEUTRAL, ResultUnit.FPS);
+
+        // Alternatively, activities can invoke TestResult directly to record metrics
+        ReportLog reportLog = new PassFailButtons.CtsVerifierReportLog();
+        reportLog.setSummary("Sample Summary", 1.0, ResultType.HIGHER_BETTER, ResultUnit.BYTE);
+        getReportLog().addValues("Sample Values", metricValues, ResultType.NEUTRAL, ResultUnit.FPS);
+        TestResult.setPassedResult(this, "manualSample", "manualDetails", reportLog);
+    }
+
     /**
      * Creates a temporary file containing the test string and then issues the intent to share it.
      *
diff --git a/apps/NotificationBot/Android.mk b/apps/NotificationBot/Android.mk
new file mode 100644
index 0000000..9d9c9f9
--- /dev/null
+++ b/apps/NotificationBot/Android.mk
@@ -0,0 +1,36 @@
+#
+# 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-Iaidl-files-under, src)
+
+LOCAL_PACKAGE_NAME := NotificationBot
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/apps/NotificationBot/AndroidManifest.xml b/apps/NotificationBot/AndroidManifest.xml
new file mode 100644
index 0000000..b63791f
--- /dev/null
+++ b/apps/NotificationBot/AndroidManifest.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2010 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.android.cts.robot"
+      android:versionCode="1"
+      android:versionName="1.0">
+
+    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="21"/>
+
+    <application android:label="@string/app_name"
+            android:icon="@drawable/icon"
+            android:debuggable="true">
+
+        <!-- Required because a bare service won't show up in the app notifications list. -->
+        <activity android:name=".NotificationBotActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <!-- services used by the CtsVerifier to test notifications. -->
+        <receiver android:name=".NotificationBot" android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.cts.robot.ACTION_POST" />
+                <action android:name="com.android.cts.robot.ACTION_CANCEL" />
+            </intent-filter>
+        </receiver>
+
+
+    </application>
+
+</manifest>
diff --git a/apps/NotificationBot/proguard.flags b/apps/NotificationBot/proguard.flags
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/NotificationBot/proguard.flags
diff --git a/apps/NotificationBot/res/drawable-hdpi/icon.png b/apps/NotificationBot/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000..ecaabbe
--- /dev/null
+++ b/apps/NotificationBot/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/apps/NotificationBot/res/drawable-ldpi/icon.png b/apps/NotificationBot/res/drawable-ldpi/icon.png
new file mode 100644
index 0000000..f2de61f
--- /dev/null
+++ b/apps/NotificationBot/res/drawable-ldpi/icon.png
Binary files differ
diff --git a/apps/NotificationBot/res/drawable-mdpi/icon.png b/apps/NotificationBot/res/drawable-mdpi/icon.png
new file mode 100644
index 0000000..4950761
--- /dev/null
+++ b/apps/NotificationBot/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/apps/NotificationBot/res/drawable-xhdpi/icon.png b/apps/NotificationBot/res/drawable-xhdpi/icon.png
new file mode 100644
index 0000000..9b39cfb
--- /dev/null
+++ b/apps/NotificationBot/res/drawable-xhdpi/icon.png
Binary files differ
diff --git a/apps/NotificationBot/res/drawable-xxhdpi/icon.png b/apps/NotificationBot/res/drawable-xxhdpi/icon.png
new file mode 100644
index 0000000..b944c10
--- /dev/null
+++ b/apps/NotificationBot/res/drawable-xxhdpi/icon.png
Binary files differ
diff --git a/apps/NotificationBot/res/layout/main.xml b/apps/NotificationBot/res/layout/main.xml
new file mode 100644
index 0000000..bf84fa9
--- /dev/null
+++ b/apps/NotificationBot/res/layout/main.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent"
+             android:orientation="vertical"
+        >
+
+    <Space android:layout_width="match_parent"
+           android:layout_height="0dp"
+           android:layout_weight="1"
+            />
+    <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/call_to_action"
+            android:textAlignment="center"
+            />
+    <Space android:layout_width="match_parent"
+           android:layout_height="0dp"
+           android:layout_weight="1"
+            />
+</LinearLayout>
\ No newline at end of file
diff --git a/apps/NotificationBot/res/values/strings.xml b/apps/NotificationBot/res/values/strings.xml
new file mode 100644
index 0000000..866a9ec
--- /dev/null
+++ b/apps/NotificationBot/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+    <string name="app_name">CTS Robot</string>
+    <string name="call_to_action">Nothing to do here,\nPlease run the CTSVerifier App instead.</string>
+</resources>
\ No newline at end of file
diff --git a/apps/NotificationBot/src/com/android/cts/robot/NotificationBot.java b/apps/NotificationBot/src/com/android/cts/robot/NotificationBot.java
new file mode 100644
index 0000000..2aa5f41
--- /dev/null
+++ b/apps/NotificationBot/src/com/android/cts/robot/NotificationBot.java
@@ -0,0 +1,67 @@
+/*
+ * 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.cts.robot;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+
+public class NotificationBot extends BroadcastReceiver {
+    private static final String TAG = "NotificationBot";
+    private static final String EXTRA_ID = "ID";
+    private static final String EXTRA_NOTIFICATION = "NOTIFICATION";
+    private static final String ACTION_POST = "com.android.cts.robot.ACTION_POST";
+    private static final String ACTION_CANCEL = "com.android.cts.robot.ACTION_CANCEL";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.i(TAG, "received intent: " + intent);
+        if (ACTION_POST.equals(intent.getAction())) {
+            Log.i(TAG, ACTION_POST);
+            if (!intent.hasExtra(EXTRA_NOTIFICATION) || !intent.hasExtra(EXTRA_ID)) {
+                Log.e(TAG, "received post action with missing content");
+                return;
+            }
+            int id = intent.getIntExtra(EXTRA_ID, -1);
+            Log.i(TAG, "id: " + id);
+            Notification n = (Notification) intent.getParcelableExtra(EXTRA_NOTIFICATION);
+            Log.i(TAG, "n: " + n);
+            NotificationManager noMa =
+                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+            noMa.notify(id, n);
+
+        } else if (ACTION_CANCEL.equals(intent.getAction())) {
+            Log.i(TAG, ACTION_CANCEL);
+            int id = intent.getIntExtra(EXTRA_ID, -1);
+            Log.i(TAG, "id: " + id);
+            if (id < 0) {
+                Log.e(TAG, "received cancel action with no ID");
+                return;
+            }
+            NotificationManager noMa =
+                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+            noMa.cancel(id);
+
+        } else {
+            Log.i(TAG, "received unexpected action: " + intent.getAction());
+        }
+    }
+}
diff --git a/common/util/tests/src/com/android/compatibility/common/util/CommonUtilTest.java b/apps/NotificationBot/src/com/android/cts/robot/NotificationBotActivity.java
similarity index 64%
copy from common/util/tests/src/com/android/compatibility/common/util/CommonUtilTest.java
copy to apps/NotificationBot/src/com/android/cts/robot/NotificationBotActivity.java
index a376373..1b9408e 100644
--- a/common/util/tests/src/com/android/compatibility/common/util/CommonUtilTest.java
+++ b/apps/NotificationBot/src/com/android/cts/robot/NotificationBotActivity.java
@@ -13,13 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.cts.robot;
 
-package com.android.compatibility.common.util;
+import android.app.Activity;
+import android.os.Bundle;
+import com.android.cts.robot.R;
 
-import junit.framework.TestCase;
-
-public class CommonUtilTest extends TestCase {
-
-    // TODO(stuartscott): Add tests when there is something to test.
-
-}
+public class NotificationBotActivity extends Activity {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main);
+    }
+}
\ No newline at end of file
diff --git a/common/util/Android.mk b/common/util/Android.mk
index b7842559..84ced65 100644
--- a/common/util/Android.mk
+++ b/common/util/Android.mk
@@ -42,6 +42,8 @@
 
 LOCAL_MODULE := compatibility-common-util-hostsidelib_v2
 
+LOCAL_STATIC_JAVA_LIBRARIES := kxml2-2.3.0
+
 include $(BUILD_HOST_JAVA_LIBRARY)
 
 ###############################################################################
@@ -52,7 +54,10 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, tests/src)
 
-LOCAL_JAVA_LIBRARIES := junit
+LOCAL_STATIC_JAVA_LIBRARIES := \
+                        junit \
+                        kxml2-2.3.0 \
+                        compatibility-common-util-hostsidelib_v2
 
 LOCAL_MODULE := compatibility-common-util-tests_v2
 
diff --git a/common/util/run_unit_tests.sh b/common/util/run_unit_tests.sh
new file mode 100755
index 0000000..04a6745
--- /dev/null
+++ b/common/util/run_unit_tests.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+
+# 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.
+
+# helper script for running the cts common unit tests
+
+checkFile() {
+    if [ ! -f "$1" ]; then
+        echo "Unable to locate $1"
+        exit
+    fi;
+}
+
+# check if in Android build env
+if [ ! -z ${ANDROID_BUILD_TOP} ]; then
+    HOST=`uname`
+    if [ "$HOST" == "Linux" ]; then
+        OS="linux-x86"
+    elif [ "$HOST" == "Darwin" ]; then
+        OS="darwin-x86"
+    else
+        echo "Unrecognized OS"
+        exit
+    fi;
+fi;
+
+JAR_DIR=${ANDROID_BUILD_TOP}/out/host/$OS/framework
+JARS="tradefed-prebuilt.jar compatibility-common-util-hostsidelib_v2.jar compatibility-common-util-tests_v2.jar"
+
+for JAR in $JARS; do
+    checkFile ${JAR_DIR}/${JAR}
+    JAR_PATH=${JAR_PATH}:${JAR_DIR}/${JAR}
+done
+
+java $RDBG_FLAG \
+  -cp ${JAR_PATH} com.android.tradefed.command.Console run singleCommand host -n --class com.android.compatibility.common.util.UnitTests "$@"
+
diff --git a/common/util/src/com/android/compatibility/common/util/MetricsXmlSerializer.java b/common/util/src/com/android/compatibility/common/util/MetricsXmlSerializer.java
new file mode 100644
index 0000000..0e2b004
--- /dev/null
+++ b/common/util/src/com/android/compatibility/common/util/MetricsXmlSerializer.java
@@ -0,0 +1,70 @@
+/*
+ * 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.compatibility.common.util;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Serialize Metric data from {@link ReportLog} into compatibility report friendly XML
+ */
+public final class MetricsXmlSerializer {
+
+    private final XmlSerializer mXmlSerializer;
+
+    public MetricsXmlSerializer(XmlSerializer xmlSerializer) {
+        this.mXmlSerializer = xmlSerializer;
+    }
+
+    public void serialize(ReportLog reportLog) throws IOException {
+        if (reportLog == null) {
+            return;
+        }
+        ReportLog.Result summary = reportLog.getSummary();
+        List<ReportLog.Result> detailedMetrics = reportLog.getDetailedMetrics();
+        // <Summary message="Average" scoreType="lower_better" unit="ms">195.2</Summary>
+        if (summary != null) {
+            mXmlSerializer.startTag(null, "Summary");
+            mXmlSerializer.attribute(null, "message", summary.getMessage());
+            mXmlSerializer.attribute(null, "scoreType", summary.getType().getXmlString());
+            mXmlSerializer.attribute(null, "unit", summary.getUnit().getXmlString());
+            mXmlSerializer.text(Double.toString(summary.getValues()[0]));
+            mXmlSerializer.endTag(null, "Summary");
+        }
+
+        if (!detailedMetrics.isEmpty()) {
+            mXmlSerializer.startTag(null, "Details");
+            for (ReportLog.Result result : detailedMetrics) {
+                mXmlSerializer.startTag(null, "ValueArray");
+                mXmlSerializer.attribute(null, "source", result.getLocation());
+                mXmlSerializer.attribute(null, "message", result.getMessage());
+                mXmlSerializer.attribute(null, "scoreType", result.getType().getXmlString());
+                mXmlSerializer.attribute(null, "unit", result.getUnit().getXmlString());
+
+                for (double value : result.getValues()) {
+                    mXmlSerializer.startTag(null, "Value");
+                    mXmlSerializer.text(Double.toString(value));
+                    mXmlSerializer.endTag(null, "Value");
+                }
+                mXmlSerializer.endTag(null, "ValueArray");
+            }
+            mXmlSerializer.endTag(null, "Details");
+        }
+    }
+}
diff --git a/common/util/src/com/android/compatibility/common/util/ReportLog.java b/common/util/src/com/android/compatibility/common/util/ReportLog.java
index 9e733e4..8cfc086 100644
--- a/common/util/src/com/android/compatibility/common/util/ReportLog.java
+++ b/common/util/src/com/android/compatibility/common/util/ReportLog.java
@@ -16,6 +16,9 @@
 
 package com.android.compatibility.common.util;
 
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
@@ -28,8 +31,8 @@
     private Result mSummary;
     private final List<Result> mDetails = new ArrayList<Result>();
 
-    private class Result implements Serializable {
-        private static final int BASE_DEPTH = 2;// 0:constructor, 1:addValues/setSummary, 2:caller
+    class Result implements Serializable {
+        private static final int CALLER_STACKTRACE_DEPTH = 5;
         private String mLocation;
         private String mMessage;
         private double[] mValues;
@@ -53,15 +56,35 @@
         private Result(String message, double[] values, ResultType type,
                 ResultUnit unit, int depth) {
             final StackTraceElement[] trace = Thread.currentThread().getStackTrace();
-            final StackTraceElement e = trace[Math.min(BASE_DEPTH + depth, trace.length - 1)];
-            mLocation = String.format("%s#%s:%d",
-                    e.getClassName(), e.getMethodName(), e.getLineNumber());
+            final StackTraceElement e =
+                    trace[Math.min(CALLER_STACKTRACE_DEPTH + depth, trace.length - 1)];
+            mLocation = String.format(
+                    "%s#%s:%d", e.getClassName(), e.getMethodName(), e.getLineNumber());
             mMessage = message;
             mValues = values;
             mType = type;
             mUnit = unit;
         }
 
+        public String getLocation() {
+            return mLocation;
+        }
+
+        public String getMessage() {
+            return mMessage;
+        }
+
+        public double[] getValues() {
+            return mValues;
+        }
+
+        public ResultType getType() {
+            return mType;
+        }
+
+        public ResultUnit getUnit() {
+            return mUnit;
+        }
     }
 
     /**
@@ -108,4 +131,12 @@
             ResultUnit unit, int depth) {
         mSummary = new Result(message, new double[] {value}, type, unit, depth);
     }
+
+    public Result getSummary() {
+        return mSummary;
+    }
+
+    public List<Result> getDetailedMetrics() {
+        return new ArrayList<Result>(mDetails);
+    }
 }
diff --git a/common/util/tests/src/com/android/compatibility/common/util/MetricsXmlSerializerTest.java b/common/util/tests/src/com/android/compatibility/common/util/MetricsXmlSerializerTest.java
new file mode 100644
index 0000000..70da820
--- /dev/null
+++ b/common/util/tests/src/com/android/compatibility/common/util/MetricsXmlSerializerTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.compatibility.common.util;
+
+import junit.framework.TestCase;
+
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Unit tests for {@link MetricsXmlSerializer}
+ */
+public class MetricsXmlSerializerTest extends TestCase {
+
+    static class LocalReportLog extends ReportLog {}
+    private static final double[] VALUES = new double[] {1, 11, 21, 1211, 111221};
+    private static final String HEADER = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>";
+    private static final String EXPECTED_XML =
+            HEADER
+            + "<Summary message=\"Sample\" scoreType=\"higher_better\" unit=\"byte\">1.0</Summary>"
+            + "<Details>"
+                    + "<ValueArray source=\"sun.reflect.NativeMethodAccessorImpl#invoke0:-2\""
+                    + " message=\"Details\" scoreType=\"neutral\" unit=\"fps\">"
+                        + "<Value>1.0</Value>"
+                        + "<Value>11.0</Value>"
+                        + "<Value>21.0</Value>"
+                        + "<Value>1211.0</Value>"
+                        + "<Value>111221.0</Value>"
+                    + "</ValueArray>"
+            + "</Details>";
+
+    private LocalReportLog mLocalReportLog;
+    private MetricsXmlSerializer mMetricsXmlSerializer;
+    private ByteArrayOutputStream mByteArrayOutputStream;
+    private XmlSerializer xmlSerializer;
+
+    @Override
+    public void setUp() throws Exception {
+        mLocalReportLog = new LocalReportLog();
+        mByteArrayOutputStream = new ByteArrayOutputStream();
+        XmlPullParserFactory factory = XmlPullParserFactory.newInstance(null, null);
+        xmlSerializer = factory.newSerializer();
+        xmlSerializer.setOutput(mByteArrayOutputStream, "utf-8");
+
+        this.mMetricsXmlSerializer = new MetricsXmlSerializer(xmlSerializer);
+    }
+
+    public void testSerialize_null() throws IOException {
+        xmlSerializer.startDocument("utf-8", true);
+        mMetricsXmlSerializer.serialize(null);
+        xmlSerializer.endDocument();
+
+        assertEquals(HEADER.length(), mByteArrayOutputStream.toByteArray().length);
+    }
+
+    public void testSerialize_noData() throws IOException {
+        xmlSerializer.startDocument("utf-8", true);
+        mMetricsXmlSerializer.serialize(mLocalReportLog);
+        xmlSerializer.endDocument();
+
+        assertEquals(HEADER.length(), mByteArrayOutputStream.toByteArray().length);
+    }
+
+    public void testSerialize() throws IOException {
+        mLocalReportLog.setSummary("Sample", 1.0, ResultType.HIGHER_BETTER, ResultUnit.BYTE);
+        mLocalReportLog.addValues("Details", VALUES, ResultType.NEUTRAL, ResultUnit.FPS);
+
+        xmlSerializer.startDocument("utf-8", true);
+        mMetricsXmlSerializer.serialize(mLocalReportLog);
+        xmlSerializer.endDocument();
+
+        assertEquals(EXPECTED_XML, mByteArrayOutputStream.toString("utf-8"));
+    }
+}
diff --git a/common/util/tests/src/com/android/compatibility/common/util/CommonUtilTest.java b/common/util/tests/src/com/android/compatibility/common/util/UnitTests.java
similarity index 70%
rename from common/util/tests/src/com/android/compatibility/common/util/CommonUtilTest.java
rename to common/util/tests/src/com/android/compatibility/common/util/UnitTests.java
index a376373..b9a17e1 100644
--- a/common/util/tests/src/com/android/compatibility/common/util/CommonUtilTest.java
+++ b/common/util/tests/src/com/android/compatibility/common/util/UnitTests.java
@@ -11,15 +11,21 @@
  * 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.
+ * limitations under the License
  */
 
 package com.android.compatibility.common.util;
 
-import junit.framework.TestCase;
+import junit.framework.TestSuite;
 
-public class CommonUtilTest extends TestCase {
+/**
+ * A {@link TestSuite} for the common.util package.
+ */
+public class UnitTests extends TestSuite {
 
-    // TODO(stuartscott): Add tests when there is something to test.
+    public UnitTests() {
+        super();
 
+        addTestSuite(MetricsXmlSerializerTest.class);
+    }
 }
diff --git a/tests/tests/widget/src/android/widget/cts/MockPopupWindowCtsActivity.java b/tests/tests/widget/src/android/widget/cts/MockPopupWindowCtsActivity.java
index a68286a..41018a9 100644
--- a/tests/tests/widget/src/android/widget/cts/MockPopupWindowCtsActivity.java
+++ b/tests/tests/widget/src/android/widget/cts/MockPopupWindowCtsActivity.java
@@ -21,15 +21,37 @@
 import android.app.Activity;
 import android.os.Bundle;
 import android.widget.PopupWindow;
+import android.view.View;
+import android.view.View.OnApplyWindowInsetsListener;
+import android.view.Window;
+import android.view.WindowInsets;
 
 /**
  * Stub activity for testing {@link PopupWindow}
  */
 public class MockPopupWindowCtsActivity extends Activity {
+    private boolean isFirstRun = true;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setContentView(R.layout.popupwindow);
+        Window window = getWindow();
+        final View decor = window.getDecorView();
+        decor.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() {
+            @Override
+            public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+                if (isFirstRun) {
+                    if (insets.isRound()) {
+                        decor.setPadding(decor.getPaddingLeft(), decor.getPaddingTop(),
+                                decor.getPaddingRight(),
+                                decor.getPaddingBottom() + insets.getSystemWindowInsetBottom());
+                    }
+                    isFirstRun = false;
+                    setContentView(R.layout.popupwindow);
+                }
+                return insets.consumeSystemWindowInsets();
+            }
+        });
     }
 }