Loading android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java +45 −40 Original line number Diff line number Diff line Loading @@ -159,8 +159,6 @@ public class HeadsetStateMachine extends StateMachine { // Audio disconnect timeout retry count private int mAudioDisconnectRetry = 0; static final int HFP_SET_AUDIO_POLICY = 1; private BluetoothSinkAudioPolicy mHsClientAudioPolicy; // Keys are AT commands, and values are the company IDs. Loading Loading @@ -1966,87 +1964,90 @@ public class HeadsetStateMachine extends StateMachine { } /** * Process Android specific AT commands. * Look for Android specific AT command starts with AT+ANDROID and try to process it * * @param atString AT command after the "AT+" prefix. Starts with "ANDROID" * @param atString AT command in string * @param device Remote device that has sent this command * @return true if the command is processed, false if not. */ private void processAndroidAt(String atString, BluetoothDevice device) { log("processAndroidSpecificAt - atString = " + atString); @VisibleForTesting boolean checkAndProcessAndroidAt(String atString, BluetoothDevice device) { log("checkAndProcessAndroidAt - atString = " + atString); if (atString.equals("+ANDROID=?")) { // feature request type command processAndroidAtFeatureRequest(device); mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0); return true; } else if (atString.startsWith("+ANDROID=")) { // set type command int equalIndex = atString.indexOf("="); String arg = atString.substring(equalIndex + 1); if (arg.isEmpty()) { Log.e(TAG, "Command Invalid!"); mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); return; Log.w(TAG, "Android AT command is empty"); return false; } Object[] args = generateArgs(arg); if (!(args[0] instanceof Integer)) { Log.e(TAG, "Type ID is invalid"); mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); return; } int type = (Integer) args[0]; String type = (String) args[0]; if (type == HFP_SET_AUDIO_POLICY) { processAndroidAtSetAudioPolicy(args, device); if (type.equals(BluetoothSinkAudioPolicy.HFP_SET_SINK_AUDIO_POLICY_ID)) { Log.d(TAG, "Processing command: " + atString); if (processAndroidAtSinkAudioPolicy(args, device)) { mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0); } else { Log.w(TAG, "Undefined AT+ANDROID command"); mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); return; Log.w(TAG, "Invalid SinkAudioPolicy parameters!"); mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } return true; } else { Log.e(TAG, "Undefined AT+ANDROID command"); mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); return; Log.w(TAG, "Undefined Android command type: " + type); return false; } mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0); } Log.w(TAG, "Unhandled +ANDROID command: " + atString); return false; } private void processAndroidAtFeatureRequest(BluetoothDevice device) { /* replying with +ANDROID=1 here, 1 is the feature id for audio policy currently we only support one type of feature replying with +ANDROID: (<feature1>, <feature2>, ...) currently we only support one type of feature: SINKAUDIOPOLICY */ mNativeInterface.atResponseString(device, BluetoothHeadset.VENDOR_RESULT_CODE_COMMAND_ANDROID + ": " + HFP_SET_AUDIO_POLICY); + ": (" + BluetoothSinkAudioPolicy.HFP_SET_SINK_AUDIO_POLICY_ID + ")"); } /** * Process AT+ANDROID AT command * Process AT+ANDROID=SINKAUDIOPOLICY AT command * * @param args command arguments after the equal sign * @param device Remote device that has sent this command * @return true on success, false on error */ private void processAndroidAtSetAudioPolicy(Object[] args, BluetoothDevice device) { @VisibleForTesting boolean processAndroidAtSinkAudioPolicy(Object[] args, BluetoothDevice device) { if (args.length != 4) { Log.e(TAG, "processAndroidAtSetAudioPolicy() args length must be 4: " Log.e(TAG, "processAndroidAtSinkAudioPolicy() args length must be 4: " + String.valueOf(args.length)); return; return false; } if (!(args[1] instanceof Integer) || !(args[2] instanceof Integer) || !(args[3] instanceof Integer)) { Log.e(TAG, "processAndroidAtSetAudioPolicy() argument types not matched"); return; Log.e(TAG, "processAndroidAtSinkAudioPolicy() argument types not matched"); return false; } if (!mDevice.equals(device)) { Log.e(TAG, "processAndroidAtSetAudioPolicy(): argument device " + device Log.e(TAG, "processAndroidAtSinkAudioPolicy(): argument device " + device + " doesn't match mDevice " + mDevice); return; return false; } int callEstablishPolicy = (Integer) args[1]; Loading @@ -2058,6 +2059,7 @@ public class HeadsetStateMachine extends StateMachine { .setActiveDevicePolicyAfterConnection(connectingTimePolicy) .setInBandRingtonePolicy(inbandPolicy) .build()); return true; } /** Loading Loading @@ -2129,6 +2131,9 @@ public class HeadsetStateMachine extends StateMachine { processAtCpbs(atCommand.substring(5), commandType, device); } else if (atCommand.startsWith("+CPBR")) { processAtCpbr(atCommand.substring(5), commandType, device); } else if (atCommand.startsWith("+ANDROID") && checkAndProcessAndroidAt(atCommand, device)) { // Do nothing } else { processVendorSpecificAt(atCommand, device); } Loading android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java +42 −5 Original line number Diff line number Diff line Loading @@ -188,8 +188,6 @@ public class HeadsetClientStateMachine extends StateMachine { private final boolean mClccPollDuringCall; private static final int CALL_AUDIO_POLICY_FEATURE_ID = 1; public int mAudioPolicyRemoteSupported; private BluetoothSinkAudioPolicy mHsClientAudioPolicy; private final int mConnectingTimePolicyProperty; Loading Loading @@ -1217,14 +1215,15 @@ public class HeadsetClientStateMachine extends StateMachine { break; case StackEvent.EVENT_TYPE_UNKNOWN_EVENT: if (mVendorProcessor.processEvent(event.valueString, event.device)) { mQueuedActions.poll(); if (mVendorProcessor.isAndroidAtCommand(event.valueString) && processAndroidSlcCommand(event.valueString, event.device)) { transitionTo(mConnected); } else { Log.e(TAG, "Unknown event :" + event.valueString + " for device " + event.device); } break; case StackEvent.EVENT_TYPE_SUBSCRIBER_INFO: case StackEvent.EVENT_TYPE_CURRENT_CALLS: case StackEvent.EVENT_TYPE_OPERATOR_NAME: Loading Loading @@ -2027,6 +2026,44 @@ public class HeadsetClientStateMachine extends StateMachine { HfpClientConnectionService.onAudioStateChanged(device, newState, prevState); } @VisibleForTesting boolean processAndroidSlcCommand(String atString, BluetoothDevice device) { if (!mCurrentDevice.equals(device) || atString.lastIndexOf("+ANDROID:") < 0) { return false; } // Check if it is +ANDROID: (<feature1>,...),(<feature2>, ...) the reply for AT+ANDROID=? while (true) { int indexUpperBucket = atString.indexOf("("); int indexLowerBucket = atString.indexOf(")"); if (indexUpperBucket < 0 || indexLowerBucket < 0 || indexUpperBucket >= indexLowerBucket) { break; } String feature = atString.substring(indexUpperBucket + 1, indexLowerBucket); Log.d(TAG, "processAndroidSlcCommand: feature=[" + feature + "]"); processAndroidAtFeature(feature.split(",")); atString = atString.substring(indexLowerBucket + 1); } return true; } private void processAndroidAtFeature(String[] args) { if (args.length < 1) { Log.e(TAG, "processAndroidAtFeature: Invalid feature length"); return; } String featureId = args[0]; if (featureId.equals(BluetoothSinkAudioPolicy.HFP_SET_SINK_AUDIO_POLICY_ID)) { Log.i(TAG, "processAndroidAtFeature:" + BluetoothSinkAudioPolicy.HFP_SET_SINK_AUDIO_POLICY_ID + " supported"); setAudioPolicyRemoteSupported(true); } } // This method does not check for error condition (newState == prevState) private void broadcastConnectionState(BluetoothDevice device, int newState, int prevState) { logD("Connection state " + device + ": " + prevState + "->" + newState); Loading Loading @@ -2226,7 +2263,7 @@ public class HeadsetClientStateMachine extends StateMachine { private String createMaskString(BluetoothSinkAudioPolicy policies) { StringBuilder mask = new StringBuilder(); mask.append(Integer.toString(CALL_AUDIO_POLICY_FEATURE_ID)); mask.append(BluetoothSinkAudioPolicy.HFP_SET_SINK_AUDIO_POLICY_ID); mask.append("," + policies.getCallEstablishPolicy()); mask.append("," + policies.getActiveDevicePolicyAfterConnection()); mask.append("," + policies.getInBandRingtonePolicy()); Loading android/app/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java +20 −9 Original line number Diff line number Diff line Loading @@ -127,12 +127,7 @@ class VendorCommandResponseProcessor { return true; } public boolean processEvent(String atString, BluetoothDevice device) { if (device == null) { Log.w(TAG, "processVendorEvent device is null"); return false; } private String getVendorIdFromAtCommand(String atString) { // Get event code int indexOfEqual = atString.indexOf('='); int indexOfColon = atString.indexOf(':'); Loading @@ -148,13 +143,29 @@ class VendorCommandResponseProcessor { // replace all white spaces eventCode = eventCode.replaceAll("\\s+", ""); return eventCode; } public boolean isAndroidAtCommand(String atString) { String eventCode = getVendorIdFromAtCommand(atString); Integer vendorId = SUPPORTED_VENDOR_EVENTS.get(eventCode); if (vendorId == null) { return false; } return vendorId == BluetoothAssignedNumbers.GOOGLE; } public boolean processEvent(String atString, BluetoothDevice device) { if (device == null) { Log.w(TAG, "processVendorEvent device is null"); return false; } String eventCode = getVendorIdFromAtCommand(atString); Integer vendorId = SUPPORTED_VENDOR_EVENTS.get(eventCode); if (vendorId == null) { Log.e(TAG, "Invalid response: " + atString + ". " + eventCode); return false; } else if (vendorId == BluetoothAssignedNumbers.GOOGLE) { Log.i(TAG, "received +ANDROID event. Setting Audio policy to true"); mService.setAudioPolicyRemoteSupported(device, true); } else { broadcastVendorSpecificEventIntent(vendorId, eventCode, atString, device); logD("process vendor event " + vendorId + ", " + eventCode + ", " Loading android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java +105 −5 Original line number Diff line number Diff line Loading @@ -1439,18 +1439,118 @@ public class HeadsetStateMachineTest { /** * A test to validate received Android AT commands and processing */ @Ignore("b/275668166") @Test public void testProcessAndroidAt() { setUpConnectedState(); public void testCheckAndProcessAndroidAt() { // Commands that will be handled Assert.assertTrue(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID=?" , mTestDevice)); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_OK, 0); Assert.assertTrue(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID=SINKAUDIOPOLICY,1,1,1" , mTestDevice)); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_OK, 0); Assert.assertTrue(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID=SINKAUDIOPOLICY,100,100,100" , mTestDevice)); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(3)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_OK, 0); Assert.assertTrue(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID=SINKAUDIOPOLICY,1,2,3,4,5" , mTestDevice)); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); // Commands with correct format but will not be handled Assert.assertFalse(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID=" , mTestDevice)); Assert.assertFalse(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID: PROBE,1,\"`AB\"" , mTestDevice)); Assert.assertFalse(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID= PROBE,1,\"`AB\"" , mTestDevice)); Assert.assertFalse(mHeadsetStateMachine.checkAndProcessAndroidAt( "AT+ANDROID=PROBE,1,1,\"PQGHRSBCTU__\"" , mTestDevice)); // Incorrect format AT command Assert.assertFalse(mHeadsetStateMachine.checkAndProcessAndroidAt( "RANDOM FORMAT" , mTestDevice)); // Check no any AT result was sent for the failed ones verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(3)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_OK, 0); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } @Test public void testCheckAndProcessAndroidAt_replyAndroidAtFeatureRequest() { // Commands that will be handled Assert.assertTrue(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID=?" , mTestDevice)); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseString( mTestDevice, "+ANDROID: (SINKAUDIOPOLICY)"); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_OK, 0); } /** * A end to end test to validate received Android AT commands and processing */ @Test public void testCehckAndProcessAndroidAtFromStateMachine() { // setAudioPolicyMetadata is invoked in HeadsetStateMachine.init() so start from 1 int expectCallTimes = 1; // setup Audio Policy Feature setUpConnectedState(); setUpAudioPolicy(); // receive and set android policy mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT, new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_UNKNOWN_AT, "+ANDROID=1,1,1,1", mTestDevice)); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)) "+ANDROID=SINKAUDIOPOLICY,1,1,1", mTestDevice)); expectCallTimes++; verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(expectCallTimes)) .setAudioPolicyMetadata(anyObject(), anyObject()); // receive and not set android policy mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT, new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_UNKNOWN_AT, "AT+ANDROID=PROBE,1,1,\"PQGHRSBCTU__\"", mTestDevice)); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(expectCallTimes)) .setAudioPolicyMetadata(anyObject(), anyObject()); } /** * A test to verify whether the sink audio policy command is valid */ @Test public void testProcessAndroidAtSinkAudioPolicy() { // expected format Assert.assertTrue(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0,0,0", mTestDevice)); Assert.assertTrue(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0,0,1", mTestDevice)); Assert.assertTrue(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0,1,0", mTestDevice)); Assert.assertTrue(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,1,0,0", mTestDevice)); Assert.assertTrue(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,1,1,1", mTestDevice)); // invalid format Assert.assertFalse(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0", mTestDevice)); Assert.assertFalse(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0,0", mTestDevice)); Assert.assertFalse(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0,0,0,0", mTestDevice)); Assert.assertFalse(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,NOT,INT,TYPE", mTestDevice)); Assert.assertFalse(setSinkAudioPolicyArgs("RANDOM,VALUE-#$%,*(&^", mTestDevice)); // wrong device BluetoothDevice device = mAdapter.getRemoteDevice("01:01:01:01:01:01"); Assert.assertFalse(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0,0,0", device)); } /** * set sink audio policy * @param arg body of the AT command * @return the result from processAndroidAtSinkAudioPolicy */ private boolean setSinkAudioPolicyArgs(String arg, BluetoothDevice device) { Object[] args = HeadsetStateMachine.generateArgs(arg); return mHeadsetStateMachine.processAndroidAtSinkAudioPolicy(args, device); } /** Loading android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java +85 −14 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java +45 −40 Original line number Diff line number Diff line Loading @@ -159,8 +159,6 @@ public class HeadsetStateMachine extends StateMachine { // Audio disconnect timeout retry count private int mAudioDisconnectRetry = 0; static final int HFP_SET_AUDIO_POLICY = 1; private BluetoothSinkAudioPolicy mHsClientAudioPolicy; // Keys are AT commands, and values are the company IDs. Loading Loading @@ -1966,87 +1964,90 @@ public class HeadsetStateMachine extends StateMachine { } /** * Process Android specific AT commands. * Look for Android specific AT command starts with AT+ANDROID and try to process it * * @param atString AT command after the "AT+" prefix. Starts with "ANDROID" * @param atString AT command in string * @param device Remote device that has sent this command * @return true if the command is processed, false if not. */ private void processAndroidAt(String atString, BluetoothDevice device) { log("processAndroidSpecificAt - atString = " + atString); @VisibleForTesting boolean checkAndProcessAndroidAt(String atString, BluetoothDevice device) { log("checkAndProcessAndroidAt - atString = " + atString); if (atString.equals("+ANDROID=?")) { // feature request type command processAndroidAtFeatureRequest(device); mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0); return true; } else if (atString.startsWith("+ANDROID=")) { // set type command int equalIndex = atString.indexOf("="); String arg = atString.substring(equalIndex + 1); if (arg.isEmpty()) { Log.e(TAG, "Command Invalid!"); mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); return; Log.w(TAG, "Android AT command is empty"); return false; } Object[] args = generateArgs(arg); if (!(args[0] instanceof Integer)) { Log.e(TAG, "Type ID is invalid"); mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); return; } int type = (Integer) args[0]; String type = (String) args[0]; if (type == HFP_SET_AUDIO_POLICY) { processAndroidAtSetAudioPolicy(args, device); if (type.equals(BluetoothSinkAudioPolicy.HFP_SET_SINK_AUDIO_POLICY_ID)) { Log.d(TAG, "Processing command: " + atString); if (processAndroidAtSinkAudioPolicy(args, device)) { mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0); } else { Log.w(TAG, "Undefined AT+ANDROID command"); mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); return; Log.w(TAG, "Invalid SinkAudioPolicy parameters!"); mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } return true; } else { Log.e(TAG, "Undefined AT+ANDROID command"); mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); return; Log.w(TAG, "Undefined Android command type: " + type); return false; } mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0); } Log.w(TAG, "Unhandled +ANDROID command: " + atString); return false; } private void processAndroidAtFeatureRequest(BluetoothDevice device) { /* replying with +ANDROID=1 here, 1 is the feature id for audio policy currently we only support one type of feature replying with +ANDROID: (<feature1>, <feature2>, ...) currently we only support one type of feature: SINKAUDIOPOLICY */ mNativeInterface.atResponseString(device, BluetoothHeadset.VENDOR_RESULT_CODE_COMMAND_ANDROID + ": " + HFP_SET_AUDIO_POLICY); + ": (" + BluetoothSinkAudioPolicy.HFP_SET_SINK_AUDIO_POLICY_ID + ")"); } /** * Process AT+ANDROID AT command * Process AT+ANDROID=SINKAUDIOPOLICY AT command * * @param args command arguments after the equal sign * @param device Remote device that has sent this command * @return true on success, false on error */ private void processAndroidAtSetAudioPolicy(Object[] args, BluetoothDevice device) { @VisibleForTesting boolean processAndroidAtSinkAudioPolicy(Object[] args, BluetoothDevice device) { if (args.length != 4) { Log.e(TAG, "processAndroidAtSetAudioPolicy() args length must be 4: " Log.e(TAG, "processAndroidAtSinkAudioPolicy() args length must be 4: " + String.valueOf(args.length)); return; return false; } if (!(args[1] instanceof Integer) || !(args[2] instanceof Integer) || !(args[3] instanceof Integer)) { Log.e(TAG, "processAndroidAtSetAudioPolicy() argument types not matched"); return; Log.e(TAG, "processAndroidAtSinkAudioPolicy() argument types not matched"); return false; } if (!mDevice.equals(device)) { Log.e(TAG, "processAndroidAtSetAudioPolicy(): argument device " + device Log.e(TAG, "processAndroidAtSinkAudioPolicy(): argument device " + device + " doesn't match mDevice " + mDevice); return; return false; } int callEstablishPolicy = (Integer) args[1]; Loading @@ -2058,6 +2059,7 @@ public class HeadsetStateMachine extends StateMachine { .setActiveDevicePolicyAfterConnection(connectingTimePolicy) .setInBandRingtonePolicy(inbandPolicy) .build()); return true; } /** Loading Loading @@ -2129,6 +2131,9 @@ public class HeadsetStateMachine extends StateMachine { processAtCpbs(atCommand.substring(5), commandType, device); } else if (atCommand.startsWith("+CPBR")) { processAtCpbr(atCommand.substring(5), commandType, device); } else if (atCommand.startsWith("+ANDROID") && checkAndProcessAndroidAt(atCommand, device)) { // Do nothing } else { processVendorSpecificAt(atCommand, device); } Loading
android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java +42 −5 Original line number Diff line number Diff line Loading @@ -188,8 +188,6 @@ public class HeadsetClientStateMachine extends StateMachine { private final boolean mClccPollDuringCall; private static final int CALL_AUDIO_POLICY_FEATURE_ID = 1; public int mAudioPolicyRemoteSupported; private BluetoothSinkAudioPolicy mHsClientAudioPolicy; private final int mConnectingTimePolicyProperty; Loading Loading @@ -1217,14 +1215,15 @@ public class HeadsetClientStateMachine extends StateMachine { break; case StackEvent.EVENT_TYPE_UNKNOWN_EVENT: if (mVendorProcessor.processEvent(event.valueString, event.device)) { mQueuedActions.poll(); if (mVendorProcessor.isAndroidAtCommand(event.valueString) && processAndroidSlcCommand(event.valueString, event.device)) { transitionTo(mConnected); } else { Log.e(TAG, "Unknown event :" + event.valueString + " for device " + event.device); } break; case StackEvent.EVENT_TYPE_SUBSCRIBER_INFO: case StackEvent.EVENT_TYPE_CURRENT_CALLS: case StackEvent.EVENT_TYPE_OPERATOR_NAME: Loading Loading @@ -2027,6 +2026,44 @@ public class HeadsetClientStateMachine extends StateMachine { HfpClientConnectionService.onAudioStateChanged(device, newState, prevState); } @VisibleForTesting boolean processAndroidSlcCommand(String atString, BluetoothDevice device) { if (!mCurrentDevice.equals(device) || atString.lastIndexOf("+ANDROID:") < 0) { return false; } // Check if it is +ANDROID: (<feature1>,...),(<feature2>, ...) the reply for AT+ANDROID=? while (true) { int indexUpperBucket = atString.indexOf("("); int indexLowerBucket = atString.indexOf(")"); if (indexUpperBucket < 0 || indexLowerBucket < 0 || indexUpperBucket >= indexLowerBucket) { break; } String feature = atString.substring(indexUpperBucket + 1, indexLowerBucket); Log.d(TAG, "processAndroidSlcCommand: feature=[" + feature + "]"); processAndroidAtFeature(feature.split(",")); atString = atString.substring(indexLowerBucket + 1); } return true; } private void processAndroidAtFeature(String[] args) { if (args.length < 1) { Log.e(TAG, "processAndroidAtFeature: Invalid feature length"); return; } String featureId = args[0]; if (featureId.equals(BluetoothSinkAudioPolicy.HFP_SET_SINK_AUDIO_POLICY_ID)) { Log.i(TAG, "processAndroidAtFeature:" + BluetoothSinkAudioPolicy.HFP_SET_SINK_AUDIO_POLICY_ID + " supported"); setAudioPolicyRemoteSupported(true); } } // This method does not check for error condition (newState == prevState) private void broadcastConnectionState(BluetoothDevice device, int newState, int prevState) { logD("Connection state " + device + ": " + prevState + "->" + newState); Loading Loading @@ -2226,7 +2263,7 @@ public class HeadsetClientStateMachine extends StateMachine { private String createMaskString(BluetoothSinkAudioPolicy policies) { StringBuilder mask = new StringBuilder(); mask.append(Integer.toString(CALL_AUDIO_POLICY_FEATURE_ID)); mask.append(BluetoothSinkAudioPolicy.HFP_SET_SINK_AUDIO_POLICY_ID); mask.append("," + policies.getCallEstablishPolicy()); mask.append("," + policies.getActiveDevicePolicyAfterConnection()); mask.append("," + policies.getInBandRingtonePolicy()); Loading
android/app/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java +20 −9 Original line number Diff line number Diff line Loading @@ -127,12 +127,7 @@ class VendorCommandResponseProcessor { return true; } public boolean processEvent(String atString, BluetoothDevice device) { if (device == null) { Log.w(TAG, "processVendorEvent device is null"); return false; } private String getVendorIdFromAtCommand(String atString) { // Get event code int indexOfEqual = atString.indexOf('='); int indexOfColon = atString.indexOf(':'); Loading @@ -148,13 +143,29 @@ class VendorCommandResponseProcessor { // replace all white spaces eventCode = eventCode.replaceAll("\\s+", ""); return eventCode; } public boolean isAndroidAtCommand(String atString) { String eventCode = getVendorIdFromAtCommand(atString); Integer vendorId = SUPPORTED_VENDOR_EVENTS.get(eventCode); if (vendorId == null) { return false; } return vendorId == BluetoothAssignedNumbers.GOOGLE; } public boolean processEvent(String atString, BluetoothDevice device) { if (device == null) { Log.w(TAG, "processVendorEvent device is null"); return false; } String eventCode = getVendorIdFromAtCommand(atString); Integer vendorId = SUPPORTED_VENDOR_EVENTS.get(eventCode); if (vendorId == null) { Log.e(TAG, "Invalid response: " + atString + ". " + eventCode); return false; } else if (vendorId == BluetoothAssignedNumbers.GOOGLE) { Log.i(TAG, "received +ANDROID event. Setting Audio policy to true"); mService.setAudioPolicyRemoteSupported(device, true); } else { broadcastVendorSpecificEventIntent(vendorId, eventCode, atString, device); logD("process vendor event " + vendorId + ", " + eventCode + ", " Loading
android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java +105 −5 Original line number Diff line number Diff line Loading @@ -1439,18 +1439,118 @@ public class HeadsetStateMachineTest { /** * A test to validate received Android AT commands and processing */ @Ignore("b/275668166") @Test public void testProcessAndroidAt() { setUpConnectedState(); public void testCheckAndProcessAndroidAt() { // Commands that will be handled Assert.assertTrue(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID=?" , mTestDevice)); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_OK, 0); Assert.assertTrue(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID=SINKAUDIOPOLICY,1,1,1" , mTestDevice)); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_OK, 0); Assert.assertTrue(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID=SINKAUDIOPOLICY,100,100,100" , mTestDevice)); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(3)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_OK, 0); Assert.assertTrue(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID=SINKAUDIOPOLICY,1,2,3,4,5" , mTestDevice)); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); // Commands with correct format but will not be handled Assert.assertFalse(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID=" , mTestDevice)); Assert.assertFalse(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID: PROBE,1,\"`AB\"" , mTestDevice)); Assert.assertFalse(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID= PROBE,1,\"`AB\"" , mTestDevice)); Assert.assertFalse(mHeadsetStateMachine.checkAndProcessAndroidAt( "AT+ANDROID=PROBE,1,1,\"PQGHRSBCTU__\"" , mTestDevice)); // Incorrect format AT command Assert.assertFalse(mHeadsetStateMachine.checkAndProcessAndroidAt( "RANDOM FORMAT" , mTestDevice)); // Check no any AT result was sent for the failed ones verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(3)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_OK, 0); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } @Test public void testCheckAndProcessAndroidAt_replyAndroidAtFeatureRequest() { // Commands that will be handled Assert.assertTrue(mHeadsetStateMachine.checkAndProcessAndroidAt( "+ANDROID=?" , mTestDevice)); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseString( mTestDevice, "+ANDROID: (SINKAUDIOPOLICY)"); verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode( mTestDevice, HeadsetHalConstants.AT_RESPONSE_OK, 0); } /** * A end to end test to validate received Android AT commands and processing */ @Test public void testCehckAndProcessAndroidAtFromStateMachine() { // setAudioPolicyMetadata is invoked in HeadsetStateMachine.init() so start from 1 int expectCallTimes = 1; // setup Audio Policy Feature setUpConnectedState(); setUpAudioPolicy(); // receive and set android policy mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT, new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_UNKNOWN_AT, "+ANDROID=1,1,1,1", mTestDevice)); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)) "+ANDROID=SINKAUDIOPOLICY,1,1,1", mTestDevice)); expectCallTimes++; verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(expectCallTimes)) .setAudioPolicyMetadata(anyObject(), anyObject()); // receive and not set android policy mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT, new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_UNKNOWN_AT, "AT+ANDROID=PROBE,1,1,\"PQGHRSBCTU__\"", mTestDevice)); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(expectCallTimes)) .setAudioPolicyMetadata(anyObject(), anyObject()); } /** * A test to verify whether the sink audio policy command is valid */ @Test public void testProcessAndroidAtSinkAudioPolicy() { // expected format Assert.assertTrue(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0,0,0", mTestDevice)); Assert.assertTrue(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0,0,1", mTestDevice)); Assert.assertTrue(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0,1,0", mTestDevice)); Assert.assertTrue(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,1,0,0", mTestDevice)); Assert.assertTrue(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,1,1,1", mTestDevice)); // invalid format Assert.assertFalse(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0", mTestDevice)); Assert.assertFalse(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0,0", mTestDevice)); Assert.assertFalse(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0,0,0,0", mTestDevice)); Assert.assertFalse(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,NOT,INT,TYPE", mTestDevice)); Assert.assertFalse(setSinkAudioPolicyArgs("RANDOM,VALUE-#$%,*(&^", mTestDevice)); // wrong device BluetoothDevice device = mAdapter.getRemoteDevice("01:01:01:01:01:01"); Assert.assertFalse(setSinkAudioPolicyArgs("SINKAUDIOPOLICY,0,0,0", device)); } /** * set sink audio policy * @param arg body of the AT command * @return the result from processAndroidAtSinkAudioPolicy */ private boolean setSinkAudioPolicyArgs(String arg, BluetoothDevice device) { Object[] args = HeadsetStateMachine.generateArgs(arg); return mHeadsetStateMachine.processAndroidAtSinkAudioPolicy(args, device); } /** Loading
android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java +85 −14 File changed.Preview size limit exceeded, changes collapsed. Show changes