Include wifi certificates in settings

Deleting wifi certificates isn't supported yet since cascading the
removal back into wifi configs will need some easy way of enumerating
wifi configs first.

Bug: 29208062
Change-Id: I2d9d1ea7e0974701009bfa6ea162b8bc80806639
diff --git a/res/layout/user_credential.xml b/res/layout/user_credential.xml
index 905822d..f441bda 100644
--- a/res/layout/user_credential.xml
+++ b/res/layout/user_credential.xml
@@ -13,52 +13,65 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="fill_parent"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:orientation="vertical"
-    android:gravity="center_vertical"
-    android:background="?android:attr/selectableItemBackground"
-    android:paddingTop="15dip"
-    android:paddingBottom="15dip">
+    android:orientation="vertical">
 
     <TextView
         android:id="@+id/alias"
-        android:layout_width="fill_parent"
+        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:textAppearance="?android:attr/textAppearanceMedium" />
 
-    <LinearLayout
-        android:orientation="vertical"
-        android:layout_width="fill_parent"
+    <TextView
+        android:id="@+id/purpose"
+        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_weight="1">
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?android:attr/textColorSecondary" />
+
+    <LinearLayout
+        android:id="@+id/contents"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:paddingTop="10dp">
+
+        <TextView
+            android:id="@+id/contents_title"
+            android:text="@string/credential_contains"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="?android:attr/textColorSecondary"/>
 
         <TextView
             android:id="@+id/contents_userkey"
             android:text="@string/one_userkey"
-            android:layout_width="fill_parent"
+            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:textAppearance="?android:attr/textAppearanceSmall"
-            android:textColor="?android:attr/textColorSecondary" />
+            android:textColor="?android:attr/textColorTertiary"
+            android:paddingStart="?android:attr/listPreferredItemPaddingStart"/>
 
         <TextView
             android:id="@+id/contents_usercrt"
             android:text="@string/one_usercrt"
-            android:layout_width="fill_parent"
+            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:textAppearance="?android:attr/textAppearanceSmall"
-            android:textColor="?android:attr/textColorSecondary" />
+            android:textColor="?android:attr/textColorTertiary"
+            android:paddingStart="?android:attr/listPreferredItemPaddingStart"/>
 
         <TextView
             android:id="@+id/contents_cacrt"
             android:text="@string/one_cacrt"
-            android:layout_width="fill_parent"
+            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:textAppearance="?android:attr/textAppearanceSmall"
-            android:textColor="?android:attr/textColorSecondary" />
-
+            android:textColor="?android:attr/textColorTertiary"
+            android:paddingStart="?android:attr/listPreferredItemPaddingStart"/>
     </LinearLayout>
 </LinearLayout>
diff --git a/res/layout/user_credential_preference.xml b/res/layout/user_credential_preference.xml
new file mode 100644
index 0000000..3671b7f
--- /dev/null
+++ b/res/layout/user_credential_preference.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:gravity="center_vertical"
+    android:background="?android:attr/selectableItemBackground"
+    android:paddingTop="15dip"
+    android:paddingBottom="15dip">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingEnd="?android:attr/listPreferredItemPaddingStart"
+        android:src="@android:drawable/ic_lock_lock"
+        android:tint="?android:attr/colorAccent"/>
+
+    <include layout="@layout/user_credential"/>
+</LinearLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7d04744..5163dec 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -4643,6 +4643,10 @@
     <string name="credential_storage_type_software">Software only</string>
     <!-- Error message for users that aren't allowed to see or modify credentials [CHAR LIMIT=none] -->
     <string name="credentials_settings_not_available">Credentials are not available for this user</string>
+    <!-- Sub-heading for a user credential installed to be used by apps and as part of VPN configurations. [CHAR LIMIT=NONE] -->
+    <string name="credential_for_vpn_and_apps">Installed for VPN and apps</string>
+    <!-- Sub-heading for a user credential installed to be used as part of a Wi-Fi configuration. [CHAR LIMIT=NONE]. -->
+    <string name="credential_for_wifi">Installed for Wi-Fi</string>
 
     <!-- Title of dialog to enable credential storage [CHAR LIMIT=30] -->
     <string name="credentials_unlock"></string>
