HfpClientConnectionService.java revision 8b2711a9c8537df86e73a0d01b9c3f737f29c643
1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.bluetooth.hfpclient.connserv;
17
18import android.bluetooth.BluetoothAdapter;
19import android.bluetooth.BluetoothDevice;
20import android.bluetooth.BluetoothHeadsetClient;
21import android.bluetooth.BluetoothHeadsetClientCall;
22import android.bluetooth.BluetoothProfile;
23import android.content.BroadcastReceiver;
24import android.content.ComponentName;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.net.Uri;
29import android.os.Bundle;
30import android.os.Handler;
31import android.telecom.Connection;
32import android.telecom.ConnectionRequest;
33import android.telecom.ConnectionService;
34import android.telecom.PhoneAccount;
35import android.telecom.PhoneAccountHandle;
36import android.telecom.TelecomManager;
37import android.util.Log;
38
39import com.android.bluetooth.hfpclient.HeadsetClientService;
40
41import java.util.Arrays;
42import java.util.ArrayList;
43import java.util.Collection;
44import java.util.HashMap;
45import java.util.List;
46import java.util.Map;
47
48public class HfpClientConnectionService extends ConnectionService {
49    private static final String TAG = "HfpClientConnService";
50    private static final boolean DBG = true;
51
52    public static final String HFP_SCHEME = "hfpc";
53
54    private BluetoothAdapter mAdapter;
55    // Currently active device.
56    private BluetoothDevice mDevice;
57    // Phone account associated with the above device.
58    private PhoneAccount mDevicePhoneAccount;
59    // BluetoothHeadset proxy.
60    private BluetoothHeadsetClient mHeadsetProfile;
61    private TelecomManager mTelecomManager;
62
63    private Map<ConnectionKey, HfpClientConnection> mConnections =
64        new HashMap<ConnectionKey, HfpClientConnection>();
65    private HfpClientConference mConference;
66
67    private boolean mPendingAcceptCall;
68
69    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
70        @Override
71        public void onReceive(Context context, Intent intent) {
72            Log.d(TAG, "onReceive " + intent);
73            String action = intent != null ? intent.getAction() : null;
74
75            if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
76                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
77                int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
78
79                if (newState == BluetoothProfile.STATE_CONNECTED) {
80                    Log.d(TAG, "Established connection with " + device);
81                    synchronized (HfpClientConnectionService.this) {
82                        if (device.equals(mDevice)) {
83                            // We are already connected and this message can be safeuly ignored.
84                            Log.w(TAG, "Got connected for previously connected device, ignoring.");
85                        } else {
86                            // Since we are connected to a new device close down the previous
87                            // account and register the new one.
88                            if (mDevicePhoneAccount != null) {
89                                mTelecomManager.unregisterPhoneAccount(
90                                    mDevicePhoneAccount.getAccountHandle());
91                            }
92                            // Reset the device and the phone account associated.
93                            mDevice = device;
94                            mDevicePhoneAccount =
95                                getAccount(HfpClientConnectionService.this, device);
96                            mTelecomManager.registerPhoneAccount(mDevicePhoneAccount);
97                            mTelecomManager.enablePhoneAccount(
98                                mDevicePhoneAccount.getAccountHandle(), true);
99                            mTelecomManager.setUserSelectedOutgoingPhoneAccount(
100                                mDevicePhoneAccount.getAccountHandle());
101                        }
102                    }
103
104                    // Add any existing calls to the telecom stack.
105                    if (mHeadsetProfile != null) {
106                        List<BluetoothHeadsetClientCall> calls =
107                                mHeadsetProfile.getCurrentCalls(mDevice);
108                        Log.d(TAG, "Got calls " + calls);
109                        if (calls == null) {
110                            // We can get null as a return if we are not connected. Hence there may
111                            // be a race in getting the broadcast and HFP Client getting
112                            // disconnected before broadcast gets delivered.
113                            Log.w(TAG, "Got connected but calls were null, ignoring the broadcast");
114                            return;
115                        }
116                        for (BluetoothHeadsetClientCall call : calls) {
117                            handleCall(call);
118                        }
119                    } else {
120                        Log.e(TAG, "headset profile is null, ignoring broadcast.");
121                    }
122                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
123                    Log.d(TAG, "Disconnecting from " + device);
124                    // Disconnect any inflight calls from the connection service.
125                    synchronized (HfpClientConnectionService.this) {
126                        if (device.equals(mDevice)) {
127                            Log.d(TAG, "Resetting state for " + device);
128                            mDevice = null;
129                            disconnectAll();
130                            mTelecomManager.unregisterPhoneAccount(
131                                mDevicePhoneAccount.getAccountHandle());
132                            mDevicePhoneAccount = null;
133                        }
134                    }
135                }
136            } else if (BluetoothHeadsetClient.ACTION_CALL_CHANGED.equals(action)) {
137                // If we are not connected, then when we actually do get connected -- the calls should
138                // be added (see ACTION_CONNECTION_STATE_CHANGED intent above).
139                handleCall((BluetoothHeadsetClientCall)
140                        intent.getParcelableExtra(BluetoothHeadsetClient.EXTRA_CALL));
141                Log.d(TAG, mConnections.size() + " remaining");
142            }
143        }
144    };
145
146    @Override
147    public void onCreate() {
148        super.onCreate();
149        Log.d(TAG, "onCreate");
150        mAdapter = BluetoothAdapter.getDefaultAdapter();
151        mTelecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
152        mAdapter.getProfileProxy(this, mServiceListener, BluetoothProfile.HEADSET_CLIENT);
153    }
154
155    @Override
156    public void onDestroy() {
157        Log.d(TAG, "onDestroy called");
158        // Close the profile.
159        if (mHeadsetProfile != null) {
160            mAdapter.closeProfileProxy(BluetoothProfile.HEADSET_CLIENT, mHeadsetProfile);
161        }
162
163        // Unregister the broadcast receiver.
164        try {
165            unregisterReceiver(mBroadcastReceiver);
166        } catch (IllegalArgumentException ex) {
167            Log.w(TAG, "Receiver was not registered.");
168        }
169
170        // Unregister the phone account. This should ideally happen when disconnection ensues but in
171        // case the service crashes we may need to force clean.
172        synchronized (this) {
173            mDevice = null;
174            if (mDevicePhoneAccount != null) {
175                mTelecomManager.unregisterPhoneAccount(mDevicePhoneAccount.getAccountHandle());
176                mDevicePhoneAccount = null;
177            }
178        }
179    }
180
181    @Override
182    public int onStartCommand(Intent intent, int flags, int startId) {
183        Log.d(TAG, "onStartCommand " + intent);
184        // In order to make sure that the service is sticky (recovers from errors when HFP
185        // connection is still active) and to stop it we need a special intent since stopService
186        // only recreates it.
187        if (intent.getBooleanExtra(HeadsetClientService.HFP_CLIENT_STOP_TAG, false)) {
188            // Stop the service.
189            stopSelf();
190            return 0;
191        } else {
192            IntentFilter filter = new IntentFilter();
193            filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
194            filter.addAction(BluetoothHeadsetClient.ACTION_CALL_CHANGED);
195            registerReceiver(mBroadcastReceiver, filter);
196            return START_STICKY;
197        }
198    }
199
200    // Find the connection specified by the key, also update the key with ID if present.
201    private synchronized HfpClientConnection findConnectionAndUpdateKey(ConnectionKey key) {
202        if (DBG) {
203            Log.d(TAG, "findConnectionAndUpdateKey local key set " + mConnections.toString());
204        }
205
206        HfpClientConnection conn = mConnections.get(key);
207        if (conn != null && key.getId() != ConnectionKey.INVALID_ID) {
208            Log.d(TAG, "Updating key for " + key.getPhoneNumber() + " to " + key.getId());
209            mConnections.remove(key);
210            mConnections.put(key, conn);
211        }
212        return conn;
213    }
214
215    private void handleCall(BluetoothHeadsetClientCall call) {
216        Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null);
217        Log.d(TAG, "Got call " + call.toString(true) + "/" + number);
218        ConnectionKey incomingKey = ConnectionKey.getKey(call);
219        HfpClientConnection connection = findConnectionAndUpdateKey(incomingKey);
220
221        if (connection != null) {
222            connection.handleCallChanged(call);
223        }
224
225        if (connection == null) {
226            // Create the connection here, trigger Telecom to bind to us.
227            buildConnection(call.getDevice(), call, number);
228
229            PhoneAccountHandle handle = getHandle();
230            TelecomManager manager =
231                    (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
232
233            // Depending on where this call originated make it an incoming call or outgoing
234            // (represented as unknown call in telecom since). Since BluetoothHeadsetClientCall is a
235            // parcelable we simply pack the entire object in there.
236            Bundle b = new Bundle();
237            if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_DIALING ||
238                call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ALERTING ||
239                call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) {
240                // This is an outgoing call. Even if it is an active call we do not have a way of
241                // putting that parcelable in a seaprate field.
242                b.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, call);
243                manager.addNewUnknownCall(handle, b);
244            } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_INCOMING) {
245                // This is an incoming call.
246                b.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, call);
247                manager.addNewIncomingCall(handle, b);
248            }
249        } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) {
250            Log.d(TAG, "Removing number " + number);
251            synchronized (this) {
252                mConnections.remove(ConnectionKey.getKey(call));
253            }
254        }
255        updateConferenceableConnections();
256    }
257
258    // This method is called whenever there is a new incoming call (or right after BT connection).
259    @Override
260    public Connection onCreateIncomingConnection(
261            PhoneAccountHandle connectionManagerAccount,
262            ConnectionRequest request) {
263        Log.d(TAG, "onCreateIncomingConnection " + connectionManagerAccount + " req: " + request);
264        if (connectionManagerAccount != null &&
265                !getHandle().equals(connectionManagerAccount)) {
266            Log.w(TAG, "HfpClient does not support having a connection manager");
267            return null;
268        }
269
270        // We should already have a connection by this time.
271        BluetoothHeadsetClientCall call =
272            request.getExtras().getParcelable(
273                TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
274        HfpClientConnection connection = null;
275
276        synchronized (this) {
277            connection = mConnections.get(ConnectionKey.getKey(call));
278        }
279
280        if (connection != null) {
281            connection.onAdded();
282            updateConferenceableConnections();
283            return connection;
284        } else {
285            Log.e(TAG, "Connection should exist in our db, if it doesn't we dont know how to " +
286                "handle this call.");
287            return null;
288        }
289    }
290
291    // This method is called *only if* Dialer UI is used to place an outgoing call.
292    @Override
293    public Connection onCreateOutgoingConnection(
294            PhoneAccountHandle connectionManagerAccount,
295            ConnectionRequest request) {
296        Log.d(TAG, "onCreateOutgoingConnection " + connectionManagerAccount);
297        if (connectionManagerAccount != null &&
298                !getHandle().equals(connectionManagerAccount)) {
299            Log.w(TAG, "HfpClient does not support having a connection manager");
300            return null;
301        }
302
303        HfpClientConnection connection =
304                buildConnection(getDevice(request.getAccountHandle()), null, request.getAddress());
305        connection.onAdded();
306        return connection;
307    }
308
309    // This method is called when:
310    // 1. Outgoing call created from the AG.
311    // 2. Call transfer from AG -> HF (on connection when existed call present).
312    @Override
313    public Connection onCreateUnknownConnection(
314            PhoneAccountHandle connectionManagerAccount,
315            ConnectionRequest request) {
316        Log.d(TAG, "onCreateUnknownConnection " + connectionManagerAccount);
317        if (connectionManagerAccount != null &&
318                !getHandle().equals(connectionManagerAccount)) {
319            Log.w(TAG, "HfpClient does not support having a connection manager");
320            return null;
321        }
322
323        // We should already have a connection by this time.
324        BluetoothHeadsetClientCall call =
325            request.getExtras().getParcelable(
326                TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS);
327        Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null);
328
329        HfpClientConnection connection = null;
330        synchronized (this) {
331            connection = mConnections.get(ConnectionKey.getKey(call));
332        }
333
334        if (connection != null) {
335            connection.onAdded();
336            updateConferenceableConnections();
337            return connection;
338        } else {
339            Log.e(TAG, "Connection should exist in our db, if it doesn't we dont know how to " +
340                "handle this call " + call);
341            return null;
342        }
343    }
344
345    @Override
346    public void onConference(Connection connection1, Connection connection2) {
347        Log.d(TAG, "onConference " + connection1 + " " + connection2);
348        if (mConference == null) {
349            BluetoothDevice device = getDevice(getHandle());
350            mConference = new HfpClientConference(getHandle(), device, mHeadsetProfile);
351            addConference(mConference);
352        }
353        mConference.setActive();
354        if (connection1.getConference() == null) {
355            mConference.addConnection(connection1);
356        }
357        if (connection2.getConference() == null) {
358            mConference.addConnection(connection2);
359        }
360    }
361
362    private void updateConferenceableConnections() {
363        Collection<HfpClientConnection> all = mConnections.values();
364
365        List<Connection> held = new ArrayList<>();
366        List<Connection> active = new ArrayList<>();
367        List<Connection> group = new ArrayList<>();
368        for (HfpClientConnection connection : all) {
369            switch (connection.getState()) {
370                case Connection.STATE_ACTIVE:
371                    active.add(connection);
372                    break;
373                case Connection.STATE_HOLDING:
374                    held.add(connection);
375                    break;
376                default:
377                    break;
378            }
379            if (connection.inConference()) {
380                group.add(connection);
381            }
382        }
383        for (Connection connection : held) {
384            connection.setConferenceableConnections(active);
385        }
386        for (Connection connection : active) {
387            connection.setConferenceableConnections(held);
388        }
389        if (group.size() > 1 && mConference == null) {
390            BluetoothDevice device = getDevice(getHandle());
391            mConference = new HfpClientConference(getHandle(), device, mHeadsetProfile);
392            if (group.get(0).getState() == Connection.STATE_ACTIVE) {
393                mConference.setActive();
394            } else {
395                mConference.setOnHold();
396            }
397            for (Connection connection : group) {
398                mConference.addConnection(connection);
399            }
400            addConference(mConference);
401        }
402        if (mConference != null) {
403            List<Connection> toRemove = new ArrayList<>();
404            for (Connection connection : mConference.getConnections()) {
405                if (!((HfpClientConnection) connection).inConference()) {
406                    toRemove.add(connection);
407                }
408            }
409            for (Connection connection : toRemove) {
410                mConference.removeConnection(connection);
411            }
412            if (mConference.getConnections().size() <= 1) {
413                mConference.destroy();
414                mConference = null;
415            } else {
416                List<Connection> notConferenced = new ArrayList<>();
417                for (Connection connection : all) {
418                    if (connection.getConference() == null &&
419                            (connection.getState() == Connection.STATE_HOLDING ||
420                             connection.getState() == Connection.STATE_ACTIVE)) {
421                        if (((HfpClientConnection) connection).inConference()) {
422                            mConference.addConnection(connection);
423                        } else {
424                            notConferenced.add(connection);
425                        }
426                    }
427                }
428                mConference.setConferenceableConnections(notConferenced);
429            }
430        }
431    }
432
433    private synchronized void disconnectAll() {
434        for (HfpClientConnection connection : mConnections.values()) {
435            connection.onHfpDisconnected();
436        }
437        mConnections.clear();
438        if (mConference != null) {
439            mConference.destroy();
440            mConference = null;
441        }
442    }
443
444    private BluetoothDevice getDevice(PhoneAccountHandle handle) {
445        PhoneAccount account = mTelecomManager.getPhoneAccount(handle);
446        String btAddr = account.getAddress().getSchemeSpecificPart();
447        return mAdapter.getRemoteDevice(btAddr);
448    }
449
450    private synchronized HfpClientConnection buildConnection(
451            BluetoothDevice device, BluetoothHeadsetClientCall call, Uri number) {
452        Log.d(TAG, "Creating connection on " + device + " for " + call + "/" + number);
453        HfpClientConnection connection =
454                new HfpClientConnection(this, device, mHeadsetProfile, call, number);
455        mConnections.put(new ConnectionKey(ConnectionKey.INVALID_ID, number), connection);
456        return connection;
457    }
458
459    BluetoothProfile.ServiceListener mServiceListener = new BluetoothProfile.ServiceListener() {
460        @Override
461        public void onServiceConnected(int profile, BluetoothProfile proxy) {
462            Log.d(TAG, "onServiceConnected");
463            mHeadsetProfile = (BluetoothHeadsetClient) proxy;
464
465            List<BluetoothDevice> devices = mHeadsetProfile.getConnectedDevices();
466            if (devices == null || devices.size() != 1) {
467                Log.w(TAG, "No connected or more than one connected devices found." + devices);
468            } else { // We have exactly one device connected.
469                Log.d(TAG, "Creating phone account.");
470                synchronized (HfpClientConnectionService.this) {
471                    mDevice = devices.get(0);
472                    mDevicePhoneAccount = getAccount(HfpClientConnectionService.this, mDevice);
473                    mTelecomManager.registerPhoneAccount(mDevicePhoneAccount);
474                    mTelecomManager.enablePhoneAccount(
475                        mDevicePhoneAccount.getAccountHandle(), true);
476                    mTelecomManager.setUserSelectedOutgoingPhoneAccount(
477                        mDevicePhoneAccount.getAccountHandle());
478                }
479            }
480
481            for (HfpClientConnection connection : mConnections.values()) {
482                connection.onHfpConnected(mHeadsetProfile);
483            }
484
485            List<BluetoothHeadsetClientCall> calls = mHeadsetProfile.getCurrentCalls(mDevice);
486            Log.d(TAG, "Got calls " + calls);
487            if (calls != null) {
488                for (BluetoothHeadsetClientCall call : calls) {
489                    handleCall(call);
490                }
491            }
492
493            if (mPendingAcceptCall) {
494                mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_NONE);
495                mPendingAcceptCall = false;
496            }
497        }
498
499        @Override
500        public void onServiceDisconnected(int profile) {
501            Log.d(TAG, "onServiceDisconnected " + profile);
502            mHeadsetProfile = null;
503            disconnectAll();
504        }
505    };
506
507    public static boolean hasHfpClientEcc(BluetoothHeadsetClient client, BluetoothDevice device) {
508        Bundle features = client.getCurrentAgEvents(device);
509        return features == null ? false :
510                features.getBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_ECC, false);
511    }
512
513    public synchronized PhoneAccountHandle getHandle() {
514        if (mDevicePhoneAccount == null) throw new IllegalStateException("Handle null??");
515        return mDevicePhoneAccount.getAccountHandle();
516    }
517
518    public static PhoneAccount getAccount(Context context, BluetoothDevice device) {
519        Uri addr = Uri.fromParts(HfpClientConnectionService.HFP_SCHEME, device.getAddress(), null);
520        PhoneAccountHandle handle = new PhoneAccountHandle(
521            new ComponentName(context, HfpClientConnectionService.class), device.getAddress());
522        PhoneAccount account =
523                new PhoneAccount.Builder(handle, "HFP")
524                    .setAddress(addr)
525                    .setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_TEL))
526                    .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
527                    .build();
528        Log.d(TAG, "phoneaccount: " + account);
529        return account;
530    }
531}
532