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 
68     /** Shell command used to obtain current device density. */
69     private static final String WM_DENSITY = "wm density";
70 
71     /** Overall test timeout is 30 minutes. Should only take about 5. */
72     private static final int TEST_RESULT_TIMEOUT = 30 * 60 * 1000;
73 
74     /** Map of reference image names and files. */
75     private Map<String, File> mReferences;
76 
77     /** A reference to the device under test. */
78     private ITestDevice mDevice;
79 
80     private ExecutorService mExecutionService;
81 
82     private ExecutorCompletionService<Pair<String, File>> mCompletionService;
83 
84     // Density to which the device should be restored, or -1 if unnecessary.
85     private int mRestoreDensity;
86 
87 
88     @Override
setUp()89     protected void setUp() throws Exception {
90         super.setUp();
91 
92         mDevice = getDevice();
93         mRestoreDensity = resetDensityIfNeeded(mDevice);
94         final String density = getDensityBucketForDevice(mDevice);
95         final String referenceZipAssetPath = String.format("/%s.zip", density);
96         mReferences = extractReferenceImages(referenceZipAssetPath);
97 
98         final int numCores = Runtime.getRuntime().availableProcessors();
99         mExecutionService = Executors.newFixedThreadPool(numCores * 2);
100         mCompletionService = new ExecutorCompletionService<>(mExecutionService);
101     }
102 
extractReferenceImages(String zipFile)103     private Map<String, File> extractReferenceImages(String zipFile) throws Exception {
104         final Map<String, File> references = new HashMap<>();
105         final InputStream zipStream = ThemeHostTest.class.getResourceAsStream(zipFile);
106         if (zipStream != null) {
107             try (ZipInputStream in = new ZipInputStream(zipStream)) {
108                 final byte[] buffer = new byte[1024];
109                 for (ZipEntry ze; (ze = in.getNextEntry()) != null; ) {
110                     final String name = ze.getName();
111                     final File tmp = File.createTempFile("ref_" + name, ".png");
112                     tmp.deleteOnExit();
113                     try (FileOutputStream out = new FileOutputStream(tmp)) {
114                         for (int count; (count = in.read(buffer)) != -1; ) {
115                             out.write(buffer, 0, count);
116                         }
117                     }
118 
119                     references.put(name, tmp);
120                 }
121             } catch (IOException e) {
122                 fail("Failed to unzip assets: " + zipFile);
123             }
124         } else {
125             if (checkHardwareTypeSkipTest()) {
126                 Log.logAndDisplay(LogLevel.WARN, LOG_TAG,
127                         "Could not obtain resources for skipped themes test: " + zipFile);
128             } else {
129                 fail("Failed to get resource: " + zipFile);
130             }
131         }
132 
133         return references;
134     }
135 
136     @Override
tearDown()137     protected void tearDown() throws Exception {
138         mExecutionService.shutdown();
139 
140         // Remove generated images.
141         mDevice.executeShellCommand(CLEAR_GENERATED_CMD);
142 
143         restoreDensityIfNeeded(mDevice, mRestoreDensity);
144 
145         super.tearDown();
146     }
147 
testThemes()148     public void testThemes() throws Exception {
149         if (checkHardwareTypeSkipTest()) {
150             Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "Skipped themes test for watch / TV / automotive");
151             return;
152         }
153         if (mReferences.isEmpty()) {
154             Log.logAndDisplay(LogLevel.INFO, LOG_TAG,
155                     "Skipped themes test due to missing reference images");
156             return;
157         }
158 
159         assertTrue("Aborted image generation, see device log for details", generateDeviceImages());
160 
161         // Pull ZIP file from remote device.
162         final File localZip = File.createTempFile("generated", ".zip");
163         assertTrue("Failed to pull generated assets from device",
164                 mDevice.pullFile(GENERATED_ASSETS_ZIP, localZip));
165 
166         final int numTasks = extractGeneratedImages(localZip, mReferences);
167 
168         int failureCount = 0;
169         for (int i = numTasks; i > 0; i--) {
170             final Pair<String, File> comparison = mCompletionService.take().get();
171             if (comparison != null) {
172                 InputStreamSource inputStream = new FileInputStreamSource(comparison.second);
173                 try{
174                     // Log the diff file
175                     addTestLog(comparison.first, LogDataType.PNG, inputStream);
176                 } finally {
177                     StreamUtil.cancel(inputStream);
178                 }
179                 failureCount++;
180             }
181         }
182 
183         assertTrue(failureCount + " failures in theme test", failureCount == 0);
184     }
185 
extractGeneratedImages(File localZip, Map<String, File> references)186     private int extractGeneratedImages(File localZip, Map<String, File> references)
187             throws IOException {
188         int numTasks = 0;
189 
190         // Extract generated images to temporary files.
191         final byte[] data = new byte[8192];
192         try (ZipInputStream zipInput = new ZipInputStream(new FileInputStream(localZip))) {
193             for (ZipEntry entry; (entry = zipInput.getNextEntry()) != null; ) {
194                 final String name = entry.getName();
195                 final File expected = references.get(name);
196                 if (expected != null && expected.exists()) {
197                     final File actual = File.createTempFile("actual_" + name, ".png");
198                     actual.deleteOnExit();
199 
200                     try (FileOutputStream pngOutput = new FileOutputStream(actual)) {
201                         for (int count; (count = zipInput.read(data, 0, data.length)) != -1; ) {
202                             pngOutput.write(data, 0, count);
203                         }
204                     }
205 
206                     final String shortName = name.substring(0, name.indexOf('.'));
207                     mCompletionService.submit(new ComparisonTask(shortName, expected, actual));
208                     numTasks++;
209                 } else {
210                     Log.logAndDisplay(LogLevel.INFO, LOG_TAG,
211                             "Missing reference image for " + name);
212                 }
213 
214                 zipInput.closeEntry();
215             }
216         }
217 
218         return numTasks;
219     }
220 
generateDeviceImages()221     private boolean generateDeviceImages() throws Exception {
222         // Stop any existing instances.
223         mDevice.executeShellCommand(STOP_CMD);
224 
225         // try a maximum of 3 times
226         for (int i = 0; i < 3; i++) {
227             final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
228             final Exception[] exception = new Exception[1];
229             Thread thread = new Thread(() -> {
230                 // Start instrumentation test.
231                 try {
232                     mDevice.executeShellCommand(START_CMD, receiver, TEST_RESULT_TIMEOUT,
233                             TimeUnit.MILLISECONDS, 0);
234                 } catch (Exception e) {
235                     exception[0] = e;
236                 }
237             });
238             thread.start();
239             try {
240                 // Wait 30 seconds to see if the test starts.
241                 thread.join(30_000);
242             } catch (InterruptedException e) {
243                 // will retry if the test failed to start
244             }
245             if (exception[0] != null) {
246                 throw exception[0];
247             }
248             if (!receiver.getOutput().contains("ReferenceImagesTest")) {
249                 // Stop the test and try again
250                 mDevice.executeShellCommand(STOP_CMD);
251                 thread.join();
252             } else {
253                 thread.join(); // test was started, so wait for it to finish
254                 return receiver.getOutput().contains("OK ");
255             }
256         }
257         return false; // Tried 3 times and failed to execute it all 3 times
258     }
259 
getDensityBucketForDevice(ITestDevice device)260     private static String getDensityBucketForDevice(ITestDevice device) {
261         final int density;
262         try {
263             density = getDensityForDevice(device);
264         } catch (DeviceNotAvailableException e) {
265             throw new RuntimeException("Failed to detect device density", e);
266         }
267         final String bucket;
268         switch (density) {
269             case 120:
270                 bucket = "ldpi";
271                 break;
272             case 160:
273                 bucket = "mdpi";
274                 break;
275             case 213:
276                 bucket = "tvdpi";
277                 break;
278             case 240:
279                 bucket = "hdpi";
280                 break;
281             case 320:
282                 bucket = "xhdpi";
283                 break;
284             case 480:
285                 bucket = "xxhdpi";
286                 break;
287             case 640:
288                 bucket = "xxxhdpi";
289                 break;
290             default:
291                 bucket = density + "dpi";
292                 break;
293         }
294 
295         Log.logAndDisplay(LogLevel.INFO, LOG_TAG,
296                 "Device density detected as " + density + " (" + bucket + ")");
297         return bucket;
298     }
299 
resetDensityIfNeeded(ITestDevice device)300     private static int resetDensityIfNeeded(ITestDevice device) throws DeviceNotAvailableException {
301         final String output = device.executeShellCommand(WM_DENSITY);
302          final Pattern p = Pattern.compile("Override density: (\\d+)");
303          final Matcher m = p.matcher(output);
304          if (m.find()) {
305              device.executeShellCommand(WM_DENSITY + " reset");
306              int restoreDensity = Integer.parseInt(m.group(1));
307              return restoreDensity;
308          }
309          return -1;
310     }
311 
restoreDensityIfNeeded(ITestDevice device, int restoreDensity)312     private static void restoreDensityIfNeeded(ITestDevice device, int restoreDensity)
313             throws DeviceNotAvailableException {
314         if (restoreDensity > 0) {
315             device.executeShellCommand(WM_DENSITY + " " + restoreDensity);
316         }
317     }
318 
getDensityForDevice(ITestDevice device)319     private static int getDensityForDevice(ITestDevice device) throws DeviceNotAvailableException {
320         final String output = device.executeShellCommand(WM_DENSITY);
321         final Pattern p = Pattern.compile("Physical density: (\\d+)");
322         final Matcher m = p.matcher(output);
323         if (m.find()) {
324             return Integer.parseInt(m.group(1));
325         }
326         throw new RuntimeException("Failed to detect device density");
327     }
328 
checkHardwareTypeSkipTest()329     private boolean checkHardwareTypeSkipTest() {
330         try {
331          if( mDevice.hasFeature("feature:android.hardware.type.watch")
332                  || mDevice.hasFeature("feature:android.hardware.type.television")
333                  || mDevice.hasFeature("feature:android.hardware.type.automotive")) {
334              return true;
335          }
336         } catch (DeviceNotAvailableException ex) {
337              return false;
338         }
339         return false;
340     }
341 
isEmulator(ITestDevice device)342     private static boolean isEmulator(ITestDevice device) {
343         // Expecting something like "emulator-XXXX" or "EMULATORXXXX".
344         return device.getSerialNumber().toLowerCase().startsWith("emulator");
345     }
346 }
347