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