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