Home | History | Annotate | Download | only in settings
      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