1 /* 2 * Copyright (C) 2024 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.PassFailButtons.showInfoDialog; 20 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.database.DataSetObserver; 26 import android.os.Bundle; 27 import android.util.Log; 28 import android.view.View; 29 import android.widget.ListView; 30 31 import com.android.cts.verifier.TestListAdapter.TestListItem; 32 33 import org.json.JSONException; 34 import org.json.JSONObject; 35 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.HashSet; 39 import java.util.Iterator; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Set; 43 import java.util.TreeMap; 44 45 /** 46 * General activity to support host-side tests in CtsVerifier. 47 * 48 * <ul> 49 * <li>Show a list of tests. 50 * <li>Register a BroadcastReceiver to record results of tests executed on the host. 51 * <li>Parse and update the results of the tests. 52 * </ul> 53 */ 54 public class HostTestsActivity extends PassFailButtons.TestListActivity { 55 56 private static final String TAG = "HostTestsActivity"; 57 58 // Add module categories 59 private static final HostTestCategory[] HOST_TEST_CATEGORIES = { 60 new HostTestCategory("CompanionDeviceManager Tests") 61 .addTest( 62 "CtsCompanionDeviceManagerMultiDeviceTestCases", 63 "CtsCompanionDeviceManagerMultiDeviceTestCases"), 64 new HostTestCategory("NFC Tests") 65 .addTest("CtsNfcHceMultiDeviceTestCases", "CtsNfcHceMultiDeviceTestCases"), 66 new HostTestCategory("UWB Tests") 67 .addTest("CtsUwbMultiDeviceFiraRangingTests", "CtsUwbMultiDeviceFiraRangingTests") 68 .addTest("CtsMultiDeviceGenericRangingTests", "CtsMultiDeviceGenericRangingTests") 69 .addTest("CtsUwbMultiDeviceUwbManagerTests", "CtsUwbMultiDeviceUwbManagerTests"), 70 new HostTestCategory("Wi-Fi Tests") 71 .addTest("CtsWifiAwareTests", "CtsWifiAwareTests") 72 .addTest("CtsWifiSoftApTestCases", "CtsWifiSoftApTestCases") 73 }; 74 75 // The action to identify the broadcast Intent. 76 private static final String ACTION_HOST_TEST_RESULT = 77 "com.android.cts.verifier.ACTION_HOST_TEST_RESULT"; 78 // The key of the test results passed as the extra data of a broadcast Intent. 79 private static final String EXTRA_HOST_TEST_RESULT = 80 "com.android.cts.verifier.extra.HOST_TEST_RESULT"; 81 // The key for a test result in the JSON data. 82 private static final String TEST_RESULT_KEY = "result"; 83 // Represents a pass test result. 84 private static final String TEST_RESULT_PASS = "PASS"; 85 // Represents a fail test result. 86 private static final String TEST_RESULT_FAIL = "FAIL"; 87 // The key for a test details string in the JSON data. 88 private static final String TEST_DETAILS_KEY = "details"; 89 // The key for subtests in the JSON data. 90 private static final String TEST_SUBTESTS_KEY = "subtests"; 91 // Separator between module, class and testcase of host-side tests 92 static final String TEST_ID_SEPARATOR = "#"; 93 94 /** Represents a host-side test case in CtsVerifier. It's a test without an {@link Intent}. */ 95 public static final class HostTestListItem extends TestListItem { 96 97 /** 98 * Creates a test case shown in the UI with required test name and ID. 99 * 100 * @param testName name of the test shown in the UI 101 * @param testId ID of the test to record its result in test report 102 */ HostTestListItem(String testName, String testId)103 public HostTestListItem(String testName, String testId) { 104 super( 105 testName, 106 testId, 107 /* intent= */ null, 108 /* requiredFeatures= */ null, 109 /* excludedFeatures= */ null, 110 /* applicableFeatures= */ null); 111 } 112 113 @Override isTest()114 boolean isTest() { 115 return true; 116 } 117 } 118 119 /** Represents a category and its belonging tests. */ 120 public static final class HostTestCategory { 121 // The title of the category. 122 private final String mTitle; 123 // Test name -> test ID mappings of all tests of this category. 124 private final TreeMap<String, String> mTests; 125 // IDs of all tests of this category. 126 private final Set<String> mTestIds; 127 HostTestCategory(String title)128 public HostTestCategory(String title) { 129 mTitle = title; 130 mTests = new TreeMap<>(); 131 mTestIds = new HashSet<>(); 132 } 133 134 /** Adds a test that belongs to this category. */ addTest(String testName, String testId)135 public HostTestCategory addTest(String testName, String testId) { 136 mTests.put(testName, testId); 137 mTestIds.add(testId); 138 return this; 139 } 140 141 /** Generates a list of {@link TestListItem}s to render this test category in the UI. */ generateTestListItems()142 public List<TestListItem> generateTestListItems() { 143 List<TestListItem> testListItems = new ArrayList<>(); 144 testListItems.add(TestListItem.newCategory(mTitle)); 145 for (Map.Entry<String, String> entry : mTests.entrySet()) { 146 testListItems.add(new HostTestListItem(entry.getKey(), entry.getValue())); 147 } 148 return testListItems; 149 } 150 151 /** Gets the IDs of all tests that belong to this test category. */ getTestIds()152 public Set<String> getTestIds() { 153 return mTestIds; 154 } 155 } 156 157 /** 158 * The {@link BroadcastReceiver} to receive the broadcast {@link Intent} to update 159 * status/results of tests. 160 * 161 * <p>The result data is in JSON format by default, a sample result data is: 162 * 163 * <pre> 164 * { 165 * "test_id_1": { 166 * "result": "PASS" 167 * }, 168 * "test_id_2": { 169 * "result": "FAIL", 170 * "details": "expected 'true' but was 'false'" 171 * } 172 * } 173 * </pre> 174 */ 175 public final class ResultsReceiver extends BroadcastReceiver { 176 177 @Override onReceive(Context context, Intent intent)178 public void onReceive(Context context, Intent intent) { 179 if (ACTION_HOST_TEST_RESULT.equals(intent.getAction())) { 180 Log.i(TAG, "Parsing test results..."); 181 String testResults = intent.getStringExtra(EXTRA_HOST_TEST_RESULT); 182 if (testResults == null || testResults.isEmpty()) { 183 Log.i(TAG, EXTRA_HOST_TEST_RESULT + " is empty in the Intent."); 184 return; 185 } 186 JSONObject jsonResults; 187 try { 188 jsonResults = new JSONObject(testResults); 189 } catch (JSONException e) { 190 Log.e(TAG, "Error parsing json result string: " + testResults, e); 191 return; 192 } 193 Log.i(TAG, "Parsed test results: " + jsonResults); 194 handleJsonResults(jsonResults, /* prefix= */ ""); 195 } else { 196 Log.e(TAG, "Unknown Intent action " + intent.getAction()); 197 } 198 } 199 handleJsonResults(JSONObject jsonResults, String prefix)200 private void handleJsonResults(JSONObject jsonResults, String prefix) { 201 Iterator<String> testIds = jsonResults.keys(); 202 while (testIds.hasNext()) { 203 String testId = testIds.next(); 204 if (prefix.isEmpty() && !mAllTestIds.contains(testId)) { 205 Log.e( 206 TAG, 207 "Unknown test ID " + testId + " that doesn't belong to this activity."); 208 continue; 209 } 210 String fullTestId = prefix.isEmpty() ? testId : prefix + TEST_ID_SEPARATOR + testId; 211 JSONObject testObject; 212 String result; 213 String testDetails = null; 214 try { 215 testObject = jsonResults.getJSONObject(testId); 216 result = testObject.getString(TEST_RESULT_KEY); 217 if (testObject.has(TEST_DETAILS_KEY)) { 218 testDetails = testObject.getString(TEST_DETAILS_KEY); 219 } 220 } catch (JSONException e) { 221 Log.e(TAG, "Error getting result of test " + fullTestId, e); 222 continue; 223 } 224 225 if (TEST_RESULT_PASS.equals(result)) { 226 updateTestResult(TestResult.TEST_RESULT_PASSED, fullTestId, testDetails); 227 } else if (TEST_RESULT_FAIL.equals(result)) { 228 updateTestResult(TestResult.TEST_RESULT_FAILED, fullTestId, testDetails); 229 } else { 230 Log.w(TAG, "Unrecognized result " + result + " for test " + fullTestId); 231 } 232 233 if (testObject.has(TEST_SUBTESTS_KEY)) { 234 try { 235 handleJsonResults(testObject.getJSONObject(TEST_SUBTESTS_KEY), fullTestId); 236 } catch (JSONException e) { 237 Log.e(TAG, "Error getting subtest results of test " + fullTestId, e); 238 } 239 } 240 } 241 } 242 } 243 244 // The resource ID of the title of the dialog to show when entering the activity first time. 245 private final int mTitleId; 246 // The resource ID of the message of the dialog to show when entering the activity first time. 247 private final int mMessageId; 248 // All test categories to render in this activity. 249 private final List<HostTestCategory> mHostTestCategories; 250 // IDs of all tests belong to this activity. 251 private final Set<String> mAllTestIds; 252 // The receiver to update test results via broadcast. 253 private final ResultsReceiver mResultsReceiver = new ResultsReceiver(); 254 private boolean mReceiverRegistered = false; 255 256 // The adapter to render all tests in a list. 257 protected ArrayTestListAdapter mTestListAdapter; 258 HostTestsActivity()259 public HostTestsActivity() { 260 this( 261 R.string.host_tests_dialog_title, 262 R.string.host_tests_dialog_content, 263 HOST_TEST_CATEGORIES); 264 } 265 HostTestsActivity(int titleId, int messageId, HostTestCategory... hostTestCategories)266 private HostTestsActivity(int titleId, int messageId, HostTestCategory... hostTestCategories) { 267 mTitleId = titleId; 268 mMessageId = messageId; 269 mHostTestCategories = new ArrayList<>(Arrays.asList(hostTestCategories)); 270 mAllTestIds = new HashSet<>(); 271 for (HostTestCategory testCategory : hostTestCategories) { 272 mAllTestIds.addAll(testCategory.getTestIds()); 273 } 274 } 275 276 @Override handleItemClick(ListView l, View v, int position, long id)277 protected void handleItemClick(ListView l, View v, int position, long id) { 278 TestListAdapter.TestListItem item = mTestListAdapter.getItem(position); 279 if (mTestListAdapter.getTestResult(position) == TestResult.TEST_RESULT_NOT_EXECUTED) { 280 showInfoDialog( 281 this, 282 R.string.host_tests_dialog_title, 283 R.string.host_tests_dialog_content, 284 R.layout.host_tests_dialog); 285 return; 286 } 287 Intent intent = new Intent(this, HostTestListActivity.class); 288 intent.putExtra(HostTestListActivity.MODULE_TITLE, item.title); 289 intent.putExtra(HostTestListActivity.MODULE_NAME, item.testName); 290 Log.i(TAG, "Launching activity with " + IntentDrivenTestActivity.toString(this, intent)); 291 startActivity(intent); 292 } 293 294 @Override onCreate(Bundle savedInstanceState)295 protected void onCreate(Bundle savedInstanceState) { 296 super.onCreate(savedInstanceState); 297 298 setContentView(R.layout.pass_fail_list); 299 setInfoResources(mTitleId, mMessageId, R.layout.host_tests_dialog); 300 setPassFailButtonClickListeners(); 301 getPassButton().setEnabled(false); 302 303 mTestListAdapter = new ArrayTestListAdapter(this); 304 for (HostTestCategory testCategory : mHostTestCategories) { 305 mTestListAdapter.addAll(testCategory.generateTestListItems()); 306 } 307 mTestListAdapter.registerDataSetObserver( 308 new DataSetObserver() { 309 310 @Override 311 public void onChanged() { 312 updatePassButton(); 313 } 314 }); 315 setTestListAdapter(mTestListAdapter); 316 } 317 318 @Override onResume()319 protected void onResume() { 320 super.onResume(); 321 322 Log.i(TAG, "Registering broadcast receivers..."); 323 IntentFilter filter = new IntentFilter(ACTION_HOST_TEST_RESULT); 324 registerReceiver(mResultsReceiver, filter, Context.RECEIVER_EXPORTED); 325 mReceiverRegistered = true; 326 Log.i(TAG, "Registered broadcast receivers."); 327 } 328 329 @Override onDestroy()330 public void onDestroy() { 331 super.onDestroy(); 332 333 Log.i(TAG, "Unregistering broadcast receivers..."); 334 if (mReceiverRegistered) { 335 unregisterReceiver(mResultsReceiver); 336 mReceiverRegistered = false; 337 } 338 } 339 340 /** Updates a test with the given test result. */ updateTestResult(int testResult, String testId, String testDetails)341 private void updateTestResult(int testResult, String testId, String testDetails) { 342 Intent resultIntent = new Intent(); 343 TestResult.addResultData( 344 resultIntent, 345 testResult, 346 testId, 347 testDetails, 348 /* reportLog= */ null, 349 /* historyCollection= */ null); 350 handleLaunchTestResult(RESULT_OK, resultIntent); 351 } 352 } 353