1 /*
2  * Copyright (C) 2015 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 android.app.AlertDialog;
20 import android.content.ActivityNotFoundException;
21 import android.content.Context;
22 import android.content.DialogInterface;
23 import android.content.Intent;
24 import android.content.pm.PackageManager;
25 import android.database.DataSetObserver;
26 import android.os.Bundle;
27 import android.util.Log;
28 import android.view.LayoutInflater;
29 import android.view.MotionEvent;
30 import android.view.View;
31 import android.view.View.OnClickListener;
32 import android.widget.Button;
33 import android.widget.ImageView;
34 import android.widget.ListView;
35 import android.widget.ScrollView;
36 import android.widget.TextView;
37 import android.widget.Toast;
38 
39 import androidx.annotation.Nullable;
40 
41 /**
42  * Test list activity that supports showing dialogs with pass/fail buttons instead of
43  * starting new activities.
44  *
45  * <p>In addition to that dialogs have a 'go' button that can be configured to launch an intent.
46  * Instructions are shown on top of the screen and a test preparation button is provided.
47  */
48 public abstract class DialogTestListActivity extends PassFailButtons.TestListActivity {
49     private final String TAG = "DialogTestListActivity";
50     private final int mLayoutId;
51     private final int mTitleStringId;
52     private final int mInfoStringId;
53     private final int mInstructionsStringId;
54 
55     /**
56      * The button to prepare the test, populated if the layout ID used to construct the object
57      * contains a {@code prepare_test_button} {@link Button}.
58      */
59     @Nullable protected Button mPrepareTestButton;
60 
61     protected ListView mTestFeaturesList;
62 
63     protected int mCurrentTestPosition;
64 
65     /**
66      * Constructs the activity with the given resources.
67      *
68      * @param layoutId The layout to inflate, which must contain a {@link TextView} with ID {@code
69      * test_instructions} for the test instructions within a {@link ScrollView}, a {@link ListView}
70      * with ID {@code android:list} for the tests, and must include the {@code pass_fail_buttons}
71      * layout. If a custom layout is not required, {@code dialog_test_list_activity} can be used.
72      * @param titleStringId The string resource to use for the title at the top of the screen.
73      * @param infoStringId The string resource to use for the info button between the pass and fail
74      * buttons.
75      * @param instructionsStringId The string resource to use for the explanation that populates the
76      * {@link TextView} with ID {@code test_instructions} in the given layout.
77      */
DialogTestListActivity(int layoutId, int titleStringId, int infoStringId, int instructionsStringId)78     protected DialogTestListActivity(int layoutId, int titleStringId, int infoStringId,
79             int instructionsStringId) {
80         mLayoutId = layoutId;
81         mTitleStringId = titleStringId;
82         mInfoStringId = infoStringId;
83         mInstructionsStringId = instructionsStringId;
84     }
85 
86     @Override
onCreate(Bundle savedInstanceState)87     protected void onCreate(Bundle savedInstanceState) {
88         super.onCreate(savedInstanceState);
89 
90         setContentView(mLayoutId);
91         setInfoResources(mTitleStringId, mInfoStringId, -1);
92         setPassFailButtonClickListeners();
93         getPassButton().setEnabled(false);
94         setResult(RESULT_CANCELED);
95 
96         ArrayTestListAdapter adapter = new ArrayTestListAdapter(this);
97 
98         setupTests(adapter);
99 
100         adapter.registerDataSetObserver(new DataSetObserver() {
101             @Override
102             public void onChanged() {
103                 updatePassButton();
104             }
105         });
106 
107         setTestListAdapter(adapter);
108 
109         mCurrentTestPosition = 0;
110 
111         TextView instructionTextView = (TextView) findViewById(R.id.test_instructions);
112         instructionTextView.setText(mInstructionsStringId);
113         mPrepareTestButton = (Button) findViewById(R.id.prepare_test_button);
114         mTestFeaturesList = (ListView) findViewById(android.R.id.list);
115         if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
116             mTestFeaturesList.setOnTouchListener((View v, MotionEvent e) -> {
117                 switch (e.getAction()) {
118                     case MotionEvent.ACTION_DOWN:
119                         v.getParent().requestDisallowInterceptTouchEvent(true);
120                         break;
121                     case MotionEvent.ACTION_UP:
122                         v.getParent().requestDisallowInterceptTouchEvent(false);
123                         break;
124                     default:
125                 }
126                 return false;
127             });
128         }
129     }
130 
131     /**
132      * Subclasses must add their tests items to the provided adapter(usually instances of
133      * {@link DialogTestListItem} or {@link DialogTestListItemWithIcon} but any class deriving from
134      * {@link TestListAdapter.TestListItem} will do).
135      *
136      * @param adapter The adapter to add test items to.
137      */
setupTests(ArrayTestListAdapter adapter)138     protected abstract void setupTests(ArrayTestListAdapter adapter);
139 
140     public class DefaultTestCallback implements DialogTestListItem.TestCallback {
141         final private DialogTestListItem mTest;
142 
DefaultTestCallback(DialogTestListItem test)143         public DefaultTestCallback(DialogTestListItem test) {
144             mTest = test;
145         }
146 
147         @Override
onPass()148         public void onPass() {
149             clearRemainingState(mTest);
150             setTestResult(mTest, TestResult.TEST_RESULT_PASSED);
151         }
152 
153         @Override
onFail()154         public void onFail() {
155             clearRemainingState(mTest);
156             setTestResult(mTest, TestResult.TEST_RESULT_FAILED);
157         }
158     }
159 
showManualTestDialog(final DialogTestListItem test)160     public void showManualTestDialog(final DialogTestListItem test) {
161         showManualTestDialog(test, new DefaultTestCallback(test));
162     }
163 
showManualTestDialog(final DialogTestListItem test, final DialogTestListItem.TestCallback callback)164     public void showManualTestDialog(final DialogTestListItem test,
165             final DialogTestListItem.TestCallback callback) {
166         AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this)
167                 .setIcon(android.R.drawable.ic_dialog_info)
168                 .setTitle(mTitleStringId)
169                 .setPositiveButton(R.string.pass_button_text, new AlertDialog.OnClickListener() {
170                     @Override
171                     public void onClick(DialogInterface dialog, int which) {
172                         callback.onPass();
173                     }
174                 })
175                 .setNegativeButton(R.string.fail_button_text, new AlertDialog.OnClickListener() {
176                     @Override
177                     public void onClick(DialogInterface dialog, int which) {
178                         callback.onFail();
179                     }
180                 });
181         if (test.intent != null) {
182             dialogBuilder.setNeutralButton(R.string.go_button_text, null);
183         }
184         View customView = test.getCustomView();
185         if (customView != null) {
186             dialogBuilder.setView(customView);
187         } else {
188             dialogBuilder.setMessage(test.getManualTestInstruction());
189         }
190         final AlertDialog dialog = dialogBuilder.show();
191         // Note: Setting the OnClickListener on the Dialog rather than the Builder, prevents the
192         // dialog being dismissed on onClick.
193         if (test.intent != null) {
194             dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(new OnClickListener() {
195                 @Override
196                 public void onClick(View v) {
197                     if (!startTestIntent(test)) {
198                         dialog.dismiss();
199                     }
200                 }
201             });
202         }
203     }
204 
205     @Override
handleItemClick(ListView l, View v, int position, long id)206     protected void handleItemClick(ListView l, View v, int position, long id) {
207         TestListAdapter.TestListItem test = (TestListAdapter.TestListItem) getListAdapter()
208                 .getItem(position);
209         if (test instanceof DialogTestListItem) {
210             mCurrentTestPosition = position;
211             ((DialogTestListItem) test).performTest(this);
212         } else {
213             try {
214                 super.handleItemClick(l, v, position, id);
215             } catch (ActivityNotFoundException e) {
216                 Log.d(TAG, "handleItemClick() threw exception: ", e);
217                 setTestResult(test, TestResult.TEST_RESULT_FAILED);
218                 showToast(R.string.test_failed_cannot_start_intent);
219             }
220         }
221     }
222 
223 
224     /**
225      * Start a test's manual intent
226      *
227      * @param test The test the manual intent of which is to be started.
228      * @return true if activity could be started successfully, false otherwise.
229      */
startTestIntent(final DialogTestListItem test)230     boolean startTestIntent(final DialogTestListItem test) {
231         final Intent intent = test.intent;
232         try {
233             startActivity(intent);
234         } catch (ActivityNotFoundException e) {
235             Log.w(TAG, "Cannot start activity.", e);
236             Toast.makeText(this, "Cannot start " + intent, Toast.LENGTH_LONG).show();
237             setTestResult(test, TestResult.TEST_RESULT_FAILED);
238             return false;
239         }
240         return true;
241     }
242 
clearRemainingState(final DialogTestListItem test)243     protected void clearRemainingState(final DialogTestListItem test) {
244         // do nothing, override in subclass if needed
245     }
246 
setTestResult(TestListAdapter.TestListItem test, int result)247     protected void setTestResult(TestListAdapter.TestListItem test, int result) {
248         // Bundle result in an intent to feed into handleLaunchTestResult
249         Intent resultIntent = new Intent();
250         TestResult.addResultData(resultIntent, result, test.testName, /* testDetails */ null,
251                 /* reportLog */ null, null);
252         handleLaunchTestResult(RESULT_OK, resultIntent);
253         getListView().smoothScrollToPosition(mCurrentTestPosition + 1);
254     }
255 
setTestResult(String testName, int result)256     protected void setTestResult(String testName, int result) {
257         // Bundle result in an intent to feed into handleLaunchTestResult
258         Intent resultIntent = new Intent();
259         TestResult.addResultData(resultIntent, result, testName, /* testDetails */ null,
260                 /* reportLog */ null, null);
261         handleLaunchTestResult(RESULT_OK, resultIntent);
262         getListView().smoothScrollToPosition(mCurrentTestPosition + 1);
263     }
264 
showToast(int messageId)265     protected void showToast(int messageId) {
266         String message = getString(messageId);
267         Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
268     }
269 
270     protected static class DialogTestListItem extends TestListAdapter.TestListItem {
271 
272         public interface TestCallback {
onPass()273             void onPass();
274 
onFail()275             void onFail();
276         }
277 
278         private String mManualInstruction;
279 
DialogTestListItem(Context context, String nameId, String testId)280         public DialogTestListItem(Context context, String nameId, String testId) {
281             super(nameId, testId, null, null, null, null);
282         }
283 
DialogTestListItem(Context context, int nameResId, String testId)284         public DialogTestListItem(Context context, int nameResId, String testId) {
285             super(context.getString(nameResId), testId, null, null, null, null);
286         }
287 
DialogTestListItem(Context context, int nameResId, String testId, int testInstructionResId)288         public DialogTestListItem(Context context, int nameResId, String testId,
289                 int testInstructionResId) {
290             this(context, nameResId, testId, testInstructionResId, null);
291         }
292 
DialogTestListItem(Context context, int nameResId, String testId, int testInstructionResId, Intent testIntent)293         public DialogTestListItem(Context context, int nameResId, String testId,
294                 int testInstructionResId, Intent testIntent) {
295             super(context.getString(nameResId), testId, testIntent, null, null, null);
296             mManualInstruction = context.getString(testInstructionResId);
297         }
298 
performTest(DialogTestListActivity activity)299         public void performTest(DialogTestListActivity activity) {
300             activity.showManualTestDialog(this);
301         }
302 
getManualTestInstruction()303         public String getManualTestInstruction() {
304             return mManualInstruction;
305         }
306 
getManualTestIntent()307         public Intent getManualTestIntent() {
308             return intent;
309         }
310 
getCustomView()311         public View getCustomView() {
312             return null;
313         }
314 
315         @Override
isTest()316         boolean isTest() {
317             return true;
318         }
319     }
320 
321     protected static class DialogTestListItemWithIcon extends DialogTestListItem {
322 
323         private final int mImageResId;
324         private final Context mContext;
325 
DialogTestListItemWithIcon(Context context, int nameResId, String testId, int testInstructionResId, Intent testIntent, int imageResId)326         public DialogTestListItemWithIcon(Context context, int nameResId, String testId,
327                 int testInstructionResId, Intent testIntent, int imageResId) {
328             super(context, nameResId, testId, testInstructionResId, testIntent);
329             mContext = context;
330             mImageResId = imageResId;
331         }
332 
333         @Override
getCustomView()334         public View getCustomView() {
335             LayoutInflater layoutInflater = LayoutInflater.from(mContext);
336             View view = layoutInflater.inflate(R.layout.dialog_custom_view,
337                     null /* root */);
338             ((ImageView) view.findViewById(R.id.sample_icon)).setImageResource(mImageResId);
339             ((TextView) view.findViewById(R.id.message)).setText(getManualTestInstruction());
340             return view;
341         }
342     }
343 }
344