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