Warn users when selecting non-Direct Boot apps.

Certain apps like Phone, SMS, Emergency Info, and IME are critical
enough that they ideally need to be runnable before the device is
unlocked after a reboot.  Users can still pick non-Direct Boot aware
apps, but this change now warns users that the selected app won't be
runnable until after unlocking.

Bug: 27196876
Change-Id: I0498904d2f664fb41e8c1e6bb30d1cbf437cf4b9
diff --git a/src/com/android/settings/CustomListPreference.java b/src/com/android/settings/CustomListPreference.java
index ae83013..e7c7600 100644
--- a/src/com/android/settings/CustomListPreference.java
+++ b/src/com/android/settings/CustomListPreference.java
@@ -18,8 +18,13 @@
 
 import android.app.AlertDialog;
 import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
 import android.content.Context;
 import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
 import android.os.Bundle;
 import android.support.v14.preference.ListPreferenceDialogFragment;
 import android.support.v7.preference.ListPreference;
@@ -50,6 +55,18 @@
         return true;
     }
 
+    /**
+     * Called when a user is about to choose the given value, to determine if we
+     * should show a confirmation dialog.
+     *
+     * @param value the value the user is about to choose
+     * @return the message to show in a confirmation dialog, or {@code null} to
+     *         not request confirmation
+     */
+    protected CharSequence getConfirmationMessage(String value) {
+        return null;
+    }
+
     protected void onDialogStateRestored(Dialog dialog, Bundle savedInstanceState) {
     }
 
@@ -82,9 +99,7 @@
                 builder.setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialog, int which) {
-                        CustomListPreferenceDialogFragment.this.onClick(dialog,
-                                DialogInterface.BUTTON_POSITIVE);
-                        dialog.dismiss();
+                        onItemChosen();
                     }
                 });
             }
@@ -115,18 +130,11 @@
 
         protected DialogInterface.OnClickListener getOnItemClickListener() {
             return new DialogInterface.OnClickListener() {
+                @Override
                 public void onClick(DialogInterface dialog, int which) {
                     setClickedDialogEntryIndex(which);
-
-
                     if (getCustomizablePreference().isAutoClosePreference()) {
-                        /*
-                         * Clicking on an item simulates the positive button
-                         * click, and dismisses the dialog.
-                         */
-                        CustomListPreferenceDialogFragment.this.onClick(dialog,
-                                DialogInterface.BUTTON_POSITIVE);
-                        dialog.dismiss();
+                        onItemChosen();
                     }
                 }
             };
@@ -136,17 +144,74 @@
             mClickedDialogEntryIndex = which;
         }
 
+        private String getValue() {
+            final ListPreference preference = getCustomizablePreference();
+            if (mClickedDialogEntryIndex >= 0 && preference.getEntryValues() != null) {
+                return preference.getEntryValues()[mClickedDialogEntryIndex].toString();
+            } else {
+                return null;
+            }
+        }
+
+        /**
+         * Called when user has made a concrete item choice, but we might need
+         * to make a quick detour to confirm that choice with a second dialog.
+         */
+        protected void onItemChosen() {
+            final CharSequence message = getCustomizablePreference()
+                    .getConfirmationMessage(getValue());
+            if (message != null) {
+                final Fragment f = new ConfirmDialogFragment();
+                final Bundle args = new Bundle();
+                args.putCharSequence(Intent.EXTRA_TEXT, message);
+                f.setArguments(args);
+                f.setTargetFragment(CustomListPreferenceDialogFragment.this, 0);
+                final FragmentTransaction ft = getFragmentManager().beginTransaction();
+                ft.add(f, getTag() + "-Confirm");
+                ft.commitAllowingStateLoss();
+            } else {
+                onItemConfirmed();
+            }
+        }
+
+        /**
+         * Called when user has made a concrete item choice and we've fully
+         * confirmed they want to move forward (if we took a detour above).
+         */
+        protected void onItemConfirmed() {
+            onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
+            getDialog().dismiss();
+        }
+
         @Override
         public void onDialogClosed(boolean positiveResult) {
             getCustomizablePreference().onDialogClosed(positiveResult);
             final ListPreference preference = getCustomizablePreference();
-            if (positiveResult && mClickedDialogEntryIndex >= 0 &&
-                    preference.getEntryValues() != null) {
-                String value = preference.getEntryValues()[mClickedDialogEntryIndex].toString();
+            final String value = getValue();
+            if (positiveResult && value != null) {
                 if (preference.callChangeListener(value)) {
                     preference.setValue(value);
                 }
             }
         }
     }
