| /* | |
| * Copyright (C) 2022 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. | |
| */ | |
| package com.android.settings; | |
| import static android.app.admin.DevicePolicyResources.Strings.Settings.PERSONAL_CATEGORY_HEADER; | |
| import static android.app.admin.DevicePolicyResources.Strings.Settings.PRIVATE_CATEGORY_HEADER; | |
| import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_CATEGORY_HEADER; | |
| import static android.widget.LinearLayout.LayoutParams.MATCH_PARENT; | |
| import static android.widget.LinearLayout.LayoutParams.WRAP_CONTENT; | |
| import android.annotation.UiThread; | |
| import android.app.Activity; | |
| import android.app.KeyguardManager; | |
| import android.app.admin.DevicePolicyManager; | |
| import android.content.BroadcastReceiver; | |
| import android.content.Context; | |
| import android.content.Intent; | |
| import android.content.IntentFilter; | |
| import android.content.pm.UserInfo; | |
| import android.content.res.TypedArray; | |
| import android.database.DataSetObserver; | |
| import android.graphics.drawable.Drawable; | |
| import android.net.http.SslCertificate; | |
| import android.os.AsyncTask; | |
| import android.os.Bundle; | |
| import android.os.Parcelable; | |
| import android.os.RemoteException; | |
| import android.os.UserHandle; | |
| import android.os.UserManager; | |
| import android.security.IKeyChainService; | |
| import android.security.KeyChain; | |
| import android.security.KeyChain.KeyChainConnection; | |
| import android.util.ArraySet; | |
| import android.util.Log; | |
| import android.util.SparseArray; | |
| import android.view.LayoutInflater; | |
| import android.view.View; | |
| import android.view.ViewGroup; | |
| import android.widget.AdapterView; | |
| import android.widget.BaseAdapter; | |
| import android.widget.BaseExpandableListAdapter; | |
| import android.widget.CompoundButton; | |
| import android.widget.ExpandableListView; | |
| import android.widget.FrameLayout; | |
| import android.widget.ImageView; | |
| import android.widget.LinearLayout; | |
| import android.widget.ListView; | |
| import android.widget.ProgressBar; | |
| import android.widget.TextView; | |
| import androidx.annotation.NonNull; | |
| import com.android.internal.annotations.GuardedBy; | |
| import com.android.internal.app.UnlaunchableAppActivity; | |
| import com.android.internal.widget.LockPatternUtils; | |
| import com.android.settings.TrustedCredentialsSettings.Tab; | |
| import com.android.settingslib.core.lifecycle.ObservableFragment; | |
| import java.security.cert.CertificateEncodingException; | |
| import java.security.cert.X509Certificate; | |
| import java.util.ArrayList; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import java.util.Set; | |
| import java.util.function.IntConsumer; | |
| /** | |
| * Fragment to display trusted credentials settings for one tab. | |
| */ | |
| public class TrustedCredentialsFragment extends ObservableFragment | |
| implements TrustedCredentialsDialogBuilder.DelegateInterface { | |
| public static final String ARG_POSITION = "tab"; | |
| public static final String ARG_SHOW_NEW_FOR_USER = "ARG_SHOW_NEW_FOR_USER"; | |
| private static final String TAG = "TrustedCredentialsFragment"; | |
| private DevicePolicyManager mDevicePolicyManager; | |
| private UserManager mUserManager; | |
| private KeyguardManager mKeyguardManager; | |
| private int mTrustAllCaUserId; | |
| private static final String SAVED_CONFIRMED_CREDENTIAL_USERS = "ConfirmedCredentialUsers"; | |
| private static final String SAVED_CONFIRMING_CREDENTIAL_USER = "ConfirmingCredentialUser"; | |
| private static final int REQUEST_CONFIRM_CREDENTIALS = 1; | |
| private GroupAdapter mGroupAdapter; | |
| private AliasOperation mAliasOperation; | |
| private ArraySet<Integer> mConfirmedCredentialUsers; | |
| private int mConfirmingCredentialUser; | |
| private IntConsumer mConfirmingCredentialListener; | |
| private final Set<AdapterData.AliasLoader> mAliasLoaders = new ArraySet<>(2); | |
| @GuardedBy("mKeyChainConnectionByProfileId") | |
| private final SparseArray<KeyChainConnection> | |
| mKeyChainConnectionByProfileId = new SparseArray<>(); | |
| private ViewGroup mFragmentView; | |
| private final BroadcastReceiver mProfileChangedReceiver = new BroadcastReceiver() { | |
| @Override | |
| public void onReceive(Context context, Intent intent) { | |
| if (isBroadcastValidForAction(intent)) { | |
| mGroupAdapter.load(); | |
| } | |
| } | |
| }; | |
| private boolean isBroadcastValidForAction(Intent intent) { | |
| String action = intent.getAction(); | |
| if (android.os.Flags.allowPrivateProfile() | |
| && android.multiuser.Flags.enablePrivateSpaceFeatures() | |
| && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) { | |
| UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class); | |
| if (userHandle == null) { | |
| Log.w(TAG, "received action " + action + " with missing user extra"); | |
| return false; | |
| } | |
| UserInfo userInfo = mUserManager.getUserInfo(userHandle.getIdentifier()); | |
| return (Intent.ACTION_PROFILE_AVAILABLE.equals(action) | |
| || Intent.ACTION_PROFILE_UNAVAILABLE.equals(action) | |
| || Intent.ACTION_PROFILE_ACCESSIBLE.equals(action)) | |
| && (userInfo.isManagedProfile() || userInfo.isPrivateProfile()); | |
| } | |
| return (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) | |
| || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action) | |
| || Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)); | |
| } | |
| @Override | |
| public void onCreate(Bundle savedInstanceState) { | |
| super.onCreate(savedInstanceState); | |
| Activity activity = getActivity(); | |
| mDevicePolicyManager = activity.getSystemService(DevicePolicyManager.class); | |
| mUserManager = activity.getSystemService(UserManager.class); | |
| mKeyguardManager = activity.getSystemService(KeyguardManager.class); | |
| mTrustAllCaUserId = activity.getIntent().getIntExtra(ARG_SHOW_NEW_FOR_USER, | |
| UserHandle.USER_NULL); | |
| mConfirmedCredentialUsers = new ArraySet<>(2); | |
| mConfirmingCredentialUser = UserHandle.USER_NULL; | |
| if (savedInstanceState != null) { | |
| mConfirmingCredentialUser = savedInstanceState.getInt(SAVED_CONFIRMING_CREDENTIAL_USER, | |
| UserHandle.USER_NULL); | |
| ArrayList<Integer> users = savedInstanceState.getIntegerArrayList( | |
| SAVED_CONFIRMED_CREDENTIAL_USERS); | |
| if (users != null) { | |
| mConfirmedCredentialUsers.addAll(users); | |
| } | |
| } | |
| IntentFilter filter = new IntentFilter(); | |
| if (android.os.Flags.allowPrivateProfile() | |
| && android.multiuser.Flags.enablePrivateSpaceFeatures() | |
| && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) { | |
| filter.addAction(Intent.ACTION_PROFILE_AVAILABLE); | |
| filter.addAction(Intent.ACTION_PROFILE_UNAVAILABLE); | |
| filter.addAction(Intent.ACTION_PROFILE_ACCESSIBLE); | |
| } else { | |
| filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); | |
| filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); | |
| filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED); | |
| } | |
| activity.registerReceiver(mProfileChangedReceiver, filter); | |
| } | |
| @Override | |
| public void onSaveInstanceState(Bundle outState) { | |
| super.onSaveInstanceState(outState); | |
| outState.putIntegerArrayList(SAVED_CONFIRMED_CREDENTIAL_USERS, new ArrayList<>( | |
| mConfirmedCredentialUsers)); | |
| outState.putInt(SAVED_CONFIRMING_CREDENTIAL_USER, mConfirmingCredentialUser); | |
| mGroupAdapter.saveState(outState); | |
| } | |
| @Override | |
| public View onCreateView( | |
| LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { | |
| mFragmentView = (ViewGroup) inflater.inflate(R.layout.trusted_credentials, parent, false); | |
| ViewGroup contentView = mFragmentView.findViewById(R.id.content); | |
| mGroupAdapter = new GroupAdapter( | |
| requireArguments().getInt(ARG_POSITION) == 0 ? Tab.SYSTEM : Tab.USER); | |
| int profilesSize = mGroupAdapter.getGroupCount(); | |
| for (int i = 0; i < profilesSize; i++) { | |
| Bundle childState = savedInstanceState == null ? null | |
| : savedInstanceState.getBundle(mGroupAdapter.getKey(i)); | |
| createChildView(inflater, contentView, childState, i); | |
| } | |
| return mFragmentView; | |
| } | |
| private void createChildView( | |
| LayoutInflater inflater, ViewGroup parent, Bundle childState, int i) { | |
| UserInfo userInfo = mGroupAdapter.getUserInfoByGroup(i); | |
| if (Utils.shouldHideUser(userInfo.getUserHandle(), mUserManager)) { | |
| return; | |
| } | |
| boolean isProfile = userInfo.isManagedProfile(); | |
| if (android.os.Flags.allowPrivateProfile() | |
| && android.multiuser.Flags.enablePrivateSpaceFeatures() | |
| && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) { | |
| isProfile |= userInfo.isPrivateProfile(); | |
| } | |
| ChildAdapter adapter = mGroupAdapter.createChildAdapter(i); | |
| LinearLayout containerView = (LinearLayout) inflater.inflate( | |
| R.layout.trusted_credential_list_container, parent, false); | |
| adapter.setContainerView(containerView, childState); | |
| int profilesSize = mGroupAdapter.getGroupCount(); | |
| adapter.showHeader(profilesSize > 1); | |
| adapter.showDivider(isProfile); | |
| adapter.setExpandIfAvailable(profilesSize <= 2 || !isProfile, childState); | |
| if (isProfile) { | |
| parent.addView(containerView); | |
| } else { | |
| parent.addView(containerView, 0); | |
| } | |
| } | |
| @Override | |
| public void onResume() { | |
| super.onResume(); | |
| mFragmentView.requestLayout(); | |
| } | |
| @Override | |
| public void onDestroy() { | |
| getActivity().unregisterReceiver(mProfileChangedReceiver); | |
| for (AdapterData.AliasLoader aliasLoader : mAliasLoaders) { | |
| aliasLoader.cancel(true); | |
| } | |
| mAliasLoaders.clear(); | |
| if (mAliasOperation != null) { | |
| mAliasOperation.cancel(true); | |
| mAliasOperation = null; | |
| } | |
| closeKeyChainConnections(); | |
| super.onDestroy(); | |
| } | |
| @Override | |
| public void onActivityResult(int requestCode, int resultCode, Intent data) { | |
| if (requestCode == REQUEST_CONFIRM_CREDENTIALS) { | |
| int userId = mConfirmingCredentialUser; | |
| IntConsumer listener = mConfirmingCredentialListener; | |
| // reset them before calling the listener because the listener may call back to start | |
| // activity again. (though it should never happen.) | |
| mConfirmingCredentialUser = UserHandle.USER_NULL; | |
| mConfirmingCredentialListener = null; | |
| if (resultCode == Activity.RESULT_OK) { | |
| mConfirmedCredentialUsers.add(userId); | |
| if (listener != null) { | |
| listener.accept(userId); | |
| } | |
| } | |
| } | |
| } | |
| private void closeKeyChainConnections() { | |
| synchronized (mKeyChainConnectionByProfileId) { | |
| int n = mKeyChainConnectionByProfileId.size(); | |
| for (int i = 0; i < n; ++i) { | |
| mKeyChainConnectionByProfileId.valueAt(i).close(); | |
| } | |
| mKeyChainConnectionByProfileId.clear(); | |
| } | |
| } | |
| /** | |
| * Start work challenge activity. | |
| * | |
| * @return true if screenlock exists | |
| */ | |
| private boolean startConfirmCredential(int userId) { | |
| Intent newIntent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, null, userId); | |
| if (newIntent == null) { | |
| return false; | |
| } | |
| mConfirmingCredentialUser = userId; | |
| startActivityForResult(newIntent, REQUEST_CONFIRM_CREDENTIALS); | |
| return true; | |
| } | |
| /** | |
| * Adapter for expandable list view of certificates. Groups in the view correspond to profiles | |
| * whereas children correspond to certificates. | |
| */ | |
| private class GroupAdapter extends BaseExpandableListAdapter implements | |
| ExpandableListView.OnGroupClickListener, ExpandableListView.OnChildClickListener { | |
| private final AdapterData mData; | |
| private final ArrayList<ChildAdapter> mChildAdapters = new ArrayList<>(); | |
| private GroupAdapter(Tab tab) { | |
| mData = new AdapterData(tab, this); | |
| load(); | |
| } | |
| @Override | |
| public int getGroupCount() { | |
| return mData.mCertHoldersByUserId.size(); | |
| } | |
| @Override | |
| public int getChildrenCount(int groupPosition) { | |
| List<CertHolder> certHolders = mData.mCertHoldersByUserId.valueAt(groupPosition); | |
| if (certHolders != null) { | |
| return certHolders.size(); | |
| } | |
| return 0; | |
| } | |
| @Override | |
| public UserHandle getGroup(int groupPosition) { | |
| return new UserHandle(mData.mCertHoldersByUserId.keyAt(groupPosition)); | |
| } | |
| @Override | |
| public CertHolder getChild(int groupPosition, int childPosition) { | |
| return mData.mCertHoldersByUserId.get(getUserIdByGroup(groupPosition)).get( | |
| childPosition); | |
| } | |
| @Override | |
| public long getGroupId(int groupPosition) { | |
| return getUserIdByGroup(groupPosition); | |
| } | |
| private int getUserIdByGroup(int groupPosition) { | |
| return mData.mCertHoldersByUserId.keyAt(groupPosition); | |
| } | |
| public UserInfo getUserInfoByGroup(int groupPosition) { | |
| return mUserManager.getUserInfo(getUserIdByGroup(groupPosition)); | |
| } | |
| @Override | |
| public long getChildId(int groupPosition, int childPosition) { | |
| return childPosition; | |
| } | |
| @Override | |
| public boolean hasStableIds() { | |
| return false; | |
| } | |
| @Override | |
| public View getGroupView(int groupPosition, boolean isExpanded, View convertView, | |
| ViewGroup parent) { | |
| if (convertView == null) { | |
| LayoutInflater inflater = (LayoutInflater) getActivity() | |
| .getSystemService(Context.LAYOUT_INFLATER_SERVICE); | |
| convertView = Utils.inflateCategoryHeader(inflater, parent); | |
| } | |
| TextView title = convertView.findViewById(android.R.id.title); | |
| UserInfo userInfo = getUserInfoByGroup(groupPosition); | |
| if (userInfo.isManagedProfile()) { | |
| title.setText(mDevicePolicyManager.getResources().getString(WORK_CATEGORY_HEADER, | |
| () -> getString(com.android.settingslib.R.string.category_work))); | |
| } else if (android.os.Flags.allowPrivateProfile() | |
| && android.multiuser.Flags.enablePrivateSpaceFeatures() | |
| && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace() | |
| && userInfo.isPrivateProfile()) { | |
| title.setText(mDevicePolicyManager.getResources().getString(PRIVATE_CATEGORY_HEADER, | |
| () -> getString(com.android.settingslib.R.string.category_private))); | |
| } else { | |
| title.setText(mDevicePolicyManager.getResources().getString( | |
| PERSONAL_CATEGORY_HEADER, | |
| () -> getString(com.android.settingslib.R.string.category_personal))); | |
| } | |
| title.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); | |
| return convertView; | |
| } | |
| @Override | |
| public View getChildView(int groupPosition, int childPosition, boolean isLastChild, | |
| View convertView, ViewGroup parent) { | |
| return getViewForCertificate(getChild(groupPosition, childPosition), mData.mTab, | |
| convertView, parent); | |
| } | |
| @Override | |
| public boolean isChildSelectable(int groupPosition, int childPosition) { | |
| return true; | |
| } | |
| @Override | |
| public boolean onChildClick(ExpandableListView expandableListView, View view, | |
| int groupPosition, int childPosition, long id) { | |
| showCertDialog(getChild(groupPosition, childPosition)); | |
| return true; | |
| } | |
| @Override | |
| public boolean onGroupClick(ExpandableListView expandableListView, View view, | |
| int groupPosition, long id) { | |
| return !checkGroupExpandableAndStartWarningActivity(groupPosition); | |
| } | |
| public void load() { | |
| mData.new AliasLoader().execute(); | |
| } | |
| public void remove(CertHolder certHolder) { | |
| mData.remove(certHolder); | |
| } | |
| ChildAdapter createChildAdapter(int groupPosition) { | |
| ChildAdapter childAdapter = new ChildAdapter(this, groupPosition); | |
| mChildAdapters.add(childAdapter); | |
| return childAdapter; | |
| } | |
| public boolean checkGroupExpandableAndStartWarningActivity(int groupPosition) { | |
| return checkGroupExpandableAndStartWarningActivity(groupPosition, true); | |
| } | |
| public boolean checkGroupExpandableAndStartWarningActivity(int groupPosition, | |
| boolean startActivity) { | |
| UserHandle groupUser = getGroup(groupPosition); | |
| int groupUserId = groupUser.getIdentifier(); | |
| if (mUserManager.isQuietModeEnabled(groupUser)) { | |
| if (startActivity) { | |
| Intent intent = | |
| UnlaunchableAppActivity.createInQuietModeDialogIntent(groupUserId); | |
| getActivity().startActivity(intent); | |
| } | |
| return false; | |
| } else if (!mUserManager.isUserUnlocked(groupUser)) { | |
| LockPatternUtils lockPatternUtils = new LockPatternUtils(getActivity()); | |
| if (lockPatternUtils.isSeparateProfileChallengeEnabled(groupUserId)) { | |
| if (startActivity) { | |
| startConfirmCredential(groupUserId); | |
| } | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| private View getViewForCertificate(CertHolder certHolder, Tab mTab, View convertView, | |
| ViewGroup parent) { | |
| ViewHolder holder; | |
| if (convertView == null) { | |
| holder = new ViewHolder(); | |
| LayoutInflater inflater = LayoutInflater.from(getActivity()); | |
| convertView = inflater.inflate(R.layout.trusted_credential, parent, false); | |
| convertView.setTag(holder); | |
| holder.mSubjectPrimaryView = | |
| convertView.findViewById(R.id.trusted_credential_subject_primary); | |
| holder.mSubjectSecondaryView = | |
| convertView.findViewById(R.id.trusted_credential_subject_secondary); | |
| holder.mSwitch = convertView.findViewById(R.id.trusted_credential_status); | |
| holder.mSwitch.setOnClickListener(view -> { | |
| removeOrInstallCert((CertHolder) view.getTag()); | |
| }); | |
| } else { | |
| holder = (ViewHolder) convertView.getTag(); | |
| } | |
| holder.mSubjectPrimaryView.setText(certHolder.mSubjectPrimary); | |
| holder.mSubjectSecondaryView.setText(certHolder.mSubjectSecondary); | |
| if (mTab.mSwitch) { | |
| holder.mSwitch.setChecked(!certHolder.mDeleted); | |
| holder.mSwitch.setEnabled(!mUserManager.hasUserRestriction( | |
| UserManager.DISALLOW_CONFIG_CREDENTIALS, | |
| new UserHandle(certHolder.mProfileId))); | |
| holder.mSwitch.setVisibility(View.VISIBLE); | |
| holder.mSwitch.setTag(certHolder); | |
| } | |
| return convertView; | |
| } | |
| private void saveState(Bundle outState) { | |
| for (int groupPosition = 0, mChildAdaptersSize = mChildAdapters.size(); | |
| groupPosition < mChildAdaptersSize; groupPosition++) { | |
| ChildAdapter childAdapter = mChildAdapters.get(groupPosition); | |
| outState.putBundle(getKey(groupPosition), childAdapter.saveState()); | |
| } | |
| } | |
| @NonNull | |
| private String getKey(int groupPosition) { | |
| return "Group" + getUserIdByGroup(groupPosition); | |
| } | |
| private static class ViewHolder { | |
| private TextView mSubjectPrimaryView; | |
| private TextView mSubjectSecondaryView; | |
| private CompoundButton mSwitch; | |
| } | |
| } | |
| private class ChildAdapter extends BaseAdapter implements View.OnClickListener, | |
| AdapterView.OnItemClickListener { | |
| private static final String KEY_CONTAINER = "Container"; | |
| private static final String KEY_IS_LIST_EXPANDED = "IsListExpanded"; | |
| private final int[] mGroupExpandedStateSet = {com.android.internal.R.attr.state_expanded}; | |
| private final int[] mEmptyStateSet = {}; | |
| private final LinearLayout.LayoutParams mHideContainerLayoutParams = | |
| new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT, 0f); | |
| private final LinearLayout.LayoutParams mHideListLayoutParams = | |
| new LinearLayout.LayoutParams(MATCH_PARENT, 0); | |
| private final LinearLayout.LayoutParams mShowLayoutParams = new LinearLayout.LayoutParams( | |
| LinearLayout.LayoutParams.MATCH_PARENT, MATCH_PARENT, 1f); | |
| private final GroupAdapter mParent; | |
| private final int mGroupPosition; | |
| /* | |
| * This class doesn't hold the actual data. Events should notify parent. | |
| * When notifying DataSet events in this class, events should be forwarded to mParent. | |
| * i.e. this.notifyDataSetChanged -> mParent.notifyDataSetChanged -> mObserver.onChanged | |
| * -> outsideObservers.onChanged() (e.g. ListView) | |
| */ | |
| private final DataSetObserver mObserver = new DataSetObserver() { | |
| @Override | |
| public void onChanged() { | |
| super.onChanged(); | |
| TrustedCredentialsFragment.ChildAdapter.super.notifyDataSetChanged(); | |
| } | |
| @Override | |
| public void onInvalidated() { | |
| super.onInvalidated(); | |
| TrustedCredentialsFragment.ChildAdapter.super.notifyDataSetInvalidated(); | |
| } | |
| }; | |
| private boolean mIsListExpanded = true; | |
| private LinearLayout mContainerView; | |
| private ViewGroup mHeaderView; | |
| private ListView mListView; | |
| private ImageView mIndicatorView; | |
| private ChildAdapter(GroupAdapter parent, int groupPosition) { | |
| mParent = parent; | |
| mGroupPosition = groupPosition; | |
| mParent.registerDataSetObserver(mObserver); | |
| } | |
| @Override | |
| public int getCount() { | |
| return mParent.getChildrenCount(mGroupPosition); | |
| } | |
| @Override | |
| public CertHolder getItem(int position) { | |
| return mParent.getChild(mGroupPosition, position); | |
| } | |
| @Override | |
| public long getItemId(int position) { | |
| return mParent.getChildId(mGroupPosition, position); | |
| } | |
| @Override | |
| public View getView(int position, View convertView, ViewGroup parent) { | |
| return mParent.getChildView(mGroupPosition, position, false, convertView, parent); | |
| } | |
| // DataSet events | |
| @Override | |
| public void notifyDataSetChanged() { | |
| // Don't call super as the parent will propagate this event back later in mObserver | |
| mParent.notifyDataSetChanged(); | |
| } | |
| @Override | |
| public void notifyDataSetInvalidated() { | |
| // Don't call super as the parent will propagate this event back later in mObserver | |
| mParent.notifyDataSetInvalidated(); | |
| } | |
| // View related codes | |
| @Override | |
| public void onClick(View view) { | |
| mIsListExpanded = checkGroupExpandableAndStartWarningActivity() && !mIsListExpanded; | |
| refreshViews(); | |
| } | |
| @Override | |
| public void onItemClick(AdapterView<?> adapterView, View view, int pos, long id) { | |
| showCertDialog(getItem(pos)); | |
| } | |
| public void setContainerView(LinearLayout containerView, Bundle savedState) { | |
| mContainerView = containerView; | |
| // Handle manually because multiple groups with same id elements. | |
| mContainerView.setSaveFromParentEnabled(false); | |
| mListView = mContainerView.findViewById(R.id.cert_list); | |
| mListView.setAdapter(this); | |
| mListView.setOnItemClickListener(this); | |
| mListView.setItemsCanFocus(true); | |
| mHeaderView = mContainerView.findViewById(R.id.header_view); | |
| mHeaderView.setOnClickListener(this); | |
| mIndicatorView = mHeaderView.findViewById(R.id.group_indicator); | |
| mIndicatorView.setImageDrawable(getGroupIndicator()); | |
| FrameLayout headerContentContainer = | |
| mHeaderView.findViewById(R.id.header_content_container); | |
| headerContentContainer.addView( | |
| mParent.getGroupView(mGroupPosition, true /* parent ignores it */, null, | |
| headerContentContainer)); | |
| if (savedState != null) { | |
| SparseArray<Parcelable> containerStates = | |
| savedState.getSparseParcelableArray(KEY_CONTAINER, Parcelable.class); | |
| if (containerStates != null) { | |
| mContainerView.restoreHierarchyState(containerStates); | |
| } | |
| } | |
| } | |
| public void showHeader(boolean showHeader) { | |
| mHeaderView.setVisibility(showHeader ? View.VISIBLE : View.GONE); | |
| } | |
| public void showDivider(boolean showDivider) { | |
| View dividerView = mHeaderView.findViewById(R.id.header_divider); | |
| dividerView.setVisibility(showDivider ? View.VISIBLE : View.GONE); | |
| } | |
| public void setExpandIfAvailable(boolean expanded, Bundle savedState) { | |
| if (savedState != null) { | |
| expanded = savedState.getBoolean(KEY_IS_LIST_EXPANDED); | |
| } | |
| mIsListExpanded = expanded && mParent.checkGroupExpandableAndStartWarningActivity( | |
| mGroupPosition, false /* startActivity */); | |
| refreshViews(); | |
| } | |
| private boolean checkGroupExpandableAndStartWarningActivity() { | |
| return mParent.checkGroupExpandableAndStartWarningActivity(mGroupPosition); | |
| } | |
| private void refreshViews() { | |
| mIndicatorView.setImageState(mIsListExpanded ? mGroupExpandedStateSet | |
| : mEmptyStateSet, false); | |
| mListView.setLayoutParams(mIsListExpanded ? mShowLayoutParams | |
| : mHideListLayoutParams); | |
| mContainerView.setLayoutParams(mIsListExpanded ? mShowLayoutParams | |
| : mHideContainerLayoutParams); | |
| } | |
| // Get group indicator from styles of ExpandableListView | |
| private Drawable getGroupIndicator() { | |
| TypedArray a = getActivity().obtainStyledAttributes(null, | |
| com.android.internal.R.styleable.ExpandableListView, | |
| com.android.internal.R.attr.expandableListViewStyle, 0); | |
| Drawable groupIndicator = a.getDrawable( | |
| com.android.internal.R.styleable.ExpandableListView_groupIndicator); | |
| a.recycle(); | |
| return groupIndicator; | |
| } | |
| private Bundle saveState() { | |
| Bundle bundle = new Bundle(); | |
| SparseArray<Parcelable> states = new SparseArray<>(); | |
| mContainerView.saveHierarchyState(states); | |
| bundle.putSparseParcelableArray(KEY_CONTAINER, states); | |
| bundle.putBoolean(KEY_IS_LIST_EXPANDED, mIsListExpanded); | |
| return bundle; | |
| } | |
| } | |
| private class AdapterData { | |
| private final SparseArray<List<CertHolder>> mCertHoldersByUserId = | |
| new SparseArray<>(); | |
| private final Tab mTab; | |
| private final GroupAdapter mAdapter; | |
| private AdapterData(Tab tab, GroupAdapter adapter) { | |
| mAdapter = adapter; | |
| mTab = tab; | |
| } | |
| private class AliasLoader extends AsyncTask<Void, Integer, SparseArray<List<CertHolder>>> { | |
| private ProgressBar mProgressBar; | |
| private View mContentView; | |
| private Context mContext; | |
| AliasLoader() { | |
| mContext = getActivity(); | |
| mAliasLoaders.add(this); | |
| List<UserHandle> profiles = mUserManager.getUserProfiles(); | |
| for (UserHandle profile : profiles) { | |
| mCertHoldersByUserId.put(profile.getIdentifier(), new ArrayList<>()); | |
| } | |
| } | |
| private boolean shouldSkipProfile(UserHandle userHandle) { | |
| return mUserManager.isQuietModeEnabled(userHandle) | |
| || !mUserManager.isUserUnlocked(userHandle.getIdentifier()); | |
| } | |
| @Override | |
| protected void onPreExecute() { | |
| mProgressBar = mFragmentView.findViewById(R.id.progress); | |
| mContentView = mFragmentView.findViewById(R.id.content); | |
| mProgressBar.setVisibility(View.VISIBLE); | |
| mContentView.setVisibility(View.GONE); | |
| } | |
| @Override | |
| protected SparseArray<List<CertHolder>> doInBackground(Void... params) { | |
| SparseArray<List<CertHolder>> certHoldersByProfile = | |
| new SparseArray<>(); | |
| try { | |
| synchronized (mKeyChainConnectionByProfileId) { | |
| List<UserHandle> profiles = mUserManager.getUserProfiles(); | |
| // First we get all aliases for all profiles in order to show progress | |
| // correctly. Otherwise this could all be in a single loop. | |
| SparseArray<List<String>> aliasesByProfileId = | |
| new SparseArray<>(profiles.size()); | |
| int max = 0; | |
| int progress = 0; | |
| for (UserHandle profile : profiles) { | |
| int profileId = profile.getIdentifier(); | |
| if (shouldSkipProfile(profile)) { | |
| continue; | |
| } | |
| KeyChainConnection keyChainConnection = KeyChain.bindAsUser(mContext, | |
| profile); | |
| // Saving the connection for later use on the certificate dialog. | |
| mKeyChainConnectionByProfileId.put(profileId, keyChainConnection); | |
| IKeyChainService service = keyChainConnection.getService(); | |
| List<String> aliases = mTab.getAliases(service); | |
| if (isCancelled()) { | |
| return new SparseArray<>(); | |
| } | |
| max += aliases.size(); | |
| aliasesByProfileId.put(profileId, aliases); | |
| } | |
| for (UserHandle profile : profiles) { | |
| int profileId = profile.getIdentifier(); | |
| List<String> aliases = aliasesByProfileId.get(profileId); | |
| if (isCancelled()) { | |
| return new SparseArray<>(); | |
| } | |
| KeyChainConnection keyChainConnection = | |
| mKeyChainConnectionByProfileId.get( | |
| profileId); | |
| if (shouldSkipProfile(profile) || aliases == null | |
| || keyChainConnection == null) { | |
| certHoldersByProfile.put(profileId, new ArrayList<>(0)); | |
| continue; | |
| } | |
| IKeyChainService service = keyChainConnection.getService(); | |
| List<CertHolder> certHolders = new ArrayList<>(max); | |
| for (String alias : aliases) { | |
| byte[] encodedCertificate = service.getEncodedCaCertificate(alias, | |
| true); | |
| X509Certificate cert = KeyChain.toCertificate(encodedCertificate); | |
| certHolders.add(new CertHolder(service, mAdapter, | |
| mTab, alias, cert, profileId)); | |
| publishProgress(++progress, max); | |
| } | |
| Collections.sort(certHolders); | |
| certHoldersByProfile.put(profileId, certHolders); | |
| } | |
| return certHoldersByProfile; | |
| } | |
| } catch (RemoteException e) { | |
| Log.e(TAG, "Remote exception while loading aliases.", e); | |
| return new SparseArray<>(); | |
| } catch (InterruptedException e) { | |
| Log.e(TAG, "InterruptedException while loading aliases.", e); | |
| return new SparseArray<>(); | |
| } | |
| } | |
| @Override | |
| protected void onProgressUpdate(Integer... progressAndMax) { | |
| int progress = progressAndMax[0]; | |
| int max = progressAndMax[1]; | |
| if (max != mProgressBar.getMax()) { | |
| mProgressBar.setMax(max); | |
| } | |
| mProgressBar.setProgress(progress); | |
| } | |
| @Override | |
| protected void onPostExecute(SparseArray<List<CertHolder>> certHolders) { | |
| mCertHoldersByUserId.clear(); | |
| int n = certHolders.size(); | |
| for (int i = 0; i < n; ++i) { | |
| mCertHoldersByUserId.put(certHolders.keyAt(i), certHolders.valueAt(i)); | |
| } | |
| mAdapter.notifyDataSetChanged(); | |
| mProgressBar.setVisibility(View.GONE); | |
| mContentView.setVisibility(View.VISIBLE); | |
| mProgressBar.setProgress(0); | |
| mAliasLoaders.remove(this); | |
| showTrustAllCaDialogIfNeeded(); | |
| } | |
| private boolean isUserTabAndTrustAllCertMode() { | |
| return isTrustAllCaCertModeInProgress() && mTab == Tab.USER; | |
| } | |
| @UiThread | |
| private void showTrustAllCaDialogIfNeeded() { | |
| if (!isUserTabAndTrustAllCertMode()) { | |
| return; | |
| } | |
| List<CertHolder> certHolders = mCertHoldersByUserId.get(mTrustAllCaUserId); | |
| if (certHolders == null) { | |
| return; | |
| } | |
| List<CertHolder> unapprovedUserCertHolders = new ArrayList<>(); | |
| DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); | |
| for (CertHolder cert : certHolders) { | |
| if (cert != null && !dpm.isCaCertApproved(cert.mAlias, mTrustAllCaUserId)) { | |
| unapprovedUserCertHolders.add(cert); | |
| } | |
| } | |
| if (unapprovedUserCertHolders.size() == 0) { | |
| Log.w(TAG, "no cert is pending approval for user " + mTrustAllCaUserId); | |
| return; | |
| } | |
| showTrustAllCaDialog(unapprovedUserCertHolders); | |
| } | |
| } | |
| public void remove(CertHolder certHolder) { | |
| if (mCertHoldersByUserId != null) { | |
| List<CertHolder> certs = mCertHoldersByUserId.get(certHolder.mProfileId); | |
| if (certs != null) { | |
| certs.remove(certHolder); | |
| } | |
| } | |
| } | |
| } | |
| /* package */ static class CertHolder implements Comparable<CertHolder> { | |
| public int mProfileId; | |
| private final IKeyChainService mService; | |
| private final GroupAdapter mAdapter; | |
| private final Tab mTab; | |
| private final String mAlias; | |
| private final X509Certificate mX509Cert; | |
| private final SslCertificate mSslCert; | |
| private final String mSubjectPrimary; | |
| private final String mSubjectSecondary; | |
| private boolean mDeleted; | |
| private CertHolder(IKeyChainService service, | |
| GroupAdapter adapter, | |
| Tab tab, | |
| String alias, | |
| X509Certificate x509Cert, | |
| int profileId) { | |
| mProfileId = profileId; | |
| mService = service; | |
| mAdapter = adapter; | |
| mTab = tab; | |
| mAlias = alias; | |
| mX509Cert = x509Cert; | |
| mSslCert = new SslCertificate(x509Cert); | |
| String cn = mSslCert.getIssuedTo().getCName(); | |
| String o = mSslCert.getIssuedTo().getOName(); | |
| String ou = mSslCert.getIssuedTo().getUName(); | |
| // if we have a O, use O as primary subject, secondary prefer CN over OU | |
| // if we don't have an O, use CN as primary, empty secondary | |
| // if we don't have O or CN, use DName as primary, empty secondary | |
| if (!o.isEmpty()) { | |
| if (!cn.isEmpty()) { | |
| mSubjectPrimary = o; | |
| mSubjectSecondary = cn; | |
| } else { | |
| mSubjectPrimary = o; | |
| mSubjectSecondary = ou; | |
| } | |
| } else { | |
| if (!cn.isEmpty()) { | |
| mSubjectPrimary = cn; | |
| mSubjectSecondary = ""; | |
| } else { | |
| mSubjectPrimary = mSslCert.getIssuedTo().getDName(); | |
| mSubjectSecondary = ""; | |
| } | |
| } | |
| try { | |
| mDeleted = mTab.deleted(mService, mAlias); | |
| } catch (RemoteException e) { | |
| Log.e(TAG, "Remote exception while checking if alias " + mAlias + " is deleted.", | |
| e); | |
| mDeleted = false; | |
| } | |
| } | |
| @Override | |
| public int compareTo(CertHolder o) { | |
| int primary = this.mSubjectPrimary.compareToIgnoreCase(o.mSubjectPrimary); | |
| if (primary != 0) { | |
| return primary; | |
| } | |
| return this.mSubjectSecondary.compareToIgnoreCase(o.mSubjectSecondary); | |
| } | |
| @Override | |
| public boolean equals(Object o) { | |
| if (!(o instanceof CertHolder)) { | |
| return false; | |
| } | |
| CertHolder other = (CertHolder) o; | |
| return mAlias.equals(other.mAlias); | |
| } | |
| @Override | |
| public int hashCode() { | |
| return mAlias.hashCode(); | |
| } | |
| public int getUserId() { | |
| return mProfileId; | |
| } | |
| public String getAlias() { | |
| return mAlias; | |
| } | |
| public boolean isSystemCert() { | |
| return mTab == Tab.SYSTEM; | |
| } | |
| public boolean isDeleted() { | |
| return mDeleted; | |
| } | |
| } | |
| private boolean isTrustAllCaCertModeInProgress() { | |
| return mTrustAllCaUserId != UserHandle.USER_NULL; | |
| } | |
| private void showTrustAllCaDialog(List<CertHolder> unapprovedCertHolders) { | |
| CertHolder[] arr = | |
| unapprovedCertHolders.toArray(new CertHolder[unapprovedCertHolders.size()]); | |
| new TrustedCredentialsDialogBuilder(getActivity(), this) | |
| .setCertHolders(arr) | |
| .setOnDismissListener(dialogInterface -> { | |
| // Avoid starting dialog again after Activity restart. | |
| getActivity().getIntent().removeExtra(ARG_SHOW_NEW_FOR_USER); | |
| mTrustAllCaUserId = UserHandle.USER_NULL; | |
| }) | |
| .show(); | |
| } | |
| private void showCertDialog(final CertHolder certHolder) { | |
| new TrustedCredentialsDialogBuilder(getActivity(), this) | |
| .setCertHolder(certHolder) | |
| .show(); | |
| } | |
| @Override | |
| public List<X509Certificate> getX509CertsFromCertHolder(CertHolder certHolder) { | |
| List<X509Certificate> certificates = null; | |
| try { | |
| synchronized (mKeyChainConnectionByProfileId) { | |
| KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( | |
| certHolder.mProfileId); | |
| IKeyChainService service = keyChainConnection.getService(); | |
| List<String> chain = service.getCaCertificateChainAliases(certHolder.mAlias, true); | |
| certificates = new ArrayList<>(chain.size()); | |
| for (String s : chain) { | |
| byte[] encodedCertificate = service.getEncodedCaCertificate(s, true); | |
| X509Certificate certificate = KeyChain.toCertificate(encodedCertificate); | |
| certificates.add(certificate); | |
| } | |
| } | |
| } catch (RemoteException ex) { | |
| Log.e(TAG, "RemoteException while retrieving certificate chain for root " | |
| + certHolder.mAlias, ex); | |
| } | |
| return certificates; | |
| } | |
| @Override | |
| public void removeOrInstallCert(CertHolder certHolder) { | |
| new AliasOperation(certHolder).execute(); | |
| } | |
| @Override | |
| public boolean startConfirmCredentialIfNotConfirmed(int userId, | |
| IntConsumer onCredentialConfirmedListener) { | |
| if (mConfirmedCredentialUsers.contains(userId)) { | |
| // Credential has been confirmed. Don't start activity. | |
| return false; | |
| } | |
| boolean result = startConfirmCredential(userId); | |
| if (result) { | |
| mConfirmingCredentialListener = onCredentialConfirmedListener; | |
| } | |
| return result; | |
| } | |
| private class AliasOperation extends AsyncTask<Void, Void, Boolean> { | |
| private final CertHolder mCertHolder; | |
| private AliasOperation(CertHolder certHolder) { | |
| mCertHolder = certHolder; | |
| mAliasOperation = this; | |
| } | |
| @Override | |
| protected Boolean doInBackground(Void... params) { | |
| try { | |
| synchronized (mKeyChainConnectionByProfileId) { | |
| KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( | |
| mCertHolder.mProfileId); | |
| IKeyChainService service = keyChainConnection.getService(); | |
| if (mCertHolder.mDeleted) { | |
| byte[] bytes = mCertHolder.mX509Cert.getEncoded(); | |
| service.installCaCertificate(bytes); | |
| return true; | |
| } else { | |
| return service.deleteCaCertificate(mCertHolder.mAlias); | |
| } | |
| } | |
| } catch (CertificateEncodingException | SecurityException | IllegalStateException | |
| | RemoteException e) { | |
| Log.w(TAG, "Error while toggling alias " + mCertHolder.mAlias, e); | |
| return false; | |
| } | |
| } | |
| @Override | |
| protected void onPostExecute(Boolean ok) { | |
| if (ok) { | |
| if (mCertHolder.mTab.mSwitch) { | |
| mCertHolder.mDeleted = !mCertHolder.mDeleted; | |
| } else { | |
| mCertHolder.mAdapter.remove(mCertHolder); | |
| } | |
| mCertHolder.mAdapter.notifyDataSetChanged(); | |
| } else { | |
| // bail, reload to reset to known state | |
| mCertHolder.mAdapter.load(); | |
| } | |
| mAliasOperation = null; | |
| } | |
| } | |
| } |