blob: eaeb5f3f0867b8bebba7e4c5c81cc4c4f19e4390 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.cts.verifier;
import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode;
import static com.android.cts.verifier.TestListActivity.sInitialLaunch;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.os.Bundle;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.widget.ListView;
import com.android.cts.verifier.TestListActivity.DisplayMode;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* {@link TestListAdapter} that populates the {@link TestListActivity}'s {@link ListView} by
* reading data from the CTS Verifier's AndroidManifest.xml.
* <p>
* Making a new test activity to appear in the list requires the following steps:
*
* <ol>
* <li>REQUIRED: Add an activity to the AndroidManifest.xml with an intent filter with a
* main action and the MANUAL_TEST category.
* <pre>
* <intent-filter>
* <action android:name="android.intent.action.MAIN" />
* <category android:name="android.cts.intent.category.MANUAL_TEST" />
* </intent-filter>
* </pre>
* </li>
* <li>REQUIRED: Add a meta data attribute to indicate which display modes of tests the activity
* should belong to. "single_display_mode" indicates a test is only needed to run on the
* main display mode (i.e. unfolded), and "multi_display_mode" indicates a test is required
* to run under both modes (i.e. both folded and unfolded).If you don't add this attribute,
* your test will show up in both unfolded and folded modes.
* <pre>
* <meta-data android:name="display_mode" android:value="multi_display_mode" />
* </pre>
* </li>
* <li>OPTIONAL: Add a meta data attribute to indicate what category of tests the activity
* should belong to. If you don't add this attribute, your test will show up in the
* "Other" tests category.
* <pre>
* <meta-data android:name="test_category" android:value="@string/test_category_security" />
* </pre>
* </li>
* <li>OPTIONAL: Add a meta data attribute to indicate whether this test has a parent test.
* <pre>
* <meta-data android:name="test_parent" android:value="com.android.cts.verifier.bluetooth.BluetoothTestActivity" />
* </pre>
* </li>
* <li>OPTIONAL: Add a meta data attribute to indicate what features are required to run the
* test. If the device does not have all of the required features then it will not appear
* in the test list. Use a colon (:) to specify multiple required features.
* <pre>
* <meta-data android:name="test_required_features" android:value="android.hardware.sensor.accelerometer" />
* </pre>
* </li>
* <li>OPTIONAL: Add a meta data attribute to indicate features such that, if any present, the
* test gets excluded from being shown. If the device has any of the excluded features then
* the test will not appear in the test list. Use a colon (:) to specify multiple features
* to exclude for the test. Note that the colon means "or" in this case.
* <pre>
* <meta-data android:name="test_excluded_features" android:value="android.hardware.type.television" />
* </pre>
* </li>
* <li>OPTIONAL: Add a meta data attribute to indicate features such that, if any present,
* the test is applicable to run. If the device has any of the applicable features then
* the test will appear in the test list. Use a colon (:) to specify multiple features
* <pre>
* <meta-data android:name="test_applicable_features" android:value="android.hardware.sensor.compass" />
* </pre>
* </li>
* <li>OPTIONAL: Add a meta data attribute to indicate which intent actions are required to run
* the test. If the device does not have activities that handle all those actions, then it
* will not appear in the test list. Use a colon (:) to specify multiple required intent actions.
* <pre>
* <meta-data android:name="test_required_actions" android:value="android.app.action.ADD_DEVICE_ADMIN" />
* </pre>
* </li>
*
* </ol>
*/
public class ManifestTestListAdapter extends TestListAdapter {
private static final String LOG_TAG = "ManifestTestListAdapter";
private static final String TEST_CATEGORY_META_DATA = "test_category";
private static final String TEST_PARENT_META_DATA = "test_parent";
private static final String TEST_REQUIRED_FEATURES_META_DATA = "test_required_features";
private static final String TEST_EXCLUDED_FEATURES_META_DATA = "test_excluded_features";
private static final String TEST_APPLICABLE_FEATURES_META_DATA = "test_applicable_features";
private static final String TEST_REQUIRED_CONFIG_META_DATA = "test_required_configs";
private static final String TEST_REQUIRED_ACTIONS_META_DATA = "test_required_actions";
private static final String TEST_DISPLAY_MODE_META_DATA = "display_mode";
private static final String CONFIG_NO_EMULATOR = "config_no_emulator";
private static final String CONFIG_VOICE_CAPABLE = "config_voice_capable";
private static final String CONFIG_HAS_RECENTS = "config_has_recents";
private static final String CONFIG_HDMI_SOURCE = "config_hdmi_source";
private static final String CONFIG_QUICK_SETTINGS_SUPPORTED = "config_quick_settings_supported";
/** The config to represent that a test is only needed to run in the main display mode
* (i.e. unfolded) */
private static final String SINGLE_DISPLAY_MODE = "single_display_mode";
/** The config to represent that a test is needed to run in the multiple display modes
* (i.e. both unfolded and folded) */
private static final String MULTIPLE_DISPLAY_MODE = "multi_display_mode";
private final HashSet<String> mDisabledTests;
private Context mContext;
private String mTestParent;
public ManifestTestListAdapter(Context context, String testParent, String[] disabledTestArray) {
super(context);
mContext = context;
mTestParent = testParent;
mDisabledTests = new HashSet<>(disabledTestArray.length);
for (int i = 0; i < disabledTestArray.length; i++) {
mDisabledTests.add(disabledTestArray[i]);
}
// Configs to distinct that the adapter is for top-level tests or subtests.
if (testParent == null) {
// For top-level tests.
hasTestParentInManifestAdapter = false;
} else {
hasTestParentInManifestAdapter = true;
}
adapterFromManifest = true;
}
public ManifestTestListAdapter(Context context, String testParent) {
this(context, testParent, context.getResources().getStringArray(R.array.disabled_tests));
}
@Override
protected List<TestListItem> getRows() {
// When launching at the first time or after killing the process, needs to fetch the
// test items of all display modes as the bases for switching.
if (!sInitialLaunch) {
return getRowsWithDisplayMode(sCurrentDisplayMode);
}
List<TestListItem> allRows = new ArrayList<TestListItem>();
for (DisplayMode mode: DisplayMode.values()) {
allRows = getRowsWithDisplayMode(mode.toString());
mDisplayModesTests.put(mode.toString(), allRows);
}
return allRows;
}
/**
* Gets all rows based on the specific display mode.
*
* @param mode Given display mode.
* @return A list containing all test itmes in the given display mode.
*/
private List<TestListItem> getRowsWithDisplayMode (String mode) {
/*
* 1. Get all the tests belonging to the test parent.
* 2. Get all the tests keyed by their category.
* 3. Flatten the tests and categories into one giant list for the list view.
*/
List<TestListItem> allRows = new ArrayList<TestListItem>();
List<ResolveInfo> infos = getResolveInfosForParent();
Map<String, List<TestListItem>> testsByCategory = getTestsByCategory(infos);
List<String> testCategories = new ArrayList<String>(testsByCategory.keySet());
Collections.sort(testCategories);
for (String testCategory : testCategories) {
List<TestListItem> tests = filterTests(testsByCategory.get(testCategory), mode);
if (!tests.isEmpty()) {
allRows.add(TestListItem.newCategory(testCategory));
Collections.sort(tests, Comparator.comparing(item -> item.title));
allRows.addAll(tests);
}
}
return allRows;
}
List<ResolveInfo> getResolveInfosForParent() {
Intent mainIntent = new Intent(Intent.ACTION_MAIN);
mainIntent.addCategory(CATEGORY_MANUAL_TEST);
mainIntent.setPackage(mContext.getPackageName());
PackageManager packageManager = mContext.getPackageManager();
List<ResolveInfo> list = packageManager.queryIntentActivities(mainIntent,
PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
int size = list.size();
List<ResolveInfo> matchingList = new ArrayList<>();
for (int i = 0; i < size; i++) {
ResolveInfo info = list.get(i);
String parent = getTestParent(info.activityInfo.metaData);
if ((mTestParent == null && parent == null)
|| (mTestParent != null && mTestParent.equals(parent))) {
matchingList.add(info);
}
}
return matchingList;
}
Map<String, List<TestListItem>> getTestsByCategory(List<ResolveInfo> list) {
Map<String, List<TestListItem>> testsByCategory = new HashMap<>();
int size = list.size();
for (int i = 0; i < size; i++) {
ResolveInfo info = list.get(i);
if (info.activityInfo == null || mDisabledTests.contains(info.activityInfo.name)) {
Log.w(LOG_TAG, "ignoring disabled test: " + info.activityInfo.name);
continue;
}
String title = getTitle(mContext, info.activityInfo);
String testName = info.activityInfo.name;
Intent intent = getActivityIntent(info.activityInfo);
String[] requiredFeatures = getRequiredFeatures(info.activityInfo.metaData);
String[] requiredConfigs = getRequiredConfigs(info.activityInfo.metaData);
String[] requiredActions = getRequiredActions(info.activityInfo.metaData);
String[] excludedFeatures = getExcludedFeatures(info.activityInfo.metaData);
String[] applicableFeatures = getApplicableFeatures(info.activityInfo.metaData);
String displayMode = getDisplayMode(info.activityInfo.metaData);
TestListItem item = TestListItem.newTest(title, testName, intent, requiredFeatures,
requiredConfigs, requiredActions, excludedFeatures, applicableFeatures,
displayMode);
String testCategory = getTestCategory(mContext, info.activityInfo.metaData);
addTestToCategory(testsByCategory, testCategory, item);
}
return testsByCategory;
}
static String getTestCategory(Context context, Bundle metaData) {
String testCategory = null;
if (metaData != null) {
testCategory = metaData.getString(TEST_CATEGORY_META_DATA);
}
if (testCategory != null) {
return testCategory;
} else {
return context.getString(R.string.test_category_other);
}
}
static String getTestParent(Bundle metaData) {
return metaData != null ? metaData.getString(TEST_PARENT_META_DATA) : null;
}
static String[] getRequiredFeatures(Bundle metaData) {
if (metaData == null) {
return null;
} else {
String value = metaData.getString(TEST_REQUIRED_FEATURES_META_DATA);
if (value == null) {
return null;
} else {
return value.split(":");
}
}
}
static String[] getRequiredActions(Bundle metaData) {
if (metaData == null) {
return null;
} else {
String value = metaData.getString(TEST_REQUIRED_ACTIONS_META_DATA);
if (value == null) {
return null;
} else {
return value.split(":");
}
}
}
static String[] getRequiredConfigs(Bundle metaData) {
if (metaData == null) {
return null;
} else {
String value = metaData.getString(TEST_REQUIRED_CONFIG_META_DATA);
if (value == null) {
return null;
} else {
return value.split(":");
}
}
}
static String[] getExcludedFeatures(Bundle metaData) {
if (metaData == null) {
return null;
} else {
String value = metaData.getString(TEST_EXCLUDED_FEATURES_META_DATA);
if (value == null) {
return null;
} else {
return value.split(":");
}
}
}
static String[] getApplicableFeatures(Bundle metaData) {
if (metaData == null) {
return null;
} else {
String value = metaData.getString(TEST_APPLICABLE_FEATURES_META_DATA);
if (value == null) {
return null;
} else {
return value.split(":");
}
}
}
/**
* Gets the configuration of the display mode per test. The default value is multi_display_mode.
*
* @param metaData Given metadata of the display mode.
* @return A string representing the display mode of the test.
*/
static String getDisplayMode(Bundle metaData) {
if (metaData == null) {
return MULTIPLE_DISPLAY_MODE;
}
String displayMode = metaData.getString(TEST_DISPLAY_MODE_META_DATA);
return displayMode == null ? MULTIPLE_DISPLAY_MODE : displayMode;
}
static String getTitle(Context context, ActivityInfo activityInfo) {
if (activityInfo.labelRes != 0) {
return context.getString(activityInfo.labelRes);
} else {
return activityInfo.name;
}
}
static Intent getActivityIntent(ActivityInfo activityInfo) {
Intent intent = new Intent();
intent.setClassName(activityInfo.packageName, activityInfo.name);
return intent;
}
static void addTestToCategory(Map<String, List<TestListItem>> testsByCategory,
String testCategory, TestListItem item) {
List<TestListItem> tests;
if (testsByCategory.containsKey(testCategory)) {
tests = testsByCategory.get(testCategory);
} else {
tests = new ArrayList<TestListItem>();
}
testsByCategory.put(testCategory, tests);
tests.add(item);
}
private boolean hasAnyFeature(String[] features) {
if (features != null) {
PackageManager packageManager = mContext.getPackageManager();
for (String feature : features) {
if (packageManager.hasSystemFeature(feature)) {
return true;
}
}
Log.v(LOG_TAG, "Missing features " + Arrays.toString(features));
}
return false;
}
private boolean hasAllFeatures(String[] features) {
if (features != null) {
PackageManager packageManager = mContext.getPackageManager();
for (String feature : features) {
if (!packageManager.hasSystemFeature(feature)) {
Log.v(LOG_TAG, "Missing feature " + feature);
return false;
}
}
}
return true;
}
private boolean hasAllActions(String[] actions) {
if (actions != null) {
PackageManager packageManager = mContext.getPackageManager();
for (String action : actions) {
Intent intent = new Intent(action);
if (packageManager.queryIntentActivities(intent, /* flags= */ 0).isEmpty()) {
Log.v(LOG_TAG, "Missing action " + action);
return false;
}
}
}
return true;
}
private boolean matchAllConfigs(String[] configs) {
if (configs != null) {
for (String config : configs) {
switch (config) {
case CONFIG_NO_EMULATOR:
try {
Method getStringMethod = ClassLoader.getSystemClassLoader()
.loadClass("android.os.SystemProperties")
.getMethod("get", String.class);
String emulatorKernel = (String) getStringMethod.invoke("0",
"ro.boot.qemu");
if (emulatorKernel.equals("1")) {
return false;
}
} catch (Exception e) {
Log.e(LOG_TAG, "Exception while checking for emulator support.", e);
}
break;
case CONFIG_VOICE_CAPABLE:
TelephonyManager telephonyManager = mContext.getSystemService(
TelephonyManager.class);
if (!telephonyManager.isVoiceCapable()) {
return false;
}
break;
case CONFIG_HAS_RECENTS:
if (!getSystemResourceFlag("config_hasRecents")) {
return false;
}
break;
case CONFIG_HDMI_SOURCE:
final int DEVICE_TYPE_HDMI_SOURCE = 4;
try {
if (!getHdmiDeviceType().contains(DEVICE_TYPE_HDMI_SOURCE)) {
return false;
}
} catch (Exception exception) {
Log.e(
LOG_TAG,
"Exception while looking up HDMI device type.",
exception);
}
break;
case CONFIG_QUICK_SETTINGS_SUPPORTED:
if (!getSystemResourceFlag("config_quickSettingsSupported")) {
return false;
}
break;
default:
break;
}
}
}
return true;
}
/**
* Check if the test should be ran by the given display mode.
*
* @param mode Configs of the display mode.
* @param currentMode Given display mode.
* @return True if the given display mode matches the configs, otherwise, return false;
*/
private boolean matchDisplayMode(String mode, String currentMode) {
if (mode == null) {
return false;
}
switch (mode) {
case SINGLE_DISPLAY_MODE:
return currentMode.equals(DisplayMode.UNFOLDED.toString());
case MULTIPLE_DISPLAY_MODE:
return true;
default:
return false;
}
}
private boolean getSystemResourceFlag(String key) {
final Resources systemRes = mContext.getResources().getSystem();
final int id = systemRes.getIdentifier(key, "bool", "android");
if (id == Resources.ID_NULL) {
// The flag being queried should exist in
// frameworks/base/core/res/res/values/config.xml.
throw new RuntimeException("System resource flag " + key + " not found");
}
return systemRes.getBoolean(id);
}
private static List<Integer> getHdmiDeviceType()
throws InvocationTargetException, IllegalAccessException, ClassNotFoundException,
NoSuchMethodException {
Method getStringMethod =
ClassLoader.getSystemClassLoader()
.loadClass("android.os.SystemProperties")
.getMethod("get", String.class);
String deviceTypesStr = (String) getStringMethod.invoke(null, "ro.hdmi.device_type");
if (deviceTypesStr.equals("")) {
return new ArrayList<>();
}
return Arrays.stream(deviceTypesStr.split(","))
.map(Integer::parseInt)
.collect(Collectors.toList());
}
List<TestListItem> filterTests(List<TestListItem> tests, String mode) {
List<TestListItem> filteredTests = new ArrayList<>();
for (TestListItem test : tests) {
if (!hasAnyFeature(test.excludedFeatures) && hasAllFeatures(test.requiredFeatures)
&& hasAllActions(test.requiredActions)
&& matchAllConfigs(test.requiredConfigs)
&& matchDisplayMode(test.displayMode, mode)) {
if (test.applicableFeatures == null || hasAnyFeature(test.applicableFeatures)) {
// Add suffix in test name if the test is in the folded mode.
test.testName = setTestNameSuffix(mode, test.testName);
filteredTests.add(test);
} else {
Log.d(LOG_TAG, "Skipping " + test.testName + " due to metadata filtering");
}
} else {
Log.d(LOG_TAG, "Skipping " + test.testName + " due to metadata filtering");
}
}
return filteredTests;
}
}