/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth.hfpclient.connserv; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadsetClient; import android.bluetooth.BluetoothHeadsetClientCall; import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.telecom.Connection; import android.telecom.DisconnectCause; import android.telecom.PhoneAccount; import android.telecom.TelecomManager; import android.util.Log; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; // Helper class that manages the call handling for one device. HfpClientConnectionService holdes a // list of such blocks and routes traffic from the UI. // // Lifecycle of a Device Block is managed entirely by the Service which creates it. In essence it // has only the active state otherwise the block should be GCed. public class HfpClientDeviceBlock { private final String mTAG; private static final boolean DBG = false; private final Context mContext; private final BluetoothDevice mDevice; private final PhoneAccount mPhoneAccount; private final Map mConnections = new HashMap<>(); private final TelecomManager mTelecomManager; private final HfpClientConnectionService mConnServ; private HfpClientConference mConference; private BluetoothHeadsetClient mHeadsetProfile; HfpClientDeviceBlock(HfpClientConnectionService connServ, BluetoothDevice device, BluetoothHeadsetClient headsetProfile) { mConnServ = connServ; mContext = connServ; mDevice = device; mTAG = "HfpClientDeviceBlock." + mDevice.getAddress(); mPhoneAccount = HfpClientConnectionService.createAccount(mContext, device); mTelecomManager = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE); // Register the phone account since block is created only when devices are connected mTelecomManager.registerPhoneAccount(mPhoneAccount); mTelecomManager.enablePhoneAccount(mPhoneAccount.getAccountHandle(), true); mTelecomManager.setUserSelectedOutgoingPhoneAccount(mPhoneAccount.getAccountHandle()); mHeadsetProfile = headsetProfile; // Read the current calls and add them to telecom if already present if (mHeadsetProfile != null) { List calls = mHeadsetProfile.getCurrentCalls(mDevice); if (DBG) { Log.d(mTAG, "Got calls " + calls); } if (calls == null) { // We can get null as a return if we are not connected. Hence there may // be a race in getting the broadcast and HFP Client getting // disconnected before broadcast gets delivered. Log.w(mTAG, "Got connected but calls were null, ignoring the broadcast"); return; } for (BluetoothHeadsetClientCall call : calls) { handleCall(call); } } else { Log.e(mTAG, "headset profile is null, ignoring broadcast."); } } synchronized HfpClientConnection onCreateIncomingConnection(BluetoothHeadsetClientCall call) { HfpClientConnection connection = mConnections.get(call.getUUID()); if (connection != null) { connection.onAdded(); return connection; } else { Log.e(mTAG, "Call " + call + " ignored: connection does not exist"); return null; } } HfpClientConnection onCreateOutgoingConnection(Uri address) { HfpClientConnection connection = buildConnection(null, address); if (connection != null) { connection.onAdded(); } return connection; } synchronized HfpClientConnection onCreateUnknownConnection(BluetoothHeadsetClientCall call) { Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null); HfpClientConnection connection = mConnections.get(call.getUUID()); if (connection != null) { connection.onAdded(); return connection; } else { Log.e(mTAG, "Call " + call + " ignored: connection does not exist"); return null; } } synchronized void onConference(Connection connection1, Connection connection2) { if (mConference == null) { mConference = new HfpClientConference(mPhoneAccount.getAccountHandle(), mDevice, mHeadsetProfile); } if (connection1.getConference() == null) { mConference.addConnection(connection1); } if (connection2.getConference() == null) { mConference.addConnection(connection2); } } // Remove existing calls and the phone account associated, the object will get garbage // collected soon synchronized void cleanup() { Log.d(mTAG, "Resetting state for device " + mDevice); disconnectAll(); mTelecomManager.unregisterPhoneAccount(mPhoneAccount.getAccountHandle()); } // Handle call change synchronized void handleCall(BluetoothHeadsetClientCall call) { if (DBG) { Log.d(mTAG, "Got call " + call.toString(true)); } HfpClientConnection connection = findConnectionKey(call); // We need to have special handling for calls that mysteriously convert from // DISCONNECTING -> ACTIVE/INCOMING state. This can happen for PTS (b/31159015). // We terminate the previous call and create a new one here. if (connection != null && isDisconnectingToActive(connection, call)) { connection.close(DisconnectCause.ERROR); mConnections.remove(call.getUUID()); connection = null; } if (connection != null) { connection.updateCall(call); connection.handleCallChanged(); } if (connection == null) { // Create the connection here, trigger Telecom to bind to us. buildConnection(call, null); // Depending on where this call originated make it an incoming call or outgoing // (represented as unknown call in telecom since). Since BluetoothHeadsetClientCall is a // parcelable we simply pack the entire object in there. Bundle b = new Bundle(); if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_DIALING || call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ALERTING || call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE || call.getState() == BluetoothHeadsetClientCall.CALL_STATE_HELD) { // This is an outgoing call. Even if it is an active call we do not have a way of // putting that parcelable in a seaprate field. b.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, call); mTelecomManager.addNewUnknownCall(mPhoneAccount.getAccountHandle(), b); } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_INCOMING || call.getState() == BluetoothHeadsetClientCall.CALL_STATE_WAITING) { // This is an incoming call. b.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, call); b.putBoolean(TelecomManager.EXTRA_CALL_EXTERNAL_RINGER, call.isInBandRing()); mTelecomManager.addNewIncomingCall(mPhoneAccount.getAccountHandle(), b); } } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) { if (DBG) { Log.d(mTAG, "Removing call " + call); } mConnections.remove(call.getUUID()); } updateConferenceableConnections(); } // Find the connection specified by the key, also update the key with ID if present. private synchronized HfpClientConnection findConnectionKey(BluetoothHeadsetClientCall call) { if (DBG) { Log.d(mTAG, "findConnectionKey local key set " + mConnections.toString()); } return mConnections.get(call.getUUID()); } // Disconnect all calls private void disconnectAll() { for (HfpClientConnection connection : mConnections.values()) { connection.onHfpDisconnected(); } mConnections.clear(); if (mConference != null) { mConference.destroy(); mConference = null; } } private boolean isDisconnectingToActive(HfpClientConnection prevConn, BluetoothHeadsetClientCall newCall) { if (DBG) { Log.d(mTAG, "prevConn " + prevConn.isClosing() + " new call " + newCall.getState()); } if (prevConn.isClosing() && prevConn.getCall().getState() != newCall.getState() && newCall.getState() != BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) { return true; } return false; } private synchronized HfpClientConnection buildConnection(BluetoothHeadsetClientCall call, Uri number) { if (mHeadsetProfile == null) { Log.e(mTAG, "Cannot create connection for call " + call + " when Profile not available"); return null; } if (call == null && number == null) { Log.e(mTAG, "Both call and number cannot be null."); return null; } if (DBG) { Log.d(mTAG, "Creating connection on " + mDevice + " for " + call + "/" + number); } HfpClientConnection connection = null; if (call != null) { connection = new HfpClientConnection(mConnServ, mDevice, mHeadsetProfile, call); } else { connection = new HfpClientConnection(mConnServ, mDevice, mHeadsetProfile, number); } if (connection.getState() != Connection.STATE_DISCONNECTED) { mConnections.put(connection.getUUID(), connection); } return connection; } // Updates any conferencable connections. private void updateConferenceableConnections() { boolean addConf = false; if (DBG) { Log.d(mTAG, "Existing connections: " + mConnections + " existing conference " + mConference); } // If we have an existing conference call then loop through all connections and update any // connections that may have switched from conference -> non-conference. if (mConference != null) { for (Connection confConn : mConference.getConnections()) { if (!((HfpClientConnection) confConn).inConference()) { if (DBG) { Log.d(mTAG, "Removing connection " + confConn + " from conference."); } mConference.removeConnection(confConn); } } } // If we have connections that are not already part of the conference then add them. // NOTE: addConnection takes care of duplicates (by mem addr) and the lifecycle of a // connection is maintained by the UUID. for (Connection otherConn : mConnections.values()) { if (((HfpClientConnection) otherConn).inConference()) { // If this is the first connection with conference, create the conference first. if (mConference == null) { mConference = new HfpClientConference(mPhoneAccount.getAccountHandle(), mDevice, mHeadsetProfile); } if (mConference.addConnection(otherConn)) { if (DBG) { Log.d(mTAG, "Adding connection " + otherConn + " to conference."); } addConf = true; } } } // If we have no connections in the conference we should simply end it. if (mConference != null && mConference.getConnections().size() == 0) { if (DBG) { Log.d(mTAG, "Conference has no connection, destroying"); } mConference.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); mConference.destroy(); mConference = null; } // If we have a valid conference and not previously added then add it. if (mConference != null && addConf) { if (DBG) { Log.d(mTAG, "Adding conference to stack."); } mConnServ.addConference(mConference); } } }