| /* | |
| * 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.app.Activity; | |
| 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 androidx.annotation.NonNull; | |
| import androidx.appcompat.app.AlertDialog; | |
| import com.android.internal.widget.LockPatternUtils; | |
| import com.android.settings.TrustedCredentialsFragment.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) { | |
| onClickEnableOrDisable(); | |
| } | |
| } | |
| 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 onClickEnableOrDisable() { | |
| final CertHolder certHolder = getCurrentCertInfo(); | |
| DialogInterface.OnClickListener onConfirm = new DialogInterface.OnClickListener() { | |
| @Override | |
| public void onClick(DialogInterface dialog, int id) { | |
| mDelegate.removeOrInstallCert(certHolder); | |
| nextOrDismiss(); | |
| } | |
| }; | |
| if (certHolder.isSystemCert()) { | |
| // Removing system certs is reversible, so skip confirmation. | |
| onConfirm.onClick(null, -1); | |
| } else { | |
| new AlertDialog.Builder(mActivity) | |
| .setMessage(R.string.trusted_credentials_remove_confirmation) | |
| .setPositiveButton(android.R.string.ok, onConfirm) | |
| .setNegativeButton(android.R.string.cancel, 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, UserHandle.of(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); | |
| // Prevent content overlapping with spinner | |
| certLayout.setClipChildren(true); | |
| 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 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(); | |
| } | |
| }); | |
| } | |
| } | |
| } |