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.ddmlib.Log; 20 import com.android.ddmlib.Log.LogLevel; 21 import com.android.tradefed.device.CollectingOutputReceiver; 22 import com.android.tradefed.device.DeviceNotAvailableException; 23 import com.android.tradefed.device.ITestDevice; 24 import com.android.tradefed.result.FileInputStreamSource; 25 import com.android.tradefed.result.InputStreamSource; 26 import com.android.tradefed.result.LogDataType; 27 import com.android.tradefed.testtype.DeviceTestCase; 28 import com.android.tradefed.util.Pair; 29 import com.android.tradefed.util.StreamUtil; 30 31 import java.io.File; 32 import java.io.FileInputStream; 33 import java.io.FileOutputStream; 34 import java.io.IOException; 35 import java.io.InputStream; 36 import java.util.HashMap; 37 import java.util.Map; 38 import java.util.concurrent.ExecutorCompletionService; 39 import java.util.concurrent.ExecutorService; 40 import java.util.concurrent.Executors; 41 import java.util.concurrent.TimeUnit; 42 import java.util.regex.Matcher; 43 import java.util.regex.Pattern; 44 import java.util.zip.ZipEntry; 45 import java.util.zip.ZipInputStream; 46 47 /** 48 * Test to check non-modifiable themes have not been changed. 49 */ 50 public class ThemeHostTest extends DeviceTestCase { 51 52 private static final String LOG_TAG = "ThemeHostTest"; 53 private static final String APP_PACKAGE_NAME = "android.theme.app"; 54 55 private static final String GENERATED_ASSETS_ZIP = "/sdcard/cts-theme-assets.zip"; 56 57 /** The class name of the main activity in the APK. */ 58 private static final String TEST_CLASS = "androidx.test.runner.AndroidJUnitRunner"; 59 60 /** The command to launch the main instrumentation test. */ 61 private static final String START_CMD = String.format( 62 "am instrument -w --no-isolated-storage --no-window-animation %s/%s", 63 APP_PACKAGE_NAME, TEST_CLASS); 64 65 private static final String CLEAR_GENERATED_CMD = "rm -rf %s/*.png"; 66 private static final String STOP_CMD = String.format("am force-stop %s", APP_PACKAGE_NAME); 67 private static final String HARDWARE_TYPE_CMD = "dumpsys | grep android.hardware.type"; 68 private static final String DENSITY_PROP_DEVICE = "ro.sf.lcd_density"; 69 private static final String DENSITY_PROP_EMULATOR = "qemu.sf.lcd_density"; 70 71 /** Shell command used to obtain current device density. */ 72 private static final String WM_DENSITY = "wm density"; 73 74 /** Overall test timeout is 30 minutes. Should only take about 5. */ 75 private static final int TEST_RESULT_TIMEOUT = 30 * 60 * 1000; 76 77 /** Map of reference image names and files. */ 78 private Map<String, File> mReferences; 79 80 /** A reference to the device under test. */ 81 private ITestDevice mDevice; 82 83 private ExecutorService mExecutionService; 84 85 private ExecutorCompletionService<Pair<String, File>> mCompletionService; 86 87 // Density to which the device should be restored, or -1 if unnecessary. 88 private int mRestoreDensity; 89 90 91 @Override setUp()92 protected void setUp() throws Exception { 93 super.setUp(); 94 95 mDevice = getDevice(); 96 mRestoreDensity = resetDensityIfNeeded(mDevice); 97 final String density = getDensityBucketForDevice(mDevice); 98 final String referenceZipAssetPath = String.format("/%s.zip", density); 99 mReferences = extractReferenceImages(referenceZipAssetPath); 100 101 final int numCores = Runtime.getRuntime().availableProcessors(); 102 mExecutionService = Executors.newFixedThreadPool(numCores * 2); 103 mCompletionService = new ExecutorCompletionService<>(mExecutionService); 104 } 105 extractReferenceImages(String zipFile)106 private Map<String, File> extractReferenceImages(String zipFile) throws Exception { 107 final Map<String, File> references = new HashMap<>(); 108 final InputStream zipStream = ThemeHostTest.class.getResourceAsStream(zipFile); 109 if (zipStream != null) { 110 try (ZipInputStream in = new ZipInputStream(zipStream)) { 111 final byte[] buffer = new byte[1024]; 112 for (ZipEntry ze; (ze = in.getNextEntry()) != null; ) { 113 final String name = ze.getName(); 114 final File tmp = File.createTempFile("ref_" + name, ".png"); 115 tmp.deleteOnExit(); 116 try (FileOutputStream out = new FileOutputStream(tmp)) { 117 for (int count; (count = in.read(buffer)) != -1; ) { 118 out.write(buffer, 0, count); 119 } 120 } 121 122 references.put(name, tmp); 123 } 124 } catch (IOException e) { 125 fail("Failed to unzip assets: " + zipFile); 126 } 127 } else { 128 if (checkHardwareTypeSkipTest(mDevice.executeShellCommand(HARDWARE_TYPE_CMD).trim())) { 129 Log.logAndDisplay(LogLevel.WARN, LOG_TAG, 130 "Could not obtain resources for skipped themes test: " + zipFile); 131 } else { 132 fail("Failed to get resource: " + zipFile); 133 } 134 } 135 136 return references; 137 } 138 139 @Override tearDown()140 protected void tearDown() throws Exception { 141 mExecutionService.shutdown(); 142 143 // Remove generated images. 144 mDevice.executeShellCommand(CLEAR_GENERATED_CMD); 145 146 restoreDensityIfNeeded(mDevice, mRestoreDensity); 147 148 super.tearDown(); 149 } 150 testThemes()151 public void testThemes() throws Exception { 152 if (checkHardwareTypeSkipTest(mDevice.executeShellCommand(HARDWARE_TYPE_CMD).trim())) { 153 Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "Skipped themes test for watch / TV / automotive"); 154 return; 155 } 156 157 if (mReferences.isEmpty()) { 158 Log.logAndDisplay(LogLevel.INFO, LOG_TAG, 159 "Skipped themes test due to missing reference images"); 160 return; 161 } 162 163 assertTrue("Aborted image generation, see device log for details", generateDeviceImages()); 164 165 // Pull ZIP file from remote device. 166 final File localZip = File.createTempFile("generated", ".zip"); 167 assertTrue("Failed to pull generated assets from device", 168 mDevice.pullFile(GENERATED_ASSETS_ZIP, localZip)); 169 170 final int numTasks = extractGeneratedImages(localZip, mReferences); 171 172 int failureCount = 0; 173 for (int i = numTasks; i > 0; i--) { 174 final Pair<String, File> comparison = mCompletionService.take().get(); 175 if (comparison != null) { 176 InputStreamSource inputStream = new FileInputStreamSource(comparison.second); 177 try{ 178 // Log the diff file 179 addTestLog(comparison.first, LogDataType.PNG, inputStream); 180 } finally { 181 StreamUtil.cancel(inputStream); 182 } 183 failureCount++; 184 } 185 } 186 187 assertTrue(failureCount + " failures in theme test", failureCount == 0); 188 } 189 extractGeneratedImages(File localZip, Map<String, File> references)190 private int extractGeneratedImages(File localZip, Map<String, File> references) 191 throws IOException { 192 int numTasks = 0; 193 194 // Extract generated images to temporary files. 195 final byte[] data = new byte[8192]; 196 try (ZipInputStream zipInput = new ZipInputStream(new FileInputStream(localZip))) { 197 for (ZipEntry entry; (entry = zipInput.getNextEntry()) != null; ) { 198 final String name = entry.getName(); 199 final File expected = references.get(name); 200 if (expected != null && expected.exists()) { 201 final File actual = File.createTempFile("actual_" + name, ".png"); 202 actual.deleteOnExit(); 203 204 try (FileOutputStream pngOutput = new FileOutputStream(actual)) { 205 for (int count; (count = zipInput.read(data, 0, data.length)) != -1; ) { 206 pngOutput.write(data, 0, count); 207 } 208 } 209 210 final String shortName = name.substring(0, name.indexOf('.')); 211 mCompletionService.submit(new ComparisonTask(shortName, expected, actual)); 212 numTasks++; 213 } else { 214 Log.logAndDisplay(LogLevel.INFO, LOG_TAG, 215 "Missing reference image for " + name); 216 } 217 218 zipInput.closeEntry(); 219 } 220 } 221 222 return numTasks; 223 } 224 generateDeviceImages()225 private boolean generateDeviceImages() throws Exception { 226 // Stop any existing instances. 227 mDevice.executeShellCommand(STOP_CMD); 228 229 // Start instrumentation test. 230 final CollectingOutputReceiver receiver = new CollectingOutputReceiver(); 231 mDevice.executeShellCommand(START_CMD, receiver, TEST_RESULT_TIMEOUT, 232 TimeUnit.MILLISECONDS, 0); 233 234 return receiver.getOutput().contains("OK "); 235 } 236 getDensityBucketForDevice(ITestDevice device)237 private static String getDensityBucketForDevice(ITestDevice device) { 238 final int density; 239 try { 240 density = getDensityForDevice(device); 241 } catch (DeviceNotAvailableException e) { 242 throw new RuntimeException("Failed to detect device density", e); 243 } 244 final String bucket; 245 switch (density) { 246 case 120: 247 bucket = "ldpi"; 248 break; 249 case 160: 250 bucket = "mdpi"; 251 break; 252 case 213: 253 bucket = "tvdpi"; 254 break; 255 case 240: 256 bucket = "hdpi"; 257 break; 258 case 320: 259 bucket = "xhdpi"; 260 break; 261 case 480: 262 bucket = "xxhdpi"; 263 break; 264 case 640: 265 bucket = "xxxhdpi"; 266 break; 267 default: 268 bucket = density + "dpi"; 269 break; 270 } 271 272 Log.logAndDisplay(LogLevel.INFO, LOG_TAG, 273 "Device density detected as " + density + " (" + bucket + ")"); 274 return bucket; 275 } 276 resetDensityIfNeeded(ITestDevice device)277 private static int resetDensityIfNeeded(ITestDevice device) throws DeviceNotAvailableException { 278 final String output = device.executeShellCommand(WM_DENSITY); 279 final Pattern p = Pattern.compile("Override density: (\\d+)"); 280 final Matcher m = p.matcher(output); 281 if (m.find()) { 282 device.executeShellCommand(WM_DENSITY + " reset"); 283 int restoreDensity = Integer.parseInt(m.group(1)); 284 return restoreDensity; 285 } 286 return -1; 287 } 288 restoreDensityIfNeeded(ITestDevice device, int restoreDensity)289 private static void restoreDensityIfNeeded(ITestDevice device, int restoreDensity) 290 throws DeviceNotAvailableException { 291 if (restoreDensity > 0) { 292 device.executeShellCommand(WM_DENSITY + " " + restoreDensity); 293 } 294 } 295 getDensityForDevice(ITestDevice device)296 private static int getDensityForDevice(ITestDevice device) throws DeviceNotAvailableException { 297 final String densityProp; 298 if (device.getSerialNumber().startsWith("emulator-")) { 299 densityProp = DENSITY_PROP_EMULATOR; 300 } else { 301 densityProp = DENSITY_PROP_DEVICE; 302 } 303 return Integer.parseInt(device.getProperty(densityProp)); 304 } 305 checkHardwareTypeSkipTest(String hardwareTypeString)306 private static boolean checkHardwareTypeSkipTest(String hardwareTypeString) { 307 return hardwareTypeString.contains("android.hardware.type.watch") 308 || hardwareTypeString.contains("android.hardware.type.television") 309 || hardwareTypeString.contains("android.hardware.type.automotive"); 310 } 311 } 312