1 /* 2 * Copyright (C) 2011 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.settings; 18 19 import android.app.Activity; 20 import android.app.StatusBarManager; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.os.AsyncTask; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.IBinder; 29 import android.os.Message; 30 import android.os.PowerManager; 31 import android.os.RemoteException; 32 import android.os.ServiceManager; 33 import android.os.SystemProperties; 34 import android.os.storage.IMountService; 35 import android.telephony.TelephonyManager; 36 import android.text.TextUtils; 37 import android.util.Log; 38 import android.view.KeyEvent; 39 import android.view.View; 40 import android.view.View.OnClickListener; 41 import android.view.inputmethod.EditorInfo; 42 import android.view.inputmethod.InputMethodInfo; 43 import android.view.inputmethod.InputMethodManager; 44 import android.view.inputmethod.InputMethodSubtype; 45 import android.widget.Button; 46 import android.widget.EditText; 47 import android.widget.ProgressBar; 48 import android.widget.TextView; 49 50 import com.android.internal.telephony.ITelephony; 51 52 import java.util.List; 53 54 /** 55 * Settings screens to show the UI flows for encrypting/decrypting the device. 56 * 57 * This may be started via adb for debugging the UI layout, without having to go through 58 * encryption flows everytime. It should be noted that starting the activity in this manner 59 * is only useful for verifying UI-correctness - the behavior will not be identical. 60 * <pre> 61 * $ adb shell pm enable com.android.settings/.CryptKeeper 62 * $ adb shell am start \ 63 * -e "com.android.settings.CryptKeeper.DEBUG_FORCE_VIEW" "progress" \ 64 * -n com.android.settings/.CryptKeeper 65 * </pre> 66 */ 67 public class CryptKeeper extends Activity implements TextView.OnEditorActionListener { 68 private static final String TAG = "CryptKeeper"; 69 70 private static final String DECRYPT_STATE = "trigger_restart_framework"; 71 72 private static final int UPDATE_PROGRESS = 1; 73 private static final int COOLDOWN = 2; 74 75 private static final int MAX_FAILED_ATTEMPTS = 30; 76 private static final int COOL_DOWN_ATTEMPTS = 10; 77 private static final int COOL_DOWN_INTERVAL = 30; // 30 seconds 78 79 // Intent action for launching the Emergency Dialer activity. 80 static final String ACTION_EMERGENCY_DIAL = "com.android.phone.EmergencyDialer.DIAL"; 81 82 // Debug Intent extras so that this Activity may be started via adb for debugging UI layouts 83 private static final String EXTRA_FORCE_VIEW = 84 "com.android.settings.CryptKeeper.DEBUG_FORCE_VIEW"; 85 private static final String FORCE_VIEW_PROGRESS = "progress"; 86 private static final String FORCE_VIEW_ENTRY = "entry"; 87 private static final String FORCE_VIEW_ERROR = "error"; 88 89 /** When encryption is detected, this flag indivates whether or not we've checked for erros. */ 90 private boolean mValidationComplete; 91 private boolean mValidationRequested; 92 /** A flag to indicate that the volume is in a bad state (e.g. partially encrypted). */ 93 private boolean mEncryptionGoneBad; 94 95 private int mCooldown; 96 PowerManager.WakeLock mWakeLock; 97 private EditText mPasswordEntry; 98 99 /** 100 * Used to propagate state through configuration changes (e.g. screen rotation) 101 */ 102 private static class NonConfigurationInstanceState { 103 final PowerManager.WakeLock wakelock; 104 105 NonConfigurationInstanceState(PowerManager.WakeLock _wakelock) { 106 wakelock = _wakelock; 107 } 108 } 109 110 // This activity is used to fade the screen to black after the password is entered. 111 public static class Blank extends Activity { 112 @Override 113 public void onCreate(Bundle savedInstanceState) { 114 super.onCreate(savedInstanceState); 115 setContentView(R.layout.crypt_keeper_blank); 116 } 117 } 118 119 private class DecryptTask extends AsyncTask<String, Void, Integer> { 120 @Override 121 protected Integer doInBackground(String... params) { 122 IMountService service = getMountService(); 123 try { 124 return service.decryptStorage(params[0]); 125 } catch (Exception e) { 126 Log.e(TAG, "Error while decrypting...", e); 127 return -1; 128 } 129 } 130 131 @Override 132 protected void onPostExecute(Integer failedAttempts) { 133 if (failedAttempts == 0) { 134 // The password was entered successfully. Start the Blank activity 135 // so this activity animates to black before the devices starts. Note 136 // It has 1 second to complete the animation or it will be frozen 137 // until the boot animation comes back up. 138 Intent intent = new Intent(CryptKeeper.this, Blank.class); 139 finish(); 140 startActivity(intent); 141 } else if (failedAttempts == MAX_FAILED_ATTEMPTS) { 142 // Factory reset the device. 143 sendBroadcast(new Intent("android.intent.action.MASTER_CLEAR")); 144 } else if ((failedAttempts % COOL_DOWN_ATTEMPTS) == 0) { 145 mCooldown = COOL_DOWN_INTERVAL; 146 cooldown(); 147 } else { 148 TextView tv = (TextView) findViewById(R.id.status); 149 tv.setText(R.string.try_again); 150 tv.setVisibility(View.VISIBLE); 151 152 // Reenable the password entry 153 mPasswordEntry.setEnabled(true); 154 } 155 } 156 } 157 158 private class ValidationTask extends AsyncTask<Void, Void, Boolean> { 159 @Override 160 protected Boolean doInBackground(Void... params) { 161 IMountService service = getMountService(); 162 try { 163 Log.d(TAG, "Validating encryption state."); 164 int state = service.getEncryptionState(); 165 if (state == IMountService.ENCRYPTION_STATE_NONE) { 166 Log.w(TAG, "Unexpectedly in CryptKeeper even though there is no encryption."); 167 return true; // Unexpected, but fine, I guess... 168 } 169 return state == IMountService.ENCRYPTION_STATE_OK; 170 } catch (RemoteException e) { 171 Log.w(TAG, "Unable to get encryption state properly"); 172 return true; 173 } 174 } 175 176 @Override 177 protected void onPostExecute(Boolean result) { 178 mValidationComplete = true; 179 if (Boolean.FALSE.equals(result)) { 180 Log.w(TAG, "Incomplete, or corrupted encryption detected. Prompting user to wipe."); 181 mEncryptionGoneBad = true; 182 } else { 183 Log.d(TAG, "Encryption state validated. Proceeding to configure UI"); 184 } 185 setupUi(); 186 } 187 } 188 189 private final Handler mHandler = new Handler() { 190 @Override 191 public void handleMessage(Message msg) { 192 switch (msg.what) { 193 case UPDATE_PROGRESS: 194 updateProgress(); 195 break; 196 197 case COOLDOWN: 198 cooldown(); 199 break; 200 } 201 } 202 }; 203 204 /** @return whether or not this Activity was started for debugging the UI only. */ 205 private boolean isDebugView() { 206 return getIntent().hasExtra(EXTRA_FORCE_VIEW); 207 } 208 209 /** @return whether or not this Activity was started for debugging the specific UI view only. */ 210 private boolean isDebugView(String viewType /* non-nullable */) { 211 return viewType.equals(getIntent().getStringExtra(EXTRA_FORCE_VIEW)); 212 } 213 214 @Override 215 public void onCreate(Bundle savedInstanceState) { 216 super.onCreate(savedInstanceState); 217 218 // If we are not encrypted or encrypting, get out quickly. 219 String state = SystemProperties.get("vold.decrypt"); 220 if (!isDebugView() && ("".equals(state) || DECRYPT_STATE.equals(state))) { 221 // Disable the crypt keeper. 222 PackageManager pm = getPackageManager(); 223 ComponentName name = new ComponentName(this, CryptKeeper.class); 224 pm.setComponentEnabledSetting(name, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 225 PackageManager.DONT_KILL_APP); 226 // Typically CryptKeeper is launched as the home app. We didn't 227 // want to be running, so need to finish this activity. We can count 228 // on the activity manager re-launching the new home app upon finishing 229 // this one, since this will leave the activity stack empty. 230 // NOTE: This is really grungy. I think it would be better for the 231 // activity manager to explicitly launch the crypt keeper instead of 232 // home in the situation where we need to decrypt the device 233 finish(); 234 return; 235 } 236 237 // Disable the status bar 238 StatusBarManager sbm = (StatusBarManager) getSystemService(Context.STATUS_BAR_SERVICE); 239 sbm.disable(StatusBarManager.DISABLE_EXPAND 240 | StatusBarManager.DISABLE_NOTIFICATION_ICONS 241 | StatusBarManager.DISABLE_NOTIFICATION_ALERTS 242 | StatusBarManager.DISABLE_SYSTEM_INFO 243 | StatusBarManager.DISABLE_HOME 244 | StatusBarManager.DISABLE_RECENT 245 | StatusBarManager.DISABLE_BACK); 246 247 // Check for (and recover) retained instance data 248 Object lastInstance = getLastNonConfigurationInstance(); 249 if (lastInstance instanceof NonConfigurationInstanceState) { 250 NonConfigurationInstanceState retained = (NonConfigurationInstanceState) lastInstance; 251 mWakeLock = retained.wakelock; 252 Log.d(TAG, "Restoring wakelock from NonConfigurationInstanceState"); 253 } 254 } 255 256 /** 257 * Note, we defer the state check and screen setup to onStart() because this will be 258 * re-run if the user clicks the power button (sleeping/waking the screen), and this is 259 * especially important if we were to lose the wakelock for any reason. 260 */ 261 @Override 262 public void onStart() { 263 super.onStart(); 264 265 setupUi(); 266 } 267 268 /** 269 * Initializes the UI based on the current state of encryption. 270 * This is idempotent - calling repeatedly will simply re-initialize the UI. 271 */ 272 private void setupUi() { 273 if (mEncryptionGoneBad || isDebugView(FORCE_VIEW_ERROR)) { 274 setContentView(R.layout.crypt_keeper_progress); 275 showFactoryReset(); 276 return; 277 } 278 279 String progress = SystemProperties.get("vold.encrypt_progress"); 280 if (!"".equals(progress) || isDebugView(FORCE_VIEW_PROGRESS)) { 281 setContentView(R.layout.crypt_keeper_progress); 282 encryptionProgressInit(); 283 } else if (mValidationComplete) { 284 setContentView(R.layout.crypt_keeper_password_entry); 285 passwordEntryInit(); 286 } else if (!mValidationRequested) { 287 // We're supposed to be encrypted, but no validation has been done. 288 new ValidationTask().execute((Void[]) null); 289 mValidationRequested = true; 290 } 291 } 292 293 @Override 294 public void onStop() { 295 super.onStop(); 296 297 mHandler.removeMessages(COOLDOWN); 298 mHandler.removeMessages(UPDATE_PROGRESS); 299 } 300 301 /** 302 * Reconfiguring, so propagate the wakelock to the next instance. This runs between onStop() 303 * and onDestroy() and only if we are changing configuration (e.g. rotation). Also clears 304 * mWakeLock so the subsequent call to onDestroy does not release it. 305 */ 306 @Override 307 public Object onRetainNonConfigurationInstance() { 308 NonConfigurationInstanceState state = new NonConfigurationInstanceState(mWakeLock); 309 Log.d(TAG, "Handing wakelock off to NonConfigurationInstanceState"); 310 mWakeLock = null; 311 return state; 312 } 313 314 @Override 315 public void onDestroy() { 316 super.onDestroy(); 317 318 if (mWakeLock != null) { 319 Log.d(TAG, "Releasing and destroying wakelock"); 320 mWakeLock.release(); 321 mWakeLock = null; 322 } 323 } 324 325 private void encryptionProgressInit() { 326 // Accquire a partial wakelock to prevent the device from sleeping. Note 327 // we never release this wakelock as we will be restarted after the device 328 // is encrypted. 329 330 Log.d(TAG, "Encryption progress screen initializing."); 331 if (mWakeLock == null) { 332 Log.d(TAG, "Acquiring wakelock."); 333 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 334 mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG); 335 mWakeLock.acquire(); 336 } 337 338 ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress_bar); 339 progressBar.setIndeterminate(true); 340 341 updateProgress(); 342 } 343 344 private void showFactoryReset() { 345 // Hide the encryption-bot to make room for the "factory reset" button 346 findViewById(R.id.encroid).setVisibility(View.GONE); 347 348 // Show the reset button, failure text, and a divider 349 Button button = (Button) findViewById(R.id.factory_reset); 350 button.setVisibility(View.VISIBLE); 351 button.setOnClickListener(new OnClickListener() { 352 public void onClick(View v) { 353 // Factory reset the device. 354 sendBroadcast(new Intent("android.intent.action.MASTER_CLEAR")); 355 } 356 }); 357 358 TextView tv = (TextView) findViewById(R.id.title); 359 tv.setText(R.string.crypt_keeper_failed_title); 360 361 tv = (TextView) findViewById(R.id.status); 362 tv.setText(R.string.crypt_keeper_failed_summary); 363 364 View view = findViewById(R.id.bottom_divider); 365 if (view != null) { 366 view.setVisibility(View.VISIBLE); 367 } 368 } 369 370 private void updateProgress() { 371 String state = SystemProperties.get("vold.encrypt_progress"); 372 373 if ("error_partially_encrypted".equals(state)) { 374 showFactoryReset(); 375 return; 376 } 377 378 int progress = 0; 379 try { 380 // Force a 50% progress state when debugging the view. 381 progress = isDebugView() ? 50 : Integer.parseInt(state); 382 } catch (Exception e) { 383 Log.w(TAG, "Error parsing progress: " + e.toString()); 384 } 385 386 CharSequence status = getText(R.string.crypt_keeper_setup_description); 387 Log.v(TAG, "Encryption progress: " + progress); 388 TextView tv = (TextView) findViewById(R.id.status); 389 tv.setText(TextUtils.expandTemplate(status, Integer.toString(progress))); 390 391 // Check the progress every 5 seconds 392 mHandler.removeMessages(UPDATE_PROGRESS); 393 mHandler.sendEmptyMessageDelayed(UPDATE_PROGRESS, 5000); 394 } 395 396 private void cooldown() { 397 TextView tv = (TextView) findViewById(R.id.status); 398 399 if (mCooldown <= 0) { 400 // Re-enable the password entry 401 mPasswordEntry.setEnabled(true); 402 403 tv.setVisibility(View.GONE); 404 } else { 405 CharSequence template = getText(R.string.crypt_keeper_cooldown); 406 tv.setText(TextUtils.expandTemplate(template, Integer.toString(mCooldown))); 407 408 tv.setVisibility(View.VISIBLE); 409 410 mCooldown--; 411 mHandler.removeMessages(COOLDOWN); 412 mHandler.sendEmptyMessageDelayed(COOLDOWN, 1000); // Tick every second 413 } 414 } 415 416 private void passwordEntryInit() { 417 mPasswordEntry = (EditText) findViewById(R.id.passwordEntry); 418 mPasswordEntry.setOnEditorActionListener(this); 419 mPasswordEntry.requestFocus(); 420 421 View imeSwitcher = findViewById(R.id.switch_ime_button); 422 final InputMethodManager imm = (InputMethodManager) getSystemService( 423 Context.INPUT_METHOD_SERVICE); 424 if (imeSwitcher != null && hasMultipleEnabledIMEsOrSubtypes(imm, false)) { 425 imeSwitcher.setVisibility(View.VISIBLE); 426 imeSwitcher.setOnClickListener(new OnClickListener() { 427 public void onClick(View v) { 428 imm.showInputMethodPicker(); 429 } 430 }); 431 } 432 433 // Asynchronously throw up the IME, since there are issues with requesting it to be shown 434 // immediately. 435 mHandler.postDelayed(new Runnable() { 436 @Override public void run() { 437 imm.showSoftInputUnchecked(0, null); 438 } 439 }, 0); 440 441 updateEmergencyCallButtonState(); 442 } 443 444 /** 445 * Method adapted from com.android.inputmethod.latin.Utils 446 * 447 * @param imm The input method manager 448 * @param shouldIncludeAuxiliarySubtypes 449 * @return true if we have multiple IMEs to choose from 450 */ 451 private boolean hasMultipleEnabledIMEsOrSubtypes(InputMethodManager imm, 452 final boolean shouldIncludeAuxiliarySubtypes) { 453 final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList(); 454 455 // Number of the filtered IMEs 456 int filteredImisCount = 0; 457 458 for (InputMethodInfo imi : enabledImis) { 459 // We can return true immediately after we find two or more filtered IMEs. 460 if (filteredImisCount > 1) return true; 461 final List<InputMethodSubtype> subtypes = 462 imm.getEnabledInputMethodSubtypeList(imi, true); 463 // IMEs that have no subtypes should be counted. 464 if (subtypes.isEmpty()) { 465 ++filteredImisCount; 466 continue; 467 } 468 469 int auxCount = 0; 470 for (InputMethodSubtype subtype : subtypes) { 471 if (subtype.isAuxiliary()) { 472 ++auxCount; 473 } 474 } 475 final int nonAuxCount = subtypes.size() - auxCount; 476 477 // IMEs that have one or more non-auxiliary subtypes should be counted. 478 // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary 479 // subtypes should be counted as well. 480 if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) { 481 ++filteredImisCount; 482 continue; 483 } 484 } 485 486 return filteredImisCount > 1 487 // imm.getEnabledInputMethodSubtypeList(null, false) will return the current IME's enabled 488 // input method subtype (The current IME should be LatinIME.) 489 || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1; 490 } 491 492 private IMountService getMountService() { 493 IBinder service = ServiceManager.getService("mount"); 494 if (service != null) { 495 return IMountService.Stub.asInterface(service); 496 } 497 return null; 498 } 499 500 @Override 501 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 502 if (actionId == EditorInfo.IME_NULL || actionId == EditorInfo.IME_ACTION_DONE) { 503 // Get the password 504 String password = v.getText().toString(); 505 506 if (TextUtils.isEmpty(password)) { 507 return true; 508 } 509 510 // Now that we have the password clear the password field. 511 v.setText(null); 512 513 // Disable the password entry while checking the password. This 514 // we either be reenabled if the password was wrong or after the 515 // cooldown period. 516 mPasswordEntry.setEnabled(false); 517 518 Log.d(TAG, "Attempting to send command to decrypt"); 519 new DecryptTask().execute(password); 520 521 return true; 522 } 523 return false; 524 } 525 526 // 527 // Code to update the state of, and handle clicks from, the "Emergency call" button. 528 // 529 // This code is mostly duplicated from the corresponding code in 530 // LockPatternUtils and LockPatternKeyguardView under frameworks/base. 531 // 532 533 private void updateEmergencyCallButtonState() { 534 Button button = (Button) findViewById(R.id.emergencyCallButton); 535 // The button isn't present at all in some configurations. 536 if (button == null) return; 537 538 if (isEmergencyCallCapable()) { 539 button.setVisibility(View.VISIBLE); 540 button.setOnClickListener(new View.OnClickListener() { 541 public void onClick(View v) { 542 takeEmergencyCallAction(); 543 } 544 }); 545 } else { 546 button.setVisibility(View.GONE); 547 return; 548 } 549 550 int newState = TelephonyManager.getDefault().getCallState(); 551 int textId; 552 if (newState == TelephonyManager.CALL_STATE_OFFHOOK) { 553 // show "return to call" text and show phone icon 554 textId = R.string.cryptkeeper_return_to_call; 555 int phoneCallIcon = R.drawable.stat_sys_phone_call; 556 button.setCompoundDrawablesWithIntrinsicBounds(phoneCallIcon, 0, 0, 0); 557 } else { 558 textId = R.string.cryptkeeper_emergency_call; 559 int emergencyIcon = R.drawable.ic_emergency; 560 button.setCompoundDrawablesWithIntrinsicBounds(emergencyIcon, 0, 0, 0); 561 } 562 button.setText(textId); 563 } 564 565 private boolean isEmergencyCallCapable() { 566 return getResources().getBoolean(com.android.internal.R.bool.config_voice_capable); 567 } 568 569 private void takeEmergencyCallAction() { 570 if (TelephonyManager.getDefault().getCallState() == TelephonyManager.CALL_STATE_OFFHOOK) { 571 resumeCall(); 572 } else { 573 launchEmergencyDialer(); 574 } 575 } 576 577 private void resumeCall() { 578 ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); 579 if (phone != null) { 580 try { 581 phone.showCallScreen(); 582 } catch (RemoteException e) { 583 Log.e(TAG, "Error calling ITelephony service: " + e); 584 } 585 } 586 } 587 588 private void launchEmergencyDialer() { 589 Intent intent = new Intent(ACTION_EMERGENCY_DIAL); 590 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 591 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 592 startActivity(intent); 593 } 594 } 595