/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings;

import static android.app.admin.DevicePolicyResources.Strings.Settings.PERSONAL_CATEGORY_HEADER;
import static android.app.admin.DevicePolicyResources.Strings.Settings.PRIVATE_CATEGORY_HEADER;
import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_CATEGORY_HEADER;
import static android.widget.LinearLayout.LayoutParams.MATCH_PARENT;
import static android.widget.LinearLayout.LayoutParams.WRAP_CONTENT;

import android.annotation.UiThread;
import android.app.Activity;
import android.app.KeyguardManager;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.UserInfo;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.drawable.Drawable;
import android.net.http.SslCertificate;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.security.IKeyChainService;
import android.security.KeyChain;
import android.security.KeyChain.KeyChainConnection;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.BaseExpandableListAdapter;
import android.widget.CompoundButton;
import android.widget.ExpandableListView;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.annotation.NonNull;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.UnlaunchableAppActivity;
import com.android.internal.widget.LockPatternUtils;
import com.android.settings.TrustedCredentialsSettings.Tab;
import com.android.settingslib.core.lifecycle.ObservableFragment;

import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.IntConsumer;

/**
 * Fragment to display trusted credentials settings for one tab.
 */
public class TrustedCredentialsFragment extends ObservableFragment
        implements TrustedCredentialsDialogBuilder.DelegateInterface {

    public static final String ARG_POSITION = "tab";
    public static final String ARG_SHOW_NEW_FOR_USER = "ARG_SHOW_NEW_FOR_USER";

    private static final String TAG = "TrustedCredentialsFragment";

    private DevicePolicyManager mDevicePolicyManager;
    private UserManager mUserManager;
    private KeyguardManager mKeyguardManager;
    private int mTrustAllCaUserId;

    private static final String SAVED_CONFIRMED_CREDENTIAL_USERS = "ConfirmedCredentialUsers";
    private static final String SAVED_CONFIRMING_CREDENTIAL_USER = "ConfirmingCredentialUser";
    private static final int REQUEST_CONFIRM_CREDENTIALS = 1;

    private GroupAdapter mGroupAdapter;
    private AliasOperation mAliasOperation;
    private ArraySet<Integer> mConfirmedCredentialUsers;
    private int mConfirmingCredentialUser;
    private IntConsumer mConfirmingCredentialListener;
    private final Set<AdapterData.AliasLoader> mAliasLoaders = new ArraySet<>(2);
    @GuardedBy("mKeyChainConnectionByProfileId")
    private final SparseArray<KeyChainConnection>
            mKeyChainConnectionByProfileId = new SparseArray<>();
    private ViewGroup mFragmentView;

    private final BroadcastReceiver mProfileChangedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (isBroadcastValidForAction(intent)) {
                mGroupAdapter.load();
            }
        }
    };

    private boolean isBroadcastValidForAction(Intent intent) {
        String action = intent.getAction();
        if (android.os.Flags.allowPrivateProfile()
                && android.multiuser.Flags.enablePrivateSpaceFeatures()
                && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) {
            UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class);
            if (userHandle == null) {
                Log.w(TAG, "received action " + action + " with missing user extra");
                return false;
            }

            UserInfo userInfo = mUserManager.getUserInfo(userHandle.getIdentifier());
            return (Intent.ACTION_PROFILE_AVAILABLE.equals(action)
                    || Intent.ACTION_PROFILE_UNAVAILABLE.equals(action)
                    || Intent.ACTION_PROFILE_ACCESSIBLE.equals(action))
                    && (userInfo.isManagedProfile() || userInfo.isPrivateProfile());
        }
        return (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
                || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)
                || Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action));
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Activity activity = getActivity();
        mDevicePolicyManager = activity.getSystemService(DevicePolicyManager.class);
        mUserManager = activity.getSystemService(UserManager.class);
        mKeyguardManager = activity.getSystemService(KeyguardManager.class);
        mTrustAllCaUserId = activity.getIntent().getIntExtra(ARG_SHOW_NEW_FOR_USER,
                UserHandle.USER_NULL);
        mConfirmedCredentialUsers = new ArraySet<>(2);
        mConfirmingCredentialUser = UserHandle.USER_NULL;
        if (savedInstanceState != null) {
            mConfirmingCredentialUser = savedInstanceState.getInt(SAVED_CONFIRMING_CREDENTIAL_USER,
                    UserHandle.USER_NULL);
            ArrayList<Integer> users = savedInstanceState.getIntegerArrayList(
                    SAVED_CONFIRMED_CREDENTIAL_USERS);
            if (users != null) {
                mConfirmedCredentialUsers.addAll(users);
            }
        }

        IntentFilter filter = new IntentFilter();
        if (android.os.Flags.allowPrivateProfile()
                && android.multiuser.Flags.enablePrivateSpaceFeatures()
                && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) {
            filter.addAction(Intent.ACTION_PROFILE_AVAILABLE);
            filter.addAction(Intent.ACTION_PROFILE_UNAVAILABLE);
            filter.addAction(Intent.ACTION_PROFILE_ACCESSIBLE);
        } else {
            filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
            filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
            filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED);
        }
        activity.registerReceiver(mProfileChangedReceiver, filter);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putIntegerArrayList(SAVED_CONFIRMED_CREDENTIAL_USERS, new ArrayList<>(
                mConfirmedCredentialUsers));
        outState.putInt(SAVED_CONFIRMING_CREDENTIAL_USER, mConfirmingCredentialUser);
        mGroupAdapter.saveState(outState);
    }

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
        mFragmentView = (ViewGroup) inflater.inflate(R.layout.trusted_credentials, parent, false);

        ViewGroup contentView = mFragmentView.findViewById(R.id.content);

        mGroupAdapter = new GroupAdapter(
                requireArguments().getInt(ARG_POSITION) == 0 ? Tab.SYSTEM : Tab.USER);
        int profilesSize = mGroupAdapter.getGroupCount();
        for (int i = 0; i < profilesSize; i++) {
            Bundle childState = savedInstanceState == null ? null
                    : savedInstanceState.getBundle(mGroupAdapter.getKey(i));
            createChildView(inflater, contentView, childState, i);
        }
        return mFragmentView;
    }

    private void createChildView(
            LayoutInflater inflater, ViewGroup parent, Bundle childState, int i) {
        UserInfo userInfo = mGroupAdapter.getUserInfoByGroup(i);
        if (Utils.shouldHideUser(userInfo.getUserHandle(), mUserManager)) {
            return;
        }
        boolean isProfile = userInfo.isManagedProfile();
        if (android.os.Flags.allowPrivateProfile()
                && android.multiuser.Flags.enablePrivateSpaceFeatures()
                && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) {
            isProfile |= userInfo.isPrivateProfile();
        }
        ChildAdapter adapter = mGroupAdapter.createChildAdapter(i);

        LinearLayout containerView = (LinearLayout) inflater.inflate(
                R.layout.trusted_credential_list_container, parent, false);
        adapter.setContainerView(containerView, childState);

        int profilesSize = mGroupAdapter.getGroupCount();
        adapter.showHeader(profilesSize > 1);
        adapter.showDivider(isProfile);
        adapter.setExpandIfAvailable(profilesSize <= 2 || !isProfile, childState);
        if (isProfile) {
            parent.addView(containerView);
        } else {
            parent.addView(containerView, 0);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        mFragmentView.requestLayout();
    }

    @Override
    public void onDestroy() {
        getActivity().unregisterReceiver(mProfileChangedReceiver);
        for (AdapterData.AliasLoader aliasLoader : mAliasLoaders) {
            aliasLoader.cancel(true);
        }
        mAliasLoaders.clear();
        if (mAliasOperation != null) {
            mAliasOperation.cancel(true);
            mAliasOperation = null;
        }
        closeKeyChainConnections();
        super.onDestroy();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CONFIRM_CREDENTIALS) {
            int userId = mConfirmingCredentialUser;
            IntConsumer listener = mConfirmingCredentialListener;
            // reset them before calling the listener because the listener may call back to start
            // activity again. (though it should never happen.)
            mConfirmingCredentialUser = UserHandle.USER_NULL;
            mConfirmingCredentialListener = null;
            if (resultCode == Activity.RESULT_OK) {
                mConfirmedCredentialUsers.add(userId);
                if (listener != null) {
                    listener.accept(userId);
                }
            }
        }
    }

    private void closeKeyChainConnections() {
        synchronized (mKeyChainConnectionByProfileId) {
            int n = mKeyChainConnectionByProfileId.size();
            for (int i = 0; i < n; ++i) {
                mKeyChainConnectionByProfileId.valueAt(i).close();
            }
            mKeyChainConnectionByProfileId.clear();
        }
    }

    /**
     * Start work challenge activity.
     *
     * @return true if screenlock exists
     */
    private boolean startConfirmCredential(int userId) {
        Intent newIntent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, null, userId);
        if (newIntent == null) {
            return false;
        }
        mConfirmingCredentialUser = userId;
        startActivityForResult(newIntent, REQUEST_CONFIRM_CREDENTIALS);
        return true;
    }

    /**
     * Adapter for expandable list view of certificates. Groups in the view correspond to profiles
     * whereas children correspond to certificates.
     */
    private class GroupAdapter extends BaseExpandableListAdapter implements
            ExpandableListView.OnGroupClickListener, ExpandableListView.OnChildClickListener {
        private final AdapterData mData;
        private final ArrayList<ChildAdapter> mChildAdapters = new ArrayList<>();

        private GroupAdapter(Tab tab) {
            mData = new AdapterData(tab, this);
            load();
        }

        @Override
        public int getGroupCount() {
            return mData.mCertHoldersByUserId.size();
        }

        @Override
        public int getChildrenCount(int groupPosition) {
            List<CertHolder> certHolders = mData.mCertHoldersByUserId.valueAt(groupPosition);
            if (certHolders != null) {
                return certHolders.size();
            }
            return 0;
        }

        @Override
        public UserHandle getGroup(int groupPosition) {
            return new UserHandle(mData.mCertHoldersByUserId.keyAt(groupPosition));
        }

        @Override
        public CertHolder getChild(int groupPosition, int childPosition) {
            return mData.mCertHoldersByUserId.get(getUserIdByGroup(groupPosition)).get(
                    childPosition);
        }

        @Override
        public long getGroupId(int groupPosition) {
            return getUserIdByGroup(groupPosition);
        }

        private int getUserIdByGroup(int groupPosition) {
            return mData.mCertHoldersByUserId.keyAt(groupPosition);
        }

        public UserInfo getUserInfoByGroup(int groupPosition) {
            return mUserManager.getUserInfo(getUserIdByGroup(groupPosition));
        }

        @Override
        public long getChildId(int groupPosition, int childPosition) {
            return childPosition;
        }

        @Override
        public boolean hasStableIds() {
            return false;
        }

        @Override
        public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
                ViewGroup parent) {
            if (convertView == null) {
                LayoutInflater inflater = (LayoutInflater) getActivity()
                        .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                convertView = Utils.inflateCategoryHeader(inflater, parent);
            }

            TextView title = convertView.findViewById(android.R.id.title);
            UserInfo userInfo = getUserInfoByGroup(groupPosition);
            if (userInfo.isManagedProfile()) {
                title.setText(mDevicePolicyManager.getResources().getString(WORK_CATEGORY_HEADER,
                        () -> getString(com.android.settingslib.R.string.category_work)));
            } else if (android.os.Flags.allowPrivateProfile()
                    && android.multiuser.Flags.enablePrivateSpaceFeatures()
                    && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()
                    && userInfo.isPrivateProfile()) {
                title.setText(mDevicePolicyManager.getResources().getString(PRIVATE_CATEGORY_HEADER,
                        () -> getString(com.android.settingslib.R.string.category_private)));
            } else {
                title.setText(mDevicePolicyManager.getResources().getString(
                        PERSONAL_CATEGORY_HEADER,
                        () -> getString(com.android.settingslib.R.string.category_personal)));

            }
            title.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);

            return convertView;
        }

        @Override
        public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
                View convertView, ViewGroup parent) {
            return getViewForCertificate(getChild(groupPosition, childPosition), mData.mTab,
                    convertView, parent);
        }

        @Override
        public boolean isChildSelectable(int groupPosition, int childPosition) {
            return true;
        }

        @Override
        public boolean onChildClick(ExpandableListView expandableListView, View view,
                int groupPosition, int childPosition, long id) {
            showCertDialog(getChild(groupPosition, childPosition));
            return true;
        }

        @Override
        public boolean onGroupClick(ExpandableListView expandableListView, View view,
                int groupPosition, long id) {
            return !checkGroupExpandableAndStartWarningActivity(groupPosition);
        }

        public void load() {
            mData.new AliasLoader().execute();
        }

        public void remove(CertHolder certHolder) {
            mData.remove(certHolder);
        }

        ChildAdapter createChildAdapter(int groupPosition) {
            ChildAdapter childAdapter = new ChildAdapter(this, groupPosition);
            mChildAdapters.add(childAdapter);
            return childAdapter;
        }

        public boolean checkGroupExpandableAndStartWarningActivity(int groupPosition) {
            return checkGroupExpandableAndStartWarningActivity(groupPosition, true);
        }

        public boolean checkGroupExpandableAndStartWarningActivity(int groupPosition,
                boolean startActivity) {
            UserHandle groupUser = getGroup(groupPosition);
            int groupUserId = groupUser.getIdentifier();
            if (mUserManager.isQuietModeEnabled(groupUser)) {
                if (startActivity) {
                    Intent intent =
                            UnlaunchableAppActivity.createInQuietModeDialogIntent(groupUserId);
                    getActivity().startActivity(intent);
                }
                return false;
            } else if (!mUserManager.isUserUnlocked(groupUser)) {
                LockPatternUtils lockPatternUtils = new LockPatternUtils(getActivity());
                if (lockPatternUtils.isSeparateProfileChallengeEnabled(groupUserId)) {
                    if (startActivity) {
                        startConfirmCredential(groupUserId);
                    }
                    return false;
                }
            }
            return true;
        }

        private View getViewForCertificate(CertHolder certHolder, Tab mTab, View convertView,
                ViewGroup parent) {
            ViewHolder holder;
            if (convertView == null) {
                holder = new ViewHolder();
                LayoutInflater inflater = LayoutInflater.from(getActivity());
                convertView = inflater.inflate(R.layout.trusted_credential, parent, false);
                convertView.setTag(holder);
                holder.mSubjectPrimaryView =
                        convertView.findViewById(R.id.trusted_credential_subject_primary);
                holder.mSubjectSecondaryView =
                        convertView.findViewById(R.id.trusted_credential_subject_secondary);
                holder.mSwitch = convertView.findViewById(R.id.trusted_credential_status);
                holder.mSwitch.setOnClickListener(view -> {
                    removeOrInstallCert((CertHolder) view.getTag());
                });
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.mSubjectPrimaryView.setText(certHolder.mSubjectPrimary);
            holder.mSubjectSecondaryView.setText(certHolder.mSubjectSecondary);
            if (mTab.mSwitch) {
                holder.mSwitch.setChecked(!certHolder.mDeleted);
                holder.mSwitch.setEnabled(!mUserManager.hasUserRestriction(
                        UserManager.DISALLOW_CONFIG_CREDENTIALS,
                        new UserHandle(certHolder.mProfileId)));
                holder.mSwitch.setVisibility(View.VISIBLE);
                holder.mSwitch.setTag(certHolder);
            }
            return convertView;
        }

        private void saveState(Bundle outState) {
            for (int groupPosition = 0, mChildAdaptersSize = mChildAdapters.size();
                    groupPosition < mChildAdaptersSize; groupPosition++) {
                ChildAdapter childAdapter = mChildAdapters.get(groupPosition);
                outState.putBundle(getKey(groupPosition), childAdapter.saveState());
            }
        }

        @NonNull
        private String getKey(int groupPosition) {
            return "Group" + getUserIdByGroup(groupPosition);
        }

        private static class ViewHolder {
            private TextView mSubjectPrimaryView;
            private TextView mSubjectSecondaryView;
            private CompoundButton mSwitch;
        }
    }

    private class ChildAdapter extends BaseAdapter implements View.OnClickListener,
            AdapterView.OnItemClickListener {
        private static final String KEY_CONTAINER = "Container";
        private static final String KEY_IS_LIST_EXPANDED = "IsListExpanded";
        private final int[] mGroupExpandedStateSet = {com.android.internal.R.attr.state_expanded};
        private final int[] mEmptyStateSet = {};
        private final LinearLayout.LayoutParams mHideContainerLayoutParams =
                new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT, 0f);
        private final LinearLayout.LayoutParams mHideListLayoutParams =
                new LinearLayout.LayoutParams(MATCH_PARENT, 0);
        private final LinearLayout.LayoutParams mShowLayoutParams = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT, MATCH_PARENT, 1f);
        private final GroupAdapter mParent;
        private final int mGroupPosition;
        /*
         * This class doesn't hold the actual data. Events should notify parent.
         * When notifying DataSet events in this class, events should be forwarded to mParent.
         * i.e. this.notifyDataSetChanged -> mParent.notifyDataSetChanged -> mObserver.onChanged
         * -> outsideObservers.onChanged() (e.g. ListView)
         */
        private final DataSetObserver mObserver = new DataSetObserver() {
            @Override
            public void onChanged() {
                super.onChanged();
                TrustedCredentialsFragment.ChildAdapter.super.notifyDataSetChanged();
            }

            @Override
            public void onInvalidated() {
                super.onInvalidated();
                TrustedCredentialsFragment.ChildAdapter.super.notifyDataSetInvalidated();
            }
        };

        private boolean mIsListExpanded = true;
        private LinearLayout mContainerView;
        private ViewGroup mHeaderView;
        private ListView mListView;
        private ImageView mIndicatorView;

        private ChildAdapter(GroupAdapter parent, int groupPosition) {
            mParent = parent;
            mGroupPosition = groupPosition;
            mParent.registerDataSetObserver(mObserver);
        }

        @Override
        public int getCount() {
            return mParent.getChildrenCount(mGroupPosition);
        }

        @Override
        public CertHolder getItem(int position) {
            return mParent.getChild(mGroupPosition, position);
        }

        @Override
        public long getItemId(int position) {
            return mParent.getChildId(mGroupPosition, position);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return mParent.getChildView(mGroupPosition, position, false, convertView, parent);
        }

        // DataSet events
        @Override
        public void notifyDataSetChanged() {
            // Don't call super as the parent will propagate this event back later in mObserver
            mParent.notifyDataSetChanged();
        }

        @Override
        public void notifyDataSetInvalidated() {
            // Don't call super as the parent will propagate this event back later in mObserver
            mParent.notifyDataSetInvalidated();
        }

        // View related codes
        @Override
        public void onClick(View view) {
            mIsListExpanded = checkGroupExpandableAndStartWarningActivity() && !mIsListExpanded;
            refreshViews();
        }

        @Override
        public void onItemClick(AdapterView<?> adapterView, View view, int pos, long id) {
            showCertDialog(getItem(pos));
        }

        public void setContainerView(LinearLayout containerView, Bundle savedState) {
            mContainerView = containerView;
            // Handle manually because multiple groups with same id elements.
            mContainerView.setSaveFromParentEnabled(false);

            mListView = mContainerView.findViewById(R.id.cert_list);
            mListView.setAdapter(this);
            mListView.setOnItemClickListener(this);
            mListView.setItemsCanFocus(true);

            mHeaderView = mContainerView.findViewById(R.id.header_view);
            mHeaderView.setOnClickListener(this);

            mIndicatorView = mHeaderView.findViewById(R.id.group_indicator);
            mIndicatorView.setImageDrawable(getGroupIndicator());

            FrameLayout headerContentContainer =
                    mHeaderView.findViewById(R.id.header_content_container);
            headerContentContainer.addView(
                    mParent.getGroupView(mGroupPosition, true /* parent ignores it */, null,
                            headerContentContainer));

            if (savedState != null) {
                SparseArray<Parcelable> containerStates =
                        savedState.getSparseParcelableArray(KEY_CONTAINER, Parcelable.class);
                if (containerStates != null) {
                    mContainerView.restoreHierarchyState(containerStates);
                }
            }
        }

        public void showHeader(boolean showHeader) {
            mHeaderView.setVisibility(showHeader ? View.VISIBLE : View.GONE);
        }

        public void showDivider(boolean showDivider) {
            View dividerView = mHeaderView.findViewById(R.id.header_divider);
            dividerView.setVisibility(showDivider ? View.VISIBLE : View.GONE);
        }

        public void setExpandIfAvailable(boolean expanded, Bundle savedState) {
            if (savedState != null) {
                expanded = savedState.getBoolean(KEY_IS_LIST_EXPANDED);
            }
            mIsListExpanded = expanded && mParent.checkGroupExpandableAndStartWarningActivity(
                    mGroupPosition, false /* startActivity */);
            refreshViews();
        }

        private boolean checkGroupExpandableAndStartWarningActivity() {
            return mParent.checkGroupExpandableAndStartWarningActivity(mGroupPosition);
        }

        private void refreshViews() {
            mIndicatorView.setImageState(mIsListExpanded ? mGroupExpandedStateSet
                    : mEmptyStateSet, false);
            mListView.setLayoutParams(mIsListExpanded ? mShowLayoutParams
                    : mHideListLayoutParams);
            mContainerView.setLayoutParams(mIsListExpanded ? mShowLayoutParams
                    : mHideContainerLayoutParams);
        }

        // Get group indicator from styles of ExpandableListView
        private Drawable getGroupIndicator() {
            TypedArray a = getActivity().obtainStyledAttributes(null,
                    com.android.internal.R.styleable.ExpandableListView,
                    com.android.internal.R.attr.expandableListViewStyle, 0);
            Drawable groupIndicator = a.getDrawable(
                    com.android.internal.R.styleable.ExpandableListView_groupIndicator);
            a.recycle();
            return groupIndicator;
        }

        private Bundle saveState() {
            Bundle bundle = new Bundle();
            SparseArray<Parcelable> states = new SparseArray<>();
            mContainerView.saveHierarchyState(states);
            bundle.putSparseParcelableArray(KEY_CONTAINER, states);
            bundle.putBoolean(KEY_IS_LIST_EXPANDED, mIsListExpanded);
            return bundle;
        }
    }

    private class AdapterData {
        private final SparseArray<List<CertHolder>> mCertHoldersByUserId =
                new SparseArray<>();
        private final Tab mTab;
        private final GroupAdapter mAdapter;

        private AdapterData(Tab tab, GroupAdapter adapter) {
            mAdapter = adapter;
            mTab = tab;
        }

        private class AliasLoader extends AsyncTask<Void, Integer, SparseArray<List<CertHolder>>> {
            private ProgressBar mProgressBar;
            private View mContentView;
            private Context mContext;

            AliasLoader() {
                mContext = getActivity();
                mAliasLoaders.add(this);
                List<UserHandle> profiles = mUserManager.getUserProfiles();
                for (UserHandle profile : profiles) {
                    mCertHoldersByUserId.put(profile.getIdentifier(), new ArrayList<>());
                }
            }

            private boolean shouldSkipProfile(UserHandle userHandle) {
                return mUserManager.isQuietModeEnabled(userHandle)
                        || !mUserManager.isUserUnlocked(userHandle.getIdentifier());
            }

            @Override
            protected void onPreExecute() {
                mProgressBar = mFragmentView.findViewById(R.id.progress);
                mContentView = mFragmentView.findViewById(R.id.content);
                mProgressBar.setVisibility(View.VISIBLE);
                mContentView.setVisibility(View.GONE);
            }

            @Override
            protected SparseArray<List<CertHolder>> doInBackground(Void... params) {
                SparseArray<List<CertHolder>> certHoldersByProfile =
                        new SparseArray<>();
                try {
                    synchronized (mKeyChainConnectionByProfileId) {
                        List<UserHandle> profiles = mUserManager.getUserProfiles();
                        // First we get all aliases for all profiles in order to show progress
                        // correctly. Otherwise this could all be in a single loop.
                        SparseArray<List<String>> aliasesByProfileId =
                                new SparseArray<>(profiles.size());
                        int max = 0;
                        int progress = 0;
                        for (UserHandle profile : profiles) {
                            int profileId = profile.getIdentifier();
                            if (shouldSkipProfile(profile)) {
                                continue;
                            }
                            KeyChainConnection keyChainConnection = KeyChain.bindAsUser(mContext,
                                    profile);
                            // Saving the connection for later use on the certificate dialog.
                            mKeyChainConnectionByProfileId.put(profileId, keyChainConnection);
                            IKeyChainService service = keyChainConnection.getService();
                            List<String> aliases = mTab.getAliases(service);
                            if (isCancelled()) {
                                return new SparseArray<>();
                            }
                            max += aliases.size();
                            aliasesByProfileId.put(profileId, aliases);
                        }
                        for (UserHandle profile : profiles) {
                            int profileId = profile.getIdentifier();
                            List<String> aliases = aliasesByProfileId.get(profileId);
                            if (isCancelled()) {
                                return new SparseArray<>();
                            }
                            KeyChainConnection keyChainConnection =
                                    mKeyChainConnectionByProfileId.get(
                                            profileId);
                            if (shouldSkipProfile(profile) || aliases == null
                                    || keyChainConnection == null) {
                                certHoldersByProfile.put(profileId, new ArrayList<>(0));
                                continue;
                            }
                            IKeyChainService service = keyChainConnection.getService();
                            List<CertHolder> certHolders = new ArrayList<>(max);
                            for (String alias : aliases) {
                                byte[] encodedCertificate = service.getEncodedCaCertificate(alias,
                                        true);
                                X509Certificate cert = KeyChain.toCertificate(encodedCertificate);
                                certHolders.add(new CertHolder(service, mAdapter,
                                        mTab, alias, cert, profileId));
                                publishProgress(++progress, max);
                            }
                            Collections.sort(certHolders);
                            certHoldersByProfile.put(profileId, certHolders);
                        }
                        return certHoldersByProfile;
                    }
                } catch (RemoteException e) {
                    Log.e(TAG, "Remote exception while loading aliases.", e);
                    return new SparseArray<>();
                } catch (InterruptedException e) {
                    Log.e(TAG, "InterruptedException while loading aliases.", e);
                    return new SparseArray<>();
                }
            }

            @Override
            protected void onProgressUpdate(Integer... progressAndMax) {
                int progress = progressAndMax[0];
                int max = progressAndMax[1];
                if (max != mProgressBar.getMax()) {
                    mProgressBar.setMax(max);
                }
                mProgressBar.setProgress(progress);
            }

            @Override
            protected void onPostExecute(SparseArray<List<CertHolder>> certHolders) {
                mCertHoldersByUserId.clear();
                int n = certHolders.size();
                for (int i = 0; i < n; ++i) {
                    mCertHoldersByUserId.put(certHolders.keyAt(i), certHolders.valueAt(i));
                }
                mAdapter.notifyDataSetChanged();
                mProgressBar.setVisibility(View.GONE);
                mContentView.setVisibility(View.VISIBLE);
                mProgressBar.setProgress(0);
                mAliasLoaders.remove(this);
                showTrustAllCaDialogIfNeeded();
            }

            private boolean isUserTabAndTrustAllCertMode() {
                return isTrustAllCaCertModeInProgress() && mTab == Tab.USER;
            }

            @UiThread
            private void showTrustAllCaDialogIfNeeded() {
                if (!isUserTabAndTrustAllCertMode()) {
                    return;
                }
                List<CertHolder> certHolders = mCertHoldersByUserId.get(mTrustAllCaUserId);
                if (certHolders == null) {
                    return;
                }

                List<CertHolder> unapprovedUserCertHolders = new ArrayList<>();
                DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
                for (CertHolder cert : certHolders) {
                    if (cert != null && !dpm.isCaCertApproved(cert.mAlias, mTrustAllCaUserId)) {
                        unapprovedUserCertHolders.add(cert);
                    }
                }

                if (unapprovedUserCertHolders.size() == 0) {
                    Log.w(TAG, "no cert is pending approval for user " + mTrustAllCaUserId);
                    return;
                }
                showTrustAllCaDialog(unapprovedUserCertHolders);
            }
        }

        public void remove(CertHolder certHolder) {
            if (mCertHoldersByUserId != null) {
                List<CertHolder> certs = mCertHoldersByUserId.get(certHolder.mProfileId);
                if (certs != null) {
                    certs.remove(certHolder);
                }
            }
        }
    }

    /* package */ static class CertHolder implements Comparable<CertHolder> {
        public int mProfileId;
        private final IKeyChainService mService;
        private final GroupAdapter mAdapter;
        private final Tab mTab;
        private final String mAlias;
        private final X509Certificate mX509Cert;

        private final SslCertificate mSslCert;
        private final String mSubjectPrimary;
        private final String mSubjectSecondary;
        private boolean mDeleted;

        private CertHolder(IKeyChainService service,
                GroupAdapter adapter,
                Tab tab,
                String alias,
                X509Certificate x509Cert,
                int profileId) {
            mProfileId = profileId;
            mService = service;
            mAdapter = adapter;
            mTab = tab;
            mAlias = alias;
            mX509Cert = x509Cert;

            mSslCert = new SslCertificate(x509Cert);

            String cn = mSslCert.getIssuedTo().getCName();
            String o = mSslCert.getIssuedTo().getOName();
            String ou = mSslCert.getIssuedTo().getUName();
            // if we have a O, use O as primary subject, secondary prefer CN over OU
            // if we don't have an O, use CN as primary, empty secondary
            // if we don't have O or CN, use DName as primary, empty secondary
            if (!o.isEmpty()) {
                if (!cn.isEmpty()) {
                    mSubjectPrimary = o;
                    mSubjectSecondary = cn;
                } else {
                    mSubjectPrimary = o;
                    mSubjectSecondary = ou;
                }
            } else {
                if (!cn.isEmpty()) {
                    mSubjectPrimary = cn;
                    mSubjectSecondary = "";
                } else {
                    mSubjectPrimary = mSslCert.getIssuedTo().getDName();
                    mSubjectSecondary = "";
                }
            }
            try {
                mDeleted = mTab.deleted(mService, mAlias);
            } catch (RemoteException e) {
                Log.e(TAG, "Remote exception while checking if alias " + mAlias + " is deleted.",
                        e);
                mDeleted = false;
            }
        }

        @Override
        public int compareTo(CertHolder o) {
            int primary = this.mSubjectPrimary.compareToIgnoreCase(o.mSubjectPrimary);
            if (primary != 0) {
                return primary;
            }
            return this.mSubjectSecondary.compareToIgnoreCase(o.mSubjectSecondary);
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof CertHolder)) {
                return false;
            }
            CertHolder other = (CertHolder) o;
            return mAlias.equals(other.mAlias);
        }

        @Override
        public int hashCode() {
            return mAlias.hashCode();
        }

        public int getUserId() {
            return mProfileId;
        }

        public String getAlias() {
            return mAlias;
        }

        public boolean isSystemCert() {
            return mTab == Tab.SYSTEM;
        }

        public boolean isDeleted() {
            return mDeleted;
        }
    }


    private boolean isTrustAllCaCertModeInProgress() {
        return mTrustAllCaUserId != UserHandle.USER_NULL;
    }

    private void showTrustAllCaDialog(List<CertHolder> unapprovedCertHolders) {
        CertHolder[] arr =
                unapprovedCertHolders.toArray(new CertHolder[unapprovedCertHolders.size()]);
        new TrustedCredentialsDialogBuilder(getActivity(), this)
                .setCertHolders(arr)
                .setOnDismissListener(dialogInterface -> {
                    // Avoid starting dialog again after Activity restart.
                    getActivity().getIntent().removeExtra(ARG_SHOW_NEW_FOR_USER);
                    mTrustAllCaUserId = UserHandle.USER_NULL;
                })
                .show();
    }

    private void showCertDialog(final CertHolder certHolder) {
        new TrustedCredentialsDialogBuilder(getActivity(), this)
                .setCertHolder(certHolder)
                .show();
    }

    @Override
    public List<X509Certificate> getX509CertsFromCertHolder(CertHolder certHolder) {
        List<X509Certificate> certificates = null;
        try {
            synchronized (mKeyChainConnectionByProfileId) {
                KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get(
                        certHolder.mProfileId);
                IKeyChainService service = keyChainConnection.getService();
                List<String> chain = service.getCaCertificateChainAliases(certHolder.mAlias, true);
                certificates = new ArrayList<>(chain.size());
                for (String s : chain) {
                    byte[] encodedCertificate = service.getEncodedCaCertificate(s, true);
                    X509Certificate certificate = KeyChain.toCertificate(encodedCertificate);
                    certificates.add(certificate);
                }
            }
        } catch (RemoteException ex) {
            Log.e(TAG, "RemoteException while retrieving certificate chain for root "
                    + certHolder.mAlias, ex);
        }
        return certificates;
    }

    @Override
    public void removeOrInstallCert(CertHolder certHolder) {
        new AliasOperation(certHolder).execute();
    }

    @Override
    public boolean startConfirmCredentialIfNotConfirmed(int userId,
            IntConsumer onCredentialConfirmedListener) {
        if (mConfirmedCredentialUsers.contains(userId)) {
            // Credential has been confirmed. Don't start activity.
            return false;
        }

        boolean result = startConfirmCredential(userId);
        if (result) {
            mConfirmingCredentialListener = onCredentialConfirmedListener;
        }
        return result;
    }

    private class AliasOperation extends AsyncTask<Void, Void, Boolean> {
        private final CertHolder mCertHolder;

        private AliasOperation(CertHolder certHolder) {
            mCertHolder = certHolder;
            mAliasOperation = this;
        }

        @Override
        protected Boolean doInBackground(Void... params) {
            try {
                synchronized (mKeyChainConnectionByProfileId) {
                    KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get(
                            mCertHolder.mProfileId);
                    IKeyChainService service = keyChainConnection.getService();
                    if (mCertHolder.mDeleted) {
                        byte[] bytes = mCertHolder.mX509Cert.getEncoded();
                        service.installCaCertificate(bytes);
                        return true;
                    } else {
                        return service.deleteCaCertificate(mCertHolder.mAlias);
                    }
                }
            } catch (CertificateEncodingException | SecurityException | IllegalStateException
                    | RemoteException e) {
                Log.w(TAG, "Error while toggling alias " + mCertHolder.mAlias, e);
                return false;
            }
        }

        @Override
        protected void onPostExecute(Boolean ok) {
            if (ok) {
                if (mCertHolder.mTab.mSwitch) {
                    mCertHolder.mDeleted = !mCertHolder.mDeleted;
                } else {
                    mCertHolder.mAdapter.remove(mCertHolder);
                }
                mCertHolder.mAdapter.notifyDataSetChanged();
            } else {
                // bail, reload to reset to known state
                mCertHolder.mAdapter.load();
            }
            mAliasOperation = null;
        }
    }
}
