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