Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4c7e5b6f authored by Jason Hsu's avatar Jason Hsu Committed by Android (Google) Code Review
Browse files

Merge "Fix audio routing can not work after the phone get rebooted" into udc-dev

parents 6e2dd42d 05928ef3
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -51,7 +51,8 @@ public class CachedBluetoothDeviceManager {
    public CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) {
        mContext = context;
        mBtManager = localBtManager;
        mHearingAidDeviceManager = new HearingAidDeviceManager(localBtManager, mCachedDevices);
        mHearingAidDeviceManager = new HearingAidDeviceManager(context, localBtManager,
                mCachedDevices);
        mCsipDeviceManager = new CsipDeviceManager(localBtManager, mCachedDevices);
    }

+74 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;

import androidx.annotation.IntDef;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Constant values used to configure hearing aid audio routing.
 *
 * {@link HearingAidAudioRoutingHelper}
 */
public final class HearingAidAudioRoutingConstants {
    public static final int[] CALL_ROUTING_ATTRIBUTES = new int[] {
            // Stands for STRATEGY_PHONE
            AudioAttributes.USAGE_VOICE_COMMUNICATION,
    };

    public static final int[] MEDIA_ROUTING_ATTRIBUTES = new int[] {
            // Stands for STRATEGY_MEDIA, including USAGE_GAME, USAGE_ASSISTANT,
            // USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, USAGE_ASSISTANCE_SONIFICATION
            AudioAttributes.USAGE_MEDIA
    };

    public static final int[] RINGTONE_ROUTING_ATTRIBUTE = new int[] {
            // Stands for STRATEGY_SONIFICATION, including USAGE_ALARM
            AudioAttributes.USAGE_NOTIFICATION_RINGTONE
    };

    public static final int[] SYSTEM_SOUNDS_ROUTING_ATTRIBUTES = new int[] {
            // Stands for STRATEGY_SONIFICATION_RESPECTFUL, including USAGE_NOTIFICATION_EVENT
            AudioAttributes.USAGE_NOTIFICATION,
            // Stands for STRATEGY_ACCESSIBILITY
            AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY,
            // Stands for STRATEGY_DTMF
            AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING,
    };

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
            RoutingValue.AUTO,
            RoutingValue.HEARING_DEVICE,
            RoutingValue.DEVICE_SPEAKER,
    })

    public @interface RoutingValue {
        int AUTO = 0;
        int HEARING_DEVICE = 1;
        int DEVICE_SPEAKER = 2;
    }

    public static final AudioDeviceAttributes DEVICE_SPEAKER_OUT = new AudioDeviceAttributes(
            AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, "");
}
+164 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.content.Context;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.audiopolicy.AudioProductStrategy;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * A helper class to configure the routing strategy for hearing aids.
 */
public class HearingAidAudioRoutingHelper {

    private final AudioManager mAudioManager;

    public HearingAidAudioRoutingHelper(Context context) {
        mAudioManager = context.getSystemService(AudioManager.class);
    }

    /**
     * Gets the list of {@link AudioProductStrategy} referred by the given list of usage values
     * defined in {@link AudioAttributes}
     */
    public List<AudioProductStrategy> getSupportedStrategies(int[] attributeSdkUsageList) {
        final List<AudioAttributes> audioAttrList = new ArrayList<>(attributeSdkUsageList.length);
        for (int attributeSdkUsage : attributeSdkUsageList) {
            audioAttrList.add(new AudioAttributes.Builder().setUsage(attributeSdkUsage).build());
        }

        final List<AudioProductStrategy> allStrategies = getAudioProductStrategies();
        final List<AudioProductStrategy> supportedStrategies = new ArrayList<>();
        for (AudioProductStrategy strategy : allStrategies) {
            for (AudioAttributes audioAttr : audioAttrList) {
                if (strategy.supportsAudioAttributes(audioAttr)) {
                    supportedStrategies.add(strategy);
                }
            }
        }

        return supportedStrategies.stream().distinct().collect(Collectors.toList());
    }

    /**
     * Sets the preferred device for the given strategies.
     *
     * @param supportedStrategies A list of {@link AudioProductStrategy} used to configure audio
     *                            routing
     * @param hearingDevice {@link AudioDeviceAttributes} of the device to be changed in audio
     *                      routing
     * @param routingValue one of value defined in
     *                     {@link HearingAidAudioRoutingConstants.RoutingValue}, denotes routing
     *                     destination.
     * @return {code true} if the routing value successfully configure
     */
    public boolean setPreferredDeviceRoutingStrategies(
            List<AudioProductStrategy> supportedStrategies, AudioDeviceAttributes hearingDevice,
            @HearingAidAudioRoutingConstants.RoutingValue int routingValue) {
        boolean status;
        switch (routingValue) {
            case HearingAidAudioRoutingConstants.RoutingValue.AUTO:
                status = removePreferredDeviceForStrategies(supportedStrategies);
                return status;
            case HearingAidAudioRoutingConstants.RoutingValue.HEARING_DEVICE:
                status = removePreferredDeviceForStrategies(supportedStrategies);
                status &= setPreferredDeviceForStrategies(supportedStrategies, hearingDevice);
                return status;
            case HearingAidAudioRoutingConstants.RoutingValue.DEVICE_SPEAKER:
                status = removePreferredDeviceForStrategies(supportedStrategies);
                status &= setPreferredDeviceForStrategies(supportedStrategies,
                        HearingAidAudioRoutingConstants.DEVICE_SPEAKER_OUT);
                return status;
            default:
                throw new IllegalArgumentException("Unexpected routingValue: " + routingValue);
        }
    }

