CryptKeeper.java revision 91a2f0566afb91549cbda9289b516154a6467624
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 } 239 } 240 241 /** 242 * Note, we defer the state check and screen setup to onStart() because this will be 243 * re-run if the user clicks the power button (sleeping/waking the screen), and this is 244 * especially important if we were to lose the wakelock for any reason. 245 */ 246 @Override 247 public void onStart() { 248 super.onStart(); 249 250 // Check to see why we were started. 251 String progress = SystemProperties.get("vold.encrypt_progress"); 252 if (!"".equals(progress) 253 || isDebugView(FORCE_VIEW_PROGRESS) 254 || isDebugView(FORCE_VIEW_ERROR)) { 255 setContentView(R.layout.crypt_keeper_progress); 256 encryptionProgressInit(); 257 } else { 258 setContentView(R.layout.crypt_keeper_password_entry); 259 passwordEntryInit(); 260 } 261 } 262 263 @Override 264 public void onStop() { 265 super.onStop(); 266 267 mHandler.removeMessages(COOLDOWN); 268 mHandler.removeMessages(UPDATE_PROGRESS); 269 } 270 271 /** 272 * Reconfiguring, so propagate the wakelock to the next instance. This runs between onStop() 273 * and onDestroy() and only if we are changing configuration (e.g. rotation). Also clears 274 * mWakeLock so the subsequent call to onDestroy does not release it. 275 */ 276 @Override 277 public Object onRetainNonConfigurationInstance() { 278 NonConfigurationInstanceState state = new NonConfigurationInstanceState(mWakeLock); 279 mWakeLock = null; 280 return state; 281 } 282 283 @Override 284 public void onDestroy() { 285 super.onDestroy(); 286 287 if (mWakeLock != null) { 288 mWakeLock.release(); 289 mWakeLock = null; 290 } 291 } 292 293 private void encryptionProgressInit() { 294 // Accquire a partial wakelock to prevent the device from sleeping. Note 295 // we never release this wakelock as we will be restarted after the device 296 // is encrypted. 297 298 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 299 mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG); 300 301 mWakeLock.acquire(); 302 303 ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress_bar); 304 progressBar.setIndeterminate(true); 305 306 updateProgress(); 307 } 308 309 private void showFactoryReset() { 310 // Hide the encryption-bot to make room for the "factory reset" button 311 findViewById(R.id.encroid).setVisibility(View.GONE); 312 313 // Show the reset button, failure text, and a divider 314 Button button = (Button) findViewById(R.id.factory_reset); 315 button.setVisibility(View.VISIBLE); 316 button.setOnClickListener(new OnClickListener() { 317 public void onClick(View v) { 318 // Factory reset the device. 319 sendBroadcast(new Intent("android.intent.action.MASTER_CLEAR")); 320 } 321 }); 322 323 TextView tv = (TextView) findViewById(R.id.title); 324 tv.setText(R.string.crypt_keeper_failed_title); 325 326 tv = (TextView) findViewById(R.id.status); 327 tv.setText(R.string.crypt_keeper_failed_summary); 328 329 View view = findViewById(R.id.bottom_divider); 330 view.setVisibility(View.VISIBLE); 331 } 332 333 private void updateProgress() { 334 String state = SystemProperties.get("vold.encrypt_progress"); 335 336 if ("error_partially_encrypted".equals(state) || isDebugView(FORCE_VIEW_ERROR)) { 337 showFactoryReset(); 338 return; 339 } 340 341 int progress = 0; 342 try { 343 // Force a 50% progress state when debugging the view. 344 progress = isDebugView() ? 50 : Integer.parseInt(state); 345 } catch (Exception e) { 346 Log.w(TAG, "Error parsing progress: " + e.toString()); 347 } 348 349 CharSequence status = getText(R.string.crypt_keeper_setup_description); 350 TextView tv = (TextView) findViewById(R.id.status); 351 tv.setText(TextUtils.expandTemplate(status, Integer.toString(progress))); 352 353 // Check the progress every 5 seconds 354 mHandler.removeMessages(UPDATE_PROGRESS); 355 mHandler.sendEmptyMessageDelayed(UPDATE_PROGRESS, 5000); 356 } 357 358 private void cooldown() { 359 TextView tv = (TextView) findViewById(R.id.status); 360 361 if (mCooldown <= 0) { 362 // Re-enable the password entry 363 mPasswordEntry.setEnabled(true); 364 365 tv.setVisibility(View.GONE); 366 } else { 367 CharSequence template = getText(R.string.crypt_keeper_cooldown); 368 tv.setText(TextUtils.expandTemplate(template, Integer.toString(mCooldown))); 369 370 tv.setVisibility(View.VISIBLE); 371 372 mCooldown--; 373 mHandler.removeMessages(COOLDOWN); 374 mHandler.sendEmptyMessageDelayed(COOLDOWN, 1000); // Tick every second 375 } 376 } 377 378 private void passwordEntryInit() { 379 mPasswordEntry = (EditText) findViewById(R.id.passwordEntry); 380 mPasswordEntry.setOnEditorActionListener(this); 381 382 KeyboardView keyboardView = (PasswordEntryKeyboardView) findViewById(R.id.keyboard); 383 384 if (keyboardView != null) { 385 PasswordEntryKeyboardHelper keyboardHelper = new PasswordEntryKeyboardHelper(this, 386 keyboardView, mPasswordEntry, false); 387 keyboardHelper.setKeyboardMode(PasswordEntryKeyboardHelper.KEYBOARD_MODE_ALPHA); 388 } 389 390 updateEmergencyCallButtonState(); 391 } 392 393 private IMountService getMountService() { 394 IBinder service = ServiceManager.getService("mount"); 395 if (service != null) { 396 return IMountService.Stub.asInterface(service); 397 } 398 return null; 399 } 400 401 @Override 402 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 403 if (actionId == EditorInfo.IME_NULL || actionId == EditorInfo.IME_ACTION_DONE) { 404 // Get the password 405 String password = v.getText().toString(); 406 407 if (TextUtils.isEmpty(password)) { 408 return true; 409 } 410 411 // Now that we have the password clear the password field. 412 v.setText(null); 413 414 // Disable the password entry while checking the password. This 415 // we either be reenabled if the password was wrong or after the 416 // cooldown period. 417 mPasswordEntry.setEnabled(false); 418 419 new DecryptTask().execute(password); 420 421 return true; 422 } 423 return false; 424 } 425 426 // 427 // Code to update the state of, and handle clicks from, the "Emergency call" button. 428 // 429 // This code is mostly duplicated from the corresponding code in 430 // LockPatternUtils and LockPatternKeyguardView under frameworks/base. 431 // 432 433 private void updateEmergencyCallButtonState() { 434 Button button = (Button) findViewById(R.id.emergencyCallButton); 435 // The button isn't present at all in some configurations. 436 if (button == null) return; 437 438 if (isEmergencyCallCapable()) { 439 button.setVisibility(View.VISIBLE); 440 button.setOnClickListener(new View.OnClickListener() { 441 public void onClick(View v) { 442 takeEmergencyCallAction(); 443 } 444 }); 445 } else { 446 button.setVisibility(View.GONE); 447 return; 448 } 449 450 int newState = TelephonyManager.getDefault().getCallState(); 451 int textId; 452 if (newState == TelephonyManager.CALL_STATE_OFFHOOK) { 453 // show "return to call" text and show phone icon 454 textId = R.string.cryptkeeper_return_to_call; 455 int phoneCallIcon = R.drawable.stat_sys_phone_call; 456 button.setCompoundDrawablesWithIntrinsicBounds(phoneCallIcon, 0, 0, 0); 457 } else { 458 textId = R.string.cryptkeeper_emergency_call; 459 int emergencyIcon = R.drawable.ic_emergency; 460 button.setCompoundDrawablesWithIntrinsicBounds(emergencyIcon, 0, 0, 0); 461 } 462 button.setText(textId); 463 } 464 465 private boolean isEmergencyCallCapable() { 466 return getResources().getBoolean(com.android.internal.R.bool.config_voice_capable); 467 } 468 469 private void takeEmergencyCallAction() { 470 if (TelephonyManager.getDefault().getCallState() == TelephonyManager.CALL_STATE_OFFHOOK) { 471 resumeCall(); 472 } else { 473 launchEmergencyDialer(); 474 } 475 } 476 477 private void resumeCall() { 478 ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); 479 if (phone != null) { 480 try { 481 phone.showCallScreen(); 482 } catch (RemoteException e) { 483 Log.e(TAG, "Error calling ITelephony service: " + e); 484 } 485 } 486 } 487 488 private void launchEmergencyDialer() { 489 Intent intent = new Intent(ACTION_EMERGENCY_DIAL); 490 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 491 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 492 startActivity(intent); 493 } 494} 495