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