+
+    public static class ConfirmDialogFragment extends DialogFragment {
+        @Override
+        public Dialog onCreateDialog(Bundle savedInstanceState) {
+            return new AlertDialog.Builder(getActivity())
+                    .setMessage(getArguments().getCharSequence(Intent.EXTRA_TEXT))
+                    .setPositiveButton(android.R.string.ok, new OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialog, int which) {
+                            final Fragment f = getTargetFragment();
+                            if (f != null) {
+                                ((CustomListPreferenceDialogFragment) f).onItemConfirmed();
+                            }
+                        }
+                    })
+                    .setNegativeButton(android.R.string.cancel, null)
+                    .create();
+        }
+    }
 }
diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java
index 90dd9e0..0725386 100644
--- a/src/com/android/settings/Utils.java
+++ b/src/com/android/settings/Utils.java
@@ -1150,5 +1150,14 @@
         }
         return false;
     }
-}
 
+    public static boolean isPackageDirectBootAware(Context context, String packageName) {
+        try {
+            final ApplicationInfo ai = context.getPackageManager().getApplicationInfo(
+                    packageName, 0);
+            return ai.isDirectBootAware() || ai.isPartiallyDirectBootAware();
+        } catch (NameNotFoundException ignored) {
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/settings/applications/DefaultEmergencyPreference.java b/src/com/android/settings/applications/DefaultEmergencyPreference.java
index f0a97b1..dd4dc2e 100644
--- a/src/com/android/settings/applications/DefaultEmergencyPreference.java
+++ b/src/com/android/settings/applications/DefaultEmergencyPreference.java
@@ -16,7 +16,6 @@
 
 package com.android.settings.applications;
 
-import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -30,9 +29,11 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.AttributeSet;
-import com.android.internal.telephony.SmsApplication;
+
 import com.android.settings.AppListPreference;
+import com.android.settings.R;
 import com.android.settings.SelfAvailablePreference;
+import com.android.settings.Utils;
 
 import java.util.List;
 import java.util.Objects;
@@ -57,6 +58,12 @@
     }
 
     @Override
+    protected CharSequence getConfirmationMessage(String value) {
+        return Utils.isPackageDirectBootAware(getContext(), value) ? null
+                : getContext().getText(R.string.direct_boot_unaware_dialog_message);
+    }
+
+    @Override
     protected boolean persistString(String value) {
         String previousValue = Settings.Secure.getString(mContentResolver,
                 Settings.Secure.EMERGENCY_ASSISTANCE_APPLICATION);
diff --git a/src/com/android/settings/applications/DefaultPhonePreference.java b/src/com/android/settings/applications/DefaultPhonePreference.java
index fdaf7ad..5689c83 100644
--- a/src/com/android/settings/applications/DefaultPhonePreference.java
+++ b/src/com/android/settings/applications/DefaultPhonePreference.java
@@ -24,23 +24,28 @@
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+
 import com.android.settings.AppListPreference;
+import com.android.settings.R;
 import com.android.settings.SelfAvailablePreference;
+import com.android.settings.Utils;
 
 import java.util.List;
 import java.util.Objects;
 
 public class DefaultPhonePreference extends AppListPreference implements SelfAvailablePreference {
-    private final Context mContext;
-
     public DefaultPhonePreference(Context context, AttributeSet attrs) {
         super(context, attrs);
-
-        mContext = context.getApplicationContext();
         loadDialerApps();
     }
 
     @Override
+    protected CharSequence getConfirmationMessage(String value) {
+        return Utils.isPackageDirectBootAware(getContext(), value) ? null
+                : getContext().getText(R.string.direct_boot_unaware_dialog_message);
+    }
+
+    @Override
     protected boolean persistString(String value) {
         if (!TextUtils.isEmpty(value) && !Objects.equals(value, getDefaultPackage())) {
             DefaultDialerManager.setDefaultDialerApplication(getContext(), value, mUserId);
diff --git a/src/com/android/settings/applications/DefaultSmsPreference.java b/src/com/android/settings/applications/DefaultSmsPreference.java
index 9315102..96ac9a2 100644
--- a/src/com/android/settings/applications/DefaultSmsPreference.java
+++ b/src/com/android/settings/applications/DefaultSmsPreference.java
@@ -22,19 +22,20 @@
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+
 import com.android.internal.telephony.SmsApplication;
 import com.android.internal.telephony.SmsApplication.SmsApplicationData;
 import com.android.settings.AppListPreference;
+import com.android.settings.R;
 import com.android.settings.SelfAvailablePreference;
+import com.android.settings.Utils;
 
 import java.util.Collection;
 import java.util.Objects;
 
 public class DefaultSmsPreference extends AppListPreference implements SelfAvailablePreference {
-
     public DefaultSmsPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
-
         loadSmsApps();
     }
 
@@ -60,6 +61,12 @@
     }
 
     @Override
+    protected CharSequence getConfirmationMessage(String value) {
+        return Utils.isPackageDirectBootAware(getContext(), value) ? null
+                : getContext().getText(R.string.direct_boot_unaware_dialog_message);
+    }
+
+    @Override
     protected boolean persistString(String value) {
         if (!TextUtils.isEmpty(value) && !Objects.equals(value, getDefaultPackage())) {
             SmsApplication.setDefaultApplication(value, getContext());
diff --git a/src/com/android/settings/inputmethod/InputMethodPreference.java b/src/com/android/settings/inputmethod/InputMethodPreference.java
index 1d4fa67..2c27700 100755
--- a/src/com/android/settings/inputmethod/InputMethodPreference.java
+++ b/src/com/android/settings/inputmethod/InputMethodPreference.java
@@ -35,6 +35,7 @@
 
 import com.android.internal.inputmethod.InputMethodUtils;
 import com.android.settings.R;
+import com.android.settings.Utils;
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.settingslib.RestrictedSwitchPreference;
 
@@ -142,18 +143,22 @@
         }
         if (isChecked()) {
             // Disable this IME.
-            setChecked(false);
-            mOnSaveListener.onSaveInputMethodPreference(this);
+            setCheckedInternal(false);
             return false;
         }
         if (InputMethodUtils.isSystemIme(mImi)) {
-            // Enable a system IME. No need to show a security warning dialog.
-            setChecked(true);
-            mOnSaveListener.onSaveInputMethodPreference(this);
-            return false;
+            // Enable a system IME. No need to show a security warning dialog,
+            // but we might need to prompt if it's not Direct Boot aware.
+            if (Utils.isPackageDirectBootAware(getContext(), mImi.getPackageName())) {
+                setCheckedInternal(true);
+            } else {
+                showDirectBootWarnDialog();
+            }
+        } else {
+            // Once security is confirmed, we might prompt if the IME isn't
+            // Direct Boot aware.
+            showSecurityWarnDialog();
         }
-        // Enable a 3rd party IME.
-        showSecurityWarnDialog(mImi);
         return false;
     }
 
@@ -218,7 +223,13 @@
                 subtypes, getContext(), mImi);
     }
 
-    private void showSecurityWarnDialog(final InputMethodInfo imi) {
+    private void setCheckedInternal(boolean checked) {
+        super.setChecked(checked);
+        mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this);
+        notifyChanged();
+    }
+
+    private void showSecurityWarnDialog() {
         if (mDialog != null && mDialog.isShowing()) {
             mDialog.dismiss();
         }
@@ -226,25 +237,50 @@
         final AlertDialog.Builder builder = new AlertDialog.Builder(context);
         builder.setCancelable(true /* cancelable */);
         builder.setTitle(android.R.string.dialog_alert_title);
-        final CharSequence label = imi.getServiceInfo().applicationInfo.loadLabel(
+        final CharSequence label = mImi.getServiceInfo().applicationInfo.loadLabel(
                 context.getPackageManager());
         builder.setMessage(context.getString(R.string.ime_security_warning, label));
         builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
             @Override
             public void onClick(final DialogInterface dialog, final int which) {
-                // The user confirmed to enable a 3rd party IME.
-                setChecked(true);
-                mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this);
-                notifyChanged();
+                // The user confirmed to enable a 3rd party IME, but we might
+                // need to prompt if it's not Direct Boot aware.
+                if (Utils.isPackageDirectBootAware(getContext(), mImi.getPackageName())) {
+                    setCheckedInternal(true);
+                } else {
+                    showDirectBootWarnDialog();
+                }
             }
         });
         builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
             @Override
             public void onClick(final DialogInterface dialog, final int which) {
                 // The user canceled to enable a 3rd party IME.
-                setChecked(false);
-                mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this);
-                notifyChanged();
+                setCheckedInternal(false);
+            }
+        });
+        mDialog = builder.create();
+        mDialog.show();
+    }
+
+    private void showDirectBootWarnDialog() {
+        if (mDialog != null && mDialog.isShowing()) {
+            mDialog.dismiss();
+        }
+        final Context context = getContext();
+        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+        builder.setCancelable(true /* cancelable */);
+        builder.setMessage(context.getText(R.string.direct_boot_unaware_dialog_message));
+        builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(final DialogInterface dialog, final int which) {
+                setCheckedInternal(true);
+            }
+        });
+        builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(final DialogInterface dialog, final int which) {
+                setCheckedInternal(false);
             }
         });
         mDialog = builder.create();