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.TestListActivity.sCurrentDisplayMode;
20 import static com.android.cts.verifier.TestListAdapter.setTestNameSuffix;
21 
22 import android.app.ActionBar;
23 import android.app.AlertDialog;
24 import android.app.Dialog;
25 import android.content.ContentResolver;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.DialogInterface;
29 import android.content.DialogInterface.OnCancelListener;
30 import android.content.pm.PackageManager;
31 import android.database.Cursor;
32 import android.os.Bundle;
33 import android.os.PowerManager;
34 import android.os.PowerManager.WakeLock;
35 import android.util.Log;
36 import android.view.LayoutInflater;
37 import android.view.MenuItem;
38 import android.view.View;
39 import android.view.View.OnClickListener;
40 import android.widget.ImageButton;
41 import android.widget.Toast;
42 
43 import com.android.compatibility.common.util.ReportLog;
44 
45 import java.util.List;
46 import java.util.stream.Collectors;
47 import java.util.stream.IntStream;
48 
49 /**
50  * {@link Activity}s to handle clicks to the pass and fail buttons of the pass fail buttons layout.
51  *
52  * <ol>
53  *     <li>Include the pass fail buttons layout in your layout:
54  *         <pre><include layout="@layout/pass_fail_buttons" /></pre>
55  *     </li>
56  *     <li>Extend one of the activities and call setPassFailButtonClickListeners after
57  *         setting your content view.</li>
58  *     <li>Make sure to call setResult(RESULT_CANCEL) in your Activity initially.</li>
59  *     <li>Optionally call setInfoTextResources to add an info button that will show a
60  *         dialog with instructional text.</li>
61  * </ol>
62  */
63 public class PassFailButtons {
64     private static final String TAG = PassFailButtons.class.getSimpleName();
65 
66     private static final int INFO_DIALOG_ID = 1337;
67 
68     private static final String INFO_DIALOG_VIEW_ID = "infoDialogViewId";
69     private static final String INFO_DIALOG_TITLE_ID = "infoDialogTitleId";
70     private static final String INFO_DIALOG_MESSAGE_ID = "infoDialogMessageId";
71 
72     // ReportLog file for CTS-Verifier. The "stream" name gets mapped to the test class name.
73     public static final String GENERAL_TESTS_REPORT_LOG_NAME = "CtsVerifierGeneralTestCases";
74     public static final String AUDIO_TESTS_REPORT_LOG_NAME = "CtsVerifierAudioTestCases";
75 
76     private static final String SECTION_UNDEFINED = "undefined_section_name";
77 
78     // Interface mostly for making documentation and refactoring easier...
79     public interface PassFailActivity {
80 
81         /**
82          * Hooks up the pass and fail buttons to click listeners that will record the test results.
83          * <p>
84          * Call from {@link Activity#onCreate} after {@link Activity #setContentView(int)}.
85          */
setPassFailButtonClickListeners()86         void setPassFailButtonClickListeners();
87 
88         /**
89          * Adds an initial informational dialog that appears when entering the test activity for
90          * the first time. Also enables the visibility of an "Info" button between the "Pass" and
91          * "Fail" buttons that can be clicked to show the information dialog again.
92          * <p>
93          * Call from {@link Activity#onCreate} after {@link Activity #setContentView(int)}.
94          *
95          * @param titleId for the text shown in the dialog title area
96          * @param messageId for the text shown in the dialog's body area
97          */
setInfoResources(int titleId, int messageId, int viewId)98         void setInfoResources(int titleId, int messageId, int viewId);
99 
getPassButton()100         View getPassButton();
101 
102         /**
103          * Returns a unique identifier for the test.  Usually, this is just the class name.
104          */
getTestId()105         String getTestId();
106 
107         /** @return null or details about the test run. */
getTestDetails()108         String getTestDetails();
109 
110         /**
111          * Set the result of the test and finish the activity.
112          *
113          * @param passed Whether or not the test passed.
114          */
setTestResultAndFinish(boolean passed)115         void setTestResultAndFinish(boolean passed);
116 
117         /**
118          * @return The name of the file to store the (suite of) ReportLog information.
119          */
getReportFileName()120         public String getReportFileName();
121 
122         /**
123          * @return A unique name to serve as a section header in the CtsVerifierReportLog file.
124          * Tests need to conform to the underscore_delineated_name standard for use with
125          * the protobuff/json ReportLog parsing in Google3
126          */
getReportSectionName()127         public String getReportSectionName();
128 
129         /**
130          * Test subclasses can override this to record their CtsVerifierReportLogs.
131          * This is called when the test is exited
132          */
recordTestResults()133         void recordTestResults();
134 
135         /** @return A {@link ReportLog} that is used to record test metric data. */
getReportLog()136         CtsVerifierReportLog getReportLog();
137 
138         /**
139          * @return A {@link TestResultHistoryCollection} that is used to record test execution time.
140          */
getHistoryCollection()141         TestResultHistoryCollection getHistoryCollection();
142     }   /* class PassFailButtons.PassFailActivity */
143 
144     public static class Activity extends android.app.Activity implements PassFailActivity {
145         private WakeLock mWakeLock;
146         private final CtsVerifierReportLog mReportLog;
147         private final TestResultHistoryCollection mHistoryCollection;
148 
Activity()149         public Activity() {
150             this.mReportLog = new CtsVerifierReportLog(getReportFileName(), getReportSectionName());
151             this.mHistoryCollection = new TestResultHistoryCollection();
152         }
153 
154         @Override
onResume()155         protected void onResume() {
156             super.onResume();
157             if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
158                 mWakeLock = ((PowerManager) getSystemService(Context.POWER_SERVICE))
159                         .newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "PassFailButtons");
160                 mWakeLock.acquire();
161             }
162         }
163 
164         @Override
onPause()165         protected void onPause() {
166             super.onPause();
167             if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
168                 mWakeLock.release();
169             }
170         }
171 
172         @Override
setPassFailButtonClickListeners()173         public void setPassFailButtonClickListeners() {
174             setPassFailClickListeners(this);
175         }
176 
177         @Override
setInfoResources(int titleId, int messageId, int viewId)178         public void setInfoResources(int titleId, int messageId, int viewId) {
179             setInfo(this, titleId, messageId, viewId);
180         }
181 
182         @Override
getPassButton()183         public View getPassButton() {
184             return getPassButtonView(this);
185         }
186 
187         @Override
onCreateDialog(int id, Bundle args)188         public Dialog onCreateDialog(int id, Bundle args) {
189             return createDialog(this, id, args);
190         }
191 
192         @Override
getTestId()193         public String getTestId() {
194             return setTestNameSuffix(sCurrentDisplayMode, getClass().getName());
195         }
196 
197         @Override
getTestDetails()198         public String getTestDetails() {
199             return null;
200         }
201 
202         @Override
setTestResultAndFinish(boolean passed)203         public void setTestResultAndFinish(boolean passed) {
204             PassFailButtons.setTestResultAndFinishHelper(
205                     this, getTestId(), getTestDetails(), passed, getReportLog(),
206                     getHistoryCollection());
207         }
208 
209         @Override
getReportLog()210         public CtsVerifierReportLog getReportLog() {
211             return mReportLog;
212         }
213 
214         /**
215          * @return The name of the file to store the (suite of) ReportLog information.
216          */
217         @Override
getReportFileName()218         public String getReportFileName() { return GENERAL_TESTS_REPORT_LOG_NAME; }
219 
220         @Override
getReportSectionName()221         public String getReportSectionName() {
222             return setTestNameSuffix(sCurrentDisplayMode, SECTION_UNDEFINED);
223         }
224 
225         @Override
getHistoryCollection()226         public TestResultHistoryCollection getHistoryCollection() { return mHistoryCollection; }
227 
228         @Override
onCreate(Bundle savedInstanceState)229         protected void onCreate(Bundle savedInstanceState) {
230             super.onCreate(savedInstanceState);
231             ActionBar actBar = getActionBar();
232             if (actBar != null) {
233                 actBar.setDisplayHomeAsUpEnabled(true);
234             }
235         }
236 
237         @Override
onOptionsItemSelected(MenuItem item)238         public boolean onOptionsItemSelected(MenuItem item) {
239             if (item.getItemId() == android.R.id.home) {
240                 onBackPressed();
241                 return true;
242             }
243             return super.onOptionsItemSelected(item);
244         }
245 
246         @Override
recordTestResults()247         public void recordTestResults() {
248             // default - NOP
249         }
250     }   /* class PassFailButtons.Activity */
251 
252     public static class ListActivity extends android.app.ListActivity implements PassFailActivity {
253 
254         private final CtsVerifierReportLog mReportLog;
255         private final TestResultHistoryCollection mHistoryCollection;
256 
ListActivity()257         public ListActivity() {
258             this.mReportLog = new CtsVerifierReportLog(getReportFileName(), getReportSectionName());
259             this.mHistoryCollection = new TestResultHistoryCollection();
260         }
261 
262         @Override
setPassFailButtonClickListeners()263         public void setPassFailButtonClickListeners() {
264             setPassFailClickListeners(this);
265         }
266 
267         @Override
setInfoResources(int titleId, int messageId, int viewId)268         public void setInfoResources(int titleId, int messageId, int viewId) {
269             setInfo(this, titleId, messageId, viewId);
270         }
271 
272         @Override
getPassButton()273         public View getPassButton() {
274             return getPassButtonView(this);
275         }
276 
277         @Override
onCreateDialog(int id, Bundle args)278         public Dialog onCreateDialog(int id, Bundle args) {
279             return createDialog(this, id, args);
280         }
281 
282         @Override
getTestId()283         public String getTestId() {
284             return setTestNameSuffix(sCurrentDisplayMode, getClass().getName());
285         }
286 
287         @Override
getTestDetails()288         public String getTestDetails() {
289             return null;
290         }
291 
292         @Override
setTestResultAndFinish(boolean passed)293         public void setTestResultAndFinish(boolean passed) {
294             PassFailButtons.setTestResultAndFinishHelper(
295                     this, getTestId(), getTestDetails(), passed, getReportLog(),
296                     getHistoryCollection());
297         }
298 
299         @Override
getReportLog()300         public CtsVerifierReportLog getReportLog() {
301             return mReportLog;
302         }
303 
304         /**
305          * @return The name of the file to store the (suite of) ReportLog information.
306          */
307         @Override
getReportFileName()308         public String getReportFileName() { return GENERAL_TESTS_REPORT_LOG_NAME; }
309 
310         @Override
getReportSectionName()311         public String getReportSectionName() {
312             return setTestNameSuffix(sCurrentDisplayMode, SECTION_UNDEFINED);
313         }
314 
315         @Override
getHistoryCollection()316         public TestResultHistoryCollection getHistoryCollection() { return mHistoryCollection; }
317 
318         @Override
onCreate(Bundle savedInstanceState)319         protected void onCreate(Bundle savedInstanceState) {
320             super.onCreate(savedInstanceState);
321             ActionBar actBar = getActionBar();
322             if (actBar != null) {
323                 actBar.setDisplayHomeAsUpEnabled(true);
324             }
325         }
326 
327         @Override
onOptionsItemSelected(MenuItem item)328         public boolean onOptionsItemSelected(MenuItem item) {
329             if (item.getItemId() == android.R.id.home) {
330                 onBackPressed();
331                 return true;
332             }
333             return super.onOptionsItemSelected(item);
334         }
335 
336         @Override
recordTestResults()337         public void recordTestResults() {
338             // default - NOP
339         }
340     } // class PassFailButtons.ListActivity
341 
342     public static class TestListActivity extends AbstractTestListActivity
343             implements PassFailActivity {
344 
345         private final CtsVerifierReportLog mReportLog;
346 
TestListActivity()347         public TestListActivity() {
348             // TODO(b/186555602): temporary hack^H^H^H^H workaround to fix crash
349             // This DOES NOT in fact fix that bug.
350             // if (true) this.mReportLog = new CtsVerifierReportLog(b/186555602, "42"); else
351 
352             this.mReportLog = new CtsVerifierReportLog(getReportFileName(), getReportSectionName());
353         }
354 
355         @Override
setPassFailButtonClickListeners()356         public void setPassFailButtonClickListeners() {
357             setPassFailClickListeners(this);
358         }
359 
360         @Override
setInfoResources(int titleId, int messageId, int viewId)361         public void setInfoResources(int titleId, int messageId, int viewId) {
362             setInfo(this, titleId, messageId, viewId);
363         }
364 
365         @Override
getPassButton()366         public View getPassButton() {
367             return getPassButtonView(this);
368         }
369 
370         @Override
onCreateDialog(int id, Bundle args)371         public Dialog onCreateDialog(int id, Bundle args) {
372             return createDialog(this, id, args);
373         }
374 
375         @Override
getTestId()376         public String getTestId() {
377             return setTestNameSuffix(sCurrentDisplayMode, getClass().getName());
378         }
379 
380         @Override
getTestDetails()381         public String getTestDetails() {
382             return null;
383         }
384 
385         @Override
setTestResultAndFinish(boolean passed)386         public void setTestResultAndFinish(boolean passed) {
387             PassFailButtons.setTestResultAndFinishHelper(
388                     this, getTestId(), getTestDetails(), passed, getReportLog(),
389                     getHistoryCollection());
390         }
391 
392         @Override
getReportLog()393         public CtsVerifierReportLog getReportLog() {
394             return mReportLog;
395         }
396 
397         /**
398          * @return The name of the file to store the (suite of) ReportLog information.
399          */
400         @Override
getReportFileName()401         public String getReportFileName() { return GENERAL_TESTS_REPORT_LOG_NAME; }
402 
403         @Override
getReportSectionName()404         public String getReportSectionName() {
405             return setTestNameSuffix(sCurrentDisplayMode, SECTION_UNDEFINED);
406         }
407 
408 
409         /**
410          * Get existing test history to aggregate.
411          */
412         @Override
getHistoryCollection()413         public TestResultHistoryCollection getHistoryCollection() {
414             List<TestResultHistoryCollection> histories =
415                 IntStream.range(0, mAdapter.getCount())
416                 .mapToObj(mAdapter::getHistoryCollection)
417                 .collect(Collectors.toList());
418             TestResultHistoryCollection historyCollection = new TestResultHistoryCollection();
419             historyCollection.merge(getTestId(), histories);
420             return historyCollection;
421         }
422 
updatePassButton()423         public void updatePassButton() {
424             getPassButton().setEnabled(mAdapter.allTestsPassed());
425         }
426 
427         @Override
onCreate(Bundle savedInstanceState)428         protected void onCreate(Bundle savedInstanceState) {
429             super.onCreate(savedInstanceState);
430             ActionBar actBar = getActionBar();
431             if (actBar != null) {
432                 actBar.setDisplayHomeAsUpEnabled(true);
433             }
434         }
435 
436         @Override
onOptionsItemSelected(MenuItem item)437         public boolean onOptionsItemSelected(MenuItem item) {
438             if (item.getItemId() == android.R.id.home) {
439                 onBackPressed();
440                 return true;
441             }
442             return super.onOptionsItemSelected(item);
443         }
444 
445         @Override
recordTestResults()446         public void recordTestResults() {
447             // default - NOP
448         }
449     } // class PassFailButtons.TestListActivity
450 
451     protected static <T extends android.app.Activity & PassFailActivity>
setPassFailClickListeners(final T activity)452             void setPassFailClickListeners(final T activity) {
453         View.OnClickListener clickListener = new View.OnClickListener() {
454             @Override
455             public void onClick(View target) {
456                 setTestResultAndFinish(activity, activity.getTestId(), activity.getTestDetails(),
457                         activity.getReportLog(), activity.getHistoryCollection(), target);
458             }
459         };
460 
461         View passButton = activity.findViewById(R.id.pass_button);
462         passButton.setOnClickListener(clickListener);
463         passButton.setOnLongClickListener(new View.OnLongClickListener() {
464             @Override
465             public boolean onLongClick(View view) {
466                 Toast.makeText(activity, R.string.pass_button_text, Toast.LENGTH_SHORT).show();
467                 return true;
468             }
469         });
470 
471         View failButton = activity.findViewById(R.id.fail_button);
472         failButton.setOnClickListener(clickListener);
473         failButton.setOnLongClickListener(new View.OnLongClickListener() {
474             @Override
475             public boolean onLongClick(View view) {
476                 Toast.makeText(activity, R.string.fail_button_text, Toast.LENGTH_SHORT).show();
477                 return true;
478             }
479         });
480     } // class PassFailButtons.<T extends android.app.Activity & PassFailActivity>
481 
setInfo(final android.app.Activity activity, final int titleId, final int messageId, final int viewId)482     protected static void setInfo(final android.app.Activity activity, final int titleId,
483             final int messageId, final int viewId) {
484         // Show the middle "info" button and make it show the info dialog when clicked.
485         View infoButton = activity.findViewById(R.id.info_button);
486         infoButton.setVisibility(View.VISIBLE);
487         infoButton.setOnClickListener(new OnClickListener() {
488             @Override
489             public void onClick(View view) {
490                 showInfoDialog(activity, titleId, messageId, viewId);
491             }
492         });
493         infoButton.setOnLongClickListener(new View.OnLongClickListener() {
494             @Override
495             public boolean onLongClick(View view) {
496                 Toast.makeText(activity, R.string.info_button_text, Toast.LENGTH_SHORT).show();
497                 return true;
498             }
499         });
500 
501         // Show the info dialog if the user has never seen it before.
502         if (!hasSeenInfoDialog(activity)) {
503             showInfoDialog(activity, titleId, messageId, viewId);
504         }
505     }
506 
hasSeenInfoDialog(android.app.Activity activity)507     protected static boolean hasSeenInfoDialog(android.app.Activity activity) {
508         ContentResolver resolver = activity.getContentResolver();
509         Cursor cursor = null;
510         try {
511             cursor = resolver.query(TestResultsProvider.getTestNameUri(activity),
512                     new String[] {TestResultsProvider.COLUMN_TEST_INFO_SEEN}, null, null, null);
513             return cursor.moveToFirst() && cursor.getInt(0) > 0;
514         } finally {
515             if (cursor != null) {
516                 cursor.close();
517             }
518         }
519     }
520 
showInfoDialog(final android.app.Activity activity, int titleId, int messageId, int viewId)521     protected static void showInfoDialog(final android.app.Activity activity, int titleId,
522             int messageId, int viewId) {
523         Bundle args = new Bundle();
524         args.putInt(INFO_DIALOG_TITLE_ID, titleId);
525         args.putInt(INFO_DIALOG_MESSAGE_ID, messageId);
526         args.putInt(INFO_DIALOG_VIEW_ID, viewId);
527         activity.showDialog(INFO_DIALOG_ID, args);
528     }
529 
createDialog(final android.app.Activity activity, int id, Bundle args)530     protected static Dialog createDialog(final android.app.Activity activity, int id, Bundle args) {
531         switch (id) {
532             case INFO_DIALOG_ID:
533                 return createInfoDialog(activity, id, args);
534             default:
535                 throw new IllegalArgumentException("Bad dialog id: " + id);
536         }
537     }
538 
createInfoDialog(final android.app.Activity activity, int id, Bundle args)539     protected static Dialog createInfoDialog(final android.app.Activity activity, int id,
540             Bundle args) {
541         int viewId = args.getInt(INFO_DIALOG_VIEW_ID);
542         int titleId = args.getInt(INFO_DIALOG_TITLE_ID);
543         int messageId = args.getInt(INFO_DIALOG_MESSAGE_ID);
544 
545         AlertDialog.Builder builder = new AlertDialog.Builder(activity).setIcon(
546                 android.R.drawable.ic_dialog_info).setTitle(titleId);
547         if (viewId > 0) {
548             LayoutInflater inflater = (LayoutInflater) activity
549                     .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
550             builder.setView(inflater.inflate(viewId, null));
551         } else {
552             builder.setMessage(messageId);
553         }
554         builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
555             @Override
556             public void onClick(DialogInterface dialog, int which) {
557                 markSeenInfoDialog(activity);
558             }
559         }).setOnCancelListener(new OnCancelListener() {
560             @Override
561             public void onCancel(DialogInterface dialog) {
562                 markSeenInfoDialog(activity);
563             }
564         });
565         return builder.create();
566     }
567 
markSeenInfoDialog(android.app.Activity activity)568     protected static void markSeenInfoDialog(android.app.Activity activity) {
569         ContentResolver resolver = activity.getContentResolver();
570         ContentValues values = new ContentValues(2);
571         String activityName = setTestNameSuffix(sCurrentDisplayMode, activity.getClass().getName());
572         values.put(TestResultsProvider.COLUMN_TEST_NAME, activityName);
573         values.put(TestResultsProvider.COLUMN_TEST_INFO_SEEN, 1);
574         int numUpdated = resolver.update(
575                 TestResultsProvider.getTestNameUri(activity), values, null, null);
576         if (numUpdated == 0) {
577             resolver.insert(TestResultsProvider.getResultContentUri(activity), values);
578         }
579     }
580 
581     /** Set the test result corresponding to the button clicked and finish the activity. */
setTestResultAndFinish(android.app.Activity activity, String testId, String testDetails, ReportLog reportLog, TestResultHistoryCollection historyCollection, View target)582     protected static void setTestResultAndFinish(android.app.Activity activity, String testId,
583             String testDetails, ReportLog reportLog, TestResultHistoryCollection historyCollection,
584             View target) {
585 
586         boolean passed;
587         if (target.getId() == R.id.pass_button) {
588             passed = true;
589         } else if (target.getId() == R.id.fail_button) {
590             passed = false;
591         } else {
592             throw new IllegalArgumentException("Unknown id: " + target.getId());
593         }
594 
595         // Let test classes record their CTSVerifierReportLogs
596         ((PassFailActivity) activity).recordTestResults();
597 
598         setTestResultAndFinishHelper(activity, testId, testDetails, passed, reportLog, historyCollection);
599     }
600 
601     /** Set the test result and finish the activity. */
setTestResultAndFinishHelper(android.app.Activity activity, String testId, String testDetails, boolean passed, ReportLog reportLog, TestResultHistoryCollection historyCollection)602     protected static void setTestResultAndFinishHelper(android.app.Activity activity, String testId,
603             String testDetails, boolean passed, ReportLog reportLog,
604             TestResultHistoryCollection historyCollection) {
605         if (passed) {
606             TestResult.setPassedResult(activity, testId, testDetails, reportLog, historyCollection);
607         } else {
608             TestResult.setFailedResult(activity, testId, testDetails, reportLog, historyCollection);
609         }
610 
611         activity.finish();
612     }
613 
getPassButtonView(android.app.Activity activity)614     protected static ImageButton getPassButtonView(android.app.Activity activity) {
615         return (ImageButton) activity.findViewById(R.id.pass_button);
616     }
617 
618 } // class PassFailButtons
619