Theme tests for Material

Bug: 21488226
Change-Id: Ie22b8618a2824029641dd18a7f362b09fe68f800
diff --git a/hostsidetests/theme/Android.mk b/hostsidetests/theme/Android.mk
index 71027c7..188bf7a 100644
--- a/hostsidetests/theme/Android.mk
+++ b/hostsidetests/theme/Android.mk
@@ -29,6 +29,8 @@
 
 LOCAL_CTS_TEST_PACKAGE := android.host.theme
 
+LOCAL_SDK_VERSION := current
+
 include $(BUILD_CTS_HOST_JAVA_LIBRARY)
 
 include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/hostsidetests/theme/README b/hostsidetests/theme/README
new file mode 100644
index 0000000..bce711a
--- /dev/null
+++ b/hostsidetests/theme/README
@@ -0,0 +1,73 @@
+* Copyright (C) 2015 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.
+
+
+INTRODUCTION
+
+The Android theme tests ensure that the Holo and Material themes have not been
+modified. They consist of API-specific sets of reference images representing
+specific themes and widgets that must be identical across devices. To pass the
+theme tests, a device must be able to generate images that are identical to the
+reference images.
+
+NOTE: Reference images should only be updated by the CTS test maintainers. Any
+      modifications to the reference images will invalidate the test results.
+
+
+INSTRUCTIONS
+
+I. Generating reference images (CTS maintainers only)
+
+Reference images are typically only generated for new API revisions. To
+generate a new set of reference images, do the following:
+
+  1. Connect one device for each DPI bucket (ldpi, xxxhdpi, etc.) that you wish
+     to generate references images for. Confirm that all devices are connected
+     with:
+
+     adb devices
+
+  2. Image generation occurs on all devices in parallel. Resulting sets of
+     reference images are saved in assets/<api>/<dpi>.zip and will overwrite
+     any existing sets. Image generation may be started using:
+
+     .cts/hostsidetests/theme/generate_images.sh
+
+A complete collection of reference images for a given API revision must include
+a set for each possible DPI bucket (tvdpi, xxhdpi, etc.) that may be tested.
+
+
+II. Building theme tests
+
+1. If you have not already built the CTS tests, run an initial make:
+
+   make cts -j32
+
+2. Subsequent changes to the theme tests, including changes to the reference
+   images, may be built using mmm:
+
+   mmm cts/hostsidetests/theme -j32
+
+
+III. Running theme tests
+
+1. Connect the device that you wish to test. Confirm that is is connected with:
+
+   adb devices
+
+2. Run the theme tests using cts-tradefed:
+
+   cts-tradefed run cts -c android.theme.cts.ThemeHostTest
+
+3. Wait for the tests to complete. This should take less than five minutes.
diff --git a/hostsidetests/theme/android_device.py b/hostsidetests/theme/android_device.py
new file mode 100644
index 0000000..97b5fdd
--- /dev/null
+++ b/hostsidetests/theme/android_device.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2015 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.
+#
+
+import os
+import re
+import sys
+import threading
+import subprocess
+import time
+
+# class for running android device from python
+# it will fork the device processor
+class androidDevice(object):
+    def __init__(self, adbDevice):
+        self._adbDevice = adbDevice
+
+    def runAdbCommand(self, cmd):
+        self.waitForAdbDevice()
+        adbCmd = "adb -s %s %s" %(self._adbDevice, cmd)
+        adbProcess = subprocess.Popen(adbCmd.split(" "), bufsize = -1, stdout = subprocess.PIPE)
+        return adbProcess.communicate()
+
+    def runShellCommand(self, cmd):
+        return self.runAdbCommand("shell " + cmd)
+
+    def waitForAdbDevice(self):
+        os.system("adb -s %s wait-for-device" %self._adbDevice)
+
+    def waitForBootComplete(self, timeout = 240):
+        boot_complete = False
+        attempts = 0
+        wait_period = 5
+        while not boot_complete and (attempts*wait_period) < timeout:
+            (output, err) = self.runShellCommand("getprop dev.bootcomplete")
+            output = output.strip()
+            if output == "1":
+                boot_complete = True
+            else:
+                time.sleep(wait_period)
+                attempts += 1
+        if not boot_complete:
+            print "***boot not complete within timeout. will proceed to the next step"
+        return boot_complete
+
+    def installApk(self, apkPath):
+        (out, err) = self.runAdbCommand("install -r -d -g " + apkPath)
+        result = out.split()
+        return (out, err, "Success" in result)
+
+    def uninstallApk(self, package):
+        (out, err) = self.runAdbCommand("uninstall " + package)
+        result = out.split()
+        return "Success" in result
+
+    def runInstrumentationTest(self, option):
+        return self.runShellCommand("am instrument -w " + option)
+
+    def isProcessAlive(self, processName):
+        (out, err) = self.runShellCommand("ps")
+        names = out.split()
+        # very lazy implementation as it does not filter out things like uid
+        # should work mostly unless processName is too simple to overlap with
+        # uid. So only use name like com.android.xyz
+        return processName in names
+
+    def getDensity(self):
+        if "emulator" in self._adbDevice:
+          return int(self.runShellCommand("getprop qemu.sf.lcd_density")[0])
+        else:
+          return int(self.runShellCommand("getprop ro.sf.lcd_density")[0])
+
+    def getSdkLevel(self):
+        return int(self.runShellCommand("getprop ro.build.version.sdk")[0])
+
+    def getOrientation(self):
+        return int(self.runShellCommand("dumpsys | grep SurfaceOrientation")[0].split()[1])
+
+def runAdbDevices():
+    devices = subprocess.check_output(["adb", "devices"])
+    devices = devices.split('\n')[1:]
+
+    deviceSerial = []
+
+    for device in devices:
+        if device is not "":
+            info = device.split('\t')
+            if info[1] == "device":
+                deviceSerial.append(info[0])
+
+    return deviceSerial
+
+if __name__ == '__main__':
+    main(sys.argv)
diff --git a/hostsidetests/theme/app/Android.mk b/hostsidetests/theme/app/Android.mk
index 70623cb..1be2983 100644
--- a/hostsidetests/theme/app/Android.mk
+++ b/hostsidetests/theme/app/Android.mk
@@ -26,6 +26,8 @@
 
 LOCAL_PROGUARD_ENABLED := disabled
 
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 #Flags to tell the Android Asset Packaging Tool not to strip for some densities
diff --git a/hostsidetests/theme/app/AndroidManifest.xml b/hostsidetests/theme/app/AndroidManifest.xml
index 81a4d9d..4e81ab0 100755
--- a/hostsidetests/theme/app/AndroidManifest.xml
+++ b/hostsidetests/theme/app/AndroidManifest.xml
@@ -22,9 +22,10 @@
 
     <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
     <application>
         <uses-library android:name="android.test.runner" />
-        <activity android:name=".HoloDeviceActivity">
+        <activity android:name=".ThemeDeviceActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -37,6 +38,13 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <activity android:name=".GenerateImagesActivity"
+                  android:exported="true" />
     </application>
 
+    <!--  self-instrumenting test package. -->
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.theme.app"
+                     android:label="Generates Theme reference images"/>
+
 </manifest>
diff --git a/hostsidetests/theme/app/res/layout/holo_test.xml b/hostsidetests/theme/app/res/layout/theme_test.xml
similarity index 100%
rename from hostsidetests/theme/app/res/layout/holo_test.xml
rename to hostsidetests/theme/app/res/layout/theme_test.xml
diff --git a/hostsidetests/theme/app/res/values/strings.xml b/hostsidetests/theme/app/res/values/strings.xml
index a69a2e0..d9d6602 100644
--- a/hostsidetests/theme/app/res/values/strings.xml
+++ b/hostsidetests/theme/app/res/values/strings.xml
@@ -14,8 +14,6 @@
      limitations under the License.
 -->
 <resources>
-    <string name="holo_test_utilities">Holo Test Utilities</string>
-
     <string name="display_info">Display Info</string>
     <string name="display_info_text">Density DPI: %1$d\nDensity Bucket: %2$s\nWidth DP: %3$d\nHeight DP: %4$d</string>
 
