• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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