• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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