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;
}
}