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