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