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 android.content.Context;
20 import android.content.Intent;
21 import android.content.pm.ActivityInfo;
22 import android.content.pm.PackageManager;
23 import android.content.pm.ResolveInfo;
24 import android.content.res.Resources;
25 import android.os.Bundle;
26 import android.telephony.TelephonyManager;
27 import android.util.Log;
28 import android.widget.ListView;
29 
30 import java.lang.reflect.InvocationTargetException;
31 import java.lang.reflect.Method;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Collections;
35 import java.util.Comparator;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.stream.Collectors;
41 
42 /**
43  * {@link TestListAdapter} that populates the {@link TestListActivity}'s {@link ListView} by
44  * reading data from the CTS Verifier's AndroidManifest.xml.
45  * <p>
46  * Making a new test activity to appear in the list requires the following steps:
47  *
48  * <ol>
49  *     <li>REQUIRED: Add an activity to the AndroidManifest.xml with an intent filter with a
50  *         main action and the MANUAL_TEST category.
51  *         <pre>
52  *             <intent-filter>
53  *                <action android:name="android.intent.action.MAIN" />
54  *                <category android:name="android.cts.intent.category.MANUAL_TEST" />
55  *             </intent-filter>
56  *         </pre>
57  *     </li>
58  *     <li>OPTIONAL: Add a meta data attribute to indicate what category of tests the activity
59  *         should belong to. If you don't add this attribute, your test will show up in the
60  *         "Other" tests category.
61  *         <pre>
62  *             <meta-data android:name="test_category" android:value="@string/test_category_security" />
63  *         </pre>
64  *     </li>
65  *     <li>OPTIONAL: Add a meta data attribute to indicate whether this test has a parent test.
66  *         <pre>
67  *             <meta-data android:name="test_parent" android:value="com.android.cts.verifier.bluetooth.BluetoothTestActivity" />
68  *         </pre>
69  *     </li>
70  *     <li>OPTIONAL: Add a meta data attribute to indicate what features are required to run the
71  *         test. If the device does not have all of the required features then it will not appear
72  *         in the test list. Use a colon (:) to specify multiple required features.
73  *         <pre>
74  *             <meta-data android:name="test_required_features" android:value="android.hardware.sensor.accelerometer" />
75  *         </pre>
76  *     </li>
77  *     <li>OPTIONAL: Add a meta data attribute to indicate features such that, if any present, the
78  *         test gets excluded from being shown. If the device has any of the excluded features then
79  *         the test will not appear in the test list. Use a colon (:) to specify multiple features
80  *         to exclude for the test. Note that the colon means "or" in this case.
81  *         <pre>
82  *             <meta-data android:name="test_excluded_features" android:value="android.hardware.type.television" />
83  *         </pre>
84  *     </li>
85  *     <li>OPTIONAL: Add a meta data attribute to indicate features such that, if any present,
86  *         the test is applicable to run. If the device has any of the applicable features then
87  *         the test will appear in the test list. Use a colon (:) to specify multiple features
88  *         <pre>
89  *             <meta-data android:name="test_applicable_features" android:value="android.hardware.sensor.compass" />
90  *         </pre>
91  *     </li>
92  *
93  * </ol>
94  */
95 public class ManifestTestListAdapter extends TestListAdapter {
96     private static final String LOG_TAG = "ManifestTestListAdapter";
97 
98     private static final String TEST_CATEGORY_META_DATA = "test_category";
99 
100     private static final String TEST_PARENT_META_DATA = "test_parent";
101 
102     private static final String TEST_REQUIRED_FEATURES_META_DATA = "test_required_features";
103 
104     private static final String TEST_EXCLUDED_FEATURES_META_DATA = "test_excluded_features";
105 
106     private static final String TEST_APPLICABLE_FEATURES_META_DATA = "test_applicable_features";
107 
108     private static final String TEST_REQUIRED_CONFIG_META_DATA = "test_required_configs";
109 
110     private static final String CONFIG_VOICE_CAPABLE = "config_voice_capable";
111 
112     private static final String CONFIG_HAS_RECENTS = "config_has_recents";
113 
114     private static final String CONFIG_HDMI_SOURCE = "config_hdmi_source";
115 
116     private final HashSet<String> mDisabledTests;
117 
118     private Context mContext;
119 
120     private String mTestParent;
121 
ManifestTestListAdapter(Context context, String testParent, String[] disabledTestArray)122     public ManifestTestListAdapter(Context context, String testParent, String[] disabledTestArray) {
123         super(context);
124         mContext = context;
125         mTestParent = testParent;
126         mDisabledTests = new HashSet<>(disabledTestArray.length);
127         for (int i = 0; i < disabledTestArray.length; i++) {
128             mDisabledTests.add(disabledTestArray[i]);
129         }
130     }
131 
ManifestTestListAdapter(Context context, String testParent)132     public ManifestTestListAdapter(Context context, String testParent) {
133         this(context, testParent, context.getResources().getStringArray(R.array.disabled_tests));
134     }
135 
136     @Override
getRows()137     protected List<TestListItem> getRows() {
138 
139         /*
140          * 1. Get all the tests belonging to the test parent.
141          * 2. Get all the tests keyed by their category.
142          * 3. Flatten the tests and categories into one giant list for the list view.
143          */
144 
145         List<ResolveInfo> infos = getResolveInfosForParent();
146         Map<String, List<TestListItem>> testsByCategory = getTestsByCategory(infos);
147 
148         List<String> testCategories = new ArrayList<String>(testsByCategory.keySet());
149         Collections.sort(testCategories);
150 
151         List<TestListItem> allRows = new ArrayList<TestListItem>();
152         for (String testCategory : testCategories) {
153             List<TestListItem> tests = filterTests(testsByCategory.get(testCategory));
154             if (!tests.isEmpty()) {
155                 allRows.add(TestListItem.newCategory(testCategory));
156                 Collections.sort(tests, Comparator.comparing(item -> item.title));
157                 allRows.addAll(tests);
158             }
159         }
160         return allRows;
161     }
162 
getResolveInfosForParent()163     List<ResolveInfo> getResolveInfosForParent() {
164         Intent mainIntent = new Intent(Intent.ACTION_MAIN);
165         mainIntent.addCategory(CATEGORY_MANUAL_TEST);
166         mainIntent.setPackage(mContext.getPackageName());
167 
168         PackageManager packageManager = mContext.getPackageManager();
169         List<ResolveInfo> list = packageManager.queryIntentActivities(mainIntent,
170                 PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
171         int size = list.size();
172 
173         List<ResolveInfo> matchingList = new ArrayList<>();
174         for (int i = 0; i < size; i++) {
175             ResolveInfo info = list.get(i);
176             String parent = getTestParent(info.activityInfo.metaData);
177             if ((mTestParent == null && parent == null)
178                     || (mTestParent != null && mTestParent.equals(parent))) {
179                 matchingList.add(info);
180             }
181         }
182         return matchingList;
183     }
184 
getTestsByCategory(List<ResolveInfo> list)185     Map<String, List<TestListItem>> getTestsByCategory(List<ResolveInfo> list) {
186         Map<String, List<TestListItem>> testsByCategory = new HashMap<>();
187 
188         int size = list.size();
189         for (int i = 0; i < size; i++) {
190             ResolveInfo info = list.get(i);
191             if (info.activityInfo == null || mDisabledTests.contains(info.activityInfo.name)) {
192                 Log.w(LOG_TAG, "ignoring disabled test: " + info.activityInfo.name);
193                 continue;
194             }
195             String title = getTitle(mContext, info.activityInfo);
196             String testName = info.activityInfo.name;
197             Intent intent = getActivityIntent(info.activityInfo);
198             String[] requiredFeatures = getRequiredFeatures(info.activityInfo.metaData);
199             String[] requiredConfigs = getRequiredConfigs(info.activityInfo.metaData);
200             String[] excludedFeatures = getExcludedFeatures(info.activityInfo.metaData);
201             String[] applicableFeatures = getApplicableFeatures(info.activityInfo.metaData);
202             TestListItem item = TestListItem.newTest(title, testName, intent, requiredFeatures,
203                      requiredConfigs, excludedFeatures, applicableFeatures);
204 
205             String testCategory = getTestCategory(mContext, info.activityInfo.metaData);
206             addTestToCategory(testsByCategory, testCategory, item);
207         }
208 
209         return testsByCategory;
210     }
211 
getTestCategory(Context context, Bundle metaData)212     static String getTestCategory(Context context, Bundle metaData) {
213         String testCategory = null;
214         if (metaData != null) {
215             testCategory = metaData.getString(TEST_CATEGORY_META_DATA);
216         }
217         if (testCategory != null) {
218             return testCategory;
219         } else {
220             return context.getString(R.string.test_category_other);
221         }
222     }
223 
getTestParent(Bundle metaData)224     static String getTestParent(Bundle metaData) {
225         return metaData != null ? metaData.getString(TEST_PARENT_META_DATA) : null;
226     }
227 
getRequiredFeatures(Bundle metaData)228     static String[] getRequiredFeatures(Bundle metaData) {
229         if (metaData == null) {
230             return null;
231         } else {
232             String value = metaData.getString(TEST_REQUIRED_FEATURES_META_DATA);
233             if (value == null) {
234                 return null;
235             } else {
236                 return value.split(":");
237             }
238         }
239     }
240 
getRequiredConfigs(Bundle metaData)241     static String[] getRequiredConfigs(Bundle metaData) {
242         if (metaData == null) {
243             return null;
244         } else {
245             String value = metaData.getString(TEST_REQUIRED_CONFIG_META_DATA);
246             if (value == null) {
247                 return null;
248             } else {
249                 return value.split(":");
250             }
251         }
252     }
253 
getExcludedFeatures(Bundle metaData)254     static String[] getExcludedFeatures(Bundle metaData) {
255         if (metaData == null) {
256             return null;
257         } else {
258             String value = metaData.getString(TEST_EXCLUDED_FEATURES_META_DATA);
259             if (value == null) {
260                 return null;
261             } else {
262                 return value.split(":");
263             }
264         }
265     }
266 
getApplicableFeatures(Bundle metaData)267     static String[] getApplicableFeatures(Bundle metaData) {
268         if (metaData == null) {
269             return null;
270         } else {
271             String value = metaData.getString(TEST_APPLICABLE_FEATURES_META_DATA);
272             if (value == null) {
273                 return null;
274             } else {
275                 return value.split(":");
276             }
277         }
278     }
279 
getTitle(Context context, ActivityInfo activityInfo)280     static String getTitle(Context context, ActivityInfo activityInfo) {
281         if (activityInfo.labelRes != 0) {
282             return context.getString(activityInfo.labelRes);
283         } else {
284             return activityInfo.name;
285         }
286     }
287 
getActivityIntent(ActivityInfo activityInfo)288     static Intent getActivityIntent(ActivityInfo activityInfo) {
289         Intent intent = new Intent();
290         intent.setClassName(activityInfo.packageName, activityInfo.name);
291         return intent;
292     }
293 
addTestToCategory(Map<String, List<TestListItem>> testsByCategory, String testCategory, TestListItem item)294     static void addTestToCategory(Map<String, List<TestListItem>> testsByCategory,
295             String testCategory, TestListItem item) {
296         List<TestListItem> tests;
297         if (testsByCategory.containsKey(testCategory)) {
298             tests = testsByCategory.get(testCategory);
299         } else {
300             tests = new ArrayList<TestListItem>();
301         }
302         testsByCategory.put(testCategory, tests);
303         tests.add(item);
304     }
305 
hasAnyFeature(String[] features)306     private boolean hasAnyFeature(String[] features) {
307         if (features != null) {
308             PackageManager packageManager = mContext.getPackageManager();
309             for (String feature : features) {
310                 if (packageManager.hasSystemFeature(feature)) {
311                     return true;
312                 }
313             }
314         }
315         return false;
316     }
317 
hasAllFeatures(String[] features)318     private boolean hasAllFeatures(String[] features) {
319         if (features != null) {
320             PackageManager packageManager = mContext.getPackageManager();
321             for (String feature : features) {
322                 if (!packageManager.hasSystemFeature(feature)) {
323                     return false;
324                 }
325             }
326         }
327         return true;
328     }
329 
matchAllConfigs(String[] configs)330     private boolean matchAllConfigs(String[] configs) {
331         if (configs != null) {
332             for (String config : configs) {
333                 switch (config) {
334                     case CONFIG_VOICE_CAPABLE:
335                         TelephonyManager telephonyManager = mContext.getSystemService(
336                                 TelephonyManager.class);
337                         if (!telephonyManager.isVoiceCapable()) {
338                             return false;
339                         }
340                         break;
341                     case CONFIG_HAS_RECENTS:
342                         final Resources systemRes = mContext.getResources().getSystem();
343                         final int id = systemRes.getIdentifier("config_hasRecents", "bool",
344                                 "android");
345                         if (id == Resources.ID_NULL || !systemRes.getBoolean(id)) {
346                             return false;
347                         }
348                         break;
349                     case CONFIG_HDMI_SOURCE:
350                         final int DEVICE_TYPE_HDMI_SOURCE = 4;
351                         try {
352                             if (!getHdmiDeviceType().contains(DEVICE_TYPE_HDMI_SOURCE)) {
353                                 return false;
354                             }
355                         } catch (Exception exception) {
356                             Log.e(
357                                     LOG_TAG,
358                                     "Exception while looking up HDMI device type.",
359                                     exception);
360                         }
361                         break;
362                     default:
363                         break;
364                 }
365             }
366         }
367         return true;
368     }
369 
getHdmiDeviceType()370     private static List<Integer> getHdmiDeviceType()
371             throws InvocationTargetException, IllegalAccessException, ClassNotFoundException,
372                     NoSuchMethodException {
373         Method getStringMethod =
374                 ClassLoader.getSystemClassLoader()
375                         .loadClass("android.os.SystemProperties")
376                         .getMethod("get", String.class);
377         String deviceTypesStr = (String) getStringMethod.invoke(null, "ro.hdmi.device_type");
378         if (deviceTypesStr.equals("")) {
379             return new ArrayList<>();
380         }
381         return Arrays.stream(deviceTypesStr.split(","))
382                 .map(Integer::parseInt)
383                 .collect(Collectors.toList());
384     }
385 
filterTests(List<TestListItem> tests)386     List<TestListItem> filterTests(List<TestListItem> tests) {
387         List<TestListItem> filteredTests = new ArrayList<>();
388         for (TestListItem test : tests) {
389             if (!hasAnyFeature(test.excludedFeatures) && hasAllFeatures(test.requiredFeatures)
390                     && matchAllConfigs(test.requiredConfigs)) {
391                 if (test.applicableFeatures == null || hasAnyFeature(test.applicableFeatures)) {
392                     filteredTests.add(test);
393                 }
394             }
395         }
396         return filteredTests;
397     }
398 }
399