Loading android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java +4 −4 Original line number Diff line number Diff line Loading @@ -80,8 +80,8 @@ public class HeadsetClientService extends ProfileService { } // Setup the JNI service mNativeInterface = new NativeInterface(); mNativeInterface.initializeNative(); mNativeInterface = NativeInterface.getInstance(); mNativeInterface.initialize(); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); if (mAudioManager == null) { Loading Loading @@ -137,7 +137,7 @@ public class HeadsetClientService extends ProfileService { mSmThread.quit(); mSmThread = null; mNativeInterface.cleanupNative(); mNativeInterface.cleanup(); mNativeInterface = null; return true; Loading Loading @@ -885,7 +885,7 @@ public class HeadsetClientService extends ProfileService { // Allocate a new SM Log.d(TAG, "Creating a new state machine"); sm = mSmFactory.make(this, mSmThread); sm = mSmFactory.make(this, mSmThread, mNativeInterface); mStateMachineMap.put(device, sm); return sm; } Loading android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java +34 −29 Original line number Diff line number Diff line Loading @@ -174,7 +174,8 @@ public class HeadsetClientStateMachine extends StateMachine { // This is returned when requesting focus from AudioManager private AudioFocusRequest mAudioFocusRequest; private AudioManager mAudioManager; private final AudioManager mAudioManager; private final NativeInterface mNativeInterface; // Accessor for the states, useful for reusing the state machines public IState getDisconnectedState() { Loading Loading @@ -264,7 +265,7 @@ public class HeadsetClientStateMachine extends StateMachine { private boolean queryCallsStart() { logD("queryCallsStart"); clearPendingAction(); NativeInterface.queryCurrentCallsNative(getByteAddress(mCurrentDevice)); mNativeInterface.queryCurrentCalls(getByteAddress(mCurrentDevice)); addQueuedAction(QUERY_CURRENT_CALLS, 0); return true; } Loading Loading @@ -487,7 +488,7 @@ public class HeadsetClientStateMachine extends StateMachine { routeHfpAudio(true); } if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), action, 0)) { if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) { addQueuedAction(ACCEPT_CALL, action); } else { Log.e(TAG, "ERROR: Couldn't accept a call, action:" + action); Loading Loading @@ -526,8 +527,8 @@ public class HeadsetClientStateMachine extends StateMachine { return; } if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) { logD("Reject call action " + action); if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), action, 0)) { addQueuedAction(REJECT_CALL, action); } else { Log.e(TAG, "ERROR: Couldn't reject a call, action:" + action); Loading @@ -551,7 +552,7 @@ public class HeadsetClientStateMachine extends StateMachine { action = HeadsetClientHalConstants.CALL_ACTION_CHLD_2; } if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), action, 0)) { if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) { addQueuedAction(HOLD_CALL, action); } else { Log.e(TAG, "ERROR: Couldn't hold a call, action:" + action); Loading @@ -572,7 +573,7 @@ public class HeadsetClientStateMachine extends StateMachine { action = HeadsetClientHalConstants.CALL_ACTION_CHLD_0; } if (c != null) { if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), action, 0)) { if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) { addQueuedAction(TERMINATE_CALL, action); } else { Log.e(TAG, "ERROR: Couldn't terminate outgoing call"); Loading @@ -590,7 +591,7 @@ public class HeadsetClientStateMachine extends StateMachine { return; } if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), HeadsetClientHalConstants.CALL_ACTION_CHLD_2X, idx)) { addQueuedAction(ENTER_PRIVATE_MODE, c); } else { Loading @@ -606,7 +607,7 @@ public class HeadsetClientStateMachine extends StateMachine { return; } if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), HeadsetClientHalConstants.CALL_ACTION_CHLD_4, -1)) { addQueuedAction(EXPLICIT_CALL_TRANSFER); } else { Loading Loading @@ -659,9 +660,11 @@ public class HeadsetClientStateMachine extends StateMachine { return b; } HeadsetClientStateMachine(HeadsetClientService context, Looper looper) { HeadsetClientStateMachine(HeadsetClientService context, Looper looper, NativeInterface nativeInterface) { super(TAG, looper); mService = context; mNativeInterface = nativeInterface; mAudioManager = mService.getAudioManager(); mAdapter = BluetoothAdapter.getDefaultAdapter(); Loading Loading @@ -699,9 +702,11 @@ public class HeadsetClientStateMachine extends StateMachine { setInitialState(mDisconnected); } static HeadsetClientStateMachine make(HeadsetClientService context, Looper l) { static HeadsetClientStateMachine make(HeadsetClientService context, Looper looper, NativeInterface nativeInterface) { logD("make"); HeadsetClientStateMachine hfcsm = new HeadsetClientStateMachine(context, l); HeadsetClientStateMachine hfcsm = new HeadsetClientStateMachine(context, looper, nativeInterface); hfcsm.start(); return hfcsm; } Loading Loading @@ -739,7 +744,7 @@ public class HeadsetClientStateMachine extends StateMachine { public void doQuit() { logD("doQuit"); if (mCurrentDevice != null) { NativeInterface.disconnectNative(getByteAddress(mCurrentDevice)); mNativeInterface.disconnect(getByteAddress(mCurrentDevice)); } routeHfpAudio(false); returnAudioFocusIfNecessary(); Loading Loading @@ -825,7 +830,7 @@ public class HeadsetClientStateMachine extends StateMachine { switch (message.what) { case CONNECT: BluetoothDevice device = (BluetoothDevice) message.obj; if (!NativeInterface.connectNative(getByteAddress(device))) { if (!mNativeInterface.connect(getByteAddress(device))) { // No state transition is involved, fire broadcast immediately broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_DISCONNECTED); Loading Loading @@ -871,7 +876,7 @@ public class HeadsetClientStateMachine extends StateMachine { + " bondState=" + device.getBondState()); // reject the connection and stay in Disconnected state // itself NativeInterface.disconnectNative(getByteAddress(device)); mNativeInterface.disconnect(getByteAddress(device)); // the other profile connection should be initiated AdapterService adapterService = AdapterService.getAdapterService(); // No state transition is involved, fire broadcast immediately Loading Loading @@ -985,7 +990,7 @@ public class HeadsetClientStateMachine extends StateMachine { // We do not support devices which do not support enhanced call status (ECS). if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_ECS) == 0) { NativeInterface.disconnectNative(getByteAddress(device)); mNativeInterface.disconnect(getByteAddress(device)); return; } Loading @@ -993,7 +998,7 @@ public class HeadsetClientStateMachine extends StateMachine { if (HeadsetClientHalConstants.HANDSFREECLIENT_NREC_SUPPORTED && ( (mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_ECNR) == HeadsetClientHalConstants.PEER_FEAT_ECNR)) { if (NativeInterface.sendATCmdNative(getByteAddress(mCurrentDevice), if (mNativeInterface.sendATCmd(getByteAddress(mCurrentDevice), HeadsetClientHalConstants.HANDSFREECLIENT_AT_CMD_NREC, 1, 0, null)) { addQueuedAction(DISABLE_NREC); Loading Loading @@ -1083,20 +1088,20 @@ public class HeadsetClientStateMachine extends StateMachine { // already connected to this device, do nothing break; } NativeInterface.connectNative(getByteAddress(device)); mNativeInterface.connect(getByteAddress(device)); break; case DISCONNECT: BluetoothDevice dev = (BluetoothDevice) message.obj; if (!mCurrentDevice.equals(dev)) { break; } if (!NativeInterface.disconnectNative(getByteAddress(dev))) { if (!mNativeInterface.disconnect(getByteAddress(dev))) { Log.e(TAG, "disconnectNative failed for " + dev); } break; case CONNECT_AUDIO: if (!NativeInterface.connectAudioNative(getByteAddress(mCurrentDevice))) { if (!mNativeInterface.connectAudio(getByteAddress(mCurrentDevice))) { Log.e(TAG, "ERROR: Couldn't connect Audio for device " + mCurrentDevice); // No state transition is involved, fire broadcast immediately broadcastAudioState(mCurrentDevice, Loading @@ -1108,14 +1113,14 @@ public class HeadsetClientStateMachine extends StateMachine { break; case DISCONNECT_AUDIO: if (!NativeInterface.disconnectAudioNative(getByteAddress(mCurrentDevice))) { if (!mNativeInterface.disconnectAudio(getByteAddress(mCurrentDevice))) { Log.e(TAG, "ERROR: Couldn't disconnect Audio for device " + mCurrentDevice); } break; case VOICE_RECOGNITION_START: if (mVoiceRecognitionActive == HeadsetClientHalConstants.VR_STATE_STOPPED) { if (NativeInterface.startVoiceRecognitionNative( if (mNativeInterface.startVoiceRecognition( getByteAddress(mCurrentDevice))) { addQueuedAction(VOICE_RECOGNITION_START); } else { Loading @@ -1126,7 +1131,7 @@ public class HeadsetClientStateMachine extends StateMachine { case VOICE_RECOGNITION_STOP: if (mVoiceRecognitionActive == HeadsetClientHalConstants.VR_STATE_STARTED) { if (NativeInterface.stopVoiceRecognitionNative( if (mNativeInterface.stopVoiceRecognition( getByteAddress(mCurrentDevice))) { addQueuedAction(VOICE_RECOGNITION_STOP); } else { Loading @@ -1146,7 +1151,7 @@ public class HeadsetClientStateMachine extends StateMachine { logD("Volume" + amVol + ":" + mCommandedSpeakerVolume); // Volume was changed by a 3rd party mCommandedSpeakerVolume = -1; if (NativeInterface.setVolumeNative(getByteAddress(mCurrentDevice), if (mNativeInterface.setVolume(getByteAddress(mCurrentDevice), HeadsetClientHalConstants.VOLUME_TYPE_SPK, hfVol)) { addQueuedAction(SET_SPEAKER_VOLUME); } Loading @@ -1157,7 +1162,7 @@ public class HeadsetClientStateMachine extends StateMachine { BluetoothHeadsetClientCall c = (BluetoothHeadsetClientCall) message.obj; mCalls.put(HF_ORIGINATED_CALL_ID, c); if (NativeInterface.dialNative(getByteAddress(mCurrentDevice), c.getNumber())) { if (mNativeInterface.dial(getByteAddress(mCurrentDevice), c.getNumber())) { addQueuedAction(DIAL_NUMBER, c.getNumber()); // Start looping on calling current calls. sendMessage(QUERY_CURRENT_CALLS); Loading Loading @@ -1189,7 +1194,7 @@ public class HeadsetClientStateMachine extends StateMachine { explicitCallTransfer(); break; case SEND_DTMF: if (NativeInterface.sendDtmfNative(getByteAddress(mCurrentDevice), if (mNativeInterface.sendDtmf(getByteAddress(mCurrentDevice), (byte) message.arg1)) { addQueuedAction(SEND_DTMF); } else { Loading @@ -1197,7 +1202,7 @@ public class HeadsetClientStateMachine extends StateMachine { } break; case SUBSCRIBER_INFO: if (NativeInterface.retrieveSubscriberInfoNative( if (mNativeInterface.retrieveSubscriberInfo( getByteAddress(mCurrentDevice))) { addQueuedAction(SUBSCRIBER_INFO); } else { Loading Loading @@ -1248,7 +1253,7 @@ public class HeadsetClientStateMachine extends StateMachine { if (mIndicatorNetworkState == HeadsetClientHalConstants.NETWORK_STATE_AVAILABLE) { if (NativeInterface.queryCurrentOperatorNameNative( if (mNativeInterface.queryCurrentOperatorName( getByteAddress(mCurrentDevice))) { addQueuedAction(QUERY_OPERATOR_NAME); } else { Loading Loading @@ -1529,7 +1534,7 @@ public class HeadsetClientStateMachine extends StateMachine { * StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED, that triggers State * Machines state changing */ if (NativeInterface.disconnectAudioNative(getByteAddress(mCurrentDevice))) { if (mNativeInterface.disconnectAudio(getByteAddress(mCurrentDevice))) { routeHfpAudio(false); returnAudioFocusIfNecessary(); } Loading android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineFactory.java +7 −2 Original line number Diff line number Diff line Loading @@ -20,7 +20,12 @@ import android.os.HandlerThread; // Factory so that StateMachine objected can be mocked public class HeadsetClientStateMachineFactory { public HeadsetClientStateMachine make(HeadsetClientService context, HandlerThread t) { return HeadsetClientStateMachine.make(context, t.getLooper()); /** * Factory method to create state machine for headset client * */ public HeadsetClientStateMachine make(HeadsetClientService context, HandlerThread t, NativeInterface nativeInterface) { return HeadsetClientStateMachine.make(context, t.getLooper(), nativeInterface); } } android/app/src/com/android/bluetooth/hfpclient/NativeInterface.java +247 −21 Original line number Diff line number Diff line Loading @@ -24,7 +24,14 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.util.Log; class NativeInterface { import com.android.internal.annotations.VisibleForTesting; /** * Defines native calls that are used by state machine/service to either send or receive * messages to/from the native stack. This file is registered for the native methods in * corresponding CPP file. */ public class NativeInterface { private static final String TAG = "NativeInterface"; private static final boolean DBG = false; Loading @@ -32,46 +39,265 @@ class NativeInterface { classInitNative(); } NativeInterface() {} private NativeInterface() {} private static NativeInterface sInterface; private static final Object INSTANCE_LOCK = new Object(); /** * This class is a singleton because native library should only be loaded once * * @return default instance */ public static NativeInterface getInstance() { synchronized (INSTANCE_LOCK) { if (sInterface == null) { sInterface = new NativeInterface(); } } return sInterface; } // Native wrappers to help unit testing /** * Initialize native stack */ @VisibleForTesting public void initialize() { initializeNative(); } /** * Close and clean up native stack */ @VisibleForTesting public void cleanup() { cleanupNative(); } /** * Connect to the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean connect(byte[] address) { return connectNative(address); } /** * Disconnect from the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean disconnect(byte[] address) { return disconnectNative(address); } /** * Initiate audio connection to the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean connectAudio(byte[] address) { return connectAudioNative(address); } /** * Close audio connection from the specified paired device * * @param address target device's address * @return True on success, False on failure */ public boolean disconnectAudio(byte[] address) { return disconnectAudioNative(address); } /** * Initiate voice recognition to the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean startVoiceRecognition(byte[] address) { return startVoiceRecognitionNative(address); } /** * Close voice recognition to the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean stopVoiceRecognition(byte[] address) { return stopVoiceRecognitionNative(address); } /** * Set volume to the specified paired device * * @param volumeType type of volume as in * HeadsetClientHalConstants.VOLUME_TYPE_xxxx * @param volume volume level * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean setVolume(byte[] address, int volumeType, int volume) { return setVolumeNative(address, volumeType, volume); } /** * dial number from the specified paired device * * @param number phone number to be dialed * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean dial(byte[] address, String number) { return dialNative(address, number); } /** * Memory dialing from the specified paired device * * @param location memory location * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean dialMemory(byte[] address, int location) { return dialMemoryNative(address, location); } /** * Apply action to call * * @action action (e.g. hold, terminate etc) * @index call index * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean handleCallAction(byte[] address, int action, int index) { return handleCallActionNative(address, action, index); } /** * Query current call status from the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean queryCurrentCalls(byte[] address) { return queryCurrentCallsNative(address); } /** * Query operator name from the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean queryCurrentOperatorName(byte[] address) { return queryCurrentOperatorNameNative(address); } /** * Retrieve subscriber number from the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean retrieveSubscriberInfo(byte[] address) { return retrieveSubscriberInfoNative(address); } /** * Transmit DTMF code * * @param code DTMF code * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean sendDtmf(byte[] address, byte code) { return sendDtmfNative(address, code); } /** * Request last voice tag * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean requestLastVoiceTagNumber(byte[] address) { return requestLastVoiceTagNumberNative(address); } /** * Send an AT command * * @param atCmd command code * @param val1 command specific argurment1 * @param val2 command specific argurment2 * @param arg other command specific argurments * @return True on success, False on failure */ @VisibleForTesting public boolean sendATCmd(byte[] address, int atCmd, int val1, int val2, String arg) { return sendATCmdNative(address, atCmd, val1, val2, arg); } // Native methods that call into the JNI interface static native void classInitNative(); private static native void classInitNative(); native void initializeNative(); private native void initializeNative(); native void cleanupNative(); private native void cleanupNative(); static native boolean connectNative(byte[] address); private static native boolean connectNative(byte[] address); static native boolean disconnectNative(byte[] address); private static native boolean disconnectNative(byte[] address); static native boolean connectAudioNative(byte[] address); private static native boolean connectAudioNative(byte[] address); static native boolean disconnectAudioNative(byte[] address); private static native boolean disconnectAudioNative(byte[] address); static native boolean startVoiceRecognitionNative(byte[] address); private static native boolean startVoiceRecognitionNative(byte[] address); static native boolean stopVoiceRecognitionNative(byte[] address); private static native boolean stopVoiceRecognitionNative(byte[] address); static native boolean setVolumeNative(byte[] address, int volumeType, int volume); private static native boolean setVolumeNative(byte[] address, int volumeType, int volume); static native boolean dialNative(byte[] address, String number); private static native boolean dialNative(byte[] address, String number); static native boolean dialMemoryNative(byte[] address, int location); private static native boolean dialMemoryNative(byte[] address, int location); static native boolean handleCallActionNative(byte[] address, int action, int index); private static native boolean handleCallActionNative(byte[] address, int action, int index); static native boolean queryCurrentCallsNative(byte[] address); private static native boolean queryCurrentCallsNative(byte[] address); static native boolean queryCurrentOperatorNameNative(byte[] address); private static native boolean queryCurrentOperatorNameNative(byte[] address); static native boolean retrieveSubscriberInfoNative(byte[] address); private static native boolean retrieveSubscriberInfoNative(byte[] address); static native boolean sendDtmfNative(byte[] address, byte code); private static native boolean sendDtmfNative(byte[] address, byte code); static native boolean requestLastVoiceTagNumberNative(byte[] address); private static native boolean requestLastVoiceTagNumberNative(byte[] address); static native boolean sendATCmdNative(byte[] address, int atCmd, int val1, int val2, private static native boolean sendATCmdNative(byte[] address, int atCmd, int val1, int val2, String arg); private BluetoothDevice getDevice(byte[] address) { Loading android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java +5 −1 Original line number Diff line number Diff line Loading @@ -53,6 +53,8 @@ public class HeadsetClientStateMachineTest { @Mock private AudioManager mAudioManager; private NativeInterface mNativeInterface; private static final int STANDARD_WAIT_MILLIS = 1000; private static final int QUERY_CURRENT_CALLS_WAIT_MILLIS = 2000; private static final int QUERY_CURRENT_CALLS_TEST_WAIT_MILLIS = QUERY_CURRENT_CALLS_WAIT_MILLIS Loading @@ -73,6 +75,7 @@ public class HeadsetClientStateMachineTest { mAudioManager); when(mHeadsetClientService.getResources()).thenReturn(mMockHfpResources); when(mMockHfpResources.getBoolean(R.bool.hfp_clcc_poll_during_call)).thenReturn(true); mNativeInterface = spy(NativeInterface.getInstance()); // This line must be called to make sure relevant objects are initialized properly mAdapter = BluetoothAdapter.getDefaultAdapter(); Loading @@ -84,7 +87,8 @@ public class HeadsetClientStateMachineTest { mHandlerThread.start(); // Manage looper execution in main test thread explicitly to guarantee timing consistency mHeadsetClientStateMachine = new HeadsetClientStateMachine(mHeadsetClientService, mHandlerThread.getLooper()); new HeadsetClientStateMachine(mHeadsetClientService, mHandlerThread.getLooper(), mNativeInterface); mHeadsetClientStateMachine.start(); } Loading Loading
android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java +4 −4 Original line number Diff line number Diff line Loading @@ -80,8 +80,8 @@ public class HeadsetClientService extends ProfileService { } // Setup the JNI service mNativeInterface = new NativeInterface(); mNativeInterface.initializeNative(); mNativeInterface = NativeInterface.getInstance(); mNativeInterface.initialize(); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); if (mAudioManager == null) { Loading Loading @@ -137,7 +137,7 @@ public class HeadsetClientService extends ProfileService { mSmThread.quit(); mSmThread = null; mNativeInterface.cleanupNative(); mNativeInterface.cleanup(); mNativeInterface = null; return true; Loading Loading @@ -885,7 +885,7 @@ public class HeadsetClientService extends ProfileService { // Allocate a new SM Log.d(TAG, "Creating a new state machine"); sm = mSmFactory.make(this, mSmThread); sm = mSmFactory.make(this, mSmThread, mNativeInterface); mStateMachineMap.put(device, sm); return sm; } Loading
android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java +34 −29 Original line number Diff line number Diff line Loading @@ -174,7 +174,8 @@ public class HeadsetClientStateMachine extends StateMachine { // This is returned when requesting focus from AudioManager private AudioFocusRequest mAudioFocusRequest; private AudioManager mAudioManager; private final AudioManager mAudioManager; private final NativeInterface mNativeInterface; // Accessor for the states, useful for reusing the state machines public IState getDisconnectedState() { Loading Loading @@ -264,7 +265,7 @@ public class HeadsetClientStateMachine extends StateMachine { private boolean queryCallsStart() { logD("queryCallsStart"); clearPendingAction(); NativeInterface.queryCurrentCallsNative(getByteAddress(mCurrentDevice)); mNativeInterface.queryCurrentCalls(getByteAddress(mCurrentDevice)); addQueuedAction(QUERY_CURRENT_CALLS, 0); return true; } Loading Loading @@ -487,7 +488,7 @@ public class HeadsetClientStateMachine extends StateMachine { routeHfpAudio(true); } if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), action, 0)) { if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) { addQueuedAction(ACCEPT_CALL, action); } else { Log.e(TAG, "ERROR: Couldn't accept a call, action:" + action); Loading Loading @@ -526,8 +527,8 @@ public class HeadsetClientStateMachine extends StateMachine { return; } if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) { logD("Reject call action " + action); if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), action, 0)) { addQueuedAction(REJECT_CALL, action); } else { Log.e(TAG, "ERROR: Couldn't reject a call, action:" + action); Loading @@ -551,7 +552,7 @@ public class HeadsetClientStateMachine extends StateMachine { action = HeadsetClientHalConstants.CALL_ACTION_CHLD_2; } if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), action, 0)) { if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) { addQueuedAction(HOLD_CALL, action); } else { Log.e(TAG, "ERROR: Couldn't hold a call, action:" + action); Loading @@ -572,7 +573,7 @@ public class HeadsetClientStateMachine extends StateMachine { action = HeadsetClientHalConstants.CALL_ACTION_CHLD_0; } if (c != null) { if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), action, 0)) { if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) { addQueuedAction(TERMINATE_CALL, action); } else { Log.e(TAG, "ERROR: Couldn't terminate outgoing call"); Loading @@ -590,7 +591,7 @@ public class HeadsetClientStateMachine extends StateMachine { return; } if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), HeadsetClientHalConstants.CALL_ACTION_CHLD_2X, idx)) { addQueuedAction(ENTER_PRIVATE_MODE, c); } else { Loading @@ -606,7 +607,7 @@ public class HeadsetClientStateMachine extends StateMachine { return; } if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), HeadsetClientHalConstants.CALL_ACTION_CHLD_4, -1)) { addQueuedAction(EXPLICIT_CALL_TRANSFER); } else { Loading Loading @@ -659,9 +660,11 @@ public class HeadsetClientStateMachine extends StateMachine { return b; } HeadsetClientStateMachine(HeadsetClientService context, Looper looper) { HeadsetClientStateMachine(HeadsetClientService context, Looper looper, NativeInterface nativeInterface) { super(TAG, looper); mService = context; mNativeInterface = nativeInterface; mAudioManager = mService.getAudioManager(); mAdapter = BluetoothAdapter.getDefaultAdapter(); Loading Loading @@ -699,9 +702,11 @@ public class HeadsetClientStateMachine extends StateMachine { setInitialState(mDisconnected); } static HeadsetClientStateMachine make(HeadsetClientService context, Looper l) { static HeadsetClientStateMachine make(HeadsetClientService context, Looper looper, NativeInterface nativeInterface) { logD("make"); HeadsetClientStateMachine hfcsm = new HeadsetClientStateMachine(context, l); HeadsetClientStateMachine hfcsm = new HeadsetClientStateMachine(context, looper, nativeInterface); hfcsm.start(); return hfcsm; } Loading Loading @@ -739,7 +744,7 @@ public class HeadsetClientStateMachine extends StateMachine { public void doQuit() { logD("doQuit"); if (mCurrentDevice != null) { NativeInterface.disconnectNative(getByteAddress(mCurrentDevice)); mNativeInterface.disconnect(getByteAddress(mCurrentDevice)); } routeHfpAudio(false); returnAudioFocusIfNecessary(); Loading Loading @@ -825,7 +830,7 @@ public class HeadsetClientStateMachine extends StateMachine { switch (message.what) { case CONNECT: BluetoothDevice device = (BluetoothDevice) message.obj; if (!NativeInterface.connectNative(getByteAddress(device))) { if (!mNativeInterface.connect(getByteAddress(device))) { // No state transition is involved, fire broadcast immediately broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_DISCONNECTED); Loading Loading @@ -871,7 +876,7 @@ public class HeadsetClientStateMachine extends StateMachine { + " bondState=" + device.getBondState()); // reject the connection and stay in Disconnected state // itself NativeInterface.disconnectNative(getByteAddress(device)); mNativeInterface.disconnect(getByteAddress(device)); // the other profile connection should be initiated AdapterService adapterService = AdapterService.getAdapterService(); // No state transition is involved, fire broadcast immediately Loading Loading @@ -985,7 +990,7 @@ public class HeadsetClientStateMachine extends StateMachine { // We do not support devices which do not support enhanced call status (ECS). if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_ECS) == 0) { NativeInterface.disconnectNative(getByteAddress(device)); mNativeInterface.disconnect(getByteAddress(device)); return; } Loading @@ -993,7 +998,7 @@ public class HeadsetClientStateMachine extends StateMachine { if (HeadsetClientHalConstants.HANDSFREECLIENT_NREC_SUPPORTED && ( (mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_ECNR) == HeadsetClientHalConstants.PEER_FEAT_ECNR)) { if (NativeInterface.sendATCmdNative(getByteAddress(mCurrentDevice), if (mNativeInterface.sendATCmd(getByteAddress(mCurrentDevice), HeadsetClientHalConstants.HANDSFREECLIENT_AT_CMD_NREC, 1, 0, null)) { addQueuedAction(DISABLE_NREC); Loading Loading @@ -1083,20 +1088,20 @@ public class HeadsetClientStateMachine extends StateMachine { // already connected to this device, do nothing break; } NativeInterface.connectNative(getByteAddress(device)); mNativeInterface.connect(getByteAddress(device)); break; case DISCONNECT: BluetoothDevice dev = (BluetoothDevice) message.obj; if (!mCurrentDevice.equals(dev)) { break; } if (!NativeInterface.disconnectNative(getByteAddress(dev))) { if (!mNativeInterface.disconnect(getByteAddress(dev))) { Log.e(TAG, "disconnectNative failed for " + dev); } break; case CONNECT_AUDIO: if (!NativeInterface.connectAudioNative(getByteAddress(mCurrentDevice))) { if (!mNativeInterface.connectAudio(getByteAddress(mCurrentDevice))) { Log.e(TAG, "ERROR: Couldn't connect Audio for device " + mCurrentDevice); // No state transition is involved, fire broadcast immediately broadcastAudioState(mCurrentDevice, Loading @@ -1108,14 +1113,14 @@ public class HeadsetClientStateMachine extends StateMachine { break; case DISCONNECT_AUDIO: if (!NativeInterface.disconnectAudioNative(getByteAddress(mCurrentDevice))) { if (!mNativeInterface.disconnectAudio(getByteAddress(mCurrentDevice))) { Log.e(TAG, "ERROR: Couldn't disconnect Audio for device " + mCurrentDevice); } break; case VOICE_RECOGNITION_START: if (mVoiceRecognitionActive == HeadsetClientHalConstants.VR_STATE_STOPPED) { if (NativeInterface.startVoiceRecognitionNative( if (mNativeInterface.startVoiceRecognition( getByteAddress(mCurrentDevice))) { addQueuedAction(VOICE_RECOGNITION_START); } else { Loading @@ -1126,7 +1131,7 @@ public class HeadsetClientStateMachine extends StateMachine { case VOICE_RECOGNITION_STOP: if (mVoiceRecognitionActive == HeadsetClientHalConstants.VR_STATE_STARTED) { if (NativeInterface.stopVoiceRecognitionNative( if (mNativeInterface.stopVoiceRecognition( getByteAddress(mCurrentDevice))) { addQueuedAction(VOICE_RECOGNITION_STOP); } else { Loading @@ -1146,7 +1151,7 @@ public class HeadsetClientStateMachine extends StateMachine { logD("Volume" + amVol + ":" + mCommandedSpeakerVolume); // Volume was changed by a 3rd party mCommandedSpeakerVolume = -1; if (NativeInterface.setVolumeNative(getByteAddress(mCurrentDevice), if (mNativeInterface.setVolume(getByteAddress(mCurrentDevice), HeadsetClientHalConstants.VOLUME_TYPE_SPK, hfVol)) { addQueuedAction(SET_SPEAKER_VOLUME); } Loading @@ -1157,7 +1162,7 @@ public class HeadsetClientStateMachine extends StateMachine { BluetoothHeadsetClientCall c = (BluetoothHeadsetClientCall) message.obj; mCalls.put(HF_ORIGINATED_CALL_ID, c); if (NativeInterface.dialNative(getByteAddress(mCurrentDevice), c.getNumber())) { if (mNativeInterface.dial(getByteAddress(mCurrentDevice), c.getNumber())) { addQueuedAction(DIAL_NUMBER, c.getNumber()); // Start looping on calling current calls. sendMessage(QUERY_CURRENT_CALLS); Loading Loading @@ -1189,7 +1194,7 @@ public class HeadsetClientStateMachine extends StateMachine { explicitCallTransfer(); break; case SEND_DTMF: if (NativeInterface.sendDtmfNative(getByteAddress(mCurrentDevice), if (mNativeInterface.sendDtmf(getByteAddress(mCurrentDevice), (byte) message.arg1)) { addQueuedAction(SEND_DTMF); } else { Loading @@ -1197,7 +1202,7 @@ public class HeadsetClientStateMachine extends StateMachine { } break; case SUBSCRIBER_INFO: if (NativeInterface.retrieveSubscriberInfoNative( if (mNativeInterface.retrieveSubscriberInfo( getByteAddress(mCurrentDevice))) { addQueuedAction(SUBSCRIBER_INFO); } else { Loading Loading @@ -1248,7 +1253,7 @@ public class HeadsetClientStateMachine extends StateMachine { if (mIndicatorNetworkState == HeadsetClientHalConstants.NETWORK_STATE_AVAILABLE) { if (NativeInterface.queryCurrentOperatorNameNative( if (mNativeInterface.queryCurrentOperatorName( getByteAddress(mCurrentDevice))) { addQueuedAction(QUERY_OPERATOR_NAME); } else { Loading Loading @@ -1529,7 +1534,7 @@ public class HeadsetClientStateMachine extends StateMachine { * StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED, that triggers State * Machines state changing */ if (NativeInterface.disconnectAudioNative(getByteAddress(mCurrentDevice))) { if (mNativeInterface.disconnectAudio(getByteAddress(mCurrentDevice))) { routeHfpAudio(false); returnAudioFocusIfNecessary(); } Loading
android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineFactory.java +7 −2 Original line number Diff line number Diff line Loading @@ -20,7 +20,12 @@ import android.os.HandlerThread; // Factory so that StateMachine objected can be mocked public class HeadsetClientStateMachineFactory { public HeadsetClientStateMachine make(HeadsetClientService context, HandlerThread t) { return HeadsetClientStateMachine.make(context, t.getLooper()); /** * Factory method to create state machine for headset client * */ public HeadsetClientStateMachine make(HeadsetClientService context, HandlerThread t, NativeInterface nativeInterface) { return HeadsetClientStateMachine.make(context, t.getLooper(), nativeInterface); } }
android/app/src/com/android/bluetooth/hfpclient/NativeInterface.java +247 −21 Original line number Diff line number Diff line Loading @@ -24,7 +24,14 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.util.Log; class NativeInterface { import com.android.internal.annotations.VisibleForTesting; /** * Defines native calls that are used by state machine/service to either send or receive * messages to/from the native stack. This file is registered for the native methods in * corresponding CPP file. */ public class NativeInterface { private static final String TAG = "NativeInterface"; private static final boolean DBG = false; Loading @@ -32,46 +39,265 @@ class NativeInterface { classInitNative(); } NativeInterface() {} private NativeInterface() {} private static NativeInterface sInterface; private static final Object INSTANCE_LOCK = new Object(); /** * This class is a singleton because native library should only be loaded once * * @return default instance */ public static NativeInterface getInstance() { synchronized (INSTANCE_LOCK) { if (sInterface == null) { sInterface = new NativeInterface(); } } return sInterface; } // Native wrappers to help unit testing /** * Initialize native stack */ @VisibleForTesting public void initialize() { initializeNative(); } /** * Close and clean up native stack */ @VisibleForTesting public void cleanup() { cleanupNative(); } /** * Connect to the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean connect(byte[] address) { return connectNative(address); } /** * Disconnect from the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean disconnect(byte[] address) { return disconnectNative(address); } /** * Initiate audio connection to the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean connectAudio(byte[] address) { return connectAudioNative(address); } /** * Close audio connection from the specified paired device * * @param address target device's address * @return True on success, False on failure */ public boolean disconnectAudio(byte[] address) { return disconnectAudioNative(address); } /** * Initiate voice recognition to the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean startVoiceRecognition(byte[] address) { return startVoiceRecognitionNative(address); } /** * Close voice recognition to the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean stopVoiceRecognition(byte[] address) { return stopVoiceRecognitionNative(address); } /** * Set volume to the specified paired device * * @param volumeType type of volume as in * HeadsetClientHalConstants.VOLUME_TYPE_xxxx * @param volume volume level * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean setVolume(byte[] address, int volumeType, int volume) { return setVolumeNative(address, volumeType, volume); } /** * dial number from the specified paired device * * @param number phone number to be dialed * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean dial(byte[] address, String number) { return dialNative(address, number); } /** * Memory dialing from the specified paired device * * @param location memory location * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean dialMemory(byte[] address, int location) { return dialMemoryNative(address, location); } /** * Apply action to call * * @action action (e.g. hold, terminate etc) * @index call index * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean handleCallAction(byte[] address, int action, int index) { return handleCallActionNative(address, action, index); } /** * Query current call status from the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean queryCurrentCalls(byte[] address) { return queryCurrentCallsNative(address); } /** * Query operator name from the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean queryCurrentOperatorName(byte[] address) { return queryCurrentOperatorNameNative(address); } /** * Retrieve subscriber number from the specified paired device * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean retrieveSubscriberInfo(byte[] address) { return retrieveSubscriberInfoNative(address); } /** * Transmit DTMF code * * @param code DTMF code * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean sendDtmf(byte[] address, byte code) { return sendDtmfNative(address, code); } /** * Request last voice tag * * @param address target device's address * @return True on success, False on failure */ @VisibleForTesting public boolean requestLastVoiceTagNumber(byte[] address) { return requestLastVoiceTagNumberNative(address); } /** * Send an AT command * * @param atCmd command code * @param val1 command specific argurment1 * @param val2 command specific argurment2 * @param arg other command specific argurments * @return True on success, False on failure */ @VisibleForTesting public boolean sendATCmd(byte[] address, int atCmd, int val1, int val2, String arg) { return sendATCmdNative(address, atCmd, val1, val2, arg); } // Native methods that call into the JNI interface static native void classInitNative(); private static native void classInitNative(); native void initializeNative(); private native void initializeNative(); native void cleanupNative(); private native void cleanupNative(); static native boolean connectNative(byte[] address); private static native boolean connectNative(byte[] address); static native boolean disconnectNative(byte[] address); private static native boolean disconnectNative(byte[] address); static native boolean connectAudioNative(byte[] address); private static native boolean connectAudioNative(byte[] address); static native boolean disconnectAudioNative(byte[] address); private static native boolean disconnectAudioNative(byte[] address); static native boolean startVoiceRecognitionNative(byte[] address); private static native boolean startVoiceRecognitionNative(byte[] address); static native boolean stopVoiceRecognitionNative(byte[] address); private static native boolean stopVoiceRecognitionNative(byte[] address); static native boolean setVolumeNative(byte[] address, int volumeType, int volume); private static native boolean setVolumeNative(byte[] address, int volumeType, int volume); static native boolean dialNative(byte[] address, String number); private static native boolean dialNative(byte[] address, String number); static native boolean dialMemoryNative(byte[] address, int location); private static native boolean dialMemoryNative(byte[] address, int location); static native boolean handleCallActionNative(byte[] address, int action, int index); private static native boolean handleCallActionNative(byte[] address, int action, int index); static native boolean queryCurrentCallsNative(byte[] address); private static native boolean queryCurrentCallsNative(byte[] address); static native boolean queryCurrentOperatorNameNative(byte[] address); private static native boolean queryCurrentOperatorNameNative(byte[] address); static native boolean retrieveSubscriberInfoNative(byte[] address); private static native boolean retrieveSubscriberInfoNative(byte[] address); static native boolean sendDtmfNative(byte[] address, byte code); private static native boolean sendDtmfNative(byte[] address, byte code); static native boolean requestLastVoiceTagNumberNative(byte[] address); private static native boolean requestLastVoiceTagNumberNative(byte[] address); static native boolean sendATCmdNative(byte[] address, int atCmd, int val1, int val2, private static native boolean sendATCmdNative(byte[] address, int atCmd, int val1, int val2, String arg); private BluetoothDevice getDevice(byte[] address) { Loading
android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java +5 −1 Original line number Diff line number Diff line Loading @@ -53,6 +53,8 @@ public class HeadsetClientStateMachineTest { @Mock private AudioManager mAudioManager; private NativeInterface mNativeInterface; private static final int STANDARD_WAIT_MILLIS = 1000; private static final int QUERY_CURRENT_CALLS_WAIT_MILLIS = 2000; private static final int QUERY_CURRENT_CALLS_TEST_WAIT_MILLIS = QUERY_CURRENT_CALLS_WAIT_MILLIS Loading @@ -73,6 +75,7 @@ public class HeadsetClientStateMachineTest { mAudioManager); when(mHeadsetClientService.getResources()).thenReturn(mMockHfpResources); when(mMockHfpResources.getBoolean(R.bool.hfp_clcc_poll_during_call)).thenReturn(true); mNativeInterface = spy(NativeInterface.getInstance()); // This line must be called to make sure relevant objects are initialized properly mAdapter = BluetoothAdapter.getDefaultAdapter(); Loading @@ -84,7 +87,8 @@ public class HeadsetClientStateMachineTest { mHandlerThread.start(); // Manage looper execution in main test thread explicitly to guarantee timing consistency mHeadsetClientStateMachine = new HeadsetClientStateMachine(mHeadsetClientService, mHandlerThread.getLooper()); new HeadsetClientStateMachine(mHeadsetClientService, mHandlerThread.getLooper(), mNativeInterface); mHeadsetClientStateMachine.start(); } Loading