Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d7d0e1b6 authored by Victor Chang's avatar Victor Chang
Browse files

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
parent b49c901f
Loading
Loading
Loading
Loading
+342 −0
Original line number Diff line number Diff line
/*
 * 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();
                }
            });
        }
    }
}
+29 −128
Original line number Diff line number Diff line
@@ -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<CertHolder> {
    /* package */ static class CertHolder implements Comparable<CertHolder> {
        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<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) {
        new TrustedCredentialsDialogBuilder(getActivity(), this)
                .setCertHolder(certHolder)
                .show();
    }
        });

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

    private void addCertChain(final CertHolder certHolder,
            final ArrayList<View> views, final ArrayList<String> titles) {

    public List<X509Certificate> getX509CertsFromCertHolder(CertHolder certHolder) {
        List<X509Certificate> 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<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 @@ 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;
            }
        }