2 * Copyright (C) 2011 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings;
19 import android.app.AlertDialog;
20 import android.app.Dialog;
21 import android.app.Fragment;
22 import android.content.Context;
23 import android.content.DialogInterface;
24 import android.content.pm.UserInfo;
25 import android.content.res.TypedArray;
26 import android.net.http.SslCertificate;
27 import android.os.AsyncTask;
28 import android.os.Bundle;
29 import android.os.RemoteException;
30 import android.os.UserHandle;
31 import android.os.UserManager;
32 import android.security.IKeyChainService;
33 import android.security.KeyChain;
34 import android.security.KeyChain.KeyChainConnection;
35 import android.util.SparseArray;
36 import android.util.Log;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.AdapterView;
41 import android.widget.AdapterView.OnItemSelectedListener;
42 import android.widget.ArrayAdapter;
43 import android.widget.BaseAdapter;
44 import android.widget.BaseExpandableListAdapter;
45 import android.widget.Button;
46 import android.widget.ExpandableListView;
47 import android.widget.LinearLayout;
48 import android.widget.ListView;
49 import android.widget.ProgressBar;
50 import android.widget.Spinner;
51 import android.widget.Switch;
52 import android.widget.TabHost;
53 import android.widget.TextView;
55 import com.android.internal.util.ParcelableString;
57 import java.security.cert.CertificateEncodingException;
58 import java.security.cert.X509Certificate;
59 import java.util.ArrayList;
60 import java.util.Collections;
61 import java.util.List;
62 import java.util.HashMap;
64 public class TrustedCredentialsSettings extends Fragment {
66 private static final String TAG = "TrustedCredentialsSettings";
68 private UserManager mUserManager;
70 private static final String USER_ACTION = "com.android.settings.TRUSTED_CREDENTIALS_USER";
74 R.string.trusted_credentials_system_tab,
78 R.id.system_expandable_list,
81 R.string.trusted_credentials_user_tab,
85 R.id.user_expandable_list,
88 private final String mTag;
89 private final int mLabel;
90 private final int mView;
91 private final int mProgress;
92 private final int mList;
93 private final int mExpandableList;
94 private final boolean mSwitch;
96 private Tab(String tag, int label, int view, int progress, int list, int expandableList,
101 mProgress = progress;
103 mExpandableList = expandableList;
104 mSwitch = withSwitch;
107 private List<ParcelableString> getAliases(IKeyChainService service) throws RemoteException {
110 return service.getSystemCaAliases().getList();
113 return service.getUserCaAliases().getList();
115 throw new AssertionError();
117 private boolean deleted(IKeyChainService service, String alias) throws RemoteException {
120 return !service.containsCaAlias(alias);
124 throw new AssertionError();
126 private int getButtonLabel(CertHolder certHolder) {
129 if (certHolder.mDeleted) {
130 return R.string.trusted_credentials_enable_label;
132 return R.string.trusted_credentials_disable_label;
134 return R.string.trusted_credentials_remove_label;
136 throw new AssertionError();
138 private int getButtonConfirmation(CertHolder certHolder) {
141 if (certHolder.mDeleted) {
142 return R.string.trusted_credentials_enable_confirmation;
144 return R.string.trusted_credentials_disable_confirmation;
146 return R.string.trusted_credentials_remove_confirmation;
148 throw new AssertionError();
150 private void postOperationUpdate(boolean ok, CertHolder certHolder) {
152 if (certHolder.mTab.mSwitch) {
153 certHolder.mDeleted = !certHolder.mDeleted;
155 certHolder.mAdapter.remove(certHolder);
157 certHolder.mAdapter.notifyDataSetChanged();
159 // bail, reload to reset to known state
160 certHolder.mAdapter.load();
165 private TabHost mTabHost;
166 private AliasOperation mAliasOperation;
167 private HashMap<Tab, AdapterData.AliasLoader>
168 mAliasLoaders = new HashMap<Tab, AdapterData.AliasLoader>(2);
169 private final SparseArray<KeyChainConnection>
170 mKeyChainConnectionByProfileId = new SparseArray<KeyChainConnection>();
173 public void onCreate(Bundle savedInstanceState) {
174 super.onCreate(savedInstanceState);
175 mUserManager = (UserManager) getActivity().getSystemService(Context.USER_SERVICE);
179 @Override public View onCreateView(
180 LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
181 mTabHost = (TabHost) inflater.inflate(R.layout.trusted_credentials, parent, false);
184 // TODO add Install button on Tab.USER to go to CertInstaller like KeyChainActivity
186 if (getActivity().getIntent() != null &&
187 USER_ACTION.equals(getActivity().getIntent().getAction())) {
188 mTabHost.setCurrentTabByTag(Tab.USER.mTag);
193 public void onDestroy() {
194 for (AdapterData.AliasLoader aliasLoader : mAliasLoaders.values()) {
195 aliasLoader.cancel(true);
197 if (mAliasOperation != null) {
198 mAliasOperation.cancel(true);
199 mAliasOperation = null;
201 closeKeyChainConnections();
205 private void closeKeyChainConnections() {
206 final int n = mKeyChainConnectionByProfileId.size();
207 for (int i = 0; i < n; ++i) {
208 mKeyChainConnectionByProfileId.valueAt(i).close();
210 mKeyChainConnectionByProfileId.clear();
213 private void addTab(Tab tab) {
214 TabHost.TabSpec systemSpec = mTabHost.newTabSpec(tab.mTag)
215 .setIndicator(getActivity().getString(tab.mLabel))
216 .setContent(tab.mView);
217 mTabHost.addTab(systemSpec);
219 if (mUserManager.getUserProfiles().size() > 1) {
220 ExpandableListView lv = (ExpandableListView) mTabHost.findViewById(tab.mExpandableList);
221 final TrustedCertificateExpandableAdapter adapter =
222 new TrustedCertificateExpandableAdapter(tab);
223 lv.setAdapter(adapter);
224 lv.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
226 public boolean onChildClick(ExpandableListView parent, View v,
227 int groupPosition, int childPosition, long id) {
228 showCertDialog(adapter.getChild(groupPosition, childPosition));
233 ListView lv = (ListView) mTabHost.findViewById(tab.mList);
234 final TrustedCertificateAdapter adapter = new TrustedCertificateAdapter(tab);
235 lv.setAdapter(adapter);
236 lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
237 @Override public void onItemClick(AdapterView<?> parent, View view,
239 showCertDialog(adapter.getItem(pos));
246 * Common interface for adapters of both expandable and non-expandable certificate lists.
248 private interface TrustedCertificateAdapterCommons {
250 * Remove a certificate from the list.
251 * @param certHolder the certificate to be removed.
253 void remove(CertHolder certHolder);
255 * Notify the adapter that the underlying data set has changed.
257 void notifyDataSetChanged();
259 * Load the certificates.
263 * Gets the identifier of the list view the adapter is connected to.
264 * @param tab the tab on which the list view resides.
265 * @return identifier of the list view.
267 int getListViewId(Tab tab);
271 * Adapter for expandable list view of certificates. Groups in the view correspond to profiles
272 * whereas children correspond to certificates.
274 private class TrustedCertificateExpandableAdapter extends BaseExpandableListAdapter implements
275 TrustedCertificateAdapterCommons {
276 private AdapterData mData;
278 private TrustedCertificateExpandableAdapter(Tab tab) {
279 mData = new AdapterData(tab, this);
283 public void remove(CertHolder certHolder) {
284 mData.remove(certHolder);
287 public int getGroupCount() {
288 return mData.mCertHoldersByUserId.size();
291 public int getChildrenCount(int groupPosition) {
292 List<CertHolder> certHolders = mData.mCertHoldersByUserId.valueAt(groupPosition);
293 if (certHolders != null) {
294 return certHolders.size();
299 public UserHandle getGroup(int groupPosition) {
300 return new UserHandle(mData.mCertHoldersByUserId.keyAt(groupPosition));
303 public CertHolder getChild(int groupPosition, int childPosition) {
304 return mData.mCertHoldersByUserId.valueAt(groupPosition).get(childPosition);
307 public long getGroupId(int groupPosition) {
308 return mData.mCertHoldersByUserId.keyAt(groupPosition);
311 public long getChildId(int groupPosition, int childPosition) {
312 return childPosition;
315 public boolean hasStableIds() {
319 public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
321 if (convertView == null) {
322 LayoutInflater inflater = (LayoutInflater) getActivity()
323 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
324 convertView = inflateCategoryHeader(inflater, parent);
327 final TextView title = (TextView) convertView.findViewById(android.R.id.title);
328 final UserHandle profile = getGroup(groupPosition);
329 final UserInfo userInfo = mUserManager.getUserInfo(profile.getIdentifier());
330 if (userInfo.isManagedProfile()) {
331 title.setText(R.string.category_work);
333 title.setText(R.string.category_personal);
335 title.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
340 public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
341 View convertView, ViewGroup parent) {
342 return getViewForCertificate(getChild(groupPosition, childPosition), mData.mTab,
343 convertView, parent);
346 public boolean isChildSelectable(int groupPosition, int childPosition) {
351 mData.new AliasLoader().execute();
354 public int getListViewId(Tab tab) {
355 return tab.mExpandableList;
357 private View inflateCategoryHeader(LayoutInflater inflater, ViewGroup parent) {
358 final TypedArray a = inflater.getContext().obtainStyledAttributes(null,
359 com.android.internal.R.styleable.Preference,
360 com.android.internal.R.attr.preferenceCategoryStyle, 0);
361 final int resId = a.getResourceId(com.android.internal.R.styleable.Preference_layout,
363 return inflater.inflate(resId, parent, false);
368 private class TrustedCertificateAdapter extends BaseAdapter implements
369 TrustedCertificateAdapterCommons {
370 private final AdapterData mData;
371 private TrustedCertificateAdapter(Tab tab) {
372 mData = new AdapterData(tab, this);
376 public void remove(CertHolder certHolder) {
377 mData.remove(certHolder);
380 public int getListViewId(Tab tab) {
385 mData.new AliasLoader().execute();
387 @Override public int getCount() {
388 List<CertHolder> certHolders = mData.mCertHoldersByUserId.valueAt(0);
389 if (certHolders != null) {
390 return certHolders.size();
394 @Override public CertHolder getItem(int position) {
395 return mData.mCertHoldersByUserId.valueAt(0).get(position);
397 @Override public long getItemId(int position) {
400 @Override public View getView(int position, View view, ViewGroup parent) {
401 return getViewForCertificate(getItem(position), mData.mTab, view, parent);
405 private class AdapterData {
406 private final SparseArray<List<CertHolder>> mCertHoldersByUserId =
407 new SparseArray<List<CertHolder>>();
408 private final Tab mTab;
409 private final TrustedCertificateAdapterCommons mAdapter;
411 private AdapterData(Tab tab, TrustedCertificateAdapterCommons adapter) {
416 private class AliasLoader extends AsyncTask<Void, Integer, SparseArray<List<CertHolder>>> {
417 private ProgressBar mProgressBar;
419 private Context mContext;
421 public AliasLoader() {
422 mContext = getActivity();
423 mAliasLoaders.put(mTab, this);
426 @Override protected void onPreExecute() {
427 View content = mTabHost.getTabContentView();
428 mProgressBar = (ProgressBar) content.findViewById(mTab.mProgress);
429 mList = content.findViewById(mAdapter.getListViewId(mTab));
430 mProgressBar.setVisibility(View.VISIBLE);
431 mList.setVisibility(View.GONE);
433 @Override protected SparseArray<List<CertHolder>> doInBackground(Void... params) {
434 SparseArray<List<CertHolder>> certHoldersByProfile =
435 new SparseArray<List<CertHolder>>();
437 List<UserHandle> profiles = mUserManager.getUserProfiles();
438 final int n = profiles.size();
439 // First we get all aliases for all profiles in order to show progress
440 // correctly. Otherwise this could all be in a single loop.
441 SparseArray<List<ParcelableString>> aliasesByProfileId = new SparseArray<
442 List<ParcelableString>>(n);
445 for (int i = 0; i < n; ++i) {
446 UserHandle profile = profiles.get(i);
447 int profileId = profile.getIdentifier();
448 KeyChainConnection keyChainConnection = KeyChain.bindAsUser(mContext,
450 // Saving the connection for later use on the certificate dialog.
451 mKeyChainConnectionByProfileId.put(profileId, keyChainConnection);
452 IKeyChainService service = keyChainConnection.getService();
453 List<ParcelableString> aliases = mTab.getAliases(service);
455 return new SparseArray<List<CertHolder>>();
457 max += aliases.size();
458 aliasesByProfileId.put(profileId, aliases);
460 for (int i = 0; i < n; ++i) {
461 UserHandle profile = profiles.get(i);
462 int profileId = profile.getIdentifier();
463 List<ParcelableString> aliases = aliasesByProfileId.get(profileId);
465 return new SparseArray<List<CertHolder>>();
467 IKeyChainService service = mKeyChainConnectionByProfileId.get(profileId)
469 List<CertHolder> certHolders = new ArrayList<CertHolder>(max);
470 final int aliasMax = aliases.size();
471 for (int j = 0; j < aliasMax; ++j) {
472 String alias = aliases.get(j).string;
473 byte[] encodedCertificate = service.getEncodedCaCertificate(alias,
475 X509Certificate cert = KeyChain.toCertificate(encodedCertificate);
476 certHolders.add(new CertHolder(service, mAdapter,
477 mTab, alias, cert, profileId));
478 publishProgress(++progress, max);
480 Collections.sort(certHolders);
481 certHoldersByProfile.put(profileId, certHolders);
483 return certHoldersByProfile;
484 } catch (RemoteException e) {
485 Log.e(TAG, "Remote exception while loading aliases.", e);
486 return new SparseArray<List<CertHolder>>();
487 } catch (InterruptedException e) {
488 Log.e(TAG, "InterruptedException while loading aliases.", e);
489 return new SparseArray<List<CertHolder>>();
492 @Override protected void onProgressUpdate(Integer... progressAndMax) {
493 int progress = progressAndMax[0];
494 int max = progressAndMax[1];
495 if (max != mProgressBar.getMax()) {
496 mProgressBar.setMax(max);
498 mProgressBar.setProgress(progress);
500 @Override protected void onPostExecute(SparseArray<List<CertHolder>> certHolders) {
501 mCertHoldersByUserId.clear();
502 final int n = certHolders.size();
503 for (int i = 0; i < n; ++i) {
504 mCertHoldersByUserId.put(certHolders.keyAt(i), certHolders.valueAt(i));
506 mAdapter.notifyDataSetChanged();
507 mProgressBar.setVisibility(View.GONE);
508 mList.setVisibility(View.VISIBLE);
509 mProgressBar.setProgress(0);
510 mAliasLoaders.remove(mTab);
514 public void remove(CertHolder certHolder) {
515 if (mCertHoldersByUserId != null) {
516 final List<CertHolder> certs = mCertHoldersByUserId.get(certHolder.mProfileId);
518 certs.remove(certHolder);
524 private static class CertHolder implements Comparable<CertHolder> {
525 public int mProfileId;
526 private final IKeyChainService mService;
527 private final TrustedCertificateAdapterCommons mAdapter;
528 private final Tab mTab;
529 private final String mAlias;
530 private final X509Certificate mX509Cert;
532 private final SslCertificate mSslCert;
533 private final String mSubjectPrimary;
534 private final String mSubjectSecondary;
535 private boolean mDeleted;
537 private CertHolder(IKeyChainService service,
538 TrustedCertificateAdapterCommons adapter,
541 X509Certificate x509Cert,
543 mProfileId = profileId;
548 mX509Cert = x509Cert;
550 mSslCert = new SslCertificate(x509Cert);
552 String cn = mSslCert.getIssuedTo().getCName();
553 String o = mSslCert.getIssuedTo().getOName();
554 String ou = mSslCert.getIssuedTo().getUName();
555 // if we have a O, use O as primary subject, secondary prefer CN over OU
556 // if we don't have an O, use CN as primary, empty secondary
557 // if we don't have O or CN, use DName as primary, empty secondary
561 mSubjectSecondary = cn;
564 mSubjectSecondary = ou;
568 mSubjectPrimary = cn;
569 mSubjectSecondary = "";
571 mSubjectPrimary = mSslCert.getIssuedTo().getDName();
572 mSubjectSecondary = "";
576 mDeleted = mTab.deleted(mService, mAlias);
577 } catch (RemoteException e) {
578 Log.e(TAG, "Remote exception while checking if alias " + mAlias + " is deleted.",
583 @Override public int compareTo(CertHolder o) {
584 int primary = this.mSubjectPrimary.compareToIgnoreCase(o.mSubjectPrimary);
588 return this.mSubjectSecondary.compareToIgnoreCase(o.mSubjectSecondary);
590 @Override public boolean equals(Object o) {
591 if (!(o instanceof CertHolder)) {
594 CertHolder other = (CertHolder) o;
595 return mAlias.equals(other.mAlias);
597 @Override public int hashCode() {
598 return mAlias.hashCode();
602 private View getViewForCertificate(CertHolder certHolder, Tab mTab, View convertView,
605 if (convertView == null) {
606 LayoutInflater inflater = LayoutInflater.from(getActivity());
607 convertView = inflater.inflate(R.layout.trusted_credential, parent, false);
608 holder = new ViewHolder();
609 holder.mSubjectPrimaryView = (TextView)
610 convertView.findViewById(R.id.trusted_credential_subject_primary);
611 holder.mSubjectSecondaryView = (TextView)
612 convertView.findViewById(R.id.trusted_credential_subject_secondary);
613 holder.mSwitch = (Switch) convertView.findViewById(
614 R.id.trusted_credential_status);
615 convertView.setTag(holder);
617 holder = (ViewHolder) convertView.getTag();
619 holder.mSubjectPrimaryView.setText(certHolder.mSubjectPrimary);
620 holder.mSubjectSecondaryView.setText(certHolder.mSubjectSecondary);
622 holder.mSwitch.setChecked(!certHolder.mDeleted);
623 holder.mSwitch.setEnabled(!mUserManager.hasUserRestriction(
624 UserManager.DISALLOW_CONFIG_CREDENTIALS,
625 new UserHandle(certHolder.mProfileId)));
626 holder.mSwitch.setVisibility(View.VISIBLE);
631 private static class ViewHolder {
632 private TextView mSubjectPrimaryView;
633 private TextView mSubjectSecondaryView;
634 private Switch mSwitch;
637 private void showCertDialog(final CertHolder certHolder) {
638 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
639 builder.setTitle(com.android.internal.R.string.ssl_certificate);
641 final ArrayList<View> views = new ArrayList<View>();
642 final ArrayList<String> titles = new ArrayList<String>();
643 addCertChain(certHolder, views, titles);
645 ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(getActivity(),
646 android.R.layout.simple_spinner_item,
648 arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
649 Spinner spinner = new Spinner(getActivity());
650 spinner.setAdapter(arrayAdapter);
651 spinner.setOnItemSelectedListener(new OnItemSelectedListener() {
653 public void onItemSelected(AdapterView<?> parent, View view, int position,
655 for(int i = 0; i < views.size(); i++) {
656 views.get(i).setVisibility(i == position ? View.VISIBLE : View.GONE);
660 public void onNothingSelected(AdapterView<?> parent) { }
663 LinearLayout container = new LinearLayout(getActivity());
664 container.setOrientation(LinearLayout.VERTICAL);
665 container.addView(spinner);
666 for (int i = 0; i < views.size(); ++i) {
667 View certificateView = views.get(i);
669 certificateView.setVisibility(View.GONE);
671 container.addView(certificateView);
673 builder.setView(container);
674 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
675 @Override public void onClick(DialogInterface dialog, int id) {
679 final Dialog certDialog = builder.create();
681 ViewGroup body = (ViewGroup) container.findViewById(com.android.internal.R.id.body);
682 LayoutInflater inflater = LayoutInflater.from(getActivity());
683 Button removeButton = (Button) inflater.inflate(R.layout.trusted_credential_details,
686 if (!mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS,
687 new UserHandle(certHolder.mProfileId))) {
688 body.addView(removeButton);
690 removeButton.setText(certHolder.mTab.getButtonLabel(certHolder));
691 removeButton.setOnClickListener(new View.OnClickListener() {
692 @Override public void onClick(View v) {
693 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
694 builder.setMessage(certHolder.mTab.getButtonConfirmation(certHolder));
695 builder.setPositiveButton(
696 android.R.string.yes, new DialogInterface.OnClickListener() {
697 @Override public void onClick(DialogInterface dialog, int id) {
698 new AliasOperation(certHolder).execute();
700 certDialog.dismiss();
703 builder.setNegativeButton(
704 android.R.string.no, new DialogInterface.OnClickListener() {
705 @Override public void onClick(DialogInterface dialog, int id) {
709 AlertDialog alert = builder.create();
717 private void addCertChain(final CertHolder certHolder,
718 final ArrayList<View> views, final ArrayList<String> titles) {
720 List<X509Certificate> certificates = null;
722 KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get(
723 certHolder.mProfileId);
724 IKeyChainService service = keyChainConnection.getService();
725 List<String> chain = service.getCaCertificateChainAliases(certHolder.mAlias, true);
726 final int n = chain.size();
727 certificates = new ArrayList<X509Certificate>(n);
728 for (int i = 0; i < n; ++i) {
729 byte[] encodedCertificate = service.getEncodedCaCertificate(chain.get(i), true);
730 X509Certificate certificate = KeyChain.toCertificate(encodedCertificate);
731 certificates.add(certificate);
733 } catch (RemoteException ex) {
734 Log.e(TAG, "RemoteException while retrieving certificate chain for root "
735 + certHolder.mAlias, ex);
738 for (X509Certificate certificate : certificates) {
739 addCertDetails(certificate, views, titles);
743 private void addCertDetails(X509Certificate certificate, final ArrayList<View> views,
744 final ArrayList<String> titles) {
745 SslCertificate sslCert = new SslCertificate(certificate);
746 views.add(sslCert.inflateCertificateView(getActivity()));
747 titles.add(sslCert.getIssuedTo().getCName());
750 private class AliasOperation extends AsyncTask<Void, Void, Boolean> {
751 private final CertHolder mCertHolder;
753 private AliasOperation(CertHolder certHolder) {
754 mCertHolder = certHolder;
755 mAliasOperation = this;
759 protected Boolean doInBackground(Void... params) {
761 KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get(
762 mCertHolder.mProfileId);
763 IKeyChainService service = keyChainConnection.getService();
764 if (mCertHolder.mDeleted) {
765 byte[] bytes = mCertHolder.mX509Cert.getEncoded();
766 service.installCaCertificate(bytes);
769 return service.deleteCaCertificate(mCertHolder.mAlias);
771 } catch (CertificateEncodingException | SecurityException | IllegalStateException
772 | RemoteException e) {
773 Log.w(TAG, "Error while toggling alias " + mCertHolder.mAlias,
780 protected void onPostExecute(Boolean ok) {
781 mCertHolder.mTab.postOperationUpdate(ok, mCertHolder);
782 mAliasOperation = null;