chromium/media/base/android/java/src/org/chromium/media/AudioDeviceSelectorPostS.java

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.media;

import android.Manifest;
import android.content.pm.PackageManager;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import android.os.Process;

import androidx.annotation.RequiresApi;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;

import java.util.ArrayList;
import java.util.List;

@RequiresApi(Build.VERSION_CODES.S)
class AudioDeviceSelectorPostS extends AudioDeviceSelector {
    private static final String TAG = "media";

    private boolean mHasBluetoothConnectPermission;

    public AudioDeviceSelectorPostS(AudioManager audioManager) {
        super(audioManager);
    }

    private static List<Integer> getTargetTypesFromId(int deviceId) {
        List<Integer> types = new ArrayList<Integer>();

        switch (deviceId) {
            case Devices.ID_SPEAKERPHONE:
                types.add(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
                break;
            case Devices.ID_WIRED_HEADSET:
                types.add(AudioDeviceInfo.TYPE_WIRED_HEADSET);
                types.add(AudioDeviceInfo.TYPE_WIRED_HEADPHONES);
                break;
            case Devices.ID_EARPIECE:
                types.add(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
                break;
            case Devices.ID_BLUETOOTH_HEADSET:
                // Add Bluteooth LE Audio devices.
                types.add(AudioDeviceInfo.TYPE_BLE_HEADSET);
                types.add(AudioDeviceInfo.TYPE_BLE_SPEAKER);

                // Add "classic" bluetooth devices.
                types.add(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
                types.add(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
                break;
            case Devices.ID_USB_AUDIO:
                types.add(AudioDeviceInfo.TYPE_USB_HEADSET);
                types.add(AudioDeviceInfo.TYPE_USB_DEVICE);
                break;
        }

        return types;
    }

    @Override
    public void init() {
        mHasBluetoothConnectPermission =
                ApiCompatibilityUtils.checkPermission(
                                ContextUtils.getApplicationContext(),
                                Manifest.permission.BLUETOOTH_CONNECT,
                                Process.myPid(),
                                Process.myUid())
                        == PackageManager.PERMISSION_GRANTED;

        if (!mHasBluetoothConnectPermission) {
            Log.w(TAG, "BLUETOOTH_CONNECT permission is missing.");
        }

        mDeviceListener.init(mHasBluetoothConnectPermission);
    }

    @Override
    public void close() {
        mDeviceListener.close();
    }

    @Override
    public void setCommunicationAudioModeOn(boolean on) {
        if (on) {
            // TODO(crbug.com/40222537): Prompt for BLUETOOTH_CONNECT permission at this point if we
            // don't have it.
        } else {
            mDeviceStates.clearRequestedDevice();
            mAudioManager.clearCommunicationDevice();
        }
    }

    @Override
    public boolean isSpeakerphoneOn() {
        AudioDeviceInfo currentDevice = mAudioManager.getCommunicationDevice();
        return currentDevice != null
                && currentDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
    }

    @Override
    public void setSpeakerphoneOn(boolean on) {
        boolean isCurrentlyOn = isSpeakerphoneOn();

        if (isCurrentlyOn == on) return;

        if (on) {
            setAudioDevice(Devices.ID_SPEAKERPHONE);
        } else {
            // Turn speakerphone OFF.
            mAudioManager.clearCommunicationDevice();
            maybeUpdateSelectedDevice();
        }
    }

    @Override
    public boolean[] getAvailableDevices_Locked() {
        List<AudioDeviceInfo> communicationDevices =
                mAudioManager.getAvailableCommunicationDevices();

        boolean[] availableDevices = new boolean[Devices.DEVICE_COUNT];

        for (AudioDeviceInfo device : communicationDevices) {
            switch (device.getType()) {
                case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
                    availableDevices[Devices.ID_SPEAKERPHONE] = true;
                    break;

                case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
                case AudioDeviceInfo.TYPE_WIRED_HEADSET:
                    availableDevices[Devices.ID_WIRED_HEADSET] = true;
                    break;

                case AudioDeviceInfo.TYPE_USB_DEVICE:
                case AudioDeviceInfo.TYPE_USB_HEADSET:
                    availableDevices[Devices.ID_USB_AUDIO] = true;
                    break;

                case AudioDeviceInfo.TYPE_BLE_HEADSET:
                case AudioDeviceInfo.TYPE_BLE_SPEAKER:
                case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
                case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
                    availableDevices[Devices.ID_BLUETOOTH_HEADSET] = true;
                    break;

                case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
                    availableDevices[Devices.ID_EARPIECE] = true;
                    break;
            }
        }

        return availableDevices;
    }

    public AudioDeviceInfo getMatchingCommunicationDevice(List<Integer> targetTypes) {
        // Despite supporting 2 BT devices being connected at once,
        // `getAvailableCommunicationDevices()` only seems to return the last connected BT device.
        // There should therefore never be a conflict between choosing between BT headsets.
        List<AudioDeviceInfo> availableDevices = mAudioManager.getAvailableCommunicationDevices();

        for (AudioDeviceInfo device : availableDevices) {
            if (targetTypes.contains(device.getType())) return device;
        }

        return null;
    }

    @Override
    protected void setAudioDevice(int deviceId) {
        if (!DeviceHelpers.isDeviceValid(deviceId)) return;

        AudioDeviceInfo targetDevice =
                getMatchingCommunicationDevice(getTargetTypesFromId(deviceId));

        if (targetDevice != null) {
            boolean result = mAudioManager.setCommunicationDevice(targetDevice);
            if (!result) {
                loge("Error setting communication device");
            }
        } else {
            loge("Couldn't find available device for: " + DeviceHelpers.getDeviceName(deviceId));
        }
    }
}