diff --git a/hostsidetests/theme/app/src/android/theme/app/DisplayInfoActivity.java b/hostsidetests/theme/app/src/android/theme/app/DisplayInfoActivity.java
index 5255698..530675d 100644
--- a/hostsidetests/theme/app/src/android/theme/app/DisplayInfoActivity.java
+++ b/hostsidetests/theme/app/src/android/theme/app/DisplayInfoActivity.java
@@ -25,7 +25,8 @@
 import android.widget.TextView;
 
 /**
- * An activity to display information about the device, including density bucket and dimensions.
+ * An activity to display information about the device, including density
+ * bucket and dimensions.
  */
 public class DisplayInfoActivity extends Activity {
 
diff --git a/hostsidetests/theme/app/src/android/theme/app/GenerateBitmapTask.java b/hostsidetests/theme/app/src/android/theme/app/GenerateBitmapTask.java
new file mode 100644
index 0000000..05b6dcf
--- /dev/null
+++ b/hostsidetests/theme/app/src/android/theme/app/GenerateBitmapTask.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2015 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 android.theme.app;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.Canvas;
+import android.os.AsyncTask;
+import android.os.Environment;
+import android.util.Log;
+import android.view.View;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+/**
+ * A task which gets the UI element to render to a bitmap and then saves that
+ * as a PNG asynchronously.
+ */
+class GenerateBitmapTask extends AsyncTask<Void, Void, Boolean> {
+    private static final String TAG = "GenerateBitmapTask";
+
+    private final View mView;
+    private final File mOutDir;
+
+    private Bitmap mBitmap;
+
+    protected final String mName;
+
+    public GenerateBitmapTask(View view, File outDir, String name) {
+        mView = view;
+        mOutDir = outDir;
+        mName = name;
+    }
+
+    @Override
+    protected void onPreExecute() {
+        if (mView.getWidth() == 0 || mView.getHeight() == 0) {
+            Log.e(TAG, "Unable to draw view due to incorrect size: " + mName);
+            return;
+        }
+
+        mBitmap = Bitmap.createBitmap(mView.getWidth(), mView.getHeight(),
+                Bitmap.Config.ARGB_8888);
+
+        final Canvas canvas = new Canvas(mBitmap);
+        mView.draw(canvas);
+    }
+
+    @Override
+    protected Boolean doInBackground(Void... ignored) {
+        final Bitmap bitmap = mBitmap;
+        if (bitmap == null) {
+            return false;
+        }
+
+        final File file = new File(mOutDir, mName + ".png");
+        if (file.exists() && !file.canWrite()) {
+            Log.e(TAG, "Unable to write file: " + file.getAbsolutePath());
+            return false;
+        }
+
+        boolean success = false;
+        try {
+            FileOutputStream stream = null;
+            try {
+                stream = new FileOutputStream(file);
+                success = bitmap.compress(CompressFormat.PNG, 100, stream);
+            } finally {
+                if (stream != null) {
+                    stream.flush();
+                    stream.close();
+                }
+            }
+        } catch (Exception e) {
+            Log.e(TAG, e.getMessage());
+        } finally {
+            bitmap.recycle();
+        }
+
+        return success;
+    }
+}
diff --git a/hostsidetests/theme/app/src/android/theme/app/GenerateImagesActivity.java b/hostsidetests/theme/app/src/android/theme/app/GenerateImagesActivity.java
new file mode 100644
index 0000000..e7f5aa2
--- /dev/null
+++ b/hostsidetests/theme/app/src/android/theme/app/GenerateImagesActivity.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2015 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 android.theme.app;
+
+import android.Manifest.permission;
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build.VERSION;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.util.Log;
+import android.view.WindowManager.LayoutParams;
+
+import java.io.File;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Generates images by iterating through all themes and launching instances of
+ * {@link ThemeDeviceActivity}.
+ */
+public class GenerateImagesActivity extends Activity {
+    private static final String TAG = "GenerateImagesActivity";
+
+    private static final String OUT_DIR = "cts-theme-assets";
+    private static final int REQUEST_CODE = 1;
+
+    private final CountDownLatch mLatch = new CountDownLatch(1);
+
+    private File mOutputDir;
+    private int mCurrentTheme;
+    private String mFinishReason;
+    private boolean mFinishSuccess;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON
+                | LayoutParams.FLAG_TURN_SCREEN_ON
+                | LayoutParams.FLAG_DISMISS_KEYGUARD);
+
+        mOutputDir = new File(Environment.getExternalStorageDirectory(), OUT_DIR);
+        ThemeTestUtils.deleteDirectory(mOutputDir);
+        mOutputDir.mkdirs();
+
+        if (!mOutputDir.exists()) {
+            finish("Failed to create output directory " + mOutputDir.getAbsolutePath(), false);
+            return;
+        }
+
+        final boolean canDisableKeyguard = checkCallingOrSelfPermission(
+                permission.DISABLE_KEYGUARD) == PackageManager.PERMISSION_GRANTED;
+        if (!canDisableKeyguard) {
+            finish("Not granted permission to disable keyguard", false);
+            return;
+        }
+
+        new KeyguardCheck(this) {
+            @Override
+            public void onSuccess() {
+                generateNextImage();
+            }
+
+            @Override
+            public void onFailure() {
+                finish("Device is locked", false);
+            }
+        }.start();
+    }
+
+    private void finish(String reason, boolean success) {
+        mFinishSuccess = success;
+        mFinishReason = reason;
+
+        Log.i(TAG, (success ? "OKAY" : "FAIL") + ":" + reason);
+        finish();
+    }
+
+    public boolean isFinishSuccess() {
+        return mFinishSuccess;
+    }
+
+    public String getFinishReason() {
+        return mFinishReason;
+    }
+
+    static abstract class KeyguardCheck implements Runnable {
+        private static final int MAX_RETRIES = 3;
+        private static final int RETRY_DELAY = 500;
+
+        private final Handler mHandler;
+        private final KeyguardManager mKeyguard;
+
+        private int mRetries;
+
+        public KeyguardCheck(Context context) {
+            mHandler = new Handler(context.getMainLooper());
+            mKeyguard = (KeyguardManager) context.getSystemService(KEYGUARD_SERVICE);
+        }
+
+        public void start() {
+            mRetries = 0;
+
+            mHandler.removeCallbacks(this);
+            mHandler.post(this);
+        }
+
+        public void cancel() {
+            mHandler.removeCallbacks(this);
+        }
+
+        @Override
+        public void run() {
+            if (!mKeyguard.isKeyguardLocked()) {
+                onSuccess();
+            } else if (mRetries < MAX_RETRIES) {
+                mRetries++;
+                mHandler.postDelayed(this, RETRY_DELAY);
+            } else {
+                onFailure();
+            }
+
+        }
+
+        public abstract void onSuccess();
+        public abstract void onFailure();
+    }
+
+    public File getOutputDir() {
+        return mOutputDir;
+    }
+
+    /**
+     * Starts the activity to generate the next image.
+     */
+    private boolean generateNextImage() {
+        final ThemeDeviceActivity.Theme theme = ThemeDeviceActivity.THEMES[mCurrentTheme];
+        if (theme.apiLevel > VERSION.SDK_INT) {
+            Log.v(TAG, "Skipping theme \"" + theme.name
+                    + "\" (requires API " + theme.apiLevel + ")");
+            return false;
+        }
+
+        Log.v(TAG, "Generating images for theme \"" + theme.name + "\"...");
+
+        final Intent intent = new Intent(this, ThemeDeviceActivity.class);
+        intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+        intent.putExtra(ThemeDeviceActivity.EXTRA_THEME, mCurrentTheme);
+        intent.putExtra(ThemeDeviceActivity.EXTRA_OUTPUT_DIR, mOutputDir.getAbsolutePath());
+        startActivityForResult(intent, REQUEST_CODE);
+        return true;
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (resultCode != RESULT_OK) {
+            Log.i(TAG, "FAIL:Failed to generate images for theme " + mCurrentTheme);
+            finish();
+            return;
+        }
+
+        // Keep trying themes until one works.
+        boolean success = false;
+        while (++mCurrentTheme < ThemeDeviceActivity.THEMES.length && !success) {
+            success = generateNextImage();
+        }
+
+        // If we ran out of themes, we're done.
+        if (!success) {
+            finish("Image generation complete!", true);
+        }
+    }
+
+    public void finish() {
+        mLatch.countDown();
+        super.finish();
+    }
+
+    public void waitForCompletion() throws InterruptedException {
+        mLatch.await();
+    }
+}
diff --git a/hostsidetests/theme/app/src/android/theme/app/HoloDeviceActivity.java b/hostsidetests/theme/app/src/android/theme/app/HoloDeviceActivity.java
deleted file mode 100644
index 8ae9fc8..0000000
--- a/hostsidetests/theme/app/src/android/theme/app/HoloDeviceActivity.java
+++ /dev/null
@@ -1,331 +0,0 @@
-/*
- * 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 android.theme.app;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.CompressFormat;
-import android.graphics.Canvas;
-import android.os.AsyncTask;
-import android.os.Environment;
-import android.os.Bundle;
-import android.os.Handler;
-import android.theme.app.modifiers.DatePickerModifier;
-import android.theme.app.modifiers.ProgressBarModifier;
-import android.theme.app.modifiers.SearchViewModifier;
-import android.theme.app.modifiers.TimePickerModifier;
-import android.theme.app.modifiers.ViewCheckedModifier;
-import android.theme.app.modifiers.ViewPressedModifier;
-import android.theme.app.R;
-import android.theme.app.ReferenceViewGroup;
-import android.util.Log;
-import android.view.View;
-import android.widget.CheckBox;
-import android.widget.DatePicker;
-import android.widget.LinearLayout;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.lang.Override;
-
-/**
- * A activity which display various UI elements with Holo theme.
- */
-public class HoloDeviceActivity extends Activity {
-
-    public static final String EXTRA_THEME = "holo_theme_extra";
-
-    private static final String TAG = HoloDeviceActivity.class.getSimpleName();
-
-    /**
-     * The duration of the CalendarView adjustement to settle to its final position.
-     */
-    private static final long CALENDAR_VIEW_ADJUSTMENT_DURATION = 540;
-
-    private Theme mTheme;
-
-    private ReferenceViewGroup mViewGroup;
-
-    private int mLayoutIndex;
-
-    @Override
-    protected void onCreate(Bundle icicle) {
-        super.onCreate(icicle);
-
-        mTheme = THEMES[getIntent().getIntExtra(EXTRA_THEME, 0)];
-        setTheme(mTheme.mId);
-        setContentView(R.layout.holo_test);
-        mViewGroup = (ReferenceViewGroup) findViewById(R.id.reference_view_group);
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        setNextLayout();
-    }
-
-    @Override
-    protected void onPause() {
-        if (!isFinishing()) {
-            // The Activity got paused for some reasons, for finish it as the host won't move on to
-            // the next theme otherwise.
-            Log.w(TAG, "onPause called without a call to finish().");
-            finish();
-        }
-        super.onPause();
-    }
-
-    @Override
-    protected void onDestroy() {
-        if (mLayoutIndex != LAYOUTS.length) {
-            Log.w(TAG, "Not all layouts got rendered: " + mLayoutIndex);
-        }
-        Log.i(TAG, "OKAY:" + mTheme.mName);
-        super.onDestroy();
-    }
-
-    /**
-     * Sets the next layout in the UI.
-     */
-    private void setNextLayout() {
-        if (mLayoutIndex >= LAYOUTS.length) {
-            finish();
-            return;
-        }
-        final Layout layout = LAYOUTS[mLayoutIndex++];
-        final String layoutName = String.format("%s_%s", mTheme.mName, layout.mName);
-
-        mViewGroup.removeAllViews();
-        final View view = getLayoutInflater().inflate(layout.mId, mViewGroup, false);
-        if (layout.mModifier != null) {
-            layout.mModifier.modifyView(view);
-        }
-        mViewGroup.addView(view);
-        view.setFocusable(false);
-
-        final Runnable generateBitmapRunnable = new Runnable() {
-            @Override
-            public void run() {
-                new GenerateBitmapTask(view, layoutName).execute();
-            }
-        };
-
-        if (view instanceof DatePicker) {
-            // DatePicker uses a CalendarView that has a non-configurable adjustment duration of
-            // 540ms
-            view.postDelayed(generateBitmapRunnable, CALENDAR_VIEW_ADJUSTMENT_DURATION);
-        } else {
-            view.post(generateBitmapRunnable);
-        }
-    }
-
-    /**
-     * A task which gets the UI element to render to a bitmap and then saves that as a png
-     * asynchronously
-     */
-    private class GenerateBitmapTask extends AsyncTask<Void, Void, Boolean> {
-
-        private final View mView;
-
-        private final String mName;
-
-        public GenerateBitmapTask(final View view, final String name) {
-            super();
-            mView = view;
-            mName = name;
-        }
-
-        @Override
-        protected Boolean doInBackground(Void... ignored) {
-            if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
-                Log.i(TAG, "External storage for saving bitmaps is not mounted");
-                return false;
-            }
-            if (mView.getWidth() == 0 || mView.getHeight() == 0) {
-                Log.w(TAG, "Unable to draw View due to incorrect size: " + mName);
-                return false;
-            }
-
-            final Bitmap bitmap = Bitmap.createBitmap(
-                    mView.getWidth(), mView.getHeight(), Bitmap.Config.ARGB_8888);
-            final Canvas canvas = new Canvas(bitmap);
-
-            mView.draw(canvas);
-            final File dir = new File(Environment.getExternalStorageDirectory(), "cts-holo-assets");
-            dir.mkdirs();
-            boolean success = false;
-            try {
-                final File file = new File(dir, mName + ".png");
-                FileOutputStream stream = null;
-                try {
-                    stream = new FileOutputStream(file);
-                    success = bitmap.compress(CompressFormat.PNG, 100, stream);
-                } finally {
-                    if (stream != null) {
-                        stream.close();
-                    }
-                }
-            } catch (Exception e) {
-                Log.e(TAG, e.getMessage());
-            } finally {
-                bitmap.recycle();
-            }
-            return success;
-        }
-
-        @Override
-        protected void onPostExecute(Boolean success) {
-            setNextLayout();
-        }
-    }
-
-    /**
-     * A class to encapsulate information about a holo theme.
-     */
-    private static class Theme {
-
-        public final int mId;
-
-        public final String mName;
-
-        private Theme(int id, String name) {
-            mId = id;
-            mName = name;
-        }
-    }
-
-    private static final Theme[] THEMES = {
-            new Theme(android.R.style.Theme_Holo,
-                    "holo"),
-            new Theme(android.R.style.Theme_Holo_Dialog,
-                    "holo_dialog"),
-            new Theme(android.R.style.Theme_Holo_Dialog_MinWidth,
-                    "holo_dialog_minwidth"),
-            new Theme(android.R.style.Theme_Holo_Dialog_NoActionBar,
-                    "holo_dialog_noactionbar"),
-            new Theme(android.R.style.Theme_Holo_Dialog_NoActionBar_MinWidth,
-                    "holo_dialog_noactionbar_minwidth"),
-            new Theme(android.R.style.Theme_Holo_DialogWhenLarge,
-                    "holo_dialogwhenlarge"),
-            new Theme(android.R.style.Theme_Holo_DialogWhenLarge_NoActionBar,
-                    "holo_dialogwhenlarge_noactionbar"),
-            new Theme(android.R.style.Theme_Holo_InputMethod,
-                    "holo_inputmethod"),
-            new Theme(android.R.style.Theme_Holo_Light,
-                    "holo_light"),
-            new Theme(android.R.style.Theme_Holo_Light_DarkActionBar,
-                    "holo_light_darkactionbar"),
-            new Theme(android.R.style.Theme_Holo_Light_Dialog,
-                    "holo_light_dialog"),
-            new Theme(android.R.style.Theme_Holo_Light_Dialog_MinWidth,
-                    "holo_light_dialog_minwidth"),
-            new Theme(android.R.style.Theme_Holo_Light_Dialog_NoActionBar,
-                    "holo_light_dialog_noactionbar"),
-            new Theme(android.R.style.Theme_Holo_Light_Dialog_NoActionBar_MinWidth,
-                    "holo_light_dialog_noactionbar_minwidth"),
-            new Theme(android.R.style.Theme_Holo_Light_DialogWhenLarge,
-                    "holo_light_dialogwhenlarge"),
-            new Theme(android.R.style.Theme_Holo_Light_DialogWhenLarge_NoActionBar,
-                    "holo_light_dialogwhenlarge_noactionbar"),
-            new Theme(android.R.style.Theme_Holo_Light_NoActionBar,
-                    "holo_light_noactionbar"),
-            new Theme(android.R.style.Theme_Holo_Light_NoActionBar_Fullscreen,
-                    "holo_light_noactionbar_fullscreen"),
-            new Theme(android.R.style.Theme_Holo_Light_Panel,
-                    "holo_light_panel"),
-            new Theme(android.R.style.Theme_Holo_NoActionBar,
-                    "holo_noactionbar"),
-            new Theme(android.R.style.Theme_Holo_NoActionBar_Fullscreen,
-                    "holo_noactionbar_fullscreen"),
-            new Theme(android.R.style.Theme_Holo_Panel,
-                    "holo_panel"),
-            new Theme(android.R.style.Theme_Holo_Wallpaper,
-                    "holo_wallpaper"),
-            new Theme(android.R.style.Theme_Holo_Wallpaper_NoTitleBar,
-                    "holo_wallpaper_notitlebar")
-    };
-
-    /**
-     * A class to encapsulate information about a holo layout.
-     */
-    private static class Layout {
-
-        public final int mId;
-
-        public final String mName;
-
-        public final LayoutModifier mModifier;
-
-        private Layout(int id, String name, LayoutModifier modifier) {
-            mId = id;
-            mName = name;
-            mModifier = modifier;
-        }
-    }
-
-    private static final Layout[] LAYOUTS = {
-            new Layout(R.layout.button, "button", null),
-            new Layout(R.layout.button, "button_pressed", new ViewPressedModifier()),
-            new Layout(R.layout.checkbox, "checkbox", null),
-            new Layout(R.layout.checkbox, "checkbox_checked", new ViewCheckedModifier()),
-            new Layout(R.layout.chronometer, "chronometer", null),
-            new Layout(R.layout.color_blue_bright, "color_blue_bright", null),
-            new Layout(R.layout.color_blue_dark, "color_blue_dark", null),
-            new Layout(R.layout.color_blue_light, "color_blue_light", null),
-            new Layout(R.layout.color_green_dark, "color_green_dark", null),
-            new Layout(R.layout.color_green_light, "color_green_light", null),
-            new Layout(R.layout.color_orange_dark, "color_orange_dark", null),
-            new Layout(R.layout.color_orange_light, "color_orange_light", null),
-            new Layout(R.layout.color_purple, "color_purple", null),
-            new Layout(R.layout.color_red_dark, "color_red_dark", null),
-            new Layout(R.layout.color_red_light, "color_red_light", null),
-            new Layout(R.layout.datepicker, "datepicker", new DatePickerModifier()),
-            new Layout(R.layout.display_info, "display_info", null),
-            new Layout(R.layout.edittext, "edittext", null),
-            new Layout(R.layout.progressbar_horizontal_0, "progressbar_horizontal_0", null),
-            new Layout(R.layout.progressbar_horizontal_100, "progressbar_horizontal_100", null),
-            new Layout(R.layout.progressbar_horizontal_50, "progressbar_horizontal_50", null),
-            new Layout(R.layout.progressbar_large, "progressbar_large", new ProgressBarModifier()),
-            new Layout(R.layout.progressbar_small, "progressbar_small", new ProgressBarModifier()),
-            new Layout(R.layout.progressbar, "progressbar", new ProgressBarModifier()),
-            new Layout(R.layout.radiobutton_checked, "radiobutton_checked", null),
-            new Layout(R.layout.radiobutton, "radiobutton", null),
-            new Layout(R.layout.radiogroup_horizontal, "radiogroup_horizontal", null),
-            new Layout(R.layout.radiogroup_vertical, "radiogroup_vertical", null),
-            new Layout(R.layout.ratingbar_0, "ratingbar_0", null),
-            new Layout(R.layout.ratingbar_2point5, "ratingbar_2point5", null),
-            new Layout(R.layout.ratingbar_5, "ratingbar_5", null),
-            new Layout(R.layout.ratingbar_0, "ratingbar_0_pressed", new ViewPressedModifier()),
-            new Layout(R.layout.ratingbar_2point5, "ratingbar_2point5_pressed", new ViewPressedModifier()),
-            new Layout(R.layout.ratingbar_5, "ratingbar_5_pressed", new ViewPressedModifier()),
-            new Layout(R.layout.searchview, "searchview_query", new SearchViewModifier(SearchViewModifier.QUERY)),
-            new Layout(R.layout.searchview, "searchview_query_hint", new SearchViewModifier(SearchViewModifier.QUERY_HINT)),
-            new Layout(R.layout.seekbar_0, "seekbar_0", null),
-            new Layout(R.layout.seekbar_100, "seekbar_100", null),
-            new Layout(R.layout.seekbar_50, "seekbar_50", null),
-            new Layout(R.layout.spinner, "spinner", null),
-            new Layout(R.layout.switch_button_checked, "switch_button_checked", null),
-            new Layout(R.layout.switch_button, "switch_button", null),
-            new Layout(R.layout.textview, "textview", null),
-            new Layout(R.layout.timepicker, "timepicker", new TimePickerModifier()),
-            new Layout(R.layout.togglebutton_checked, "togglebutton_checked", null),
-            new Layout(R.layout.togglebutton, "togglebutton", null),
-            new Layout(R.layout.zoomcontrols, "zoomcontrols", null),
-    };
-}
diff --git a/hostsidetests/theme/app/src/android/theme/app/ReferenceImagesTest.java b/hostsidetests/theme/app/src/android/theme/app/ReferenceImagesTest.java
new file mode 100644
index 0000000..7569252
--- /dev/null
+++ b/hostsidetests/theme/app/src/android/theme/app/ReferenceImagesTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 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 android.theme.app;
+
+import android.test.ActivityInstrumentationTestCase2;
+
+import java.io.File;
+
+/**
+ * Activity test case used to instrument generation of reference images.
+ */
+public class ReferenceImagesTest extends ActivityInstrumentationTestCase2<GenerateImagesActivity> {
+
+    public ReferenceImagesTest() {
+        super(GenerateImagesActivity.class);
+    }
+
+    public void testGenerateReferenceImages() throws Exception {
+        setActivityInitialTouchMode(true);
+
+        final GenerateImagesActivity activity = getActivity();
+        activity.waitForCompletion();
+
+        assertTrue(activity.getFinishReason(), activity.isFinishSuccess());
+
+        final File outputDir = activity.getOutputDir();
+        final File outputZip = new File(outputDir.getParentFile(), outputDir.getName() + ".zip");
+        if (outputZip.exists()) {
+            // Remove any old test results.
+            outputZip.delete();
+        }
+
+        ThemeTestUtils.compressDirectory(outputDir, outputZip);
+        ThemeTestUtils.deleteDirectory(outputDir);
+
+        assertTrue("Generated reference image ZIP", outputZip.exists());
+    }
+}
diff --git a/hostsidetests/theme/app/src/android/theme/app/ThemeDeviceActivity.java b/hostsidetests/theme/app/src/android/theme/app/ThemeDeviceActivity.java
new file mode 100644
index 0000000..d8b1f30
--- /dev/null
+++ b/hostsidetests/theme/app/src/android/theme/app/ThemeDeviceActivity.java
@@ -0,0 +1,414 @@
+/*
+ * Copyright (C) 2015 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 android.theme.app;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.theme.app.modifiers.DatePickerModifier;
+import android.theme.app.modifiers.ProgressBarModifier;
+import android.theme.app.modifiers.SearchViewModifier;
+import android.theme.app.modifiers.TimePickerModifier;
+import android.theme.app.modifiers.ViewCheckedModifier;
+import android.theme.app.modifiers.ViewPressedModifier;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager.LayoutParams;
+import android.widget.DatePicker;
+
+import java.io.File;
+import java.lang.Override;
+
+/**
+ * A activity which display various UI elements with non-modifiable themes.
+ */
+public class ThemeDeviceActivity extends Activity {
+    public static final String EXTRA_THEME = "theme";
+    public static final String EXTRA_OUTPUT_DIR = "outputDir";
+
+    private static final String TAG = "ThemeDeviceActivity";
+
+    /**
+     * The duration of the CalendarView adjustment to settle to its final
+     * position.
+     */
+    private static final long CALENDAR_VIEW_ADJUSTMENT_DURATION = 540;
+
+    private Theme mTheme;
+    private ReferenceViewGroup mViewGroup;
+    private File mOutputDir;
+    private int mLayoutIndex;
+    private boolean mIsRunning;
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        final Intent intent = getIntent();
+        final int themeIndex = intent.getIntExtra(EXTRA_THEME, -1);
+        if (themeIndex < 0) {
+            Log.e(TAG, "No theme specified");
+            finish();
+        }
+
+        final String outputDir = intent.getStringExtra(EXTRA_OUTPUT_DIR);
+        if (outputDir == null) {
+            Log.e(TAG, "No output directory specified");
+            finish();
+        }
+
+        mOutputDir = new File(outputDir);
+        mTheme = THEMES[themeIndex];
+
+        setTheme(mTheme.id);
+        setContentView(R.layout.theme_test);
+
+        mViewGroup = (ReferenceViewGroup) findViewById(R.id.reference_view_group);
+
+        getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON
+                | LayoutParams.FLAG_TURN_SCREEN_ON
+                | LayoutParams.FLAG_DISMISS_KEYGUARD );
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        mIsRunning = true;
+
+        setNextLayout();
+    }
+
+    @Override
+    protected void onPause() {
+        mIsRunning = false;
+
+        if (!isFinishing()) {
+            // The activity paused for some reason, likely a system crash
+            // dialog. Finish it so we can move to the next theme.
+            Log.w(TAG, "onPause() called without a call to finish()", new RuntimeException());
+            finish();
+        }
+
+        super.onPause();
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (mLayoutIndex < LAYOUTS.length) {
+            Log.e(TAG, "Not all layouts got rendered: " + mLayoutIndex);
+            setResult(RESULT_CANCELED);
+        }
+
+        super.onDestroy();
+    }
+
+    /**
+     * Sets the next layout in the UI.
+     */
+    private void setNextLayout() {
+        if (mLayoutIndex >= LAYOUTS.length) {
+            setResult(RESULT_OK);
+            finish();
+            return;
+        }
+
+        mViewGroup.removeAllViews();
+
+        final Layout layout = LAYOUTS[mLayoutIndex++];
+        final String layoutName = String.format("%s_%s", mTheme.name, layout.name);
+        final View view = getLayoutInflater().inflate(layout.id, mViewGroup, false);
+        if (layout.modifier != null) {
+            layout.modifier.modifyView(view);
+        }
+
+        mViewGroup.addView(view);
+        view.setFocusable(false);
+
+        Log.v(TAG, "Rendering layout " + layoutName
+                + " (" + mLayoutIndex + "/" + LAYOUTS.length + ")");
+
+        final Runnable generateBitmapRunnable = new Runnable() {
+            @Override
+            public void run() {
+                new BitmapTask(view, layoutName).execute();
+            }
+        };
+
+        if (view instanceof DatePicker) {
+            // The Holo-styled DatePicker uses a CalendarView that has a
+            // non-configurable adjustment duration of 540ms.
+            view.postDelayed(generateBitmapRunnable, CALENDAR_VIEW_ADJUSTMENT_DURATION);
+        } else {
+            view.post(generateBitmapRunnable);
+        }
+    }
+
+    private class BitmapTask extends GenerateBitmapTask {
+        public BitmapTask(View view, String name) {
+            super(view, mOutputDir, name);
+        }
+
+        @Override
+        protected void onPostExecute(Boolean success) {
+            if (success && mIsRunning) {
+                setNextLayout();
+            } else {
+                Log.e(TAG, "Failed to render view to bitmap: " + mName + " (activity running? "
+                        + mIsRunning + ")");
+                finish();
+            }
+        }
+    }
+
+    /**
+     * A class to encapsulate information about a theme.
+     */
+    static class Theme {
+        public final int id;
+        public final int apiLevel;
+        public final String name;
+
+        private Theme(int id, int apiLevel, String name) {
+            this.id = id;
+            this.apiLevel = apiLevel;
+            this.name = name;
+        }
+    }
+
+    // List of themes to verify.
+    static final Theme[] THEMES = {
+            // Holo
+            new Theme(android.R.style.Theme_Holo,
+                    Build.VERSION_CODES.HONEYCOMB, "holo"),
+            new Theme(android.R.style.Theme_Holo_Dialog,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_dialog"),
+            new Theme(android.R.style.Theme_Holo_Dialog_MinWidth,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_dialog_minwidth"),
+            new Theme(android.R.style.Theme_Holo_Dialog_NoActionBar,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_dialog_noactionbar"),
+            new Theme(android.R.style.Theme_Holo_Dialog_NoActionBar_MinWidth,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_dialog_noactionbar_minwidth"),
+            new Theme(android.R.style.Theme_Holo_DialogWhenLarge,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_dialogwhenlarge"),
+            new Theme(android.R.style.Theme_Holo_DialogWhenLarge_NoActionBar,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_dialogwhenlarge_noactionbar"),
+            new Theme(android.R.style.Theme_Holo_InputMethod,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_inputmethod"),
+            new Theme(android.R.style.Theme_Holo_NoActionBar,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_noactionbar"),
+            new Theme(android.R.style.Theme_Holo_NoActionBar_Fullscreen,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_noactionbar_fullscreen"),
+            new Theme(android.R.style.Theme_Holo_NoActionBar_Overscan,
+                    Build.VERSION_CODES.JELLY_BEAN_MR2, "holo_noactionbar_overscan"),
+            new Theme(android.R.style.Theme_Holo_NoActionBar_TranslucentDecor,
+                    Build.VERSION_CODES.KITKAT, "holo_noactionbar_translucentdecor"),
+            new Theme(android.R.style.Theme_Holo_Panel,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_panel"),
+            new Theme(android.R.style.Theme_Holo_Wallpaper,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_wallpaper"),
+            new Theme(android.R.style.Theme_Holo_Wallpaper_NoTitleBar,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_wallpaper_notitlebar"),
+
+            // Holo Light
+            new Theme(android.R.style.Theme_Holo_Light,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_light"),
+            new Theme(android.R.style.Theme_Holo_Light_DarkActionBar,
+                    Build.VERSION_CODES.ICE_CREAM_SANDWICH, "holo_light_darkactionbar"),
+            new Theme(android.R.style.Theme_Holo_Light_Dialog,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_light_dialog"),
+            new Theme(android.R.style.Theme_Holo_Light_Dialog_MinWidth,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_light_dialog_minwidth"),
+            new Theme(android.R.style.Theme_Holo_Light_Dialog_NoActionBar,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_light_dialog_noactionbar"),
+            new Theme(android.R.style.Theme_Holo_Light_Dialog_NoActionBar_MinWidth,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_light_dialog_noactionbar_minwidth"),
+            new Theme(android.R.style.Theme_Holo_Light_DialogWhenLarge,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_light_dialogwhenlarge"),
+            new Theme(android.R.style.Theme_Holo_Light_DialogWhenLarge_NoActionBar,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_light_dialogwhenlarge_noactionbar"),
+            new Theme(android.R.style.Theme_Holo_Light_NoActionBar,
+                    Build.VERSION_CODES.HONEYCOMB_MR2, "holo_light_noactionbar"),
+            new Theme(android.R.style.Theme_Holo_Light_NoActionBar_Fullscreen,
+                    Build.VERSION_CODES.HONEYCOMB_MR2, "holo_light_noactionbar_fullscreen"),
+            new Theme(android.R.style.Theme_Holo_Light_NoActionBar_Overscan,
+                    Build.VERSION_CODES.JELLY_BEAN_MR2, "holo_light_noactionbar_overscan"),
+            new Theme(android.R.style.Theme_Holo_Light_NoActionBar_TranslucentDecor,
+                    Build.VERSION_CODES.KITKAT, "holo_light_noactionbar_translucentdecor"),
+            new Theme(android.R.style.Theme_Holo_Light_Panel,
+                    Build.VERSION_CODES.HONEYCOMB, "holo_light_panel"),
+
+            // Material
+            new Theme(android.R.style.Theme_Material,
+                    Build.VERSION_CODES.LOLLIPOP, "material"),
+            new Theme(android.R.style.Theme_Material_Dialog,
+                    Build.VERSION_CODES.LOLLIPOP, "material_dialog"),
+            new Theme(android.R.style.Theme_Material_Dialog_Alert,
+                    Build.VERSION_CODES.LOLLIPOP, "material_dialog_alert"),
+            new Theme(android.R.style.Theme_Material_Dialog_MinWidth,
+                    Build.VERSION_CODES.LOLLIPOP, "material_dialog_minwidth"),
+            new Theme(android.R.style.Theme_Material_Dialog_NoActionBar,
+                    Build.VERSION_CODES.LOLLIPOP, "material_dialog_noactionbar"),
+            new Theme(android.R.style.Theme_Material_Dialog_NoActionBar_MinWidth,
+                    Build.VERSION_CODES.LOLLIPOP, "material_dialog_noactionbar_minwidth"),
+            new Theme(android.R.style.Theme_Material_Dialog_Presentation,
+                    Build.VERSION_CODES.LOLLIPOP, "material_dialog_presentation"),
+            new Theme(android.R.style.Theme_Material_DialogWhenLarge,
+                    Build.VERSION_CODES.LOLLIPOP, "material_dialogwhenlarge"),
+            new Theme(android.R.style.Theme_Material_DialogWhenLarge_NoActionBar,
+                    Build.VERSION_CODES.LOLLIPOP, "material_dialogwhenlarge_noactionbar"),
+            new Theme(android.R.style.Theme_Material_InputMethod,
+                    Build.VERSION_CODES.LOLLIPOP, "material_inputmethod"),
+            new Theme(android.R.style.Theme_Material_NoActionBar,
+                    Build.VERSION_CODES.LOLLIPOP, "material_noactionbar"),
+            new Theme(android.R.style.Theme_Material_NoActionBar_Fullscreen,
+                    Build.VERSION_CODES.LOLLIPOP, "material_noactionbar_fullscreen"),
+            new Theme(android.R.style.Theme_Material_NoActionBar_Overscan,
+                    Build.VERSION_CODES.LOLLIPOP, "material_noactionbar_overscan"),
+            new Theme(android.R.style.Theme_Material_NoActionBar_TranslucentDecor,
+                    Build.VERSION_CODES.LOLLIPOP, "material_noactionbar_translucentdecor"),
+            new Theme(android.R.style.Theme_Material_Panel,
+                    Build.VERSION_CODES.LOLLIPOP, "material_panel"),
+            new Theme(android.R.style.Theme_Material_Settings,
+                    Build.VERSION_CODES.LOLLIPOP, "material_settings"),
+            new Theme(android.R.style.Theme_Material_Voice,
+                    Build.VERSION_CODES.LOLLIPOP, "material_voice"),
+            new Theme(android.R.style.Theme_Material_Wallpaper,
+                    Build.VERSION_CODES.LOLLIPOP, "material_wallpaper"),
+            new Theme(android.R.style.Theme_Material_Wallpaper_NoTitleBar,
+                    Build.VERSION_CODES.LOLLIPOP, "material_wallpaper_notitlebar"),
+
+            // Material Light
+            new Theme(android.R.style.Theme_Material_Light,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light"),
+            new Theme(android.R.style.Theme_Material_Light_DarkActionBar,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_darkactionbar"),
+            new Theme(android.R.style.Theme_Material_Light_Dialog,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_dialog"),
+            new Theme(android.R.style.Theme_Material_Light_Dialog_Alert,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_dialog_alert"),
+            new Theme(android.R.style.Theme_Material_Light_Dialog_MinWidth,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_dialog_minwidth"),
+            new Theme(android.R.style.Theme_Material_Light_Dialog_NoActionBar,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_dialog_noactionbar"),
+            new Theme(android.R.style.Theme_Material_Light_Dialog_NoActionBar_MinWidth,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_dialog_noactionbar_minwidth"),
+            new Theme(android.R.style.Theme_Material_Light_Dialog_Presentation,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_dialog_presentation"),
+            new Theme(android.R.style.Theme_Material_Light_DialogWhenLarge,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_dialogwhenlarge"),
+            new Theme(android.R.style.Theme_Material_Light_DialogWhenLarge_NoActionBar,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_dialogwhenlarge_noactionbar"),
+            new Theme(android.R.style.Theme_Material_Light_LightStatusBar,
+                    Build.VERSION_CODES.M, "material_light_lightstatusbar"),
+            new Theme(android.R.style.Theme_Material_Light_NoActionBar,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_noactionbar"),
+            new Theme(android.R.style.Theme_Material_Light_NoActionBar_Fullscreen,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_noactionbar_fullscreen"),
+            new Theme(android.R.style.Theme_Material_Light_NoActionBar_Overscan,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_noactionbar_overscan"),
+            new Theme(android.R.style.Theme_Material_Light_NoActionBar_TranslucentDecor,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_noactionbar_translucentdecor"),
+            new Theme(android.R.style.Theme_Material_Light_Panel,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_panel"),
+            new Theme(android.R.style.Theme_Material_Light_Voice,
+                    Build.VERSION_CODES.LOLLIPOP, "material_light_voice")
+    };
+
+    /**
+     * A class to encapsulate information about a layout.
+     */
+    private static class Layout {
+        public final int id;
+        public final String name;
+        public final LayoutModifier modifier;
+
+        private Layout(int id, String name) {
+            this(id, name, null);
+        }
+
+        private Layout(int id, String name, LayoutModifier modifier) {
+            this.id = id;
+            this.name = name;
+            this.modifier = modifier;
+        }
+    }
+
+    // List of layouts to verify for each theme.
+    private static final Layout[] LAYOUTS = {
+            new Layout(R.layout.button, "button"),
+            new Layout(R.layout.button, "button_pressed",
+                    new ViewPressedModifier()),
+            new Layout(R.layout.checkbox, "checkbox"),
+            new Layout(R.layout.checkbox, "checkbox_checked",
+                    new ViewCheckedModifier()),
+            new Layout(R.layout.chronometer, "chronometer"),
+            new Layout(R.layout.color_blue_bright, "color_blue_bright"),
+            new Layout(R.layout.color_blue_dark, "color_blue_dark"),
+            new Layout(R.layout.color_blue_light, "color_blue_light"),
+            new Layout(R.layout.color_green_dark, "color_green_dark"),
+            new Layout(R.layout.color_green_light, "color_green_light"),
+            new Layout(R.layout.color_orange_dark, "color_orange_dark"),
+            new Layout(R.layout.color_orange_light, "color_orange_light"),
+            new Layout(R.layout.color_purple, "color_purple"),
+            new Layout(R.layout.color_red_dark, "color_red_dark"),
+            new Layout(R.layout.color_red_light, "color_red_light"),
+            new Layout(R.layout.datepicker, "datepicker",
+                    new DatePickerModifier()),
+            new Layout(R.layout.display_info, "display_info"),
+            new Layout(R.layout.edittext, "edittext"),
+            new Layout(R.layout.progressbar_horizontal_0, "progressbar_horizontal_0"),
+            new Layout(R.layout.progressbar_horizontal_100, "progressbar_horizontal_100"),
+            new Layout(R.layout.progressbar_horizontal_50, "progressbar_horizontal_50"),
+            new Layout(R.layout.progressbar_large, "progressbar_large",
+                    new ProgressBarModifier()),
+            new Layout(R.layout.progressbar_small, "progressbar_small",
+                    new ProgressBarModifier()),
+            new Layout(R.layout.progressbar, "progressbar",
+                    new ProgressBarModifier()),
+            new Layout(R.layout.radiobutton_checked, "radiobutton_checked"),
+            new Layout(R.layout.radiobutton, "radiobutton"),
+            new Layout(R.layout.radiogroup_horizontal, "radiogroup_horizontal"),
+            new Layout(R.layout.radiogroup_vertical, "radiogroup_vertical"),
+            new Layout(R.layout.ratingbar_0, "ratingbar_0"),
+            new Layout(R.layout.ratingbar_2point5, "ratingbar_2point5"),
+            new Layout(R.layout.ratingbar_5, "ratingbar_5"),
+            new Layout(R.layout.ratingbar_0, "ratingbar_0_pressed",
+                    new ViewPressedModifier()),
+            new Layout(R.layout.ratingbar_2point5, "ratingbar_2point5_pressed",
+                    new ViewPressedModifier()),
+            new Layout(R.layout.ratingbar_5, "ratingbar_5_pressed",
+                    new ViewPressedModifier()),
+            new Layout(R.layout.searchview, "searchview_query",
+                    new SearchViewModifier(SearchViewModifier.QUERY)),
+            new Layout(R.layout.searchview, "searchview_query_hint",
+                    new SearchViewModifier(SearchViewModifier.QUERY_HINT)),
+            new Layout(R.layout.seekbar_0, "seekbar_0"),
+            new Layout(R.layout.seekbar_100, "seekbar_100"),
+            new Layout(R.layout.seekbar_50, "seekbar_50"),
+            new Layout(R.layout.spinner, "spinner"),
+            new Layout(R.layout.switch_button_checked, "switch_button_checked"),
+            new Layout(R.layout.switch_button, "switch_button"),
+            new Layout(R.layout.textview, "textview"),
+            new Layout(R.layout.timepicker, "timepicker",
+                    new TimePickerModifier()),
+            new Layout(R.layout.togglebutton_checked, "togglebutton_checked"),
+            new Layout(R.layout.togglebutton, "togglebutton"),
+            new Layout(R.layout.zoomcontrols, "zoomcontrols"),
+    };
+}
diff --git a/hostsidetests/theme/app/src/android/theme/app/ThemeTestUtils.java b/hostsidetests/theme/app/src/android/theme/app/ThemeTestUtils.java
new file mode 100644
index 0000000..4daca6c
--- /dev/null
+++ b/hostsidetests/theme/app/src/android/theme/app/ThemeTestUtils.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015 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 android.theme.app;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class ThemeTestUtils {
+
+    /**
+     * Compresses the contents of a directory to a ZIP file.
+     *
+     * @param dir the directory to compress
+     * @return {@code true} on success, {@code false} on failure
+     */
+    public static boolean compressDirectory(File dir, File file) throws IOException {
+        if (dir == null || !dir.exists() || file == null || file.exists()) {
+            return false;
+        }
+
+        final File[] srcFiles = dir.listFiles();
+        if (srcFiles.length == 0) {
+            return false;
+        }
+
+        final ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(file));
+        final byte[] data = new byte[4096];
+        for (int i = 0; i < srcFiles.length; i++) {
+            final FileInputStream fileIn = new FileInputStream(srcFiles[i]);
+            final ZipEntry entry = new ZipEntry(srcFiles[i].getName());
+            zipOut.putNextEntry(entry);
+
+            int count;
+            while ((count = fileIn.read(data, 0, data.length)) != -1) {
+                zipOut.write(data, 0, count);
+                zipOut.flush();
+            }
+
+            zipOut.closeEntry();
+            fileIn.close();
+        }
+
+        zipOut.close();
+        return true;
+    }
+
+    /**
+     * Recursively deletes a directory and its contents.
+     *
+     * @param dir the directory to delete
+     * @return {@code true} on success, {@code false} on failure
+     */
+    public static boolean deleteDirectory(File dir) {
+        final File files[] = dir.listFiles();
+        if (files != null) {
+            for (File file : files) {
+                deleteDirectory(file);
+            }
+        }
+        return dir.delete();
+    }
+}
diff --git a/hostsidetests/theme/assets/23/400dpi.zip b/hostsidetests/theme/assets/23/400dpi.zip
new file mode 100644
index 0000000..be0891f
--- /dev/null
+++ b/hostsidetests/theme/assets/23/400dpi.zip
Binary files differ
diff --git a/hostsidetests/theme/assets/23/560dpi.zip b/hostsidetests/theme/assets/23/560dpi.zip
new file mode 100644
index 0000000..cf0a559
--- /dev/null
+++ b/hostsidetests/theme/assets/23/560dpi.zip
Binary files differ
diff --git a/hostsidetests/theme/assets/23/hdpi.zip b/hostsidetests/theme/assets/23/hdpi.zip
new file mode 100644
index 0000000..80c12a7
--- /dev/null
+++ b/hostsidetests/theme/assets/23/hdpi.zip
Binary files differ
diff --git a/hostsidetests/theme/assets/23/ldpi.zip b/hostsidetests/theme/assets/23/ldpi.zip
new file mode 100644
index 0000000..937914a
--- /dev/null
+++ b/hostsidetests/theme/assets/23/ldpi.zip
Binary files differ
diff --git a/hostsidetests/theme/assets/23/mdpi.zip b/hostsidetests/theme/assets/23/mdpi.zip
new file mode 100644
index 0000000..f842676
--- /dev/null
+++ b/hostsidetests/theme/assets/23/mdpi.zip
Binary files differ
diff --git a/hostsidetests/theme/assets/23/tvdpi.zip b/hostsidetests/theme/assets/23/tvdpi.zip
new file mode 100644
index 0000000..77386e5
--- /dev/null
+++ b/hostsidetests/theme/assets/23/tvdpi.zip
Binary files differ
diff --git a/hostsidetests/theme/assets/23/xhdpi.zip b/hostsidetests/theme/assets/23/xhdpi.zip
new file mode 100644
index 0000000..a8310d5
--- /dev/null
+++ b/hostsidetests/theme/assets/23/xhdpi.zip
Binary files differ
diff --git a/hostsidetests/theme/assets/23/xxhdpi.zip b/hostsidetests/theme/assets/23/xxhdpi.zip
new file mode 100644
index 0000000..f88711f
--- /dev/null
+++ b/hostsidetests/theme/assets/23/xxhdpi.zip
Binary files differ
diff --git a/hostsidetests/theme/generate_images.sh b/hostsidetests/theme/generate_images.sh
new file mode 100755
index 0000000..9bcc3e5
--- /dev/null
+++ b/hostsidetests/theme/generate_images.sh
@@ -0,0 +1,55 @@
+#!/bin/sh
+# Copyright (C) 2015 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.
+
+# This script is used to generate reference images for the CTS theme tests.
+# See the accompanying README file for more information.
+
+# retry <command> <tries> <message> <delay>
+function retry {
+  RETRY="0"
+  while true; do
+    if (("$RETRY" >= "$2")); then
+      echo $OUTPUT
+      exit
+    fi
+
+    OUTPUT=`$1 |& grep error`
+
+    if [ -z "$OUTPUT" ]; then
+      break
+    fi
+
+    echo $3
+    sleep $4
+    RETRY=$[$RETRY + 1]
+  done
+}
+
+themeApkPath="$ANDROID_HOST_OUT/cts/android-cts/repository/testcases/CtsThemeDeviceApp.apk"
+outDir="$ANDROID_BUILD_TOP/cts/hostsidetests/theme/assets"
+exe="$ANDROID_BUILD_TOP/cts/hostsidetests/theme/run_theme_capture_device.py"
+
+if [ -z "$ANDROID_BUILD_TOP" ]; then
+  echo "Missing environment variables. Did you run build/envsetup.sh and lunch?"
+  exit
+fi
+
+if [ ! -e "$themeApkPath" ]; then
+  echo "Couldn't find test APK. Did you run make cts?"
+  exit
+fi
+
+adb devices
+python $exe $themeApkPath $outDir
diff --git a/hostsidetests/theme/run_theme_capture_device.py b/hostsidetests/theme/run_theme_capture_device.py
new file mode 100755
index 0000000..23171db
--- /dev/null
+++ b/hostsidetests/theme/run_theme_capture_device.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2015 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.
+#
+
+import os
+import sys
+import threading
+import time
+import Queue
+sys.path.append(sys.path[0])
+from android_device import *
+
+CTS_THEME_dict = {
+    120 : "ldpi",
+    160 : "mdpi",
+    213 : "tvdpi",
+    240 : "hdpi",
+    320 : "xhdpi",
+    400 : "400dpi",
+    480 : "xxhdpi",
+    560 : "560dpi",
+    640 : "xxxhdpi",
+}
+
+OUT_FILE = "/sdcard/cts-theme-assets.zip"
+
+# pass a function with number of instances to be executed in parallel
+# each thread continues until config q is empty.
+def executeParallel(tasks, setup, q, numberThreads):
+    class ParallelExecutor(threading.Thread):
+        def __init__(self, tasks, q):
+            threading.Thread.__init__(self)
+            self._q = q
+            self._tasks = tasks
+            self._setup = setup
+            self._result = 0
+
+        def run(self):
+            try:
+                while True:
+                    config = q.get(block=True, timeout=2)
+                    for t in self._tasks:
+                        try:
+                            if t(self._setup, config):
+                                self._result += 1
+                        except KeyboardInterrupt:
+                            raise
+                        except:
+                            print "Failed to execute thread:", sys.exc_info()[0]
+                    q.task_done()
+            except KeyboardInterrupt:
+                raise
+            except Queue.Empty:
+                pass
+
+        def getResult(self):
+            return self._result
+
+    result = 0;
+    threads = []
+    for i in range(numberThreads):
+        t = ParallelExecutor(tasks, q)
+        t.start()
+        threads.append(t)
+    for t in threads:
+        t.join()
+        result += t.getResult()
+    return result;
+
+def printAdbResult(device, out, err):
+    print "device: " + device
+    if out is not None:
+        print "out:\n" + out
+    if err is not None:
+        print "err:\n" + err
+
+def getResDir(outPath, resName):
+    resDir = outPath + "/" + resName
+    return resDir
+
+def doCapturing(setup, deviceSerial):
+    (themeApkPath, outPath) = setup
+
+    print "Found device: " + deviceSerial
+    device = androidDevice(deviceSerial)
+
+    # outPath = outPath + "/%d/" % (device.getSdkLevel()) + deviceSerial
+    outPath = outPath + "/%d" % (device.getSdkLevel())
+    density = device.getDensity()
+    resName = CTS_THEME_dict[density]
+
+    device.uninstallApk("android.theme.app")
+
+    (out, err, success) = device.installApk(themeApkPath)
+    if not success:
+        print "Failed to install APK on " + deviceSerial
+        printAdbResult(device, out, err)
+        return False
+
+    print "Generating images on " + deviceSerial + "..."
+    try:
+        (out, err) = device.runInstrumentationTest("android.theme.app/android.support.test.runner.AndroidJUnitRunner")
+    except KeyboardInterrupt:
+        raise
+    except:
+        (out, err) = device.runInstrumentationTest("android.theme.app/android.test.InstrumentationTestRunner")
+
+    # Detect test failure and abort.
+    if "FAILURES!!!" in out.split():
+        printAdbResult(deviceSerial, out, err)
+        return False
+
+    # Make sure that the run is complete by checking the process itself
+    print "Waiting for " + deviceSerial + "..."
+    waitTime = 0
+    while device.isProcessAlive("android.theme.app"):
+        time.sleep(1)
+        waitTime = waitTime + 1
+        if waitTime > 180:
+            print "Timed out"
+            break
+
+    time.sleep(10)
+    resDir = getResDir(outPath, resName)
+
+    print "Pulling images from " + deviceSerial + " to " + resDir + ".zip"
+    device.runAdbCommand("pull " + OUT_FILE + " " + resDir + ".zip")
+    device.runAdbCommand("shell rm -rf " + OUT_FILE)
+    return True
+
+def main(argv):
+    if len(argv) < 3:
+        print "run_theme_capture_device.py themeApkPath outDir"
+        sys.exit(1)
+    themeApkPath = argv[1]
+    outPath = os.path.abspath(argv[2])
+    os.system("mkdir -p " + outPath)
+
+    tasks = []
+    tasks.append(doCapturing)
+
+    devices = runAdbDevices();
+    numberThreads = len(devices)
+
+    configQ = Queue.Queue()
+    for device in devices:
+        configQ.put(device)
+    setup = (themeApkPath, outPath)
+    result = executeParallel(tasks, setup, configQ, numberThreads)
+
+    if result > 0:
+        print 'Generated reference images for %(count)d devices' % {"count": result}
+    else:
+        print 'Failed to generate reference images'
+
+if __name__ == '__main__':
+    main(sys.argv)
diff --git a/hostsidetests/theme/src/android/theme/cts/ComparisonTask.java b/hostsidetests/theme/src/android/theme/cts/ComparisonTask.java
index ba880d7..63c7472 100644
--- a/hostsidetests/theme/src/android/theme/cts/ComparisonTask.java
+++ b/hostsidetests/theme/src/android/theme/cts/ComparisonTask.java
@@ -23,6 +23,7 @@
 import java.awt.Color;
 import java.awt.image.BufferedImage;
 import java.io.File;
+import java.io.IOException;
 import java.lang.String;
 import java.util.concurrent.Callable;
 
@@ -32,70 +33,62 @@
  * Compares the images generated by the device with the reference images.
  */
 public class ComparisonTask implements Callable<Boolean> {
-
-    private static final String TAG = ComparisonTask.class.getSimpleName();
+    private static final String TAG = "ComparisonTask";
 
     private static final int IMAGE_THRESHOLD = 2;
 
-    private static final String STORAGE_PATH_DEVICE = "/sdcard/cts-holo-assets/%s.png";
-
     private final ITestDevice mDevice;
+    private final File mExpected;
+    private final File mActual;
 
-    private final File mReference;
-
-    private final String mName;
-
-    public ComparisonTask(ITestDevice device, File reference, String name) {
+    public ComparisonTask(ITestDevice device, File expected, File actual) {
         mDevice = device;
-        mReference = reference;
-        mName = name;
+        mExpected = expected;
+        mActual = actual;
     }
 
     public Boolean call() {
         boolean success = false;
-        File generated = null;
+
         try {
-            generated = File.createTempFile("gen_" + mName, ".png");
-
-            final String remoteGenerated = String.format(STORAGE_PATH_DEVICE, mName);
-            if (!mDevice.doesFileExist(remoteGenerated)) {
-                Log.logAndDisplay(LogLevel.ERROR, TAG, "File " + remoteGenerated + " have not been saved on device");
-                return false;
-            }
-            mDevice.pullFile(remoteGenerated, generated);
-
-            final BufferedImage ref = ImageIO.read(mReference);
-            final BufferedImage gen = ImageIO.read(generated);
-            if (compare(ref, gen, IMAGE_THRESHOLD)) {
+            final BufferedImage expected = ImageIO.read(mExpected);
+            final BufferedImage actual = ImageIO.read(mActual);
+            if (compare(expected, actual, IMAGE_THRESHOLD)) {
                 success = true;
             } else {
-                File diff = File.createTempFile("diff_" + mName, ".png");
-                createDiff(ref, gen, diff);
+                final File diff = File.createTempFile("diff_" + mExpected.getName(), ".png");
+                createDiff(expected, actual, diff);
                 Log.logAndDisplay(LogLevel.INFO, TAG, "Diff created: " + diff.getPath());
             }
-        } catch (Exception e) {
-            Log.logAndDisplay(LogLevel.ERROR, TAG, String.format(STORAGE_PATH_DEVICE, mName));
+        } catch (IOException e) {
             Log.logAndDisplay(LogLevel.ERROR, TAG, e.toString());
             e.printStackTrace();
-        } finally {
-            if (generated != null) {
-                generated.delete();
-            }
         }
+
         return success;
     }
 
-    private static boolean compare(BufferedImage reference, BufferedImage generated, int threshold) {
-        final int w = generated.getWidth();
-        final int h = generated.getHeight();
-        if (w != reference.getWidth() || h != reference.getHeight()) {
+    /**
+     * Verifies that the pixels of reference and generated images are similar
+     * within a specified threshold.
+     *
+     * @param expected expected image
+     * @param actual actual image
+     * @param threshold maximum difference per channel
+     * @return {@code true} if the images are similar, false otherwise
+     */
+    private static boolean compare(BufferedImage expected, BufferedImage actual,
+            int threshold) {
+        final int w = actual.getWidth();
+        final int h = actual.getHeight();
+        if (w != expected.getWidth() || h != expected.getHeight()) {
             return false;
         }
 
         for (int i = 0; i < w; i++) {
             for (int j = 0; j < h; j++) {
-                final int p1 = reference.getRGB(i, j);
-                final int p2 = generated.getRGB(i, j);
+                final int p1 = expected.getRGB(i, j);
+                final int p2 = actual.getRGB(i, j);
                 final int dr = (p1 & 0x000000FF) - (p2 & 0x000000FF);
                 final int dg = ((p1 & 0x0000FF00) - (p2 & 0x0000FF00)) >> 8;
                 final int db = ((p1 & 0x00FF0000) - (p2 & 0x00FF0000)) >> 16;
@@ -112,45 +105,49 @@
         return true;
     }
 
-    private static void createDiff(BufferedImage image1, BufferedImage image2, File out)
-            throws Exception {
-        final int w1 = image1.getWidth();
-        final int h1 = image1.getHeight();
-        final int w2 = image2.getWidth();
-        final int h2 = image2.getHeight();
+    private static void createDiff(BufferedImage expected, BufferedImage actual, File out)
+            throws IOException {
+        final int w1 = expected.getWidth();
+        final int h1 = expected.getHeight();
+        final int w2 = actual.getWidth();
+        final int h2 = actual.getHeight();
         final int width = Math.max(w1, w2);
         final int height = Math.max(h1, h2);
+
         // The diff will contain image1, image2 and the difference between the two.
-        final BufferedImage diff = new BufferedImage(width * 3, height, BufferedImage.TYPE_INT_ARGB);
+        final BufferedImage diff = new BufferedImage(
+                width * 3, height, BufferedImage.TYPE_INT_ARGB);
 
         for (int i = 0; i < width; i++) {
             for (int j = 0; j < height; j++) {
                 final boolean inBounds1 = i < w1 && j < h1;
                 final boolean inBounds2 = i < w2 && j < h2;
-                int color1 = Color.WHITE.getRGB();
-                int color2 = Color.WHITE.getRGB();
-                int color3;
+                int colorExpected = Color.WHITE.getRGB();
+                int colorActual = Color.WHITE.getRGB();
+                int colorDiff;
                 if (inBounds1 && inBounds2) {
-                    color1 = image1.getRGB(i, j);
-                    color2 = image2.getRGB(i, j);
-                    color3 = color1 == color2 ? color1 : Color.RED.getRGB();
+                    colorExpected = expected.getRGB(i, j);
+                    colorActual = actual.getRGB(i, j);
+                    colorDiff = colorExpected == colorActual ? colorExpected : Color.RED.getRGB();
                 } else if (inBounds1 && !inBounds2) {
-                    color1 = image1.getRGB(i, j);
-                    color3 = Color.BLUE.getRGB();
+                    colorExpected = expected.getRGB(i, j);
+                    colorDiff = Color.BLUE.getRGB();
                 } else if (!inBounds1 && inBounds2) {
-                    color2 = image2.getRGB(i, j);
-                    color3 = Color.GREEN.getRGB();
+                    colorActual = actual.getRGB(i, j);
+                    colorDiff = Color.GREEN.getRGB();
                 } else {
-                    color3 = Color.MAGENTA.getRGB();
+                    colorDiff = Color.MAGENTA.getRGB();
                 }
+
                 int x = i;
-                diff.setRGB(x, j, color1);
+                diff.setRGB(x, j, colorExpected);
                 x += width;
-                diff.setRGB(x, j, color2);
+                diff.setRGB(x, j, colorActual);
                 x += width;
-                diff.setRGB(x, j, color3);
+                diff.setRGB(x, j, colorDiff);
             }
         }
+
         ImageIO.write(diff, "png", out);
     }
 
diff --git a/hostsidetests/theme/src/android/theme/cts/ThemeHostTest.java b/hostsidetests/theme/src/android/theme/cts/ThemeHostTest.java
index 8326b1f..b4bb748 100644
--- a/hostsidetests/theme/src/android/theme/cts/ThemeHostTest.java
+++ b/hostsidetests/theme/src/android/theme/cts/ThemeHostTest.java
@@ -21,8 +21,8 @@
 import com.android.cts.util.TimeoutReq;
 import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
-import com.android.ddmlib.IShellOutputReceiver;
 import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.testtype.DeviceTestCase;
 import com.android.tradefed.testtype.IAbi;
@@ -30,145 +30,44 @@
 import com.android.tradefed.testtype.IBuildReceiver;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.lang.String;
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Scanner;
-import java.util.concurrent.Callable;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ExecutorCompletionService;
 import java.util.concurrent.ExecutorService;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
 /**
- * Test to check the Holo theme has not been changed.
+ * Test to check non-modifiable themes have not been changed.
  */
 public class ThemeHostTest extends DeviceTestCase implements IAbiReceiver, IBuildReceiver {
+    private static final String LOG_TAG = "ThemeHostTest";
+    private static final String APK_NAME = "CtsThemeDeviceApp";
+    private static final String APP_PACKAGE_NAME = "android.theme.app";
 
-    private static final String TAG = ThemeHostTest.class.getSimpleName();
-
-    private static final int ADB_TIMEOUT = 60 * 60 * 1000;//60mins in ms
-
-    /** The package name of the APK. */
-    private static final String PACKAGE = "android.theme.app";
-
-    /** The file name of the APK. */
-    private static final String APK = "CtsThemeDeviceApp.apk";
+    private static final String GENERATED_ASSETS_ZIP = "/sdcard/cts-theme-assets.zip";
 
     /** The class name of the main activity in the APK. */
-    private static final String CLASS = "HoloDeviceActivity";
+    private static final String CLASS = "GenerateImagesActivity";
 
     /** The command to launch the main activity. */
     private static final String START_CMD = String.format(
-            "am start -W -a android.intent.action.MAIN -n %s/%s.%s", PACKAGE, PACKAGE, CLASS);
+            "am start -W -a android.intent.action.MAIN -n %s/%s.%s", APP_PACKAGE_NAME,
+            APP_PACKAGE_NAME, CLASS);
 
-    private static final String CLEAR_GENERATED_CMD = "rm -rf /sdcard/cts-holo-assets/*.png";
-
-    private static final String STOP_CMD = String.format("am force-stop %s", PACKAGE);
-
+    private static final String CLEAR_GENERATED_CMD = "rm -rf %s/*.png";
+    private static final String STOP_CMD = String.format("am force-stop %s", APP_PACKAGE_NAME);
     private static final String HARDWARE_TYPE_CMD = "dumpsys | grep android.hardware.type";
-
     private static final String DENSITY_PROP_DEVICE = "ro.sf.lcd_density";
-
     private static final String DENSITY_PROP_EMULATOR = "qemu.sf.lcd_density";
 
-    // Intent extras
-    protected final static String INTENT_STRING_EXTRA = " --es %s %s";
-
-    protected final static String INTENT_BOOLEAN_EXTRA = " --ez %s %b";
-
-    protected final static String INTENT_INTEGER_EXTRA = " --ei %s %d";
-
-    // Intent extra keys
-    private static final String EXTRA_THEME = "holo_theme_extra";
-
-    private static final String[] THEMES = {
-            "holo",
-            "holo_dialog",
-            "holo_dialog_minwidth",
-            "holo_dialog_noactionbar",
-            "holo_dialog_noactionbar_minwidth",
-            "holo_dialogwhenlarge",
-            "holo_dialogwhenlarge_noactionbar",
-            "holo_inputmethod",
-            "holo_light",
-            "holo_light_darkactionbar",
-            "holo_light_dialog",
-            "holo_light_dialog_minwidth",
-            "holo_light_dialog_noactionbar",
-            "holo_light_dialog_noactionbar_minwidth",
-            "holo_light_dialogwhenlarge",
-            "holo_light_dialogwhenlarge_noactionbar",
-            "holo_light_noactionbar",
-            "holo_light_noactionbar_fullscreen",
-            "holo_light_panel",
-            "holo_noactionbar",
-            "holo_noactionbar_fullscreen",
-            "holo_panel",
-            "holo_wallpaper",
-            "holo_wallpaper_notitlebar",
-    };
-
-    private final int NUM_THEMES = THEMES.length;
-
-    private static final String[] LAYOUTS = {
-            "button",
-            "button_pressed",
-            "checkbox",
-            "checkbox_checked",
-            "chronometer",
-            "color_blue_bright",
-            "color_blue_dark",
-            "color_blue_light",
-            "color_green_dark",
-            "color_green_light",
-            "color_orange_dark",
-            "color_orange_light",
-            "color_purple",
-            "color_red_dark",
-            "color_red_light",
-            "datepicker",
-            "display_info",
-            "edittext",
-            "progressbar_horizontal_0",
-            "progressbar_horizontal_100",
-            "progressbar_horizontal_50",
-            "progressbar_large",
-            "progressbar_small",
-            "progressbar",
-            "radiobutton_checked",
-            "radiobutton",
-            "radiogroup_horizontal",
-            "radiogroup_vertical",
-            "ratingbar_0",
-            "ratingbar_2point5",
-            "ratingbar_5",
-            "ratingbar_0_pressed",
-            "ratingbar_2point5_pressed",
-            "ratingbar_5_pressed",
-            "searchview_query",
-            "searchview_query_hint",
-            "seekbar_0",
-            "seekbar_100",
-            "seekbar_50",
-            "spinner",
-            "switch_button_checked",
-            "switch_button",
-            "textview",
-            "timepicker",
-            "togglebutton_checked",
-            "togglebutton",
-            "zoomcontrols",
-    };
-
-    private final int NUM_LAYOUTS = LAYOUTS.length;
-
-    private final HashMap<String, File> mReferences = new HashMap<String, File>();
+    private final HashMap<String, File> mReferences = new HashMap<>();
 
     /** The ABI to use. */
     private IAbi mAbi;
@@ -197,32 +96,21 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        // Get the device, this gives a handle to run commands and install APKs.
+
         mDevice = getDevice();
-        // Remove any previously installed versions of this APK.
-        mDevice.uninstallPackage(PACKAGE);
+        mDevice.uninstallPackage(APP_PACKAGE_NAME);
+
         // Get the APK from the build.
-        File app = mBuild.getTestApp(APK);
-        // Get the ABI flag.
-        String[] options = {AbiUtils.createAbiFlag(mAbi.getName())};
-        // Install the APK on the device.
+        final File app = mBuild.getTestApp(String.format("%s.apk", APK_NAME));
+        final String[] options = {AbiUtils.createAbiFlag(mAbi.getName())};
+
         mDevice.installPackage(app, false, options);
-        // Remove previously generated images.
-        mDevice.executeShellCommand(CLEAR_GENERATED_CMD);
-        final String densityProp;
 
-        if (mDevice.getSerialNumber().startsWith("emulator-")) {
-            densityProp = DENSITY_PROP_EMULATOR;
-        } else {
-            densityProp = DENSITY_PROP_DEVICE;
-        }
+        final String density = getDensityBucketForDevice(mDevice);
+        final String zipFile = String.format("/%s.zip", density);
+        Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "Loading resources from " + zipFile);
 
-        final String zip = String.format("/%s.zip",
-                getDensityBucket(Integer.parseInt(mDevice.getProperty(densityProp))));
-        Log.logAndDisplay(LogLevel.INFO, TAG, "Loading resources from " + zip);
-
-
-        final InputStream zipStream = this.getClass().getResourceAsStream(zip);
+        final InputStream zipStream = ThemeHostTest.class.getResourceAsStream(zipFile);
         if (zipStream != null) {
             final ZipInputStream in = new ZipInputStream(zipStream);
             try {
@@ -232,21 +120,28 @@
                     final String name = ze.getName();
                     final File tmp = File.createTempFile("ref_" + name, ".png");
                     final FileOutputStream out = new FileOutputStream(tmp);
+
                     int count;
                     while ((count = in.read(buffer)) != -1) {
                         out.write(buffer, 0, count);
                     }
+
                     out.flush();
                     out.close();
                     mReferences.put(name, tmp);
                 }
+            } catch (IOException e) {
+                Log.logAndDisplay(LogLevel.ERROR, LOG_TAG, "Failed to unzip assets: " + zipFile);
             } finally {
                 in.close();
             }
+        } else {
+            Log.logAndDisplay(LogLevel.ERROR, LOG_TAG, "Failed to get resource: " + zipFile);
         }
 
-        mExecutionService = Executors.newFixedThreadPool(2);// 2 worker threads
-        mCompletionService = new ExecutorCompletionService<Boolean>(mExecutionService);
+        final int numCores = Runtime.getRuntime().availableProcessors();
+        mExecutionService = Executors.newFixedThreadPool(numCores * 2);
+        mCompletionService = new ExecutorCompletionService<>(mExecutionService);
     }
 
     @Override
@@ -255,86 +150,148 @@
         for (File ref : mReferences.values()) {
             ref.delete();
         }
+
         mExecutionService.shutdown();
+
         // Remove the APK.
-        mDevice.uninstallPackage(PACKAGE);
+        mDevice.uninstallPackage(APP_PACKAGE_NAME);
+
         // Remove generated images.
         mDevice.executeShellCommand(CLEAR_GENERATED_CMD);
+
         super.tearDown();
     }
 
     @TimeoutReq(minutes = 60)
-    public void testHoloThemes() throws Exception {
-        if (checkHardwareTypeSkipTest(
-                mDevice.executeShellCommand(HARDWARE_TYPE_CMD).trim())) {
-            Log.logAndDisplay(LogLevel.INFO, TAG, "Skipped HoloThemes test for watch and TV");
+    public void testThemes() throws Exception {
+        if (checkHardwareTypeSkipTest(mDevice.executeShellCommand(HARDWARE_TYPE_CMD).trim())) {
+            Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "Skipped themes test for watch");
             return;
         }
 
         if (mReferences.isEmpty()) {
-            Log.logAndDisplay(LogLevel.INFO, TAG,
-                    "Skipped HoloThemes test due to no reference images");
+            Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "Skipped themes test due to no reference images");
             return;
         }
 
+        Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "Generating device images...");
+
+        assertTrue("Aborted image generation", generateDeviceImages());
+
+        // Pull ZIP file from remote device.
+        final File localZip = File.createTempFile("generated", ".zip");
+        mDevice.pullFile(GENERATED_ASSETS_ZIP, localZip);
+
         int numTasks = 0;
-        for (int i = 0; i < NUM_THEMES; i++) {
-            final String themeName = THEMES[i];
-            runCapture(i, themeName);
-            for (int j = 0; j < NUM_LAYOUTS; j++) {
-                final String name = String.format("%s_%s", themeName, LAYOUTS[j]);
-                final File ref = mReferences.get(name + ".png");
-                if (!ref.exists()) {
-                    Log.logAndDisplay(LogLevel.INFO, TAG,
-                            "Skipping theme test due to missing reference for reference image " +
-                            name);
-                    continue;
+
+        Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "Extracting generated images...");
+
+        // Extract generated images to temporary files.
+        final byte[] data = new byte[4096];
+        final ZipInputStream zipInput = new ZipInputStream(new FileInputStream(localZip));
+        ZipEntry entry;
+        while ((entry = zipInput.getNextEntry()) != null) {
+            final String name = entry.getName();
+            final File expected = mReferences.get(name);
+            if (expected != null && expected.exists()) {
+                final File actual = File.createTempFile("actual_" + name, ".png");
+                final FileOutputStream pngOutput = new FileOutputStream(actual);
+
+                int count;
+                while ((count = zipInput.read(data, 0, data.length)) != -1) {
+                    pngOutput.write(data, 0, count);
                 }
-                mCompletionService.submit(new ComparisonTask(mDevice, ref, name));
+
+                pngOutput.flush();
+                pngOutput.close();
+
+                mCompletionService.submit(new ComparisonTask(mDevice, expected, actual));
                 numTasks++;
+            } else {
+                Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "Missing reference image for " + name);
             }
+
+            zipInput.closeEntry();
         }
+
+        zipInput.close();
+
+        Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "Waiting for comparison tasks...");
+
         int failures = 0;
-        for (int i = 0; i < numTasks; i++) {
+        for (int i = numTasks; i > 0; i--) {
             failures += mCompletionService.take().get() ? 0 : 1;
         }
+
         assertTrue(failures + " failures in theme test", failures == 0);
+
+        Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "Finished!");
     }
 
-    private void runCapture(int themeId, String themeName) throws Exception {
-        final StringBuilder sb = new StringBuilder(START_CMD);
-        sb.append(String.format(INTENT_INTEGER_EXTRA, EXTRA_THEME, themeId));
-        final String startCommand = sb.toString();
+    private boolean generateDeviceImages() throws Exception {
         // Clear logcat
         mDevice.executeAdbCommand("logcat", "-c");
+
         // Stop any existing instances
         mDevice.executeShellCommand(STOP_CMD);
-        // Start activity
-        mDevice.executeShellCommand(startCommand);
 
+        // Start activity
+        mDevice.executeShellCommand(START_CMD);
+
+        Log.logAndDisplay(LogLevel.VERBOSE, LOG_TAG, "Starting image generation...");
+
+        boolean aborted = false;
         boolean waiting = true;
         do {
             // Dump logcat.
             final String logs = mDevice.executeAdbCommand(
                     "logcat", "-v", "brief", "-d", CLASS + ":I", "*:S");
+
             // Search for string.
             final Scanner in = new Scanner(logs);
             while (in.hasNextLine()) {
                 final String line = in.nextLine();
                 if (line.startsWith("I/" + CLASS)) {
                     final String[] lineSplit = line.split(":");
-                    final String s = lineSplit[1].trim();
-                    final String themeNameGenerated = lineSplit[2].trim();
-                    if (s.equals("OKAY") && themeNameGenerated.equals(themeName)) {
-                        waiting = false;
+                    if (lineSplit.length >= 3) {
+                        final String cmd = lineSplit[1].trim();
+                        final String arg = lineSplit[2].trim();
+                        switch (cmd) {
+                            case "FAIL":
+                                Log.logAndDisplay(LogLevel.WARN, LOG_TAG, line);
+                                Log.logAndDisplay(LogLevel.WARN, LOG_TAG, "Aborting! Check host logs for details.");
+                                aborted = true;
+                                // fall-through
+                            case "OKAY":
+                                waiting = false;
+                                break;
+                        }
                     }
                 }
             }
             in.close();
-        } while (waiting);
+        } while (waiting && !aborted);
+
+        Log.logAndDisplay(LogLevel.VERBOSE, LOG_TAG, "Image generation completed!");
+
+        return !aborted;
     }
 
-    private static String getDensityBucket(int density) {
+    private static String getDensityBucketForDevice(ITestDevice device) {
+        final String densityProp;
+        if (device.getSerialNumber().startsWith("emulator-")) {
+            densityProp = DENSITY_PROP_EMULATOR;
+        } else {
+            densityProp = DENSITY_PROP_DEVICE;
+        }
+
+        final int density;
+        try {
+            density = Integer.parseInt(device.getProperty(densityProp));
+        } catch (DeviceNotAvailableException e) {
+            return "unknown";
+        }
+
         switch (density) {
             case 120:
                 return "ldpi";
@@ -363,9 +320,7 @@
         if (hardwareTypeString.contains("android.hardware.type.watch")) {
             return true;
         }
-        if (hardwareTypeString.contains("android.hardware.type.television")) {
-            return true;
-        }
+
         return false;
     }
 }