/* * 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 = true; 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"); setDisconnected(new DisconnectCause(cause)); mClosed = true; mCurrentCall = null; destroy(); } public synchronized boolean isClosing() { return mClosing; } @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 int hashCode() { return getAddress() == null ? 0 : getAddress().hashCode(); } @Override public String toString() { return "HfpClientConnection{" + getAddress() + "," + stateToString(getState()) + "," + mCurrentCall + "}"; } }