@@ -5331,6 +5335,8 @@
     <!-- Alert dialog confirmation when removing a user CA certificate. -->
     <string name="trusted_credentials_remove_confirmation">Permanently remove the user CA certificate?</string>
 
+    <!-- Header for a list of items that a credential entry contains. For example, one private key and one certificate. [CHAR LIMIT=NONE] -->
+    <string name="credential_contains">This entry contains:</string>
     <!-- Item found in the PKCS12 keystore being investigated [CHAR LIMIT=NONE] -->
     <string name="one_userkey">one user key</string>
     <!-- Item found in the PKCS12 keystore being investigated [CHAR LIMIT=NONE] -->
diff --git a/src/com/android/settings/UserCredentialsSettings.java b/src/com/android/settings/UserCredentialsSettings.java
index 2681da6..bbab0c5 100644
--- a/src/com/android/settings/UserCredentialsSettings.java
+++ b/src/com/android/settings/UserCredentialsSettings.java
@@ -16,6 +16,8 @@
 
 package com.android.settings;
 
+import android.annotation.LayoutRes;
+import android.annotation.Nullable;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.DialogFragment;
@@ -26,6 +28,7 @@
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -35,6 +38,7 @@
 import android.security.KeyChain.KeyChainConnection;
 import android.security.KeyStore;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -49,7 +53,9 @@
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
 
+import java.util.ArrayList;
 import java.util.EnumSet;
+import java.util.List;
 import java.util.SortedMap;
 import java.util.TreeMap;
 
@@ -59,7 +65,6 @@
 public class UserCredentialsSettings extends OptionsMenuFragment implements OnItemClickListener {
     private static final String TAG = "UserCredentialsSettings";
 
-    private View mRootView;
     private ListView mListView;
 
     @Override
@@ -76,13 +81,13 @@
     @Override
     public View onCreateView(
             LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
-        mRootView = inflater.inflate(R.layout.user_credentials, parent, false);
+        final View rootView = inflater.inflate(R.layout.user_credentials, parent, false);
 
         // Set up an OnItemClickListener for the credential list.
-        mListView = (ListView) mRootView.findViewById(R.id.credential_list);
+        mListView = (ListView) rootView.findViewById(R.id.credential_list);
         mListView.setOnItemClickListener(this);
 
-        return mRootView;
+        return rootView;
     }
 
     @Override
@@ -122,15 +127,13 @@
         @Override
         public Dialog onCreateDialog(Bundle savedInstanceState) {
             final Credential item = (Credential) getArguments().getParcelable(ARG_CREDENTIAL);
+
             View root = getActivity().getLayoutInflater()
                     .inflate(R.layout.user_credential_dialog, null);
             ViewGroup infoContainer = (ViewGroup) root.findViewById(R.id.credential_container);
-            View view = new CredentialAdapter(getActivity(), R.layout.user_credential,
-                    new Credential[] {item}).getView(0, null, null);
-            infoContainer.addView(view);
-
-            UserManager userManager
-                    = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
+            View contentView = getCredentialView(item, R.layout.user_credential, null,
+                    infoContainer, /* expanded */ true);
+            infoContainer.addView(contentView);
 
             AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
                     .setView(root)
@@ -148,49 +151,74 @@
                             RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(),
                                     admin);
                         } else {
-                            new RemoveCredentialsTask(getTargetFragment()).execute(item.alias);
+                            new RemoveCredentialsTask(getContext(), getTargetFragment())
+                                    .execute(item);
                         }
                         dialog.dismiss();
                     }
                 };
-                builder.setNegativeButton(R.string.trusted_credentials_remove_label, listener);
+                if (item.isSystem()) {
+                    // TODO: a safe means of clearing wifi certificates. Configs refer to aliases
+                    //       directly so deleting certs will break dependent access points.
+                    builder.setNegativeButton(R.string.trusted_credentials_remove_label, listener);
+                }
             }
             return builder.create();
         }
 
