blob: 749bb7542afbecf656df42935118cd08c9297b35 [file] [log] [blame]
/*
* Copyright (C) 2016 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.bluetooth.BluetoothProfile;
import android.content.Context;
import android.net.Uri;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.TelecomManager;
import android.util.Log;
import java.util.UUID;
public class HfpClientConnection extends Connection {
private static final String TAG = "HfpClientConnection";
private static final boolean DBG = false;
private final Context mContext;
private final BluetoothDevice mDevice;
private BluetoothHeadsetClient mHeadsetProfile;
private BluetoothHeadsetClientCall mCurrentCall;
private boolean mClosed;
private boolean mClosing = false;
private boolean mLocalDisconnect;
private boolean mClientHasEcc;
private boolean mAdded;
// Constructor to be used when there's an existing call (such as that created on the AG or
// when connection happens and we see calls for the first time).
public HfpClientConnection(Context context, BluetoothDevice device,
BluetoothHeadsetClient client, BluetoothHeadsetClientCall call) {
mDevice = device;
mContext = context;
mHeadsetProfile = client;
if (call == null) {
throw new IllegalStateException("Call is null");
}
mCurrentCall = call;
handleCallChanged();
finishInitializing();
}
// Constructor to be used when a call is intiated on the HF. The call handle is obtained by
// using the dial() command.
public HfpClientConnection(Context context, BluetoothDevice device,
BluetoothHeadsetClient client, Uri number) {
mDevice = device;
mContext = context;
mHeadsetProfile = client;
if (mHeadsetProfile == null) {
throw new IllegalStateException("HeadsetProfile is null, returning");
}
mCurrentCall = mHeadsetProfile.dial(
mDevice, number.getSchemeSpecificPart());
if (mCurrentCall == null) {
close(DisconnectCause.ERROR);
Log.e(TAG, "Failed to create the call, dial failed.");
return;
}
setInitializing();
setDialing();
finishInitializing();
}
void finishInitializing() {
mClientHasEcc = HfpClientConnectionService.hasHfpClientEcc(mHeadsetProfile, mDevice);
setAudioModeIsVoip(false);
Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, mCurrentCall.getNumber(), null);
setAddress(number, TelecomManager.PRESENTATION_ALLOWED);
setConnectionCapabilities(CAPABILITY_SUPPORT_HOLD | CAPABILITY_MUTE |
CAPABILITY_SEPARATE_FROM_CONFERENCE | CAPABILITY_DISCONNECT_FROM_CONFERENCE |
(getState() == STATE_ACTIVE || getState() == STATE_HOLDING ? CAPABILITY_HOLD : 0));
}
public UUID getUUID() {
return mCurrentCall.getUUID();
}
public void onHfpDisconnected() {
mHeadsetProfile = null;
close(DisconnectCause.ERROR);
}
public void onAdded() {
mAdded = true;
}
public BluetoothHeadsetClientCall getCall() {
return mCurrentCall;
}
public boolean inConference() {
return mAdded && mCurrentCall != null && mCurrentCall.isMultiParty() &&
getState() != Connection.STATE_DISCONNECTED;
}
public void enterPrivateMode() {
mHeadsetProfile.enterPrivateMode(mDevice, mCurrentCall.getId());
setActive();
}
public void updateCall(BluetoothHeadsetClientCall call) {
if (call == null) {
Log.e(TAG, "Updating call to a null value.");
return;
}
mCurrentCall = call;
}
public void handleCallChanged() {
HfpClientConference conference = (HfpClientConference) getConference();
int state = mCurrentCall.getState();
if (DBG) {
Log.d(TAG, "Got call state change to " + state);
}
switch (state) {
case BluetoothHeadsetClientCall.CALL_STATE_ACTIVE:
setActive();
if (conference != null) {
conference.setActive();
}
break;
case BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD:
case BluetoothHeadsetClientCall.CALL_STATE_HELD:
setOnHold();
if (conference != null) {
conference.setOnHold();
}
break;
case BluetoothHeadsetClientCall.CALL_STATE_DIALING:
case BluetoothHeadsetClientCall.CALL_STATE_ALERTING:
setDialing();
break;
case BluetoothHeadsetClientCall.CALL_STATE_INCOMING:
case BluetoothHeadsetClientCall.CALL_STATE_WAITING:
setRinging();
break;
case BluetoothHeadsetClientCall.CALL_STATE_TERMINATED:
// TODO Use more specific causes
close(mLocalDisconnect ? DisconnectCause.LOCAL : DisconnectCause.REMOTE);
break;
default:
Log.wtf(TAG, "Unexpected phone state " + state);
}
}
public synchronized void close(int cause) {
if (DBG) {
Log.d(TAG, "Closing call " + mCurrentCall + "state: " + mClosed);
}
if (mClosed) {
return;
}
Log.d(TAG, "Setting " + mCurrentCall + " to disconnected " + getTelecomCallId());
setDisconnected(new DisconnectCause(cause));
mClosed = true;
mCurrentCall = null;
destroy();
}
public synchronized boolean isClosing() {
return mClosing;
}
public synchronized BluetoothDevice getDevice() {
return mDevice;
}
@Override
public synchronized void onPlayDtmfTone(char c) {
if (DBG) {
Log.d(TAG, "onPlayDtmfTone " + c + " " + mCurrentCall);
}
if (!mClosed) {
mHeadsetProfile.sendDTMF(mDevice, (byte) c);
}
}
@Override
public synchronized void onDisconnect() {
if (DBG) {
Log.d(TAG, "onDisconnect call: " + mCurrentCall + " state: " + mClosed);
}
// The call is not closed so we should send a terminate here.
if (!mClosed) {
mHeadsetProfile.terminateCall(mDevice, mCurrentCall);
mLocalDisconnect = true;
mClosing = true;
}
}
@Override
public void onAbort() {
if (DBG) {
Log.d(TAG, "onAbort " + mCurrentCall);
}
onDisconnect();
}
@Override
public synchronized void onHold() {
if (DBG) {
Log.d(TAG, "onHold " + mCurrentCall);
}
if (!mClosed) {
mHeadsetProfile.holdCall(mDevice);
}
}
@Override
public synchronized void onUnhold() {
if (getConnectionService().getAllConnections().size() > 1) {
Log.w(TAG, "Ignoring unhold; call hold on the foreground call");
return;
}
if (DBG) {
Log.d(TAG, "onUnhold " + mCurrentCall);
}
if (!mClosed) {
mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_HOLD);
}
}
@Override
public synchronized void onAnswer() {
if (DBG) {
Log.d(TAG, "onAnswer " + mCurrentCall);
}
if (!mClosed) {
mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_NONE);
}
}
@Override
public synchronized void onReject() {
if (DBG) {
Log.d(TAG, "onReject " + mCurrentCall);
}
if (!mClosed) {
mHeadsetProfile.rejectCall(mDevice);
}
}
@Override
public boolean equals(Object o) {
if (!(o instanceof HfpClientConnection)) {
return false;
}
Uri otherAddr = ((HfpClientConnection) o).getAddress();
return getAddress() == otherAddr || otherAddr != null && otherAddr.equals(getAddress());
}
@Override
public String toString() {
return "HfpClientConnection{" + getAddress() + "," + stateToString(getState()) + "," +
mCurrentCall + "}";
}
}