• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.BluetoothDevice;
19 import android.bluetooth.BluetoothHeadsetClient;
20 import android.bluetooth.BluetoothHeadsetClientCall;
21 import android.content.Context;
22 import android.net.Uri;
23 import android.os.Bundle;
24 import android.telecom.Connection;
25 import android.telecom.DisconnectCause;
26 import android.telecom.PhoneAccount;
27 import android.telecom.TelecomManager;
28 import android.util.Log;
29 
30 import com.android.bluetooth.hfpclient.HeadsetClientService;
31 
32 import java.util.HashMap;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.UUID;
36 
37 // Helper class that manages the call handling for one device. HfpClientConnectionService holds a
38 // list of such blocks and routes traffic from the UI.
39 //
40 // Lifecycle of a Device Block is managed entirely by the Service which creates it. In essence it
41 // has only the active state otherwise the block should be GCed.
42 public class HfpClientDeviceBlock {
43     private static final String KEY_SCO_STATE = "com.android.bluetooth.hfpclient.SCO_STATE";
44     private static final boolean DBG = false;
45 
46     private final String mTAG;
47     private final Context mContext;
48     private final BluetoothDevice mDevice;
49     private final PhoneAccount mPhoneAccount;
50     private final Map<UUID, HfpClientConnection> mConnections = new HashMap<>();
51     private final TelecomManager mTelecomManager;
52     private final HfpClientConnectionService mConnServ;
53     private HfpClientConference mConference;
54     private BluetoothHeadsetClient mHeadsetProfile;
55     private Bundle mScoState;
56 
HfpClientDeviceBlock(HfpClientConnectionService connServ, BluetoothDevice device, BluetoothHeadsetClient headsetProfile)57     HfpClientDeviceBlock(HfpClientConnectionService connServ, BluetoothDevice device,
58             BluetoothHeadsetClient headsetProfile) {
59         mConnServ = connServ;
60         mContext = connServ;
61         mDevice = device;
62         mTAG = "HfpClientDeviceBlock." + mDevice.getAddress();
63         mPhoneAccount = HfpClientConnectionService.createAccount(mContext, device);
64         mTelecomManager = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
65 
66         // Register the phone account since block is created only when devices are connected
67         mTelecomManager.registerPhoneAccount(mPhoneAccount);
68         mTelecomManager.enablePhoneAccount(mPhoneAccount.getAccountHandle(), true);
69         mTelecomManager.setUserSelectedOutgoingPhoneAccount(mPhoneAccount.getAccountHandle());
70         mHeadsetProfile = headsetProfile;
71         mScoState = getScoStateFromDevice(device);
72         if (DBG) {
73             Log.d(mTAG, "SCO state = " + mScoState);
74         }
75 
76         // Read the current calls and add them to telecom if already present
77         if (mHeadsetProfile != null) {
78             List<BluetoothHeadsetClientCall> calls = mHeadsetProfile.getCurrentCalls(mDevice);
79             if (DBG) {
80                 Log.d(mTAG, "Got calls " + calls);
81             }
82             if (calls == null) {
83                 // We can get null as a return if we are not connected. Hence there may
84                 // be a race in getting the broadcast and HFP Client getting
85                 // disconnected before broadcast gets delivered.
86                 Log.w(mTAG, "Got connected but calls were null, ignoring the broadcast");
87                 return;
88             }
89 
90             for (BluetoothHeadsetClientCall call : calls) {
91                 handleCall(call);
92             }
93         } else {
94             Log.e(mTAG, "headset profile is null, ignoring broadcast.");
95         }
96     }
97 
onCreateIncomingConnection(BluetoothHeadsetClientCall call)98     synchronized HfpClientConnection onCreateIncomingConnection(BluetoothHeadsetClientCall call) {
99         HfpClientConnection connection = mConnections.get(call.getUUID());
100         if (connection != null) {
101             connection.onAdded();
102             return connection;
103         } else {
104             Log.e(mTAG, "Call " + call + " ignored: connection does not exist");
105             return null;
106         }
107     }
108 
onCreateOutgoingConnection(Uri address)109     HfpClientConnection onCreateOutgoingConnection(Uri address) {
110         HfpClientConnection connection = buildConnection(null, address);
111         if (connection != null) {
112             connection.onAdded();
113         }
114         return connection;
115     }
116 
onAudioStateChange(int newState, int oldState)117     synchronized void onAudioStateChange(int newState, int oldState) {
118         if (DBG) {
119             Log.d(mTAG, "Call audio state changed " + oldState + " -> " + newState);
120         }
121         mScoState.putInt(KEY_SCO_STATE, newState);
122 
123         for (HfpClientConnection connection : mConnections.values()) {
124             connection.setExtras(mScoState);
125         }
126         if (mConference != null) {
127             mConference.setExtras(mScoState);
128         }
129     }
130 
onCreateUnknownConnection(BluetoothHeadsetClientCall call)131     synchronized HfpClientConnection onCreateUnknownConnection(BluetoothHeadsetClientCall call) {
132         Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null);
133         HfpClientConnection connection = mConnections.get(call.getUUID());
134 
135         if (connection != null) {
136             connection.onAdded();
137             return connection;
138         } else {
139             Log.e(mTAG, "Call " + call + " ignored: connection does not exist");
140             return null;
141         }
142     }
143 
onConference(Connection connection1, Connection connection2)144     synchronized void onConference(Connection connection1, Connection connection2) {
145         if (mConference == null) {
146             mConference = new HfpClientConference(mPhoneAccount.getAccountHandle(), mDevice,
147                     mHeadsetProfile);
148             mConference.setExtras(mScoState);
149         }
150 
151         if (connection1.getConference() == null) {
152             mConference.addConnection(connection1);
153         }
154 
155         if (connection2.getConference() == null) {
156             mConference.addConnection(connection2);
157         }
158     }
159 
160     // Remove existing calls and the phone account associated, the object will get garbage
161     // collected soon
cleanup()162     synchronized void cleanup() {
163         Log.d(mTAG, "Resetting state for device " + mDevice);
164         disconnectAll();
165         mTelecomManager.unregisterPhoneAccount(mPhoneAccount.getAccountHandle());
166     }
167 
168     // Handle call change
handleCall(BluetoothHeadsetClientCall call)169     synchronized void handleCall(BluetoothHeadsetClientCall call) {
170         if (DBG) {
171             Log.d(mTAG, "Got call " + call.toString(true));
172         }
173 
174         HfpClientConnection connection = findConnectionKey(call);
175 
176         // We need to have special handling for calls that mysteriously convert from
177         // DISCONNECTING -> ACTIVE/INCOMING state. This can happen for PTS (b/31159015).
178         // We terminate the previous call and create a new one here.
179         if (connection != null && isDisconnectingToActive(connection, call)) {
180             connection.close(DisconnectCause.ERROR);
181             mConnections.remove(call.getUUID());
182             connection = null;
183         }
184 
185         if (connection != null) {
186             connection.updateCall(call);
187             connection.handleCallChanged();
188         }
189 
190         if (connection == null) {
191             // Create the connection here, trigger Telecom to bind to us.
192             buildConnection(call, null);
193 
194             // Depending on where this call originated make it an incoming call or outgoing
195             // (represented as unknown call in telecom since). Since BluetoothHeadsetClientCall is a
196             // parcelable we simply pack the entire object in there.
197             Bundle b = new Bundle();
198             if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_DIALING
199                     || call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ALERTING
200                     || call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE
201                     || call.getState() == BluetoothHeadsetClientCall.CALL_STATE_HELD) {
202                 // This is an outgoing call. Even if it is an active call we do not have a way of
203                 // putting that parcelable in a seaprate field.
204                 b.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, call);
205                 mTelecomManager.addNewUnknownCall(mPhoneAccount.getAccountHandle(), b);
206             } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_INCOMING
207                     || call.getState() == BluetoothHeadsetClientCall.CALL_STATE_WAITING) {
208                 // This is an incoming call.
209                 b.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, call);
210                 b.putBoolean(TelecomManager.EXTRA_CALL_EXTERNAL_RINGER, call.isInBandRing());
211                 mTelecomManager.addNewIncomingCall(mPhoneAccount.getAccountHandle(), b);
212             }
213         } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) {
214             if (DBG) {
215                 Log.d(mTAG, "Removing call " + call);
216             }
217             mConnections.remove(call.getUUID());
218         }
219 
220         updateConferenceableConnections();
221     }
222 
223     // Find the connection specified by the key, also update the key with ID if present.
findConnectionKey(BluetoothHeadsetClientCall call)224     private synchronized HfpClientConnection findConnectionKey(BluetoothHeadsetClientCall call) {
225         if (DBG) {
226             Log.d(mTAG, "findConnectionKey local key set " + mConnections.toString());
227         }
228         return mConnections.get(call.getUUID());
229     }
230 
231     // Disconnect all calls
disconnectAll()232     private void disconnectAll() {
233         for (HfpClientConnection connection : mConnections.values()) {
234             connection.onHfpDisconnected();
235         }
236 
237         mConnections.clear();
238 
239         if (mConference != null) {
240             mConference.destroy();
241             mConference = null;
242         }
243     }
244 
isDisconnectingToActive(HfpClientConnection prevConn, BluetoothHeadsetClientCall newCall)245     private boolean isDisconnectingToActive(HfpClientConnection prevConn,
246             BluetoothHeadsetClientCall newCall) {
247         if (DBG) {
248             Log.d(mTAG, "prevConn " + prevConn.isClosing() + " new call " + newCall.getState());
249         }
250         if (prevConn.isClosing() && prevConn.getCall().getState() != newCall.getState()
251                 && newCall.getState() != BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) {
252             return true;
253         }
254         return false;
255     }
256 
buildConnection(BluetoothHeadsetClientCall call, Uri number)257     private synchronized HfpClientConnection buildConnection(BluetoothHeadsetClientCall call,
258             Uri number) {
259         if (mHeadsetProfile == null) {
260             Log.e(mTAG,
261                     "Cannot create connection for call " + call + " when Profile not available");
262             return null;
263         }
264 
265         if (call == null && number == null) {
266             Log.e(mTAG, "Both call and number cannot be null.");
267             return null;
268         }
269 
270         if (DBG) {
271             Log.d(mTAG, "Creating connection on " + mDevice + " for " + call + "/" + number);
272         }
273 
274         HfpClientConnection connection = null;
275         if (call != null) {
276             connection = new HfpClientConnection(mConnServ, mDevice, mHeadsetProfile, call);
277         } else {
278             connection = new HfpClientConnection(mConnServ, mDevice, mHeadsetProfile, number);
279         }
280         connection.setExtras(mScoState);
281         if (DBG) {
282             Log.d(mTAG, "Connection extras = " + connection.getExtras().toString());
283         }
284 
285         if (connection.getState() != Connection.STATE_DISCONNECTED) {
286             mConnections.put(connection.getUUID(), connection);
287         }
288 
289         return connection;
290     }
291 
292     // Updates any conferencable connections.
updateConferenceableConnections()293     private void updateConferenceableConnections() {
294         boolean addConf = false;
295         if (DBG) {
296             Log.d(mTAG, "Existing connections: " + mConnections + " existing conference "
297                     + mConference);
298         }
299 
300         // If we have an existing conference call then loop through all connections and update any
301         // connections that may have switched from conference -> non-conference.
302         if (mConference != null) {
303             for (Connection confConn : mConference.getConnections()) {
304                 if (!((HfpClientConnection) confConn).inConference()) {
305                     if (DBG) {
306                         Log.d(mTAG, "Removing connection " + confConn + " from conference.");
307                     }
308                     mConference.removeConnection(confConn);
309                 }
310             }
311         }
312 
313         // If we have connections that are not already part of the conference then add them.
314         // NOTE: addConnection takes care of duplicates (by mem addr) and the lifecycle of a
315         // connection is maintained by the UUID.
316         for (Connection otherConn : mConnections.values()) {
317             if (((HfpClientConnection) otherConn).inConference()) {
318                 // If this is the first connection with conference, create the conference first.
319                 if (mConference == null) {
320                     mConference = new HfpClientConference(mPhoneAccount.getAccountHandle(), mDevice,
321                             mHeadsetProfile);
322                     mConference.setExtras(mScoState);
323                 }
324                 if (mConference.addConnection(otherConn)) {
325                     if (DBG) {
326                         Log.d(mTAG, "Adding connection " + otherConn + " to conference.");
327                     }
328                     addConf = true;
329                 }
330             }
331         }
332 
333         // If we have no connections in the conference we should simply end it.
334         if (mConference != null && mConference.getConnections().size() == 0) {
335             if (DBG) {
336                 Log.d(mTAG, "Conference has no connection, destroying");
337             }
338             mConference.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
339             mConference.destroy();
340             mConference = null;
341         }
342 
343         // If we have a valid conference and not previously added then add it.
344         if (mConference != null && addConf) {
345             if (DBG) {
346                 Log.d(mTAG, "Adding conference to stack.");
347             }
348             mConnServ.addConference(mConference);
349         }
350     }
351 
getScoStateFromDevice(BluetoothDevice device)352     private Bundle getScoStateFromDevice(BluetoothDevice device) {
353         Bundle bundle = new Bundle();
354 
355         HeadsetClientService headsetClientService = HeadsetClientService.getHeadsetClientService();
356         if (headsetClientService == null) {
357             return bundle;
358         }
359 
360         bundle.putInt(KEY_SCO_STATE, headsetClientService.getAudioState(device));
361 
362         return bundle;
363     }
364 }
365