Loading android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java +19 −6 Original line number Diff line number Diff line Loading @@ -68,6 +68,7 @@ import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.flags.Flags; import com.android.bluetooth.hfp.HeadsetService; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IState; Loading Loading @@ -134,8 +135,8 @@ public class HeadsetClientStateMachine extends StateMachine { static final int CONNECTING_TIMEOUT_MS = 10000; // 10s private static final int ROUTING_DELAY_MS = 250; private static final int MAX_HFP_SCO_VOICE_CALL_VOLUME = 15; // HFP 1.5 spec. private static final int MIN_HFP_SCO_VOICE_CALL_VOLUME = 1; // HFP 1.5 spec. @VisibleForTesting static final int MAX_HFP_SCO_VOICE_CALL_VOLUME = 15; // HFP 1.5 spec. @VisibleForTesting static final int MIN_HFP_SCO_VOICE_CALL_VOLUME = 1; // HFP 1.5 spec. static final int HF_ORIGINATED_CALL_ID = -1; private static final long OUTGOING_TIMEOUT_MILLI = 10 * 1000; // 10 seconds Loading Loading @@ -995,8 +996,14 @@ public class HeadsetClientStateMachine extends StateMachine { static int hfToAmVol(int hfVol) { int amRange = sMaxAmVcVol - sMinAmVcVol; int hfRange = MAX_HFP_SCO_VOICE_CALL_VOLUME - MIN_HFP_SCO_VOICE_CALL_VOLUME; int amVol = 0; if (Flags.headsetClientAmHfVolumeSymmetric()) { amVol = (int) Math.round((hfVol - MIN_HFP_SCO_VOICE_CALL_VOLUME) * ((double) amRange / hfRange)) + sMinAmVcVol; } else { int amOffset = (amRange * (hfVol - MIN_HFP_SCO_VOICE_CALL_VOLUME)) / hfRange; int amVol = sMinAmVcVol + amOffset; amVol = sMinAmVcVol + amOffset; } Log.d(TAG, "HF -> AM " + hfVol + " " + amVol); return amVol; } Loading @@ -1004,8 +1011,14 @@ public class HeadsetClientStateMachine extends StateMachine { static int amToHfVol(int amVol) { int amRange = (sMaxAmVcVol > sMinAmVcVol) ? (sMaxAmVcVol - sMinAmVcVol) : 1; int hfRange = MAX_HFP_SCO_VOICE_CALL_VOLUME - MIN_HFP_SCO_VOICE_CALL_VOLUME; int hfVol = 0; if (Flags.headsetClientAmHfVolumeSymmetric()) { hfVol = (int) Math.round((amVol - sMinAmVcVol) * ((double) hfRange / amRange)) + MIN_HFP_SCO_VOICE_CALL_VOLUME; } else { int hfOffset = (hfRange * (amVol - sMinAmVcVol)) / amRange; int hfVol = MIN_HFP_SCO_VOICE_CALL_VOLUME + hfOffset; hfVol = MIN_HFP_SCO_VOICE_CALL_VOLUME + hfOffset; } Log.d(TAG, "AM -> HF " + amVol + " " + hfVol); return hfVol; } Loading android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java +98 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,8 @@ import static android.content.pm.PackageManager.FEATURE_WATCH; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.AT_OK; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.ENTER_PRIVATE_MODE; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.EXPLICIT_CALL_TRANSFER; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.MAX_HFP_SCO_VOICE_CALL_VOLUME; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.MIN_HFP_SCO_VOICE_CALL_VOLUME; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.VOICE_RECOGNITION_START; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.VOICE_RECOGNITION_STOP; Loading @@ -47,6 +49,8 @@ import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; import androidx.test.InstrumentationRegistry; Loading @@ -61,6 +65,7 @@ import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.RemoteDevices; import com.android.bluetooth.flags.Flags; import com.android.bluetooth.hfp.HeadsetService; import org.hamcrest.core.AllOf; Loading @@ -78,7 +83,9 @@ import org.mockito.hamcrest.MockitoHamcrest; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** Loading @@ -95,6 +102,8 @@ public class HeadsetClientStateMachineTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private AdapterService mAdapterService; @Mock private Resources mMockHfpResources; @Mock private HeadsetService mHeadsetService; Loading Loading @@ -157,6 +166,95 @@ public class HeadsetClientStateMachineTest { verifyNoMoreInteractions(mHeadsetService); } private void quitHeadsetClientStateMachine() { if (mHeadsetClientStateMachine != null) { TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper()); mHeadsetClientStateMachine.allowConnect = null; mHeadsetClientStateMachine.doQuit(); TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper()); mHeadsetClientStateMachine = null; } } private void startHeadsetClientStateMachine() { if (mHeadsetClientStateMachine == null) { mHeadsetClientStateMachine = new TestHeadsetClientStateMachine( mHeadsetClientService, mHeadsetService, mHandlerThread.getLooper(), mNativeInterface); mHeadsetClientStateMachine.start(); TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper()); } } /** * Test AM to HF volume symmetric. The test takes the AM volume range 1-10 and HF 1-15. All the * AM values are mapped with corresponding HF values. After all collected, the test converts * back HF values and checks if they match AM. This proves that the conversion is symmetric. */ @Test @EnableFlags(Flags.FLAG_HEADSET_CLIENT_AM_HF_VOLUME_SYMMETRIC) public void testAmHfVolumeSymmetric_AmLowerRange() { int amMin = 1; int amMax = 10; Map<Integer, Integer> amToHfMap = new HashMap<>(); Assert.assertTrue(amMax < MAX_HFP_SCO_VOICE_CALL_VOLUME); quitHeadsetClientStateMachine(); when(mAudioManager.getStreamMaxVolume(anyInt())).thenReturn(amMax); when(mAudioManager.getStreamMinVolume(anyInt())).thenReturn(amMin); startHeadsetClientStateMachine(); for (int i = amMin; i <= amMax; i++) { // Collect AM to HF conversion amToHfMap.put(i, HeadsetClientStateMachine.amToHfVol(i)); } for (Map.Entry entry : amToHfMap.entrySet()) { // Convert back from collected HF to AM and check if equal the saved AM value Assert.assertEquals( HeadsetClientStateMachine.hfToAmVol((int) entry.getValue()), entry.getKey()); } } /** * Test HF to AM volume symmetric. The test takes the AM volume range 1-20 and HF 1-15. All the * HF values are mapped with corresponding AM values. After all collected, the test converts * back AM values and checks if they match HF. This proves that the conversion is symmetric. */ @Test @EnableFlags(Flags.FLAG_HEADSET_CLIENT_AM_HF_VOLUME_SYMMETRIC) public void testAmHfVolumeSymmetric_HfLowerRange() { int amMin = 1; int amMax = 20; Map<Integer, Integer> hfToAmMap = new HashMap<>(); Assert.assertTrue(amMax > MAX_HFP_SCO_VOICE_CALL_VOLUME); quitHeadsetClientStateMachine(); when(mAudioManager.getStreamMaxVolume(anyInt())).thenReturn(amMax); when(mAudioManager.getStreamMinVolume(anyInt())).thenReturn(amMin); startHeadsetClientStateMachine(); for (int i = MIN_HFP_SCO_VOICE_CALL_VOLUME; i <= MAX_HFP_SCO_VOICE_CALL_VOLUME; i++) { // Collect HF to AM conversion hfToAmMap.put(i, HeadsetClientStateMachine.hfToAmVol(i)); } for (Map.Entry entry : hfToAmMap.entrySet()) { // Convert back from collected AM to HF and check if equal the saved HF value Assert.assertEquals( HeadsetClientStateMachine.amToHfVol((int) entry.getValue()), entry.getKey()); } } /** * Test that default state is disconnected */ Loading flags/hfp.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -116,3 +116,13 @@ flag { purpose: PURPOSE_BUGFIX } } flag { name: "headset_client_am_hf_volume_symmetric" namespace: "bluetooth" description: "Fix AM/HF volume conversion to be symmetric" bug: "340482648" metadata { purpose: PURPOSE_BUGFIX } } Loading
android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java +19 −6 Original line number Diff line number Diff line Loading @@ -68,6 +68,7 @@ import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.flags.Flags; import com.android.bluetooth.hfp.HeadsetService; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IState; Loading Loading @@ -134,8 +135,8 @@ public class HeadsetClientStateMachine extends StateMachine { static final int CONNECTING_TIMEOUT_MS = 10000; // 10s private static final int ROUTING_DELAY_MS = 250; private static final int MAX_HFP_SCO_VOICE_CALL_VOLUME = 15; // HFP 1.5 spec. private static final int MIN_HFP_SCO_VOICE_CALL_VOLUME = 1; // HFP 1.5 spec. @VisibleForTesting static final int MAX_HFP_SCO_VOICE_CALL_VOLUME = 15; // HFP 1.5 spec. @VisibleForTesting static final int MIN_HFP_SCO_VOICE_CALL_VOLUME = 1; // HFP 1.5 spec. static final int HF_ORIGINATED_CALL_ID = -1; private static final long OUTGOING_TIMEOUT_MILLI = 10 * 1000; // 10 seconds Loading Loading @@ -995,8 +996,14 @@ public class HeadsetClientStateMachine extends StateMachine { static int hfToAmVol(int hfVol) { int amRange = sMaxAmVcVol - sMinAmVcVol; int hfRange = MAX_HFP_SCO_VOICE_CALL_VOLUME - MIN_HFP_SCO_VOICE_CALL_VOLUME; int amVol = 0; if (Flags.headsetClientAmHfVolumeSymmetric()) { amVol = (int) Math.round((hfVol - MIN_HFP_SCO_VOICE_CALL_VOLUME) * ((double) amRange / hfRange)) + sMinAmVcVol; } else { int amOffset = (amRange * (hfVol - MIN_HFP_SCO_VOICE_CALL_VOLUME)) / hfRange; int amVol = sMinAmVcVol + amOffset; amVol = sMinAmVcVol + amOffset; } Log.d(TAG, "HF -> AM " + hfVol + " " + amVol); return amVol; } Loading @@ -1004,8 +1011,14 @@ public class HeadsetClientStateMachine extends StateMachine { static int amToHfVol(int amVol) { int amRange = (sMaxAmVcVol > sMinAmVcVol) ? (sMaxAmVcVol - sMinAmVcVol) : 1; int hfRange = MAX_HFP_SCO_VOICE_CALL_VOLUME - MIN_HFP_SCO_VOICE_CALL_VOLUME; int hfVol = 0; if (Flags.headsetClientAmHfVolumeSymmetric()) { hfVol = (int) Math.round((amVol - sMinAmVcVol) * ((double) hfRange / amRange)) + MIN_HFP_SCO_VOICE_CALL_VOLUME; } else { int hfOffset = (hfRange * (amVol - sMinAmVcVol)) / amRange; int hfVol = MIN_HFP_SCO_VOICE_CALL_VOLUME + hfOffset; hfVol = MIN_HFP_SCO_VOICE_CALL_VOLUME + hfOffset; } Log.d(TAG, "AM -> HF " + amVol + " " + hfVol); return hfVol; } Loading
android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java +98 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,8 @@ import static android.content.pm.PackageManager.FEATURE_WATCH; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.AT_OK; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.ENTER_PRIVATE_MODE; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.EXPLICIT_CALL_TRANSFER; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.MAX_HFP_SCO_VOICE_CALL_VOLUME; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.MIN_HFP_SCO_VOICE_CALL_VOLUME; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.VOICE_RECOGNITION_START; import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.VOICE_RECOGNITION_STOP; Loading @@ -47,6 +49,8 @@ import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; import androidx.test.InstrumentationRegistry; Loading @@ -61,6 +65,7 @@ import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.RemoteDevices; import com.android.bluetooth.flags.Flags; import com.android.bluetooth.hfp.HeadsetService; import org.hamcrest.core.AllOf; Loading @@ -78,7 +83,9 @@ import org.mockito.hamcrest.MockitoHamcrest; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** Loading @@ -95,6 +102,8 @@ public class HeadsetClientStateMachineTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private AdapterService mAdapterService; @Mock private Resources mMockHfpResources; @Mock private HeadsetService mHeadsetService; Loading Loading @@ -157,6 +166,95 @@ public class HeadsetClientStateMachineTest { verifyNoMoreInteractions(mHeadsetService); } private void quitHeadsetClientStateMachine() { if (mHeadsetClientStateMachine != null) { TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper()); mHeadsetClientStateMachine.allowConnect = null; mHeadsetClientStateMachine.doQuit(); TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper()); mHeadsetClientStateMachine = null; } } private void startHeadsetClientStateMachine() { if (mHeadsetClientStateMachine == null) { mHeadsetClientStateMachine = new TestHeadsetClientStateMachine( mHeadsetClientService, mHeadsetService, mHandlerThread.getLooper(), mNativeInterface); mHeadsetClientStateMachine.start(); TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper()); } } /** * Test AM to HF volume symmetric. The test takes the AM volume range 1-10 and HF 1-15. All the * AM values are mapped with corresponding HF values. After all collected, the test converts * back HF values and checks if they match AM. This proves that the conversion is symmetric. */ @Test @EnableFlags(Flags.FLAG_HEADSET_CLIENT_AM_HF_VOLUME_SYMMETRIC) public void testAmHfVolumeSymmetric_AmLowerRange() { int amMin = 1; int amMax = 10; Map<Integer, Integer> amToHfMap = new HashMap<>(); Assert.assertTrue(amMax < MAX_HFP_SCO_VOICE_CALL_VOLUME); quitHeadsetClientStateMachine(); when(mAudioManager.getStreamMaxVolume(anyInt())).thenReturn(amMax); when(mAudioManager.getStreamMinVolume(anyInt())).thenReturn(amMin); startHeadsetClientStateMachine(); for (int i = amMin; i <= amMax; i++) { // Collect AM to HF conversion amToHfMap.put(i, HeadsetClientStateMachine.amToHfVol(i)); } for (Map.Entry entry : amToHfMap.entrySet()) { // Convert back from collected HF to AM and check if equal the saved AM value Assert.assertEquals( HeadsetClientStateMachine.hfToAmVol((int) entry.getValue()), entry.getKey()); } } /** * Test HF to AM volume symmetric. The test takes the AM volume range 1-20 and HF 1-15. All the * HF values are mapped with corresponding AM values. After all collected, the test converts * back AM values and checks if they match HF. This proves that the conversion is symmetric. */ @Test @EnableFlags(Flags.FLAG_HEADSET_CLIENT_AM_HF_VOLUME_SYMMETRIC) public void testAmHfVolumeSymmetric_HfLowerRange() { int amMin = 1; int amMax = 20; Map<Integer, Integer> hfToAmMap = new HashMap<>(); Assert.assertTrue(amMax > MAX_HFP_SCO_VOICE_CALL_VOLUME); quitHeadsetClientStateMachine(); when(mAudioManager.getStreamMaxVolume(anyInt())).thenReturn(amMax); when(mAudioManager.getStreamMinVolume(anyInt())).thenReturn(amMin); startHeadsetClientStateMachine(); for (int i = MIN_HFP_SCO_VOICE_CALL_VOLUME; i <= MAX_HFP_SCO_VOICE_CALL_VOLUME; i++) { // Collect HF to AM conversion hfToAmMap.put(i, HeadsetClientStateMachine.hfToAmVol(i)); } for (Map.Entry entry : hfToAmMap.entrySet()) { // Convert back from collected AM to HF and check if equal the saved HF value Assert.assertEquals( HeadsetClientStateMachine.amToHfVol((int) entry.getValue()), entry.getKey()); } } /** * Test that default state is disconnected */ Loading
flags/hfp.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -116,3 +116,13 @@ flag { purpose: PURPOSE_BUGFIX } } flag { name: "headset_client_am_hf_volume_symmetric" namespace: "bluetooth" description: "Fix AM/HF volume conversion to be symmetric" bug: "340482648" metadata { purpose: PURPOSE_BUGFIX } }