    /**
     * Gets the matched hearing device {@link AudioDeviceAttributes} for {@code device}.
     *
     * <p>Will also try to match the {@link CachedBluetoothDevice#getSubDevice()} of {@code device}
     *
     * @param device the {@link CachedBluetoothDevice} need to be hearing aid device
     * @return the requested AudioDeviceAttributes or {@code null} if not match
     */
    @Nullable
    public AudioDeviceAttributes getMatchedHearingDeviceAttributes(CachedBluetoothDevice device) {
        if (device == null || !device.isHearingAidDevice()) {
            return null;
        }

        AudioDeviceInfo[] audioDevices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
        for (AudioDeviceInfo audioDevice : audioDevices) {
            // ASHA for TYPE_HEARING_AID, HAP for TYPE_BLE_HEADSET
            if (audioDevice.getType() == AudioDeviceInfo.TYPE_HEARING_AID
                    || audioDevice.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) {
                if (matchAddress(device, audioDevice)) {
                    return new AudioDeviceAttributes(audioDevice);
                }
            }
        }
        return null;
    }

    private boolean matchAddress(CachedBluetoothDevice device, AudioDeviceInfo audioDevice) {
        final String audioDeviceAddress = audioDevice.getAddress();
        final CachedBluetoothDevice subDevice = device.getSubDevice();
        final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();

        return device.getAddress().equals(audioDeviceAddress)
                || (subDevice != null && subDevice.getAddress().equals(audioDeviceAddress))
                || (!memberDevices.isEmpty() && memberDevices.stream().anyMatch(
                    m -> m.getAddress().equals(audioDeviceAddress)));
    }

    private boolean setPreferredDeviceForStrategies(List<AudioProductStrategy> strategies,
            AudioDeviceAttributes audioDevice) {
        boolean status = true;
        for (AudioProductStrategy strategy : strategies) {
            status &= mAudioManager.setPreferredDeviceForStrategy(strategy, audioDevice);

        }

        return status;
    }

    private boolean removePreferredDeviceForStrategies(List<AudioProductStrategy> strategies) {
        boolean status = true;
        for (AudioProductStrategy strategy : strategies) {
            status &= mAudioManager.removePreferredDeviceForStrategy(strategy);
        }

        return status;
    }

