1 /*
2  * Copyright (C) 2010 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.ReportExporter.LOGS_DIRECTORY;
20 import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode;
21 
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.database.ContentObserver;
26 import android.database.Cursor;
27 import android.hardware.devicestate.DeviceStateManager;
28 import android.os.AsyncTask;
29 import android.os.Environment;
30 import android.os.Handler;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.widget.BaseAdapter;
35 import android.widget.ListView;
36 import android.widget.TextView;
37 
38 import com.android.compatibility.common.util.ReportLog;
39 import com.android.compatibility.common.util.TestScreenshotsMetadata;
40 import com.android.cts.verifier.TestListActivity.DisplayMode;
41 
42 import java.io.ByteArrayInputStream;
43 import java.io.File;
44 import java.io.IOException;
45 import java.io.ObjectInputStream;
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.HashMap;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Set;
52 import java.util.concurrent.atomic.AtomicBoolean;
53 import java.util.stream.Collectors;
54 
55 /**
56  * {@link BaseAdapter} that handles loading, refreshing, and setting test results. What tests are
57  * shown can be customized by overriding {@link #getRows()}. See {@link ArrayTestListAdapter} and
58  * {@link ManifestTestListAdapter} for examples.
59  */
60 public abstract class TestListAdapter extends BaseAdapter {
61 
62     /** Activities implementing {@link Intent#ACTION_MAIN} and this will appear in the list. */
63     public static final String CATEGORY_MANUAL_TEST = "android.cts.intent.category.MANUAL_TEST";
64 
65     /** View type for a category of tests like "Sensors" or "Features" */
66     private static final int CATEGORY_HEADER_VIEW_TYPE = 0;
67 
68     /** View type for an actual test like the Accelerometer test. */
69     private static final int TEST_VIEW_TYPE = 1;
70 
71     /** Padding around the text views and icons. */
72     private static final int PADDING = 10;
73 
74     private final Context mContext;
75 
76     /** Immutable data of tests like the test's title and launch intent. */
77     private final List<TestListItem> mRows = new ArrayList<TestListItem>();
78 
79     /** Mutable test results that will change as each test activity finishes. */
80     private final Map<String, Integer> mTestResults = new HashMap<String, Integer>();
81 
82     /** Map from test name to test details. */
83     private final Map<String, String> mTestDetails = new HashMap<String, String>();
84 
85     /** Map from test name to {@link ReportLog}. */
86     private final Map<String, ReportLog> mReportLogs = new HashMap<String, ReportLog>();
87 
88     /** Map from test name to {@link TestResultHistoryCollection}. */
89     private final Map<String, TestResultHistoryCollection> mHistories = new HashMap<>();
90 
91     /** Map from test name to {@link TestScreenshotsMetadata}. */
92     private final Map<String, TestScreenshotsMetadata> mScreenshotsMetadata = new HashMap<>();
93 
94     /** Flag to identify whether the mHistories has been loaded. */
95     private final AtomicBoolean mHasLoadedResultHistory = new AtomicBoolean(false);
96 
97     private final LayoutInflater mLayoutInflater;
98 
99     /**
100      * Map from display mode to the list of {@link TestListItem}. Records the TestListItem from main
101      * view only, including unfolded mode and folded mode respectively.
102      */
103     protected Map<String, List<TestListItem>> mDisplayModesTests = new HashMap<>();
104 
105     /** A keyword to help filter out test cases by the test name. */
106     protected String mTestFilter;
107 
108     /** {@link ListView} row that is either a test category header or a test. */
109     public static class TestListItem {
110 
111         /** Title shown in the {@link ListView}. */
112         public final String title;
113 
114         /** Test name with class and test ID to uniquely identify the test. Null for categories. */
115         public String testName;
116 
117         /** Intent used to launch the activity from the list. Null for categories. */
118         public final Intent intent;
119 
120         /** Features necessary to run this test. */
121         public final String[] requiredFeatures;
122 
123         /** Configs necessary to run this test. */
124         public final String[] requiredConfigs;
125 
126         /** Intent actions necessary to run this test. */
127         public final String[] requiredActions;
128 
129         /** Features such that, if any present, the test gets excluded from being shown. */
130         public final String[] excludedFeatures;
131 
132         /** User "types" that, if any present, the test gets excluded from being shown. */
133         public final String[] excludedUserTypes;
134 
135         /** If any of of the features are present the test is meaningful to run. */
136         public final String[] applicableFeatures;
137 
138         /** Configs display mode to run this test. */
139         public final String displayMode;
140 
141         /** Configs test pass mode to record the test result. */
142         public final boolean passInEitherMode;
143 
144         // TODO: refactor to use a Builder approach instead
145 
146         /**
147          * Creates a new test item with given required, excluded and applicable features, the
148          * context and the resource ID of the title.
149          */
newTest( Context context, int titleResId, String testName, Intent intent, String[] requiredFeatures, String[] excludedFeatures, String[] applicableFeatures)150         public static TestListItem newTest(
151                 Context context,
152                 int titleResId,
153                 String testName,
154                 Intent intent,
155                 String[] requiredFeatures,
156                 String[] excludedFeatures,
157                 String[] applicableFeatures) {
158             return newTest(
159                     context.getString(titleResId),
160                     testName,
161                     intent,
162                     requiredFeatures,
163                     excludedFeatures,
164                     applicableFeatures);
165         }
166 
167         /**
168          * Creates a new test item with given required and excluded features, the context and the
169          * resource ID of the title.
170          */
newTest( Context context, int titleResId, String testName, Intent intent, String[] requiredFeatures, String[] excludedFeatures)171         public static TestListItem newTest(
172                 Context context,
173                 int titleResId,
174                 String testName,
175                 Intent intent,
176                 String[] requiredFeatures,
177                 String[] excludedFeatures) {
178             return newTest(
179                     context.getString(titleResId),
180                     testName,
181                     intent,
182                     requiredFeatures,
183                     excludedFeatures,
184                     /* applicableFeatures= */ null);
185         }
186 
187         /**
188          * Creates a new test item with given required features, the context and the resource ID of
189          * the title.
190          */
newTest( Context context, int titleResId, String testName, Intent intent, String[] requiredFeatures)191         public static TestListItem newTest(
192                 Context context,
193                 int titleResId,
194                 String testName,
195                 Intent intent,
196                 String[] requiredFeatures) {
197             return newTest(
198                     context.getString(titleResId),
199                     testName,
200                     intent,
201                     requiredFeatures,
202                     /* excludedFeatures= */ null,
203                     /* applicableFeatures= */ null);
204         }
205 
206         /**
207          * Creates a new test item with given display mode, the required, excluded, applicable
208          * features and required configureations and actions.
209          */
newTest( String title, String testName, Intent intent, String[] requiredFeatures, String[] requiredConfigs, String[] requiredActions, String[] excludedFeatures, String[] applicableFeatures, String[] excludedUserTypes, String displayMode, boolean passInEitherMode)210         public static TestListItem newTest(
211                 String title,
212                 String testName,
213                 Intent intent,
214                 String[] requiredFeatures,
215                 String[] requiredConfigs,
216                 String[] requiredActions,
217                 String[] excludedFeatures,
218                 String[] applicableFeatures,
219                 String[] excludedUserTypes,
220                 String displayMode,
221                 boolean passInEitherMode) {
222             return new TestListItem(
223                     title,
224                     testName,
225                     intent,
226                     requiredFeatures,
227                     requiredConfigs,
228                     requiredActions,
229                     excludedFeatures,
230                     applicableFeatures,
231                     excludedUserTypes,
232                     displayMode,
233                     passInEitherMode);
234         }
235 
236         /**
237          * Creates a new test item with given display mode, the required, excluded, applicable
238          * features, required configurations and actions and test pass mode.
239          */
newTest( String title, String testName, Intent intent, String[] requiredFeatures, String[] requiredConfigs, String[] requiredActions, String[] excludedFeatures, String[] applicableFeatures, String[] excludedUserTypes, String displayMode)240         public static TestListItem newTest(
241                 String title,
242                 String testName,
243                 Intent intent,
244                 String[] requiredFeatures,
245                 String[] requiredConfigs,
246                 String[] requiredActions,
247                 String[] excludedFeatures,
248                 String[] applicableFeatures,
249                 String[] excludedUserTypes,
250                 String displayMode) {
251             return new TestListItem(
252                     title,
253                     testName,
254                     intent,
255                     requiredFeatures,
256                     requiredConfigs,
257                     requiredActions,
258                     excludedFeatures,
259                     applicableFeatures,
260                     excludedUserTypes,
261                     displayMode,
262                     /* passInEitherMode= */ false);
263         }
264 
265         /**
266          * Creates a new test item with given required, excluded, applicable features and required
267          * configureations.
268          */
newTest( String title, String testName, Intent intent, String[] requiredFeatures, String[] requiredConfigs, String[] excludedFeatures, String[] applicableFeatures)269         public static TestListItem newTest(
270                 String title,
271                 String testName,
272                 Intent intent,
273                 String[] requiredFeatures,
274                 String[] requiredConfigs,
275                 String[] excludedFeatures,
276                 String[] applicableFeatures) {
277             return new TestListItem(
278                     title,
279                     testName,
280                     intent,
281                     requiredFeatures,
282                     requiredConfigs,
283                     /* requiredActions= */ null,
284                     excludedFeatures,
285                     applicableFeatures,
286                     /* excludedUserTypes= */ null,
287                     /* displayMode= */ null,
288                     /* passInEitherMode= */ false);
289         }
290 
291         /** Creates a new test item with given required, excluded and applicable features. */
newTest( String title, String testName, Intent intent, String[] requiredFeatures, String[] excludedFeatures, String[] applicableFeatures)292         public static TestListItem newTest(
293                 String title,
294                 String testName,
295                 Intent intent,
296                 String[] requiredFeatures,
297                 String[] excludedFeatures,
298                 String[] applicableFeatures) {
299             return new TestListItem(
300                     title,
301                     testName,
302                     intent,
303                     requiredFeatures,
304                     /* requiredConfigs= */ null,
305                     /* requiredActions= */ null,
306                     excludedFeatures,
307                     applicableFeatures,
308                     /* excludedUserTypes= */ null,
309                     /* displayMode= */ null,
310                     /* passInEitherMode= */ false);
311         }
312 
313         /** Creates a new test item with given required and excluded features. */
newTest( String title, String testName, Intent intent, String[] requiredFeatures, String[] excludedFeatures)314         public static TestListItem newTest(
315                 String title,
316                 String testName,
317                 Intent intent,
318                 String[] requiredFeatures,
319                 String[] excludedFeatures) {
320             return new TestListItem(
321                     title,
322                     testName,
323                     intent,
324                     requiredFeatures,
325                     /* requiredConfigs= */ null,
326                     /* requiredActions= */ null,
327                     excludedFeatures,
328                     /* applicableFeatures= */ null,
329                     /* excludedUserTypes= */ null,
330                     /* displayMode= */ null,
331                     /* passInEitherMode= */ false);
332         }
333 
334         /** Creates a new test item with given required features. */
newTest( String title, String testName, Intent intent, String[] requiredFeatures)335         public static TestListItem newTest(
336                 String title, String testName, Intent intent, String[] requiredFeatures) {
337             return new TestListItem(
338                     title,
339                     testName,
340                     intent,
341                     requiredFeatures,
342                     /* requiredConfigs= */ null,
343                     /* requiredActions= */ null,
344                     /* excludedFeatures= */ null,
345                     /* applicableFeatures= */ null,
346                     /* excludedUserTypes= */ null,
347                     /* displayMode= */ null,
348                     /* passInEitherMode= */ false);
349         }
350 
newCategory(Context context, int titleResId)351         public static TestListItem newCategory(Context context, int titleResId) {
352             return newCategory(context.getString(titleResId));
353         }
354 
newCategory(String title)355         public static TestListItem newCategory(String title) {
356             return new TestListItem(
357                     title,
358                     /* testName= */ null,
359                     /* intent= */ null,
360                     /* requiredFeatures= */ null,
361                     /* requiredConfigs= */ null,
362                     /* requiredActions= */ null,
363                     /* excludedFeatures= */ null,
364                     /* applicableFeatures= */ null,
365                     /* excludedUserTypes= */ null,
366                     /* displayMode= */ null,
367                     /* passInEitherMode= */ false);
368         }
369 
TestListItem( String title, String testName, Intent intent, String[] requiredFeatures, String[] excludedFeatures, String[] applicableFeatures)370         protected TestListItem(
371                 String title,
372                 String testName,
373                 Intent intent,
374                 String[] requiredFeatures,
375                 String[] excludedFeatures,
376                 String[] applicableFeatures) {
377             this(
378                     title,
379                     testName,
380                     intent,
381                     requiredFeatures,
382                     /* requiredConfigs= */ null,
383                     /* requiredActions= */ null,
384                     excludedFeatures,
385                     applicableFeatures,
386                     /* excludedUserTypes= */ null,
387                     /* displayMode= */ null,
388                     /* passInEitherMode= */ false);
389         }
390 
TestListItem( String title, String testName, Intent intent, String[] requiredFeatures, String[] requiredConfigs, String[] requiredActions, String[] excludedFeatures, String[] applicableFeatures, String[] excludedUserTypes, String displayMode, boolean passInEitherMode)391         protected TestListItem(
392                 String title,
393                 String testName,
394                 Intent intent,
395                 String[] requiredFeatures,
396                 String[] requiredConfigs,
397                 String[] requiredActions,
398                 String[] excludedFeatures,
399                 String[] applicableFeatures,
400                 String[] excludedUserTypes,
401                 String displayMode,
402                 boolean passInEitherMode) {
403             this.title = title;
404             this.testName = setTestNameSuffix(sCurrentDisplayMode, testName);
405             this.intent = intent;
406             this.requiredActions = requiredActions;
407             this.requiredFeatures = requiredFeatures;
408             this.requiredConfigs = requiredConfigs;
409             this.excludedFeatures = excludedFeatures;
410             this.applicableFeatures = applicableFeatures;
411             this.excludedUserTypes = excludedUserTypes;
412             this.displayMode = displayMode;
413             this.passInEitherMode = passInEitherMode;
414         }
415 
isTest()416         boolean isTest() {
417             return intent != null;
418         }
419     }
420 
TestListAdapter(Context context)421     public TestListAdapter(Context context) {
422         this.mContext = context;
423         this.mLayoutInflater =
424                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
425 
426         TestResultContentObserver observer = new TestResultContentObserver();
427         ContentResolver resolver = context.getContentResolver();
428         resolver.registerContentObserver(
429                 TestResultsProvider.getResultContentUri(context), true, observer);
430     }
431 
loadTestResults()432     public void loadTestResults() {
433         new RefreshTestResultsTask().execute();
434     }
435 
clearTestResults()436     public void clearTestResults() {
437         new ClearTestResultsTask().execute();
438     }
439 
setTestResult(TestResult testResult)440     public void setTestResult(TestResult testResult) {
441         String name = testResult.getName();
442 
443         // Append existing history
444         TestResultHistoryCollection histories = testResult.getHistoryCollection();
445         histories.merge(null, mHistories.get(name));
446 
447         new SetTestResultTask(
448                         name,
449                         testResult.getResult(),
450                         testResult.getDetails(),
451                         testResult.getReportLog(),
452                         histories,
453                         mScreenshotsMetadata.get(name))
454                 .execute();
455     }
456 
setTestFilter(String testFilter)457     void setTestFilter(String testFilter) {
458         mTestFilter = testFilter;
459     }
460 
461     class RefreshTestResultsTask extends AsyncTask<Void, Void, RefreshResult> {
462 
463         @Override
doInBackground(Void... params)464         protected RefreshResult doInBackground(Void... params) {
465             return getRefreshResults(getRows());
466         }
467 
468         @Override
onPostExecute(RefreshResult result)469         protected void onPostExecute(RefreshResult result) {
470             super.onPostExecute(result);
471             mRows.clear();
472             mRows.addAll(result.mItems);
473             mTestResults.clear();
474             mTestResults.putAll(result.mResults);
475             mTestDetails.clear();
476             mTestDetails.putAll(result.mDetails);
477             mReportLogs.clear();
478             mReportLogs.putAll(result.mReportLogs);
479             mHistories.clear();
480             mHistories.putAll(result.mHistories);
481             mScreenshotsMetadata.clear();
482             mScreenshotsMetadata.putAll(result.mScreenshotsMetadata);
483             mHasLoadedResultHistory.set(true);
484             notifyDataSetChanged();
485         }
486     }
487 
488     static class RefreshResult {
489         List<TestListItem> mItems;
490         Map<String, Integer> mResults;
491         Map<String, String> mDetails;
492         Map<String, ReportLog> mReportLogs;
493         Map<String, TestResultHistoryCollection> mHistories;
494         Map<String, TestScreenshotsMetadata> mScreenshotsMetadata;
495 
RefreshResult( List<TestListItem> items, Map<String, Integer> results, Map<String, String> details, Map<String, ReportLog> reportLogs, Map<String, TestResultHistoryCollection> histories, Map<String, TestScreenshotsMetadata> screenshotsMetadata)496         RefreshResult(
497                 List<TestListItem> items,
498                 Map<String, Integer> results,
499                 Map<String, String> details,
500                 Map<String, ReportLog> reportLogs,
501                 Map<String, TestResultHistoryCollection> histories,
502                 Map<String, TestScreenshotsMetadata> screenshotsMetadata) {
503             mItems = items;
504             mResults = results;
505             mDetails = details;
506             mReportLogs = reportLogs;
507             mHistories = histories;
508             mScreenshotsMetadata = screenshotsMetadata;
509         }
510     }
511 
getRows()512     protected abstract List<TestListItem> getRows();
513 
514     static final String[] REFRESH_PROJECTION = {
515         TestResultsProvider._ID,
516         TestResultsProvider.COLUMN_TEST_NAME,
517         TestResultsProvider.COLUMN_TEST_RESULT,
518         TestResultsProvider.COLUMN_TEST_DETAILS,
519         TestResultsProvider.COLUMN_TEST_METRICS,
520         TestResultsProvider.COLUMN_TEST_RESULT_HISTORY,
521         TestResultsProvider.COLUMN_TEST_SCREENSHOTS_METADATA,
522     };
523 
getRefreshResults(List<TestListItem> items)524     RefreshResult getRefreshResults(List<TestListItem> items) {
525         Map<String, Integer> results = new HashMap<String, Integer>();
526         Map<String, String> details = new HashMap<String, String>();
527         Map<String, ReportLog> reportLogs = new HashMap<String, ReportLog>();
528         Map<String, TestResultHistoryCollection> histories = new HashMap<>();
529         Map<String, TestScreenshotsMetadata> screenshotsMetadata = new HashMap<>();
530         ContentResolver resolver = mContext.getContentResolver();
531         Cursor cursor = null;
532         try {
533             cursor =
534                     resolver.query(
535                             TestResultsProvider.getResultContentUri(mContext),
536                             REFRESH_PROJECTION,
537                             null,
538                             null,
539                             null);
540             if (cursor.moveToFirst()) {
541                 do {
542                     String testName = cursor.getString(1);
543                     int testResult = cursor.getInt(2);
544                     String testDetails = cursor.getString(3);
545                     ReportLog reportLog = (ReportLog) deserialize(cursor.getBlob(4));
546                     TestResultHistoryCollection historyCollection =
547                             (TestResultHistoryCollection) deserialize(cursor.getBlob(5));
548                     TestScreenshotsMetadata screenshots =
549                             (TestScreenshotsMetadata) deserialize(cursor.getBlob(6));
550                     results.put(testName, testResult);
551                     details.put(testName, testDetails);
552                     reportLogs.put(testName, reportLog);
553                     histories.put(testName, historyCollection);
554                     screenshotsMetadata.put(testName, screenshots);
555                 } while (cursor.moveToNext());
556             }
557         } finally {
558             if (cursor != null) {
559                 cursor.close();
560             }
561         }
562         return new RefreshResult(
563                 items, results, details, reportLogs, histories, screenshotsMetadata);
564     }
565 
566     class ClearTestResultsTask extends AsyncTask<Void, Void, Void> {
567 
deleteDirectory(File file)568         private void deleteDirectory(File file) {
569             for (File subfile : file.listFiles()) {
570                 if (subfile.isDirectory()) {
571                     deleteDirectory(subfile);
572                 }
573                 subfile.delete();
574             }
575         }
576 
577         @Override
doInBackground(Void... params)578         protected Void doInBackground(Void... params) {
579             ContentResolver resolver = mContext.getContentResolver();
580             resolver.delete(TestResultsProvider.getResultContentUri(mContext), "1", null);
581 
582             // Apart from deleting metadata from content resolver database, need to delete
583             // files generated in LOGS_DIRECTORY. For example screenshots.
584             File resFolder =
585                     new File(
586                             Environment.getExternalStorageDirectory().getAbsolutePath()
587                                     + File.separator
588                                     + LOGS_DIRECTORY);
589             deleteDirectory(resFolder);
590 
591             return null;
592         }
593     }
594 
595     class SetTestResultTask extends AsyncTask<Void, Void, Void> {
596 
597         private final String mTestName;
598         private final int mResult;
599         private final String mDetails;
600         private final ReportLog mReportLog;
601         private final TestResultHistoryCollection mHistoryCollection;
602         private final TestScreenshotsMetadata mScreenshotsMetadata;
603 
SetTestResultTask( String testName, int result, String details, ReportLog reportLog, TestResultHistoryCollection historyCollection, TestScreenshotsMetadata screenshotsMetadata)604         SetTestResultTask(
605                 String testName,
606                 int result,
607                 String details,
608                 ReportLog reportLog,
609                 TestResultHistoryCollection historyCollection,
610                 TestScreenshotsMetadata screenshotsMetadata) {
611             mTestName = testName;
612             mResult = result;
613             mDetails = details;
614             mReportLog = reportLog;
615             mHistoryCollection = historyCollection;
616             mScreenshotsMetadata = screenshotsMetadata;
617         }
618 
619         @Override
doInBackground(Void... params)620         protected Void doInBackground(Void... params) {
621             if (mHasLoadedResultHistory.get()) {
622                 mHistoryCollection.merge(null, mHistories.get(mTestName));
623             } else {
624                 // Loads history from ContentProvider directly if it has not been loaded yet.
625                 ContentResolver resolver = mContext.getContentResolver();
626 
627                 try (Cursor cursor =
628                         resolver.query(
629                                 TestResultsProvider.getTestNameUri(mContext, mTestName),
630                                 new String[] {TestResultsProvider.COLUMN_TEST_RESULT_HISTORY},
631                                 null,
632                                 null,
633                                 null)) {
634                     if (cursor.moveToFirst()) {
635                         do {
636                             TestResultHistoryCollection historyCollection =
637                                     (TestResultHistoryCollection) deserialize(cursor.getBlob(0));
638                             mHistoryCollection.merge(null, historyCollection);
639                         } while (cursor.moveToNext());
640                     }
641                 }
642             }
643             TestResultsProvider.setTestResult(
644                     mContext,
645                     mTestName,
646                     mResult,
647                     mDetails,
648                     mReportLog,
649                     mHistoryCollection,
650                     mScreenshotsMetadata);
651             return null;
652         }
653     }
654 
655     class TestResultContentObserver extends ContentObserver {
656 
TestResultContentObserver()657         public TestResultContentObserver() {
658             super(new Handler());
659         }
660 
661         @Override
onChange(boolean selfChange)662         public void onChange(boolean selfChange) {
663             super.onChange(selfChange);
664             loadTestResults();
665         }
666     }
667 
668     @Override
areAllItemsEnabled()669     public boolean areAllItemsEnabled() {
670         // Section headers for test categories are not clickable.
671         return false;
672     }
673 
674     @Override
isEnabled(int position)675     public boolean isEnabled(int position) {
676         if (getItem(position) == null) {
677             return false;
678         }
679         return getItem(position).isTest();
680     }
681 
682     @Override
getItemViewType(int position)683     public int getItemViewType(int position) {
684         if (getItem(position) == null) {
685             return CATEGORY_HEADER_VIEW_TYPE;
686         }
687         return getItem(position).isTest() ? TEST_VIEW_TYPE : CATEGORY_HEADER_VIEW_TYPE;
688     }
689 
690     @Override
getViewTypeCount()691     public int getViewTypeCount() {
692         return 2;
693     }
694 
695     @Override
getCount()696     public int getCount() {
697         return mRows.size();
698     }
699 
700     @Override
getItem(int position)701     public TestListItem getItem(int position) {
702         return mRows.get(position);
703     }
704 
705     @Override
getItemId(int position)706     public long getItemId(int position) {
707         return position;
708     }
709 
710     /** Gets {@link TestListItem} with the given test name. */
getItemByName(String testName)711     public TestListItem getItemByName(String testName) {
712         for (TestListItem item : mRows) {
713             if (item != null && item.testName != null && item.testName.equals(testName)) {
714                 return item;
715             }
716         }
717         return null;
718     }
719 
getTestResult(int position)720     public int getTestResult(int position) {
721         TestListItem item = getItem(position);
722         return mTestResults.containsKey(item.testName)
723                 ? mTestResults.get(item.testName)
724                 : TestResult.TEST_RESULT_NOT_EXECUTED;
725     }
726 
getTestDetails(int position)727     public String getTestDetails(int position) {
728         TestListItem item = getItem(position);
729         return mTestDetails.containsKey(item.testName) ? mTestDetails.get(item.testName) : null;
730     }
731 
getReportLog(int position)732     public ReportLog getReportLog(int position) {
733         TestListItem item = getItem(position);
734         return mReportLogs.containsKey(item.testName) ? mReportLogs.get(item.testName) : null;
735     }
736 
737     /**
738      * Get test result histories.
739      *
740      * @param position The position of test.
741      * @return A {@link TestResultHistoryCollection} object containing test result histories of
742      *     tests.
743      */
getHistoryCollection(int position)744     public TestResultHistoryCollection getHistoryCollection(int position) {
745         TestListItem item = getItem(position);
746         if (item == null) {
747             return null;
748         }
749         return mHistories.containsKey(item.testName) ? mHistories.get(item.testName) : null;
750     }
751 
752     /**
753      * Get test screenshots metadata
754      *
755      * @param position The position of test
756      * @return A {@link TestScreenshotsMetadata} object containing test screenshots metadata.
757      */
getScreenshotsMetadata(String mode, int position)758     public TestScreenshotsMetadata getScreenshotsMetadata(String mode, int position) {
759         TestListItem item = getItem(mode, position);
760         return mScreenshotsMetadata.containsKey(item.testName)
761                 ? mScreenshotsMetadata.get(item.testName)
762                 : null;
763     }
764 
765     /**
766      * Get test item by the given display mode and position.
767      *
768      * @param mode The display mode.
769      * @param position The position of test.
770      * @return A {@link TestListItem} object containing the test item.
771      */
getItem(String mode, int position)772     public TestListItem getItem(String mode, int position) {
773         return mDisplayModesTests.get(mode).get(position);
774     }
775 
776     /**
777      * Get test item count by the given display mode.
778      *
779      * @param mode The display mode.
780      * @return A count of test items.
781      */
getCount(String mode)782     public int getCount(String mode) {
783         return mDisplayModesTests.getOrDefault(mode, new ArrayList<>()).size();
784     }
785 
786     /**
787      * Get test result by the given display mode and position.
788      *
789      * @param mode The display mode.
790      * @param position The position of test.
791      * @return The test item result.
792      */
getTestResult(String mode, int position)793     public int getTestResult(String mode, int position) {
794         TestListItem item = mDisplayModesTests.get(mode).get(position);
795         return mTestResults.containsKey(item.testName)
796                 ? mTestResults.get(item.testName)
797                 : TestResult.TEST_RESULT_NOT_EXECUTED;
798     }
799 
800     /**
801      * Get test details by the given display mode and position.
802      *
803      * @param mode The display mode.
804      * @param position The position of test.
805      * @return A string containing the test details.
806      */
getTestDetails(String mode, int position)807     public String getTestDetails(String mode, int position) {
808         TestListItem item = mDisplayModesTests.get(mode).get(position);
809         return mTestDetails.containsKey(item.testName) ? mTestDetails.get(item.testName) : null;
810     }
811 
812     /**
813      * Get test report log by the given display mode and position.
814      *
815      * @param mode The display mode.
816      * @param position The position of test.
817      * @return A {@link ReportLog} object containing the test report log of the test item.
818      */
getReportLog(String mode, int position)819     public ReportLog getReportLog(String mode, int position) {
820         TestListItem item = mDisplayModesTests.get(mode).get(position);
821         return mReportLogs.containsKey(item.testName) ? mReportLogs.get(item.testName) : null;
822     }
823 
824     /**
825      * Get test result histories by the given display mode and position.
826      *
827      * @param mode The display mode.
828      * @param position The position of test.
829      * @return A {@link TestResultHistoryCollection} object containing the test result histories of
830      *     the test item.
831      */
getHistoryCollection(String mode, int position)832     public TestResultHistoryCollection getHistoryCollection(String mode, int position) {
833         TestListItem item = mDisplayModesTests.get(mode).get(position);
834         return mHistories.containsKey(item.testName) ? mHistories.get(item.testName) : null;
835     }
836 
allTestsPassed()837     public boolean allTestsPassed() {
838         for (TestListItem item : mRows) {
839             if (item != null
840                     && item.isTest()
841                     && (!mTestResults.containsKey(item.testName)
842                             || (mTestResults.get(item.testName)
843                                     != TestResult.TEST_RESULT_PASSED))) {
844                 return false;
845             }
846         }
847         return true;
848     }
849 
850     @Override
getView(int position, View convertView, ViewGroup parent)851     public View getView(int position, View convertView, ViewGroup parent) {
852         TextView textView;
853         if (convertView == null) {
854             int layout = getLayout(position);
855             textView = (TextView) mLayoutInflater.inflate(layout, parent, false);
856         } else {
857             textView = (TextView) convertView;
858         }
859 
860         TestListItem item = getItem(position);
861 
862         if (item == null) {
863             return textView;
864         }
865 
866         textView.setText(item.title);
867         textView.setPadding(PADDING, 0, PADDING, 0);
868         textView.setCompoundDrawablePadding(PADDING);
869 
870         if (item.isTest()) {
871             int testResult = getTestResult(position);
872             int backgroundResource = 0;
873             int iconResource = 0;
874 
875             /** TODO: Remove fs_ prefix from feature icons since they are used here too. */
876             switch (testResult) {
877                 case TestResult.TEST_RESULT_PASSED:
878                     backgroundResource = R.drawable.test_pass_gradient;
879                     iconResource = R.drawable.fs_good;
880                     break;
881 
882                 case TestResult.TEST_RESULT_FAILED:
883                     backgroundResource = R.drawable.test_fail_gradient;
884                     iconResource = R.drawable.fs_error;
885                     break;
886 
887                 case TestResult.TEST_RESULT_NOT_EXECUTED:
888                     break;
889 
890                 default:
891                     throw new IllegalArgumentException("Unknown test result: " + testResult);
892             }
893 
894             textView.setBackgroundResource(backgroundResource);
895             textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, iconResource, 0);
896         }
897 
898         return textView;
899     }
900 
901     /**
902      * Uses {@link DeviceStateManager} to determine if the device is foldable or not. It relies on
903      * the OEM exposing supported states, and setting
904      * com.android.internal.R.array.config_foldedDeviceStates correctly with the folded states.
905      *
906      * @return true if the device is foldable, false otherwise
907      */
isFoldableDevice()908     public boolean isFoldableDevice() {
909         DeviceStateManager deviceStateManager = mContext.getSystemService(DeviceStateManager.class);
910         if (deviceStateManager == null) {
911             return false;
912         }
913         Set<Integer> supportedStates = deviceStateManager.getSupportedDeviceStates().stream().map(
914                 state -> state.getIdentifier()).collect(Collectors.toSet());
915         int identifier =
916                 mContext.getResources()
917                         .getIdentifier("config_foldedDeviceStates", "array", "android");
918         int[] foldedDeviceStates = mContext.getResources().getIntArray(identifier);
919         return Arrays.stream(foldedDeviceStates).anyMatch(supportedStates::contains);
920     }
921 
getLayout(int position)922     private int getLayout(int position) {
923         int viewType = getItemViewType(position);
924         switch (viewType) {
925             case CATEGORY_HEADER_VIEW_TYPE:
926                 return R.layout.test_category_row;
927             case TEST_VIEW_TYPE:
928                 return android.R.layout.simple_list_item_1;
929             default:
930                 throw new IllegalArgumentException("Illegal view type: " + viewType);
931         }
932     }
933 
deserialize(byte[] bytes)934     public static Object deserialize(byte[] bytes) {
935         if (bytes == null || bytes.length == 0) {
936             return null;
937         }
938         ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes);
939         ObjectInputStream objectInput = null;
940         try {
941             objectInput = new ObjectInputStream(byteStream);
942             return objectInput.readObject();
943         } catch (IOException e) {
944             return null;
945         } catch (ClassNotFoundException e) {
946             return null;
947         } finally {
948             try {
949                 if (objectInput != null) {
950                     objectInput.close();
951                 }
952                 byteStream.close();
953             } catch (IOException e) {
954                 // Ignore close exception.
955             }
956         }
957     }
958 
959     /**
960      * Sets test name suffix. In the folded mode, the suffix is [folded]; otherwise, it is empty
961      * string.
962      *
963      * @param mode A string of current display mode.
964      * @param name A string of test name.
965      * @return A string of test name with suffix, [folded], in the folded mode. A string of input
966      *     test name in the unfolded mode.
967      */
setTestNameSuffix(String mode, String name)968     public static String setTestNameSuffix(String mode, String name) {
969         if (name != null
970                 && mode.equalsIgnoreCase(DisplayMode.FOLDED.toString())
971                 && !name.endsWith(DisplayMode.FOLDED.asSuffix())) {
972             return name + DisplayMode.FOLDED.asSuffix();
973         }
974         return name;
975     }
976 
977     /**
978      * Removes test name suffix. In the unfolded mode, remove the suffix [folded].
979      *
980      * @param mode A string of current display mode.
981      * @param name A string of test name.
982      * @return A string of test name without suffix, [folded], in the unfolded mode. A string of
983      *     input test name in the folded mode.
984      */
removeTestNameSuffix(String mode, String name)985     public static String removeTestNameSuffix(String mode, String name) {
986         if (name != null
987                 && mode.equalsIgnoreCase(DisplayMode.UNFOLDED.toString())
988                 && name.endsWith(DisplayMode.FOLDED.asSuffix())) {
989             return name.substring(0, name.length() - DisplayMode.FOLDED.asSuffix().length());
990         }
991         return name;
992     }
993 }
994