• 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.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.List;
47 import java.util.Map;
48 import java.util.UUID;
49 
50 // Helper class that manages the call handling for one device. HfpClientConnectionService holdes a
51 // list of such blocks and routes traffic from the UI.
52 //
53 // Lifecycle of a Device Block is managed entirely by the Service which creates it. In essence it
54 // has only the active state otherwise the block should be GCed.
55 public class HfpClientDeviceBlock {
56     private final String TAG;
57     private final boolean DBG = false;
58     private final Context mContext;
59     private final BluetoothDevice mDevice;
60     private final PhoneAccount mPhoneAccount;
61     private final Map<UUID, HfpClientConnection> mConnections = new HashMap<>();
62     private final TelecomManager mTelecomManager;
63     private final HfpClientConnectionService mConnServ;
64     private HfpClientConference mConference;
65 
66     private BluetoothHeadsetClient mHeadsetProfile;
67 
HfpClientDeviceBlock( HfpClientConnectionService connServ, BluetoothDevice device, BluetoothHeadsetClient headsetProfile)68     HfpClientDeviceBlock(
69             HfpClientConnectionService connServ,
70             BluetoothDevice device,
71             BluetoothHeadsetClient headsetProfile) {
72         mConnServ = connServ;
73         mContext = connServ;
74         mDevice = device;
75         TAG = "HfpClientDeviceBlock." + mDevice.getAddress();
76         mPhoneAccount = HfpClientConnectionService.createAccount(mContext, device);
77         mTelecomManager = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
78 
79         // Register the phone account since block is created only when devices are connected
80         mTelecomManager.registerPhoneAccount(mPhoneAccount);
81         mTelecomManager.enablePhoneAccount(mPhoneAccount.getAccountHandle(), true);
82         mTelecomManager.setUserSelectedOutgoingPhoneAccount(mPhoneAccount.getAccountHandle());
83         mHeadsetProfile = headsetProfile;
84 
85         // Read the current calls and add them to telecom if already present
86         if (mHeadsetProfile != null) {
87             List<BluetoothHeadsetClientCall> calls =
88                     mHeadsetProfile.getCurrentCalls(mDevice);
89             if (DBG) {
90                 Log.d(TAG, "Got calls " + calls);
91             }
92             if (calls == null) {
93                 // We can get null as a return if we are not connected. Hence there may
94                 // be a race in getting the broadcast and HFP Client getting
95                 // disconnected before broadcast gets delivered.
96                 Log.w(TAG, "Got connected but calls were null, ignoring the broadcast");
97                 return;
98             }
99 
100             for (BluetoothHeadsetClientCall call : calls) {
101                 handleCall(call);
102             }
103         } else {
104             Log.e(TAG, "headset profile is null, ignoring broadcast.");
105         }
106     }
107 
onCreateIncomingConnection(BluetoothHeadsetClientCall call)108     synchronized Connection onCreateIncomingConnection(BluetoothHeadsetClientCall call) {
109         HfpClientConnection connection = connection = mConnections.get(call.getUUID());
110         if (connection != null) {
111             connection.onAdded();
112             updateConferenceableConnections();
113             return connection;
114         } else {
115             Log.e(TAG, "Call " + call + " ignored: connection does not exist");
116             return null;
117         }
118     }
119 
onCreateOutgoingConnection(Uri address)120     Connection onCreateOutgoingConnection(Uri address) {
121         HfpClientConnection connection = buildConnection(null, address);
122         if (connection != null) {
123             connection.onAdded();
124         }
125         return connection;
126     }
127 
onCreateUnknownConnection(BluetoothHeadsetClientCall call)128     synchronized Connection onCreateUnknownConnection(BluetoothHeadsetClientCall call) {
129         Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null);
130         HfpClientConnection connection = connection = mConnections.get(call.getUUID());
131 
132         if (connection != null) {
133             connection.onAdded();
134             updateConferenceableConnections();
135             return connection;
136         } else {
137             Log.e(TAG, "Call " + call + " 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 = new HfpClientConference(
145                 mPhoneAccount.getAccountHandle(), mDevice, mHeadsetProfile);
146         }
147 
148         if (connection1.getConference() == null) {
149             mConference.addConnection(connection1);
150         }
151 
152         if (connection2.getConference() == null) {
153             mConference.addConnection(connection2);
154         }
155     }
156 
157     // Remove existing calls and the phone account associated, the object will get garbage
158     // collected soon
cleanup()159     synchronized void cleanup() {
160         Log.d(TAG, "Resetting state for device " + mDevice);
161         disconnectAll();
162         mTelecomManager.unregisterPhoneAccount(mPhoneAccount.getAccountHandle());
163     }
164 
165     // Handle call change
handleCall(BluetoothHeadsetClientCall call)166     synchronized void handleCall(BluetoothHeadsetClientCall call) {
167         if (DBG) {
168             Log.d(TAG, "Got call " + call.toString(true));
169         }
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 BluetoothHeadsetClientCall is a
193             // parcelable we simply pack the entire object in there.
194             Bundle b = new Bundle();
195             if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_DIALING ||
196                 call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ALERTING ||
197                 call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE ||
198                 call.getState() == BluetoothHeadsetClientCall.CALL_STATE_WAITING) {
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(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, call);
202                 mTelecomManager.addNewUnknownCall(mPhoneAccount.getAccountHandle(), b);
203             } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_INCOMING) {
204                 // This is an incoming call.
205                 b.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, call);
206                 mTelecomManager.addNewIncomingCall(mPhoneAccount.getAccountHandle(), b);
207             }
208         } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) {
209             if (DBG) {
210                 Log.d(TAG, "Removing call " + call);
211             }
212             mConnections.remove(call.getUUID());
213         }
214 
215         updateConferenceableConnections();
216     }
217 
218     // Find the connection specified by the key, also update the key with ID if present.
findConnectionKey(BluetoothHeadsetClientCall call)219     private synchronized HfpClientConnection findConnectionKey(BluetoothHeadsetClientCall call) {
220         if (DBG) {
221             Log.d(TAG, "findConnectionKey local key set " + mConnections.toString());
222         }
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, BluetoothHeadsetClientCall newCall)240     private boolean isDisconnectingToActive(HfpClientConnection prevConn,
241             BluetoothHeadsetClientCall newCall) {
242         if (DBG) {
243             Log.d(TAG, "prevConn " + prevConn.isClosing() + " new call " + newCall.getState());
244         }
245         if (prevConn.isClosing() &&
246                 newCall.getState() != BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) {
247             return true;
248         }
249         return false;
250     }
251 
buildConnection( BluetoothHeadsetClientCall call, Uri number)252     private synchronized HfpClientConnection buildConnection(
253             BluetoothHeadsetClientCall call, Uri number) {
254         if (mHeadsetProfile == null) {
255             Log.e(TAG, "Cannot create connection for call " + call + " when Profile not available");
256             return null;
257         }
258 
259         if (call == null && number == null) {
260             Log.e(TAG, "Both call and number cannot be null.");
261             return null;
262         }
263 
264         if (DBG) {
265             Log.d(TAG, "Creating connection on " + mDevice + " for " + call + "/" + number);
266         }
267 
268         HfpClientConnection connection = null;
269         if (call != null) {
270             connection = new HfpClientConnection(mConnServ, mDevice, mHeadsetProfile, call);
271         } else {
272             connection = new HfpClientConnection(mConnServ, mDevice, mHeadsetProfile, number);
273         }
274 
275         if (connection.getState() != Connection.STATE_DISCONNECTED) {
276             mConnections.put(connection.getUUID(), connection);
277         }
278 
279         return connection;
280     }
281 
282     // Updates any conferencable connections.
updateConferenceableConnections()283     private void updateConferenceableConnections() {
284         boolean addConf = false;
285         if (DBG) {
286             Log.d(TAG, "Existing connections: " + mConnections + " existing conference " +
287                 mConference);
288         }
289 
290         // If we have an existing conference call then loop through all connections and update any
291         // connections that may have switched from conference -> non-conference.
292         if (mConference != null) {
293             for (Connection confConn : mConference.getConnections()) {
294                 if (!((HfpClientConnection) confConn).inConference()) {
295                     if (DBG) {
296                         Log.d(TAG, "Removing connection " + confConn + " from conference.");
297                     }
298                     mConference.removeConnection(confConn);
299                 }
300             }
301         }
302 
303         // If we have connections that are not already part of the conference then add them.
304         // NOTE: addConnection takes care of duplicates (by mem addr) and the lifecycle of a
305         // connection is maintained by the UUID.
306         for (Connection otherConn : mConnections.values()) {
307             if (((HfpClientConnection) otherConn).inConference()) {
308                 // If this is the first connection with conference, create the conference first.
309                 if (mConference == null) {
310                     mConference = new HfpClientConference(
311                         mPhoneAccount.getAccountHandle(), mDevice, mHeadsetProfile);
312                 }
313                 if (mConference.addConnection(otherConn)) {
314                     if (DBG) {
315                         Log.d(TAG, "Adding connection " + otherConn + " to conference.");
316                     }
317                     addConf = true;
318                 }
319             }
320         }
321 
322         // If we have no connections in the conference we should simply end it.
323         if (mConference != null && mConference.getConnections().size() == 0) {
324             if (DBG) {
325                 Log.d(TAG, "Conference has no connection, destroying");
326             }
327             mConference.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
328             mConference.destroy();
329             mConference = null;
330         }
331 
332         // If we have a valid conference and not previously added then add it.
333         if (mConference != null && addConf) {
334             if (DBG) {
335                 Log.d(TAG, "Adding conference to stack.");
336             }
337             mConnServ.addConference(mConference);
338         }
339     }
340 
341 }
342