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 (grantResults.length == 0) { 150 return; 151 } 152 if (requestCode == CTS_VERIFIER_BACKGROUND_LOCATION_PERMISSION_REQUEST) { 153 if (grantResults[0] == PackageManager.PERMISSION_DENIED) { 154 Log.v(TAG, "Didn't grant background permission."); 155 sendUserToSettings(); 156 } 157 return; 158 } 159 } 160 161 @Override onCreateOptionsMenu(Menu menu)162 public boolean onCreateOptionsMenu(Menu menu) { 163 MenuInflater inflater = getMenuInflater(); 164 inflater.inflate(R.menu.test_list_menu, menu); 165 166 // Switch button for unfolded and folded tests. 167 MenuItem item = (MenuItem) menu.findItem(R.id.switch_item); 168 item.setActionView(R.layout.display_mode_switch); 169 Switch displayModeSwitch = item.getActionView().findViewById(R.id.switch_button); 170 171 // Get the current display mode to show switch status. 172 boolean isFoldedMode = sCurrentDisplayMode.equals(DisplayMode.FOLDED.toString()); 173 displayModeSwitch.setChecked(isFoldedMode); 174 175 displayModeSwitch.setOnCheckedChangeListener( 176 new CompoundButton.OnCheckedChangeListener() { 177 @Override 178 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 179 if (isChecked) { 180 sCurrentDisplayMode = DisplayMode.FOLDED.toString(); 181 } else { 182 sCurrentDisplayMode = DisplayMode.UNFOLDED.toString(); 183 } 184 handleSwitchItemSelected(); 185 } 186 }); 187 188 // Switch button for verifier-system plan. 189 item = (MenuItem) menu.findItem(R.id.system_switch_item); 190 if (item != null) { 191 item.setActionView(R.layout.system_switch); 192 final Switch systemSwitch = item.getActionView().findViewById( 193 R.id.system_switch_button); 194 195 systemSwitch.setChecked(sIsSystemEnabled); 196 systemSwitch.setOnCheckedChangeListener( 197 new CompoundButton.OnCheckedChangeListener() { 198 199 @Override 200 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 201 if (!sHasSystemToggled && isChecked) { 202 sHasSystemToggled = true; 203 notifyUserSystemPlan(systemSwitch); 204 } else { 205 updateIsSystemEnabled(isChecked); 206 } 207 } 208 }); 209 } 210 211 SearchView searchView = (SearchView) menu.findItem(R.id.search_test).getActionView(); 212 searchView.setOnQueryTextListener( 213 new SearchView.OnQueryTextListener() { 214 215 public boolean onQueryTextSubmit(String query) { 216 Log.i(TAG, "Got submitted query: " + query); 217 handleQueryUpdated(query); 218 return true; 219 } 220 221 public boolean onQueryTextChange(String newText) { 222 if (newText == null || newText.isEmpty()) { 223 Log.i(TAG, "Clear filter"); 224 handleQueryUpdated(newText); 225 return true; 226 } else { 227 return false; 228 } 229 } 230 }); 231 232 return true; 233 } 234 235 @Override onOptionsItemSelected(MenuItem item)236 public boolean onOptionsItemSelected(MenuItem item) { 237 return handleMenuItemSelected(item.getItemId()) ? true : super.onOptionsItemSelected(item); 238 } 239 240 /** Gets the verifier-system plan enabled status. */ getIsSystemEnabled()241 static boolean getIsSystemEnabled() { 242 return sIsSystemEnabled; 243 } 244 245 /** Checks if a list of int array contains a given int value. */ arrayContains(int[] array, int value)246 private static boolean arrayContains(int[] array, int value) { 247 if (array == null) { 248 return false; 249 } 250 for (int element : array) { 251 if (element == value) { 252 return true; 253 } 254 } 255 return false; 256 } 257 258 /** Removes the first occurrence of a string from a given string array. */ removeString(String[] cur, String val)259 private static String[] removeString(String[] cur, String val) { 260 if (cur == null) { 261 return null; 262 } 263 final int n = cur.length; 264 for (int i = 0; i < n; i++) { 265 if (Objects.equals(cur[i], val)) { 266 String[] ret = new String[n - 1]; 267 if (i > 0) { 268 System.arraycopy(cur, 0, ret, 0, i); 269 } 270 if (i < (n - 1)) { 271 System.arraycopy(cur, i + 1, ret, i, n - i - 1); 272 } 273 return ret; 274 } 275 } 276 return cur; 277 } 278 createContinue()279 private void createContinue() { 280 if (!isTaskRoot()) { 281 finish(); 282 } 283 284 // Restores the last display mode when launching the app after killing the process. 285 if (getCurrentDisplayMode().equals(DisplayMode.FOLDED.toString())) { 286 sCurrentDisplayMode = DisplayMode.FOLDED.toString(); 287 } 288 289 setTitle(getString(R.string.title_version, Version.getVersionName(this))); 290 291 if (!getWindow().hasFeature(Window.FEATURE_ACTION_BAR)) { 292 View footer = getLayoutInflater().inflate(R.layout.test_list_footer, null); 293 294 footer.findViewById(R.id.clear).setOnClickListener(this); 295 footer.findViewById(R.id.export).setOnClickListener(this); 296 297 getListView().addFooterView(footer); 298 } 299 300 setTestListAdapter( 301 new ManifestTestListAdapter(/* context= */ this, /* testParent= */ null)); 302 } 303 sendUserToSettings()304 private AlertDialog sendUserToSettings() { 305 return new AlertDialog.Builder(this) 306 .setTitle("Please grant all permissions") 307 .setPositiveButton( 308 "Ok", 309 (dialog, which) -> { 310 if (which == AlertDialog.BUTTON_POSITIVE) { 311 startActivity( 312 new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 313 .setData( 314 Uri.fromParts( 315 "package", 316 getPackageName(), 317 null))); 318 } 319 }) 320 .show(); 321 } 322 323 /** Notifies the user about the verifier-system plan. */ 324 private AlertDialog notifyUserSystemPlan(Switch systemSwitch) { 325 return new AlertDialog.Builder(this) 326 .setTitle("Verifier System Plan") 327 .setMessage( 328 "This is a feature to execute verifier tests for the system layer" 329 + " partitions. Click \"Yes\" to proceed or \"No\" to run all of the" 330 + " verifier tests.") 331 .setPositiveButton( 332 "Yes", 333 new DialogInterface.OnClickListener() { 334 335 public void onClick(DialogInterface dialog, int whichButton) { 336 updateIsSystemEnabled(true); 337 } 338 }) 339 .setNegativeButton( 340 "No", 341 new DialogInterface.OnClickListener() { 342 343 public void onClick(DialogInterface dialog, int whichButton) { 344 systemSwitch.setChecked(false); 345 } 346 }) 347 .show(); 348 } 349 350 /** Updates the verifier-system plan enabled status and refreshes the test list. */ 351 private void updateIsSystemEnabled(boolean isChecked) { 352 Log.i(TAG, "verifier-system plan enabled: " + isChecked); 353 sIsSystemEnabled = isChecked; 354 handleSwitchItemSelected(); 355 } 356 357 /** Sets up the flags after switching display mode and reloads tests on UI. */ 358 private void handleSwitchItemSelected() { 359 setCurrentDisplayMode(sCurrentDisplayMode); 360 mAdapter.loadTestResults(); 361 } 362 363 private void handleClearItemSelected() { 364 new AlertDialog.Builder(this) 365 .setMessage(R.string.test_results_clear_title) 366 .setPositiveButton( 367 R.string.test_results_clear_yes, 368 new DialogInterface.OnClickListener() { 369 public void onClick(DialogInterface dialog, int id) { 370 mAdapter.clearTestResults(); 371 Toast.makeText( 372 TestListActivity.this, 373 R.string.test_results_cleared, 374 Toast.LENGTH_SHORT) 375 .show(); 376 } 377 }) 378 .setNegativeButton(R.string.test_results_clear_cancel, null) 379 .show(); 380 } 381 382 private void handleExportItemSelected() { 383 new ReportExporter(this, mAdapter).execute(); 384 } 385 386 private boolean handleMenuItemSelected(int id) { 387 if (id == R.id.clear) { 388 handleClearItemSelected(); 389 } else if (id == R.id.export) { 390 handleExportItemSelected(); 391 } else { 392 return false; 393 } 394 395 return true; 396 } 397 398 /** Triggered when a new query is input. */ 399 private void handleQueryUpdated(String query) { 400 if (query != null && !query.isEmpty()) { 401 mAdapter.setTestFilter(query); 402 } else { 403 // Reset the filter as null to show all tests. 404 mAdapter.setTestFilter(/* testFilter= */ null); 405 } 406 mAdapter.loadTestResults(); 407 } 408 409 /** 410 * Sets current display mode to sharedpreferences. 411 * 412 * @param mode a string of current display mode 413 */ 414 private void setCurrentDisplayMode(String mode) { 415 SharedPreferences pref = getSharedPreferences(DisplayMode.class.getName(), MODE_PRIVATE); 416 pref.edit().putString(DisplayMode.class.getName(), mode).commit(); 417 } 418 419 /** 420 * Gets current display mode from sharedpreferences. 421 * 422 * @return a string of current display mode 423 */ 424 private String getCurrentDisplayMode() { 425 String mode = 426 getSharedPreferences(DisplayMode.class.getName(), MODE_PRIVATE) 427 .getString(DisplayMode.class.getName(), ""); 428 return mode; 429 } 430 } 431