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

Commit de135fd4 authored by Yuyang Huang's avatar Yuyang Huang
Browse files

Enhance CLCC inference

sort CLCC response by index
add null check
clear inference when conference call children arrive
clear inference when parent call ends

Bug: 262199042
Test: atest com.android.bluetooth.telephony.BluetoothInCallServiceTest
Change-Id: I35d2f8569af1deb13b830a1f23ec540679435233
parent 0e79b458
Loading
Loading
Loading
Loading
+84 −28
Original line number Diff line number Diff line
@@ -62,6 +62,8 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -75,7 +77,7 @@ import java.util.concurrent.Executors;
public class BluetoothInCallService extends InCallService {

    private static final String TAG = "BluetoothInCallService";
    private static final String CLCC_INFERENCE = "ConferenceCallInference";
    @VisibleForTesting static final String CLCC_INFERENCE = "ConferenceCallInference";

    // match up with bthf_call_state_t of bt_hf.h
    private static final int CALL_STATE_ACTIVE = 0;
@@ -164,11 +166,13 @@ public class BluetoothInCallService extends InCallService {
                public void onServiceConnected(int profile, BluetoothProfile proxy) {
                    synchronized (LOCK) {
                        if (profile == BluetoothProfile.HEADSET) {
                            setBluetoothHeadset(new BluetoothHeadsetProxy((BluetoothHeadset) proxy));
                            setBluetoothHeadset(
                                    new BluetoothHeadsetProxy((BluetoothHeadset) proxy));
                            updateHeadsetWithCallState(true /* force */);
                        } else {
                            setBluetoothLeCallControl(new BluetoothLeCallControlProxy((
                                    BluetoothLeCallControl) proxy));
                            setBluetoothLeCallControl(
                                    new BluetoothLeCallControlProxy(
                                            (BluetoothLeCallControl) proxy));
                            sendTbsCurrentCallsList();
                        }
                    }
@@ -639,9 +643,16 @@ public class BluetoothInCallService extends InCallService {
        if (mBluetoothCallHashMap.containsKey(call.getId())) {
            mBluetoothCallHashMap.remove(call.getId());

            DisconnectCause cause = call.getDisconnectCause();
            if (cause != null && cause.getCode() == DisconnectCause.OTHER) {
                Log.d(TAG, "add inference call with reason: " + cause.getReason());
                mBluetoothCallQueue.add(call.getId());
                mBluetoothConferenceCallInference.put(call.getId(), call);
            mClccInferenceIndexMap.put(getClccMapKey(call), mClccIndexMap.get(getClccMapKey(call)));
                Integer indexValue = mClccIndexMap.get(getClccMapKey(call));
                mClccInferenceIndexMap.put(getClccMapKey(call), indexValue);
                if (indexValue == null) {
                    Log.w(TAG, "CLCC index value is null");
                }
                // queue size limited to 2 because merge operation only happens on 2 calls
                // we are only interested in last 2 calls merged
                if (mBluetoothCallQueue.size() > 2) {
@@ -650,6 +661,14 @@ public class BluetoothInCallService extends InCallService {
                    mBluetoothConferenceCallInference.remove(callId);
                }
            }
            // As there is at most 1 conference call, so clear inference when parent call ends
            if (call.isConference()) {
                Log.d(TAG, "conference call ends, clear inference");
                mBluetoothConferenceCallInference.clear();
                mClccInferenceIndexMap.clear();
                mBluetoothCallQueue.clear();
            }
        }

        mClccIndexMap.remove(getClccMapKey(call));
        updateHeadsetWithCallState(false /* force */);
@@ -745,18 +764,34 @@ public class BluetoothInCallService extends InCallService {
        Log.d(TAG, "is conference call inference enabled: " + isInferenceEnabled);
        for (BluetoothCall call : calls) {
            if (isInferenceEnabled && call.isConference()
                    && call.getChildrenIds().size() < 2
                    && !mBluetoothConferenceCallInference.isEmpty()) {
                Log.d(TAG, "conference call inferred size: "
                SortedMap<Integer, Object[]> clccResponseMap = new TreeMap<>();
                Log.d(
                        TAG,
                        "conference call inferred size: "
                                + mBluetoothConferenceCallInference.size()
                        + "current size: " + mBluetoothCallHashMap.size());
                                + " current size: "
                                + mBluetoothCallHashMap.size());
                // Do conference call inference until at least 2 children arrive
                // If carrier does send children info, then inference will end when info arrives.
                // If carrier does not send children info, then inference won't impact actual value.
                if (call.getChildrenIds().size() >= 2) {
                    mBluetoothConferenceCallInference.clear();
                    break;
                }
                for (BluetoothCall inferredCall : mBluetoothConferenceCallInference.values()) {
                    int index = mClccInferenceIndexMap.get(getClccMapKey(inferredCall));
                    String clccMapKey = getClccMapKey(inferredCall);
                    if (!mClccInferenceIndexMap.containsKey(clccMapKey)) {
                        Log.w(TAG, "Inference Index Map does not have: " + clccMapKey);
                        continue;
                    }
                    if (mClccInferenceIndexMap.get(clccMapKey) == null) {
                        Log.w(TAG, "inferred index is null");
                        continue;
                    }
                    int index = mClccInferenceIndexMap.get(clccMapKey);
                    // save the index so later on when real children arrive, index is the same
                    mClccIndexMap.put(getClccMapKey(inferredCall), index);
                    mClccIndexMap.put(clccMapKey, index);
                    int direction = inferredCall.isIncoming() ? 1 : 0;
                    int state = CALL_STATE_ACTIVE;
                    boolean isPartOfConference = true;
@@ -770,17 +805,38 @@ public class BluetoothInCallService extends InCallService {
                    if (address != null) {
                        address = PhoneNumberUtils.stripSeparators(address);
                    }

                    int addressType =
                            address == null ? -1 : PhoneNumberUtils.toaFromString(address);
                    Log.i(TAG, "sending inferred clcc for BluetoothCall "
                            + index + ", "
                            + direction + ", "
                            + state + ", "
                            + isPartOfConference + ", "
                            + addressType);
                    clccResponseMap.put(
                            index,
                            new Object[] {
                                index, direction, state, 0, isPartOfConference, address, addressType
                            });
                }
                // ensure response is sorted by index
                for (Object[] response : clccResponseMap.values()) {
                    if (response.length < 7) {
                        Log.e(TAG, "clccResponseMap entry too short");
                        continue;
                    }
                    Log.i(
                            TAG,
                            String.format(
                                    "sending inferred clcc for BluetoothCall: index %d, direction"
                                        + " %d, state %d, isPartOfConference %b, addressType %d",
                                    (int) response[0],
                                    (int) response[1],
                                    (int) response[2],
                                    (boolean) response[4],
                                    (int) response[6]));
                    mBluetoothHeadset.clccResponse(
                            index, direction, state, 0, isPartOfConference, address, addressType);
                            (int) response[0],
                            (int) response[1],
                            (int) response[2],
                            (int) response[3],
                            (boolean) response[4],
                            (String) response[5],
                            (int) response[6]);
                }
                sendClccEndMarker();
                return;
+140 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.bluetooth.telephony;

import static com.android.bluetooth.telephony.BluetoothInCallService.CLCC_INFERENCE;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@@ -31,6 +32,7 @@ import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.DeviceConfig;
import android.telecom.BluetoothCallQualityReport;
import android.telecom.Call;
import android.telecom.Connection;
@@ -766,6 +768,144 @@ public class BluetoothInCallServiceTest {
                eq(1), eq(1), eq(0), eq(0), eq(true), eq("5551234"), eq(129));
    }

    @Test
    public void testListCurrentCallsConferenceEmptyChildrenInference() throws Exception {
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_BLUETOOTH, CLCC_INFERENCE, "true", false);

        ArrayList<BluetoothCall> calls = new ArrayList<>();
        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);

        // active call is added
        BluetoothCall activeCall = createActiveCall(UUID.randomUUID());
        calls.add(activeCall);
        mBluetoothInCallService.onCallAdded(activeCall);

        when(activeCall.getState()).thenReturn(Call.STATE_ACTIVE);
        when(activeCall.isIncoming()).thenReturn(false);
        when(activeCall.isConference()).thenReturn(false);
        when(activeCall.getHandle()).thenReturn(Uri.parse("tel:555-0001"));
        when(activeCall.getGatewayInfo())
                .thenReturn(new GatewayInfo(null, null, Uri.parse("tel:555-0001")));

        // holding call is added
        BluetoothCall holdingCall = createHeldCall(UUID.randomUUID());
        calls.add(holdingCall);
        mBluetoothInCallService.onCallAdded(holdingCall);

        when(holdingCall.getState()).thenReturn(Call.STATE_HOLDING);
        when(holdingCall.isIncoming()).thenReturn(true);
        when(holdingCall.isConference()).thenReturn(false);
        when(holdingCall.getHandle()).thenReturn(Uri.parse("tel:555-0002"));
        when(holdingCall.getGatewayInfo())
                .thenReturn(new GatewayInfo(null, null, Uri.parse("tel:555-0002")));

        // needs to have at least one CLCC response before merge to enable call inference
        clearInvocations(mMockBluetoothHeadset);
        mBluetoothInCallService.listCurrentCalls();
        verify(mMockBluetoothHeadset)
                .clccResponse(
                        1, 0, CALL_STATE_ACTIVE, 0, false, "5550001", PhoneNumberUtils.TOA_Unknown);
        verify(mMockBluetoothHeadset)
                .clccResponse(
                        2, 1, CALL_STATE_HELD, 0, false, "5550002", PhoneNumberUtils.TOA_Unknown);
        calls.clear();

        // calls merged for conference call
        DisconnectCause cause = new DisconnectCause(DisconnectCause.OTHER);
        when(activeCall.getDisconnectCause()).thenReturn(cause);
        when(holdingCall.getDisconnectCause()).thenReturn(cause);
        mBluetoothInCallService.onCallRemoved(activeCall, true);
        mBluetoothInCallService.onCallRemoved(holdingCall, true);

        BluetoothCall conferenceCall = createActiveCall(UUID.randomUUID());
        addCallCapability(conferenceCall, Connection.CAPABILITY_MANAGE_CONFERENCE);

        when(conferenceCall.getHandle()).thenReturn(Uri.parse("tel:555-1234"));
        when(conferenceCall.isConference()).thenReturn(true);
        when(conferenceCall.getState()).thenReturn(Call.STATE_ACTIVE);
        when(conferenceCall.hasProperty(Call.Details.PROPERTY_GENERIC_CONFERENCE)).thenReturn(true);
        when(conferenceCall.can(Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN))
                .thenReturn(false);
        when(conferenceCall.isIncoming()).thenReturn(true);
        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);

        // parent call arrived, but children have not, then do inference on children
        calls.add(conferenceCall);
        Assert.assertEquals(calls.size(), 1);
        mBluetoothInCallService.onCallAdded(conferenceCall);

        clearInvocations(mMockBluetoothHeadset);
        mBluetoothInCallService.listCurrentCalls();
        verify(mMockBluetoothHeadset)
                .clccResponse(
                        1, 0, CALL_STATE_ACTIVE, 0, true, "5550001", PhoneNumberUtils.TOA_Unknown);
        verify(mMockBluetoothHeadset)
                .clccResponse(
                        2, 1, CALL_STATE_ACTIVE, 0, true, "5550002", PhoneNumberUtils.TOA_Unknown);

        // real children arrive, no change on CLCC response
        calls.add(activeCall);
        mBluetoothInCallService.onCallAdded(activeCall);
        when(activeCall.isConference()).thenReturn(true);
        calls.add(holdingCall);
        mBluetoothInCallService.onCallAdded(holdingCall);
        when(holdingCall.getState()).thenReturn(Call.STATE_ACTIVE);
        when(holdingCall.isConference()).thenReturn(true);
        when(conferenceCall.getChildrenIds()).thenReturn(new ArrayList<>(Arrays.asList(1, 2)));

        clearInvocations(mMockBluetoothHeadset);
        mBluetoothInCallService.listCurrentCalls();
        verify(mMockBluetoothHeadset)
                .clccResponse(
                        1, 0, CALL_STATE_ACTIVE, 0, true, "5550001", PhoneNumberUtils.TOA_Unknown);
        verify(mMockBluetoothHeadset)
                .clccResponse(
                        2, 1, CALL_STATE_ACTIVE, 0, true, "5550002", PhoneNumberUtils.TOA_Unknown);

        // when call is terminated, children first removed, then parent
        cause = new DisconnectCause(DisconnectCause.LOCAL);
        when(activeCall.getDisconnectCause()).thenReturn(cause);
        when(holdingCall.getDisconnectCause()).thenReturn(cause);
        mBluetoothInCallService.onCallRemoved(activeCall, true);
        mBluetoothInCallService.onCallRemoved(holdingCall, true);
        calls.remove(activeCall);
        calls.remove(holdingCall);
        Assert.assertEquals(calls.size(), 1);

        clearInvocations(mMockBluetoothHeadset);
        mBluetoothInCallService.listCurrentCalls();
        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
        verify(mMockBluetoothHeadset, times(1))
                .clccResponse(
                        anyInt(),
                        anyInt(),
                        anyInt(),
                        anyInt(),
                        anyBoolean(),
                        nullable(String.class),
                        anyInt());

        // when parent is removed
        when(conferenceCall.getDisconnectCause()).thenReturn(cause);
        calls.remove(conferenceCall);
        mBluetoothInCallService.onCallRemoved(conferenceCall, true);

        clearInvocations(mMockBluetoothHeadset);
        mBluetoothInCallService.listCurrentCalls();
        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
        verify(mMockBluetoothHeadset, times(1))
                .clccResponse(
                        anyInt(),
                        anyInt(),
                        anyInt(),
                        anyInt(),
                        anyBoolean(),
                        nullable(String.class),
                        anyInt());

        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_BLUETOOTH, CLCC_INFERENCE, "false", false);
    }

    @Test
    public void testQueryPhoneState() throws Exception {
        BluetoothCall ringingCall = createRingingCall(UUID.randomUUID());