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