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