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 android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
20 
21 import android.Manifest;
22 import android.app.AlertDialog;
23 import android.app.ListActivity;
24 import android.content.DialogInterface;
25 import android.content.Intent;
26 import android.content.SharedPreferences;
27 import android.content.pm.PackageInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.PackageManager.NameNotFoundException;
30 import android.net.Uri;
31 import android.os.Bundle;
32 import android.provider.Settings;
33 import android.util.Log;
34 import android.view.Menu;
35 import android.view.MenuInflater;
36 import android.view.MenuItem;
37 import android.view.View;
38 import android.view.Window;
39 import android.widget.CompoundButton;
40 import android.widget.SearchView;
41 import android.widget.Switch;
42 import android.widget.Toast;
43 
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.Objects;
47 
48 /** Top-level {@link ListActivity} for launching tests and managing results. */
49 public class TestListActivity extends AbstractTestListActivity implements View.OnClickListener {
50     private static final int CTS_VERIFIER_PERMISSION_REQUEST = 1;
51     private static final int CTS_VERIFIER_BACKGROUND_LOCATION_PERMISSION_REQUEST = 2;
52 
53     private static final String TAG = TestListActivity.class.getSimpleName();
54     // Records the current display mode.
55     // Default is unfolded mode, and it will be changed when clicking the switch button.
56     public static volatile String sCurrentDisplayMode = DisplayMode.UNFOLDED.toString();
57 
58     // Whether the verifier-system plan is enabled.
59     private static boolean sIsSystemEnabled = false;
60     // Whether the system switch has been toggled.
61     private static boolean sHasSystemToggled = false;
62 
63     private String[] mRequestedPermissions;
64 
65     // Enumerates the display modes, including unfolded and folded.
66     protected enum DisplayMode {
67         UNFOLDED,
68         FOLDED;
69 
70         @Override
toString()71         public String toString() {
72             return name().toLowerCase();
73         }
74 
75         /**
76          * Converts the mode as suffix with brackets for test name.
77          *
78          * @return a string containing mode with brackets for folded mode; empty string for unfolded
79          *     mode
80          */
asSuffix()81         public String asSuffix() {
82             if (name().equals(FOLDED.name())) {
83                 return String.format("[%s]", toString());
84             }
85             return "";
86         }
87     }
88 
89     @Override
onClick(View v)90     public void onClick(View v) {
91         handleMenuItemSelected(v.getId());
92     }
93 
94     @Override
onCreate(Bundle savedInstanceState)95     protected void onCreate(Bundle savedInstanceState) {
96         super.onCreate(savedInstanceState);
97 
98         try {
99             PackageManager pm = getPackageManager();
100             PackageInfo packageInfo =
101                     pm.getPackageInfo(
102                             getApplicationInfo().packageName, PackageManager.GET_PERMISSIONS);
103             mRequestedPermissions = packageInfo.requestedPermissions;
104 
105             if (mRequestedPermissions != null) {
106                 String[] permissionsToRequest =
107                         removeString(
108                                 mRequestedPermissions,
109                                 Manifest.permission.ACCESS_BACKGROUND_LOCATION);
110                 permissionsToRequest =
111                         Arrays.stream(permissionsToRequest)
112                                 .filter(
113                                         s -> {
114                                             try {
115                                                 return (pm.getPermissionInfo(s, 0).getProtection()
116                                                                 & PROTECTION_DANGEROUS)
117                                                         != 0;
118                                             } catch (NameNotFoundException e) {
119                                                 return false;
120                                             }
121                                         })
122                                 .toArray(String[]::new);
123                 requestPermissions(permissionsToRequest, CTS_VERIFIER_PERMISSION_REQUEST);
124             }
125             createContinue();
126         } catch (NameNotFoundException e) {
127             Log.e(TAG, "Unable to load package's permissions", e);
128             Toast.makeText(this, R.string.runtime_permissions_error, Toast.LENGTH_SHORT).show();
129         }
130     }
131 
132     @Override
onRequestPermissionsResult( int requestCode, String permissions[], int[] grantResults)133     public void onRequestPermissionsResult(
134             int requestCode, String permissions[], int[] grantResults) {
135         if (requestCode == CTS_VERIFIER_PERMISSION_REQUEST) {
136             if (arrayContains(grantResults, PackageManager.PERMISSION_DENIED)) {
137                 Log.v(TAG, "Didn't grant all permissions.");
138                 // If we're sending them to settings we don't need to request background location
139                 // since they can just grant in settings.
140                 sendUserToSettings();
141             } else if (new ArrayList<>(Arrays.asList(mRequestedPermissions))
142                     .contains(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) {
143                 requestPermissions(
144                         new String[] {Manifest.permission.ACCESS_BACKGROUND_LOCATION},
145                         CTS_VERIFIER_BACKGROUND_LOCATION_PERMISSION_REQUEST);
146             }
147             return;
148         }
149         if (requestCode == CTS_VERIFIER_BACKGROUND_LOCATION_PERMISSION_REQUEST) {
150             if (grantResults[0] == PackageManager.PERMISSION_DENIED) {
151                 Log.v(TAG, "Didn't grant background permission.");
152                 sendUserToSettings();
153             }
154             return;
155         }
156     }
157 
158     @Override
onCreateOptionsMenu(Menu menu)159     public boolean onCreateOptionsMenu(Menu menu) {
160         MenuInflater inflater = getMenuInflater();
161         inflater.inflate(R.menu.test_list_menu, menu);
162 
163         // Switch button for unfolded and folded tests.
164         MenuItem item = (MenuItem) menu.findItem(R.id.switch_item);
165         item.setActionView(R.layout.display_mode_switch);
166         Switch displayModeSwitch = item.getActionView().findViewById(R.id.switch_button);
167 
168         // Get the current display mode to show switch status.
169         boolean isFoldedMode = sCurrentDisplayMode.equals(DisplayMode.FOLDED.toString());
170         displayModeSwitch.setChecked(isFoldedMode);
171 
172         displayModeSwitch.setOnCheckedChangeListener(
173                 new CompoundButton.OnCheckedChangeListener() {
174                     @Override
175                     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
176                         if (isChecked) {
177                             sCurrentDisplayMode = DisplayMode.FOLDED.toString();
178                         } else {
179                             sCurrentDisplayMode = DisplayMode.UNFOLDED.toString();
180                         }
181                         handleSwitchItemSelected();
182                     }
183                 });
184 
185         // Switch button for verifier-system plan.
186         item = (MenuItem) menu.findItem(R.id.system_switch_item);
187         if (item != null) {
188             item.setActionView(R.layout.system_switch);
189             final Switch systemSwitch = item.getActionView().findViewById(
190                     R.id.system_switch_button);
191 
192             systemSwitch.setChecked(sIsSystemEnabled);
193             systemSwitch.setOnCheckedChangeListener(
194                     new CompoundButton.OnCheckedChangeListener() {
195 
196                         @Override
197                         public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
198                             if (!sHasSystemToggled && isChecked) {
199                                 sHasSystemToggled = true;
200                                 notifyUserSystemPlan(systemSwitch);
201                             } else {
202                                 updateIsSystemEnabled(isChecked);
203                             }
204                         }
205                     });
206         }
207 
208         SearchView searchView = (SearchView) menu.findItem(R.id.search_test).getActionView();
209         searchView.setOnQueryTextListener(
210                 new SearchView.OnQueryTextListener() {
211 
212                     public boolean onQueryTextSubmit(String query) {
213                         Log.i(TAG, "Got submitted query: " + query);
214                         handleQueryUpdated(query);
215                         return true;
216                     }
217 
218                     public boolean onQueryTextChange(String newText) {
219                         if (newText == null || newText.isEmpty()) {
220                             Log.i(TAG, "Clear filter");
221                             handleQueryUpdated(newText);
222                             return true;
223                         } else {
224                             return false;
225                         }
226                     }
227                 });
228 
229         return true;
230     }
231 
232     @Override
onOptionsItemSelected(MenuItem item)233     public boolean onOptionsItemSelected(MenuItem item) {
234         return handleMenuItemSelected(item.getItemId()) ? true : super.onOptionsItemSelected(item);
235     }
236 
237     /** Gets the verifier-system plan enabled status. */
getIsSystemEnabled()238     static boolean getIsSystemEnabled() {
239         return sIsSystemEnabled;
240     }
241 
242     /** Checks if a list of int array contains a given int value. */
arrayContains(int[] array, int value)243     private static boolean arrayContains(int[] array, int value) {
244         if (array == null) {
245             return false;
246         }
247         for (int element : array) {
248             if (element == value) {
249                 return true;
250             }
251         }
252         return false;
253     }
254 
255     /** Removes the first occurrence of a string from a given string array. */
removeString(String[] cur, String val)256     private static String[] removeString(String[] cur, String val) {
257         if (cur == null) {
258             return null;
259         }
260         final int n = cur.length;
261         for (int i = 0; i < n; i++) {
262             if (Objects.equals(cur[i], val)) {
263                 String[] ret = new String[n - 1];
264                 if (i > 0) {
265                     System.arraycopy(cur, 0, ret, 0, i);
266                 }
267                 if (i < (n - 1)) {
268                     System.arraycopy(cur, i + 1, ret, i, n - i - 1);
269                 }
270                 return ret;
271             }
272         }
273         return cur;
274     }
275 
createContinue()276     private void createContinue() {
277         if (!isTaskRoot()) {
278             finish();
279         }
280 
281         // Restores the last display mode when launching the app after killing the process.
282         if (getCurrentDisplayMode().equals(DisplayMode.FOLDED.toString())) {
283             sCurrentDisplayMode = DisplayMode.FOLDED.toString();
284         }
285 
286         setTitle(getString(R.string.title_version, Version.getVersionName(this)));
287 
288         if (!getWindow().hasFeature(Window.FEATURE_ACTION_BAR)) {
289             View footer = getLayoutInflater().inflate(R.layout.test_list_footer, null);
290 
291             footer.findViewById(R.id.clear).setOnClickListener(this);
292             footer.findViewById(R.id.export).setOnClickListener(this);
293 
294             getListView().addFooterView(footer);
295         }
296 
297         setTestListAdapter(
298                 new ManifestTestListAdapter(/* context= */ this, /* testParent= */ null));
299     }
300 
sendUserToSettings()301     private AlertDialog sendUserToSettings() {
302         return new AlertDialog.Builder(this)
303                 .setTitle("Please grant all permissions")
304                 .setPositiveButton(
305                         "Ok",
306                         (dialog, which) -> {
307                             if (which == AlertDialog.BUTTON_POSITIVE) {
308                                 startActivity(
309                                         new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
310                                                 .setData(
311                                                         Uri.fromParts(
312                                                                 "package",
313                                                                 getPackageName(),
314                                                                 null)));
315                             }
316                         })
317                 .show();
318     }
319 
320     /** Notifies the user about the verifier-system plan. */
321     private AlertDialog notifyUserSystemPlan(Switch systemSwitch) {
322         return new AlertDialog.Builder(this)
323                 .setTitle("Verifier System Plan")
324                 .setMessage(
325                         "This is a feature to execute verifier tests for the system layer"
326                             + " partitions. Click \"Yes\" to proceed or \"No\" to run all of the"
327                             + " verifier tests.")
328                 .setPositiveButton(
329                         "Yes",
330                         new DialogInterface.OnClickListener() {
331 
332                             public void onClick(DialogInterface dialog, int whichButton) {
333                                 updateIsSystemEnabled(true);
334                             }
335                         })
336                 .setNegativeButton(
337                         "No",
338                         new DialogInterface.OnClickListener() {
339 
340                             public void onClick(DialogInterface dialog, int whichButton) {
341                                 systemSwitch.setChecked(false);
342                             }
343                         })
344                 .show();
345     }
346 
347     /** Updates the verifier-system plan enabled status and refreshes the test list. */
348     private void updateIsSystemEnabled(boolean isChecked) {
349         Log.i(TAG, "verifier-system plan enabled: " + isChecked);
350         sIsSystemEnabled = isChecked;
351         handleSwitchItemSelected();
352     }
353 
354     /** Sets up the flags after switching display mode and reloads tests on UI. */
355     private void handleSwitchItemSelected() {
356         setCurrentDisplayMode(sCurrentDisplayMode);
357         mAdapter.loadTestResults();
358     }
359 
360     private void handleClearItemSelected() {
361         new AlertDialog.Builder(this)
362                 .setMessage(R.string.test_results_clear_title)
363                 .setPositiveButton(
364                         R.string.test_results_clear_yes,
365                         new DialogInterface.OnClickListener() {
366                             public void onClick(DialogInterface dialog, int id) {
367                                 mAdapter.clearTestResults();
368                                 Toast.makeText(
369                                                 TestListActivity.this,
370                                                 R.string.test_results_cleared,
371                                                 Toast.LENGTH_SHORT)
372                                         .show();
373                             }
374                         })
375                 .setNegativeButton(R.string.test_results_clear_cancel, null)
376                 .show();
377     }
378 
379     private void handleExportItemSelected() {
380         new ReportExporter(this, mAdapter).execute();
381     }
382 
383     private boolean handleMenuItemSelected(int id) {
384         if (id == R.id.clear) {
385             handleClearItemSelected();
386         } else if (id == R.id.export) {
387             handleExportItemSelected();
388         } else {
389             return false;
390         }
391 
392         return true;
393     }
394 
395     /** Triggered when a new query is input. */
396     private void handleQueryUpdated(String query) {
397         if (query != null && !query.isEmpty()) {
398             mAdapter.setTestFilter(query);
399         } else {
400             // Reset the filter as null to show all tests.
401             mAdapter.setTestFilter(/* testFilter= */ null);
402         }
403         mAdapter.loadTestResults();
404     }
405 
406     /**
407      * Sets current display mode to sharedpreferences.
408      *
409      * @param mode a string of current display mode
410      */
411     private void setCurrentDisplayMode(String mode) {
412         SharedPreferences pref = getSharedPreferences(DisplayMode.class.getName(), MODE_PRIVATE);
413         pref.edit().putString(DisplayMode.class.getName(), mode).commit();
414     }
415 
416     /**
417      * Gets current display mode from sharedpreferences.
418      *
419      * @return a string of current display mode
420      */
421     private String getCurrentDisplayMode() {
422         String mode =
423                 getSharedPreferences(DisplayMode.class.getName(), MODE_PRIVATE)
424                         .getString(DisplayMode.class.getName(), "");
425         return mode;
426     }
427 }
428