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