blob: 73f1d9e6c776ea579042fa2c3a7fcd035b05d471 [file] [log] [blame]
Robin Leebaefdcf2015-08-26 10:57:44 +01001/*
2 * Copyright (C) 2015 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
17package com.android.settings;
18
Robin Leee68d9572016-07-19 16:10:39 +010019import android.annotation.LayoutRes;
20import android.annotation.Nullable;
Robin Lee04046a12016-01-19 11:42:57 +000021import android.app.Dialog;
Fan Zhang31b21002019-01-16 13:49:47 -080022import android.app.settings.SettingsEnums;
Robin Leebaefdcf2015-08-26 10:57:44 +010023import android.content.Context;
24import android.content.DialogInterface;
25import android.os.AsyncTask;
26import android.os.Bundle;
Robin Lee04046a12016-01-19 11:42:57 +000027import android.os.Parcel;
28import android.os.Parcelable;
Robin Leee68d9572016-07-19 16:10:39 +010029import android.os.Process;
Robin Leeda7bc512016-02-24 17:39:32 +000030import android.os.RemoteException;
Robin Leec421db72016-03-11 16:22:23 +000031import android.os.UserHandle;
32import android.os.UserManager;
Robin Leebaefdcf2015-08-26 10:57:44 +010033import android.security.Credentials;
Robin Leeda7bc512016-02-24 17:39:32 +000034import android.security.IKeyChainService;
35import android.security.KeyChain;
36import android.security.KeyChain.KeyChainConnection;
Janis Danisevskisa05bd652021-01-25 14:54:48 -080037import android.security.keystore.KeyProperties;
38import android.security.keystore2.AndroidKeyStoreLoadStoreParameter;
Robin Leeda7bc512016-02-24 17:39:32 +000039import android.util.Log;
Robin Leee68d9572016-07-19 16:10:39 +010040import android.util.SparseArray;
Robin Leebaefdcf2015-08-26 10:57:44 +010041import android.view.LayoutInflater;
42import android.view.View;
43import android.view.ViewGroup;
Robin Leebaefdcf2015-08-26 10:57:44 +010044import android.widget.TextView;
45
Weng Su28791f52022-12-15 15:53:04 +080046import androidx.annotation.VisibleForTesting;
Fan Zhang23f8d592018-08-28 15:11:40 -070047import androidx.appcompat.app.AlertDialog;
48import androidx.fragment.app.DialogFragment;
49import androidx.fragment.app.Fragment;
50import androidx.recyclerview.widget.RecyclerView;
51
Fan Zhang1e516282016-09-16 12:45:07 -070052import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
Weng Su28791f52022-12-15 15:53:04 +080053import com.android.settings.wifi.helper.SavedWifiHelper;
Robin Leec421db72016-03-11 16:22:23 +000054import com.android.settingslib.RestrictedLockUtils;
55import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
Philip P. Moltmanne3f72112018-08-28 15:01:43 -070056import com.android.settingslib.RestrictedLockUtilsInternal;
Fan Zhangc7162cd2018-06-18 15:21:41 -070057
Janis Danisevskisa05bd652021-01-25 14:54:48 -080058import java.security.Key;
59import java.security.KeyStore;
60import java.security.KeyStoreException;
61import java.security.NoSuchAlgorithmException;
Janis Danisevskisb705e1a2017-04-19 16:23:02 -070062import java.security.UnrecoverableKeyException;
Janis Danisevskisa05bd652021-01-25 14:54:48 -080063import java.security.cert.Certificate;
Robin Leee68d9572016-07-19 16:10:39 +010064import java.util.ArrayList;
Robin Leebaefdcf2015-08-26 10:57:44 +010065import java.util.EnumSet;
Janis Danisevskisa05bd652021-01-25 14:54:48 -080066import java.util.Enumeration;
Robin Leee68d9572016-07-19 16:10:39 +010067import java.util.List;
Robin Leebaefdcf2015-08-26 10:57:44 +010068import java.util.SortedMap;
69import java.util.TreeMap;
70
Janis Danisevskisa05bd652021-01-25 14:54:48 -080071import javax.crypto.SecretKey;
72
Robin Leeccaf9c92017-03-24 14:50:05 +000073public class UserCredentialsSettings extends SettingsPreferenceFragment
74 implements View.OnClickListener {
Robin Leebaefdcf2015-08-26 10:57:44 +010075 private static final String TAG = "UserCredentialsSettings";
76
Janis Danisevskis75a91832021-03-10 09:30:46 -080077 private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
78
Weng Su28791f52022-12-15 15:53:04 +080079 @VisibleForTesting
80 protected SavedWifiHelper mSavedWifiHelper;
81
Robin Leebaefdcf2015-08-26 10:57:44 +010082 @Override
Fan Zhang65076132016-08-08 10:25:13 -070083 public int getMetricsCategory() {
Fan Zhang31b21002019-01-16 13:49:47 -080084 return SettingsEnums.USER_CREDENTIALS;
Robin Leebaefdcf2015-08-26 10:57:44 +010085 }
86
87 @Override
88 public void onResume() {
89 super.onResume();
Robin Lee04046a12016-01-19 11:42:57 +000090 refreshItems();
Robin Leebaefdcf2015-08-26 10:57:44 +010091 }
92
93 @Override
Robin Leeccaf9c92017-03-24 14:50:05 +000094 public void onClick(final View view) {
95 final Credential item = (Credential) view.getTag();
Weng Su28791f52022-12-15 15:53:04 +080096 if (item == null) return;
97 if (item.isInUse()) {
98 item.setUsedByNames(mSavedWifiHelper.getCertificateNetworkNames(item.alias));
Robin Leeccaf9c92017-03-24 14:50:05 +000099 }
Weng Su28791f52022-12-15 15:53:04 +0800100 showCredentialDialogFragment(item);
Robin Lee04046a12016-01-19 11:42:57 +0000101 }
Robin Leebaefdcf2015-08-26 10:57:44 +0100102
Doris Ling03a3b512017-10-18 14:25:01 -0700103 @Override
Doris Linged4685f2017-10-25 14:08:57 -0700104 public void onCreate(@Nullable Bundle savedInstanceState) {
105 super.onCreate(savedInstanceState);
Doris Ling4a012832017-11-13 17:58:13 -0800106 getActivity().setTitle(R.string.user_credentials);
Weng Su28791f52022-12-15 15:53:04 +0800107 mSavedWifiHelper = SavedWifiHelper.getInstance(getContext(), getSettingsLifecycle());
108 }
109
110 @VisibleForTesting
111 protected void showCredentialDialogFragment(Credential item) {
112 CredentialDialogFragment.show(this, item);
Doris Ling03a3b512017-10-18 14:25:01 -0700113 }
114
Robin Lee11fd5502016-05-16 15:42:34 +0100115 protected void announceRemoval(String alias) {
Robin Leeccaf9c92017-03-24 14:50:05 +0000116 if (!isAdded()) {
117 return;
Robin Lee11fd5502016-05-16 15:42:34 +0100118 }
Robin Leeccaf9c92017-03-24 14:50:05 +0000119 getListView().announceForAccessibility(getString(R.string.user_credential_removed, alias));
Robin Lee11fd5502016-05-16 15:42:34 +0100120 }
121
Robin Lee04046a12016-01-19 11:42:57 +0000122 protected void refreshItems() {
123 if (isAdded()) {
124 new AliasLoader().execute();
125 }
126 }
Robin Leebaefdcf2015-08-26 10:57:44 +0100127
Weng Su28791f52022-12-15 15:53:04 +0800128 /** The fragment to show the credential information. */
129 public static class CredentialDialogFragment extends InstrumentedDialogFragment
130 implements DialogInterface.OnShowListener {
Robin Lee04046a12016-01-19 11:42:57 +0000131 private static final String TAG = "CredentialDialogFragment";
132 private static final String ARG_CREDENTIAL = "credential";
133
134 public static void show(Fragment target, Credential item) {
135 final Bundle args = new Bundle();
136 args.putParcelable(ARG_CREDENTIAL, item);
137
Robin Leef8e2dbf2016-04-07 13:17:24 +0100138 if (target.getFragmentManager().findFragmentByTag(TAG) == null) {
139 final DialogFragment frag = new CredentialDialogFragment();
140 frag.setTargetFragment(target, /* requestCode */ -1);
141 frag.setArguments(args);
142 frag.show(target.getFragmentManager(), TAG);
143 }
Robin Lee04046a12016-01-19 11:42:57 +0000144 }
145
146 @Override
147 public Dialog onCreateDialog(Bundle savedInstanceState) {
148 final Credential item = (Credential) getArguments().getParcelable(ARG_CREDENTIAL);
Robin Leee68d9572016-07-19 16:10:39 +0100149
Robin Lee04046a12016-01-19 11:42:57 +0000150 View root = getActivity().getLayoutInflater()
151 .inflate(R.layout.user_credential_dialog, null);
152 ViewGroup infoContainer = (ViewGroup) root.findViewById(R.id.credential_container);
Robin Leee68d9572016-07-19 16:10:39 +0100153 View contentView = getCredentialView(item, R.layout.user_credential, null,
154 infoContainer, /* expanded */ true);
155 infoContainer.addView(contentView);
Robin Leec421db72016-03-11 16:22:23 +0000156
157 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
Robin Lee04046a12016-01-19 11:42:57 +0000158 .setView(root)
159 .setTitle(R.string.user_credential_title)
Robin Leec421db72016-03-11 16:22:23 +0000160 .setPositiveButton(R.string.done, null);
161
162 final String restriction = UserManager.DISALLOW_CONFIG_CREDENTIALS;
163 final int myUserId = UserHandle.myUserId();
Philip P. Moltmanne3f72112018-08-28 15:01:43 -0700164 if (!RestrictedLockUtilsInternal.hasBaseUserRestriction(getContext(), restriction,
165 myUserId)) {
Robin Leec421db72016-03-11 16:22:23 +0000166 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
167 @Override public void onClick(DialogInterface dialog, int id) {
Philip P. Moltmanne3f72112018-08-28 15:01:43 -0700168 final EnforcedAdmin admin = RestrictedLockUtilsInternal
169 .checkIfRestrictionEnforced(getContext(), restriction, myUserId);
Robin Leec421db72016-03-11 16:22:23 +0000170 if (admin != null) {
171 RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(),
172 admin);
173 } else {
Robin Leee68d9572016-07-19 16:10:39 +0100174 new RemoveCredentialsTask(getContext(), getTargetFragment())
175 .execute(item);
Robin Leec421db72016-03-11 16:22:23 +0000176 }
177 dialog.dismiss();
178 }
179 };
Hai Shalom23666052019-03-07 15:54:25 -0800180 builder.setNegativeButton(R.string.trusted_credentials_remove_label, listener);
Robin Leec421db72016-03-11 16:22:23 +0000181 }
Weng Su28791f52022-12-15 15:53:04 +0800182 AlertDialog dialog = builder.create();
183 dialog.setOnShowListener(this);
184 return dialog;
185 }
186
187 /**
188 * Override for the negative button enablement on demand.
189 */
190 @Override
191 public void onShow(DialogInterface dialogInterface) {
192 final Credential item = (Credential) getArguments().getParcelable(ARG_CREDENTIAL);
193 if (item.isInUse()) {
194 ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEGATIVE)
195 .setEnabled(false);
196 }
Robin Lee04046a12016-01-19 11:42:57 +0000197 }
198
Fan Zhang1e516282016-09-16 12:45:07 -0700199 @Override
200 public int getMetricsCategory() {
Fan Zhang31b21002019-01-16 13:49:47 -0800201 return SettingsEnums.DIALOG_USER_CREDENTIAL;
Fan Zhang1e516282016-09-16 12:45:07 -0700202 }
203
Robin Leee68d9572016-07-19 16:10:39 +0100204 /**
205 * Deletes all certificates and keys under a given alias.
206 *
207 * If the {@link Credential} is for a system alias, all active grants to the alias will be
Hai Shalom23666052019-03-07 15:54:25 -0800208 * removed using {@link KeyChain}. If the {@link Credential} is for Wi-Fi alias, all
209 * credentials and keys will be removed using {@link KeyStore}.
Robin Leee68d9572016-07-19 16:10:39 +0100210 */
211 private class RemoveCredentialsTask extends AsyncTask<Credential, Void, Credential[]> {
212 private Context context;
Robin Leeda7bc512016-02-24 17:39:32 +0000213 private Fragment targetFragment;
214
Robin Leee68d9572016-07-19 16:10:39 +0100215 public RemoveCredentialsTask(Context context, Fragment targetFragment) {
216 this.context = context;
Robin Leeda7bc512016-02-24 17:39:32 +0000217 this.targetFragment = targetFragment;
Robin Lee04046a12016-01-19 11:42:57 +0000218 }
Robin Leeda7bc512016-02-24 17:39:32 +0000219
220 @Override
Robin Leee68d9572016-07-19 16:10:39 +0100221 protected Credential[] doInBackground(Credential... credentials) {
222 for (final Credential credential : credentials) {
223 if (credential.isSystem()) {
224 removeGrantsAndDelete(credential);
Hai Shalom23666052019-03-07 15:54:25 -0800225 } else {
226 deleteWifiCredential(credential);
Robin Leeda7bc512016-02-24 17:39:32 +0000227 }
Robin Leeda7bc512016-02-24 17:39:32 +0000228 }
Robin Leee68d9572016-07-19 16:10:39 +0100229 return credentials;
230 }
231
Hai Shalom23666052019-03-07 15:54:25 -0800232 private void deleteWifiCredential(final Credential credential) {
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800233 try {
Janis Danisevskis75a91832021-03-10 09:30:46 -0800234 final KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
235 keyStore.load(
236 new AndroidKeyStoreLoadStoreParameter(
237 KeyProperties.NAMESPACE_WIFI));
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800238 keyStore.deleteEntry(credential.getAlias());
239 } catch (Exception e) {
240 throw new RuntimeException("Failed to delete keys from keystore.");
Hai Shalom23666052019-03-07 15:54:25 -0800241 }
242 }
243
Robin Leee68d9572016-07-19 16:10:39 +0100244 private void removeGrantsAndDelete(final Credential credential) {
245 final KeyChainConnection conn;
246 try {
247 conn = KeyChain.bind(getContext());
248 } catch (InterruptedException e) {
249 Log.w(TAG, "Connecting to KeyChain", e);
250 return;
251 }
252
253 try {
254 IKeyChainService keyChain = conn.getService();
255 keyChain.removeKeyPair(credential.alias);
256 } catch (RemoteException e) {
257 Log.w(TAG, "Removing credentials", e);
258 } finally {
259 conn.close();
260 }
Robin Leeda7bc512016-02-24 17:39:32 +0000261 }
262
263 @Override
Robin Leee68d9572016-07-19 16:10:39 +0100264 protected void onPostExecute(Credential... credentials) {
Robin Lee11fd5502016-05-16 15:42:34 +0100265 if (targetFragment instanceof UserCredentialsSettings && targetFragment.isAdded()) {
266 final UserCredentialsSettings target = (UserCredentialsSettings) targetFragment;
Robin Leee68d9572016-07-19 16:10:39 +0100267 for (final Credential credential : credentials) {
268 target.announceRemoval(credential.alias);
Robin Lee11fd5502016-05-16 15:42:34 +0100269 }
270 target.refreshItems();
Robin Leeda7bc512016-02-24 17:39:32 +0000271 }
272 }
Robin Lee04046a12016-01-19 11:42:57 +0000273 }
Robin Leebaefdcf2015-08-26 10:57:44 +0100274 }
275
276 /**
277 * Opens a background connection to KeyStore to list user credentials.
278 * The credentials are stored in a {@link CredentialAdapter} attached to the main
279 * {@link ListView} in the fragment.
280 */
Robin Leee68d9572016-07-19 16:10:39 +0100281 private class AliasLoader extends AsyncTask<Void, Void, List<Credential>> {
282 /**
283 * @return a list of credentials ordered:
284 * <ol>
285 * <li>first by purpose;</li>
286 * <li>then by alias.</li>
287 * </ol>
288 */
Robin Leebaefdcf2015-08-26 10:57:44 +0100289 @Override
Robin Leee68d9572016-07-19 16:10:39 +0100290 protected List<Credential> doInBackground(Void... params) {
Robin Leee68d9572016-07-19 16:10:39 +0100291 // Certificates can be installed into SYSTEM_UID or WIFI_UID through CertInstaller.
292 final int myUserId = UserHandle.myUserId();
293 final int systemUid = UserHandle.getUid(myUserId, Process.SYSTEM_UID);
294 final int wifiUid = UserHandle.getUid(myUserId, Process.WIFI_UID);
295
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800296 try {
Janis Danisevskis75a91832021-03-10 09:30:46 -0800297 KeyStore processKeystore = KeyStore.getInstance(KEYSTORE_PROVIDER);
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800298 processKeystore.load(null);
299 KeyStore wifiKeystore = null;
300 if (myUserId == 0) {
Janis Danisevskis75a91832021-03-10 09:30:46 -0800301 wifiKeystore = KeyStore.getInstance(KEYSTORE_PROVIDER);
302 wifiKeystore.load(new AndroidKeyStoreLoadStoreParameter(
303 KeyProperties.NAMESPACE_WIFI));
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800304 }
Robin Leee68d9572016-07-19 16:10:39 +0100305
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800306 List<Credential> credentials = new ArrayList<>();
307 credentials.addAll(getCredentialsForUid(processKeystore, systemUid).values());
308 if (wifiKeystore != null) {
309 credentials.addAll(getCredentialsForUid(wifiKeystore, wifiUid).values());
Janis Danisevskisb705e1a2017-04-19 16:23:02 -0700310 }
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800311 return credentials;
312 } catch (Exception e) {
313 throw new RuntimeException("Failed to load credentials from Keystore.", e);
314 }
Janis Danisevskisb705e1a2017-04-19 16:23:02 -0700315 }
316
Robin Leee68d9572016-07-19 16:10:39 +0100317 private SortedMap<String, Credential> getCredentialsForUid(KeyStore keyStore, int uid) {
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800318 try {
319 final SortedMap<String, Credential> aliasMap = new TreeMap<>();
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800320 Enumeration<String> aliases = keyStore.aliases();
321 while (aliases.hasMoreElements()) {
322 String alias = aliases.nextElement();
323 Credential c = new Credential(alias, uid);
Weng Su28791f52022-12-15 15:53:04 +0800324 if (!c.isSystem()) {
325 c.setInUse(mSavedWifiHelper.isCertificateInUse(alias));
326 }
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800327 Key key = null;
328 try {
329 key = keyStore.getKey(alias, null);
330 } catch (NoSuchAlgorithmException | UnrecoverableKeyException e) {
331 Log.e(TAG, "Error tying to retrieve key: " + alias, e);
332 continue;
333 }
334 if (key != null) {
335 // So we have a key
336 if (key instanceof SecretKey) {
337 // We don't display any symmetric key entries.
338 continue;
339 }
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800340 // At this point we have determined that we have an asymmetric key.
341 // so we have at least a USER_KEY and USER_CERTIFICATE.
342 c.storedTypes.add(Credential.Type.USER_KEY);
343
344 Certificate[] certs = keyStore.getCertificateChain(alias);
345 if (certs != null) {
346 c.storedTypes.add(Credential.Type.USER_CERTIFICATE);
347 if (certs.length > 1) {
348 c.storedTypes.add(Credential.Type.CA_CERTIFICATE);
Janis Danisevskisb705e1a2017-04-19 16:23:02 -0700349 }
Rubin Xu52221d82017-02-09 11:09:11 +0000350 }
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800351 } else {
352 // So there is no key but we have an alias. This must mean that we have
353 // some certificate.
354 if (keyStore.isCertificateEntry(alias)) {
355 c.storedTypes.add(Credential.Type.CA_CERTIFICATE);
356 } else {
357 // This is a weired inconsistent case that should not exist.
358 // Pure trusted certificate entries should be stored in CA_CERTIFICATE,
359 // but if isCErtificateEntry returns null this means that only the
360 // USER_CERTIFICATE is populated which should never be the case without
361 // a private key. It can still be retrieved with
362 // keystore.getCertificate().
363 c.storedTypes.add(Credential.Type.USER_CERTIFICATE);
Rubin Xu52221d82017-02-09 11:09:11 +0000364 }
Ricky Wai95792742016-05-24 19:28:53 +0100365 }
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800366 aliasMap.put(alias, c);
Robin Leebaefdcf2015-08-26 10:57:44 +0100367 }
Janis Danisevskisa05bd652021-01-25 14:54:48 -0800368 return aliasMap;
369 } catch (KeyStoreException e) {
370 throw new RuntimeException("Failed to load credential from Android Keystore.", e);
Robin Leebaefdcf2015-08-26 10:57:44 +0100371 }
Robin Leebaefdcf2015-08-26 10:57:44 +0100372 }
373
374 @Override
Robin Leee68d9572016-07-19 16:10:39 +0100375 protected void onPostExecute(List<Credential> credentials) {
Robin Leeccaf9c92017-03-24 14:50:05 +0000376 if (!isAdded()) {
377 return;
378 }
379
380 if (credentials == null || credentials.size() == 0) {
381 // Create a "no credentials installed" message for the empty case.
382 TextView emptyTextView = (TextView) getActivity().findViewById(android.R.id.empty);
383 emptyTextView.setText(R.string.user_credential_none_installed);
384 setEmptyView(emptyTextView);
385 } else {
386 setEmptyView(null);
387 }
388
389 getListView().setAdapter(
390 new CredentialAdapter(credentials, UserCredentialsSettings.this));
Robin Leebaefdcf2015-08-26 10:57:44 +0100391 }
392 }
393
394 /**
395 * Helper class to display {@link Credential}s in a list.
396 */
Robin Leeccaf9c92017-03-24 14:50:05 +0000397 private static class CredentialAdapter extends RecyclerView.Adapter<ViewHolder> {
Robin Leee68d9572016-07-19 16:10:39 +0100398 private static final int LAYOUT_RESOURCE = R.layout.user_credential_preference;
399
Robin Leeccaf9c92017-03-24 14:50:05 +0000400 private final List<Credential> mItems;
401 private final View.OnClickListener mListener;
402
403 public CredentialAdapter(List<Credential> items, @Nullable View.OnClickListener listener) {
404 mItems = items;
405 mListener = listener;
Robin Leebaefdcf2015-08-26 10:57:44 +0100406 }
407
408 @Override
Robin Leeccaf9c92017-03-24 14:50:05 +0000409 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
410 final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
411 return new ViewHolder(inflater.inflate(LAYOUT_RESOURCE, parent, false));
412 }
413
414 @Override
415 public void onBindViewHolder(ViewHolder h, int position) {
416 getCredentialView(mItems.get(position), LAYOUT_RESOURCE, h.itemView, null, false);
417 h.itemView.setTag(mItems.get(position));
418 h.itemView.setOnClickListener(mListener);
419 }
420
421 @Override
422 public int getItemCount() {
423 return mItems.size();
424 }
425 }
426
427 private static class ViewHolder extends RecyclerView.ViewHolder {
428 public ViewHolder(View item) {
429 super(item);
Robin Leebaefdcf2015-08-26 10:57:44 +0100430 }
431 }
432
Robin Leee68d9572016-07-19 16:10:39 +0100433 /**
434 * Mapping from View IDs in {@link R} to the types of credentials they describe.
435 */
436 private static final SparseArray<Credential.Type> credentialViewTypes = new SparseArray<>();
437 static {
Janis Danisevskisb705e1a2017-04-19 16:23:02 -0700438 credentialViewTypes.put(R.id.contents_userkey, Credential.Type.USER_KEY);
Robin Leee68d9572016-07-19 16:10:39 +0100439 credentialViewTypes.put(R.id.contents_usercrt, Credential.Type.USER_CERTIFICATE);
440 credentialViewTypes.put(R.id.contents_cacrt, Credential.Type.CA_CERTIFICATE);
441 }
442
443 protected static View getCredentialView(Credential item, @LayoutRes int layoutResource,
444 @Nullable View view, ViewGroup parent, boolean expanded) {
445 if (view == null) {
446 view = LayoutInflater.from(parent.getContext()).inflate(layoutResource, parent, false);
447 }
448
449 ((TextView) view.findViewById(R.id.alias)).setText(item.alias);
Weng Su28791f52022-12-15 15:53:04 +0800450 updatePurposeView(view.findViewById(R.id.purpose), item);
Robin Leee68d9572016-07-19 16:10:39 +0100451
452 view.findViewById(R.id.contents).setVisibility(expanded ? View.VISIBLE : View.GONE);
453 if (expanded) {
Weng Su28791f52022-12-15 15:53:04 +0800454 updateUsedByViews(view.findViewById(R.id.credential_being_used_by_title),
455 view.findViewById(R.id.credential_being_used_by_content), item);
456
Robin Leee68d9572016-07-19 16:10:39 +0100457 for (int i = 0; i < credentialViewTypes.size(); i++) {
458 final View detail = view.findViewById(credentialViewTypes.keyAt(i));
459 detail.setVisibility(item.storedTypes.contains(credentialViewTypes.valueAt(i))
460 ? View.VISIBLE : View.GONE);
461 }
462 }
463 return view;
464 }
465
Weng Su28791f52022-12-15 15:53:04 +0800466 @VisibleForTesting
467 protected static void updatePurposeView(TextView purpose, Credential item) {
468 int subTextResId = R.string.credential_for_vpn_and_apps;
469 if (!item.isSystem()) {
470 subTextResId = (item.isInUse())
471 ? R.string.credential_for_wifi_in_use
472 : R.string.credential_for_wifi;
473 }
474 purpose.setText(subTextResId);
475 }
476
477 @VisibleForTesting
478 protected static void updateUsedByViews(TextView title, TextView content, Credential item) {
479 List<String> usedByNames = item.getUsedByNames();
480 if (usedByNames.size() > 0) {
481 title.setVisibility(View.VISIBLE);
482 content.setText(String.join("\n", usedByNames));
483 content.setVisibility(View.VISIBLE);
484 } else {
485 title.setVisibility(View.GONE);
486 content.setVisibility(View.GONE);
487 }
488 }
489
Robin Leee68d9572016-07-19 16:10:39 +0100490 static class AliasEntry {
491 public String alias;
492 public int uid;
493 }
494
Robin Leee2680422016-01-25 12:24:27 +0000495 static class Credential implements Parcelable {
496 static enum Type {
Robin Leebaefdcf2015-08-26 10:57:44 +0100497 CA_CERTIFICATE (Credentials.CA_CERTIFICATE),
498 USER_CERTIFICATE (Credentials.USER_CERTIFICATE),
Janis Danisevskisb705e1a2017-04-19 16:23:02 -0700499 USER_KEY(Credentials.USER_PRIVATE_KEY, Credentials.USER_SECRET_KEY);
Robin Leebaefdcf2015-08-26 10:57:44 +0100500
Janis Danisevskisb705e1a2017-04-19 16:23:02 -0700501 final String[] prefix;
Robin Leebaefdcf2015-08-26 10:57:44 +0100502
Janis Danisevskisb705e1a2017-04-19 16:23:02 -0700503 Type(String... prefix) {
Robin Leebaefdcf2015-08-26 10:57:44 +0100504 this.prefix = prefix;
505 }
506 }
507
508 /**
509 * Main part of the credential's alias. To fetch an item from KeyStore, prepend one of the
510 * prefixes from {@link CredentialItem.storedTypes}.
511 */
512 final String alias;
513
514 /**
Robin Leee68d9572016-07-19 16:10:39 +0100515 * UID under which this credential is stored. Typically {@link Process#SYSTEM_UID} but can
516 * also be {@link Process#WIFI_UID} for credentials installed as wifi certificates.
517 */
518 final int uid;
519
520 /**
Weng Su28791f52022-12-15 15:53:04 +0800521 * Indicate whether or not this credential is in use.
522 */
523 boolean mIsInUse;
524
525 /**
526 * The list of networks which use this credential.
527 */
528 List<String> mUsedByNames = new ArrayList<>();
529
530 /**
Robin Leebaefdcf2015-08-26 10:57:44 +0100531 * Should contain some non-empty subset of:
532 * <ul>
533 * <li>{@link Credentials.CA_CERTIFICATE}</li>
534 * <li>{@link Credentials.USER_CERTIFICATE}</li>
Janis Danisevskisb705e1a2017-04-19 16:23:02 -0700535 * <li>{@link Credentials.USER_KEY}</li>
Robin Leebaefdcf2015-08-26 10:57:44 +0100536 * </ul>
537 */
Robin Lee04046a12016-01-19 11:42:57 +0000538 final EnumSet<Type> storedTypes = EnumSet.noneOf(Type.class);
Robin Leebaefdcf2015-08-26 10:57:44 +0100539
Robin Leee68d9572016-07-19 16:10:39 +0100540 Credential(final String alias, final int uid) {
Robin Leebaefdcf2015-08-26 10:57:44 +0100541 this.alias = alias;
Robin Leee68d9572016-07-19 16:10:39 +0100542 this.uid = uid;
Robin Leebaefdcf2015-08-26 10:57:44 +0100543 }
Robin Lee04046a12016-01-19 11:42:57 +0000544
545 Credential(Parcel in) {
Robin Leee68d9572016-07-19 16:10:39 +0100546 this(in.readString(), in.readInt());
Robin Lee04046a12016-01-19 11:42:57 +0000547
548 long typeBits = in.readLong();
549 for (Type i : Type.values()) {
550 if ((typeBits & (1L << i.ordinal())) != 0L) {
551 storedTypes.add(i);
552 }
553 }
554 }
555
556 public void writeToParcel(Parcel out, int flags) {
557 out.writeString(alias);
Robin Leee68d9572016-07-19 16:10:39 +0100558 out.writeInt(uid);
Robin Lee04046a12016-01-19 11:42:57 +0000559
560 long typeBits = 0;
561 for (Type i : storedTypes) {
562 typeBits |= 1L << i.ordinal();
563 }
564 out.writeLong(typeBits);
565 }
566
567 public int describeContents() {
568 return 0;
569 }
570
571 public static final Parcelable.Creator<Credential> CREATOR
572 = new Parcelable.Creator<Credential>() {
573 public Credential createFromParcel(Parcel in) {
574 return new Credential(in);
575 }
576
577 public Credential[] newArray(int size) {
578 return new Credential[size];
579 }
580 };
Robin Leee68d9572016-07-19 16:10:39 +0100581
582 public boolean isSystem() {
583 return UserHandle.getAppId(uid) == Process.SYSTEM_UID;
584 }
Hai Shalom23666052019-03-07 15:54:25 -0800585
Weng Su28791f52022-12-15 15:53:04 +0800586 public String getAlias() {
587 return alias;
588 }
Hai Shalom23666052019-03-07 15:54:25 -0800589
590 public EnumSet<Type> getStoredTypes() {
591 return storedTypes;
592 }
Weng Su28791f52022-12-15 15:53:04 +0800593
594 public void setInUse(boolean inUse) {
595 mIsInUse = inUse;
596 }
597
598 public boolean isInUse() {
599 return mIsInUse;
600 }
601
602 public void setUsedByNames(List<String> names) {
603 mUsedByNames = new ArrayList<>(names);
604 }
605
606 public List<String> getUsedByNames() {
607 return new ArrayList<String>(mUsedByNames);
608 }
Robin Leebaefdcf2015-08-26 10:57:44 +0100609 }
610}