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
diff --git a/src/com/android/settings/TrustedCredentialsDialogBuilder.java b/src/com/android/settings/TrustedCredentialsDialogBuilder.java
new file mode 100644
index 0000000..22dc936
--- /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<X509Certificate> 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<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();
+ }
+ });
+ }
+ }
+}
diff --git a/src/com/android/settings/TrustedCredentialsSettings.java b/src/com/android/settings/TrustedCredentialsSettings.java
index 5e0aea7..1513571 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.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.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 @@
}
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 @@
}
}
- private static class CertHolder implements Comparable<CertHolder> {
+ /* package */ static class CertHolder implements Comparable<CertHolder> {
public int mProfileId;
private final IKeyChainService mService;
private final TrustedCertificateAdapterCommons mAdapter;
@@ -679,6 +647,22 @@
@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 @@
}
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<View> views = new ArrayList<View>();
- final ArrayList<String> titles = new ArrayList<String>();
- addCertChain(certHolder, views, titles);
-
- ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(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<View> views, final ArrayList<String> titles) {
-
+ @Override
+ public List<X509Certificate> getX509CertsFromCertHolder(CertHolder certHolder) {
List<X509Certificate> certificates = null;
try {
KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get(
@@ -817,18 +724,13 @@
} 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<View> views,
- final ArrayList<String> 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<Void, Void, Boolean> {
@@ -854,8 +756,7 @@
}
} 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;
}
}