/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings; import android.annotation.NonNull; import android.app.Activity; import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; import android.content.DialogInterface; import android.content.pm.UserInfo; import android.net.http.SslCertificate; import android.os.UserHandle; import android.os.UserManager; import android.view.View; import android.view.animation.AnimationUtils; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.LinearLayout; import android.widget.Spinner; import com.android.internal.widget.LockPatternUtils; import com.android.settings.TrustedCredentialsSettings.CertHolder; import com.android.settingslib.RestrictedLockUtils; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.function.IntConsumer; class TrustedCredentialsDialogBuilder extends AlertDialog.Builder { public interface DelegateInterface { List<X509Certificate> getX509CertsFromCertHolder(CertHolder certHolder); void removeOrInstallCert(CertHolder certHolder); boolean startConfirmCredentialIfNotConfirmed(int userId, IntConsumer onCredentialConfirmedListener); } private final DialogEventHandler mDialogEventHandler; public TrustedCredentialsDialogBuilder(Activity activity, DelegateInterface delegate) { super(activity); mDialogEventHandler = new DialogEventHandler(activity, delegate); initDefaultBuilderParams(); } public TrustedCredentialsDialogBuilder setCertHolder(CertHolder certHolder) { return setCertHolders(certHolder == null ? new CertHolder[0] : new CertHolder[]{certHolder}); } public TrustedCredentialsDialogBuilder setCertHolders(@NonNull CertHolder[] certHolders) { mDialogEventHandler.setCertHolders(certHolders); return this; } @Override public AlertDialog create() { AlertDialog dialog = super.create(); dialog.setOnShowListener(mDialogEventHandler); mDialogEventHandler.setDialog(dialog); return dialog; } private void initDefaultBuilderParams() { setTitle(com.android.internal.R.string.ssl_certificate); setView(mDialogEventHandler.mRootContainer); // Enable buttons here. The actual labels and listeners are configured in nextOrDismiss setPositiveButton(R.string.trusted_credentials_trust_label, null); setNegativeButton(android.R.string.ok, null); } private static class DialogEventHandler implements DialogInterface.OnShowListener, View.OnClickListener { private static final long OUT_DURATION_MS = 300; private static final long IN_DURATION_MS = 200; private final Activity mActivity; private final DevicePolicyManager mDpm; private final UserManager mUserManager; private final DelegateInterface mDelegate; private final LinearLayout mRootContainer; private int mCurrentCertIndex = -1; private AlertDialog mDialog; private Button mPositiveButton; private Button mNegativeButton; private boolean mNeedsApproval; private CertHolder[] mCertHolders = new CertHolder[0]; private View mCurrentCertLayout = null; public DialogEventHandler(Activity activity, DelegateInterface delegate) { mActivity = activity; mDpm = activity.getSystemService(DevicePolicyManager.class); mUserManager = activity.getSystemService(UserManager.class); mDelegate = delegate; mRootContainer = new LinearLayout(mActivity); mRootContainer.setOrientation(LinearLayout.VERTICAL); } public void setDialog(AlertDialog dialog) { mDialog = dialog; } public void setCertHolders(CertHolder[] certHolder) { mCertHolders = certHolder; } @Override public void onShow(DialogInterface dialogInterface) { // Config the display content only when the dialog is shown because the // positive/negative buttons don't exist until the dialog is shown nextOrDismiss(); } @Override public void onClick(View view) { if (view == mPositiveButton) { if (mNeedsApproval) { onClickTrust(); } else { onClickOk(); } } else if (view == mNegativeButton) { onClickRemove(); } } private void onClickOk() { nextOrDismiss(); } private void onClickTrust() { CertHolder certHolder = getCurrentCertInfo(); if (!mDelegate.startConfirmCredentialIfNotConfirmed(certHolder.getUserId(), this::onCredentialConfirmed)) { mDpm.approveCaCert(certHolder.getAlias(), certHolder.getUserId(), true); nextOrDismiss(); } } private void onClickRemove() { final CertHolder certHolder = getCurrentCertInfo(); new AlertDialog.Builder(mActivity) .setMessage(getButtonConfirmation(certHolder)) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { mDelegate.removeOrInstallCert(certHolder); dialog.dismiss(); nextOrDismiss(); } }) .setNegativeButton(android.R.string.no, null) .show(); } private void onCredentialConfirmed(int userId) { if (mDialog.isShowing() && mNeedsApproval && getCurrentCertInfo() != null && getCurrentCertInfo().getUserId() == userId) { // Treat it as user just clicks "trust" for this cert onClickTrust(); } } private CertHolder getCurrentCertInfo() { return mCurrentCertIndex < mCertHolders.length ? mCertHolders[mCurrentCertIndex] : null; } private void nextOrDismiss() { mCurrentCertIndex++; // find next non-null cert or dismiss while (mCurrentCertIndex < mCertHolders.length && getCurrentCertInfo() == null) { mCurrentCertIndex++; } if (mCurrentCertIndex >= mCertHolders.length) { mDialog.dismiss(); return; } updateViewContainer(); updatePositiveButton(); updateNegativeButton(); } /** * @return true if current user or parent user is guarded by screenlock */ private boolean isUserSecure(int userId) { final LockPatternUtils lockPatternUtils = new LockPatternUtils(mActivity); if (lockPatternUtils.isSecure(userId)) { return true; } UserInfo parentUser = mUserManager.getProfileParent(userId); if (parentUser == null) { return false; } return lockPatternUtils.isSecure(parentUser.id); } private void updatePositiveButton() { final CertHolder certHolder = getCurrentCertInfo(); mNeedsApproval = !certHolder.isSystemCert() && isUserSecure(certHolder.getUserId()) && !mDpm.isCaCertApproved(certHolder.getAlias(), certHolder.getUserId()); final boolean isProfileOrDeviceOwner = RestrictedLockUtils.getProfileOrDeviceOwner( mActivity, certHolder.getUserId()) != null; // Show trust button only when it requires consumer user (non-PO/DO) to approve CharSequence displayText = mActivity.getText(!isProfileOrDeviceOwner && mNeedsApproval ? R.string.trusted_credentials_trust_label : android.R.string.ok); mPositiveButton = updateButton(DialogInterface.BUTTON_POSITIVE, displayText); } private void updateNegativeButton() { final CertHolder certHolder = getCurrentCertInfo(); final boolean showRemoveButton = !mUserManager.hasUserRestriction( UserManager.DISALLOW_CONFIG_CREDENTIALS, new UserHandle(certHolder.getUserId())); CharSequence displayText = mActivity.getText(getButtonLabel(certHolder)); mNegativeButton = updateButton(DialogInterface.BUTTON_NEGATIVE, displayText); mNegativeButton.setVisibility(showRemoveButton ? View.VISIBLE : View.GONE); } /** * mDialog.setButton doesn't trigger text refresh since mDialog has been shown. * It's invoked only in case mDialog is refreshed. * setOnClickListener is invoked to avoid dismiss dialog onClick */ private Button updateButton(int buttonType, CharSequence displayText) { mDialog.setButton(buttonType, displayText, (DialogInterface.OnClickListener) null); Button button = mDialog.getButton(buttonType); button.setText(displayText); button.setOnClickListener(this); return button; } private void updateViewContainer() { CertHolder certHolder = getCurrentCertInfo(); LinearLayout nextCertLayout = getCertLayout(certHolder); // Displaying first cert doesn't require animation if (mCurrentCertLayout == null) { mCurrentCertLayout = nextCertLayout; mRootContainer.addView(mCurrentCertLayout); } else { animateViewTransition(nextCertLayout); } } private LinearLayout getCertLayout(final CertHolder certHolder) { final ArrayList<View> views = new ArrayList<View>(); final ArrayList<String> titles = new ArrayList<String>(); List<X509Certificate> certificates = mDelegate.getX509CertsFromCertHolder(certHolder); if (certificates != null) { for (X509Certificate certificate : certificates) { SslCertificate sslCert = new SslCertificate(certificate); views.add(sslCert.inflateCertificateView(mActivity)); titles.add(sslCert.getIssuedTo().getCName()); } } ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(mActivity, android.R.layout.simple_spinner_item, titles); arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); Spinner spinner = new Spinner(mActivity); spinner.setAdapter(arrayAdapter); spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { for (int i = 0; i < views.size(); i++) { views.get(i).setVisibility(i == position ? View.VISIBLE : View.GONE); } } @Override public void onNothingSelected(AdapterView<?> parent) { } }); LinearLayout certLayout = new LinearLayout(mActivity); certLayout.setOrientation(LinearLayout.VERTICAL); certLayout.addView(spinner); for (int i = 0; i < views.size(); ++i) { View certificateView = views.get(i); // Show first cert by default certificateView.setVisibility(i == 0 ? View.VISIBLE : View.GONE); certLayout.addView(certificateView); } return certLayout; } private static int getButtonConfirmation(CertHolder certHolder) { return certHolder.isSystemCert() ? ( certHolder.isDeleted() ? R.string.trusted_credentials_enable_confirmation : R.string.trusted_credentials_disable_confirmation ) : R.string.trusted_credentials_remove_confirmation; } private static int getButtonLabel(CertHolder certHolder) { return certHolder.isSystemCert() ? ( certHolder.isDeleted() ? R.string.trusted_credentials_enable_label : R.string.trusted_credentials_disable_label ) : R.string.trusted_credentials_remove_label; } /* Animation code */ private void animateViewTransition(final View nextCertView) { animateOldContent(new Runnable() { @Override public void run() { addAndAnimateNewContent(nextCertView); } }); } private void animateOldContent(Runnable callback) { // Fade out mCurrentCertLayout.animate() .alpha(0) .setDuration(OUT_DURATION_MS) .setInterpolator(AnimationUtils.loadInterpolator(mActivity, android.R.interpolator.fast_out_linear_in)) .withEndAction(callback) .start(); } private void addAndAnimateNewContent(View nextCertLayout) { mCurrentCertLayout = nextCertLayout; mRootContainer.removeAllViews(); mRootContainer.addView(nextCertLayout); mRootContainer.addOnLayoutChangeListener( new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { mRootContainer.removeOnLayoutChangeListener(this); // Animate slide in from the right final int containerWidth = mRootContainer.getWidth(); mCurrentCertLayout.setTranslationX(containerWidth); mCurrentCertLayout.animate() .translationX(0) .setInterpolator(AnimationUtils.loadInterpolator(mActivity, android.R.interpolator.linear_out_slow_in)) .setDuration(IN_DURATION_MS) .start(); } }); } } }