blob: 806da92099bbd3a3cf3216b77b1f1e23f98a400e [file] [log] [blame]
Victor Changd7d0e1b2016-04-05 20:01:24 +01001/*
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;
Victor Changd7d0e1b2016-04-05 20:01:24 +010020import android.app.admin.DevicePolicyManager;
21import android.content.DialogInterface;
Victor Changf2e02db2016-04-21 15:45:59 +010022import android.content.pm.UserInfo;
Victor Changd7d0e1b2016-04-05 20:01:24 +010023import android.net.http.SslCertificate;
24import android.os.UserHandle;
25import android.os.UserManager;
26import android.view.View;
27import android.view.animation.AnimationUtils;
28import android.widget.AdapterView;
29import android.widget.ArrayAdapter;
30import android.widget.Button;
31import android.widget.LinearLayout;
32import android.widget.Spinner;
33
Fan Zhang23f8d592018-08-28 15:11:40 -070034import androidx.appcompat.app.AlertDialog;
35
Victor Changf2e02db2016-04-21 15:45:59 +010036import com.android.internal.widget.LockPatternUtils;
Victor Changd7d0e1b2016-04-05 20:01:24 +010037import com.android.settings.TrustedCredentialsSettings.CertHolder;
Victor Chang71d1fed2016-05-31 22:07:20 +010038import com.android.settingslib.RestrictedLockUtils;
Victor Changd7d0e1b2016-04-05 20:01:24 +010039
40import java.security.cert.X509Certificate;
41import java.util.ArrayList;
42import java.util.List;
Victor Chang45ca9062016-05-23 19:47:38 +010043import java.util.function.IntConsumer;
Victor Changd7d0e1b2016-04-05 20:01:24 +010044
45class TrustedCredentialsDialogBuilder extends AlertDialog.Builder {
46 public interface DelegateInterface {
47 List<X509Certificate> getX509CertsFromCertHolder(CertHolder certHolder);
48 void removeOrInstallCert(CertHolder certHolder);
Victor Chang45ca9062016-05-23 19:47:38 +010049 boolean startConfirmCredentialIfNotConfirmed(int userId,
50 IntConsumer onCredentialConfirmedListener);
Victor Changd7d0e1b2016-04-05 20:01:24 +010051 }
52
53 private final DialogEventHandler mDialogEventHandler;
54
55 public TrustedCredentialsDialogBuilder(Activity activity, DelegateInterface delegate) {
56 super(activity);
57 mDialogEventHandler = new DialogEventHandler(activity, delegate);
58
59 initDefaultBuilderParams();
60 }
61
62 public TrustedCredentialsDialogBuilder setCertHolder(CertHolder certHolder) {
63 return setCertHolders(certHolder == null ? new CertHolder[0]
64 : new CertHolder[]{certHolder});
65 }
66
67 public TrustedCredentialsDialogBuilder setCertHolders(@NonNull CertHolder[] certHolders) {
68 mDialogEventHandler.setCertHolders(certHolders);
69 return this;
70 }
71
72 @Override
73 public AlertDialog create() {
74 AlertDialog dialog = super.create();
75 dialog.setOnShowListener(mDialogEventHandler);
76 mDialogEventHandler.setDialog(dialog);
77 return dialog;
78 }
79
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
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
118 public void setDialog(AlertDialog dialog) {
119 mDialog = dialog;
120 }
121
122 public void setCertHolders(CertHolder[] certHolder) {
123 mCertHolders = certHolder;
124 }
125
126 @Override
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
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) {
Robin Leebed85592016-09-01 18:35:00 +0100142 onClickEnableOrDisable();
Victor Changd7d0e1b2016-04-05 20:01:24 +0100143 }
144 }
145
146 private void onClickOk() {
147 nextOrDismiss();
148 }
149
150 private void onClickTrust() {
151 CertHolder certHolder = getCurrentCertInfo();
Victor Chang45ca9062016-05-23 19:47:38 +0100152 if (!mDelegate.startConfirmCredentialIfNotConfirmed(certHolder.getUserId(),
153 this::onCredentialConfirmed)) {
Victor Chang01f4dbc2016-05-09 16:13:10 +0100154 mDpm.approveCaCert(certHolder.getAlias(), certHolder.getUserId(), true);
155 nextOrDismiss();
156 }
Victor Changd7d0e1b2016-04-05 20:01:24 +0100157 }
158
Robin Leebed85592016-09-01 18:35:00 +0100159 private void onClickEnableOrDisable() {
Victor Changd7d0e1b2016-04-05 20:01:24 +0100160 final CertHolder certHolder = getCurrentCertInfo();
Robin Leebed85592016-09-01 18:35:00 +0100161 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 }
Victor Changd7d0e1b2016-04-05 20:01:24 +0100179 }
180
Victor Chang45ca9062016-05-23 19:47:38 +0100181 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
Victor Changd7d0e1b2016-04-05 20:01:24 +0100189 private CertHolder getCurrentCertInfo() {
190 return mCurrentCertIndex < mCertHolders.length ? mCertHolders[mCurrentCertIndex] : null;
191 }
192
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
Victor Changf2e02db2016-04-21 15:45:59 +0100210 /**
211 * @return true if current user or parent user is guarded by screenlock
212 */
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
Victor Changd7d0e1b2016-04-05 20:01:24 +0100225 private void updatePositiveButton() {
226 final CertHolder certHolder = getCurrentCertInfo();
Victor Changf2e02db2016-04-21 15:45:59 +0100227 mNeedsApproval = !certHolder.isSystemCert()
228 && isUserSecure(certHolder.getUserId())
229 && !mDpm.isCaCertApproved(certHolder.getAlias(), certHolder.getUserId());
Victor Changd7d0e1b2016-04-05 20:01:24 +0100230
Victor Chang71d1fed2016-05-31 22:07:20 +0100231 final boolean isProfileOrDeviceOwner = RestrictedLockUtils.getProfileOrDeviceOwner(
Philip P. Moltmann035ea3b2018-10-08 10:46:04 -0700232 mActivity, UserHandle.of(certHolder.getUserId())) != null;
Victor Chang71d1fed2016-05-31 22:07:20 +0100233
234 // Show trust button only when it requires consumer user (non-PO/DO) to approve
235 CharSequence displayText = mActivity.getText(!isProfileOrDeviceOwner && mNeedsApproval
Victor Changd7d0e1b2016-04-05 20:01:24 +0100236 ? R.string.trusted_credentials_trust_label
237 : android.R.string.ok);
238 mPositiveButton = updateButton(DialogInterface.BUTTON_POSITIVE, displayText);
239 }
240
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 */
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
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
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
Victor Changd7d0e1b2016-04-05 20:01:24 +0100323 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 */
331 private void animateViewTransition(final View nextCertView) {
332 animateOldContent(new Runnable() {
333 @Override
334 public void run() {
335 addAndAnimateNewContent(nextCertView);
336 }
337 });
338 }
339
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
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}