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.DisconnectCause;
35 import android.telecom.PhoneAccount;
36 import android.telecom.PhoneAccountHandle;
37 import android.telecom.TelecomManager;
38 import android.util.Log;
39 
40 import com.android.bluetooth.hfpclient.HeadsetClientService;
41 
42 import java.util.Arrays;
43 import java.util.ArrayList;
44 import java.util.Collection;
45 import java.util.HashMap;
46 import java.util.Iterator;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Objects;
50 import java.util.UUID;
51 
52 public class HfpClientConnectionService extends ConnectionService {
53     private static final String TAG = "HfpClientConnService";
54     private static final boolean DBG = true;
55 
56     public static final String HFP_SCHEME = "hfpc";
57 
58     private BluetoothAdapter mAdapter;
59 
60     // BluetoothHeadset proxy.
61     private BluetoothHeadsetClient mHeadsetProfile;
62     private TelecomManager mTelecomManager;
63 
64     private final Map<BluetoothDevice, HfpClientDeviceBlock> mDeviceBlocks =
65         new HashMap<>();
66 
67     private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
68         @Override
69         public void onReceive(Context context, Intent intent) {
70             if (DBG) {
71                 Log.d(TAG, "onReceive " + intent);
72             }
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                     if (DBG) {
81                         Log.d(TAG, "Established connection with " + device);
82                     }
83 
84                     HfpClientDeviceBlock block = null;
85                     if ((block = createBlockForDevice(device)) == null) {
86                         Log.w(TAG, "Block already exists for device " + device + " ignoring.");
87                     }
88                 } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
89                     if (DBG) {
90                         Log.d(TAG, "Disconnecting from " + device);
91                     }
92 
93                     // Disconnect any inflight calls from the connection service.
94                     synchronized (HfpClientConnectionService.this) {
95                         HfpClientDeviceBlock block = mDeviceBlocks.remove(device);
96                         if (block == null) {
97                             Log.w(TAG, "Disconnect for device but no block " + device);
98                             return;
99                         }
100                         block.cleanup();
101                         // Block should be subsequently garbage collected
102                         block = null;
103                     }
104                 }
105             } else if (BluetoothHeadsetClient.ACTION_CALL_CHANGED.equals(action)) {
106                 BluetoothHeadsetClientCall call =
107                     intent.getParcelableExtra(BluetoothHeadsetClient.EXTRA_CALL);
108                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
109                 HfpClientDeviceBlock block = findBlockForDevice(call.getDevice());
110                 if (block == null) {
111                     Log.w(TAG, "Call changed but no block for device " + device);
112                     return;
113                 }
114 
115                 // If we are not connected, then when we actually do get connected --
116                 // the calls should
117                 // be added (see ACTION_CONNECTION_STATE_CHANGED intent above).
118                 block.handleCall(call);
119             }
120         }
121     };
122 
123     @Override
onCreate()124     public void onCreate() {
125         super.onCreate();
126         if (DBG) {
127             Log.d(TAG, "onCreate");
128         }
129         mAdapter = BluetoothAdapter.getDefaultAdapter();
130         mTelecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
131         mAdapter.getProfileProxy(this, mServiceListener, BluetoothProfile.HEADSET_CLIENT);
132     }
133 
134     @Override
onDestroy()135     public void onDestroy() {
136         if (DBG) {
137             Log.d(TAG, "onDestroy called");
138         }
139         // Close the profile.
140         if (mHeadsetProfile != null) {
141             mAdapter.closeProfileProxy(BluetoothProfile.HEADSET_CLIENT, mHeadsetProfile);
142         }
143 
144         // Unregister the broadcast receiver.
145         try {
146             unregisterReceiver(mBroadcastReceiver);
147         } catch (IllegalArgumentException ex) {
148             Log.w(TAG, "Receiver was not registered.");
149         }
150 
151         // Unregister the phone account. This should ideally happen when disconnection ensues but in
152         // case the service crashes we may need to force clean.
153         disconnectAll();
154     }
155 
disconnectAll()156     private synchronized void disconnectAll() {
157         for (Iterator<Map.Entry<BluetoothDevice, HfpClientDeviceBlock>> it =
158                 mDeviceBlocks.entrySet().iterator(); it.hasNext();) {
159             it.next().getValue().cleanup();
160             it.remove();
161         }
162     }
163 
164     @Override
onStartCommand(Intent intent, int flags, int startId)165     public int onStartCommand(Intent intent, int flags, int startId) {
166         if (DBG) {
167             Log.d(TAG, "onStartCommand " + intent);
168         }
169         // In order to make sure that the service is sticky (recovers from errors when HFP
170         // connection is still active) and to stop it we need a special intent since stopService
171         // only recreates it.
172         if (intent != null &&
173             intent.getBooleanExtra(HeadsetClientService.HFP_CLIENT_STOP_TAG, false)) {
174             // Stop the service.
175             stopSelf();
176             return 0;
177         } else {
178             IntentFilter filter = new IntentFilter();
179             filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
180             filter.addAction(BluetoothHeadsetClient.ACTION_CALL_CHANGED);
181             registerReceiver(mBroadcastReceiver, filter);
182             return START_STICKY;
183         }
184     }
185 
186     // This method is called whenever there is a new incoming call (or right after BT connection).
187     @Override
onCreateIncomingConnection( PhoneAccountHandle connectionManagerAccount, ConnectionRequest request)188     public Connection onCreateIncomingConnection(
189             PhoneAccountHandle connectionManagerAccount,
190             ConnectionRequest request) {
191         if (DBG) {
192             Log.d(TAG, "onCreateIncomingConnection " + connectionManagerAccount +
193                 " req: " + request);
194         }
195 
196         HfpClientDeviceBlock block = findBlockForHandle(connectionManagerAccount);
197         if (block == null) {
198             Log.w(TAG, "HfpClient does not support having a connection manager");
199             return null;
200         }
201 
202         // We should already have a connection by this time.
203         BluetoothHeadsetClientCall call =
204             request.getExtras().getParcelable(
205                 TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
206         return block.onCreateIncomingConnection(call);
207     }
208 
209     // This method is called *only if* Dialer UI is used to place an outgoing call.
210     @Override
onCreateOutgoingConnection( PhoneAccountHandle connectionManagerAccount, ConnectionRequest request)211     public Connection onCreateOutgoingConnection(
212             PhoneAccountHandle connectionManagerAccount,
213             ConnectionRequest request) {
214         if (DBG) {
215             Log.d(TAG, "onCreateOutgoingConnection " + connectionManagerAccount);
216         }
217         HfpClientDeviceBlock block = findBlockForHandle(connectionManagerAccount);
218         if (block == null) {
219             Log.w(TAG, "HfpClient does not support having a connection manager");
220             return null;
221         }
222 
223         return block.onCreateOutgoingConnection(request.getAddress());
224     }
225 
226     // This method is called when:
227     // 1. Outgoing call created from the AG.
228     // 2. Call transfer from AG -> HF (on connection when existed call present).
229     @Override
onCreateUnknownConnection( PhoneAccountHandle connectionManagerAccount, ConnectionRequest request)230     public Connection onCreateUnknownConnection(
231             PhoneAccountHandle connectionManagerAccount,
232             ConnectionRequest request) {
233         if (DBG) {
234             Log.d(TAG, "onCreateUnknownConnection " + connectionManagerAccount);
235         }
236         HfpClientDeviceBlock block = findBlockForHandle(connectionManagerAccount);
237         if (block == null) {
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_OUTGOING_CALL_EXTRAS);
246         return block.onCreateUnknownConnection(call);
247     }
248 
249     @Override
onConference(Connection connection1, Connection connection2)250     public void onConference(Connection connection1, Connection connection2) {
251         if (DBG) {
252             Log.d(TAG, "onConference " + connection1 + " " + connection2);
253         }
254 
255         BluetoothDevice bd1 = ((HfpClientConnection) connection1).getDevice();
256         BluetoothDevice bd2 = ((HfpClientConnection) connection2).getDevice();
257         // We can only conference two connections on same device
258         if (!Objects.equals(bd1, bd2)) {
259             Log.e(TAG, "Cannot conference calls from two different devices "
260                             + "bd1 " + bd1 + " bd2 " + bd2 + " conn1 " + connection1
261                             + "connection2 " + connection2);
262             return;
263         }
264 
265         HfpClientDeviceBlock block = findBlockForDevice(bd1);
266         block.onConference(connection1, connection2);
267     }
268 
getDevice(PhoneAccountHandle handle)269     private BluetoothDevice getDevice(PhoneAccountHandle handle) {
270         PhoneAccount account = mTelecomManager.getPhoneAccount(handle);
271         String btAddr = account.getAddress().getSchemeSpecificPart();
272         return mAdapter.getRemoteDevice(btAddr);
273     }
274 
275     BluetoothProfile.ServiceListener mServiceListener = new BluetoothProfile.ServiceListener() {
276         @Override
277         public void onServiceConnected(int profile, BluetoothProfile proxy) {
278             if (DBG) {
279                 Log.d(TAG, "onServiceConnected");
280             }
281             mHeadsetProfile = (BluetoothHeadsetClient) proxy;
282 
283             List<BluetoothDevice> devices = mHeadsetProfile.getConnectedDevices();
284             if (devices == null) {
285                 Log.w(TAG, "No connected or more than one connected devices found." + devices);
286                 return;
287             }
288             for (BluetoothDevice device : devices) {
289                 if (DBG) {
290                     Log.d(TAG, "Creating phone account for device " + device);
291                 }
292 
293                 // Creation of the block takes care of initializing the phone account and
294                 // calls.
295                 HfpClientDeviceBlock block = createBlockForDevice(device);
296             }
297         }
298 
299         @Override
300         public void onServiceDisconnected(int profile) {
301             if (DBG) {
302                 Log.d(TAG, "onServiceDisconnected " + profile);
303             }
304             mHeadsetProfile = null;
305             disconnectAll();
306         }
307     };
308 
309     // Block management functions
createBlockForDevice(BluetoothDevice device)310     synchronized HfpClientDeviceBlock createBlockForDevice(BluetoothDevice device) {
311         Log.d(TAG, "Creating block for device " + device);
312         if (mDeviceBlocks.containsKey(device)) {
313             Log.e(TAG, "Device already exists " + device + " blocks " + mDeviceBlocks);
314             return null;
315         }
316 
317         HfpClientDeviceBlock block = new HfpClientDeviceBlock(this, device, mHeadsetProfile);
318         mDeviceBlocks.put(device, block);
319         return block;
320     }
321 
findBlockForDevice(BluetoothDevice device)322     synchronized HfpClientDeviceBlock findBlockForDevice(BluetoothDevice device) {
323         Log.d(TAG, "Finding block for device " + device + " blocks " + mDeviceBlocks);
324         return mDeviceBlocks.get(device);
325     }
326 
findBlockForHandle(PhoneAccountHandle handle)327     synchronized HfpClientDeviceBlock findBlockForHandle(PhoneAccountHandle handle) {
328         PhoneAccount account = mTelecomManager.getPhoneAccount(handle);
329         String btAddr = account.getAddress().getSchemeSpecificPart();
330         BluetoothDevice device = mAdapter.getRemoteDevice(btAddr);
331         Log.d(TAG, "Finding block for handle " + handle + " device " + btAddr);
332         return mDeviceBlocks.get(device);
333     }
334 
335     // Util functions that may be used by various classes
createAccount(Context context, BluetoothDevice device)336     public static PhoneAccount createAccount(Context context, BluetoothDevice device) {
337         Uri addr = Uri.fromParts(HfpClientConnectionService.HFP_SCHEME, device.getAddress(), null);
338         PhoneAccountHandle handle = new PhoneAccountHandle(
339             new ComponentName(context, HfpClientConnectionService.class), device.getAddress());
340         PhoneAccount account =
341                 new PhoneAccount.Builder(handle, "HFP " + device.toString())
342                     .setAddress(addr)
343                     .setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_TEL))
344                     .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
345                     .build();
346         if (DBG) {
347             Log.d(TAG, "phoneaccount: " + account);
348         }
349         return account;
350     }
351 
hasHfpClientEcc(BluetoothHeadsetClient client, BluetoothDevice device)352     public static boolean hasHfpClientEcc(BluetoothHeadsetClient client, BluetoothDevice device) {
353         Bundle features = client.getCurrentAgEvents(device);
354         return features == null ? false :
355                 features.getBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_ECC, false);
356     }
357 }
358