1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.theme.cts; 18 19 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 20 import com.android.ddmlib.Log; 21 import com.android.ddmlib.Log.LogLevel; 22 import com.android.tradefed.build.IBuildInfo; 23 import com.android.tradefed.device.CollectingOutputReceiver; 24 import com.android.tradefed.device.DeviceNotAvailableException; 25 import com.android.tradefed.device.ITestDevice; 26 import com.android.tradefed.result.LogDataType; 27 import com.android.tradefed.result.LogFileSaver; 28 import com.android.tradefed.testtype.DeviceTestCase; 29 import com.android.tradefed.testtype.IAbi; 30 import com.android.tradefed.testtype.IAbiReceiver; 31 import com.android.tradefed.testtype.IBuildReceiver; 32 import com.android.tradefed.util.AbiUtils; 33 import com.android.tradefed.util.Pair; 34 35 import java.io.File; 36 import java.io.FileInputStream; 37 import java.io.FileOutputStream; 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.util.HashMap; 41 import java.util.Map; 42 import java.util.concurrent.ExecutorCompletionService; 43 import java.util.concurrent.ExecutorService; 44 import java.util.concurrent.Executors; 45 import java.util.concurrent.TimeUnit; 46 import java.util.regex.Matcher; 47 import java.util.regex.Pattern; 48 import java.util.zip.ZipEntry; 49 import java.util.zip.ZipInputStream; 50 51 /** 52 * Test to check non-modifiable themes have not been changed. 53 */ 54 public class ThemeHostTest extends DeviceTestCase implements IAbiReceiver, IBuildReceiver { 55 56 private static final String LOG_TAG = "ThemeHostTest"; 57 private static final String APK_NAME = "CtsThemeDeviceApp"; 58 private static final String APP_PACKAGE_NAME = "android.theme.app"; 59 60 private static final String GENERATED_ASSETS_ZIP = "/sdcard/cts-theme-assets.zip"; 61 62 /** The class name of the main activity in the APK. */ 63 private static final String TEST_CLASS = "android.support.test.runner.AndroidJUnitRunner"; 64 65 /** The command to launch the main instrumentation test. */ 66 private static final String START_CMD = String.format( 67 "am instrument -w --no-window-animation %s/%s", APP_PACKAGE_NAME, TEST_CLASS); 68 69 private static final String CLEAR_GENERATED_CMD = "rm -rf %s/*.png"; 70 private static final String STOP_CMD = String.format("am force-stop %s", APP_PACKAGE_NAME); 71 private static final String HARDWARE_TYPE_CMD = "dumpsys | grep android.hardware.type"; 72 private static final String DENSITY_PROP_DEVICE = "ro.sf.lcd_density"; 73 private static final String DENSITY_PROP_EMULATOR = "qemu.sf.lcd_density"; 74 75 /** Shell command used to obtain current device density. */ 76 private static final String WM_DENSITY = "wm density"; 77 78 /** Overall test timeout is 30 minutes. Should only take about 5. */ 79 private static final int TEST_RESULT_TIMEOUT = 30 * 60 * 1000; 80 81 /** Map of reference image names and files. */ 82 private Map<String, File> mReferences; 83 84 /** The ABI to use. */ 85 private IAbi mAbi; 86 87 /** A reference to the build. */ 88 private IBuildInfo mBuildInfo; 89 90 /** A reference to the device under test. */ 91 private ITestDevice mDevice; 92 93 private ExecutorService mExecutionService; 94 95 private ExecutorCompletionService<Pair<String, File>> mCompletionService; 96 97 /** the string identifying the hardware type. */ 98 private String mHardwareType; 99 100 private LogFileSaver mDiffsFileSaver; 101 102 @Override setAbi(IAbi abi)103 public void setAbi(IAbi abi) { 104 mAbi = abi; 105 } 106 107 @Override setBuild(IBuildInfo buildInfo)108 public void setBuild(IBuildInfo buildInfo) { 109 mBuildInfo = buildInfo; 110 } 111 112 @Override setUp()113 protected void setUp() throws Exception { 114 super.setUp(); 115 116 mDevice = getDevice(); 117 mDevice.uninstallPackage(APP_PACKAGE_NAME); 118 mHardwareType = mDevice.executeShellCommand(HARDWARE_TYPE_CMD).trim(); 119 120 final CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuildInfo); 121 final File diffsDir = new File(buildHelper.getResultDir(), "diffs"); 122 mDiffsFileSaver = new LogFileSaver(diffsDir); 123 124 final File testApk = buildHelper.getTestFile(String.format("%s.apk", APK_NAME)); 125 final String abiFlag = AbiUtils.createAbiFlag(mAbi.getName()); 126 mDevice.installPackage(testApk, true, true, abiFlag); 127 128 final String density = getDensityBucketForDevice(mDevice, mHardwareType); 129 final String referenceZipAssetPath = String.format("/%s.zip", density); 130 mReferences = extractReferenceImages(referenceZipAssetPath); 131 132 final int numCores = Runtime.getRuntime().availableProcessors(); 133 mExecutionService = Executors.newFixedThreadPool(numCores * 2); 134 mCompletionService = new ExecutorCompletionService<>(mExecutionService); 135 } 136 extractReferenceImages(String zipFile)137 private Map<String, File> extractReferenceImages(String zipFile) throws Exception { 138 final Map<String, File> references = new HashMap<>(); 139 final InputStream zipStream = ThemeHostTest.class.getResourceAsStream(zipFile); 140 if (zipStream != null) { 141 try (ZipInputStream in = new ZipInputStream(zipStream)) { 142 final byte[] buffer = new byte[1024]; 143 for (ZipEntry ze; (ze = in.getNextEntry()) != null; ) { 144 final String name = ze.getName(); 145 final File tmp = File.createTempFile("ref_" + name, ".png"); 146 tmp.deleteOnExit(); 147 148 try (FileOutputStream out = new FileOutputStream(tmp)) { 149 for (int count; (count = in.read(buffer)) != -1; ) { 150 out.write(buffer, 0, count); 151 } 152 } 153 154 references.put(name, tmp); 155 } 156 } catch (IOException e) { 157 fail("Failed to unzip assets: " + zipFile); 158 } 159 } else { 160 if (checkHardwareTypeSkipTest(mHardwareType)) { 161 Log.logAndDisplay(LogLevel.WARN, LOG_TAG, 162 "Could not obtain resources for skipped themes test: " + zipFile); 163 } else { 164 fail("Failed to get resource: " + zipFile); 165 } 166 } 167 168 return references; 169 } 170 171 @Override tearDown()172 protected void tearDown() throws Exception { 173 mExecutionService.shutdown(); 174 175 // Remove the APK. 176 mDevice.uninstallPackage(APP_PACKAGE_NAME); 177 178 // Remove generated images. 179 mDevice.executeShellCommand(CLEAR_GENERATED_CMD); 180 181 super.tearDown(); 182 } 183 testThemes()184 public void testThemes() throws Exception { 185 if (checkHardwareTypeSkipTest(mHardwareType)) { 186 Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "Skipped themes test for watch / TV"); 187 return; 188 } 189 190 if (mReferences.isEmpty()) { 191 Log.logAndDisplay(LogLevel.INFO, LOG_TAG, 192 "Skipped themes test due to missing reference images"); 193 return; 194 } 195 196 assertTrue("Aborted image generation", generateDeviceImages()); 197 198 // Pull ZIP file from remote device. 199 final File localZip = File.createTempFile("generated", ".zip"); 200 assertTrue("Failed to pull generated assets from device", 201 mDevice.pullFile(GENERATED_ASSETS_ZIP, localZip)); 202 203 final int numTasks = extractGeneratedImages(localZip, mReferences); 204 205 int failureCount = 0; 206 for (int i = numTasks; i > 0; i--) { 207 final Pair<String, File> comparison = mCompletionService.take().get(); 208 if (comparison != null) { 209 try (InputStream inputStream = new FileInputStream(comparison.second)) { 210 mDiffsFileSaver.saveLogData(comparison.first, LogDataType.PNG, inputStream); 211 } 212 failureCount++; 213 } 214 } 215 216 assertTrue(failureCount + " failures in theme test", failureCount == 0); 217 } 218 extractGeneratedImages(File localZip, Map<String, File> references)219 private int extractGeneratedImages(File localZip, Map<String, File> references) 220 throws IOException { 221 int numTasks = 0; 222 223 // Extract generated images to temporary files. 224 final byte[] data = new byte[8192]; 225 try (ZipInputStream zipInput = new ZipInputStream(new FileInputStream(localZip))) { 226 for (ZipEntry entry; (entry = zipInput.getNextEntry()) != null; ) { 227 final String name = entry.getName(); 228 final File expected = references.get(name); 229 if (expected != null && expected.exists()) { 230 final File actual = File.createTempFile("actual_" + name, ".png"); 231 actual.deleteOnExit(); 232 233 try (FileOutputStream pngOutput = new FileOutputStream(actual)) { 234 for (int count; (count = zipInput.read(data, 0, data.length)) != -1; ) { 235 pngOutput.write(data, 0, count); 236 } 237 } 238 239 final String shortName = name.substring(0, name.indexOf('.')); 240 mCompletionService.submit(new ComparisonTask(shortName, expected, actual)); 241 numTasks++; 242 } else { 243 Log.logAndDisplay(LogLevel.INFO, LOG_TAG, 244 "Missing reference image for " + name); 245 } 246 247 zipInput.closeEntry(); 248 } 249 } 250 251 return numTasks; 252 } 253 generateDeviceImages()254 private boolean generateDeviceImages() throws Exception { 255 // Stop any existing instances. 256 mDevice.executeShellCommand(STOP_CMD); 257 258 // Start instrumentation test. 259 final CollectingOutputReceiver receiver = new CollectingOutputReceiver(); 260 mDevice.executeShellCommand(START_CMD, receiver, TEST_RESULT_TIMEOUT, 261 TimeUnit.MILLISECONDS, 0); 262 263 return receiver.getOutput().contains("OK "); 264 } 265 getDensityBucketForDevice(ITestDevice device, String hardwareType)266 private static String getDensityBucketForDevice(ITestDevice device, String hardwareType) { 267 if (hardwareType.contains("android.hardware.type.television")) { 268 // references images for tv are under bucket "tvdpi". 269 return "tvdpi"; 270 } 271 final int density; 272 try { 273 density = getDensityForDevice(device); 274 } catch (DeviceNotAvailableException e) { 275 throw new RuntimeException("Failed to detect device density", e); 276 } 277 final String bucket; 278 switch (density) { 279 case 120: 280 bucket = "ldpi"; 281 break; 282 case 160: 283 bucket = "mdpi"; 284 break; 285 case 240: 286 bucket = "hdpi"; 287 break; 288 case 320: 289 bucket = "xhdpi"; 290 break; 291 case 480: 292 bucket = "xxhdpi"; 293 break; 294 case 640: 295 bucket = "xxxhdpi"; 296 break; 297 default: 298 bucket = density + "dpi"; 299 break; 300 } 301 302 Log.logAndDisplay(LogLevel.INFO, LOG_TAG, 303 "Device density detected as " + density + " (" + bucket + ")"); 304 return bucket; 305 } 306 getDensityForDevice(ITestDevice device)307 private static int getDensityForDevice(ITestDevice device) throws DeviceNotAvailableException { 308 final String output = device.executeShellCommand(WM_DENSITY); 309 final Pattern p = Pattern.compile("Override density: (\\d+)"); 310 final Matcher m = p.matcher(output); 311 if (m.find()) { 312 return Integer.parseInt(m.group(1)); 313 } 314 315 final String densityProp; 316 if (device.getSerialNumber().startsWith("emulator-")) { 317 densityProp = DENSITY_PROP_EMULATOR; 318 } else { 319 densityProp = DENSITY_PROP_DEVICE; 320 } 321 return Integer.parseInt(device.getProperty(densityProp)); 322 } 323 checkHardwareTypeSkipTest(String hardwareTypeString)324 private static boolean checkHardwareTypeSkipTest(String hardwareTypeString) { 325 return hardwareTypeString.contains("android.hardware.type.watch") 326 || hardwareTypeString.contains("android.hardware.type.television"); 327 } 328 } 329