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;
             }
         }