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                 .setNeutralButton(R.string.go_button_text, null)
170                 .setPositiveButton(R.string.pass_button_text, new AlertDialog.OnClickListener() {
171                     @Override
172                     public void onClick(DialogInterface dialog, int which) {
173                         callback.onPass();
174                     }
175                 })
176                 .setNegativeButton(R.string.fail_button_text, new AlertDialog.OnClickListener() {
177                     @Override
178                     public void onClick(DialogInterface dialog, int which) {
179                         callback.onFail();
180                     }
181                 });
182         View customView = test.getCustomView();
183         if (customView != null) {
184             dialogBuilder.setView(customView);
185         } else {
186             dialogBuilder.setMessage(test.getManualTestInstruction());
187         }
188         final AlertDialog dialog = dialogBuilder.show();
189         // Note: Setting the OnClickListener on the Dialog rather than the Builder, prevents the
190         // dialog being dismissed on onClick.
191         dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(new OnClickListener() {
192             @Override
193             public void onClick(View v) {
194                 if (!startTestIntent(test)) {
195                     dialog.dismiss();
196                 }
197             }
198         });
199     }
200 
201     @Override
handleItemClick(ListView l, View v, int position, long id)202     protected void handleItemClick(ListView l, View v, int position, long id) {
203         TestListAdapter.TestListItem test = (TestListAdapter.TestListItem) getListAdapter()
204                 .getItem(position);
205         if (test instanceof DialogTestListItem) {
206             mCurrentTestPosition = position;
207             ((DialogTestListItem) test).performTest(this);
208         } else {
209             try {
210                 super.handleItemClick(l, v, position, id);
211             } catch (ActivityNotFoundException e) {
212                 Log.d(TAG, "handleItemClick() threw exception: ", e);
213                 setTestResult(test, TestResult.TEST_RESULT_FAILED);
214                 showToast(R.string.test_failed_cannot_start_intent);
215             }
216         }
217     }
218 
219 
220     /**
221      * Start a test's manual intent
222      *
223      * @param test The test the manual intent of which is to be started.
224      * @return true if activity could be started successfully, false otherwise.
225      */
startTestIntent(final DialogTestListItem test)226     boolean startTestIntent(final DialogTestListItem test) {
227         final Intent intent = test.intent;
228         try {
229             startActivity(intent);
230         } catch (ActivityNotFoundException e) {
231             Log.w(TAG, "Cannot start activity.", e);
232             Toast.makeText(this, "Cannot start " + intent, Toast.LENGTH_LONG).show();
233             setTestResult(test, TestResult.TEST_RESULT_FAILED);
234             return false;
235         }
236         return true;
237     }
238 
clearRemainingState(final DialogTestListItem test)239     protected void clearRemainingState(final DialogTestListItem test) {
240         // do nothing, override in subclass if needed
241     }
242 
setTestResult(TestListAdapter.TestListItem test, int result)243     protected void setTestResult(TestListAdapter.TestListItem test, int result) {
244         // Bundle result in an intent to feed into handleLaunchTestResult
245         Intent resultIntent = new Intent();
246         TestResult.addResultData(resultIntent, result, test.testName, /* testDetails */ null,
247                 /* reportLog */ null, null);
248         handleLaunchTestResult(RESULT_OK, resultIntent);
249         getListView().smoothScrollToPosition(mCurrentTestPosition + 1);
250     }
251 
setTestResult(String testName, int result)252     protected void setTestResult(String testName, int result) {
253         // Bundle result in an intent to feed into handleLaunchTestResult
254         Intent resultIntent = new Intent();
255         TestResult.addResultData(resultIntent, result, testName, /* testDetails */ null,
256                 /* reportLog */ null, null);
257         handleLaunchTestResult(RESULT_OK, resultIntent);
258         getListView().smoothScrollToPosition(mCurrentTestPosition + 1);
259     }
260 
showToast(int messageId)261     protected void showToast(int messageId) {
262         String message = getString(messageId);
263         Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
264     }
265 
266     protected static class DialogTestListItem extends TestListAdapter.TestListItem {
267 
268         public interface TestCallback {
onPass()269             void onPass();
270 
onFail()271             void onFail();
272         }
273 
274         private String mManualInstruction;
275 
DialogTestListItem(Context context, String nameId, String testId)276         public DialogTestListItem(Context context, String nameId, String testId) {
277             super(nameId, testId, null, null, null, null);
278         }
279 
DialogTestListItem(Context context, int nameResId, String testId)280         public DialogTestListItem(Context context, int nameResId, String testId) {
281             super(context.getString(nameResId), testId, null, null, null, null);
282         }
283 
DialogTestListItem(Context context, int nameResId, String testId, int testInstructionResId, Intent testIntent)284         public DialogTestListItem(Context context, int nameResId, String testId,
285                 int testInstructionResId, Intent testIntent) {
286             super(context.getString(nameResId), testId, testIntent, null, null, null);
287             mManualInstruction = context.getString(testInstructionResId);
288         }
289 
performTest(DialogTestListActivity activity)290         public void performTest(DialogTestListActivity activity) {
291             activity.showManualTestDialog(this);
292         }
293 
getManualTestInstruction()294         public String getManualTestInstruction() {
295             return mManualInstruction;
296         }
297 
getManualTestIntent()298         public Intent getManualTestIntent() {
299             return intent;
300         }
301 
getCustomView()302         public View getCustomView() {
303             return null;
304         }
305 
306         @Override
isTest()307         boolean isTest() {
308             return true;
309         }
310     }
311 
312     protected static class DialogTestListItemWithIcon extends DialogTestListItem {
313 
314         private final int mImageResId;
315         private final Context mContext;
316 
DialogTestListItemWithIcon(Context context, int nameResId, String testId, int testInstructionResId, Intent testIntent, int imageResId)317         public DialogTestListItemWithIcon(Context context, int nameResId, String testId,
318                 int testInstructionResId, Intent testIntent, int imageResId) {
319             super(context, nameResId, testId, testInstructionResId, testIntent);
320             mContext = context;
321             mImageResId = imageResId;
322         }
323 
324         @Override
getCustomView()325         public View getCustomView() {
326             LayoutInflater layoutInflater = LayoutInflater.from(mContext);
327             View view = layoutInflater.inflate(R.layout.dialog_custom_view,
328                     null /* root */);
329             ((ImageView) view.findViewById(R.id.sample_icon)).setImageResource(mImageResId);
330             ((TextView) view.findViewById(R.id.message)).setText(getManualTestInstruction());
331             return view;
332         }
333     }
334 }
335