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.Switch; 41 import android.widget.Toast; 42 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.Objects; 46 47 /** Top-level {@link ListActivity} for launching tests and managing results. */ 48 public class TestListActivity extends AbstractTestListActivity implements View.OnClickListener { 49 private static final int CTS_VERIFIER_PERMISSION_REQUEST = 1; 50 private static final int CTS_VERIFIER_BACKGROUND_LOCATION_PERMISSION_REQUEST = 2; 51 52 private static final String TAG = TestListActivity.class.getSimpleName(); 53 // Records the current display mode. 54 // Default is unfolded mode, and it will be changed when clicking the switch button. 55 public static String sCurrentDisplayMode = DisplayMode.UNFOLDED.toString(); 56 // Flag of launch app to fetch the unfolded/folded tests in main view from AndroidManifest.xml. 57 protected static boolean sInitialLaunch; 58 59 private String[] mRequestedPermissions; 60 61 // Enumerates the display modes, including unfolded and folded. 62 protected enum DisplayMode { 63 UNFOLDED, FOLDED; 64 65 @Override toString()66 public String toString() { 67 return name().toLowerCase(); 68 } 69 70 /** 71 * Coverts the mode as suffix with brackets for test name. 72 * 73 * @return A string containing mode with brackets for folded mode; 74 * empty string for unfolded mode. 75 */ asSuffix()76 public String asSuffix() { 77 if (name().equals(FOLDED.name())) { 78 return String.format("[%s]", toString()); 79 } 80 return ""; 81 } 82 } 83 84 @Override onClick(View v)85 public void onClick(View v) { 86 handleMenuItemSelected(v.getId()); 87 } 88 89 @Override onCreate(Bundle savedInstanceState)90 protected void onCreate(Bundle savedInstanceState) { 91 super.onCreate(savedInstanceState); 92 93 try { 94 PackageManager pm = getPackageManager(); 95 PackageInfo packageInfo = pm.getPackageInfo( 96 getApplicationInfo().packageName, PackageManager.GET_PERMISSIONS); 97 mRequestedPermissions = packageInfo.requestedPermissions; 98 99 if (mRequestedPermissions != null) { 100 String[] permissionsToRequest = removeString(mRequestedPermissions, 101 Manifest.permission.ACCESS_BACKGROUND_LOCATION); 102 permissionsToRequest = Arrays.stream(permissionsToRequest).filter(s -> { 103 try { 104 return (pm.getPermissionInfo(s, 0).getProtection() & PROTECTION_DANGEROUS) 105 != 0; 106 } catch (NameNotFoundException e) { 107 return false; 108 } 109 }).toArray(String[]::new); 110 requestPermissions(permissionsToRequest, CTS_VERIFIER_PERMISSION_REQUEST); 111 } 112 createContinue(); 113 } catch (NameNotFoundException e) { 114 Log.e(TAG, "Unable to load package's permissions", e); 115 Toast.makeText(this, R.string.runtime_permissions_error, Toast.LENGTH_SHORT).show(); 116 } 117 } 118 createContinue()119 private void createContinue() { 120 if (!isTaskRoot()) { 121 finish(); 122 } 123 sInitialLaunch = true; 124 125 // Restores the last display mode when launching the app after killing the process. 126 if (getCurrentDisplayMode().equals(DisplayMode.FOLDED.toString())) { 127 sCurrentDisplayMode = DisplayMode.FOLDED.toString(); 128 } 129 130 setTitle(getString(R.string.title_version, Version.getVersionName(this))); 131 132 if (!getWindow().hasFeature(Window.FEATURE_ACTION_BAR)) { 133 View footer = getLayoutInflater().inflate(R.layout.test_list_footer, null); 134 135 footer.findViewById(R.id.clear).setOnClickListener(this); 136 footer.findViewById(R.id.export).setOnClickListener(this); 137 138 getListView().addFooterView(footer); 139 } 140 141 setTestListAdapter(new ManifestTestListAdapter(this, null)); 142 } 143 144 @Override onRequestPermissionsResult( int requestCode, String permissions[], int[] grantResults)145 public void onRequestPermissionsResult( 146 int requestCode, String permissions[], int[] grantResults) { 147 if (requestCode == CTS_VERIFIER_PERMISSION_REQUEST) { 148 if (arrayContains(grantResults, PackageManager.PERMISSION_DENIED)) { 149 Log.v(TAG, "Didn't grant all permissions."); 150 // If we're sending them to settings we don't need to request background location 151 // since they can just grant in settings. 152 sendUserToSettings(); 153 } else if (new ArrayList<>(Arrays.asList(mRequestedPermissions)).contains( 154 Manifest.permission.ACCESS_BACKGROUND_LOCATION)) { 155 requestPermissions(new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 156 CTS_VERIFIER_BACKGROUND_LOCATION_PERMISSION_REQUEST); 157 } 158 return; 159 } 160 if (requestCode == CTS_VERIFIER_BACKGROUND_LOCATION_PERMISSION_REQUEST) { 161 if (grantResults[0] == PackageManager.PERMISSION_DENIED) { 162 Log.v(TAG, "Didn't grant background permission."); 163 sendUserToSettings(); 164 } 165 return; 166 } 167 } 168 sendUserToSettings()169 private AlertDialog sendUserToSettings() { 170 return new AlertDialog.Builder(this) 171 .setTitle("Please grant all permissions") 172 .setPositiveButton( 173 "Ok", (dialog, which) -> { 174 if (which == AlertDialog.BUTTON_POSITIVE) { 175 startActivity(new Intent( 176 Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData( 177 Uri.fromParts("package", getPackageName(), null))); 178 } 179 }) 180 .show(); 181 } 182 183 @Override 184 public boolean onCreateOptionsMenu(Menu menu) { 185 MenuInflater inflater = getMenuInflater(); 186 inflater.inflate(R.menu.test_list_menu, menu); 187 188 // Switch button for unfolded and folded tests. 189 MenuItem item = (MenuItem) menu.findItem(R.id.switch_item); 190 item.setActionView(R.layout.display_mode_switch); 191 Switch displayModeSwitch = item.getActionView().findViewById(R.id.switch_button); 192 193 // Get the current display mode to show switch status. 194 boolean isFoldedMode = sCurrentDisplayMode.equals(DisplayMode.FOLDED.toString()); 195 displayModeSwitch.setChecked(isFoldedMode); 196 197 displayModeSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 198 @Override 199 public void onCheckedChanged(CompoundButton buttonView, 200 boolean isChecked) { 201 if (isChecked) { 202 sCurrentDisplayMode = DisplayMode.FOLDED.toString(); 203 } else { 204 sCurrentDisplayMode = DisplayMode.UNFOLDED.toString(); 205 } 206 handleSwitchItemSelected(); 207 } 208 }); 209 return true; 210 } 211 212 @Override 213 public boolean onOptionsItemSelected(MenuItem item) { 214 return handleMenuItemSelected(item.getItemId()) ? true : super.onOptionsItemSelected(item); 215 } 216 217 private void handleClearItemSelected() { 218 new AlertDialog.Builder(this) 219 .setMessage(R.string.test_results_clear_title) 220 .setPositiveButton(R.string.test_results_clear_yes, 221 new DialogInterface.OnClickListener() { 222 public void onClick(DialogInterface dialog, int id) { 223 mAdapter.clearTestResults(); 224 Toast.makeText( 225 TestListActivity.this, 226 R.string.test_results_cleared, 227 Toast.LENGTH_SHORT) 228 .show(); 229 } 230 }) 231 .setNegativeButton(R.string.test_results_clear_cancel, null) 232 .show(); 233 } 234 235 private void handleExportItemSelected() { 236 new ReportExporter(this, mAdapter).execute(); 237 } 238 239 // Sets up the flags after switching display mode and reloads tests on UI. 240 private void handleSwitchItemSelected() { 241 setCurrentDisplayMode(sCurrentDisplayMode); 242 mAdapter.loadTestResults(); 243 } 244 245 private boolean handleMenuItemSelected(int id) { 246 if (id == R.id.clear) { 247 handleClearItemSelected(); 248 } else if (id == R.id.export) { 249 handleExportItemSelected(); 250 } else { 251 return false; 252 } 253 254 return true; 255 } 256 257 /** 258 * Sets current display mode to sharedpreferences. 259 * 260 * @param mode A string of current display mode. 261 */ 262 private void setCurrentDisplayMode(String mode) { 263 SharedPreferences pref = getSharedPreferences(DisplayMode.class.getName(), MODE_PRIVATE); 264 pref.edit().putString(DisplayMode.class.getName(), mode).commit(); 265 } 266 267 /** 268 * Gets current display mode from sharedpreferences. 269 * 270 * @return A string of current display mode. 271 */ 272 private String getCurrentDisplayMode() { 273 String mode = getSharedPreferences(DisplayMode.class.getName(), MODE_PRIVATE) 274 .getString(DisplayMode.class.getName(), ""); 275 return mode; 276 } 277 278 private static boolean arrayContains(int[] array, int value) { 279 if (array == null) return false; 280 for (int element : array) { 281 if (element == value) { 282 return true; 283 } 284 } 285 return false; 286 } 287 288 private static String[] removeString(String[] cur, String val) { 289 if (cur == null) { 290 return null; 291 } 292 final int n = cur.length; 293 for (int i = 0; i < n; i++) { 294 if (Objects.equals(cur[i], val)) { 295 String[] ret = new String[n - 1]; 296 if (i > 0) { 297 System.arraycopy(cur, 0, ret, 0, i); 298 } 299 if (i < (n - 1)) { 300 System.arraycopy(cur, i + 1, ret, i, n - i - 1); 301 } 302 return ret; 303 } 304 } 305 return cur; 306 } 307 } 308