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