/*
 * Copyright (C) 2011 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.settingslib.bluetooth;

import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothA2dpSink;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHapClient;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothHidDevice;
import android.bluetooth.BluetoothHidHost;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothMap;
import android.bluetooth.BluetoothMapClient;
import android.bluetooth.BluetoothPan;
import android.bluetooth.BluetoothPbap;
import android.bluetooth.BluetoothPbapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothSap;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.BluetoothVolumeControl;
import android.content.Context;
import android.content.Intent;
import android.os.ParcelUuid;
import android.util.Log;

import androidx.annotation.VisibleForTesting;

import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
import com.android.settingslib.flags.Flags;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;


/**
 * LocalBluetoothProfileManager provides access to the LocalBluetoothProfile
 * objects for the available Bluetooth profiles.
 */
public class LocalBluetoothProfileManager {
    private static final String TAG = "LocalBluetoothProfileManager";
    private static final boolean DEBUG = BluetoothUtils.D;

    /**
     * An interface for notifying BluetoothHeadset IPC clients when they have
     * been connected to the BluetoothHeadset service.
     * Only used by com.android.settings.bluetooth.DockService.
     */
    public interface ServiceListener {
        /**
         * Called to notify the client when this proxy object has been
         * connected to the BluetoothHeadset service. Clients must wait for
         * this callback before making IPC calls on the BluetoothHeadset
         * service.
         */
        void onServiceConnected();

        /**
         * Called to notify the client that this proxy object has been
         * disconnected from the BluetoothHeadset service. Clients must not
         * make IPC calls on the BluetoothHeadset service after this callback.
         * This callback will currently only occur if the application hosting
         * the BluetoothHeadset service, but may be called more often in future.
         */
        void onServiceDisconnected();
    }

    private final Context mContext;
    private final CachedBluetoothDeviceManager mDeviceManager;
    private final BluetoothEventManager mEventManager;

    private A2dpProfile mA2dpProfile;
    private A2dpSinkProfile mA2dpSinkProfile;
    private HeadsetProfile mHeadsetProfile;
    private HfpClientProfile mHfpClientProfile;
    private MapProfile mMapProfile;
    private MapClientProfile mMapClientProfile;
    private HidProfile mHidProfile;
    private HidDeviceProfile mHidDeviceProfile;
    private OppProfile mOppProfile;
    private PanProfile mPanProfile;
    private PbapClientProfile mPbapClientProfile;
    private PbapServerProfile mPbapProfile;
    private HearingAidProfile mHearingAidProfile;
    private HapClientProfile mHapClientProfile;
    private CsipSetCoordinatorProfile mCsipSetCoordinatorProfile;
    private LeAudioProfile mLeAudioProfile;
    private LocalBluetoothLeBroadcast mLeAudioBroadcast;
    private LocalBluetoothLeBroadcastAssistant mLeAudioBroadcastAssistant;
    private SapProfile mSapProfile;
    private VolumeControlProfile mVolumeControlProfile;

    /**
     * Mapping from profile name, e.g. "HEADSET" to profile object.
     */
    private final Map<String, LocalBluetoothProfile>
            mProfileNameMap = new HashMap<String, LocalBluetoothProfile>();

    LocalBluetoothProfileManager(Context context,
            LocalBluetoothAdapter adapter,
            CachedBluetoothDeviceManager deviceManager,
            BluetoothEventManager eventManager) {
        mContext = context;

        mDeviceManager = deviceManager;
        mEventManager = eventManager;
        // pass this reference to adapter and event manager (circular dependency)
        adapter.setProfileManager(this);

        if (DEBUG) Log.d(TAG, "LocalBluetoothProfileManager construction complete");
    }

