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