From b2afdb9462c4d5a7ff0ac7c039b81e70edabe2e1 Mon Sep 17 00:00:00 2001 From: Yuyang Huang <yuyangh@google.com> Date: Tue, 16 May 2023 14:46:15 -0700 Subject: [PATCH] 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 (cherry picked from https://android-review.googlesource.com/q/commit:de135fd4c6a1ed974314a48f5346bfb7750b81c4) Merged-In: I35d2f8569af1deb13b830a1f23ec540679435233 Change-Id: I35d2f8569af1deb13b830a1f23ec540679435233 --- .../telephony/BluetoothInCallService.java | 112 ++++++++++---- .../telephony/BluetoothInCallServiceTest.java | 140 ++++++++++++++++++ 2 files changed, 224 insertions(+), 28 deletions(-) diff --git a/android/app/src/com/android/bluetooth/telephony/BluetoothInCallService.java b/android/app/src/com/android/bluetooth/telephony/BluetoothInCallService.java index a7fa694fd43..4ebc38dccb6 100644 --- a/android/app/src/com/android/bluetooth/telephony/BluetoothInCallService.java +++ b/android/app/src/com/android/bluetooth/telephony/BluetoothInCallService.java @@ -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; @@ -154,7 +156,7 @@ public class BluetoothInCallService extends InCallService { private int mMaxNumberOfCalls = 0; /** - * Listens to connections and disconnections of bluetooth headsets. We need to save the current + * Listens to connections and disconnections of bluetooth headsets. We need to save the current * bluetooth headset so that we know where to send BluetoothCall updates. */ @VisibleForTesting @@ -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,15 +643,30 @@ public class BluetoothInCallService extends InCallService { if (mBluetoothCallHashMap.containsKey(call.getId())) { mBluetoothCallHashMap.remove(call.getId()); - mBluetoothCallQueue.add(call.getId()); - mBluetoothConferenceCallInference.put(call.getId(), call); - mClccInferenceIndexMap.put(getClccMapKey(call), mClccIndexMap.get(getClccMapKey(call))); - // 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) { - Integer callId = mBluetoothCallQueue.peek(); - mBluetoothCallQueue.remove(); - mBluetoothConferenceCallInference.remove(callId); + 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); + 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) { + Integer callId = mBluetoothCallQueue.peek(); + mBluetoothCallQueue.remove(); + 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(); } } @@ -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: " - + mBluetoothConferenceCallInference.size() - + "current size: " + mBluetoothCallHashMap.size()); + SortedMap<Integer, Object[]> clccResponseMap = new TreeMap<>(); + Log.d( + TAG, + "conference call inferred size: " + + mBluetoothConferenceCallInference.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; diff --git a/android/app/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java index 0b503f1659b..ea787aa7330 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java @@ -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()); -- GitLab