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  */
16 package com.android.settings;
17 
18 import android.annotation.NonNull;
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.admin.DevicePolicyManager;
22 import android.content.DialogInterface;
23 import android.content.pm.UserInfo;
24 import android.net.http.SslCertificate;
25 import android.os.UserHandle;
26 import android.os.UserManager;
27 import android.view.View;
28 import android.view.animation.AnimationUtils;
29 import android.widget.AdapterView;
30 import android.widget.ArrayAdapter;
31 import android.widget.Button;
32 import android.widget.LinearLayout;
33 import android.widget.Spinner;
34 
35 import com.android.internal.widget.LockPatternUtils;
36 import com.android.settings.TrustedCredentialsSettings.CertHolder;
37 import com.android.settingslib.RestrictedLockUtils;
38 
39 import java.security.cert.X509Certificate;
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.function.IntConsumer;
43 
44 class TrustedCredentialsDialogBuilder extends AlertDialog.Builder {
45     public interface DelegateInterface {
getX509CertsFromCertHolder(CertHolder certHolder)46         List<X509Certificate> getX509CertsFromCertHolder(CertHolder certHolder);
removeOrInstallCert(CertHolder certHolder)47         void removeOrInstallCert(CertHolder certHolder);
startConfirmCredentialIfNotConfirmed(int userId, IntConsumer onCredentialConfirmedListener)48         boolean startConfirmCredentialIfNotConfirmed(int userId,
49                 IntConsumer onCredentialConfirmedListener);
50     }
51 
52     private final DialogEventHandler mDialogEventHandler;
53 
TrustedCredentialsDialogBuilder(Activity activity, DelegateInterface delegate)54     public TrustedCredentialsDialogBuilder(Activity activity, DelegateInterface delegate) {
55         super(activity);
56         mDialogEventHandler = new DialogEventHandler(activity, delegate);
57 
58         initDefaultBuilderParams();
59     }
60 
setCertHolder(CertHolder certHolder)61     public TrustedCredentialsDialogBuilder setCertHolder(CertHolder certHolder) {
62         return setCertHolders(certHolder == null ? new CertHolder[0]
63                 : new CertHolder[]{certHolder});
64     }
65 
setCertHolders(@onNull CertHolder[] certHolders)66     public TrustedCredentialsDialogBuilder setCertHolders(@NonNull CertHolder[] certHolders) {
67         mDialogEventHandler.setCertHolders(certHolders);
68         return this;
69     }
70 
71     @Override
create()72     public AlertDialog create() {
73         AlertDialog dialog = super.create();
74         dialog.setOnShowListener(mDialogEventHandler);
75         mDialogEventHandler.setDialog(dialog);
76         return dialog;
77     }
78 
initDefaultBuilderParams()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 
DialogEventHandler(Activity activity, DelegateInterface delegate)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 
setDialog(AlertDialog dialog)117         public void setDialog(AlertDialog dialog) {
118             mDialog = dialog;
119         }
120 
setCertHolders(CertHolder[] certHolder)121         public void setCertHolders(CertHolder[] certHolder) {
122             mCertHolders = certHolder;
123         }
124 
125         @Override
onShow(DialogInterface dialogInterface)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
onClick(View view)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 
onClickOk()145         private void onClickOk() {
146             nextOrDismiss();
147         }
148 
onClickTrust()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 
onClickRemove()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 
onCredentialConfirmed(int userId)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 
getCurrentCertInfo()183         private CertHolder getCurrentCertInfo() {
184             return mCurrentCertIndex < mCertHolders.length ? mCertHolders[mCurrentCertIndex] : null;
185         }
186 
nextOrDismiss()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          */
isUserSecure(int userId)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 
updatePositiveButton()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 
updateNegativeButton()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          */
updateButton(int buttonType, CharSequence displayText)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 
updateViewContainer()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 
getCertLayout(final CertHolder certHolder)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 
getButtonConfirmation(CertHolder certHolder)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 
getButtonLabel(CertHolder certHolder)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 */
animateViewTransition(final View nextCertView)332         private void animateViewTransition(final View nextCertView) {
333             animateOldContent(new Runnable() {
334                 @Override
335                 public void run() {
336                     addAndAnimateNewContent(nextCertView);
337                 }
338             });
339         }
340 
animateOldContent(Runnable callback)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 
addAndAnimateNewContent(View nextCertLayout)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