chromium/media/base/android/java/src/org/chromium/media/AudioDeviceSelector.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.media.AudioManager;

import org.chromium.base.Log;

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

abstract class AudioDeviceSelector {
    private static final String TAG = "media";

    protected static final boolean DEBUG = false;

    protected Devices mDeviceStates = new Devices();

    protected final AudioDeviceListener mDeviceListener;

    protected final AudioManager mAudioManager;

    protected AudioDeviceSelector(AudioManager audioManager) {
        mAudioManager = audioManager;

        mDeviceListener = new AudioDeviceListener(mDeviceStates);
    }

    /** Initialized the AudioDeviceSelector. */
    public abstract void init();

    /** Closes the AudioDeviceSelector. Must be called before destruction if init() was called. */
    public abstract void close();

    /**
     * Called when the AudioManager changes between MODE_IN_COMMUNICATION and MODE_NORMAL.
     *
     * @param on Whether we are entering MODE_IN_COMMUNICATION.
     */
    public abstract void setCommunicationAudioModeOn(boolean on);

    /** Gets whether the speakerphone is currently active. */
    public abstract boolean isSpeakerphoneOn();

    /**
     * Sets speakerphone on or off.
     *
     * @param on The desired speakerphone state.
     */
    public abstract void setSpeakerphoneOn(boolean on);

    /**
     * Changes selection of the currently active audio device.
     *
     * @param device Specifies the selected audio device.
     */
    protected abstract void setAudioDevice(int device);

    public abstract boolean[] getAvailableDevices_Locked();

    public void setDeviceExistence_Locked(int deviceId, boolean exists) {
        // Overridden by AudioDeviceSelectorPreS.
    }

    public AudioManagerAndroid.AudioDeviceName[] getAudioInputDeviceNames() {
        return mDeviceStates.getAudioInputDeviceNames();
    }

    /**
     * Sets the passed ID as the active device if it is available. Also sets the given
     * ID as the requested device ID, which will be prioritized when a device change
     * occurs and maybeUpdateSelectedDevice() is called.
     */
    public boolean selectDevice(String stringDeviceId) {
        int deviceId = DeviceHelpers.parseStringId(stringDeviceId);

        int nextDevice = mDeviceStates.setRequestedDeviceIdAndGetNextId(deviceId);

        if (DEBUG) logd("selectDevice: id=" + DeviceHelpers.getDeviceName(nextDevice));

        // `deviceId` is invalid, or its corresponding device is not available.
        if (nextDevice == Devices.ID_INVALID) return false;

        setAudioDevice(nextDevice);
        return true;
    }

    /**
     * Updates the active device given the current list of devices and
     * information about if a specific device has been selected or if
     * the default device is selected.
     */
    protected void maybeUpdateSelectedDevice() {
        int nextDevice = mDeviceStates.getNextDeviceIfRequested();

        if (DEBUG) logd("maybeUpdateSelectedDevice: id=" + DeviceHelpers.getDeviceName(nextDevice));

        // No device was explicitly requested.
        if (nextDevice == Devices.ID_INVALID) return;

        setAudioDevice(nextDevice);
    }

    // Collection of static helpers.
    protected static class DeviceHelpers {
        // Maps audio device types to string values. This map must be in sync
        // with the Devices.ID_* below.
        // TODO(henrika): add support for proper detection of device names and
        // localize the name strings by using resource strings.
        // See http://crbug.com/333208 for details.
        public static final String[] DEVICE_NAMES =
                new String[] {
                    "Speakerphone",
                    "Wired headset", // With or without microphone.
                    "Headset earpiece", // Only available on mobile phones.
                    "Bluetooth headset", // Requires BLUETOOTH permission.
                    "USB audio",
                };

        private static final int ID_VALID_LOWER_BOUND = Devices.ID_SPEAKERPHONE;
        private static final int ID_VALID_UPPER_BOUND = Devices.ID_USB_AUDIO;

        public static String getDeviceName(int deviceId) {
            if (deviceId == Devices.ID_INVALID) return "invalid-ID";

            if (deviceId == Devices.ID_DEFAULT) return "default-device";

            return DEVICE_NAMES[deviceId];
        }

        /**
         * Use a special selection scheme if the default device is selected.
         * The "most unique" device will be selected; Wired headset first, then USB
         * audio device, then Bluetooth and last the speaker phone.
         */
        public static int selectDefaultDevice(boolean[] devices) {
            if (devices[Devices.ID_WIRED_HEADSET]) {
                return Devices.ID_WIRED_HEADSET;
            }

            if (devices[Devices.ID_USB_AUDIO]) {
                return Devices.ID_USB_AUDIO;
            }

            if (devices[Devices.ID_BLUETOOTH_HEADSET]) {
                return Devices.ID_BLUETOOTH_HEADSET;
            }

            return Devices.ID_SPEAKERPHONE;
        }

