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.BluetoothAdapter; 19 import android.bluetooth.BluetoothDevice; 20 import android.bluetooth.BluetoothHeadsetClient; 21 import android.bluetooth.BluetoothHeadsetClientCall; 22 import android.bluetooth.BluetoothProfile; 23 import android.content.BroadcastReceiver; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.telecom.Connection; 32 import android.telecom.ConnectionRequest; 33 import android.telecom.ConnectionService; 34 import android.telecom.DisconnectCause; 35 import android.telecom.PhoneAccount; 36 import android.telecom.PhoneAccountHandle; 37 import android.telecom.TelecomManager; 38 import android.util.Log; 39 40 import com.android.bluetooth.hfpclient.HeadsetClientService; 41 42 import java.util.Arrays; 43 import java.util.ArrayList; 44 import java.util.Collection; 45 import java.util.HashMap; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.UUID; 49 50 // Helper class that manages the call handling for one device. HfpClientConnectionService holdes a 51 // list of such blocks and routes traffic from the UI. 52 // 53 // Lifecycle of a Device Block is managed entirely by the Service which creates it. In essence it 54 // has only the active state otherwise the block should be GCed. 55 public class HfpClientDeviceBlock { 56 private final String TAG; 57 private final boolean DBG = false; 58 private final Context mContext; 59 private final BluetoothDevice mDevice; 60 private final PhoneAccount mPhoneAccount; 61 private final Map<UUID, HfpClientConnection> mConnections = new HashMap<>(); 62 private final TelecomManager mTelecomManager; 63 private final HfpClientConnectionService mConnServ; 64 private HfpClientConference mConference; 65 66 private BluetoothHeadsetClient mHeadsetProfile; 67 68 HfpClientDeviceBlock( 69 HfpClientConnectionService connServ, 70 BluetoothDevice device, 71 BluetoothHeadsetClient headsetProfile) { 72 mConnServ = connServ; 73 mContext = connServ; 74 mDevice = device; 75 TAG = "HfpClientDeviceBlock." + mDevice.getAddress(); 76 mPhoneAccount = HfpClientConnectionService.createAccount(mContext, device); 77 mTelecomManager = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE); 78 79 // Register the phone account since block is created only when devices are connected 80 mTelecomManager.registerPhoneAccount(mPhoneAccount); 81 mTelecomManager.enablePhoneAccount(mPhoneAccount.getAccountHandle(), true); 82 mTelecomManager.setUserSelectedOutgoingPhoneAccount(mPhoneAccount.getAccountHandle()); 83 mHeadsetProfile = headsetProfile; 84 85 // Read the current calls and add them to telecom if already present 86 if (mHeadsetProfile != null) { 87 List<BluetoothHeadsetClientCall> calls = 88 mHeadsetProfile.getCurrentCalls(mDevice); 89 if (DBG) { 90 Log.d(TAG, "Got calls " + calls); 91 } 92 if (calls == null) { 93 // We can get null as a return if we are not connected. Hence there may 94 // be a race in getting the broadcast and HFP Client getting 95 // disconnected before broadcast gets delivered. 96 Log.w(TAG, "Got connected but calls were null, ignoring the broadcast"); 97 return; 98 } 99 100 for (BluetoothHeadsetClientCall call : calls) { 101 handleCall(call); 102 } 103 } else { 104 Log.e(TAG, "headset profile is null, ignoring broadcast."); 105 } 106 } 107 108 synchronized Connection onCreateIncomingConnection(BluetoothHeadsetClientCall call) { 109 HfpClientConnection connection = connection = mConnections.get(call.getUUID()); 110 if (connection != null) { 111 connection.onAdded(); 112 updateConferenceableConnections(); 113 return connection; 114 } else { 115 Log.e(TAG, "Call " + call + " ignored: connection does not exist"); 116 return null; 117 } 118 } 119 120 Connection onCreateOutgoingConnection(Uri address) { 121 HfpClientConnection connection = buildConnection(null, address); 122 if (connection != null) { 123 connection.onAdded(); 124 } 125 return connection; 126 } 127 128 synchronized Connection onCreateUnknownConnection(BluetoothHeadsetClientCall call) { 129 Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null); 130 HfpClientConnection connection = connection = mConnections.get(call.getUUID()); 131 132 if (connection != null) { 133 connection.onAdded(); 134 updateConferenceableConnections(); 135 return connection; 136 } else { 137 Log.e(TAG, "Call " + call + " ignored: connection does not exist"); 138 return null; 139 } 140 } 141 142 synchronized void onConference(Connection connection1, Connection connection2) { 143 if (mConference == null) { 144 mConference = new HfpClientConference( 145 mPhoneAccount.getAccountHandle(), mDevice, mHeadsetProfile); 146 } 147 148 if (connection1.getConference() == null) { 149 mConference.addConnection(connection1); 150 } 151 152 if (connection2.getConference() == null) { 153 mConference.addConnection(connection2); 154 } 155 } 156 157 // Remove existing calls and the phone account associated, the object will get garbage 158 // collected soon 159 synchronized void cleanup() { 160 Log.d(TAG, "Resetting state for device " + mDevice); 161 disconnectAll(); 162 mTelecomManager.unregisterPhoneAccount(mPhoneAccount.getAccountHandle()); 163 } 164 165 // Handle call change 166 synchronized void handleCall(BluetoothHeadsetClientCall call) { 167 if (DBG) { 168 Log.d(TAG, "Got call " + call.toString(true)); 169 } 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 BluetoothHeadsetClientCall is a 193 // parcelable we simply pack the entire object in there. 194 Bundle b = new Bundle(); 195 if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_DIALING 196 || call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ALERTING 197 || call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) { 198 // This is an outgoing call. Even if it is an active call we do not have a way of 199 // putting that parcelable in a seaprate field. 200 b.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, call); 201 mTelecomManager.addNewUnknownCall(mPhoneAccount.getAccountHandle(), b); 202 } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_INCOMING 203 || call.getState() == BluetoothHeadsetClientCall.CALL_STATE_WAITING) { 204 // This is an incoming call. 205 b.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, call); 206 mTelecomManager.addNewIncomingCall(mPhoneAccount.getAccountHandle(), b); 207 } 208 } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) { 209 if (DBG) { 210 Log.d(TAG, "Removing call " + call); 211 } 212 mConnections.remove(call.getUUID()); 213 } 214 215 updateConferenceableConnections(); 216 } 217 218 // Find the connection specified by the key, also update the key with ID if present. 219 private synchronized HfpClientConnection findConnectionKey(BluetoothHeadsetClientCall call) { 220 if (DBG) { 221 Log.d(TAG, "findConnectionKey local key set " + mConnections.toString()); 222 } 223 return mConnections.get(call.getUUID()); 224 } 225 226 // Disconnect all calls 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 240 private boolean isDisconnectingToActive(HfpClientConnection prevConn, 241 BluetoothHeadsetClientCall newCall) { 242 if (DBG) { 243 Log.d(TAG, "prevConn " + prevConn.isClosing() + " new call " + newCall.getState()); 244 } 245 if (prevConn.isClosing() && 246 newCall.getState() != BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) { 247 return true; 248 } 249 return false; 250 } 251 252 private synchronized HfpClientConnection buildConnection( 253 BluetoothHeadsetClientCall call, Uri number) { 254 if (mHeadsetProfile == null) { 255 Log.e(TAG, "Cannot create connection for call " + call + " when Profile not available"); 256 return null; 257 } 258 259 if (call == null && number == null) { 260 Log.e(TAG, "Both call and number cannot be null."); 261 return null; 262 } 263 264 if (DBG) { 265 Log.d(TAG, "Creating connection on " + mDevice + " for " + call + "/" + number); 266 } 267 268 HfpClientConnection connection = null; 269 if (call != null) { 270 connection = new HfpClientConnection(mConnServ, mDevice, mHeadsetProfile, call); 271 } else { 272 connection = new HfpClientConnection(mConnServ, mDevice, mHeadsetProfile, number); 273 } 274 275 if (connection.getState() != Connection.STATE_DISCONNECTED) { 276 mConnections.put(connection.getUUID(), connection); 277 } 278 279 return connection; 280 } 281 282 // Updates any conferencable connections. 283 private void updateConferenceableConnections() { 284 boolean addConf = false; 285 if (DBG) { 286 Log.d(TAG, "Existing connections: " + mConnections + " existing conference " + 287 mConference); 288 } 289 290 // If we have an existing conference call then loop through all connections and update any 291 // connections that may have switched from conference -> non-conference. 292 if (mConference != null) { 293 for (Connection confConn : mConference.getConnections()) { 294 if (!((HfpClientConnection) confConn).inConference()) { 295 if (DBG) { 296 Log.d(TAG, "Removing connection " + confConn + " from conference."); 297 } 298 mConference.removeConnection(confConn); 299 } 300 } 301 } 302 303 // If we have connections that are not already part of the conference then add them. 304 // NOTE: addConnection takes care of duplicates (by mem addr) and the lifecycle of a 305 // connection is maintained by the UUID. 306 for (Connection otherConn : mConnections.values()) { 307 if (((HfpClientConnection) otherConn).inConference()) { 308 // If this is the first connection with conference, create the conference first. 309 if (mConference == null) { 310 mConference = new HfpClientConference( 311 mPhoneAccount.getAccountHandle(), mDevice, mHeadsetProfile); 312 } 313 if (mConference.addConnection(otherConn)) { 314 if (DBG) { 315 Log.d(TAG, "Adding connection " + otherConn + " to conference."); 316 } 317 addConf = true; 318 } 319 } 320 } 321 322 // If we have no connections in the conference we should simply end it. 323 if (mConference != null && mConference.getConnections().size() == 0) { 324 if (DBG) { 325 Log.d(TAG, "Conference has no connection, destroying"); 326 } 327 mConference.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); 328 mConference.destroy(); 329 mConference = null; 330 } 331 332 // If we have a valid conference and not previously added then add it. 333 if (mConference != null && addConf) { 334 if (DBG) { 335 Log.d(TAG, "Adding conference to stack."); 336 } 337 mConnServ.addConference(mConference); 338 } 339 } 340 341 } 342