CryptKeeper.java revision 0460675b7c0d5a9b02dae01578c64ff0453e4fb7
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 17package com.android.settings; 18 19import android.app.Activity; 20import android.app.StatusBarManager; 21import android.content.ComponentName; 22import android.content.Context; 23import android.content.Intent; 24import android.content.pm.PackageManager; 25import android.graphics.Rect; 26import android.inputmethodservice.KeyboardView; 27import android.os.AsyncTask; 28import android.os.Bundle; 29import android.os.Handler; 30import android.os.IBinder; 31import android.os.Message; 32import android.os.PowerManager; 33import android.os.RemoteException; 34import android.os.ServiceManager; 35import android.os.SystemProperties; 36import android.os.storage.IMountService; 37import android.telephony.TelephonyManager; 38import android.text.TextUtils; 39import android.util.AttributeSet; 40import android.util.Log; 41import android.view.KeyEvent; 42import android.view.MotionEvent; 43import android.view.View; 44import android.view.View.OnClickListener; 45import android.view.inputmethod.EditorInfo; 46import android.view.inputmethod.InputMethodManager; 47import android.widget.Button; 48import android.widget.EditText; 49import android.widget.ProgressBar; 50import android.widget.TextView; 51 52import com.android.internal.telephony.ITelephony; 53import com.android.internal.widget.PasswordEntryKeyboardHelper; 54import com.android.internal.widget.PasswordEntryKeyboardView; 55 56/** 57 * Settings screens to show the UI flows for encrypting/decrypting the device. 58 * 59 * This may be started via adb for debugging the UI layout, without having to go through 60 * encryption flows everytime. It should be noted that starting the activity in this manner 61 * is only useful for verifying UI-correctness - the behavior will not be identical. 62 * <pre> 63 * $ adb shell pm enable com.android.settings/.CryptKeeper 64 * $ adb shell am start \ 65 * -e "com.android.settings.CryptKeeper.DEBUG_FORCE_VIEW" "progress" \ 66 * -n com.android.settings/.CryptKeeper 67 * </pre> 68 */ 69public class CryptKeeper extends Activity implements TextView.OnEditorActionListener { 70 private static final String TAG = "CryptKeeper"; 71 72 private static final String DECRYPT_STATE = "trigger_restart_framework"; 73 74 private static final int UPDATE_PROGRESS = 1; 75 private static final int COOLDOWN = 2; 76 77 private static final int MAX_FAILED_ATTEMPTS = 30; 78 private static final int COOL_DOWN_ATTEMPTS = 10; 79 private static final int COOL_DOWN_INTERVAL = 30; // 30 seconds 80 81 // Intent action for launching the Emergency Dialer activity. 82 static final String ACTION_EMERGENCY_DIAL = "com.android.phone.EmergencyDialer.DIAL"; 83 84 // Debug Intent extras so that this Activity may be started via adb for debugging UI layouts 85 private static final String EXTRA_FORCE_VIEW = 86 "com.android.settings.CryptKeeper.DEBUG_FORCE_VIEW"; 87 private static final String FORCE_VIEW_PROGRESS = "progress"; 88 private static final String FORCE_VIEW_ENTRY = "entry"; 89 private static final String FORCE_VIEW_ERROR = "error"; 90 91 private int mCooldown; 92 PowerManager.WakeLock mWakeLock; 93 private EditText mPasswordEntry; 94 95 /** 96 * Used to propagate state through configuration changes (e.g. screen rotation) 97 */ 98 private static class NonConfigurationInstanceState { 99 final PowerManager.WakeLock wakelock; 100 101 NonConfigurationInstanceState(PowerManager.WakeLock _wakelock) { 102 wakelock = _wakelock; 103 } 104 } 105 106 // This activity is used to fade the screen to black after the password is entered. 107 public static class Blank extends Activity { 108 @Override 109 public void onCreate(Bundle savedInstanceState) { 110 super.onCreate(savedInstanceState); 111 setContentView(R.layout.crypt_keeper_blank); 112 } 113 } 114 115 // Use a custom EditText to prevent the input method from showing. 116 public static class CryptEditText extends EditText { 117 InputMethodManager imm; 118 119 public CryptEditText(Context context, AttributeSet attrs) { 120 super(context, attrs); 121 imm = ((InputMethodManager) getContext(). 122 getSystemService(Context.INPUT_METHOD_SERVICE)); 123 } 124 125 @Override 126 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 127 super.onFocusChanged(focused, direction, previouslyFocusedRect); 128 129 if (focused && imm != null && imm.isActive(this)) { 130 imm.hideSoftInputFromWindow(getApplicationWindowToken(), 0); 131 } 132 } 133 134 @Override 135 public boolean onTouchEvent(MotionEvent event) { 136 boolean handled = super.onTouchEvent(event); 137 138 if (imm != null && imm.isActive(this)) { 139 imm.hideSoftInputFromWindow(getApplicationWindowToken(), 0); 140 } 141 142 return handled; 143 } 144 } 145 146 private class DecryptTask extends AsyncTask<String, Void, Integer> { 147 @Override 148 protected Integer doInBackground(String... params) { 149 IMountService service = getMountService(); 150 try { 151 return service.decryptStorage(params[0]); 152 } catch (Exception e) { 153 Log.e(TAG, "Error while decrypting...", e); 154 return -1; 155 } 156 } 157 158 @Override 159 protected void onPostExecute(Integer failedAttempts) { 160 if (failedAttempts == 0) { 161 // The password was entered successfully. Start the Blank activity 162 // so this activity animates to black before the devices starts. Note 163 // It has 1 second to complete the animation or it will be frozen 164 // until the boot animation comes back up. 165 Intent intent = new Intent(CryptKeeper.this, Blank.class); 166 finish(); 167 startActivity(intent); 168 } else if (failedAttempts == MAX_FAILED_ATTEMPTS) { 169 // Factory reset the device. 170 sendBroadcast(new Intent("android.intent.action.MASTER_CLEAR")); 171 } else if ((failedAttempts % COOL_DOWN_ATTEMPTS) == 0) { 172 mCooldown = COOL_DOWN_INTERVAL; 173 cooldown(); 174 } else { 175 TextView tv = (TextView) findViewById(R.id.status); 176 tv.setText(R.string.try_again); 177 tv.setVisibility(View.VISIBLE); 178 179 // Reenable the password entry 180 mPasswordEntry.setEnabled(true); 181 } 182 } 183 } 184 185 private final Handler mHandler = new Handler() { 186 @Override 187 public void handleMessage(Message msg) { 188 switch (msg.what) { 189 case UPDATE_PROGRESS: 190 updateProgress(); 191 break; 192 193 case COOLDOWN: 194 cooldown(); 195 break; 196 } 197 } 198 }; 199 200 /** @return whether or not this Activity was started for debugging the UI only. */ 201 private boolean isDebugView() { 202 return getIntent().hasExtra(EXTRA_FORCE_VIEW); 203 } 204 205 /** @return whether or not this Activity was started for debugging the specific UI view only. */ 206 private boolean isDebugView(String viewType /* non-nullable */) { 207 return viewType.equals(getIntent().getStringExtra(EXTRA_FORCE_VIEW)); 208 } 209 210 @Override 211 public void onCreate(Bundle savedInstanceState) { 212 super.onCreate(savedInstanceState); 213 214 // If we are not encrypted or encrypting, get out quickly. 215 String state = SystemProperties.get("vold.decrypt"); 216 if (!isDebugView() && ("".equals(state) || DECRYPT_STATE.equals(state))) { 217 // Disable the crypt keeper. 218 PackageManager pm = getPackageManager(); 219 ComponentName name = new ComponentName(this, CryptKeeper.class); 220 pm.setComponentEnabledSetting(name, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0); 221 return; 222 } 223 224 // Disable the status bar 225 StatusBarManager sbm = (StatusBarManager) getSystemService(Context.STATUS_BAR_SERVICE); 226 sbm.disable(StatusBarManager.DISABLE_EXPAND 227 | StatusBarManager.DISABLE_NOTIFICATION_ICONS 228 | StatusBarManager.DISABLE_NOTIFICATION_ALERTS 229 | StatusBarManager.DISABLE_SYSTEM_INFO 230 | StatusBarManager.DISABLE_NAVIGATION 231 | StatusBarManager.DISABLE_BACK); 232 233 // Check for (and recover) retained instance data 234 Object lastInstance = getLastNonConfigurationInstance(); 235 if (lastInstance instanceof NonConfigurationInstanceState) { 236 NonConfigurationInstanceState retained = (NonConfigurationInstanceState) lastInstance; 237 mWakeLock = retained.wakelock; 238 Log.d(TAG, "Restoring wakelock from NonConfigurationInstanceState"); 239 } 240 } 241 242 /** 243 * Note, we defer the state check and screen setup to onStart() because this will be 244 * re-run if the user clicks the power button (sleeping/waking the screen), and this is 245 * especially important if we were to lose the wakelock for any reason. 246 */ 247 @Override 248 public void onStart() { 249 super.onStart(); 250 251 // Check to see why we were started. 252 String progress = SystemProperties.get("vold.encrypt_progress"); 253 if (!"".equals(progress) 254 || isDebugView(FORCE_VIEW_PROGRESS) 255 || isDebugView(FORCE_VIEW_ERROR)) { 256 setContentView(R.layout.crypt_keeper_progress); 257 encryptionProgressInit(); 258 } else { 259 setContentView(R.layout.crypt_keeper_password_entry); 260 passwordEntryInit(); 261 } 262 } 263 264 @Override 265 public void onStop() { 266 super.onStop(); 267 268 mHandler.removeMessages(COOLDOWN); 269 mHandler.removeMessages(UPDATE_PROGRESS); 270 } 271 272 /** 273 * Reconfiguring, so propagate the wakelock to the next instance. This runs between onStop() 274 * and onDestroy() and only if we are changing configuration (e.g. rotation). Also clears 275 * mWakeLock so the subsequent call to onDestroy does not release it. 276 */ 277 @Override 278 public Object onRetainNonConfigurationInstance() { 279 NonConfigurationInstanceState state = new NonConfigurationInstanceState(mWakeLock); 280 Log.d(TAG, "Handing wakelock off to NonConfigurationInstanceState"); 281 mWakeLock = null; 282 return state; 283 } 284 285 @Override 286 public void onDestroy() { 287 super.onDestroy(); 288 289 if (mWakeLock != null) { 290 Log.d(TAG, "Releasing and destroying wakelock"); 291 mWakeLock.release(); 292 mWakeLock = null; 293 } 294 } 295 296 private void encryptionProgressInit() { 297 // Accquire a partial wakelock to prevent the device from sleeping. Note 298 // we never release this wakelock as we will be restarted after the device 299 // is encrypted. 300 301 Log.d(TAG, "Encryption progress screen initializing."); 302 if (mWakeLock != null) { 303 Log.d(TAG, "Acquiring wakelock."); 304 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 305 mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG); 306 mWakeLock.acquire(); 307 } 308 309 ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress_bar); 310 progressBar.setIndeterminate(true); 311 312 updateProgress(); 313 } 314 315 private void showFactoryReset() { 316 // Hide the encryption-bot to make room for the "factory reset" button 317 findViewById(R.id.encroid).setVisibility(View.GONE); 318 319 // Show the reset button, failure text, and a divider 320 Button button = (Button) findViewById(R.id.factory_reset); 321 button.setVisibility(View.VISIBLE); 322 button.setOnClickListener(new OnClickListener() { 323 public void onClick(View v) { 324 // Factory reset the device. 325 sendBroadcast(new Intent("android.intent.action.MASTER_CLEAR")); 326 } 327 }); 328 329 TextView tv = (TextView) findViewById(R.id.title); 330 tv.setText(R.string.crypt_keeper_failed_title); 331 332 tv = (TextView) findViewById(R.id.status); 333 tv.setText(R.string.crypt_keeper_failed_summary); 334 335 View view = findViewById(R.id.bottom_divider); 336 if (view != null) { 337 view.setVisibility(View.VISIBLE); 338 } 339 } 340 341 private void updateProgress() { 342 String state = SystemProperties.get("vold.encrypt_progress"); 343 344 if ("error_partially_encrypted".equals(state) || isDebugView(FORCE_VIEW_ERROR)) { 345 showFactoryReset(); 346 return; 347 } 348 349 int progress = 0; 350 try { 351 // Force a 50% progress state when debugging the view. 352 progress = isDebugView() ? 50 : Integer.parseInt(state); 353 } catch (Exception e) { 354 Log.w(TAG, "Error parsing progress: " + e.toString()); 355 } 356 357 CharSequence status = getText(R.string.crypt_keeper_setup_description); 358 Log.v(TAG, "Encryption progress: " + progress); 359 TextView tv = (TextView) findViewById(R.id.status); 360 tv.setText(TextUtils.expandTemplate(status, Integer.toString(progress))); 361 362 // Check the progress every 5 seconds 363 mHandler.removeMessages(UPDATE_PROGRESS); 364 mHandler.sendEmptyMessageDelayed(UPDATE_PROGRESS, 5000); 365 } 366 367 private void cooldown() { 368 TextView tv = (TextView) findViewById(R.id.status); 369 370 if (mCooldown <= 0) { 371 // Re-enable the password entry 372 mPasswordEntry.setEnabled(true); 373 374 tv.setVisibility(View.GONE); 375 } else { 376 CharSequence template = getText(R.string.crypt_keeper_cooldown); 377 tv.setText(TextUtils.expandTemplate(template, Integer.toString(mCooldown))); 378 379 tv.setVisibility(View.VISIBLE); 380 381 mCooldown--; 382 mHandler.removeMessages(COOLDOWN); 383 mHandler.sendEmptyMessageDelayed(COOLDOWN, 1000); // Tick every second 384 } 385 } 386 387 private void passwordEntryInit() { 388 mPasswordEntry = (EditText) findViewById(R.id.passwordEntry); 389 mPasswordEntry.setOnEditorActionListener(this); 390 391 KeyboardView keyboardView = (PasswordEntryKeyboardView) findViewById(R.id.keyboard); 392 393 if (keyboardView != null) { 394 PasswordEntryKeyboardHelper keyboardHelper = new PasswordEntryKeyboardHelper(this, 395 keyboardView, mPasswordEntry, false); 396 keyboardHelper.setKeyboardMode(PasswordEntryKeyboardHelper.KEYBOARD_MODE_ALPHA); 397 } 398 399 updateEmergencyCallButtonState(); 400 } 401 402 private IMountService getMountService() { 403 IBinder service = ServiceManager.getService("mount"); 404 if (service != null) { 405 return IMountService.Stub.asInterface(service); 406 } 407 return null; 408 } 409 410 @Override 411 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 412 if (actionId == EditorInfo.IME_NULL || actionId == EditorInfo.IME_ACTION_DONE) { 413 // Get the password 414 String password = v.getText().toString(); 415 416 if (TextUtils.isEmpty(password)) { 417 return true; 418 } 419 420 // Now that we have the password clear the password field. 421 v.setText(null); 422 423 // Disable the password entry while checking the password. This 424 // we either be reenabled if the password was wrong or after the 425 // cooldown period. 426 mPasswordEntry.setEnabled(false); 427 428 Log.d(TAG, "Attempting to send command to decrypt"); 429 new DecryptTask().execute(password); 430 431 return true; 432 } 433 return false; 434 } 435 436 // 437 // Code to update the state of, and handle clicks from, the "Emergency call" button. 438 // 439 // This code is mostly duplicated from the corresponding code in 440 // LockPatternUtils and LockPatternKeyguardView under frameworks/base. 441 // 442 443 private void updateEmergencyCallButtonState() { 444 Button button = (Button) findViewById(R.id.emergencyCallButton); 445 // The button isn't present at all in some configurations. 446 if (button == null) return; 447 448 if (isEmergencyCallCapable()) { 449 button.setVisibility(View.VISIBLE); 450 button.setOnClickListener(new View.OnClickListener() { 451 public void onClick(View v) { 452 takeEmergencyCallAction(); 453 } 454 }); 455 } else { 456 button.setVisibility(View.GONE); 457 return; 458 } 459 460 int newState = TelephonyManager.getDefault().getCallState(); 461 int textId; 462 if (newState == TelephonyManager.CALL_STATE_OFFHOOK) { 463 // show "return to call" text and show phone icon 464 textId = R.string.cryptkeeper_return_to_call; 465 int phoneCallIcon = R.drawable.stat_sys_phone_call; 466 button.setCompoundDrawablesWithIntrinsicBounds(phoneCallIcon, 0, 0, 0); 467 } else { 468 textId = R.string.cryptkeeper_emergency_call; 469 int emergencyIcon = R.drawable.ic_emergency; 470 button.setCompoundDrawablesWithIntrinsicBounds(emergencyIcon, 0, 0, 0); 471 } 472 button.setText(textId); 473 } 474 475 private boolean isEmergencyCallCapable() { 476 return getResources().getBoolean(com.android.internal.R.bool.config_voice_capable); 477 } 478 479 private void takeEmergencyCallAction() { 480 if (TelephonyManager.getDefault().getCallState() == TelephonyManager.CALL_STATE_OFFHOOK) { 481 resumeCall(); 482 } else { 483 launchEmergencyDialer(); 484 } 485 } 486 487 private void resumeCall() { 488 ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); 489 if (phone != null) { 490 try { 491 phone.showCallScreen(); 492 } catch (RemoteException e) { 493 Log.e(TAG, "Error calling ITelephony service: " + e); 494 } 495 } 496 } 497 498 private void launchEmergencyDialer() { 499 Intent intent = new Intent(ACTION_EMERGENCY_DIAL); 500 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 501 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 502 startActivity(intent); 503 } 504} 505