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