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 onClickEnableOrDisable(); 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 onClickEnableOrDisable() { 159 final CertHolder certHolder = getCurrentCertInfo(); 160 DialogInterface.OnClickListener onConfirm = new DialogInterface.OnClickListener() { 161 @Override 162 public void onClick(DialogInterface dialog, int id) { 163 mDelegate.removeOrInstallCert(certHolder); 164 nextOrDismiss(); 165 } 166 }; 167 if (certHolder.isSystemCert()) { 168 // Removing system certs is reversible, so skip confirmation. 169 onConfirm.onClick(null, -1); 170 } else { 171 new AlertDialog.Builder(mActivity) 172 .setMessage(R.string.trusted_credentials_remove_confirmation) 173 .setPositiveButton(android.R.string.yes, onConfirm) 174 .setNegativeButton(android.R.string.no, null) 175 .show(); 176 177 } 178 } 179 180 private void onCredentialConfirmed(int userId) { 181 if (mDialog.isShowing() && mNeedsApproval && getCurrentCertInfo() != null 182 && getCurrentCertInfo().getUserId() == userId) { 183 // Treat it as user just clicks "trust" for this cert 184 onClickTrust(); 185 } 186 } 187 188 private CertHolder getCurrentCertInfo() { 189 return mCurrentCertIndex < mCertHolders.length ? mCertHolders[mCurrentCertIndex] : null; 190 } 191 192 private void nextOrDismiss() { 193 mCurrentCertIndex++; 194 // find next non-null cert or dismiss 195 while (mCurrentCertIndex < mCertHolders.length && getCurrentCertInfo() == null) { 196 mCurrentCertIndex++; 197 } 198 199 if (mCurrentCertIndex >= mCertHolders.length) { 200 mDialog.dismiss(); 201 return; 202 } 203 204 updateViewContainer(); 205 updatePositiveButton(); 206 updateNegativeButton(); 207 } 208 209 /** 210 * @return true if current user or parent user is guarded by screenlock 211 */ 212 private boolean isUserSecure(int userId) { 213 final LockPatternUtils lockPatternUtils = new LockPatternUtils(mActivity); 214 if (lockPatternUtils.isSecure(userId)) { 215 return true; 216 } 217 UserInfo parentUser = mUserManager.getProfileParent(userId); 218 if (parentUser == null) { 219 return false; 220 } 221 return lockPatternUtils.isSecure(parentUser.id); 222 } 223 224 private void updatePositiveButton() { 225 final CertHolder certHolder = getCurrentCertInfo(); 226 mNeedsApproval = !certHolder.isSystemCert() 227 && isUserSecure(certHolder.getUserId()) 228 && !mDpm.isCaCertApproved(certHolder.getAlias(), certHolder.getUserId()); 229 230 final boolean isProfileOrDeviceOwner = RestrictedLockUtils.getProfileOrDeviceOwner( 231 mActivity, certHolder.getUserId()) != null; 232 233 // Show trust button only when it requires consumer user (non-PO/DO) to approve 234 CharSequence displayText = mActivity.getText(!isProfileOrDeviceOwner && mNeedsApproval 235 ? R.string.trusted_credentials_trust_label 236 : android.R.string.ok); 237 mPositiveButton = updateButton(DialogInterface.BUTTON_POSITIVE, displayText); 238 } 239 240 private void updateNegativeButton() { 241 final CertHolder certHolder = getCurrentCertInfo(); 242 final boolean showRemoveButton = !mUserManager.hasUserRestriction( 243 UserManager.DISALLOW_CONFIG_CREDENTIALS, 244 new UserHandle(certHolder.getUserId())); 245 CharSequence displayText = mActivity.getText(getButtonLabel(certHolder)); 246 mNegativeButton = updateButton(DialogInterface.BUTTON_NEGATIVE, displayText); 247 mNegativeButton.setVisibility(showRemoveButton ? View.VISIBLE : View.GONE); 248 } 249 250 /** 251 * mDialog.setButton doesn't trigger text refresh since mDialog has been shown. 252 * It's invoked only in case mDialog is refreshed. 253 * setOnClickListener is invoked to avoid dismiss dialog onClick 254 */ 255 private Button updateButton(int buttonType, CharSequence displayText) { 256 mDialog.setButton(buttonType, displayText, (DialogInterface.OnClickListener) null); 257 Button button = mDialog.getButton(buttonType); 258 button.setText(displayText); 259 button.setOnClickListener(this); 260 return button; 261 } 262 263 264 private void updateViewContainer() { 265 CertHolder certHolder = getCurrentCertInfo(); 266 LinearLayout nextCertLayout = getCertLayout(certHolder); 267 268 // Displaying first cert doesn't require animation 269 if (mCurrentCertLayout == null) { 270 mCurrentCertLayout = nextCertLayout; 271 mRootContainer.addView(mCurrentCertLayout); 272 } else { 273 animateViewTransition(nextCertLayout); 274 } 275 } 276 277 private LinearLayout getCertLayout(final CertHolder certHolder) { 278 final ArrayList<View> views = new ArrayList<View>(); 279 final ArrayList<String> titles = new ArrayList<String>(); 280 List<X509Certificate> certificates = mDelegate.getX509CertsFromCertHolder(certHolder); 281 if (certificates != null) { 282 for (X509Certificate certificate : certificates) { 283 SslCertificate sslCert = new SslCertificate(certificate); 284 views.add(sslCert.inflateCertificateView(mActivity)); 285 titles.add(sslCert.getIssuedTo().getCName()); 286 } 287 } 288 289 ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(mActivity, 290 android.R.layout.simple_spinner_item, 291 titles); 292 arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 293 Spinner spinner = new Spinner(mActivity); 294 spinner.setAdapter(arrayAdapter); 295 spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 296 @Override 297 public void onItemSelected(AdapterView<?> parent, View view, int position, 298 long id) { 299 for (int i = 0; i < views.size(); i++) { 300 views.get(i).setVisibility(i == position ? View.VISIBLE : View.GONE); 301 } 302 } 303 304 @Override 305 public void onNothingSelected(AdapterView<?> parent) { 306 } 307 }); 308 309 LinearLayout certLayout = new LinearLayout(mActivity); 310 certLayout.setOrientation(LinearLayout.VERTICAL); 311 certLayout.addView(spinner); 312 for (int i = 0; i < views.size(); ++i) { 313 View certificateView = views.get(i); 314 // Show first cert by default 315 certificateView.setVisibility(i == 0 ? View.VISIBLE : View.GONE); 316 certLayout.addView(certificateView); 317 } 318 319 return certLayout; 320 } 321 322 private static int getButtonLabel(CertHolder certHolder) { 323 return certHolder.isSystemCert() ? ( certHolder.isDeleted() 324 ? R.string.trusted_credentials_enable_label 325 : R.string.trusted_credentials_disable_label ) 326 : R.string.trusted_credentials_remove_label; 327 } 328 329 /* Animation code */ 330 private void animateViewTransition(final View nextCertView) { 331 animateOldContent(new Runnable() { 332 @Override 333 public void run() { 334 addAndAnimateNewContent(nextCertView); 335 } 336 }); 337 } 338 339 private void animateOldContent(Runnable callback) { 340 // Fade out 341 mCurrentCertLayout.animate() 342 .alpha(0) 343 .setDuration(OUT_DURATION_MS) 344 .setInterpolator(AnimationUtils.loadInterpolator(mActivity, 345 android.R.interpolator.fast_out_linear_in)) 346 .withEndAction(callback) 347 .start(); 348 } 349 350 private void addAndAnimateNewContent(View nextCertLayout) { 351 mCurrentCertLayout = nextCertLayout; 352 mRootContainer.removeAllViews(); 353 mRootContainer.addView(nextCertLayout); 354 355 mRootContainer.addOnLayoutChangeListener( new View.OnLayoutChangeListener() { 356 @Override 357 public void onLayoutChange(View v, int left, int top, int right, int bottom, 358 int oldLeft, int oldTop, int oldRight, int oldBottom) { 359 mRootContainer.removeOnLayoutChangeListener(this); 360 361 // Animate slide in from the right 362 final int containerWidth = mRootContainer.getWidth(); 363 mCurrentCertLayout.setTranslationX(containerWidth); 364 mCurrentCertLayout.animate() 365 .translationX(0) 366 .setInterpolator(AnimationUtils.loadInterpolator(mActivity, 367 android.R.interpolator.linear_out_slow_in)) 368 .setDuration(IN_DURATION_MS) 369 .start(); 370 } 371 }); 372 } 373 } 374 } 375