    @VisibleForTesting
    public List<AudioProductStrategy> getAudioProductStrategies() {
        return AudioManager.getAudioProductStrategies();
    }
}
+97 −4
Original line number Diff line number Diff line
@@ -18,6 +18,11 @@ package com.android.settingslib.bluetooth;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
import android.content.ContentResolver;
import android.content.Context;
import android.media.AudioDeviceAttributes;
import android.media.audiopolicy.AudioProductStrategy;
import android.provider.Settings;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
@@ -33,12 +38,25 @@ public class HearingAidDeviceManager {
    private static final String TAG = "HearingAidDeviceManager";
    private static final boolean DEBUG = BluetoothUtils.D;

    private final ContentResolver mContentResolver;
    private final LocalBluetoothManager mBtManager;
    private final List<CachedBluetoothDevice> mCachedDevices;
    HearingAidDeviceManager(LocalBluetoothManager localBtManager,
    private final HearingAidAudioRoutingHelper mRoutingHelper;
    HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager,
            List<CachedBluetoothDevice> CachedDevices) {
        mContentResolver = context.getContentResolver();
        mBtManager = localBtManager;
        mCachedDevices = CachedDevices;
        mRoutingHelper = new HearingAidAudioRoutingHelper(context);
    }

    @VisibleForTesting
    HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager,
            List<CachedBluetoothDevice> cachedDevices, HearingAidAudioRoutingHelper routingHelper) {
        mContentResolver = context.getContentResolver();
        mBtManager = localBtManager;
        mCachedDevices = cachedDevices;
        mRoutingHelper = routingHelper;
    }

    void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice) {
@@ -197,7 +215,6 @@ public class HearingAidDeviceManager {
                        // When main device exists and in connected state, receiving sub device
                        // connection. To refresh main device UI
                        mainDevice.refresh();
                        return true;
                    } else {
                        // When both Hearing Aid devices are disconnected, receiving sub device
                        // connection. To switch content and dispatch to notify UI change
@@ -207,9 +224,15 @@ public class HearingAidDeviceManager {
                        // It is necessary to do remove and add for updating the mapping on
                        // preference and device
                        mBtManager.getEventManager().dispatchDeviceAdded(mainDevice);
                        return true;
                        // Only need to set first device of a set. AudioDeviceInfo for
                        // GET_DEVICES_OUTPUTS will not change device.
                        setAudioRoutingConfig(cachedDevice);
                    }
                    return true;
                }
                // Only need to set first device of a set. AudioDeviceInfo for GET_DEVICES_OUTPUTS
                // will not change device.
                setAudioRoutingConfig(cachedDevice);
                break;
            case BluetoothProfile.STATE_DISCONNECTED:
                mainDevice = findMainDevice(cachedDevice);
@@ -232,13 +255,83 @@ public class HearingAidDeviceManager {
                    // It is necessary to do remove and add for updating the mapping on
                    // preference and device
                    mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice);

                    return true;
                }
                // Only need to clear when last device of a set get disconnected
                clearAudioRoutingConfig();
                break;
        }
        return false;
    }

    private void setAudioRoutingConfig(CachedBluetoothDevice device) {
        AudioDeviceAttributes hearingDeviceAttributes =
                mRoutingHelper.getMatchedHearingDeviceAttributes(device);
        if (hearingDeviceAttributes == null) {
            Log.w(TAG, "Can not find expected AudioDeviceAttributes for hearing device: "
                    + device.getDevice().getAnonymizedAddress());
            return;
        }

        final int callRoutingValue = Settings.Secure.getInt(mContentResolver,
                Settings.Secure.HEARING_AID_CALL_ROUTING,
                HearingAidAudioRoutingConstants.RoutingValue.AUTO);
        final int mediaRoutingValue = Settings.Secure.getInt(mContentResolver,
                Settings.Secure.HEARING_AID_MEDIA_ROUTING,
                HearingAidAudioRoutingConstants.RoutingValue.AUTO);
        final int ringtoneRoutingValue = Settings.Secure.getInt(mContentResolver,
                Settings.Secure.HEARING_AID_RINGTONE_ROUTING,
                HearingAidAudioRoutingConstants.RoutingValue.AUTO);
        final int systemSoundsRoutingValue = Settings.Secure.getInt(mContentResolver,
                Settings.Secure.HEARING_AID_SYSTEM_SOUNDS_ROUTING,
                HearingAidAudioRoutingConstants.RoutingValue.AUTO);

        setPreferredDeviceRoutingStrategies(
                HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES,
                hearingDeviceAttributes, callRoutingValue);
        setPreferredDeviceRoutingStrategies(
                HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES,
                hearingDeviceAttributes, mediaRoutingValue);
        setPreferredDeviceRoutingStrategies(
                HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTE,
                hearingDeviceAttributes, ringtoneRoutingValue);
        setPreferredDeviceRoutingStrategies(
                HearingAidAudioRoutingConstants.SYSTEM_SOUNDS_ROUTING_ATTRIBUTES,
                hearingDeviceAttributes, systemSoundsRoutingValue);
    }

    private void clearAudioRoutingConfig() {
        // Don't need to pass hearingDevice when we want to reset it (set to AUTO).
        setPreferredDeviceRoutingStrategies(
                HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES,
                /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
        setPreferredDeviceRoutingStrategies(
                HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES,
                /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
        setPreferredDeviceRoutingStrategies(
                HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTE,
                /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
        setPreferredDeviceRoutingStrategies(
                HearingAidAudioRoutingConstants.SYSTEM_SOUNDS_ROUTING_ATTRIBUTES,
                /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
    }

    private void setPreferredDeviceRoutingStrategies(int[] attributeSdkUsageList,
            AudioDeviceAttributes hearingDevice,
            @HearingAidAudioRoutingConstants.RoutingValue int routingValue) {
        final List<AudioProductStrategy> supportedStrategies =
                mRoutingHelper.getSupportedStrategies(attributeSdkUsageList);

        final boolean status = mRoutingHelper.setPreferredDeviceRoutingStrategies(
                supportedStrategies, hearingDevice, routingValue);

        if (!status) {
            Log.w(TAG, "routingStrategies: " + supportedStrategies.toString() + "routingValue: "
                    + routingValue + " fail to configure AudioProductStrategy");
        }
    }

    CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
        for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
            if (isValidHiSyncId(cachedDevice.getHiSyncId())) {
+1 −0
Original line number Diff line number Diff line
@@ -361,6 +361,7 @@ public class LocalBluetoothProfileManager {
                        cachedDevice.setHearingAidInfo(infoBuilder.build());
                    }
                }

                HearingAidStatsLogUtils.logHearingAidInfo(cachedDevice);
            }

Loading