Home | History | Annotate | Download | only in settings
      1 /*
      2  * Copyright (C) 2016 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 package com.android.settings;
     17 
     18 import android.annotation.NonNull;
     19 import android.app.Activity;
     20 import android.app.AlertDialog;
     21 import android.app.admin.DevicePolicyManager;
     22 import android.content.DialogInterface;
     23 import android.content.pm.UserInfo;
     24 import android.net.http.SslCertificate;
     25 import android.os.UserHandle;
     26 import android.os.UserManager;
     27 import android.view.View;
     28 import android.view.animation.AnimationUtils;
     29 import android.widget.AdapterView;
     30 import android.widget.ArrayAdapter;
     31 import android.widget.Button;
     32 import android.widget.LinearLayout;
     33 import android.widget.Spinner;
     34 
     35 import com.android.internal.widget.LockPatternUtils;
     36 import com.android.settings.TrustedCredentialsSettings.CertHolder;
     37 import com.android.settingslib.RestrictedLockUtils;
     38 
     39 import java.security.cert.X509Certificate;
     40 import java.util.ArrayList;
     41 import java.util.List;
     42 import java.util.function.IntConsumer;
     43 
     44 class TrustedCredentialsDialogBuilder extends AlertDialog.Builder {
     45     public interface DelegateInterface {
     46         List<X509Certificate> getX509CertsFromCertHolder(CertHolder certHolder);
     47         void removeOrInstallCert(CertHolder certHolder);
     48         boolean startConfirmCredentialIfNotConfirmed(int userId,
     49                 IntConsumer onCredentialConfirmedListener);
     50     }
     51 
     52     private final DialogEventHandler mDialogEventHandler;
     53 
     54     public TrustedCredentialsDialogBuilder(Activity activity, DelegateInterface delegate) {
     55         super(activity);
     56         mDialogEventHandler = new DialogEventHandler(activity, delegate);
     57 
     58         initDefaultBuilderParams();
     59     }
     60 
     61     public TrustedCredentialsDialogBuilder setCertHolder(CertHolder certHolder) {
     62         return setCertHolders(certHolder == null ? new CertHolder[0]
     63                 : new CertHolder[]{certHolder});
     64     }
     65 
     66     public TrustedCredentialsDialogBuilder setCertHolders(@NonNull CertHolder[] certHolders) {
     67         mDialogEventHandler.setCertHolders(certHolders);
     68         return this;
     69     }
     70 
     71     @Override
     72     public AlertDialog create() {
     73         AlertDialog dialog = super.create();
     74         dialog.setOnShowListener(mDialogEventHandler);
     75         mDialogEventHandler.setDialog(dialog);
     76         return dialog;
     77     }
     78 
     79     private void initDefaultBuilderParams() {
     80         setTitle(com.android.internal.R.string.ssl_certificate);
     81         setView(mDialogEventHandler.mRootContainer);
     82 
     83         // Enable buttons here. The actual labels and listeners are configured in nextOrDismiss
     84         setPositiveButton(R.string.trusted_credentials_trust_label, null);
     85         setNegativeButton(android.R.string.ok, null);
     86     }
     87 
     88     private static class DialogEventHandler implements DialogInterface.OnShowListener,
     89             View.OnClickListener  {
     90         private static final long OUT_DURATION_MS = 300;
     91         private static final long IN_DURATION_MS = 200;
     92 
     93         private final Activity mActivity;
     94         private final DevicePolicyManager mDpm;
     95         private final UserManager mUserManager;
     96         private final DelegateInterface mDelegate;
     97         private final LinearLayout mRootContainer;
     98 
     99         private int mCurrentCertIndex = -1;
    100         private AlertDialog mDialog;
    101         private Button mPositiveButton;
    102         private Button mNegativeButton;
    103         private boolean mNeedsApproval;
    104         private CertHolder[] mCertHolders = new CertHolder[0];
    105         private View mCurrentCertLayout = null;
    106 
    107         public DialogEventHandler(Activity activity, DelegateInterface delegate) {
    108             mActivity = activity;
    109             mDpm = activity.getSystemService(DevicePolicyManager.class);
    110             mUserManager = activity.getSystemService(UserManager.class);
    111             mDelegate = delegate;
    112 
    113             mRootContainer = new LinearLayout(mActivity);
    114             mRootContainer.setOrientation(LinearLayout.VERTICAL);
    115         }
    116 
    117         public void setDialog(AlertDialog dialog) {
    118             mDialog = dialog;
    119         }
    120 
    121         public void setCertHolders(CertHolder[] certHolder) {
    122             mCertHolders = certHolder;
    123         }
    124 
    125         @Override
    126         public void onShow(DialogInterface dialogInterface) {
    127             // Config the display content only when the dialog is shown because the
    128             // positive/negative buttons don't exist until the dialog is shown
    129             nextOrDismiss();
    130         }
    131 
    132         @Override
    133         public void onClick(View view) {
    134             if (view == mPositiveButton) {
    135                 if (mNeedsApproval) {
    136                     onClickTrust();
    137                 } else {
    138                     onClickOk();
    139                 }
    140             } else if (view == mNegativeButton) {
    141                 onClickRemove();
    142             }
    143         }
    144 
    145         private void onClickOk() {
    146             nextOrDismiss();
    147         }
    148 
    149         private void onClickTrust() {
    150             CertHolder certHolder = getCurrentCertInfo();
    151             if (!mDelegate.startConfirmCredentialIfNotConfirmed(certHolder.getUserId(),
    152                     this::onCredentialConfirmed)) {
    153                 mDpm.approveCaCert(certHolder.getAlias(), certHolder.getUserId(), true);
    154                 nextOrDismiss();
    155             }
    156         }
    157 
    158         private void onClickRemove() {
    159             final CertHolder certHolder = getCurrentCertInfo();
    160             new AlertDialog.Builder(mActivity)
    161                     .setMessage(getButtonConfirmation(certHolder))
    162                     .setPositiveButton(android.R.string.yes,
    163                             new DialogInterface.OnClickListener() {
    164                                 @Override
    165                                 public void onClick(DialogInterface dialog, int id) {
    166                                     mDelegate.removeOrInstallCert(certHolder);
    167                                     dialog.dismiss();
    168                                     nextOrDismiss();
    169                                 }
    170                             })
    171                     .setNegativeButton(android.R.string.no, null)
    172                     .show();
    173         }
    174 
    175         private void onCredentialConfirmed(int userId) {
    176             if (mDialog.isShowing() && mNeedsApproval && getCurrentCertInfo() != null
    177                     && getCurrentCertInfo().getUserId() == userId) {
    178                 // Treat it as user just clicks "trust" for this cert
    179                 onClickTrust();
    180             }
    181         }
    182 
    183         private CertHolder getCurrentCertInfo() {
    184             return mCurrentCertIndex < mCertHolders.length ? mCertHolders[mCurrentCertIndex] : null;
    185         }
    186 
    187         private void nextOrDismiss() {
    188             mCurrentCertIndex++;
    189             // find next non-null cert or dismiss
    190             while (mCurrentCertIndex < mCertHolders.length && getCurrentCertInfo() == null) {
    191                 mCurrentCertIndex++;
    192             }
    193 
    194             if (mCurrentCertIndex >= mCertHolders.length) {
    195                 mDialog.dismiss();
    196                 return;
    197             }
    198 
    199             updateViewContainer();
    200             updatePositiveButton();
    201             updateNegativeButton();
    202         }
    203 
    204         /**
    205          * @return true if current user or parent user is guarded by screenlock
    206          */
    207         private boolean isUserSecure(int userId) {
    208             final LockPatternUtils lockPatternUtils = new LockPatternUtils(mActivity);
    209             if (lockPatternUtils.isSecure(userId)) {
    210                 return true;
    211             }
    212             UserInfo parentUser = mUserManager.getProfileParent(userId);
    213             if (parentUser == null) {
    214                 return false;
    215             }
    216             return lockPatternUtils.isSecure(parentUser.id);
    217         }
    218 
    219         private void updatePositiveButton() {
    220             final CertHolder certHolder = getCurrentCertInfo();
    221             mNeedsApproval = !certHolder.isSystemCert()
    222                     && isUserSecure(certHolder.getUserId())
    223                     && !mDpm.isCaCertApproved(certHolder.getAlias(), certHolder.getUserId());
    224 
    225             final boolean isProfileOrDeviceOwner = RestrictedLockUtils.getProfileOrDeviceOwner(
    226                     mActivity, certHolder.getUserId()) != null;
    227 
    228             // Show trust button only when it requires consumer user (non-PO/DO) to approve
    229             CharSequence displayText = mActivity.getText(!isProfileOrDeviceOwner && mNeedsApproval
    230                     ? R.string.trusted_credentials_trust_label
    231                     : android.R.string.ok);
    232             mPositiveButton = updateButton(DialogInterface.BUTTON_POSITIVE, displayText);
    233         }
    234 
    235         private void updateNegativeButton() {
    236             final CertHolder certHolder = getCurrentCertInfo();
    237             final boolean showRemoveButton = !mUserManager.hasUserRestriction(
    238                     UserManager.DISALLOW_CONFIG_CREDENTIALS,
    239                     new UserHandle(certHolder.getUserId()));
    240             CharSequence displayText = mActivity.getText(getButtonLabel(certHolder));
    241             mNegativeButton = updateButton(DialogInterface.BUTTON_NEGATIVE, displayText);
    242             mNegativeButton.setVisibility(showRemoveButton ? View.VISIBLE : View.GONE);
    243         }
    244 
    245         /**
    246          * mDialog.setButton doesn't trigger text refresh since mDialog has been shown.
    247          * It's invoked only in case mDialog is refreshed.
    248          * setOnClickListener is invoked to avoid dismiss dialog onClick
    249          */
    250         private Button updateButton(int buttonType, CharSequence displayText) {
    251             mDialog.setButton(buttonType, displayText, (DialogInterface.OnClickListener) null);
    252             Button button = mDialog.getButton(buttonType);
    253             button.setText(displayText);
    254             button.setOnClickListener(this);
    255             return button;
    256         }
    257 
    258 
    259         private void updateViewContainer() {
    260             CertHolder certHolder = getCurrentCertInfo();
    261             LinearLayout nextCertLayout = getCertLayout(certHolder);
    262 
    263             // Displaying first cert doesn't require animation
    264             if (mCurrentCertLayout == null) {
    265                 mCurrentCertLayout = nextCertLayout;
    266                 mRootContainer.addView(mCurrentCertLayout);
    267             } else {
    268                 animateViewTransition(nextCertLayout);
    269             }
    270         }
    271 
    272         private LinearLayout getCertLayout(final CertHolder certHolder) {
    273             final ArrayList<View> views =  new ArrayList<View>();
    274             final ArrayList<String> titles = new ArrayList<String>();
    275             List<X509Certificate> certificates = mDelegate.getX509CertsFromCertHolder(certHolder);
    276             if (certificates != null) {
    277                 for (X509Certificate certificate : certificates) {
    278                     SslCertificate sslCert = new SslCertificate(certificate);
    279                     views.add(sslCert.inflateCertificateView(mActivity));
    280                     titles.add(sslCert.getIssuedTo().getCName());
    281                 }
    282             }
    283 
    284             ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(mActivity,
    285                     android.R.layout.simple_spinner_item,
    286                     titles);
    287             arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    288             Spinner spinner = new Spinner(mActivity);
    289             spinner.setAdapter(arrayAdapter);
    290             spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    291                 @Override
    292                 public void onItemSelected(AdapterView<?> parent, View view, int position,
    293                         long id) {
    294                     for (int i = 0; i < views.size(); i++) {
    295                         views.get(i).setVisibility(i == position ? View.VISIBLE : View.GONE);
    296                     }
    297                 }
    298 
    299                 @Override
    300                 public void onNothingSelected(AdapterView<?> parent) {
    301                 }
    302             });
    303 
    304             LinearLayout certLayout = new LinearLayout(mActivity);
    305             certLayout.setOrientation(LinearLayout.VERTICAL);
    306             certLayout.addView(spinner);
    307             for (int i = 0; i < views.size(); ++i) {
    308                 View certificateView = views.get(i);
    309                 // Show first cert by default
    310                 certificateView.setVisibility(i == 0 ? View.VISIBLE : View.GONE);
    311                 certLayout.addView(certificateView);
    312             }
    313 
    314             return certLayout;
    315         }
    316 
    317         private static int getButtonConfirmation(CertHolder certHolder) {
    318             return certHolder.isSystemCert() ? ( certHolder.isDeleted()
    319                         ? R.string.trusted_credentials_enable_confirmation
    320                         : R.string.trusted_credentials_disable_confirmation )
    321                     : R.string.trusted_credentials_remove_confirmation;
    322         }
    323 
    324         private static int getButtonLabel(CertHolder certHolder) {
    325             return certHolder.isSystemCert() ? ( certHolder.isDeleted()
    326                         ? R.string.trusted_credentials_enable_label
    327                         : R.string.trusted_credentials_disable_label )
    328                     : R.string.trusted_credentials_remove_label;
    329         }
    330 
    331         /* Animation code */
    332         private void animateViewTransition(final View nextCertView) {
    333             animateOldContent(new Runnable() {
    334                 @Override
    335                 public void run() {
    336                     addAndAnimateNewContent(nextCertView);
    337                 }
    338             });
    339         }
    340 
    341         private void animateOldContent(Runnable callback) {
    342             // Fade out
    343             mCurrentCertLayout.animate()
    344                     .alpha(0)
    345                     .setDuration(OUT_DURATION_MS)
    346                     .setInterpolator(AnimationUtils.loadInterpolator(mActivity,
    347                             android.R.interpolator.fast_out_linear_in))
    348                     .withEndAction(callback)
    349                     .start();
    350         }
    351 
    352         private void addAndAnimateNewContent(View nextCertLayout) {
    353             mCurrentCertLayout = nextCertLayout;
    354             mRootContainer.removeAllViews();
    355             mRootContainer.addView(nextCertLayout);
    356 
    357             mRootContainer.addOnLayoutChangeListener( new View.OnLayoutChangeListener() {
    358                 @Override
    359                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
    360                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
    361                     mRootContainer.removeOnLayoutChangeListener(this);
    362 
    363                     // Animate slide in from the right
    364                     final int containerWidth = mRootContainer.getWidth();
    365                     mCurrentCertLayout.setTranslationX(containerWidth);
    366                     mCurrentCertLayout.animate()
    367                             .translationX(0)
    368                             .setInterpolator(AnimationUtils.loadInterpolator(mActivity,
    369                                     android.R.interpolator.linear_out_slow_in))
    370                             .setDuration(IN_DURATION_MS)
    371                             .start();
    372                 }
    373             });
    374         }
    375     }
    376 }
    377