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