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