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

Commit 1ff2e6e9 authored by Sanket Agarwal's avatar Sanket Agarwal
Browse files

Improve connection handling logic in ConnService

Connection for HFP HF role in Bluetooth is currently trying to
handle all cases of connected vs disconnect HFP. This can be offloaded
to ConnServ where the connection lifecycle should be maintained (and not
in the connection itself).

1. Fixes NPEs generated by the BtCarHfpFuzzTest.
2. Reduces the number of calls to AT+CLCC

Change-Id: Ibccadb7721cbbd2260cbfc5685d66027277e8a59
(cherry picked from commit aa09a41a685e957786b69d4101f5107b9d6d1d9b)
parent 05462fe4
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -345,6 +345,7 @@ public class HeadsetClientService extends ProfileService {
        public boolean terminateCall(BluetoothDevice device, BluetoothHeadsetClientCall call) {
            HeadsetClientService service = getService();
            if (service == null) {
                Log.w(TAG, "service is null");
                return false;
            }
            return service.terminateCall(device, call.getUUID());
+18 −10
Original line number Diff line number Diff line
@@ -73,7 +73,7 @@ import com.android.bluetooth.R;

final class HeadsetClientStateMachine extends StateMachine {
    private static final String TAG = "HeadsetClientStateMachine";
    private static final boolean DBG = false;
    private static final boolean DBG = true;

    static final int NO_ACTION = 0;

@@ -111,11 +111,13 @@ final class HeadsetClientStateMachine extends StateMachine {

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

    private final Disconnected mDisconnected;
    private final Connecting mConnecting;
    private final Connected mConnected;
    private final AudioOn mAudioOn;
    private long mClccTimer = 0;

    private final HeadsetClientService mService;

@@ -463,7 +465,7 @@ final class HeadsetClientStateMachine extends StateMachine {
        }

        if (loopQueryCalls()) {
            sendMessageDelayed(QUERY_CURRENT_CALLS, 1523);
            sendMessageDelayed(QUERY_CURRENT_CALLS, QUERY_CURRENT_CALLS_WAIT_MILLIS);
        }

        mCallsUpdate.clear();
@@ -1314,7 +1316,19 @@ final class HeadsetClientStateMachine extends StateMachine {
                    }
                    break;
                case QUERY_CURRENT_CALLS:
                    // Whenever the timer expires we query calls if there are outstanding requests
                    // for query calls.
                    long currentElapsed = SystemClock.elapsedRealtime();
                    if (mClccTimer < currentElapsed) {
                        queryCallsStart();
                        mClccTimer = currentElapsed + QUERY_CURRENT_CALLS_WAIT_MILLIS;
                        // Request satisfied, ignore all other call query messages.
                        removeMessages(QUERY_CURRENT_CALLS);
                    } else {
                        // Replace all messages with one concrete message.
                        removeMessages(QUERY_CURRENT_CALLS);
                        sendMessageDelayed(QUERY_CURRENT_CALLS, QUERY_CURRENT_CALLS_WAIT_MILLIS);
                    }
                    break;
                case STACK_EVENT:
                    Intent intent = null;
@@ -1530,13 +1544,7 @@ final class HeadsetClientStateMachine extends StateMachine {
                                case TERMINATE_SPECIFIC_CALL:
                                    // if terminating specific succeed no other
                                    // event is send
                                    if (event.valueInt == BluetoothHeadsetClient.ACTION_RESULT_OK) {
                                        BluetoothHeadsetClientCall sc =
                                                (BluetoothHeadsetClientCall) queuedAction.second;
                                        setCallState(sc,
                                                BluetoothHeadsetClientCall.CALL_STATE_TERMINATED);
                                        mCalls.remove(sc.getId());
                                    } else {
                                    if (event.valueInt != BluetoothHeadsetClient.ACTION_RESULT_OK) {
                                        sendActionResultIntent(event);
                                    }
                                    break;
+76 −36
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.content.Context;
import android.net.Uri;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.TelecomManager;
import android.util.Log;

@@ -30,38 +31,64 @@ import java.util.UUID;

public class HfpClientConnection extends Connection {
    private static final String TAG = "HfpClientConnection";
    private static final boolean DBG = true;

    private final Context mContext;
    private final BluetoothDevice mDevice;

    private BluetoothHeadsetClient mHeadsetProfile;

    private BluetoothHeadsetClientCall mCurrentCall;
    private boolean mClosed;
    private boolean mLocalDisconnect;
    private boolean mClientHasEcc;
    private boolean mAdded;

    // Constructor to be used when there's an existing call (such as that created on the AG or
    // when connection happens and we see calls for the first time).
    public HfpClientConnection(Context context, BluetoothDevice device,
            BluetoothHeadsetClient client, BluetoothHeadsetClientCall call, Uri number) {
            BluetoothHeadsetClient client, BluetoothHeadsetClientCall call) {
        mDevice = device;
        mContext = context;
        mHeadsetProfile = client;

        setInitializing();
        if (call == null) {
            throw new IllegalStateException("Call is null");
        }

        if (call != null) {
        mCurrentCall = call;
        handleCallChanged();
        } else if (mHeadsetProfile != null) {
        finishInitializing();
    }

    // Constructor to be used when a call is intiated on the HF. The call handle is obtained by
    // using the dial() command.
    public HfpClientConnection(Context context, BluetoothDevice device,
            BluetoothHeadsetClient client, Uri number) {
        mDevice = device;
        mContext = context;
        mHeadsetProfile = client;

        if (mHeadsetProfile == null) {
            throw new IllegalStateException("HeadsetProfile is null, returning");
        }

        mCurrentCall = mHeadsetProfile.dial(
            mDevice, number.getSchemeSpecificPart());
        if (mCurrentCall == null) {
            close(DisconnectCause.ERROR);
            Log.e(TAG, "Failed to create the call, dial failed.");
            return;
        }

        setInitializing();
        setDialing();
        finishInitializing();
    }

        if (mHeadsetProfile != null) {
    void finishInitializing() {
        mClientHasEcc = HfpClientConnectionService.hasHfpClientEcc(mHeadsetProfile, mDevice);
        }
        setAudioModeIsVoip(false);
        Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, mCurrentCall.getNumber(), null);
        setAddress(number, TelecomManager.PRESENTATION_ALLOWED);
        setConnectionCapabilities(CAPABILITY_SUPPORT_HOLD | CAPABILITY_MUTE |
                CAPABILITY_SEPARATE_FROM_CONFERENCE | CAPABILITY_DISCONNECT_FROM_CONFERENCE |
@@ -72,12 +99,6 @@ public class HfpClientConnection extends Connection {
        return mCurrentCall.getUUID();
    }

    public void onHfpConnected(BluetoothHeadsetClient client) {
        mHeadsetProfile = client;
        mClientHasEcc = HfpClientConnectionService.hasHfpClientEcc(mHeadsetProfile, mDevice);
        handleCallChanged();
    }

    public void onHfpDisconnected() {
        mHeadsetProfile = null;
        close(DisconnectCause.ERROR);
@@ -92,7 +113,7 @@ public class HfpClientConnection extends Connection {
    }

    public boolean inConference() {
        return mAdded && mCurrentCall != null && mCurrentCall.isMultiParty() &&
        return mAdded && mCurrentCall.isMultiParty() &&
                getState() != Connection.STATE_DISCONNECTED;
    }

@@ -102,6 +123,10 @@ public class HfpClientConnection extends Connection {
    }

    public void updateCall(BluetoothHeadsetClientCall call) {
        if (call == null) {
            Log.e(TAG, "Updating call to a null value.");
            return;
        }
        mCurrentCall = call;
    }

@@ -109,7 +134,9 @@ public class HfpClientConnection extends Connection {
        HfpClientConference conference = (HfpClientConference) getConference();
        int state = mCurrentCall.getState();

        if (DBG) {
            Log.d(TAG, "Got call state change to " + state);
        }
        switch (state) {
            case BluetoothHeadsetClientCall.CALL_STATE_ACTIVE:
                setActive();
@@ -142,11 +169,12 @@ public class HfpClientConnection extends Connection {
    }

    private void close(int cause) {
        if (DBG) {
            Log.d(TAG, "Closing " + mClosed);
        }
        if (mClosed) {
            return;
        }

        setDisconnected(new DisconnectCause(cause));

        mClosed = true;
@@ -157,34 +185,40 @@ public class HfpClientConnection extends Connection {

    @Override
    public void onPlayDtmfTone(char c) {
        if (DBG) {
            Log.d(TAG, "onPlayDtmfTone " + c + " " + mCurrentCall);
        if (!mClosed && mHeadsetProfile != null) {
        }
        if (!mClosed) {
            mHeadsetProfile.sendDTMF(mDevice, (byte) c);
        }
    }

    @Override
    public synchronized void onDisconnect() {
        Log.d(TAG, "onDisconnect " + mCurrentCall);
        // In this state we can close the call without problems.
        if (mHeadsetProfile != null) {
        if (DBG) {
            Log.d(TAG, "onDisconnect call: " + mCurrentCall + " state: " + mClosed);
        }
        // The call is not closed so we should send a terminate here.
        if (!mClosed) {
            mHeadsetProfile.terminateCall(mDevice, mCurrentCall);
            mLocalDisconnect = true;
        } else if (mCurrentCall == null) {
            Log.w(TAG, "Call disconnected but call handle not received.");
        }
    }

    @Override
    public void onAbort() {
        if (DBG) {
            Log.d(TAG, "onAbort " + mCurrentCall);
        }
        onDisconnect();
    }

    @Override
    public void onHold() {
        if (DBG) {
            Log.d(TAG, "onHold " + mCurrentCall);
        if (!mClosed && mHeadsetProfile != null) {
        }
        if (!mClosed) {
            mHeadsetProfile.holdCall(mDevice);
        }
    }
@@ -195,15 +229,19 @@ public class HfpClientConnection extends Connection {
            Log.w(TAG, "Ignoring unhold; call hold on the foreground call");
            return;
        }
        if (DBG) {
            Log.d(TAG, "onUnhold " + mCurrentCall);
        if (!mClosed && mHeadsetProfile != null) {
        }
        if (!mClosed) {
            mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_HOLD);
        }
    }

    @Override
    public void onAnswer() {
        if (DBG) {
            Log.d(TAG, "onAnswer " + mCurrentCall);
        }
        if (!mClosed) {
            mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_NONE);
        }
@@ -211,7 +249,9 @@ public class HfpClientConnection extends Connection {

    @Override
    public void onReject() {
        if (DBG) {
            Log.d(TAG, "onReject " + mCurrentCall);
        }
        if (!mClosed) {
            mHeadsetProfile.rejectCall(mDevice);
        }
+92 −32
Original line number Diff line number Diff line
@@ -69,7 +69,9 @@ public class HfpClientConnectionService extends ConnectionService {
    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (DBG) {
                Log.d(TAG, "onReceive " + intent);
            }
            String action = intent != null ? intent.getAction() : null;

            if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
@@ -77,7 +79,9 @@ public class HfpClientConnectionService extends ConnectionService {
                int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);

                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    if (DBG) {
                        Log.d(TAG, "Established connection with " + device);
                    }
                    synchronized (HfpClientConnectionService.this) {
                        if (device.equals(mDevice)) {
                            // We are already connected and this message can be safeuly ignored.
@@ -105,7 +109,9 @@ public class HfpClientConnectionService extends ConnectionService {
                    if (mHeadsetProfile != null) {
                        List<BluetoothHeadsetClientCall> calls =
                                mHeadsetProfile.getCurrentCalls(mDevice);
                        if (DBG) {
                            Log.d(TAG, "Got calls " + calls);
                        }
                        if (calls == null) {
                            // We can get null as a return if we are not connected. Hence there may
                            // be a race in getting the broadcast and HFP Client getting
@@ -120,11 +126,15 @@ public class HfpClientConnectionService extends ConnectionService {
                        Log.e(TAG, "headset profile is null, ignoring broadcast.");
                    }
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    if (DBG) {
                        Log.d(TAG, "Disconnecting from " + device);
                    }
                    // Disconnect any inflight calls from the connection service.
                    synchronized (HfpClientConnectionService.this) {
                        if (device.equals(mDevice)) {
                            if (DBG) {
                                Log.d(TAG, "Resetting state for " + device);
                            }
                            mDevice = null;
                            disconnectAll();
                            mTelecomManager.unregisterPhoneAccount(
@@ -138,15 +148,19 @@ public class HfpClientConnectionService extends ConnectionService {
                // be added (see ACTION_CONNECTION_STATE_CHANGED intent above).
                handleCall((BluetoothHeadsetClientCall)
                        intent.getParcelableExtra(BluetoothHeadsetClient.EXTRA_CALL));
                if (DBG) {
                    Log.d(TAG, mConnections.size() + " remaining");
                }
            }
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        if (DBG) {
            Log.d(TAG, "onCreate");
        }
        mAdapter = BluetoothAdapter.getDefaultAdapter();
        mTelecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
        mAdapter.getProfileProxy(this, mServiceListener, BluetoothProfile.HEADSET_CLIENT);
@@ -154,7 +168,9 @@ public class HfpClientConnectionService extends ConnectionService {

    @Override
    public void onDestroy() {
        if (DBG) {
            Log.d(TAG, "onDestroy called");
        }
        // Close the profile.
        if (mHeadsetProfile != null) {
            mAdapter.closeProfileProxy(BluetoothProfile.HEADSET_CLIENT, mHeadsetProfile);
@@ -180,7 +196,9 @@ public class HfpClientConnectionService extends ConnectionService {

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (DBG) {
            Log.d(TAG, "onStartCommand " + intent);
        }
        // In order to make sure that the service is sticky (recovers from errors when HFP
        // connection is still active) and to stop it we need a special intent since stopService
        // only recreates it.
@@ -206,9 +224,11 @@ public class HfpClientConnectionService extends ConnectionService {
    }

    private void handleCall(BluetoothHeadsetClientCall call) {
        Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null);
        Log.d(TAG, "Got call " + call.toString(true) + "/" + number);
        if (DBG) {
            Log.d(TAG, "Got call " + call.toString(true));
        }
        HfpClientConnection connection = findConnectionKey(call.getUUID());

        if (connection != null) {
            connection.updateCall(call);
            connection.handleCallChanged();
@@ -216,7 +236,7 @@ public class HfpClientConnectionService extends ConnectionService {

        if (connection == null) {
            // Create the connection here, trigger Telecom to bind to us.
            buildConnection(call.getDevice(), call, number);
            buildConnection(call.getDevice(), call, null);

            PhoneAccountHandle handle = getHandle();
            TelecomManager manager =
@@ -228,7 +248,8 @@ public class HfpClientConnectionService extends ConnectionService {
            Bundle b = new Bundle();
            if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_DIALING ||
                call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ALERTING ||
                call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) {
                call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE ||
                call.getState() == BluetoothHeadsetClientCall.CALL_STATE_WAITING) {
                // This is an outgoing call. Even if it is an active call we do not have a way of
                // putting that parcelable in a seaprate field.
                b.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, call);
@@ -239,7 +260,9 @@ public class HfpClientConnectionService extends ConnectionService {
                manager.addNewIncomingCall(handle, b);
            }
        } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) {
            Log.d(TAG, "Removing number " + number);
            if (DBG) {
                Log.d(TAG, "Removing call " + call);
            }
            synchronized (this) {
                mConnections.remove(call.getUUID());
            }
@@ -252,7 +275,10 @@ public class HfpClientConnectionService extends ConnectionService {
    public Connection onCreateIncomingConnection(
            PhoneAccountHandle connectionManagerAccount,
            ConnectionRequest request) {
        Log.d(TAG, "onCreateIncomingConnection " + connectionManagerAccount + " req: " + request);
        if (DBG) {
            Log.d(TAG, "onCreateIncomingConnection " + connectionManagerAccount +
                " req: " + request);
        }
        if (connectionManagerAccount != null &&
                !getHandle().equals(connectionManagerAccount)) {
            Log.w(TAG, "HfpClient does not support having a connection manager");
@@ -285,7 +311,9 @@ public class HfpClientConnectionService extends ConnectionService {
    public Connection onCreateOutgoingConnection(
            PhoneAccountHandle connectionManagerAccount,
            ConnectionRequest request) {
        if (DBG) {
            Log.d(TAG, "onCreateOutgoingConnection " + connectionManagerAccount);
        }
        if (connectionManagerAccount != null &&
                !getHandle().equals(connectionManagerAccount)) {
            Log.w(TAG, "HfpClient does not support having a connection manager");
@@ -294,7 +322,9 @@ public class HfpClientConnectionService extends ConnectionService {

        HfpClientConnection connection =
                buildConnection(getDevice(request.getAccountHandle()), null, request.getAddress());
        if (connection != null) {
            connection.onAdded();
        }
        return connection;
    }

@@ -305,7 +335,9 @@ public class HfpClientConnectionService extends ConnectionService {
    public Connection onCreateUnknownConnection(
            PhoneAccountHandle connectionManagerAccount,
            ConnectionRequest request) {
        if (DBG) {
            Log.d(TAG, "onCreateUnknownConnection " + connectionManagerAccount);
        }
        if (connectionManagerAccount != null &&
                !getHandle().equals(connectionManagerAccount)) {
            Log.w(TAG, "HfpClient does not support having a connection manager");
@@ -336,7 +368,9 @@ public class HfpClientConnectionService extends ConnectionService {

    @Override
    public void onConference(Connection connection1, Connection connection2) {
        if (DBG) {
            Log.d(TAG, "onConference " + connection1 + " " + connection2);
        }
        if (mConference == null) {
            BluetoothDevice device = getDevice(getHandle());
            mConference = new HfpClientConference(getHandle(), device, mHeadsetProfile);
@@ -426,6 +460,7 @@ public class HfpClientConnectionService extends ConnectionService {
        for (HfpClientConnection connection : mConnections.values()) {
            connection.onHfpDisconnected();
        }

        mConnections.clear();
        if (mConference != null) {
            mConference.destroy();
@@ -441,24 +476,47 @@ public class HfpClientConnectionService extends ConnectionService {

    private synchronized HfpClientConnection buildConnection(
            BluetoothDevice device, BluetoothHeadsetClientCall call, Uri number) {
        if (mHeadsetProfile == null) {
            Log.e(TAG, "Cannot create connection for call " + call + " when Profile not available");
            return null;
        }

        if (call == null && number == null) {
            Log.e(TAG, "Both call and number cannot be null.");
            return null;
        }

        if (DBG) {
            Log.d(TAG, "Creating connection on " + device + " for " + call + "/" + number);
        HfpClientConnection connection =
                new HfpClientConnection(this, device, mHeadsetProfile, call, number);
        }
        HfpClientConnection connection = null;
        if (call != null) {
            connection = new HfpClientConnection(this, device, mHeadsetProfile, call);
        } else {
            connection = new HfpClientConnection(this, device, mHeadsetProfile, number);
        }
        if (connection.getState() != Connection.STATE_DISCONNECTED) {
            mConnections.put(connection.getUUID(), connection);
        }

        return connection;
    }

    BluetoothProfile.ServiceListener mServiceListener = new BluetoothProfile.ServiceListener() {
        @Override
        public void onServiceConnected(int profile, BluetoothProfile proxy) {
            if (DBG) {
                Log.d(TAG, "onServiceConnected");
            }
            mHeadsetProfile = (BluetoothHeadsetClient) proxy;

            List<BluetoothDevice> devices = mHeadsetProfile.getConnectedDevices();
            if (devices == null || devices.size() != 1) {
                Log.w(TAG, "No connected or more than one connected devices found." + devices);
            } else { // We have exactly one device connected.
                if (DBG) {
                    Log.d(TAG, "Creating phone account.");
                }
                synchronized (HfpClientConnectionService.this) {
                    mDevice = devices.get(0);
                    mDevicePhoneAccount = getAccount(HfpClientConnectionService.this, mDevice);
@@ -470,12 +528,10 @@ public class HfpClientConnectionService extends ConnectionService {
                }
            }

            for (HfpClientConnection connection : mConnections.values()) {
                connection.onHfpConnected(mHeadsetProfile);
            }

            List<BluetoothHeadsetClientCall> calls = mHeadsetProfile.getCurrentCalls(mDevice);
            if (DBG) {
                Log.d(TAG, "Got calls " + calls);
            }
            if (calls != null) {
                for (BluetoothHeadsetClientCall call : calls) {
                    handleCall(call);
@@ -490,7 +546,9 @@ public class HfpClientConnectionService extends ConnectionService {

        @Override
        public void onServiceDisconnected(int profile) {
            if (DBG) {
                Log.d(TAG, "onServiceDisconnected " + profile);
            }
            mHeadsetProfile = null;
            disconnectAll();
        }
@@ -517,7 +575,9 @@ public class HfpClientConnectionService extends ConnectionService {
                    .setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_TEL))
                    .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
                    .build();
        if (DBG) {
            Log.d(TAG, "phoneaccount: " + account);
        }
        return account;
    }
}