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