    /**
     * create profile instance according to bluetooth supported profile list
     */
    synchronized void updateLocalProfiles() {
        List<Integer> supportedList = BluetoothAdapter.getDefaultAdapter().getSupportedProfiles();
        if (CollectionUtils.isEmpty(supportedList)) {
            if (DEBUG) Log.d(TAG, "supportedList is null");
            return;
        }
        if (mA2dpProfile == null && supportedList.contains(BluetoothProfile.A2DP)) {
            if (DEBUG) Log.d(TAG, "Adding local A2DP profile");
            mA2dpProfile = new A2dpProfile(mContext, mDeviceManager, this);
            addProfile(mA2dpProfile, A2dpProfile.NAME,
                    BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mA2dpSinkProfile == null && supportedList.contains(BluetoothProfile.A2DP_SINK)) {
            if (DEBUG) Log.d(TAG, "Adding local A2DP SINK profile");
            mA2dpSinkProfile = new A2dpSinkProfile(mContext, mDeviceManager, this);
            addProfile(mA2dpSinkProfile, A2dpSinkProfile.NAME,
                    BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mHeadsetProfile == null && supportedList.contains(BluetoothProfile.HEADSET)) {
            if (DEBUG) Log.d(TAG, "Adding local HEADSET profile");
            mHeadsetProfile = new HeadsetProfile(mContext, mDeviceManager, this);
            addHeadsetProfile(mHeadsetProfile, HeadsetProfile.NAME,
                    BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED,
                    BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,
                    BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
        }
        if (mHfpClientProfile == null && supportedList.contains(BluetoothProfile.HEADSET_CLIENT)) {
            if (DEBUG) Log.d(TAG, "Adding local HfpClient profile");
            mHfpClientProfile = new HfpClientProfile(mContext, mDeviceManager, this);
            addProfile(mHfpClientProfile, HfpClientProfile.NAME,
                    BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mMapClientProfile == null && supportedList.contains(BluetoothProfile.MAP_CLIENT)) {
            if (DEBUG) Log.d(TAG, "Adding local MAP CLIENT profile");
            mMapClientProfile = new MapClientProfile(mContext, mDeviceManager,this);
            addProfile(mMapClientProfile, MapClientProfile.NAME,
                    BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mMapProfile == null && supportedList.contains(BluetoothProfile.MAP)) {
            if (DEBUG) Log.d(TAG, "Adding local MAP profile");
            mMapProfile = new MapProfile(mContext, mDeviceManager, this);
            addProfile(mMapProfile, MapProfile.NAME, BluetoothMap.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mOppProfile == null && supportedList.contains(BluetoothProfile.OPP)) {
            if (DEBUG) Log.d(TAG, "Adding local OPP profile");
            mOppProfile = new OppProfile();
            // Note: no event handler for OPP, only name map.
            mProfileNameMap.put(OppProfile.NAME, mOppProfile);
        }
        if (mHearingAidProfile == null && supportedList.contains(BluetoothProfile.HEARING_AID)) {
            if (DEBUG) Log.d(TAG, "Adding local Hearing Aid profile");
            mHearingAidProfile = new HearingAidProfile(mContext, mDeviceManager,
                    this);
            addProfile(mHearingAidProfile, HearingAidProfile.NAME,
                    BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mHapClientProfile == null && supportedList.contains(BluetoothProfile.HAP_CLIENT)) {
            if (DEBUG) Log.d(TAG, "Adding local HAP_CLIENT profile");
            mHapClientProfile = new HapClientProfile(mContext, mDeviceManager, this);
            addProfile(mHapClientProfile, HapClientProfile.NAME,
                    BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
        }
        if (mHidProfile == null && supportedList.contains(BluetoothProfile.HID_HOST)) {
            if (DEBUG) Log.d(TAG, "Adding local HID_HOST profile");
            mHidProfile = new HidProfile(mContext, mDeviceManager, this);
            addProfile(mHidProfile, HidProfile.NAME,
                    BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mHidDeviceProfile == null && supportedList.contains(BluetoothProfile.HID_DEVICE)) {
            if (DEBUG) Log.d(TAG, "Adding local HID_DEVICE profile");
            mHidDeviceProfile = new HidDeviceProfile(mContext, mDeviceManager, this);
            addProfile(mHidDeviceProfile, HidDeviceProfile.NAME,
                    BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mPanProfile == null && supportedList.contains(BluetoothProfile.PAN)) {
            if (DEBUG) Log.d(TAG, "Adding local PAN profile");
            mPanProfile = new PanProfile(mContext);
            addPanProfile(mPanProfile, PanProfile.NAME,
                    BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mPbapProfile == null && supportedList.contains(BluetoothProfile.PBAP)) {
            if (DEBUG) Log.d(TAG, "Adding local PBAP profile");
            mPbapProfile = new PbapServerProfile(mContext);
            addProfile(mPbapProfile, PbapServerProfile.NAME,
                    BluetoothPbap.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mPbapClientProfile == null && supportedList.contains(BluetoothProfile.PBAP_CLIENT)) {
            if (DEBUG) Log.d(TAG, "Adding local PBAP Client profile");
            mPbapClientProfile = new PbapClientProfile(mContext, mDeviceManager,this);
            addProfile(mPbapClientProfile, PbapClientProfile.NAME,
                    BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mSapProfile == null && supportedList.contains(BluetoothProfile.SAP)) {
            if (DEBUG) {
                Log.d(TAG, "Adding local SAP profile");
            }
            mSapProfile = new SapProfile(mContext, mDeviceManager, this);
            addProfile(mSapProfile, SapProfile.NAME, BluetoothSap.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mVolumeControlProfile == null
                && supportedList.contains(BluetoothProfile.VOLUME_CONTROL)) {
            if (DEBUG) {
                Log.d(TAG, "Adding local Volume Control profile");
            }
            mVolumeControlProfile = new VolumeControlProfile(mContext, mDeviceManager, this);
            addProfile(mVolumeControlProfile, VolumeControlProfile.NAME,
                    BluetoothVolumeControl.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mLeAudioProfile == null && supportedList.contains(BluetoothProfile.LE_AUDIO)) {
            if (DEBUG) {
                Log.d(TAG, "Adding local LE_AUDIO profile");
            }
            mLeAudioProfile = new LeAudioProfile(mContext, mDeviceManager, this);
            addProfile(mLeAudioProfile, LeAudioProfile.NAME,
                    BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
        }
        if (mLeAudioBroadcast == null
                && supportedList.contains(BluetoothProfile.LE_AUDIO_BROADCAST)) {
            if (DEBUG) {
                Log.d(TAG, "Adding local LE_AUDIO_BROADCAST profile");
            }
            mLeAudioBroadcast = new LocalBluetoothLeBroadcast(mContext, mDeviceManager);
            // no event handler for the LE boradcast.
            mProfileNameMap.put(LocalBluetoothLeBroadcast.NAME, mLeAudioBroadcast);
        }
        if (mLeAudioBroadcastAssistant == null
                && supportedList.contains(BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT)) {
            if (DEBUG) {
                Log.d(TAG, "Adding local LE_AUDIO_BROADCAST_ASSISTANT profile");
            }
            mLeAudioBroadcastAssistant = new LocalBluetoothLeBroadcastAssistant(mContext,
                    mDeviceManager, this);
            addProfile(mLeAudioBroadcastAssistant, LocalBluetoothLeBroadcast.NAME,
                    BluetoothLeBroadcastAssistant.ACTION_CONNECTION_STATE_CHANGED);
        }
        if (mCsipSetCoordinatorProfile == null
                && supportedList.contains(BluetoothProfile.CSIP_SET_COORDINATOR)) {
            if (DEBUG) {
                Log.d(TAG, "Adding local CSIP set coordinator profile");
            }
            mCsipSetCoordinatorProfile =
                    new CsipSetCoordinatorProfile(mContext, mDeviceManager, this);
            addProfile(mCsipSetCoordinatorProfile, mCsipSetCoordinatorProfile.NAME,
                    BluetoothCsipSetCoordinator.ACTION_CSIS_CONNECTION_STATE_CHANGED);
        }
        mEventManager.registerProfileIntentReceiver();
    }

    private void addHeadsetProfile(LocalBluetoothProfile profile, String profileName,
            String stateChangedAction, String audioStateChangedAction, int audioDisconnectedState) {
        BluetoothEventManager.Handler handler = new HeadsetStateChangeHandler(
                profile, audioStateChangedAction, audioDisconnectedState);
        mEventManager.addProfileHandler(stateChangedAction, handler);
        mEventManager.addProfileHandler(audioStateChangedAction, handler);
        mProfileNameMap.put(profileName, profile);
    }

    private final Collection<ServiceListener> mServiceListeners =
            new CopyOnWriteArrayList<ServiceListener>();

    private void addProfile(LocalBluetoothProfile profile,
            String profileName, String stateChangedAction) {
        mEventManager.addProfileHandler(stateChangedAction, new StateChangedHandler(profile));
        mProfileNameMap.put(profileName, profile);
    }

    private void addPanProfile(LocalBluetoothProfile profile,
            String profileName, String stateChangedAction) {
        mEventManager.addProfileHandler(stateChangedAction,
                new PanStateChangedHandler(profile));
        mProfileNameMap.put(profileName, profile);
    }

    public LocalBluetoothProfile getProfileByName(String name) {
        return mProfileNameMap.get(name);
    }

    // Called from LocalBluetoothAdapter when state changes to ON
    void setBluetoothStateOn() {
        updateLocalProfiles();
        mEventManager.readPairedDevices();
    }

    /**
     * Generic handler for connection state change events for the specified profile.
     */
    private class StateChangedHandler implements BluetoothEventManager.Handler {
        final LocalBluetoothProfile mProfile;

        StateChangedHandler(LocalBluetoothProfile profile) {
            mProfile = profile;
        }

        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
            CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
            if (cachedDevice == null) {
                Log.w(TAG, "StateChangedHandler found new device: " + device);
                cachedDevice = mDeviceManager.addDevice(device);
            }
            onReceiveInternal(intent, cachedDevice);
        }

        protected void onReceiveInternal(Intent intent, CachedBluetoothDevice cachedDevice) {
            int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
            int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0);
            if (newState == BluetoothProfile.STATE_DISCONNECTED &&
                    oldState == BluetoothProfile.STATE_CONNECTING) {
                Log.i(TAG, "Failed to connect " + mProfile + " device");
            }
            final boolean isAshaProfile = getHearingAidProfile() != null
                    && mProfile instanceof HearingAidProfile;
            final boolean isHapClientProfile = getHapClientProfile() != null
                    && mProfile instanceof HapClientProfile;
            final boolean isLeAudioProfile = getLeAudioProfile() != null
                    && mProfile instanceof LeAudioProfile;
            final boolean isHapClientOrLeAudioProfile = isHapClientProfile || isLeAudioProfile;
            final boolean isCsipProfile = getCsipSetCoordinatorProfile() != null
                    && mProfile instanceof CsipSetCoordinatorProfile;

            if (isAshaProfile && (newState == BluetoothProfile.STATE_CONNECTED)) {
                if (DEBUG) {
                    Log.d(TAG, "onReceive, hearing aid profile connected, check hisyncid");
                }
                // Check if the HiSyncID has being initialized
                if (cachedDevice.getHiSyncId() == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
                    long newHiSyncId = getHearingAidProfile().getHiSyncId(cachedDevice.getDevice());
                    if (newHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
                        final BluetoothDevice device = cachedDevice.getDevice();
                        final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
                                .setAshaDeviceSide(getHearingAidProfile().getDeviceSide(device))
                                .setAshaDeviceMode(getHearingAidProfile().getDeviceMode(device))
                                .setHiSyncId(newHiSyncId);
                        cachedDevice.setHearingAidInfo(infoBuilder.build());
                    }
                }

                HearingAidStatsLogUtils.logHearingAidInfo(cachedDevice);
            }

            if (isHapClientOrLeAudioProfile && newState == BluetoothProfile.STATE_CONNECTED) {
                if (DEBUG) {
                    Log.d(TAG, "onReceive, hap/lea profile connected, check hearing aid info");
                }
                // Checks if both profiles are connected to the device. Hearing aid info need
                // to be retrieved from these profiles separately.
                if (cachedDevice.isConnectedLeAudioHearingAidDevice()) {
                    final BluetoothDevice device = cachedDevice.getDevice();
                    final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
                            .setLeAudioLocation(getLeAudioProfile().getAudioLocation(device))
                            .setHapDeviceType(getHapClientProfile().getHearingAidType(device));
                    cachedDevice.setHearingAidInfo(infoBuilder.build());
                    HearingAidStatsLogUtils.logHearingAidInfo(cachedDevice);
                }
            }

            if (isCsipProfile && (newState == BluetoothProfile.STATE_CONNECTED)) {
                if (DEBUG) {
                    Log.d(TAG, "onReceive, csip profile connected, check group id");
                }
                // Check if the GroupID has being initialized
                if (cachedDevice.getGroupId() == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
                    final Map<Integer, ParcelUuid> groupIdMap = getCsipSetCoordinatorProfile()
                            .getGroupUuidMapByDevice(cachedDevice.getDevice());
                    if (DEBUG) {
                        Log.d(TAG, "csip group uuid map = " + groupIdMap);
                    }
                    if (groupIdMap != null) {
                        for (Map.Entry<Integer, ParcelUuid> entry: groupIdMap.entrySet()) {
                            if (entry.getValue().equals(BluetoothUuid.CAP)) {
                                cachedDevice.setGroupId(entry.getKey());
                                break;
                            }
                        }
                    }
                }
            }

            // LE_AUDIO, CSIP_SET_COORDINATOR profiles will also impact the connection status
            // change, e.g. device need to active on LE_AUDIO to become active connection status.
            final Set<Integer> hearingDeviceConnectionStatusProfileId = Set.of(
                    BluetoothProfile.HEARING_AID,
                    BluetoothProfile.HAP_CLIENT,
                    BluetoothProfile.LE_AUDIO,
                    BluetoothProfile.CSIP_SET_COORDINATOR
            );
            if (Flags.hearingDeviceSetConnectionStatusReport()) {
                if (hearingDeviceConnectionStatusProfileId.contains(mProfile.getProfileId())) {
                    mDeviceManager.notifyHearingDevicesConnectionStatusChangedIfNeeded(
                            cachedDevice);
                }
            }

            cachedDevice.onProfileStateChanged(mProfile, newState);
            // Dispatch profile changed after device update
            boolean needDispatchProfileConnectionState = true;
            if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
                    || cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
                mDeviceManager.syncDeviceWithinHearingAidSetIfNeeded(cachedDevice, newState,
                        mProfile.getProfileId());
                needDispatchProfileConnectionState = !mDeviceManager
                        .onProfileConnectionStateChangedIfProcessed(cachedDevice, newState,
                        mProfile.getProfileId());
            }
            if (needDispatchProfileConnectionState) {
                if (DEBUG) {
                    Log.d(TAG, "needDispatchProfileConnectionState");
                }
                cachedDevice.refresh();
                mEventManager.dispatchProfileConnectionStateChanged(cachedDevice, newState,
                        mProfile.getProfileId());
            }
        }
    }

    /** Connectivity and audio state change handler for headset profiles. */
    private class HeadsetStateChangeHandler extends StateChangedHandler {
        private final String mAudioChangeAction;
        private final int mAudioDisconnectedState;

        HeadsetStateChangeHandler(LocalBluetoothProfile profile, String audioChangeAction,
                int audioDisconnectedState) {
            super(profile);
            mAudioChangeAction = audioChangeAction;
            mAudioDisconnectedState = audioDisconnectedState;
        }

        @Override
        public void onReceiveInternal(Intent intent, CachedBluetoothDevice cachedDevice) {
            if (mAudioChangeAction.equals(intent.getAction())) {
                int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
                if (newState != mAudioDisconnectedState) {
                    cachedDevice.onProfileStateChanged(mProfile, BluetoothProfile.STATE_CONNECTED);
                }
                cachedDevice.refresh();
            } else {
                super.onReceiveInternal(intent, cachedDevice);
            }
        }
    }

    /** State change handler for NAP and PANU profiles. */
    private class PanStateChangedHandler extends StateChangedHandler {

        PanStateChangedHandler(LocalBluetoothProfile profile) {
            super(profile);
        }

        @Override
        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
            PanProfile panProfile = (PanProfile) mProfile;
            int role = intent.getIntExtra(BluetoothPan.EXTRA_LOCAL_ROLE, 0);
            panProfile.setLocalRole(device, role);
            super.onReceive(context, intent, device);
        }
    }

    // called from DockService
    public void addServiceListener(ServiceListener l) {
        mServiceListeners.add(l);
    }

    // called from DockService
    public void removeServiceListener(ServiceListener l) {
        mServiceListeners.remove(l);
    }

    // not synchronized: use only from UI thread! (TODO: verify)
    void callServiceConnectedListeners() {
        final Collection<ServiceListener> listeners = new ArrayList<>(mServiceListeners);

        for (ServiceListener l : listeners) {
            l.onServiceConnected();
        }
    }

    // not synchronized: use only from UI thread! (TODO: verify)
    void callServiceDisconnectedListeners() {
        final Collection<ServiceListener> listeners = new ArrayList<>(mServiceListeners);

        for (ServiceListener listener : listeners) {
            listener.onServiceDisconnected();
        }
    }

    // This is called by DockService, so check Headset and A2DP.
    public synchronized boolean isManagerReady() {
        // Getting just the headset profile is fine for now. Will need to deal with A2DP
        // and others if they aren't always in a ready state.
        LocalBluetoothProfile profile = mHeadsetProfile;
        if (profile != null) {
            return profile.isProfileReady();
        }
        profile = mA2dpProfile;
        if (profile != null) {
            return profile.isProfileReady();
        }
        profile = mA2dpSinkProfile;
        if (profile != null) {
            return profile.isProfileReady();
        }
        return false;
    }

    public A2dpProfile getA2dpProfile() {
        return mA2dpProfile;
    }

    public A2dpSinkProfile getA2dpSinkProfile() {
        if ((mA2dpSinkProfile != null) && (mA2dpSinkProfile.isProfileReady())) {
            return mA2dpSinkProfile;
        } else {
            return null;
        }
    }

    public HeadsetProfile getHeadsetProfile() {
        return mHeadsetProfile;
    }

    public HfpClientProfile getHfpClientProfile() {
        if ((mHfpClientProfile != null) && (mHfpClientProfile.isProfileReady())) {
            return mHfpClientProfile;
        } else {
          return null;
        }
    }

    public PbapClientProfile getPbapClientProfile() {
        return mPbapClientProfile;
    }

    public PbapServerProfile getPbapProfile(){
        return mPbapProfile;
    }

    public MapProfile getMapProfile(){
        return mMapProfile;
    }

    public MapClientProfile getMapClientProfile() {
        return mMapClientProfile;
    }

    public HearingAidProfile getHearingAidProfile() {
        return mHearingAidProfile;
    }

    public HapClientProfile getHapClientProfile() {
        return mHapClientProfile;
    }

    public LeAudioProfile getLeAudioProfile() {
        return mLeAudioProfile;
    }

    public LocalBluetoothLeBroadcast getLeAudioBroadcastProfile() {
        return mLeAudioBroadcast;
    }
    public LocalBluetoothLeBroadcastAssistant getLeAudioBroadcastAssistantProfile() {
        return mLeAudioBroadcastAssistant;
    }

    SapProfile getSapProfile() {
        return mSapProfile;
    }

    public HidProfile getHidProfile() {
        return mHidProfile;
    }

    @VisibleForTesting
    HidDeviceProfile getHidDeviceProfile() {
        return mHidDeviceProfile;
    }

    public CsipSetCoordinatorProfile getCsipSetCoordinatorProfile() {
        return mCsipSetCoordinatorProfile;
    }

    public VolumeControlProfile getVolumeControlProfile() {
        return mVolumeControlProfile;
    }

    /**
     * Fill in a list of LocalBluetoothProfile objects that are supported by
     * the local device and the remote device.
     *
     * @param uuids of the remote device
     * @param localUuids UUIDs of the local device
     * @param profiles The list of profiles to fill
     * @param removedProfiles list of profiles that were removed
     */
    synchronized void updateProfiles(ParcelUuid[] uuids, ParcelUuid[] localUuids,
            Collection<LocalBluetoothProfile> profiles,
            Collection<LocalBluetoothProfile> removedProfiles,
            boolean isPanNapConnected, BluetoothDevice device) {
        // Copy previous profile list into removedProfiles
        removedProfiles.clear();
        removedProfiles.addAll(profiles);
        if (DEBUG) {
            Log.d(TAG,"Current Profiles" + profiles.toString());
        }
        profiles.clear();

        if (uuids == null) {
            return;
        }

        // The profiles list's sequence will affect the bluetooth icon at
        // BluetoothUtils.getBtClassDrawableWithDescription(Context,CachedBluetoothDevice).

        // Moving the LE audio profile to be the first priority if the device supports LE audio.
        if (ArrayUtils.contains(uuids, BluetoothUuid.LE_AUDIO) && mLeAudioProfile != null) {
            profiles.add(mLeAudioProfile);
            removedProfiles.remove(mLeAudioProfile);
        }

        if (mHeadsetProfile != null) {
            if ((ArrayUtils.contains(localUuids, BluetoothUuid.HSP_AG)
                    && ArrayUtils.contains(uuids, BluetoothUuid.HSP))
                    || (ArrayUtils.contains(localUuids, BluetoothUuid.HFP_AG)
                    && ArrayUtils.contains(uuids, BluetoothUuid.HFP))) {
                profiles.add(mHeadsetProfile);
                removedProfiles.remove(mHeadsetProfile);
            }
        }

        if ((mHfpClientProfile != null) &&
                ArrayUtils.contains(uuids, BluetoothUuid.HFP_AG)
                && ArrayUtils.contains(localUuids, BluetoothUuid.HFP)) {
            profiles.add(mHfpClientProfile);
            removedProfiles.remove(mHfpClientProfile);
        }

        if (BluetoothUuid.containsAnyUuid(uuids, A2dpProfile.SINK_UUIDS) && mA2dpProfile != null) {
            profiles.add(mA2dpProfile);
            removedProfiles.remove(mA2dpProfile);
        }

        if (BluetoothUuid.containsAnyUuid(uuids, A2dpSinkProfile.SRC_UUIDS)
                && mA2dpSinkProfile != null) {
                profiles.add(mA2dpSinkProfile);
                removedProfiles.remove(mA2dpSinkProfile);
        }

        if (ArrayUtils.contains(uuids, BluetoothUuid.OBEX_OBJECT_PUSH) && mOppProfile != null) {
            profiles.add(mOppProfile);
            removedProfiles.remove(mOppProfile);
        }

        if ((ArrayUtils.contains(uuids, BluetoothUuid.HID)
                || ArrayUtils.contains(uuids, BluetoothUuid.HOGP)) && mHidProfile != null) {
            profiles.add(mHidProfile);
            removedProfiles.remove(mHidProfile);
        }

        if (mHidDeviceProfile != null && mHidDeviceProfile.getConnectionStatus(device)
                != BluetoothProfile.STATE_DISCONNECTED) {
            profiles.add(mHidDeviceProfile);
            removedProfiles.remove(mHidDeviceProfile);
        }

        if(isPanNapConnected)
            if(DEBUG) Log.d(TAG, "Valid PAN-NAP connection exists.");
        if ((ArrayUtils.contains(uuids, BluetoothUuid.NAP) && mPanProfile != null)
                || isPanNapConnected) {
            profiles.add(mPanProfile);
            removedProfiles.remove(mPanProfile);
        }

        if ((mMapProfile != null) &&
            (mMapProfile.getConnectionStatus(device) == BluetoothProfile.STATE_CONNECTED)) {
            profiles.add(mMapProfile);
            removedProfiles.remove(mMapProfile);
            mMapProfile.setEnabled(device, true);
        }

        if ((mPbapProfile != null) &&
            (mPbapProfile.getConnectionStatus(device) == BluetoothProfile.STATE_CONNECTED)) {
            profiles.add(mPbapProfile);
            removedProfiles.remove(mPbapProfile);
            mPbapProfile.setEnabled(device, true);
        }

        if ((mMapClientProfile != null)
                && BluetoothUuid.containsAnyUuid(uuids, MapClientProfile.UUIDS)) {
            profiles.add(mMapClientProfile);
            removedProfiles.remove(mMapClientProfile);
        }

        if ((mPbapClientProfile != null)
                && BluetoothUuid.containsAnyUuid(uuids, PbapClientProfile.SRC_UUIDS)) {
            profiles.add(mPbapClientProfile);
            removedProfiles.remove(mPbapClientProfile);
        }

        if (ArrayUtils.contains(uuids, BluetoothUuid.HEARING_AID) && mHearingAidProfile != null) {
            profiles.add(mHearingAidProfile);
            removedProfiles.remove(mHearingAidProfile);
        }

        if (mHapClientProfile != null && ArrayUtils.contains(uuids, BluetoothUuid.HAS)) {
            profiles.add(mHapClientProfile);
            removedProfiles.remove(mHapClientProfile);
        }

        if (mSapProfile != null && ArrayUtils.contains(uuids, BluetoothUuid.SAP)) {
            profiles.add(mSapProfile);
            removedProfiles.remove(mSapProfile);
        }

        if (mVolumeControlProfile != null
                && ArrayUtils.contains(uuids, BluetoothUuid.VOLUME_CONTROL)) {
            profiles.add(mVolumeControlProfile);
            removedProfiles.remove(mVolumeControlProfile);
        }

        if (mCsipSetCoordinatorProfile != null
                && ArrayUtils.contains(uuids, BluetoothUuid.COORDINATED_SET)) {
            profiles.add(mCsipSetCoordinatorProfile);
            removedProfiles.remove(mCsipSetCoordinatorProfile);
        }

        if (DEBUG) {
            Log.d(TAG,"New Profiles" + profiles.toString());
        }
    }
}
