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