1031 lines
41 KiB
Java
1031 lines
41 KiB
Java
/*
|
|
* 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<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 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<Integer> 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<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);
|
|
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<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;
|
|
}
|
|
}
|
|
}
|