/*
 * Copyright (C) 2018 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.server.telecom.bluetooth;

import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_GONE;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_PRESENT;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioDeviceInfo;
import android.os.Bundle;
import android.telecom.Log;
import android.telecom.Logging.Session;
import android.util.Pair;

import com.android.internal.os.SomeArgs;
import com.android.server.telecom.AudioRoute;
import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
import com.android.server.telecom.CallAudioRouteAdapter;
import com.android.server.telecom.CallAudioRouteController;
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.flags.Flags;

public class BluetoothStateReceiver extends BroadcastReceiver {
    private static final String LOG_TAG = BluetoothStateReceiver.class.getSimpleName();
    public static final IntentFilter INTENT_FILTER;
    static {
        INTENT_FILTER = new IntentFilter();
        INTENT_FILTER.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
        INTENT_FILTER.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
        INTENT_FILTER.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
        INTENT_FILTER.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
        INTENT_FILTER.addAction(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
        INTENT_FILTER.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
        INTENT_FILTER.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED);
        INTENT_FILTER.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
    }

    // If not in a call, BSR won't listen to the Bluetooth stack's HFP on/off messages, since
    // other apps could be turning it on and off. We don't want to interfere.
    private boolean mIsInCall = false;
    private final BluetoothRouteManager mBluetoothRouteManager;
    private final BluetoothDeviceManager mBluetoothDeviceManager;
    private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
    private FeatureFlags mFeatureFlags;
    private CallAudioRouteAdapter mCallAudioRouteAdapter;

    public void onReceive(Context context, Intent intent) {
        Log.startSession("BSR.oR");
        try {
            String action = intent.getAction();
            switch (action) {
                case BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED:
                    handleAudioStateChanged(intent);
                    break;
                case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED:
                case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED:
                case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:
                    handleConnectionStateChanged(intent);
                    break;
                case BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED:
                case BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED:
                case BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED:
                    handleActiveDeviceChanged(intent);
                    break;
            }
        } finally {
            Log.endSession();
        }
    }

    private void handleAudioStateChanged(Intent intent) {
        int bluetoothHeadsetAudioState =
                intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
                        BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
        BluetoothDevice device =
                intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
        if (device == null) {
            Log.w(LOG_TAG, "Got null device from broadcast. " +
                    "Ignoring.");
            return;
        }

        Log.i(LOG_TAG, "Device %s transitioned to audio state %d",
                device.getAddress(), bluetoothHeadsetAudioState);
        Session session = Log.createSubsession();
        SomeArgs args = SomeArgs.obtain();
        args.arg1 = session;
        args.arg2 = device.getAddress();
        switch (bluetoothHeadsetAudioState) {
            case BluetoothHeadset.STATE_AUDIO_CONNECTED:
                if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
                    CallAudioRouteController audioRouteController =
                            (CallAudioRouteController) mCallAudioRouteAdapter;
                    audioRouteController.setIsScoAudioConnected(true);
                    if (audioRouteController.isPending()) {
                        mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
                                device);
                    } else {
                        // It's possible that the initial BT connection fails but BT_AUDIO_CONNECTED
                        // is sent later, indicating that SCO audio is on. We should route
                        // appropriately in order for the UI to reflect this state.
                        AudioRoute btRoute = audioRouteController.getBluetoothRoute(
                                AudioRoute.TYPE_BLUETOOTH_SCO, device.getAddress());
                        if (btRoute != null) {
                            audioRouteController.getPendingAudioRoute().overrideDestRoute(btRoute);
                            audioRouteController.overrideIsPending(true);
                            audioRouteController.getPendingAudioRoute()
                                    .setCommunicationDeviceType(AudioRoute.TYPE_BLUETOOTH_SCO);
                            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                                    CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
                        }
                    }
                } else {
                    if (!mIsInCall) {
                        Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call");
                        return;
                    }
                    mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
                }
                break;
            case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
                if (Flags.useRefactoredAudioRouteSwitching()) {
                    CallAudioRouteController audioRouteController =
                            (CallAudioRouteController) mCallAudioRouteAdapter;
                    audioRouteController.setIsScoAudioConnected(false);
                    mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
                            device);
                }  else {
                    mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
                }
                break;
        }
    }

    private void handleConnectionStateChanged(Intent intent) {
        int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
                BluetoothHeadset.STATE_DISCONNECTED);
        BluetoothDevice device =
                intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);

        if (device == null) {
            Log.w(LOG_TAG, "Got null device from broadcast. " +
                    "Ignoring.");
            return;
        }

        int deviceType;
        @AudioRoute.AudioRouteType int audioRouteType;
        if (BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
            deviceType = BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO;
            audioRouteType = AudioRoute.TYPE_BLUETOOTH_LE;
        } else if (BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
            deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID;
            audioRouteType = AudioRoute.TYPE_BLUETOOTH_HA;
        } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
            deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEADSET;
            audioRouteType = AudioRoute.TYPE_BLUETOOTH_SCO;
        } else {
            Log.w(LOG_TAG, "handleConnectionStateChanged: %s invalid device type", device);
            return;
        }

        Log.i(LOG_TAG, "%s device %s changed state to %d",
                BluetoothDeviceManager.getDeviceTypeString(deviceType),
                device.getAddress(), bluetoothHeadsetState);

        if (bluetoothHeadsetState == BluetoothProfile.STATE_CONNECTED) {
            if (Flags.useRefactoredAudioRouteSwitching()) {
                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_ADDED,
                        audioRouteType, device);
            } else {
                mBluetoothDeviceManager.onDeviceConnected(device, deviceType);
            }
        } else if (bluetoothHeadsetState == BluetoothProfile.STATE_DISCONNECTED
                || bluetoothHeadsetState == BluetoothProfile.STATE_DISCONNECTING) {
            if (Flags.useRefactoredAudioRouteSwitching()) {
                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_REMOVED,
                        audioRouteType, device);
            } else {
                mBluetoothDeviceManager.onDeviceDisconnected(device, deviceType);
            }
        }
    }

    private void handleActiveDeviceChanged(Intent intent) {
        BluetoothDevice device =
                intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);

        int deviceType;
        @AudioRoute.AudioRouteType int audioRouteType;
        if (BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
            deviceType = BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO;
            audioRouteType = AudioRoute.TYPE_BLUETOOTH_LE;
        } else if (BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
            deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID;
            audioRouteType = AudioRoute.TYPE_BLUETOOTH_HA;
        } else if (BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
            deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEADSET;
            audioRouteType = AudioRoute.TYPE_BLUETOOTH_SCO;
        } else {
            Log.w(LOG_TAG, "handleActiveDeviceChanged: %s invalid device type", device);
            return;
        }

        Log.i(LOG_TAG, "Device %s is now the preferred BT device for %s", device,
                BluetoothDeviceManager.getDeviceTypeString(deviceType));

        if (Flags.useRefactoredAudioRouteSwitching()) {
            CallAudioRouteController audioRouteController = (CallAudioRouteController)
                    mCallAudioRouteAdapter;
            if (device == null) {
                audioRouteController.updateActiveBluetoothDevice(new Pair(audioRouteType, null));
                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_GONE,
                        audioRouteType);
            } else {
                audioRouteController.updateActiveBluetoothDevice(
                        new Pair(audioRouteType, device.getAddress()));
                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
                        audioRouteType, device.getAddress());
                if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID
                        || deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
                    if (!mBluetoothDeviceManager.setCommunicationDeviceForAddress(
                            device.getAddress())) {
                        Log.i(this, "handleActiveDeviceChanged: Failed to set "
                                + "communication device for %s. Sending PENDING_ROUTE_FAILED to "
                                + "pending audio route.", device);
                        mCallAudioRouteAdapter.getPendingAudioRoute()
                                .onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
                                        device.getAddress()), device.getAddress());
                    } else {
                        // Track the currently set communication device.
                        int routeType = deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
                                ? AudioRoute.TYPE_BLUETOOTH_LE
                                : AudioRoute.TYPE_BLUETOOTH_HA;
                        mCallAudioRouteAdapter.getPendingAudioRoute()
                                .setCommunicationDeviceType(routeType);
                    }
                }
            }
        } else {
            mBluetoothRouteManager.onActiveDeviceChanged(device, deviceType);
            if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID ||
                    deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
                Session session = Log.createSubsession();
                SomeArgs args = SomeArgs.obtain();
                args.arg1 = session;
                if (device == null) {
                    mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
                } else {
                    if (!mIsInCall) {
                        Log.i(LOG_TAG, "Ignoring audio on since we're not in a call");
                        return;
                    }
                    args.arg2 = device.getAddress();

                    boolean usePreferredAudioProfile = false;
                    BluetoothAdapter bluetoothAdapter = mBluetoothDeviceManager
                            .getBluetoothAdapter();
                    int preferredDuplexProfile = BluetoothProfile.LE_AUDIO;
                    if (bluetoothAdapter != null) {
                        Bundle preferredAudioProfiles = bluetoothAdapter.getPreferredAudioProfiles(
                                device);
                        if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
                                && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)
                                != 0) {
                            Log.i(this, "Preferred duplex profile for device=" + device + " is "
                                    + preferredAudioProfiles.getInt(
                                    BluetoothAdapter.AUDIO_MODE_DUPLEX));
                            usePreferredAudioProfile = true;
                            preferredDuplexProfile =
                                    preferredAudioProfiles.getInt(
                                            BluetoothAdapter.AUDIO_MODE_DUPLEX);
                        }
                    }

                    if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
                        /* In Le Audio case, once device got Active, the Telecom needs to make sure
                         * it is set as communication device before we can say that BT_AUDIO_IS_ON
                         */
                        boolean isLeAudioSetForCommunication =
                                mFeatureFlags.callAudioCommunicationDeviceRefactor()
                                        ? mCommunicationDeviceTracker.setCommunicationDevice(
                                        AudioDeviceInfo.TYPE_BLE_HEADSET, device)
                                        : mBluetoothDeviceManager.setLeAudioCommunicationDevice();
                        if ((!usePreferredAudioProfile
                                || preferredDuplexProfile == BluetoothProfile.LE_AUDIO)
                                && !isLeAudioSetForCommunication) {
                            Log.w(LOG_TAG,
                                    "Device %s cannot be use as LE audio communication device.",
                                    device);
                        }
                    } else {
                        boolean isHearingAidSetForCommunication =
                                mFeatureFlags.callAudioCommunicationDeviceRefactor()
                                        ? mCommunicationDeviceTracker.setCommunicationDevice(
                                        AudioDeviceInfo.TYPE_HEARING_AID, null)
                                        : mBluetoothDeviceManager
                                        .setHearingAidCommunicationDevice();
                        /* deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID */
                        if (!isHearingAidSetForCommunication) {
                            Log.w(LOG_TAG,
                                    "Device %s cannot be use as hearing aid communication device.",
                                    device);
                        } else {
                            mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
                        }
                    }
                }
            }
        }
    }

    public BluetoothDeviceManager getBluetoothDeviceManager() {
        return mBluetoothDeviceManager;
    }

    public BluetoothStateReceiver(BluetoothDeviceManager deviceManager,
            BluetoothRouteManager routeManager,
            CallAudioCommunicationDeviceTracker communicationDeviceTracker,
            FeatureFlags featureFlags) {
        mBluetoothDeviceManager = deviceManager;
        mBluetoothRouteManager = routeManager;
        mCommunicationDeviceTracker = communicationDeviceTracker;
        mFeatureFlags = featureFlags;
    }

    public void setIsInCall(boolean isInCall) {
        mIsInCall = isInCall;
    }

    public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) {
        mCallAudioRouteAdapter = adapter;
    }
}
