1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.settings;
17
18import android.annotation.NonNull;
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.app.admin.DevicePolicyManager;
22import android.content.DialogInterface;
23import android.content.pm.UserInfo;
24import android.net.http.SslCertificate;
25import android.os.UserHandle;
26import android.os.UserManager;
27import android.view.View;
28import android.view.animation.AnimationUtils;
29import android.widget.AdapterView;
30import android.widget.ArrayAdapter;
31import android.widget.Button;
32import android.widget.LinearLayout;
33import android.widget.Spinner;
34
35import com.android.internal.widget.LockPatternUtils;
36import com.android.settings.TrustedCredentialsSettings.CertHolder;
37import com.android.settingslib.RestrictedLockUtils;
38
39import java.security.cert.X509Certificate;
40import java.util.ArrayList;
41import java.util.List;
42import java.util.function.IntConsumer;
43
44class TrustedCredentialsDialogBuilder extends AlertDialog.Builder {
45    public interface DelegateInterface {
46        List<X509Certificate> getX509CertsFromCertHolder(CertHolder certHolder);
47        void removeOrInstallCert(CertHolder certHolder);
48        boolean startConfirmCredentialIfNotConfirmed(int userId,
49                IntConsumer onCredentialConfirmedListener);
50    }
51
52    private final DialogEventHandler mDialogEventHandler;
53
54    public TrustedCredentialsDialogBuilder(Activity activity, DelegateInterface delegate) {
55        super(activity);
56        mDialogEventHandler = new DialogEventHandler(activity, delegate);
57
58        initDefaultBuilderParams();
59    }
60
61    public TrustedCredentialsDialogBuilder setCertHolder(CertHolder certHolder) {
62        return setCertHolders(certHolder == null ? new CertHolder[0]
63                : new CertHolder[]{certHolder});
64    }
65
66    public TrustedCredentialsDialogBuilder setCertHolders(@NonNull CertHolder[] certHolders) {
67        mDialogEventHandler.setCertHolders(certHolders);
68        return this;
69    }
70
71    @Override
72    public AlertDialog create() {
73        AlertDialog dialog = super.create();
74        dialog.setOnShowListener(mDialogEventHandler);
75        mDialogEventHandler.setDialog(dialog);
76        return dialog;
77    }
78
79    private void initDefaultBuilderParams() {
80        setTitle(com.android.internal.R.string.ssl_certificate);
81        setView(mDialogEventHandler.mRootContainer);
82
83        // Enable buttons here. The actual labels and listeners are configured in nextOrDismiss
84        setPositiveButton(R.string.trusted_credentials_trust_label, null);
85        setNegativeButton(android.R.string.ok, null);
86    }
87
88    private static class DialogEventHandler implements DialogInterface.OnShowListener,
89            View.OnClickListener  {
90        private static final long OUT_DURATION_MS = 300;
91        private static final long IN_DURATION_MS = 200;
92
93        private final Activity mActivity;
94        private final DevicePolicyManager mDpm;
95        private final UserManager mUserManager;
96        private final DelegateInterface mDelegate;
97        private final LinearLayout mRootContainer;
98
99        private int mCurrentCertIndex = -1;
100        private AlertDialog mDialog;
101        private Button mPositiveButton;
102        private Button mNegativeButton;
103        private boolean mNeedsApproval;
104        private CertHolder[] mCertHolders = new CertHolder[0];
105        private View mCurrentCertLayout = null;
106
107        public DialogEventHandler(Activity activity, DelegateInterface delegate) {
108            mActivity = activity;
109            mDpm = activity.getSystemService(DevicePolicyManager.class);
110            mUserManager = activity.getSystemService(UserManager.class);
111            mDelegate = delegate;
112
113            mRootContainer = new LinearLayout(mActivity);
114            mRootContainer.setOrientation(LinearLayout.VERTICAL);
115        }
116
117        public void setDialog(AlertDialog dialog) {
118            mDialog = dialog;
119        }
120
121        public void setCertHolders(CertHolder[] certHolder) {
122            mCertHolders = certHolder;
123        }
124
125        @Override
126        public void onShow(DialogInterface dialogInterface) {
127            // Config the display content only when the dialog is shown because the
128            // positive/negative buttons don't exist until the dialog is shown
129            nextOrDismiss();
130        }
131
132        @Override
133        public void onClick(View view) {
134            if (view == mPositiveButton) {
135                if (mNeedsApproval) {
136                    onClickTrust();
137                } else {
138                    onClickOk();
139                }
140            } else if (view == mNegativeButton) {
141                onClickRemove();
142            }
143        }
144
145        private void onClickOk() {
146            nextOrDismiss();
147        }
148
149        private void onClickTrust() {
150            CertHolder certHolder = getCurrentCertInfo();
151            if (!mDelegate.startConfirmCredentialIfNotConfirmed(certHolder.getUserId(),
152                    this::onCredentialConfirmed)) {
153                mDpm.approveCaCert(certHolder.getAlias(), certHolder.getUserId(), true);
154                nextOrDismiss();
155            }
156        }
157
158        private void onClickRemove() {
159            final CertHolder certHolder = getCurrentCertInfo();
160            new AlertDialog.Builder(mActivity)
161                    .setMessage(getButtonConfirmation(certHolder))
162                    .setPositiveButton(android.R.string.yes,
163                            new DialogInterface.OnClickListener() {
164                                @Override
165                                public void onClick(DialogInterface dialog, int id) {
166                                    mDelegate.removeOrInstallCert(certHolder);
167                                    dialog.dismiss();
168                                    nextOrDismiss();
169                                }
170                            })
171                    .setNegativeButton(android.R.string.no, null)
172                    .show();
173        }
174
175        private void onCredentialConfirmed(int userId) {
176            if (mDialog.isShowing() && mNeedsApproval && getCurrentCertInfo() != null
177                    && getCurrentCertInfo().getUserId() == userId) {
178                // Treat it as user just clicks "trust" for this cert
179                onClickTrust();
180            }
181        }
182
183        private CertHolder getCurrentCertInfo() {
184            return mCurrentCertIndex < mCertHolders.length ? mCertHolders[mCurrentCertIndex] : null;
185        }
186
187        private void nextOrDismiss() {
188            mCurrentCertIndex++;
189            // find next non-null cert or dismiss
190            while (mCurrentCertIndex < mCertHolders.length && getCurrentCertInfo() == null) {
191                mCurrentCertIndex++;
192            }
193
194            if (mCurrentCertIndex >= mCertHolders.length) {
195                mDialog.dismiss();
196                return;
197            }
198
199            updateViewContainer();
200            updatePositiveButton();
201            updateNegativeButton();
202        }
203
204        /**
205         * @return true if current user or parent user is guarded by screenlock
206         */
207        private boolean isUserSecure(int userId) {
208            final LockPatternUtils lockPatternUtils = new LockPatternUtils(mActivity);
209            if (lockPatternUtils.isSecure(userId)) {
210                return true;
211            }
212            UserInfo parentUser = mUserManager.getProfileParent(userId);
213            if (parentUser == null) {
214                return false;
215            }
216            return lockPatternUtils.isSecure(parentUser.id);
217        }
218
219        private void updatePositiveButton() {
220            final CertHolder certHolder = getCurrentCertInfo();
221            mNeedsApproval = !certHolder.isSystemCert()
222                    && isUserSecure(certHolder.getUserId())
223                    && !mDpm.isCaCertApproved(certHolder.getAlias(), certHolder.getUserId());
224
225            final boolean isProfileOrDeviceOwner = RestrictedLockUtils.getProfileOrDeviceOwner(
226                    mActivity, certHolder.getUserId()) != null;
227
228            // Show trust button only when it requires consumer user (non-PO/DO) to approve
229            CharSequence displayText = mActivity.getText(!isProfileOrDeviceOwner && mNeedsApproval
230                    ? R.string.trusted_credentials_trust_label
231                    : android.R.string.ok);
232            mPositiveButton = updateButton(DialogInterface.BUTTON_POSITIVE, displayText);
233        }
234
235        private void updateNegativeButton() {
236            final CertHolder certHolder = getCurrentCertInfo();
237            final boolean showRemoveButton = !mUserManager.hasUserRestriction(
238                    UserManager.DISALLOW_CONFIG_CREDENTIALS,
239                    new UserHandle(certHolder.getUserId()));
240            CharSequence displayText = mActivity.getText(getButtonLabel(certHolder));
241            mNegativeButton = updateButton(DialogInterface.BUTTON_NEGATIVE, displayText);
242            mNegativeButton.setVisibility(showRemoveButton ? View.VISIBLE : View.GONE);
243        }
244
245        /**
246         * mDialog.setButton doesn't trigger text refresh since mDialog has been shown.
247         * It's invoked only in case mDialog is refreshed.
248         * setOnClickListener is invoked to avoid dismiss dialog onClick
249         */
250        private Button updateButton(int buttonType, CharSequence displayText) {
251            mDialog.setButton(buttonType, displayText, (DialogInterface.OnClickListener) null);
252            Button button = mDialog.getButton(buttonType);
253            button.setText(displayText);
254            button.setOnClickListener(this);
255            return button;
256        }
257
258
259        private void updateViewContainer() {
260            CertHolder certHolder = getCurrentCertInfo();
261            LinearLayout nextCertLayout = getCertLayout(certHolder);
262
263            // Displaying first cert doesn't require animation
264            if (mCurrentCertLayout == null) {
265                mCurrentCertLayout = nextCertLayout;
266                mRootContainer.addView(mCurrentCertLayout);
267            } else {
268                animateViewTransition(nextCertLayout);
269            }
270        }
271
272        private LinearLayout getCertLayout(final CertHolder certHolder) {
273            final ArrayList<View> views =  new ArrayList<View>();
274            final ArrayList<String> titles = new ArrayList<String>();
275            List<X509Certificate> certificates = mDelegate.getX509CertsFromCertHolder(certHolder);
276            if (certificates != null) {
277                for (X509Certificate certificate : certificates) {
278                    SslCertificate sslCert = new SslCertificate(certificate);
279                    views.add(sslCert.inflateCertificateView(mActivity));
280                    titles.add(sslCert.getIssuedTo().getCName());
281                }
282            }
283
284            ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(mActivity,
285                    android.R.layout.simple_spinner_item,
286                    titles);
287            arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
288            Spinner spinner = new Spinner(mActivity);
289            spinner.setAdapter(arrayAdapter);
290            spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
291                @Override
292                public void onItemSelected(AdapterView<?> parent, View view, int position,
293                        long id) {
294                    for (int i = 0; i < views.size(); i++) {
295                        views.get(i).setVisibility(i == position ? View.VISIBLE : View.GONE);
296                    }
297                }
298
299                @Override
300                public void onNothingSelected(AdapterView<?> parent) {
301                }
302            });
303
304            LinearLayout certLayout = new LinearLayout(mActivity);
305            certLayout.setOrientation(LinearLayout.VERTICAL);
306            certLayout.addView(spinner);
307            for (int i = 0; i < views.size(); ++i) {
308                View certificateView = views.get(i);
309                // Show first cert by default
310                certificateView.setVisibility(i == 0 ? View.VISIBLE : View.GONE);
311                certLayout.addView(certificateView);
312            }
313
314            return certLayout;
315        }
316
317        private static int getButtonConfirmation(CertHolder certHolder) {
318            return certHolder.isSystemCert() ? ( certHolder.isDeleted()
319                        ? R.string.trusted_credentials_enable_confirmation
320                        : R.string.trusted_credentials_disable_confirmation )
321                    : R.string.trusted_credentials_remove_confirmation;
322        }
323
324        private static int getButtonLabel(CertHolder certHolder) {
325            return certHolder.isSystemCert() ? ( certHolder.isDeleted()
326                        ? R.string.trusted_credentials_enable_label
327                        : R.string.trusted_credentials_disable_label )
328                    : R.string.trusted_credentials_remove_label;
329        }
330
331        /* Animation code */
332        private void animateViewTransition(final View nextCertView) {
333            animateOldContent(new Runnable() {
334                @Override
335                public void run() {
336                    addAndAnimateNewContent(nextCertView);
337                }
338            });
339        }
340
341        private void animateOldContent(Runnable callback) {
342            // Fade out
343            mCurrentCertLayout.animate()
344                    .alpha(0)
345                    .setDuration(OUT_DURATION_MS)
346                    .setInterpolator(AnimationUtils.loadInterpolator(mActivity,
347                            android.R.interpolator.fast_out_linear_in))
348                    .withEndAction(callback)
349                    .start();
350        }
351
352        private void addAndAnimateNewContent(View nextCertLayout) {
353            mCurrentCertLayout = nextCertLayout;
354            mRootContainer.removeAllViews();
355            mRootContainer.addView(nextCertLayout);
356
357            mRootContainer.addOnLayoutChangeListener( new View.OnLayoutChangeListener() {
358                @Override
359                public void onLayoutChange(View v, int left, int top, int right, int bottom,
360                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
361                    mRootContainer.removeOnLayoutChangeListener(this);
362
363                    // Animate slide in from the right
364                    final int containerWidth = mRootContainer.getWidth();
365                    mCurrentCertLayout.setTranslationX(containerWidth);
366                    mCurrentCertLayout.animate()
367                            .translationX(0)
368                            .setInterpolator(AnimationUtils.loadInterpolator(mActivity,
369                                    android.R.interpolator.linear_out_slow_in))
370                            .setDuration(IN_DURATION_MS)
371                            .start();
372                }
373            });
374        }
375    }
376}
377