• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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 com.android.cts.verifier;
18 
19 import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode;
20 import static com.android.cts.verifier.TestListActivity.sInitialLaunch;
21 
22 import android.annotation.SuppressLint;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.ActivityInfo;
26 import android.content.pm.PackageManager;
27 import android.content.pm.ResolveInfo;
28 import android.content.res.Resources;
29 import android.hardware.SensorPrivacyManager;
30 import android.os.Bundle;
31 import android.telephony.TelephonyManager;
32 import android.util.Log;
33 import android.widget.ListView;
34 
35 import com.android.cts.verifier.TestListActivity.DisplayMode;
36 
37 import java.lang.reflect.InvocationTargetException;
38 import java.lang.reflect.Method;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.Collections;
42 import java.util.Comparator;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.stream.Collectors;
48 
49 /**
50  * {@link TestListAdapter} that populates the {@link TestListActivity}'s {@link ListView} by
51  * reading data from the CTS Verifier's AndroidManifest.xml.
52  * <p>
53  * Making a new test activity to appear in the list requires the following steps:
54  *
55  * <ol>
56  *     <li>REQUIRED: Add an activity to the AndroidManifest.xml with an intent filter with a
57  *         main action and the MANUAL_TEST category.
58  *         <pre>
59  *             <intent-filter>
60  *                <action android:name="android.intent.action.MAIN" />
61  *                <category android:name="android.cts.intent.category.MANUAL_TEST" />
62  *             </intent-filter>
63  *         </pre>
64  *     </li>
65  *     <li>REQUIRED: Add a meta data attribute to indicate which display modes of tests the activity
66  *         should belong to. "single_display_mode" indicates a test is only needed to run on the
67  *         main display mode (i.e. unfolded), and "multi_display_mode" indicates a test is required
68  *         to run under both modes (i.e. both folded and unfolded).If you don't add this attribute,
69  *         your test will show up in both unfolded and folded modes.
70  *         <pre>
71  *             <meta-data android:name="display_mode" android:value="multi_display_mode" />
72  *         </pre>
73  *     </li>
74  *     <li>OPTIONAL: Add a meta data attribute to indicate what category of tests the activity
75  *         should belong to. If you don't add this attribute, your test will show up in the
76  *         "Other" tests category.
77  *         <pre>
78  *             <meta-data android:name="test_category" android:value="@string/test_category_security" />
79  *         </pre>
80  *     </li>
81  *     <li>OPTIONAL: Add a meta data attribute to indicate whether this test has a parent test.
82  *         <pre>
83  *             <meta-data android:name="test_parent" android:value="com.android.cts.verifier.bluetooth.BluetoothTestActivity" />
84  *         </pre>
85  *     </li>
86  *     <li>OPTIONAL: Add a meta data attribute to indicate what features are required to run the
87  *         test. If the device does not have all of the required features then it will not appear
88  *         in the test list. Use a colon (:) to specify multiple required features.
89  *         <pre>
90  *             <meta-data android:name="test_required_features" android:value="android.hardware.sensor.accelerometer" />
91  *         </pre>
92  *     </li>
93  *     <li>OPTIONAL: Add a meta data attribute to indicate features such that, if any present, the
94  *         test gets excluded from being shown. If the device has any of the excluded features then
95  *         the test will not appear in the test list. Use a colon (:) to specify multiple features
96  *         to exclude for the test. Note that the colon means "or" in this case.
97  *         <pre>
98  *             <meta-data android:name="test_excluded_features" android:value="android.hardware.type.television" />
99  *         </pre>
100  *     </li>
101  *     <li>OPTIONAL: Add a meta data attribute to indicate features such that, if any present,
102  *         the test is applicable to run. If the device has any of the applicable features then
103  *         the test will appear in the test list. Use a colon (:) to specify multiple features
104  *         <pre>
105  *             <meta-data android:name="test_applicable_features" android:value="android.hardware.sensor.compass" />
106  *         </pre>
107  *     </li>
108  *     <li>OPTIONAL: Add a meta data attribute to indicate which intent actions are required to run
109  *         the test. If the device does not have activities that handle all those actions, then it
110  *         will not appear in the test list. Use a colon (:) to specify multiple required intent actions.
111  *         <pre>
112  *             <meta-data android:name="test_required_actions" android:value="android.app.action.ADD_DEVICE_ADMIN" />
113  *         </pre>
114  *     </li>
115  *
116  * </ol>
117  */
118 public class ManifestTestListAdapter extends TestListAdapter {
119     private static final String LOG_TAG = "ManifestTestListAdapter";
120 
121     private static final String TEST_CATEGORY_META_DATA = "test_category";
122 
123     private static final String TEST_PARENT_META_DATA = "test_parent";
124 
125     private static final String TEST_REQUIRED_FEATURES_META_DATA = "test_required_features";
126 
127     private static final String TEST_EXCLUDED_FEATURES_META_DATA = "test_excluded_features";
128 
129     private static final String TEST_APPLICABLE_FEATURES_META_DATA = "test_applicable_features";
130 
131     private static final String TEST_REQUIRED_CONFIG_META_DATA = "test_required_configs";
132 
133     private static final String TEST_REQUIRED_ACTIONS_META_DATA = "test_required_actions";
134 
135     private static final String TEST_DISPLAY_MODE_META_DATA = "display_mode";
136 
137     private static final String CONFIG_NO_EMULATOR = "config_no_emulator";
138 
139     private static final String CONFIG_VOICE_CAPABLE = "config_voice_capable";
140 
141     private static final String CONFIG_HAS_RECENTS = "config_has_recents";
142 
143     private static final String CONFIG_HDMI_SOURCE = "config_hdmi_source";
144 
145     private static final String CONFIG_QUICK_SETTINGS_SUPPORTED = "config_quick_settings_supported";
146 
147     private static final String CONFIG_HAS_MIC_TOGGLE = "config_has_mic_toggle";
148 
149     private static final String CONFIG_HAS_CAMERA_TOGGLE = "config_has_camera_toggle";
150 
151     /** The config to represent that a test is only needed to run in the main display mode
152      * (i.e. unfolded) */
153     private static final String SINGLE_DISPLAY_MODE = "single_display_mode";
154 
155     /** The config to represent that a test is needed to run in the multiple display modes
156      * (i.e. both unfolded and folded) */
157     private static final String MULTIPLE_DISPLAY_MODE = "multi_display_mode";
158 
159     private final HashSet<String> mDisabledTests;
160 
161     private Context mContext;
162 
163     private String mTestParent;
164 
ManifestTestListAdapter(Context context, String testParent, String[] disabledTestArray)165     public ManifestTestListAdapter(Context context, String testParent, String[] disabledTestArray) {
166         super(context);
167         mContext = context;
168         mTestParent = testParent;
169         mDisabledTests = new HashSet<>(disabledTestArray.length);
170         for (int i = 0; i < disabledTestArray.length; i++) {
171             mDisabledTests.add(disabledTestArray[i]);
172         }
173     }
174 
ManifestTestListAdapter(Context context, String testParent)175     public ManifestTestListAdapter(Context context, String testParent) {
176         this(context, testParent, context.getResources().getStringArray(R.array.disabled_tests));
177     }
178 
179     @Override
getRows()180     protected List<TestListItem> getRows() {
181         List<TestListItem> allRows = new ArrayList<TestListItem>();
182 
183         // When launching at the first time or after killing the process, needs to fetch the
184         // test items of all display modes as the bases for switching.
185         if (mDisplayModesTests.isEmpty()) {
186             for (DisplayMode mode : DisplayMode.values()) {
187                 allRows = getRowsWithDisplayMode(mode.toString());
188                 mDisplayModesTests.put(mode.toString(), allRows);
189             }
190         }
191 
192         if (!sInitialLaunch) {
193             return getRowsWithDisplayMode(sCurrentDisplayMode);
194         }
195         return allRows;
196     }
197 
198     /**
199      * Gets all rows based on the specific display mode.
200      *
201      * @param mode Given display mode.
202      * @return A list containing all test itmes in the given display mode.
203      */
getRowsWithDisplayMode(String mode)204     private List<TestListItem> getRowsWithDisplayMode (String mode) {
205         /*
206          * 1. Get all the tests belonging to the test parent.
207          * 2. Get all the tests keyed by their category.
208          * 3. Flatten the tests and categories into one giant list for the list view.
209          */
210         List<TestListItem> allRows = new ArrayList<TestListItem>();
211         List<ResolveInfo> infos = getResolveInfosForParent();
212         Map<String, List<TestListItem>> testsByCategory = getTestsByCategory(infos);
213 
214         List<String> testCategories = new ArrayList<String>(testsByCategory.keySet());
215         Collections.sort(testCategories);
216         for (String testCategory : testCategories) {
217             List<TestListItem> tests = filterTests(testsByCategory.get(testCategory), mode);
218             if (!tests.isEmpty()) {
219                 allRows.add(TestListItem.newCategory(testCategory));
220                 Collections.sort(tests, Comparator.comparing(item -> item.title));
221                 allRows.addAll(tests);
222             }
223         }
224         return allRows;
225     }
226 
getResolveInfosForParent()227     List<ResolveInfo> getResolveInfosForParent() {
228         Intent mainIntent = new Intent(Intent.ACTION_MAIN);
229         mainIntent.addCategory(CATEGORY_MANUAL_TEST);
230         mainIntent.setPackage(mContext.getPackageName());
231 
232         PackageManager packageManager = mContext.getPackageManager();
233         List<ResolveInfo> list = packageManager.queryIntentActivities(mainIntent,
234                 PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
235         int size = list.size();
236 
237         List<ResolveInfo> matchingList = new ArrayList<>();
238         for (int i = 0; i < size; i++) {
239             ResolveInfo info = list.get(i);
240             String parent = getTestParent(info.activityInfo.metaData);
241             if ((mTestParent == null && parent == null)
242                     || (mTestParent != null && mTestParent.equals(parent))) {
243                 matchingList.add(info);
244             }
245         }
246         return matchingList;
247     }
248 
getTestsByCategory(List<ResolveInfo> list)249     Map<String, List<TestListItem>> getTestsByCategory(List<ResolveInfo> list) {
250         Map<String, List<TestListItem>> testsByCategory = new HashMap<>();
251 
252         int size = list.size();
253         for (int i = 0; i < size; i++) {
254             ResolveInfo info = list.get(i);
255             if (info.activityInfo == null || mDisabledTests.contains(info.activityInfo.name)) {
256                 Log.w(LOG_TAG, "ignoring disabled test: " + info.activityInfo.name);
257                 continue;
258             }
259             String title = getTitle(mContext, info.activityInfo);
260             String testName = info.activityInfo.name;
261             Intent intent = getActivityIntent(info.activityInfo);
262             String[] requiredFeatures = getRequiredFeatures(info.activityInfo.metaData);
263             String[] requiredConfigs = getRequiredConfigs(info.activityInfo.metaData);
264             String[] requiredActions = getRequiredActions(info.activityInfo.metaData);
265             String[] excludedFeatures = getExcludedFeatures(info.activityInfo.metaData);
266             String[] applicableFeatures = getApplicableFeatures(info.activityInfo.metaData);
267             String displayMode = getDisplayMode(info.activityInfo.metaData);
268 
269             TestListItem item = TestListItem.newTest(title, testName, intent, requiredFeatures,
270                      requiredConfigs, requiredActions, excludedFeatures, applicableFeatures,
271                      displayMode);
272 
273             String testCategory = getTestCategory(mContext, info.activityInfo.metaData);
274             addTestToCategory(testsByCategory, testCategory, item);
275         }
276 
277         return testsByCategory;
278     }
279 
getTestCategory(Context context, Bundle metaData)280     static String getTestCategory(Context context, Bundle metaData) {
281         String testCategory = null;
282         if (metaData != null) {
283             testCategory = metaData.getString(TEST_CATEGORY_META_DATA);
284         }
285         if (testCategory != null) {
286             return testCategory;
287         } else {
288             return context.getString(R.string.test_category_other);
289         }
290     }
291 
getTestParent(Bundle metaData)292     static String getTestParent(Bundle metaData) {
293         return metaData != null ? metaData.getString(TEST_PARENT_META_DATA) : null;
294     }
295 
getRequiredFeatures(Bundle metaData)296     static String[] getRequiredFeatures(Bundle metaData) {
297         if (metaData == null) {
298             return null;
299         } else {
300             String value = metaData.getString(TEST_REQUIRED_FEATURES_META_DATA);
301             if (value == null) {
302                 return null;
303             } else {
304                 return value.split(":");
305             }
306         }
307     }
308 
getRequiredActions(Bundle metaData)309     static String[] getRequiredActions(Bundle metaData) {
310         if (metaData == null) {
311             return null;
312         } else {
313             String value = metaData.getString(TEST_REQUIRED_ACTIONS_META_DATA);
314             if (value == null) {
315                 return null;
316             } else {
317                 return value.split(":");
318             }
319         }
320     }
321 
getRequiredConfigs(Bundle metaData)322     static String[] getRequiredConfigs(Bundle metaData) {
323         if (metaData == null) {
324             return null;
325         } else {
326             String value = metaData.getString(TEST_REQUIRED_CONFIG_META_DATA);
327             if (value == null) {
328                 return null;
329             } else {
330                 return value.split(":");
331             }
332         }
333     }
334 
getExcludedFeatures(Bundle metaData)335     static String[] getExcludedFeatures(Bundle metaData) {
336         if (metaData == null) {
337             return null;
338         } else {
339             String value = metaData.getString(TEST_EXCLUDED_FEATURES_META_DATA);
340             if (value == null) {
341                 return null;
342             } else {
343                 return value.split(":");
344             }
345         }
346     }
347 
getApplicableFeatures(Bundle metaData)348     static String[] getApplicableFeatures(Bundle metaData) {
349         if (metaData == null) {
350             return null;
351         } else {
352             String value = metaData.getString(TEST_APPLICABLE_FEATURES_META_DATA);
353             if (value == null) {
354                 return null;
355             } else {
356                 return value.split(":");
357             }
358         }
359     }
360 
361     /**
362      * Gets the configuration of the display mode per test. The default value is multi_display_mode.
363      *
364      * @param metaData Given metadata of the display mode.
365      * @return A string representing the display mode of the test.
366      */
getDisplayMode(Bundle metaData)367     static String getDisplayMode(Bundle metaData) {
368         if (metaData == null) {
369             return MULTIPLE_DISPLAY_MODE;
370         }
371         String displayMode = metaData.getString(TEST_DISPLAY_MODE_META_DATA);
372         return displayMode == null ? MULTIPLE_DISPLAY_MODE : displayMode;
373     }
374 
getTitle(Context context, ActivityInfo activityInfo)375     static String getTitle(Context context, ActivityInfo activityInfo) {
376         if (activityInfo.labelRes != 0) {
377             return context.getString(activityInfo.labelRes);
378         } else {
379             return activityInfo.name;
380         }
381     }
382 
getActivityIntent(ActivityInfo activityInfo)383     static Intent getActivityIntent(ActivityInfo activityInfo) {
384         Intent intent = new Intent();
385         intent.setClassName(activityInfo.packageName, activityInfo.name);
386         return intent;
387     }
388 
addTestToCategory(Map<String, List<TestListItem>> testsByCategory, String testCategory, TestListItem item)389     static void addTestToCategory(Map<String, List<TestListItem>> testsByCategory,
390             String testCategory, TestListItem item) {
391         List<TestListItem> tests;
392         if (testsByCategory.containsKey(testCategory)) {
393             tests = testsByCategory.get(testCategory);
394         } else {
395             tests = new ArrayList<TestListItem>();
396         }
397         testsByCategory.put(testCategory, tests);
398         tests.add(item);
399     }
400 
hasAnyFeature(String[] features)401     private boolean hasAnyFeature(String[] features) {
402         if (features != null) {
403             PackageManager packageManager = mContext.getPackageManager();
404             for (String feature : features) {
405                 if (packageManager.hasSystemFeature(feature)) {
406                     return true;
407                 }
408             }
409             Log.v(LOG_TAG, "Missing features " + Arrays.toString(features));
410         }
411         return false;
412     }
413 
hasAllFeatures(String[] features)414     private boolean hasAllFeatures(String[] features) {
415         if (features != null) {
416             PackageManager packageManager = mContext.getPackageManager();
417             for (String feature : features) {
418                 if (!packageManager.hasSystemFeature(feature)) {
419                     Log.v(LOG_TAG, "Missing feature " + feature);
420                     return false;
421                 }
422             }
423         }
424         return true;
425     }
426 
hasAllActions(String[] actions)427     private boolean hasAllActions(String[] actions) {
428         if (actions != null) {
429             PackageManager packageManager = mContext.getPackageManager();
430             for (String action : actions) {
431                 Intent intent = new Intent(action);
432                 if (packageManager.queryIntentActivities(intent, /* flags= */ 0).isEmpty()) {
433                     Log.v(LOG_TAG, "Missing action " + action);
434                     return false;
435                 }
436             }
437         }
438         return true;
439     }
440 
matchAllConfigs(String[] configs)441     private boolean matchAllConfigs(String[] configs) {
442         if (configs != null) {
443             for (String config : configs) {
444                 switch (config) {
445                     case CONFIG_NO_EMULATOR:
446                         try {
447                             Method getStringMethod = ClassLoader.getSystemClassLoader()
448                                 .loadClass("android.os.SystemProperties")
449                                 .getMethod("get", String.class);
450                             String emulatorKernel = (String) getStringMethod.invoke("0",
451                                     "ro.boot.qemu");
452                             if (emulatorKernel.equals("1")) {
453                                 return false;
454                             }
455                         } catch (Exception e) {
456                             Log.e(LOG_TAG, "Exception while checking for emulator support.", e);
457                         }
458                         break;
459                     case CONFIG_VOICE_CAPABLE:
460                         TelephonyManager telephonyManager = mContext.getSystemService(
461                                 TelephonyManager.class);
462                         if (!telephonyManager.isVoiceCapable()) {
463                             return false;
464                         }
465                         break;
466                     case CONFIG_HAS_RECENTS:
467                         if (!getSystemResourceFlag("config_hasRecents")) {
468                             return false;
469                         }
470                         break;
471                     case CONFIG_HDMI_SOURCE:
472                         final int DEVICE_TYPE_HDMI_SOURCE = 4;
473                         try {
474                             if (!getHdmiDeviceType().contains(DEVICE_TYPE_HDMI_SOURCE)) {
475                                 return false;
476                             }
477                         } catch (Exception exception) {
478                             Log.e(
479                                     LOG_TAG,
480                                     "Exception while looking up HDMI device type.",
481                                     exception);
482                         }
483                         break;
484                     case CONFIG_QUICK_SETTINGS_SUPPORTED:
485                         if (!getSystemResourceFlag("config_quickSettingsSupported")) {
486                             return false;
487                         }
488                         break;
489                     case CONFIG_HAS_MIC_TOGGLE:
490                         return isHardwareToggleSupported(SensorPrivacyManager.Sensors.MICROPHONE);
491                     case CONFIG_HAS_CAMERA_TOGGLE:
492                         return isHardwareToggleSupported(SensorPrivacyManager.Sensors.CAMERA);
493                     default:
494                         break;
495                 }
496             }
497         }
498         return true;
499     }
500 
501     /**
502      * Check if the test should be ran by the given display mode.
503      *
504      * @param mode Configs of the display mode.
505      * @param currentMode Given display mode.
506      * @return True if the given display mode matches the configs, otherwise, return false;
507      */
matchDisplayMode(String mode, String currentMode)508     private boolean matchDisplayMode(String mode, String currentMode) {
509         if (mode == null) {
510             return false;
511         }
512         switch (mode) {
513             case SINGLE_DISPLAY_MODE:
514                 return currentMode.equals(DisplayMode.UNFOLDED.toString());
515             case MULTIPLE_DISPLAY_MODE:
516                 return true;
517             default:
518                 return false;
519         }
520     }
521 
getSystemResourceFlag(String key)522     private boolean getSystemResourceFlag(String key) {
523         final Resources systemRes = mContext.getResources().getSystem();
524         final int id = systemRes.getIdentifier(key, "bool", "android");
525         if (id == Resources.ID_NULL) {
526             // The flag being queried should exist in
527             // frameworks/base/core/res/res/values/config.xml.
528             throw new RuntimeException("System resource flag " + key + " not found");
529         }
530         return systemRes.getBoolean(id);
531     }
532 
getHdmiDeviceType()533     private static List<Integer> getHdmiDeviceType()
534             throws InvocationTargetException, IllegalAccessException, ClassNotFoundException,
535                     NoSuchMethodException {
536         Method getStringMethod =
537                 ClassLoader.getSystemClassLoader()
538                         .loadClass("android.os.SystemProperties")
539                         .getMethod("get", String.class);
540         String deviceTypesStr = (String) getStringMethod.invoke(null, "ro.hdmi.device_type");
541         if (deviceTypesStr.equals("")) {
542             return new ArrayList<>();
543         }
544         return Arrays.stream(deviceTypesStr.split(","))
545                 .map(Integer::parseInt)
546                 .collect(Collectors.toList());
547     }
548 
filterTests(List<TestListItem> tests, String mode)549     List<TestListItem> filterTests(List<TestListItem> tests, String mode) {
550         List<TestListItem> filteredTests = new ArrayList<>();
551         for (TestListItem test : tests) {
552             if (!hasAnyFeature(test.excludedFeatures) && hasAllFeatures(test.requiredFeatures)
553                     && hasAllActions(test.requiredActions)
554                     && matchAllConfigs(test.requiredConfigs)
555                     && matchDisplayMode(test.displayMode, mode)) {
556                 if (test.applicableFeatures == null || hasAnyFeature(test.applicableFeatures)) {
557                     // Add suffix in test name if the test is in the folded mode.
558                     test.testName = setTestNameSuffix(mode, test.testName);
559                     filteredTests.add(test);
560                 } else {
561                     Log.d(LOG_TAG, "Skipping " + test.testName + " due to metadata filtering");
562                 }
563             } else {
564                 Log.d(LOG_TAG, "Skipping " + test.testName + " due to metadata filtering");
565             }
566         }
567         return filteredTests;
568     }
569 
570     @Override
getCount()571     public int getCount() {
572         if (!sInitialLaunch && mTestParent == null) {
573             return mDisplayModesTests.getOrDefault(sCurrentDisplayMode, new ArrayList<>()).size();
574         }
575         return super.getCount();
576     }
577 
578     @Override
getItem(int position)579     public TestListItem getItem(int position) {
580         if (mTestParent == null) {
581             return mDisplayModesTests.get(sCurrentDisplayMode).get(position);
582         }
583         return super.getItem(position);
584     }
585 
586     @Override
loadTestResults()587     public void loadTestResults() {
588         if (mTestParent == null) {
589             new RefreshTestResultsTask(true).execute();
590         } else {
591             super.loadTestResults();
592         }
593     }
594 
595     @SuppressLint("NewApi")
isHardwareToggleSupported(final int sensorType)596     private boolean isHardwareToggleSupported(final int sensorType) {
597         boolean isToggleSupported = false;
598         SensorPrivacyManager sensorPrivacyManager = mContext.getSystemService(
599                 SensorPrivacyManager.class);
600         if (sensorPrivacyManager != null) {
601             isToggleSupported = sensorPrivacyManager.supportsSensorToggle(
602                     SensorPrivacyManager.TOGGLE_TYPE_HARDWARE, sensorType);
603         }
604         return isToggleSupported;
605     }
606 }
607