-        private class RemoveCredentialsTask extends AsyncTask<String, Void, String[]> {
+        /**
+         * Deletes all certificates and keys under a given alias.
+         *
+         * If the {@link Credential} is for a system alias, all active grants to the alias will be
+         * removed using {@link KeyChain}.
+         */
+        private class RemoveCredentialsTask extends AsyncTask<Credential, Void, Credential[]> {
+            private Context context;
             private Fragment targetFragment;
 
-            public RemoveCredentialsTask(Fragment targetFragment) {
+            public RemoveCredentialsTask(Context context, Fragment targetFragment) {
+                this.context = context;
                 this.targetFragment = targetFragment;
             }
 
             @Override
-            protected String[] doInBackground(String... aliases) {
-                try {
-                    final KeyChainConnection conn = KeyChain.bind(getContext());
-                    try {
-                        IKeyChainService keyChain = conn.getService();
-                        for (String alias : aliases) {
-                            keyChain.removeKeyPair(alias);
-                        }
-                    } catch (RemoteException e) {
-                        Log.w(TAG, "Removing credentials", e);
-                    } finally {
-                        conn.close();
+            protected Credential[] doInBackground(Credential... credentials) {
+                for (final Credential credential : credentials) {
+                    if (credential.isSystem()) {
+                        removeGrantsAndDelete(credential);
+                        continue;
                     }
-                } catch (InterruptedException e) {
-                    Log.w(TAG, "Connecting to keychain", e);
+                    throw new UnsupportedOperationException(
+                            "Not implemented for wifi certificates. This should not be reachable.");
                 }
-                return aliases;
+                return credentials;
+            }
+
+            private void removeGrantsAndDelete(final Credential credential) {
+                final KeyChainConnection conn;
+                try {
+                    conn = KeyChain.bind(getContext());
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "Connecting to KeyChain", e);
+                    return;
+                }
+
+                try {
+                    IKeyChainService keyChain = conn.getService();
+                    keyChain.removeKeyPair(credential.alias);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Removing credentials", e);
+                } finally {
+                    conn.close();
+                }
             }
 
             @Override
-            protected void onPostExecute(String... aliases) {
+            protected void onPostExecute(Credential... credentials) {
                 if (targetFragment instanceof UserCredentialsSettings && targetFragment.isAdded()) {
                     final UserCredentialsSettings target = (UserCredentialsSettings) targetFragment;
-                    for (final String alias : aliases) {
-                        target.announceRemoval(alias);
+                    for (final Credential credential : credentials) {
+                        target.announceRemoval(credential.alias);
                     }
                     target.refreshItems();
                 }
@@ -203,34 +231,53 @@
      * The credentials are stored in a {@link CredentialAdapter} attached to the main
      * {@link ListView} in the fragment.
      */
-    private class AliasLoader extends AsyncTask<Void, Void, SortedMap<String, Credential>> {
+    private class AliasLoader extends AsyncTask<Void, Void, List<Credential>> {
+        /**
+         * @return a list of credentials ordered:
+         * <ol>
+         *   <li>first by purpose;</li>
+         *   <li>then by alias.</li>
+         * </ol>
+         */
         @Override
-        protected SortedMap<String, Credential> doInBackground(Void... params) {
-            // Create a list of names for credential sets, ordered by name.
-            SortedMap<String, Credential> credentials = new TreeMap<>();
-            KeyStore keyStore = KeyStore.getInstance();
+        protected List<Credential> doInBackground(Void... params) {
+            final KeyStore keyStore = KeyStore.getInstance();
+
+            // Certificates can be installed into SYSTEM_UID or WIFI_UID through CertInstaller.
+            final int myUserId = UserHandle.myUserId();
+            final int systemUid = UserHandle.getUid(myUserId, Process.SYSTEM_UID);
+            final int wifiUid = UserHandle.getUid(myUserId, Process.WIFI_UID);
+
+            List<Credential> credentials = new ArrayList<>();
+            credentials.addAll(getCredentialsForUid(keyStore, systemUid).values());
+            credentials.addAll(getCredentialsForUid(keyStore, wifiUid).values());
+            return credentials;
+        }
+
+        private SortedMap<String, Credential> getCredentialsForUid(KeyStore keyStore, int uid) {
+            final SortedMap<String, Credential> aliasMap = new TreeMap<>();
             for (final Credential.Type type : Credential.Type.values()) {
-                for (final String alias : keyStore.list(type.prefix)) {
+                for (final String alias : keyStore.list(type.prefix, uid)) {
                     // Do not show work profile keys in user credentials
                     if (alias.startsWith(LockPatternUtils.PROFILE_KEY_NAME_ENCRYPT) ||
                             alias.startsWith(LockPatternUtils.PROFILE_KEY_NAME_DECRYPT)) {
                         continue;
                     }
-                    Credential c = credentials.get(alias);
+                    Credential c = aliasMap.get(alias);
                     if (c == null) {
-                        credentials.put(alias, (c = new Credential(alias)));
+                        c = new Credential(alias, uid);
+                        aliasMap.put(alias, c);
                     }
                     c.storedTypes.add(type);
                 }
             }
-            return credentials;
+            return aliasMap;
         }
 
         @Override
-        protected void onPostExecute(SortedMap<String, Credential> credentials) {
-            // Convert the results to an array and present them using an ArrayAdapter.
-            mListView.setAdapter(new CredentialAdapter(getContext(), R.layout.user_credential,
-                    credentials.values().toArray(new Credential[0])));
+        protected void onPostExecute(List<Credential> credentials) {
+            final Credential[] credentialArray = credentials.toArray(new Credential[0]);
+            mListView.setAdapter(new CredentialAdapter(getContext(), credentialArray));
         }
     }
 
@@ -238,28 +285,56 @@
      * Helper class to display {@link Credential}s in a list.
      */
     private static class CredentialAdapter extends ArrayAdapter<Credential> {
-        public CredentialAdapter(Context context, int resource,  Credential[] objects) {
-            super(context, resource, objects);
+        private static final int LAYOUT_RESOURCE = R.layout.user_credential_preference;
+
+        public CredentialAdapter(Context context, final Credential[] objects) {
+            super(context, LAYOUT_RESOURCE, objects);
         }
 
         @Override
-        public View getView(int position, View view, ViewGroup parent) {
-            if (view == null) {
-                view = LayoutInflater.from(getContext())
-                        .inflate(R.layout.user_credential, parent, false);
-            }
-            Credential item = getItem(position);
-            ((TextView) view.findViewById(R.id.alias)).setText(item.alias);
-            view.findViewById(R.id.contents_userkey).setVisibility(
-                    item.storedTypes.contains(Credential.Type.USER_PRIVATE_KEY) ? VISIBLE : GONE);
-            view.findViewById(R.id.contents_usercrt).setVisibility(
-                    item.storedTypes.contains(Credential.Type.USER_CERTIFICATE) ? VISIBLE : GONE);
-            view.findViewById(R.id.contents_cacrt).setVisibility(
-                    item.storedTypes.contains(Credential.Type.CA_CERTIFICATE) ? VISIBLE : GONE);
-            return view;
+        public View getView(int position, @Nullable View view, ViewGroup parent) {
+            return getCredentialView(getItem(position), LAYOUT_RESOURCE, view, parent,
+                    /* expanded */ false);
         }
     }
 
+    /**
+     * Mapping from View IDs in {@link R} to the types of credentials they describe.
+     */
+    private static final SparseArray<Credential.Type> credentialViewTypes = new SparseArray<>();
+    static {
+        credentialViewTypes.put(R.id.contents_userkey, Credential.Type.USER_PRIVATE_KEY);
+        credentialViewTypes.put(R.id.contents_usercrt, Credential.Type.USER_CERTIFICATE);
+        credentialViewTypes.put(R.id.contents_cacrt, Credential.Type.CA_CERTIFICATE);
+    }
+
+    protected static View getCredentialView(Credential item, @LayoutRes int layoutResource,
+            @Nullable View view, ViewGroup parent, boolean expanded) {
+        if (view == null) {
+            view = LayoutInflater.from(parent.getContext()).inflate(layoutResource, parent, false);
+        }
+
+        ((TextView) view.findViewById(R.id.alias)).setText(item.alias);
+        ((TextView) view.findViewById(R.id.purpose)).setText(item.isSystem()
+                ? R.string.credential_for_vpn_and_apps
+                : R.string.credential_for_wifi);
+
+        view.findViewById(R.id.contents).setVisibility(expanded ? View.VISIBLE : View.GONE);
+        if (expanded) {
+            for (int i = 0; i < credentialViewTypes.size(); i++) {
+                final View detail = view.findViewById(credentialViewTypes.keyAt(i));
+                detail.setVisibility(item.storedTypes.contains(credentialViewTypes.valueAt(i))
+                        ? View.VISIBLE : View.GONE);
+            }
+        }
+        return view;
+    }
+
+    static class AliasEntry {
+        public String alias;
+        public int uid;
+    }
+
     static class Credential implements Parcelable {
         static enum Type {
             CA_CERTIFICATE (Credentials.CA_CERTIFICATE),
@@ -281,6 +356,12 @@
         final String alias;
 
         /**
+         * UID under which this credential is stored. Typically {@link Process#SYSTEM_UID} but can
+         * also be {@link Process#WIFI_UID} for credentials installed as wifi certificates.
+         */
+        final int uid;
+
+        /**
          * Should contain some non-empty subset of:
          * <ul>
          *   <li>{@link Credentials.CA_CERTIFICATE}</li>
@@ -291,12 +372,13 @@
          */
         final EnumSet<Type> storedTypes = EnumSet.noneOf(Type.class);
 
-        Credential(final String alias) {
+        Credential(final String alias, final int uid) {
             this.alias = alias;
+            this.uid = uid;
         }
 
         Credential(Parcel in) {
-            this(in.readString());
+            this(in.readString(), in.readInt());
 
             long typeBits = in.readLong();
             for (Type i : Type.values()) {
@@ -308,6 +390,7 @@
 
         public void writeToParcel(Parcel out, int flags) {
             out.writeString(alias);
+            out.writeInt(uid);
 
             long typeBits = 0;
             for (Type i : storedTypes) {
@@ -330,5 +413,9 @@
                 return new Credential[size];
             }
         };
+
+        public boolean isSystem() {
+            return UserHandle.getAppId(uid) == Process.SYSTEM_UID;
+        }
     }
 }
diff --git a/tests/unit/src/com/android/settings/UserCredentialsTest.java b/tests/unit/src/com/android/settings/UserCredentialsTest.java
index 6de252e..41ef4de 100644
--- a/tests/unit/src/com/android/settings/UserCredentialsTest.java
+++ b/tests/unit/src/com/android/settings/UserCredentialsTest.java
@@ -17,6 +17,7 @@
 package com.android.settings;
 
 import android.os.Parcel;
+import android.os.Process;
 import android.test.InstrumentationTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
@@ -36,7 +37,7 @@
     @SmallTest
     public void testCredentialIsParcelable() {
         final String alias = "credential-test-alias";
-        Credential c = new Credential(alias);
+        Credential c = new Credential(alias, Process.SYSTEM_UID);
 
         c.storedTypes.add(Credential.Type.CA_CERTIFICATE);
         c.storedTypes.add(Credential.Type.USER_SECRET_KEY);
@@ -47,6 +48,7 @@
 
         Credential r = Credential.CREATOR.createFromParcel(p);
         assertEquals(c.alias, r.alias);
+        assertEquals(c.uid, r.uid);
         assertEquals(c.storedTypes, r.storedTypes);
     }
 }