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

Commit 70f695c4 authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge "Fix bugs uncovered by HeadsetClientStateMachineTest"

parents 13f4a2f2 14dde097
Loading
Loading
Loading
Loading
+59 −50
Original line number Diff line number Diff line
@@ -47,7 +47,7 @@ import android.os.Looper;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.SystemClock;
import android.telecom.TelecomManager;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.Pair;

@@ -104,14 +104,14 @@ public class HeadsetClientStateMachine extends StateMachine {
    static final int TERMINATE_SPECIFIC_CALL = 53;

    // Timeouts.
    @VisibleForTesting
    static final int CONNECTING_TIMEOUT_MS = 10000;  // 10s
    static final int ROUTING_DELAY_MS = 250;
    static final int SCO_DISCONNECT_TIMEOUT_MS = 750;
    private static final int ROUTING_DELAY_MS = 250;

    static final int MAX_HFP_SCO_VOICE_CALL_VOLUME = 15; // HFP 1.5 spec.
    static final int MIN_HFP_SCO_VOICE_CALL_VOLUME = 1; // HFP 1.5 spec.
    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.

    public static final Integer HF_ORIGINATED_CALL_ID = new Integer(-1);
    static final int HF_ORIGINATED_CALL_ID = -1;
    private static final long OUTGOING_TIMEOUT_MILLI = 10 * 1000; // 10 seconds
    private static final long QUERY_CURRENT_CALLS_WAIT_MILLIS = 2 * 1000; // 2 seconds

@@ -122,6 +122,7 @@ public class HeadsetClientStateMachine extends StateMachine {
    private final Connecting mConnecting;
    private final Connected mConnected;
    private final AudioOn mAudioOn;
    private State mPrevState;
    private long mClccTimer = 0;

    private final HeadsetClientService mService;
@@ -156,7 +157,6 @@ public class HeadsetClientStateMachine extends StateMachine {
    private boolean mAudioWbs;
    private int mVoiceRecognitionActive;
    private final BluetoothAdapter mAdapter;
    private TelecomManager mTelecomManager;

    // currently connected device
    private BluetoothDevice mCurrentDevice = null;
@@ -666,7 +666,7 @@ public class HeadsetClientStateMachine extends StateMachine {
        return b;
    }

    protected HeadsetClientStateMachine(HeadsetClientService context, Looper looper) {
    HeadsetClientStateMachine(HeadsetClientService context, Looper looper) {
        super(TAG, looper);
        mService = context;

@@ -680,8 +680,6 @@ public class HeadsetClientStateMachine extends StateMachine {
        mAudioWbs = false;
        mVoiceRecognitionActive = HeadsetClientHalConstants.VR_STATE_STOPPED;

        mTelecomManager = (TelecomManager) context.getSystemService(context.TELECOM_SERVICE);

        mIndicatorNetworkState = HeadsetClientHalConstants.NETWORK_STATE_NOT_AVAILABLE;
        mIndicatorNetworkType = HeadsetClientHalConstants.SERVICE_TYPE_HOME;
        mIndicatorNetworkSignal = 0;
@@ -793,6 +791,17 @@ public class HeadsetClientStateMachine extends StateMachine {
            mChldFeatures = 0;

            removeMessages(QUERY_CURRENT_CALLS);

            if (mPrevState == mConnecting) {
                broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED,
                        BluetoothProfile.STATE_CONNECTING);
            } else if (mPrevState == mConnected || mPrevState == mAudioOn) {
                broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED,
                        BluetoothProfile.STATE_CONNECTED);
            } else if (mPrevState != null) { // null is the default state before Disconnected
                Log.e(TAG, "Connected: Illegal state transition from " + mPrevState.getName()
                        + " to Connecting, mCurrentDevice=" + mCurrentDevice);
            }
        }

        @Override
@@ -807,17 +816,13 @@ public class HeadsetClientStateMachine extends StateMachine {
            switch (message.what) {
                case CONNECT:
                    BluetoothDevice device = (BluetoothDevice) message.obj;
                    broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING,
                            BluetoothProfile.STATE_DISCONNECTED);

                    if (!NativeInterface.connectNative(getByteAddress(device))) {
                        // No state transition is involved, fire broadcast immediately
                        broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
                                BluetoothProfile.STATE_CONNECTING);
                                BluetoothProfile.STATE_DISCONNECTED);
                        break;
                    }

                    mCurrentDevice = device;

                    transitionTo(mConnecting);
                    break;
                case DISCONNECT:
@@ -854,8 +859,6 @@ public class HeadsetClientStateMachine extends StateMachine {
                    Log.w(TAG, "HFPClient Connecting from Disconnected state");
                    if (okToConnect(device)) {
                        Log.i(TAG, "Incoming AG accepted");
                        broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING,
                                BluetoothProfile.STATE_DISCONNECTED);
                        mCurrentDevice = device;
                        transitionTo(mConnecting);
                    } else {
@@ -866,6 +869,7 @@ public class HeadsetClientStateMachine extends StateMachine {
                        NativeInterface.disconnectNative(getByteAddress(device));
                        // the other profile connection should be initiated
                        AdapterService adapterService = AdapterService.getAdapterService();
                        // No state transition is involved, fire broadcast immediately
                        broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
                                BluetoothProfile.STATE_DISCONNECTED);
                    }
@@ -884,6 +888,7 @@ public class HeadsetClientStateMachine extends StateMachine {
            if (DBG) {
                Log.d(TAG, "Exit Disconnected: " + getCurrentMessage().what);
            }
            mPrevState = this;
        }
    }

@@ -897,6 +902,14 @@ public class HeadsetClientStateMachine extends StateMachine {
            // removed in exit. It is safe to send a CONNECTING_TIMEOUT here since
            // the only transition is when connection attempt is initiated.
            sendMessageDelayed(CONNECTING_TIMEOUT, CONNECTING_TIMEOUT_MS);
            if (mPrevState == mDisconnected) {
                broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTING,
                        BluetoothProfile.STATE_DISCONNECTED);
            } else {
                String prevStateName = mPrevState == null ? "null" : mPrevState.getName();
                Log.e(TAG, "Connected: Illegal state transition from " + prevStateName
                        + " to Connecting, mCurrentDevice=" + mCurrentDevice);
            }
        }

        @Override
@@ -953,8 +966,6 @@ public class HeadsetClientStateMachine extends StateMachine {
                    // We timed out trying to connect, transition to disconnected.
                    Log.w(TAG, "Connection timeout for " + mCurrentDevice);
                    transitionTo(mDisconnected);
                    broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED,
                            BluetoothProfile.STATE_CONNECTING);
                    break;

                default:
@@ -969,8 +980,6 @@ public class HeadsetClientStateMachine extends StateMachine {
                BluetoothDevice device) {
            switch (state) {
                case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED:
                    broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED,
                            BluetoothProfile.STATE_CONNECTING);
                    transitionTo(mDisconnected);
                    break;

@@ -986,9 +995,6 @@ public class HeadsetClientStateMachine extends StateMachine {
                        return;
                    }

                    broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTED,
                            BluetoothProfile.STATE_CONNECTING);

                    // Send AT+NREC to remote if supported by audio
                    if (HeadsetClientHalConstants.HANDSFREECLIENT_NREC_SUPPORTED && (
                            (mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_ECNR)
@@ -1001,24 +1007,23 @@ public class HeadsetClientStateMachine extends StateMachine {
                            Log.e(TAG, "Failed to send NREC");
                        }
                    }
                    transitionTo(mConnected);

                    int amVol = sAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL);
                    sendMessage(
                    deferMessage(
                            obtainMessage(HeadsetClientStateMachine.SET_SPEAKER_VOLUME, amVol, 0));
                    // Mic is either in ON state (full volume) or OFF state. There is no way in
                    // Android to change the MIC volume.
                    sendMessage(obtainMessage(HeadsetClientStateMachine.SET_MIC_VOLUME,
                    deferMessage(obtainMessage(HeadsetClientStateMachine.SET_MIC_VOLUME,
                            sAudioManager.isMicrophoneMute() ? 0 : 15, 0));

                    // query subscriber info
                    sendMessage(HeadsetClientStateMachine.SUBSCRIBER_INFO);
                    deferMessage(obtainMessage(HeadsetClientStateMachine.SUBSCRIBER_INFO));
                    transitionTo(mConnected);
                    break;

                case HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED:
                    if (!mCurrentDevice.equals(device)) {
                        Log.w(TAG, "incoming connection event, device: " + device);

                        // No state transition is involved, fire broadcast immediately
                        broadcastConnectionState(mCurrentDevice,
                                BluetoothProfile.STATE_DISCONNECTED,
                                BluetoothProfile.STATE_CONNECTING);
@@ -1047,6 +1052,7 @@ public class HeadsetClientStateMachine extends StateMachine {
                Log.d(TAG, "Exit Connecting: " + getCurrentMessage().what);
            }
            removeMessages(CONNECTING_TIMEOUT);
            mPrevState = this;
        }
    }

@@ -1060,6 +1066,14 @@ public class HeadsetClientStateMachine extends StateMachine {
            }
            mAudioWbs = false;
            mCommandedSpeakerVolume = -1;
            if (mPrevState == mConnecting) {
                broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTED,
                        BluetoothProfile.STATE_CONNECTING);
            } else if (mPrevState != mAudioOn) {
                String prevStateName = mPrevState == null ? "null" : mPrevState.getName();
                Log.e(TAG, "Connected: Illegal state transition from " + prevStateName
                        + " to Connecting, mCurrentDevice=" + mCurrentDevice);
            }
        }

        @Override
@@ -1081,7 +1095,6 @@ public class HeadsetClientStateMachine extends StateMachine {
                        // already connected to this device, do nothing
                        break;
                    }

                    NativeInterface.connectNative(getByteAddress(device));
                    break;
                case DISCONNECT:
@@ -1089,19 +1102,19 @@ public class HeadsetClientStateMachine extends StateMachine {
                    if (!mCurrentDevice.equals(dev)) {
                        break;
                    }
                    if (NativeInterface.disconnectNative(getByteAddress(dev))) {
                        // No state transition is involved, fire broadcast immediately
                        broadcastConnectionState(dev, BluetoothProfile.STATE_DISCONNECTING,
                                BluetoothProfile.STATE_CONNECTED);
                    if (!NativeInterface.disconnectNative(getByteAddress(dev))) {
                        // disconnecting failed
                        broadcastConnectionState(dev, BluetoothProfile.STATE_CONNECTED,
                                BluetoothProfile.STATE_DISCONNECTING);
                        break;
                    } else {
                        Log.e(TAG, "disconnectNative failed for " + dev);
                    }
                    break;

                case CONNECT_AUDIO:
                    if (!NativeInterface.connectAudioNative(getByteAddress(mCurrentDevice))) {
                        Log.e(TAG, "ERROR: Couldn't connect Audio for device " + mCurrentDevice);
                        // No state transition is involved, fire broadcast immediately
                        broadcastAudioState(mCurrentDevice,
                                BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED,
                                BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED);
@@ -1417,9 +1430,6 @@ public class HeadsetClientStateMachine extends StateMachine {
                    }
                    // AG disconnects
                    if (mCurrentDevice.equals(device)) {
                        broadcastConnectionState(mCurrentDevice,
                                BluetoothProfile.STATE_DISCONNECTED,
                                BluetoothProfile.STATE_CONNECTED);
                        transitionTo(mDisconnected);
                    } else {
                        Log.e(TAG, "Disconnected from unknown device: " + device);
@@ -1491,12 +1501,14 @@ public class HeadsetClientStateMachine extends StateMachine {
                    break;

                case HeadsetClientHalConstants.AUDIO_STATE_CONNECTING:
                    // No state transition is involved, fire broadcast immediately
                    broadcastAudioState(device, BluetoothHeadsetClient.STATE_AUDIO_CONNECTING,
                            mAudioState);
                    mAudioState = BluetoothHeadsetClient.STATE_AUDIO_CONNECTING;
                    break;

                case HeadsetClientHalConstants.AUDIO_STATE_DISCONNECTED:
                    // No state transition is involved, fire broadcast immediately
                    broadcastAudioState(device, BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED,
                            mAudioState);
                    mAudioState = BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED;
@@ -1513,6 +1525,7 @@ public class HeadsetClientStateMachine extends StateMachine {
            if (DBG) {
                Log.d(TAG, "Exit Connected: " + getCurrentMessage().what);
            }
            mPrevState = this;
        }
    }

@@ -1601,9 +1614,6 @@ public class HeadsetClientStateMachine extends StateMachine {
                    if (mCurrentDevice.equals(device)) {
                        processAudioEvent(HeadsetClientHalConstants.AUDIO_STATE_DISCONNECTED,
                                device);
                        broadcastConnectionState(mCurrentDevice,
                                BluetoothProfile.STATE_DISCONNECTED,
                                BluetoothProfile.STATE_CONNECTED);
                        transitionTo(mDisconnected);
                    } else {
                        Log.e(TAG, "Disconnected from unknown device: " + device);
@@ -1631,8 +1641,6 @@ public class HeadsetClientStateMachine extends StateMachine {
                    // is not much we can do here since dropping the call without user consent
                    // even if the audio connection snapped may not be a good idea.
                    routeHfpAudio(false);
                    broadcastAudioState(device, BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED,
                            BluetoothHeadsetClient.STATE_AUDIO_CONNECTED);
                    transitionTo(mConnected);
                    break;

@@ -1647,6 +1655,9 @@ public class HeadsetClientStateMachine extends StateMachine {
            if (DBG) {
                Log.d(TAG, "Exit AudioOn: " + getCurrentMessage().what);
            }
            mPrevState = this;
            broadcastAudioState(mCurrentDevice, BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED,
                    BluetoothHeadsetClient.STATE_AUDIO_CONNECTED);
        }
    }

@@ -1679,11 +1690,9 @@ public class HeadsetClientStateMachine extends StateMachine {
        Intent intent = new Intent(BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED);
        intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
        intent.putExtra(BluetoothProfile.EXTRA_STATE, newState);

        if (newState == BluetoothHeadsetClient.STATE_AUDIO_CONNECTED) {
            intent.putExtra(BluetoothHeadsetClient.EXTRA_AUDIO_WBS, mAudioWbs);
        }

        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
        mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
        if (DBG) {
@@ -1692,7 +1701,7 @@ public class HeadsetClientStateMachine extends StateMachine {
    }

    // This method does not check for error condition (newState == prevState)
    protected void broadcastConnectionState(BluetoothDevice device, int newState, int prevState) {
    private void broadcastConnectionState(BluetoothDevice device, int newState, int prevState) {
        if (DBG) {
            Log.d(TAG, "Connection state " + device + ": " + prevState + "->" + newState);
        }
+2 −1
Original line number Diff line number Diff line
@@ -15,7 +15,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \
    com.android.emailcommon \
    android-support-test \
    mockito-target \
    legacy-android-test
    legacy-android-test \
    espresso-intents

# Include all test java files.
LOCAL_SRC_FILES := $(call all-java-files-under, src)
+14 −8
Original line number Diff line number Diff line
@@ -4,14 +4,17 @@ import static org.mockito.Mockito.*;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.HandlerThread;
import android.support.test.espresso.intent.matcher.IntentMatchers;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;

import org.hamcrest.core.AllOf;
import org.hamcrest.core.IsInstanceOf;
import org.junit.After;
import org.junit.Assert;
@@ -21,6 +24,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.hamcrest.MockitoHamcrest;

@MediumTest
@RunWith(AndroidJUnit4.class)
@@ -54,10 +58,12 @@ public class HeadsetClientStateMachineTest {
        // Manage looper execution in main test thread explicitly to guarantee timing consistency
        mHeadsetClientStateMachine =
                new HeadsetClientStateMachine(mHeadsetClientService, mHandlerThread.getLooper());
        mHeadsetClientStateMachine.start();
    }

    @After
    public void tearDown() {
        mHeadsetClientStateMachine.doQuit();
        mHandlerThread.quit();
    }

@@ -75,8 +81,6 @@ public class HeadsetClientStateMachineTest {
     */
    @Test
    public void testIncomingPriorityReject() {
        mHeadsetClientStateMachine.start();

        // Return false for priority.
        when(mHeadsetClientService.getPriority(any(BluetoothDevice.class))).thenReturn(
                BluetoothProfile.PRIORITY_OFF);
@@ -87,8 +91,14 @@ public class HeadsetClientStateMachineTest {
        connStCh.device = mTestDevice;
        mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, connStCh);

        // Verify that no connection state broadcast is executed
        verify(mHeadsetClientService, never()).sendBroadcast(any(Intent.class), anyString());
        // Verify that only DISCONNECTED -> DISCONNECTED broadcast is fired
        verify(mHeadsetClientService, timeout(1000)).sendBroadcast(MockitoHamcrest.argThat(
                AllOf.allOf(IntentMatchers.hasAction(
                        BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED),
                        IntentMatchers.hasExtra(BluetoothProfile.EXTRA_STATE,
                                BluetoothProfile.STATE_DISCONNECTED),
                        IntentMatchers.hasExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
                                BluetoothProfile.STATE_DISCONNECTED))), anyString());
        // Check we are in disconnected state still.
        Assert.assertThat(mHeadsetClientStateMachine.getCurrentState(),
                IsInstanceOf.instanceOf(HeadsetClientStateMachine.Disconnected.class));
@@ -99,8 +109,6 @@ public class HeadsetClientStateMachineTest {
     */
    @Test
    public void testIncomingPriorityAccept() {
        mHeadsetClientStateMachine.start();

        // Return true for priority.
        when(mHeadsetClientService.getPriority(any(BluetoothDevice.class))).thenReturn(
                BluetoothProfile.PRIORITY_ON);
@@ -145,8 +153,6 @@ public class HeadsetClientStateMachineTest {
     */
    @Test
    public void testIncomingTimeout() {
        mHeadsetClientStateMachine.start();

        // Return true for priority.
        when(mHeadsetClientService.getPriority(any(BluetoothDevice.class))).thenReturn(
                BluetoothProfile.PRIORITY_ON);