From d7d0e1b6b0710d6bd73d5440e827d6e129b02f0c Mon Sep 17 00:00:00 2001 From: Victor Chang Date: Tue, 5 Apr 2016 20:01:24 +0100 Subject: [PATCH] CertDialog supports multiple certs - Allow user to trust multiple certs in chain in one AlertDialog - The animation is similar to GrantPermissionsViewHandlerImpl. - Fadeout current, Slide-in next cert from the right. - Not animate scale as the CustomeView in AlertDialog matchParent - Refactor CertDialogBuilder into a separate class - The change for config multiple cert into the dialog is another CL note: For single cert case when user taps on a system/user cert, no change is visible to user after this change Bug: 18224038 Change-Id: I09ee8f683031c800830af4001582882d61cd4974 --- .../settings/TrustedCredentialsDialogBuilder.java | 342 +++++++++++++++++++++ .../settings/TrustedCredentialsSettings.java | 157 ++-------- 2 files changed, 371 insertions(+), 128 deletions(-) create mode 100644 src/com/android/settings/TrustedCredentialsDialogBuilder.java diff --git a/src/com/android/settings/TrustedCredentialsDialogBuilder.java b/src/com/android/settings/TrustedCredentialsDialogBuilder.java new file mode 100644 index 0000000000..22dc93674d --- /dev/null +++ b/src/com/android/settings/TrustedCredentialsDialogBuilder.java @@ -0,0 +1,342 @@ +/* + * 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.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.settings.TrustedCredentialsSettings.CertHolder; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +class TrustedCredentialsDialogBuilder extends AlertDialog.Builder { + public interface DelegateInterface { + List getX509CertsFromCertHolder(CertHolder certHolder); + void removeOrInstallCert(CertHolder certHolder); + } + + 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(); + 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 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(); + } + + private void updatePositiveButton() { + final CertHolder certHolder = getCurrentCertInfo(); + mNeedsApproval = !certHolder.isSystemCert() && + !mDpm.isCaCertApproved(certHolder.getAlias(), certHolder.getUserId()); + + // The ok button is optional. User can still dismiss the dialog by other means. + // Display it only when trust button is not displayed, because we want users to + // either remove or trust a CA cert when the cert is installed by DPC app. + CharSequence displayText = mActivity.getText(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 views = new ArrayList(); + final ArrayList titles = new ArrayList(); + List 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 arrayAdapter = new ArrayAdapter(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(); + } + }); + } + } +} diff --git a/src/com/android/settings/TrustedCredentialsSettings.java b/src/com/android/settings/TrustedCredentialsSettings.java index 5e0aea7ed9..15135713fb 100644 --- a/src/com/android/settings/TrustedCredentialsSettings.java +++ b/src/com/android/settings/TrustedCredentialsSettings.java @@ -16,13 +16,9 @@ package com.android.settings; -import android.app.AlertDialog; -import android.app.Dialog; import android.app.KeyguardManager; -import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.UserInfo; @@ -41,16 +37,11 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.ArrayAdapter; import android.widget.BaseAdapter; import android.widget.BaseExpandableListAdapter; -import android.widget.Button; import android.widget.ExpandableListView; -import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ProgressBar; -import android.widget.Spinner; import android.widget.Switch; import android.widget.TabHost; import android.widget.TextView; @@ -67,7 +58,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; -public class TrustedCredentialsSettings extends OptionsMenuFragment { +public class TrustedCredentialsSettings extends OptionsMenuFragment + implements TrustedCredentialsDialogBuilder.DelegateInterface { private static final String TAG = "TrustedCredentialsSettings"; @@ -135,30 +127,6 @@ public class TrustedCredentialsSettings extends OptionsMenuFragment { } throw new AssertionError(); } - private int getButtonLabel(CertHolder certHolder) { - switch (this) { - case SYSTEM: - if (certHolder.mDeleted) { - return R.string.trusted_credentials_enable_label; - } - return R.string.trusted_credentials_disable_label; - case USER: - return R.string.trusted_credentials_remove_label; - } - throw new AssertionError(); - } - private int getButtonConfirmation(CertHolder certHolder) { - switch (this) { - case SYSTEM: - if (certHolder.mDeleted) { - return R.string.trusted_credentials_enable_confirmation; - } - return R.string.trusted_credentials_disable_confirmation; - case USER: - return R.string.trusted_credentials_remove_confirmation; - } - throw new AssertionError(); - } private void postOperationUpdate(boolean ok, CertHolder certHolder) { if (ok) { if (certHolder.mTab.mSwitch) { @@ -603,7 +571,7 @@ public class TrustedCredentialsSettings extends OptionsMenuFragment { } } - private static class CertHolder implements Comparable { + /* package */ static class CertHolder implements Comparable { public int mProfileId; private final IKeyChainService mService; private final TrustedCertificateAdapterCommons mAdapter; @@ -679,6 +647,22 @@ public class TrustedCredentialsSettings extends OptionsMenuFragment { @Override public int hashCode() { return mAlias.hashCode(); } + + public int getUserId() { + return mProfileId; + } + + public String getAlias() { + return mAlias; + } + + public boolean isSystemCert() { + return mTab == Tab.SYSTEM; + } + + public boolean isDeleted() { + return mDeleted; + } } private View getViewForCertificate(CertHolder certHolder, Tab mTab, View convertView, @@ -717,90 +701,13 @@ public class TrustedCredentialsSettings extends OptionsMenuFragment { } private void showCertDialog(final CertHolder certHolder) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(com.android.internal.R.string.ssl_certificate); - - final DevicePolicyManager dpm = getActivity().getSystemService(DevicePolicyManager.class); - final ArrayList views = new ArrayList(); - final ArrayList titles = new ArrayList(); - addCertChain(certHolder, views, titles); - - ArrayAdapter arrayAdapter = new ArrayAdapter(getActivity(), - android.R.layout.simple_spinner_item, - titles); - arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - Spinner spinner = new Spinner(getActivity()); - spinner.setAdapter(arrayAdapter); - spinner.setOnItemSelectedListener(new 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 container = new LinearLayout(getActivity()); - container.setOrientation(LinearLayout.VERTICAL); - container.addView(spinner); - for (int i = 0; i < views.size(); ++i) { - View certificateView = views.get(i); - if (i != 0) { - certificateView.setVisibility(View.GONE); - } - container.addView(certificateView); - } - builder.setView(container); - - if (certHolder.mTab == Tab.USER && - !dpm.isCaCertApproved(certHolder.mAlias, certHolder.mProfileId)) { - builder.setPositiveButton(R.string.trusted_credentials_trust_label, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - dpm.approveCaCert(certHolder.mAlias, certHolder.mProfileId, true); - } - }); - } else { - // The ok button is optional. Display it only when trust button is not displayed. - // User can still dismiss the dialog by other means. - builder.setPositiveButton(android.R.string.ok, null); - } - - if (!mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS, - new UserHandle(certHolder.mProfileId))) { - builder.setNegativeButton(certHolder.mTab.getButtonLabel(certHolder), - new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface parentDialog, int i) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setMessage(certHolder.mTab.getButtonConfirmation(certHolder)); - builder.setPositiveButton(android.R.string.yes, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - new AliasOperation(certHolder).execute(); - dialog.dismiss(); - parentDialog.dismiss(); - } - }); - builder.setNegativeButton(android.R.string.no, null); - AlertDialog alert = builder.create(); - alert.show(); - } - }); - } - - builder.show(); + new TrustedCredentialsDialogBuilder(getActivity(), this) + .setCertHolder(certHolder) + .show(); } - private void addCertChain(final CertHolder certHolder, - final ArrayList views, final ArrayList titles) { - + @Override + public List getX509CertsFromCertHolder(CertHolder certHolder) { List certificates = null; try { KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( @@ -817,18 +724,13 @@ public class TrustedCredentialsSettings extends OptionsMenuFragment { } catch (RemoteException ex) { Log.e(TAG, "RemoteException while retrieving certificate chain for root " + certHolder.mAlias, ex); - return; - } - for (X509Certificate certificate : certificates) { - addCertDetails(certificate, views, titles); } + return certificates; } - private void addCertDetails(X509Certificate certificate, final ArrayList views, - final ArrayList titles) { - SslCertificate sslCert = new SslCertificate(certificate); - views.add(sslCert.inflateCertificateView(getActivity())); - titles.add(sslCert.getIssuedTo().getCName()); + @Override + public void removeOrInstallCert(CertHolder certHolder) { + new AliasOperation(certHolder).execute(); } private class AliasOperation extends AsyncTask { @@ -854,8 +756,7 @@ public class TrustedCredentialsSettings extends OptionsMenuFragment { } } catch (CertificateEncodingException | SecurityException | IllegalStateException | RemoteException e) { - Log.w(TAG, "Error while toggling alias " + mCertHolder.mAlias, - e); + Log.w(TAG, "Error while toggling alias " + mCertHolder.mAlias, e); return false; } } -- 2.11.0