diff --git a/res/layout/trusted_credentials.xml b/res/layout/trusted_credentials.xml index 31f5f40a81f..3663a797282 100644 --- a/res/layout/trusted_credentials.xml +++ b/res/layout/trusted_credentials.xml @@ -13,78 +13,26 @@ See the License for the specific language governing permissions and limitations under the License. --> - + + - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone"> - + \ No newline at end of file diff --git a/src/com/android/settings/TrustedCredentialsDialogBuilder.java b/src/com/android/settings/TrustedCredentialsDialogBuilder.java index 806da92099b..0dc8c256761 100644 --- a/src/com/android/settings/TrustedCredentialsDialogBuilder.java +++ b/src/com/android/settings/TrustedCredentialsDialogBuilder.java @@ -34,7 +34,7 @@ import android.widget.Spinner; import androidx.appcompat.app.AlertDialog; import com.android.internal.widget.LockPatternUtils; -import com.android.settings.TrustedCredentialsSettings.CertHolder; +import com.android.settings.TrustedCredentialsFragment.CertHolder; import com.android.settingslib.RestrictedLockUtils; import java.security.cert.X509Certificate; diff --git a/src/com/android/settings/TrustedCredentialsFragment.java b/src/com/android/settings/TrustedCredentialsFragment.java new file mode 100644 index 00000000000..ca565a46c1e --- /dev/null +++ b/src/com/android/settings/TrustedCredentialsFragment.java @@ -0,0 +1,1030 @@ +/* + * 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.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.app.settings.SettingsEnums; +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.ExpandableListView; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.Switch; +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.settings.core.InstrumentedFragment; + +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 InstrumentedFragment + 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 mConfirmedCredentialUsers; + private int mConfirmingCredentialUser; + private IntConsumer mConfirmingCredentialListener; + private final Set mAliasLoaders = new ArraySet<>(2); + @GuardedBy("mKeyChainConnectionByProfileId") + private final SparseArray + mKeyChainConnectionByProfileId = new SparseArray<>(); + private ViewGroup mFragmentView; + + private final BroadcastReceiver mWorkProfileChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) + || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action) + || Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)) { + mGroupAdapter.load(); + } + } + }; + + @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 users = savedInstanceState.getIntegerArrayList( + SAVED_CONFIRMED_CREDENTIAL_USERS); + if (users != null) { + mConfirmedCredentialUsers.addAll(users); + } + } + + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); + filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); + filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED); + activity.registerReceiver(mWorkProfileChangedReceiver, 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; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.TRUSTED_CREDENTIALS; + } + + private void createChildView( + LayoutInflater inflater, ViewGroup parent, Bundle childState, int i) { + boolean isWork = mGroupAdapter.getUserInfoByGroup(i).isManagedProfile(); + 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(isWork); + adapter.setExpandIfAvailable(profilesSize <= 2 || !isWork, childState); + if (isWork) { + parent.addView(containerView); + } else { + parent.addView(containerView, 0); + } + } + + @Override + public void onResume() { + super.onResume(); + mFragmentView.requestLayout(); + } + + @Override + public void onDestroy() { + getActivity().unregisterReceiver(mWorkProfileChangedReceiver); + 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 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 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); + if (getUserInfoByGroup(groupPosition).isManagedProfile()) { + title.setText(mDevicePolicyManager.getResources().getString(WORK_CATEGORY_HEADER, + () -> getString(R.string.category_work))); + } else { + title.setText(mDevicePolicyManager.getResources().getString( + PERSONAL_CATEGORY_HEADER, + () -> getString(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 class ViewHolder { + private TextView mSubjectPrimaryView; + private TextView mSubjectSecondaryView; + private Switch 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 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 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> 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>> { + private ProgressBar mProgressBar; + private View mContentView; + private Context mContext; + + AliasLoader() { + mContext = getActivity(); + mAliasLoaders.add(this); + List 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> doInBackground(Void... params) { + SparseArray> certHoldersByProfile = + new SparseArray<>(); + try { + synchronized (mKeyChainConnectionByProfileId) { + List 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> 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 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 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 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> 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 certHolders = mCertHoldersByUserId.get(mTrustAllCaUserId); + if (certHolders == null) { + return; + } + + List 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 certs = mCertHoldersByUserId.get(certHolder.mProfileId); + if (certs != null) { + certs.remove(certHolder); + } + } + } + } + + /* package */ static class CertHolder implements Comparable { + 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 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 getX509CertsFromCertHolder(CertHolder certHolder) { + List certificates = null; + try { + synchronized (mKeyChainConnectionByProfileId) { + KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( + certHolder.mProfileId); + IKeyChainService service = keyChainConnection.getService(); + List 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 { + 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; + } + } +} diff --git a/src/com/android/settings/TrustedCredentialsSettings.java b/src/com/android/settings/TrustedCredentialsSettings.java index 735cb3ba650..a88019ed9e4 100644 --- a/src/com/android/settings/TrustedCredentialsSettings.java +++ b/src/com/android/settings/TrustedCredentialsSettings.java @@ -16,121 +16,114 @@ package com.android.settings; -import static android.app.admin.DevicePolicyResources.Strings.Settings.PERSONAL_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.animation.LayoutTransition; -import android.annotation.UiThread; -import android.app.Activity; -import android.app.KeyguardManager; -import android.app.admin.DevicePolicyManager; import android.app.settings.SettingsEnums; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.DialogInterface; 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.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.ExpandableListView; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.ProgressBar; -import android.widget.Switch; -import android.widget.TabHost; -import android.widget.TextView; -import com.android.internal.annotations.GuardedBy; -import com.android.internal.app.UnlaunchableAppActivity; -import com.android.internal.widget.LockPatternUtils; -import com.android.settings.core.InstrumentedFragment; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import com.android.settings.dashboard.DashboardFragment; + +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; +import com.google.common.collect.ImmutableList; -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; -public class TrustedCredentialsSettings extends InstrumentedFragment - implements TrustedCredentialsDialogBuilder.DelegateInterface { - - public static final String ARG_SHOW_NEW_FOR_USER = "ARG_SHOW_NEW_FOR_USER"; +/** + * Main fragment to display trusted credentials settings. + */ +public class TrustedCredentialsSettings extends DashboardFragment { private static final String TAG = "TrustedCredentialsSettings"; - private DevicePolicyManager mDevicePolicyManager; - private UserManager mUserManager; - private KeyguardManager mKeyguardManager; - private int mTrustAllCaUserId; + public static final String ARG_SHOW_NEW_FOR_USER = "ARG_SHOW_NEW_FOR_USER"; + + static final ImmutableList TABS = ImmutableList.of(Tab.SYSTEM, Tab.USER); - private static final String SAVED_CONFIRMED_CREDENTIAL_USERS = "ConfirmedCredentialUsers"; - private static final String SAVED_CONFIRMING_CREDENTIAL_USER = "ConfirmingCredentialUser"; private static final String USER_ACTION = "com.android.settings.TRUSTED_CREDENTIALS_USER"; - private static final int REQUEST_CONFIRM_CREDENTIALS = 1; @Override public int getMetricsCategory() { return SettingsEnums.TRUSTED_CREDENTIALS; } - private enum Tab { - SYSTEM("system", - R.string.trusted_credentials_system_tab, - R.id.system_tab, - R.id.system_progress, - R.id.system_content, - true), - USER("user", - R.string.trusted_credentials_user_tab, - R.id.user_tab, - R.id.user_progress, - R.id.user_content, - false); + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getActivity().setTitle(R.string.trusted_credentials); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.placeholder_preference_screen; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + View tabContainer = view.findViewById(R.id.tab_container); + tabContainer.setVisibility(View.VISIBLE); + + ViewPager2 viewPager = tabContainer.findViewById(R.id.view_pager); + viewPager.setAdapter(new FragmentAdapter(this)); + viewPager.setUserInputEnabled(false); + + Intent intent = getActivity().getIntent(); + if (intent != null && USER_ACTION.equals(intent.getAction())) { + viewPager.setCurrentItem(TABS.indexOf(Tab.USER), false); + } + + TabLayout tabLayout = tabContainer.findViewById(R.id.tabs); + new TabLayoutMediator(tabLayout, viewPager, false, false, + (tab, position) -> tab.setText(TABS.get(position).mLabel)).attach(); + } + + private static class FragmentAdapter extends FragmentStateAdapter { + FragmentAdapter(Fragment fragment) { + super(fragment); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + TrustedCredentialsFragment fragment = new TrustedCredentialsFragment(); + Bundle args = new Bundle(); + args.putInt(TrustedCredentialsFragment.ARG_POSITION, position); + fragment.setArguments(args); + return fragment; + } + + @Override + public int getItemCount() { + return TrustedCredentialsSettings.TABS.size(); + } + } + + enum Tab { + SYSTEM(R.string.trusted_credentials_system_tab, true), + USER(R.string.trusted_credentials_user_tab, false); - private final String mTag; private final int mLabel; - private final int mView; - private final int mProgress; - private final int mContentView; - private final boolean mSwitch; + final boolean mSwitch; - private Tab(String tag, int label, int view, int progress, int contentView, - boolean withSwitch) { - mTag = tag; + Tab(int label, boolean withSwitch) { mLabel = label; - mView = view; - mProgress = progress; - mContentView = contentView; mSwitch = withSwitch; } - private List getAliases(IKeyChainService service) throws RemoteException { + List getAliases(IKeyChainService service) throws RemoteException { switch (this) { case SYSTEM: { return service.getSystemCaAliases().getList(); @@ -140,7 +133,8 @@ public class TrustedCredentialsSettings extends InstrumentedFragment } throw new AssertionError(); } - private boolean deleted(IKeyChainService service, String alias) throws RemoteException { + + boolean deleted(IKeyChainService service, String alias) throws RemoteException { switch (this) { case SYSTEM: return !service.containsCaAlias(alias); @@ -150,895 +144,4 @@ public class TrustedCredentialsSettings extends InstrumentedFragment throw new AssertionError(); } } - - private TabHost mTabHost; - private ArrayList mGroupAdapters = new ArrayList<>(2); - private AliasOperation mAliasOperation; - private ArraySet mConfirmedCredentialUsers; - private int mConfirmingCredentialUser; - private IntConsumer mConfirmingCredentialListener; - private Set mAliasLoaders = new ArraySet(2); - @GuardedBy("mKeyChainConnectionByProfileId") - private final SparseArray - mKeyChainConnectionByProfileId = new SparseArray(); - - private BroadcastReceiver mWorkProfileChangedReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) || - Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action) || - Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)) { - for (GroupAdapter adapter : mGroupAdapters) { - adapter.load(); - } - } - } - - }; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Activity activity = getActivity(); - mDevicePolicyManager = activity.getSystemService(DevicePolicyManager.class); - mUserManager = (UserManager) activity.getSystemService(Context.USER_SERVICE); - mKeyguardManager = (KeyguardManager) activity - .getSystemService(Context.KEYGUARD_SERVICE); - 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 users = savedInstanceState.getIntegerArrayList( - SAVED_CONFIRMED_CREDENTIAL_USERS); - if (users != null) { - mConfirmedCredentialUsers.addAll(users); - } - } - - mConfirmingCredentialListener = null; - - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); - filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); - filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED); - activity.registerReceiver(mWorkProfileChangedReceiver, filter); - - activity.setTitle(R.string.trusted_credentials); - } - - @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); - } - - @Override public View onCreateView( - LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { - mTabHost = (TabHost) inflater.inflate(R.layout.trusted_credentials, parent, false); - mTabHost.setup(); - addTab(Tab.SYSTEM); - // TODO add Install button on Tab.USER to go to CertInstaller like KeyChainActivity - addTab(Tab.USER); - if (getActivity().getIntent() != null && - USER_ACTION.equals(getActivity().getIntent().getAction())) { - mTabHost.setCurrentTabByTag(Tab.USER.mTag); - } - return mTabHost; - } - @Override - public void onDestroy() { - getActivity().unregisterReceiver(mWorkProfileChangedReceiver); - for (AdapterData.AliasLoader aliasLoader : mAliasLoaders) { - aliasLoader.cancel(true); - } - mAliasLoaders.clear(); - mGroupAdapters.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) { - final int n = mKeyChainConnectionByProfileId.size(); - for (int i = 0; i < n; ++i) { - mKeyChainConnectionByProfileId.valueAt(i).close(); - } - mKeyChainConnectionByProfileId.clear(); - } - } - - private void addTab(Tab tab) { - TabHost.TabSpec systemSpec = mTabHost.newTabSpec(tab.mTag) - .setIndicator(getActivity().getString(tab.mLabel)) - .setContent(tab.mView); - mTabHost.addTab(systemSpec); - - final GroupAdapter groupAdapter = new GroupAdapter(tab); - mGroupAdapters.add(groupAdapter); - final int profilesSize = groupAdapter.getGroupCount(); - - // Add a transition for non-visibility events like resizing the pane. - final ViewGroup contentView = (ViewGroup) mTabHost.findViewById(tab.mContentView); - contentView.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); - - final LayoutInflater inflater = LayoutInflater.from(getActivity()); - for (int i = 0; i < groupAdapter.getGroupCount(); i++) { - final boolean isWork = groupAdapter.getUserInfoByGroup(i).isManagedProfile(); - final ChildAdapter adapter = groupAdapter.getChildAdapter(i); - - final LinearLayout containerView = (LinearLayout) inflater - .inflate(R.layout.trusted_credential_list_container, contentView, false); - adapter.setContainerView(containerView); - - adapter.showHeader(profilesSize > 1); - adapter.showDivider(isWork); - adapter.setExpandIfAvailable(profilesSize <= 2 ? true : !isWork); - if (isWork) { - contentView.addView(containerView); - } else { - contentView.addView(containerView, 0); - } - } - } - - /** - * Start work challenge activity. - * @return true if screenlock exists - */ - private boolean startConfirmCredential(int userId) { - final 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, - View.OnClickListener { - private final AdapterData mData; - - 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 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); - } - - final TextView title = (TextView) convertView.findViewById(android.R.id.title); - if (getUserInfoByGroup(groupPosition).isManagedProfile()) { - title.setText(mDevicePolicyManager.getResources().getString(WORK_CATEGORY_HEADER, - () -> getString(R.string.category_work))); - } else { - title.setText(mDevicePolicyManager.getResources().getString( - PERSONAL_CATEGORY_HEADER, - () -> getString(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; - } - - /** - * Called when the switch on a system certificate is clicked. This will toggle whether it - * is trusted as a credential. - */ - @Override - public void onClick(View view) { - CertHolder holder = (CertHolder) view.getTag(); - removeOrInstallCert(holder); - } - - @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); - } - - public void setExpandableListView(ExpandableListView lv) { - lv.setAdapter(this); - lv.setOnGroupClickListener(this); - lv.setOnChildClickListener(this); - lv.setVisibility(View.VISIBLE); - } - - public ChildAdapter getChildAdapter(int groupPosition) { - return new ChildAdapter(this, groupPosition); - } - - public boolean checkGroupExpandableAndStartWarningActivity(int groupPosition) { - return checkGroupExpandableAndStartWarningActivity(groupPosition, true); - } - - public boolean checkGroupExpandableAndStartWarningActivity(int groupPosition, - boolean startActivity) { - final UserHandle groupUser = getGroup(groupPosition); - final int groupUserId = groupUser.getIdentifier(); - if (mUserManager.isQuietModeEnabled(groupUser)) { - final Intent intent = UnlaunchableAppActivity.createInQuietModeDialogIntent( - groupUserId); - if (startActivity) { - getActivity().startActivity(intent); - } - return false; - } else if (!mUserManager.isUserUnlocked(groupUser)) { - final 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 = (TextView) - convertView.findViewById(R.id.trusted_credential_subject_primary); - holder.mSubjectSecondaryView = (TextView) - convertView.findViewById(R.id.trusted_credential_subject_secondary); - holder.mSwitch = (Switch) convertView.findViewById( - R.id.trusted_credential_status); - holder.mSwitch.setOnClickListener(this); - } 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 class ViewHolder { - private TextView mSubjectPrimaryView; - private TextView mSubjectSecondaryView; - private Switch mSwitch; - } - } - - private class ChildAdapter extends BaseAdapter implements View.OnClickListener, - AdapterView.OnItemClickListener { - private final int[] GROUP_EXPANDED_STATE_SET = {com.android.internal.R.attr.state_expanded}; - private final int[] EMPTY_STATE_SET = {}; - private final LinearLayout.LayoutParams HIDE_CONTAINER_LAYOUT_PARAMS = - new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT, 0f); - private final LinearLayout.LayoutParams HIDE_LIST_LAYOUT_PARAMS = - new LinearLayout.LayoutParams(MATCH_PARENT, 0); - private final LinearLayout.LayoutParams SHOW_LAYOUT_PARAMS = 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(); - ChildAdapter.super.notifyDataSetChanged(); - } - @Override - public void onInvalidated() { - super.onInvalidated(); - 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) { - mContainerView = containerView; - - mListView = (ListView) mContainerView.findViewById(R.id.cert_list); - mListView.setAdapter(this); - mListView.setOnItemClickListener(this); - mListView.setItemsCanFocus(true); - - mHeaderView = (ViewGroup) mContainerView.findViewById(R.id.header_view); - mHeaderView.setOnClickListener(this); - - mIndicatorView = (ImageView) mHeaderView.findViewById(R.id.group_indicator); - mIndicatorView.setImageDrawable(getGroupIndicator()); - - FrameLayout headerContentContainer = (FrameLayout) - mHeaderView.findViewById(R.id.header_content_container); - headerContentContainer.addView( - mParent.getGroupView(mGroupPosition, true /* parent ignores it */, null, - headerContentContainer)); - } - - 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) { - mIsListExpanded = expanded && mParent.checkGroupExpandableAndStartWarningActivity( - mGroupPosition, false /* startActivity */); - refreshViews(); - } - - private boolean checkGroupExpandableAndStartWarningActivity() { - return mParent.checkGroupExpandableAndStartWarningActivity(mGroupPosition); - } - - private void refreshViews() { - mIndicatorView.setImageState(mIsListExpanded ? GROUP_EXPANDED_STATE_SET - : EMPTY_STATE_SET, false); - mListView.setLayoutParams(mIsListExpanded ? SHOW_LAYOUT_PARAMS - : HIDE_LIST_LAYOUT_PARAMS); - mContainerView.setLayoutParams(mIsListExpanded ? SHOW_LAYOUT_PARAMS - : HIDE_CONTAINER_LAYOUT_PARAMS); - } - - // Get group indicator from styles of ExpandableListView - private Drawable getGroupIndicator() { - final 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 class AdapterData { - private final SparseArray> 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>> { - private ProgressBar mProgressBar; - private View mContentView; - private Context mContext; - - public AliasLoader() { - mContext = getActivity(); - mAliasLoaders.add(this); - List 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() { - View content = mTabHost.getTabContentView(); - mProgressBar = (ProgressBar) content.findViewById(mTab.mProgress); - mContentView = content.findViewById(mTab.mContentView); - mProgressBar.setVisibility(View.VISIBLE); - mContentView.setVisibility(View.GONE); - } - @Override protected SparseArray> doInBackground(Void... params) { - SparseArray> certHoldersByProfile = - new SparseArray>(); - try { - synchronized(mKeyChainConnectionByProfileId) { - List profiles = mUserManager.getUserProfiles(); - final int n = profiles.size(); - // First we get all aliases for all profiles in order to show progress - // correctly. Otherwise this could all be in a single loop. - SparseArray> aliasesByProfileId = new SparseArray< - List>(n); - int max = 0; - int progress = 0; - for (int i = 0; i < n; ++i) { - UserHandle profile = profiles.get(i); - 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 aliases = mTab.getAliases(service); - if (isCancelled()) { - return new SparseArray>(); - } - max += aliases.size(); - aliasesByProfileId.put(profileId, aliases); - } - for (int i = 0; i < n; ++i) { - UserHandle profile = profiles.get(i); - int profileId = profile.getIdentifier(); - List 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 certHolders = new ArrayList(max); - final int aliasMax = aliases.size(); - for (int j = 0; j < aliasMax; ++j) { - String alias = aliases.get(j); - 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> certHolders) { - mCertHoldersByUserId.clear(); - final 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 certHolders = mCertHoldersByUserId.get(mTrustAllCaUserId); - if (certHolders == null) { - return; - } - - List unapprovedUserCertHolders = new ArrayList<>(); - final 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) { - final List certs = mCertHoldersByUserId.get(certHolder.mProfileId); - if (certs != null) { - certs.remove(certHolder); - } - } - } - } - - /* package */ static class CertHolder implements Comparable { - 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 unapprovedCertHolders) { - final CertHolder[] arr = unapprovedCertHolders.toArray( - new CertHolder[unapprovedCertHolders.size()]); - new TrustedCredentialsDialogBuilder(getActivity(), this) - .setCertHolders(arr) - .setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface 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 getX509CertsFromCertHolder(CertHolder certHolder) { - List certificates = null; - try { - synchronized (mKeyChainConnectionByProfileId) { - KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( - certHolder.mProfileId); - IKeyChainService service = keyChainConnection.getService(); - List chain = service.getCaCertificateChainAliases(certHolder.mAlias, true); - final int n = chain.size(); - certificates = new ArrayList(n); - for (int i = 0; i < n; ++i) { - byte[] encodedCertificate = service.getEncodedCaCertificate(chain.get(i), 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 { - 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; - } - } }