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 TAG = HfpClientDeviceBlock.class.getSimpleName(); 42 43 private static final String KEY_SCO_STATE = "com.android.bluetooth.hfpclient.SCO_STATE"; 44 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( 55 BluetoothDevice device, 56 HfpClientConnectionService connServ, 57 HeadsetClientServiceInterface serviceInterface) { 58 mDevice = device; 59 mConnServ = connServ; 60 mServiceInterface = serviceInterface; 61 mPhoneAccount = mConnServ.createAccount(device); 62 mTelecomManager = mConnServ.getSystemService(TelecomManager.class); 63 64 // Register the phone account since block is created only when devices are connected 65 mTelecomManager.registerPhoneAccount(mPhoneAccount); 66 mTelecomManager.enablePhoneAccount(mPhoneAccount.getAccountHandle(), true); 67 mTelecomManager.setUserSelectedOutgoingPhoneAccount(mPhoneAccount.getAccountHandle()); 68 69 mScoState = getScoStateFromDevice(device); 70 debug("SCO state = " + mScoState); 71 72 List<HfpClientCall> calls = mServiceInterface.getCurrentCalls(mDevice); 73 debug("Got calls " + calls); 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 warn("Got connected but calls were null, ignoring the broadcast"); 79 return; 80 } 81 82 for (HfpClientCall call : calls) { 83 handleCall(call); 84 } 85 } 86 getDevice()87 public BluetoothDevice getDevice() { 88 return mDevice; 89 } 90 getAudioState()91 public int getAudioState() { 92 return mScoState.getInt(KEY_SCO_STATE); 93 } 94 getCalls()95 /* package */ Map<UUID, HfpClientConnection> getCalls() { 96 return mConnections; 97 } 98 onCreateIncomingConnection(UUID callUuid)99 synchronized HfpClientConnection onCreateIncomingConnection(UUID callUuid) { 100 HfpClientConnection connection = mConnections.get(callUuid); 101 if (connection != null) { 102 connection.onAdded(); 103 return connection; 104 } else { 105 error("Call " + callUuid + " ignored: connection does not exist"); 106 return null; 107 } 108 } 109 onCreateOutgoingConnection(Uri address)110 HfpClientConnection onCreateOutgoingConnection(Uri address) { 111 HfpClientConnection connection = buildConnection(null, address); 112 if (connection != null) { 113 connection.onAdded(); 114 } 115 return connection; 116 } 117 onAudioStateChange(int newState, int oldState)118 synchronized void onAudioStateChange(int newState, int oldState) { 119 debug("Call audio state changed " + oldState + " -> " + newState); 120 mScoState.putInt(KEY_SCO_STATE, newState); 121 122 for (HfpClientConnection connection : mConnections.values()) { 123 connection.setExtras(mScoState); 124 } 125 if (mConference != null) { 126 mConference.setExtras(mScoState); 127 } 128 } 129 onCreateUnknownConnection(UUID callUuid)130 synchronized HfpClientConnection onCreateUnknownConnection(UUID callUuid) { 131 HfpClientConnection connection = mConnections.get(callUuid); 132 133 if (connection != null) { 134 connection.onAdded(); 135 return connection; 136 } else { 137 error("Call " + callUuid + " 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 = 145 new HfpClientConference( 146 mDevice, mPhoneAccount.getAccountHandle(), mServiceInterface); 147 mConference.setExtras(mScoState); 148 } 149 150 if (connection1.getConference() == null) { 151 mConference.addConnection(connection1); 152 } 153 154 if (connection2.getConference() == null) { 155 mConference.addConnection(connection2); 156 } 157 } 158 159 // Remove existing calls and the phone account associated, the object will get garbage 160 // collected soon cleanup()161 synchronized void cleanup() { 162 debug("Resetting state for device " + mDevice); 163 disconnectAll(); 164 mTelecomManager.unregisterPhoneAccount(mPhoneAccount.getAccountHandle()); 165 } 166 167 // Handle call change handleCall(HfpClientCall call)168 synchronized void handleCall(HfpClientCall call) { 169 debug("Got call " + call.toString()); 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 HfpClientCall is a 193 // parcelable we simply pack the entire object in there. 194 Bundle b = new Bundle(); 195 if (call.getState() == HfpClientCall.CALL_STATE_DIALING 196 || call.getState() == HfpClientCall.CALL_STATE_ALERTING 197 || call.getState() == HfpClientCall.CALL_STATE_ACTIVE 198 || call.getState() == HfpClientCall.CALL_STATE_HELD) { 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( 202 TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, new ParcelUuid(call.getUUID())); 203 mTelecomManager.addNewUnknownCall(mPhoneAccount.getAccountHandle(), b); 204 } else if (call.getState() == HfpClientCall.CALL_STATE_INCOMING 205 || call.getState() == HfpClientCall.CALL_STATE_WAITING) { 206 // This is an incoming call. 207 b.putParcelable( 208 TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, new ParcelUuid(call.getUUID())); 209 b.putBoolean(TelecomManager.EXTRA_CALL_HAS_IN_BAND_RINGTONE, call.isInBandRing()); 210 mTelecomManager.addNewIncomingCall(mPhoneAccount.getAccountHandle(), b); 211 } 212 } else if (call.getState() == HfpClientCall.CALL_STATE_TERMINATED) { 213 debug("Removing call " + call); 214 mConnections.remove(call.getUUID()); 215 } 216 217 updateConferenceableConnections(); 218 } 219 220 // Find the connection specified by the key, also update the key with ID if present. findConnectionKey(HfpClientCall call)221 private synchronized HfpClientConnection findConnectionKey(HfpClientCall call) { 222 debug("findConnectionKey local key set " + mConnections.toString()); 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, HfpClientCall newCall)240 private boolean isDisconnectingToActive(HfpClientConnection prevConn, HfpClientCall newCall) { 241 debug("prevConn " + prevConn.isClosing() + " new call " + newCall.getState()); 242 if (prevConn.isClosing() 243 && prevConn.getCall().getState() != newCall.getState() 244 && newCall.getState() != HfpClientCall.CALL_STATE_TERMINATED) { 245 return true; 246 } 247 return false; 248 } 249 buildConnection(HfpClientCall call, Uri number)250 private synchronized HfpClientConnection buildConnection(HfpClientCall call, Uri number) { 251 if (call == null && number == null) { 252 error("Both call and number cannot be null."); 253 return null; 254 } 255 256 debug("Creating connection on " + mDevice + " for " + call + "/" + number); 257 258 HfpClientConnection connection = 259 (call != null 260 ? new HfpClientConnection(mDevice, call, mConnServ, mServiceInterface) 261 : new HfpClientConnection(mDevice, number, mConnServ, mServiceInterface)); 262 connection.setExtras(mScoState); 263 264 debug("Connection extras = " + connection.getExtras().toString()); 265 266 if (connection.getState() != Connection.STATE_DISCONNECTED) { 267 mConnections.put(connection.getUUID(), connection); 268 } 269 270 return connection; 271 } 272 273 // Updates any conferencable connections. updateConferenceableConnections()274 private void updateConferenceableConnections() { 275 boolean addConf = false; 276 debug("Existing connections: " + mConnections + " existing conference " + mConference); 277 278 // If we have an existing conference call then loop through all connections and update any 279 // connections that may have switched from conference -> non-conference. 280 if (mConference != null) { 281 for (Connection confConn : mConference.getConnections()) { 282 if (!((HfpClientConnection) confConn).inConference()) { 283 debug("Removing connection " + confConn + " from conference."); 284 mConference.removeConnection(confConn); 285 } 286 } 287 } 288 289 // If we have connections that are not already part of the conference then add them. 290 // NOTE: addConnection takes care of duplicates (by mem addr) and the lifecycle of a 291 // connection is maintained by the UUID. 292 for (Connection otherConn : mConnections.values()) { 293 if (((HfpClientConnection) otherConn).inConference()) { 294 // If this is the first connection with conference, create the conference first. 295 if (mConference == null) { 296 mConference = 297 new HfpClientConference( 298 mDevice, mPhoneAccount.getAccountHandle(), mServiceInterface); 299 mConference.setExtras(mScoState); 300 } 301 if (mConference.addConnection(otherConn)) { 302 debug("Adding connection " + otherConn + " to conference."); 303 addConf = true; 304 } 305 } 306 } 307 308 // If we have no connections in the conference we should simply end it. 309 if (mConference != null && mConference.getConnections().size() == 0) { 310 debug("Conference has no connection, destroying"); 311 mConference.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); 312 mConference.destroy(); 313 mConference = null; 314 } 315 316 // If we have a valid conference and not previously added then add it. 317 if (mConference != null && addConf) { 318 debug("Adding conference to stack."); 319 mConnServ.addConference(mConference); 320 } 321 } 322 getScoStateFromDevice(BluetoothDevice device)323 private Bundle getScoStateFromDevice(BluetoothDevice device) { 324 Bundle bundle = new Bundle(); 325 326 HeadsetClientService headsetClientService = HeadsetClientService.getHeadsetClientService(); 327 if (headsetClientService == null) { 328 return bundle; 329 } 330 331 bundle.putInt(KEY_SCO_STATE, headsetClientService.getAudioState(device)); 332 333 return bundle; 334 } 335 336 @Override toString()337 public String toString() { 338 StringBuilder sb = new StringBuilder(); 339 sb.append("<HfpClientDeviceBlock"); 340 sb.append(" device=" + mDevice); 341 sb.append(" account=" + mPhoneAccount); 342 sb.append(" connections=["); 343 boolean first = true; 344 for (HfpClientConnection connection : mConnections.values()) { 345 if (!first) { 346 sb.append(", "); 347 } 348 sb.append(connection.toString()); 349 first = false; 350 } 351 sb.append("]"); 352 sb.append(" conference=" + mConference); 353 sb.append(">"); 354 return sb.toString(); 355 } 356 357 /** Factory class for {@link HfpClientDeviceBlock} */ 358 public static class Factory { 359 private static Factory sInstance = new Factory(); 360 361 @VisibleForTesting setInstance(Factory instance)362 static void setInstance(Factory instance) { 363 sInstance = instance; 364 } 365 366 /** Returns an instance of {@link HfpClientDeviceBlock} */ build( BluetoothDevice device, HfpClientConnectionService connServ, HeadsetClientServiceInterface serviceInterface)367 public static HfpClientDeviceBlock build( 368 BluetoothDevice device, 369 HfpClientConnectionService connServ, 370 HeadsetClientServiceInterface serviceInterface) { 371 return sInstance.buildInternal(device, connServ, serviceInterface); 372 } 373 buildInternal( BluetoothDevice device, HfpClientConnectionService connServ, HeadsetClientServiceInterface serviceInterface)374 protected HfpClientDeviceBlock buildInternal( 375 BluetoothDevice device, 376 HfpClientConnectionService connServ, 377 HeadsetClientServiceInterface serviceInterface) { 378 return new HfpClientDeviceBlock(device, connServ, serviceInterface); 379 } 380 } 381 382 // Per-Device logging 383 debug(String message)384 public void debug(String message) { 385 Log.d(TAG, "[" + mDevice + "] " + message); 386 } 387 warn(String message)388 public void warn(String message) { 389 Log.w(TAG, "[" + mDevice + "] " + message); 390 } 391 error(String message)392 public void error(String message) { 393 Log.e(TAG, "[" + mDevice + "] " + message); 394 } 395 } 396