blob: aba50c0244c78b0690c10a4996d9028da70563fa [file] [log] [blame]
/*
* 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<UUID, HfpClientConnection> 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<BluetoothHeadsetClientCall> 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);
}
}
}