        public static boolean isDeviceValid(int deviceId) {
            return deviceId >= ID_VALID_LOWER_BOUND && deviceId <= ID_VALID_UPPER_BOUND;
        }

        public static boolean isDeviceValidOrDefault(int deviceId) {
            return deviceId == Devices.ID_DEFAULT || isDeviceValid(deviceId);
        }

        public static int getActiveDeviceCount(boolean[] devices) {
            int count = 0;

            for (boolean device : devices) {
                if (device) ++count;
            }

            return count;
        }

        public static int parseStringId(String stringDeviceId) {
            return stringDeviceId.isEmpty() ? Devices.ID_DEFAULT : Integer.parseInt(stringDeviceId);
        }
    }

    public class Devices {
        // Supported audio device types.
        public static final int ID_DEFAULT = -2;
        public static final int ID_INVALID = -1;
        public static final int ID_SPEAKERPHONE = 0;
        public static final int ID_WIRED_HEADSET = 1;
        public static final int ID_EARPIECE = 2;
        public static final int ID_BLUETOOTH_HEADSET = 3;
        public static final int ID_USB_AUDIO = 4;
        public static final int DEVICE_COUNT = 5;

        private Object mLock = new Object();

        private int mRequestedAudioDevice = ID_INVALID;

        /**
         * Sets the whether a device exists.
         *
         * @param deviceId The ID of the device.
         * @param exists Whether or not the device exists.
         */
        public void setDeviceExistence(int deviceId, boolean exists) {
            if (!DeviceHelpers.isDeviceValid(deviceId)) return;

            if (DEBUG) logd("Setting [" + DeviceHelpers.getDeviceName(deviceId) + "]=" + exists);

            synchronized (mLock) {
                setDeviceExistence_Locked(deviceId, exists);
            }
        }

        /** Called when an available device maybe became invalid or vice versa. */
        public void onPotentialDeviceStatusChange() {
            maybeUpdateSelectedDevice();
        }

        /**
         * Sets the requested device, and gets the device ID that should be currently selected.
         *
         * @param deviceId The requested device ID (including the DEVICE_DEFAULT ID).
         * @return The ID of the audio device which should be selected, or DEVICE_INVALID if the
         *         requested ID is unavailable.
         */
        public int setRequestedDeviceIdAndGetNextId(int deviceId) {
            if (!DeviceHelpers.isDeviceValidOrDefault(deviceId)) return Devices.ID_INVALID;

            synchronized (mLock) {
                mRequestedAudioDevice = deviceId;
                boolean[] availableDevices = getAvailableDevices_Locked();

                // Handle the default device request.
                if (deviceId == Devices.ID_DEFAULT) {
                    return DeviceHelpers.selectDefaultDevice(availableDevices);
                }

                // A non-default device is specified. Verify that it is available before using it.
                return availableDevices[deviceId] ? mRequestedAudioDevice : ID_INVALID;
            }
        }

        /**
         * Gets the ID of the device which should be currently selected, or ID_INVALID if no
         * device was ever requested.
         */
        public int getNextDeviceIfRequested() {
            synchronized (mLock) {
                if (mRequestedAudioDevice == ID_INVALID) return ID_INVALID;

                boolean[] availableDevices = getAvailableDevices_Locked();

                if (mRequestedAudioDevice == ID_DEFAULT
                        || !availableDevices[mRequestedAudioDevice]) {
                    return DeviceHelpers.selectDefaultDevice(availableDevices);
                }

                return mRequestedAudioDevice;
            }
        }

        /** Returns the list of currently available devices, to be used by the native side. */
        public AudioManagerAndroid.AudioDeviceName[] getAudioInputDeviceNames() {
            boolean devices[] = null;
            synchronized (mLock) {
                devices = getAvailableDevices_Locked();
            }
            List<String> list = new ArrayList<String>();

            int activeDeviceCount = DeviceHelpers.getActiveDeviceCount(devices);

            AudioManagerAndroid.AudioDeviceName[] array =
                    new AudioManagerAndroid.AudioDeviceName[activeDeviceCount];

            int i = 0;
            for (int id = 0; id < devices.length; ++id) {
                if (devices[id]) {
                    array[i] =
                            new AudioManagerAndroid.AudioDeviceName(
                                    id, DeviceHelpers.DEVICE_NAMES[id]);
                    list.add(DeviceHelpers.DEVICE_NAMES[id]);
                    i++;
                }
            }
            if (DEBUG) logd("getAudioInputDeviceNames: " + list);
            return array;
        }

        public void clearRequestedDevice() {
            if (DEBUG) logd("Clearing requested device");
            synchronized (mLock) {
                mRequestedAudioDevice = ID_INVALID;
            }
        }
    }

    /** Trivial helper method for debug logging */
    protected static void logd(String msg) {
        Log.d(TAG, msg);
    }

    /** Trivial helper method for error logging */
    protected static void loge(String msg) {
        Log.e(TAG, msg);
    }
}