blob: 5c69e1795a43cfa23169aecbec6d7506ef31ac90 [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.
*/
/**
* Bluetooth Headset Client StateMachine
* (Disconnected)
* | ^ ^
* CONNECT | | | DISCONNECTED
* V | |
* (Connecting) |
* | |
* CONNECTED | | DISCONNECT
* V |
* (Connected)
* | ^
* CONNECT_AUDIO | | DISCONNECT_AUDIO
* V |
* (AudioOn)
*/
package com.android.bluetooth.hfpclient;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHeadsetClientCall;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.hfp.BluetoothHfpProtoEnums;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.SystemClock;
import android.util.Log;
import android.util.Pair;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.BluetoothStatsLog;
import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.statemachine.IState;
import com.android.bluetooth.statemachine.State;
import com.android.bluetooth.statemachine.StateMachine;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Scanner;
import java.util.Set;
public class HeadsetClientStateMachine extends StateMachine {
private static final String TAG = "HeadsetClientStateMachine";
private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
static final int NO_ACTION = 0;
static final int IN_BAND_RING_ENABLED = 1;
// external actions
public static final int AT_OK = 0;
public static final int CONNECT = 1;
public static final int DISCONNECT = 2;
public static final int CONNECT_AUDIO = 3;
public static final int DISCONNECT_AUDIO = 4;
public static final int VOICE_RECOGNITION_START = 5;
public static final int VOICE_RECOGNITION_STOP = 6;
public static final int SET_MIC_VOLUME = 7;
public static final int SET_SPEAKER_VOLUME = 8;
public static final int DIAL_NUMBER = 10;
public static final int ACCEPT_CALL = 12;
public static final int REJECT_CALL = 13;
public static final int HOLD_CALL = 14;
public static final int TERMINATE_CALL = 15;
public static final int ENTER_PRIVATE_MODE = 16;
public static final int SEND_DTMF = 17;
public static final int EXPLICIT_CALL_TRANSFER = 18;
public static final int DISABLE_NREC = 20;
public static final int SEND_VENDOR_AT_COMMAND = 21;
// internal actions
private static final int QUERY_CURRENT_CALLS = 50;
private static final int QUERY_OPERATOR_NAME = 51;
private static final int SUBSCRIBER_INFO = 52;
private static final int CONNECTING_TIMEOUT = 53;
// special action to handle terminating specific call from multiparty call
static final int TERMINATE_SPECIFIC_CALL = 53;
// Timeouts.
@VisibleForTesting
static final int CONNECTING_TIMEOUT_MS = 10000; // 10s
private static final int ROUTING_DELAY_MS = 250;
private static final int MAX_HFP_SCO_VOICE_CALL_VOLUME = 15; // HFP 1.5 spec.
private static final int MIN_HFP_SCO_VOICE_CALL_VOLUME = 1; // HFP 1.5 spec.
static final int HF_ORIGINATED_CALL_ID = -1;
private static final long OUTGOING_TIMEOUT_MILLI = 10 * 1000; // 10 seconds
private static final long QUERY_CURRENT_CALLS_WAIT_MILLIS = 2 * 1000; // 2 seconds
// Keep track of audio routing across all devices.
private static boolean sAudioIsRouted = false;
private final Disconnected mDisconnected;
private final Connecting mConnecting;
private final Connected mConnected;
private final AudioOn mAudioOn;
private State mPrevState;
private long mClccTimer = 0;
private final HeadsetClientService mService;
// Set of calls that represent the accurate state of calls that exists on AG and the calls that
// are currently in process of being notified to the AG from HF.
private final Hashtable<Integer, BluetoothHeadsetClientCall> mCalls = new Hashtable<>();
// Set of calls received from AG via the AT+CLCC command. We use this map to update the mCalls
// which is eventually used to inform the telephony stack of any changes to call on HF.
private final Hashtable<Integer, BluetoothHeadsetClientCall> mCallsUpdate = new Hashtable<>();
private int mIndicatorNetworkState;
private int mIndicatorNetworkType;
private int mIndicatorNetworkSignal;
private int mIndicatorBatteryLevel;
private boolean mInBandRing;
private String mOperatorName;
private String mSubscriberInfo;
private static int sMaxAmVcVol;
private static int sMinAmVcVol;
// queue of send actions (pair action, action_data)
private Queue<Pair<Integer, Object>> mQueuedActions;
// last executed command, before action is complete e.g. waiting for some
// indicator
private Pair<Integer, Object> mPendingAction;
private int mAudioState;
private boolean mAudioWbs;
private int mVoiceRecognitionActive;
private final BluetoothAdapter mAdapter;
// currently connected device
private BluetoothDevice mCurrentDevice = null;
// general peer features and call handling features
private int mPeerFeatures;
private int mChldFeatures;
// This is returned when requesting focus from AudioManager
private AudioFocusRequest mAudioFocusRequest;
private final AudioManager mAudioManager;
private final NativeInterface mNativeInterface;
private final VendorCommandResponseProcessor mVendorProcessor;
// Accessor for the states, useful for reusing the state machines
public IState getDisconnectedState() {
return mDisconnected;
}
// Get if in band ring is currently enabled on device.
public boolean getInBandRing() {
return mInBandRing;
}
public void dump(StringBuilder sb) {
if (mCurrentDevice == null) return;
ProfileService.println(sb,
"==== StateMachine for " + mCurrentDevice + " ====");
ProfileService.println(sb, " mCurrentDevice: " + mCurrentDevice.getAddress() + "("
+ mCurrentDevice.getName() + ") " + this.toString());
ProfileService.println(sb, " mAudioState: " + mAudioState);
ProfileService.println(sb, " mAudioWbs: " + mAudioWbs);
ProfileService.println(sb, " mIndicatorNetworkState: " + mIndicatorNetworkState);
ProfileService.println(sb, " mIndicatorNetworkType: " + mIndicatorNetworkType);
ProfileService.println(sb, " mIndicatorNetworkSignal: " + mIndicatorNetworkSignal);
ProfileService.println(sb, " mIndicatorBatteryLevel: " + mIndicatorBatteryLevel);
ProfileService.println(sb, " mOperatorName: " + mOperatorName);
ProfileService.println(sb, " mSubscriberInfo: " + mSubscriberInfo);
ProfileService.println(sb, " mCalls:");
if (mCalls != null) {
for (BluetoothHeadsetClientCall call : mCalls.values()) {
ProfileService.println(sb, " " + call);
}
}
ProfileService.println(sb, " mCallsUpdate:");
if (mCallsUpdate != null) {
for (BluetoothHeadsetClientCall call : mCallsUpdate.values()) {
ProfileService.println(sb, " " + call);
}
}
// Dump the state machine logs
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
super.dump(new FileDescriptor(), printWriter, new String[]{});
printWriter.flush();
stringWriter.flush();
ProfileService.println(sb, " StateMachineLog:");
Scanner scanner = new Scanner(stringWriter.toString());
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
ProfileService.println(sb, " " + line);
}
}
@Override
protected String getLogRecString(Message msg) {
StringBuilder builder = new StringBuilder();
builder.append(getMessageName(msg.what));
builder.append(": ");
builder.append("arg1=")
.append(msg.arg1)
.append(", arg2=")
.append(msg.arg2)
.append(", obj=")
.append(msg.obj);
return builder.toString();
}
private static String getMessageName(int what) {
switch (what) {
case StackEvent.STACK_EVENT:
return "STACK_EVENT";
case CONNECT:
return "CONNECT";
case DISCONNECT:
return "DISCONNECT";
case CONNECT_AUDIO:
return "CONNECT_AUDIO";
case DISCONNECT_AUDIO:
return "DISCONNECT_AUDIO";
case VOICE_RECOGNITION_START:
return "VOICE_RECOGNITION_START";
case VOICE_RECOGNITION_STOP:
return "VOICE_RECOGNITION_STOP";
case SET_MIC_VOLUME:
return "SET_MIC_VOLUME";
case SET_SPEAKER_VOLUME:
return "SET_SPEAKER_VOLUME";
case DIAL_NUMBER:
return "DIAL_NUMBER";
case ACCEPT_CALL:
return "ACCEPT_CALL";
case REJECT_CALL:
return "REJECT_CALL";
case HOLD_CALL:
return "HOLD_CALL";
case TERMINATE_CALL:
return "TERMINATE_CALL";
case ENTER_PRIVATE_MODE:
return "ENTER_PRIVATE_MODE";
case SEND_DTMF:
return "SEND_DTMF";
case EXPLICIT_CALL_TRANSFER:
return "EXPLICIT_CALL_TRANSFER";
case DISABLE_NREC:
return "DISABLE_NREC";
case SEND_VENDOR_AT_COMMAND:
return "SEND_VENDOR_AT_COMMAND";
case QUERY_CURRENT_CALLS:
return "QUERY_CURRENT_CALLS";
case QUERY_OPERATOR_NAME:
return "QUERY_OPERATOR_NAME";
case SUBSCRIBER_INFO:
return "SUBSCRIBER_INFO";
case CONNECTING_TIMEOUT:
return "CONNECTING_TIMEOUT";
default:
return "UNKNOWN(" + what + ")";
}
}
private void clearPendingAction() {
mPendingAction = new Pair<Integer, Object>(NO_ACTION, 0);
}
private void addQueuedAction(int action) {
addQueuedAction(action, 0);
}
private void addQueuedAction(int action, Object data) {
mQueuedActions.add(new Pair<Integer, Object>(action, data));
}
private void addQueuedAction(int action, int data) {
mQueuedActions.add(new Pair<Integer, Object>(action, data));
}
private BluetoothHeadsetClientCall getCall(int... states) {
logD("getFromCallsWithStates states:" + Arrays.toString(states));
for (BluetoothHeadsetClientCall c : mCalls.values()) {
for (int s : states) {
if (c.getState() == s) {
return c;
}
}
}
return null;
}
private int callsInState(int state) {
int i = 0;
for (BluetoothHeadsetClientCall c : mCalls.values()) {
if (c.getState() == state) {
i++;
}
}
return i;
}
private void sendCallChangedIntent(BluetoothHeadsetClientCall c) {
logD("sendCallChangedIntent " + c);
Intent intent = new Intent(BluetoothHeadsetClient.ACTION_CALL_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.putExtra(BluetoothHeadsetClient.EXTRA_CALL, c);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
}
private boolean queryCallsStart() {
logD("queryCallsStart");
clearPendingAction();
mNativeInterface.queryCurrentCalls(getByteAddress(mCurrentDevice));
addQueuedAction(QUERY_CURRENT_CALLS, 0);
return true;
}
private void queryCallsDone() {
logD("queryCallsDone");
// mCalls has two types of calls:
// (a) Calls that are received from AG of a previous iteration of queryCallsStart()
// (b) Calls that are outgoing initiated from HF
// mCallsUpdate has all calls received from queryCallsUpdate() in current iteration of
// queryCallsStart().
//
// We use the following steps to make sure that calls are update correctly.
//
// If there are no calls initiated from HF (i.e. ID = -1) then:
// 1. All IDs which are common in mCalls & mCallsUpdate are updated and the upper layers are
// informed of the change calls (if any changes).
// 2. All IDs that are in mCalls but *not* in mCallsUpdate will be removed from mCalls and
// the calls should be terminated
// 3. All IDs that are new in mCallsUpdated should be added as new calls to mCalls.
//
// If there is an outgoing HF call, it is important to associate that call with one of the
// mCallsUpdated calls hence,
// 1. If from the above procedure we get N extra calls (i.e. {3}):
// choose the first call as the one to associate with the HF call.
// Create set of IDs for added calls, removed calls and consitent calls.
// WARN!!! Java Map -> Set has association hence changes to Set are reflected in the Map
// itself (i.e. removing an element from Set removes it from the Map hence use copy).
Set<Integer> currCallIdSet = new HashSet<Integer>();
currCallIdSet.addAll(mCalls.keySet());
// Remove the entry for unassigned call.
currCallIdSet.remove(HF_ORIGINATED_CALL_ID);
Set<Integer> newCallIdSet = new HashSet<Integer>();
newCallIdSet.addAll(mCallsUpdate.keySet());
// Added.
Set<Integer> callAddedIds = new HashSet<Integer>();
callAddedIds.addAll(newCallIdSet);
callAddedIds.removeAll(currCallIdSet);
// Removed.
Set<Integer> callRemovedIds = new HashSet<Integer>();
callRemovedIds.addAll(currCallIdSet);
callRemovedIds.removeAll(newCallIdSet);
// Retained.
Set<Integer> callRetainedIds = new HashSet<Integer>();
callRetainedIds.addAll(currCallIdSet);
callRetainedIds.retainAll(newCallIdSet);
logD("currCallIdSet " + mCalls.keySet() + " newCallIdSet " + newCallIdSet
+ " callAddedIds " + callAddedIds + " callRemovedIds " + callRemovedIds
+ " callRetainedIds " + callRetainedIds);
// First thing is to try to associate the outgoing HF with a valid call.
Integer hfOriginatedAssoc = -1;
if (mCalls.containsKey(HF_ORIGINATED_CALL_ID)) {
BluetoothHeadsetClientCall c = mCalls.get(HF_ORIGINATED_CALL_ID);
long cCreationElapsed = c.getCreationElapsedMilli();
if (callAddedIds.size() > 0) {
logD("Associating the first call with HF originated call");
hfOriginatedAssoc = (Integer) callAddedIds.toArray()[0];
mCalls.put(hfOriginatedAssoc, mCalls.get(HF_ORIGINATED_CALL_ID));
mCalls.remove(HF_ORIGINATED_CALL_ID);
// Adjust this call in above sets.
callAddedIds.remove(hfOriginatedAssoc);
callRetainedIds.add(hfOriginatedAssoc);
} else if (SystemClock.elapsedRealtime() - cCreationElapsed > OUTGOING_TIMEOUT_MILLI) {
Log.w(TAG, "Outgoing call did not see a response, clear the calls and send CHUP");
// We send a terminate because we are in a bad state and trying to
// recover.
terminateCall();
// Clean out the state for outgoing call.
for (Integer idx : mCalls.keySet()) {
BluetoothHeadsetClientCall c1 = mCalls.get(idx);
c1.setState(BluetoothHeadsetClientCall.CALL_STATE_TERMINATED);
sendCallChangedIntent(c1);
}
mCalls.clear();
// We return here, if there's any update to the phone we should get a
// follow up by getting some call indicators and hence update the calls.
return;
}
}
logD("ADJUST: currCallIdSet " + mCalls.keySet() + " newCallIdSet " + newCallIdSet
+ " callAddedIds " + callAddedIds + " callRemovedIds " + callRemovedIds
+ " callRetainedIds " + callRetainedIds);
// Terminate & remove the calls that are done.
for (Integer idx : callRemovedIds) {
BluetoothHeadsetClientCall c = mCalls.remove(idx);
c.setState(BluetoothHeadsetClientCall.CALL_STATE_TERMINATED);
sendCallChangedIntent(c);
}
// Add the new calls.
for (Integer idx : callAddedIds) {
BluetoothHeadsetClientCall c = mCallsUpdate.get(idx);
mCalls.put(idx, c);
sendCallChangedIntent(c);
}
// Update the existing calls.
for (Integer idx : callRetainedIds) {
BluetoothHeadsetClientCall cOrig = mCalls.get(idx);
BluetoothHeadsetClientCall cUpdate = mCallsUpdate.get(idx);
// Update the necessary fields.
cOrig.setNumber(cUpdate.getNumber());
cOrig.setState(cUpdate.getState());
cOrig.setMultiParty(cUpdate.isMultiParty());
// Send update with original object (UUID, idx).
sendCallChangedIntent(cOrig);
}
if (mCalls.size() > 0) {
if (mService.getResources().getBoolean(R.bool.hfp_clcc_poll_during_call)) {
sendMessageDelayed(QUERY_CURRENT_CALLS, QUERY_CURRENT_CALLS_WAIT_MILLIS);
} else {
if (getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING) != null) {
logD("Still have incoming call; polling");
sendMessageDelayed(QUERY_CURRENT_CALLS, QUERY_CURRENT_CALLS_WAIT_MILLIS);
} else {
removeMessages(QUERY_CURRENT_CALLS);
}
}
}
mCallsUpdate.clear();
}
private void queryCallsUpdate(int id, int state, String number, boolean multiParty,
boolean outgoing) {
logD("queryCallsUpdate: " + id);
mCallsUpdate.put(id,
new BluetoothHeadsetClientCall(mCurrentDevice, id, state, number, multiParty,
outgoing, mInBandRing));
}
private void acceptCall(int flag) {
int action = -1;
logD("acceptCall: (" + flag + ")");
BluetoothHeadsetClientCall c = getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING,
BluetoothHeadsetClientCall.CALL_STATE_WAITING);
if (c == null) {
c = getCall(BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD,
BluetoothHeadsetClientCall.CALL_STATE_HELD);
if (c == null) {
return;
}
}
logD("Call to accept: " + c);
switch (c.getState()) {
case BluetoothHeadsetClientCall.CALL_STATE_INCOMING:
if (flag != BluetoothHeadsetClient.CALL_ACCEPT_NONE) {
return;
}
action = HeadsetClientHalConstants.CALL_ACTION_ATA;
break;
case BluetoothHeadsetClientCall.CALL_STATE_WAITING:
if (callsInState(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) == 0) {
// if no active calls present only plain accept is allowed
if (flag != BluetoothHeadsetClient.CALL_ACCEPT_NONE) {
return;
}
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_2;
break;
}
// if active calls are present then we have the option to either terminate the
// existing call or hold the existing call. We hold the other call by default.
if (flag == BluetoothHeadsetClient.CALL_ACCEPT_HOLD
|| flag == BluetoothHeadsetClient.CALL_ACCEPT_NONE) {
logD("Accepting call with accept and hold");
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_2;
} else if (flag == BluetoothHeadsetClient.CALL_ACCEPT_TERMINATE) {
logD("Accepting call with accept and reject");
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_1;
} else {
Log.e(TAG, "Aceept call with invalid flag: " + flag);
return;
}
break;
case BluetoothHeadsetClientCall.CALL_STATE_HELD:
if (flag == BluetoothHeadsetClient.CALL_ACCEPT_HOLD) {
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_2;
} else if (flag == BluetoothHeadsetClient.CALL_ACCEPT_TERMINATE) {
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_1;
} else if (getCall(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) != null) {
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_3;
} else if (flag == BluetoothHeadsetClient.CALL_ACCEPT_NONE) {
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_2;
} else {
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_2;
}
break;
case BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD:
action = HeadsetClientHalConstants.CALL_ACTION_BTRH_1;
break;
case BluetoothHeadsetClientCall.CALL_STATE_ALERTING:
case BluetoothHeadsetClientCall.CALL_STATE_ACTIVE:
case BluetoothHeadsetClientCall.CALL_STATE_DIALING:
default:
return;
}
if (flag == BluetoothHeadsetClient.CALL_ACCEPT_HOLD) {
// When unholding a call over Bluetooth make sure to route audio.
routeHfpAudio(true);
}
if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) {
addQueuedAction(ACCEPT_CALL, action);
} else {
Log.e(TAG, "ERROR: Couldn't accept a call, action:" + action);
}
}
private void rejectCall() {
int action;
logD("rejectCall");
BluetoothHeadsetClientCall c = getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING,
BluetoothHeadsetClientCall.CALL_STATE_WAITING,
BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD,
BluetoothHeadsetClientCall.CALL_STATE_HELD);
if (c == null) {
logD("No call to reject, returning.");
return;
}
switch (c.getState()) {
case BluetoothHeadsetClientCall.CALL_STATE_INCOMING:
action = HeadsetClientHalConstants.CALL_ACTION_CHUP;
break;
case BluetoothHeadsetClientCall.CALL_STATE_WAITING:
case BluetoothHeadsetClientCall.CALL_STATE_HELD:
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_0;
break;
case BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD:
action = HeadsetClientHalConstants.CALL_ACTION_BTRH_2;
break;
case BluetoothHeadsetClientCall.CALL_STATE_ACTIVE:
case BluetoothHeadsetClientCall.CALL_STATE_DIALING:
case BluetoothHeadsetClientCall.CALL_STATE_ALERTING:
default:
return;
}
if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) {
logD("Reject call action " + action);
addQueuedAction(REJECT_CALL, action);
} else {
Log.e(TAG, "ERROR: Couldn't reject a call, action:" + action);
}
}
private void holdCall() {
int action;
logD("holdCall");
BluetoothHeadsetClientCall c = getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING);
if (c != null) {
action = HeadsetClientHalConstants.CALL_ACTION_BTRH_0;
} else {
c = getCall(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE);
if (c == null) {
return;
}
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_2;
}
if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) {
addQueuedAction(HOLD_CALL, action);
} else {
Log.e(TAG, "ERROR: Couldn't hold a call, action:" + action);
}
}
private void terminateCall() {
logD("terminateCall");
int action = HeadsetClientHalConstants.CALL_ACTION_CHUP;
BluetoothHeadsetClientCall c = getCall(BluetoothHeadsetClientCall.CALL_STATE_DIALING,
BluetoothHeadsetClientCall.CALL_STATE_ALERTING,
BluetoothHeadsetClientCall.CALL_STATE_ACTIVE);
if (c == null) {
// If the call being terminated is currently held, switch the action to CHLD_0
c = getCall(BluetoothHeadsetClientCall.CALL_STATE_HELD);
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_0;
}
if (c != null) {
if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) {
addQueuedAction(TERMINATE_CALL, action);
} else {
Log.e(TAG, "ERROR: Couldn't terminate outgoing call");
}
}
}
private void enterPrivateMode(int idx) {
logD("enterPrivateMode: " + idx);
BluetoothHeadsetClientCall c = mCalls.get(idx);
if (c == null || c.getState() != BluetoothHeadsetClientCall.CALL_STATE_ACTIVE
|| !c.isMultiParty()) {
return;
}
if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice),
HeadsetClientHalConstants.CALL_ACTION_CHLD_2X, idx)) {
addQueuedAction(ENTER_PRIVATE_MODE, c);
} else {
Log.e(TAG, "ERROR: Couldn't enter private " + " id:" + idx);
}
}
private void explicitCallTransfer() {
logD("explicitCallTransfer");
// can't transfer call if there is not enough call parties
if (mCalls.size() < 2) {
return;
}
if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice),
HeadsetClientHalConstants.CALL_ACTION_CHLD_4, -1)) {
addQueuedAction(EXPLICIT_CALL_TRANSFER);
} else {
Log.e(TAG, "ERROR: Couldn't transfer call");
}
}
public Bundle getCurrentAgFeatures() {
Bundle b = new Bundle();
if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_3WAY)
== HeadsetClientHalConstants.PEER_FEAT_3WAY) {
b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_3WAY_CALLING, true);
}
if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_VREC)
== HeadsetClientHalConstants.PEER_FEAT_VREC) {
b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_VOICE_RECOGNITION, true);
}
if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_REJECT)
== HeadsetClientHalConstants.PEER_FEAT_REJECT) {
b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_REJECT_CALL, true);
}
if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_ECC)
== HeadsetClientHalConstants.PEER_FEAT_ECC) {
b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_ECC, true);
}
// add individual CHLD support extras
if ((mChldFeatures & HeadsetClientHalConstants.CHLD_FEAT_HOLD_ACC)
== HeadsetClientHalConstants.CHLD_FEAT_HOLD_ACC) {
b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_ACCEPT_HELD_OR_WAITING_CALL, true);
}
if ((mChldFeatures & HeadsetClientHalConstants.CHLD_FEAT_REL)
== HeadsetClientHalConstants.CHLD_FEAT_REL) {
b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_RELEASE_HELD_OR_WAITING_CALL,
true);
}
if ((mChldFeatures & HeadsetClientHalConstants.CHLD_FEAT_REL_ACC)
== HeadsetClientHalConstants.CHLD_FEAT_REL_ACC) {
b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_RELEASE_AND_ACCEPT, true);
}
if ((mChldFeatures & HeadsetClientHalConstants.CHLD_FEAT_MERGE)
== HeadsetClientHalConstants.CHLD_FEAT_MERGE) {
b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_MERGE, true);
}
if ((mChldFeatures & HeadsetClientHalConstants.CHLD_FEAT_MERGE_DETACH)
== HeadsetClientHalConstants.CHLD_FEAT_MERGE_DETACH) {
b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_MERGE_AND_DETACH, true);
}
return b;
}
HeadsetClientStateMachine(HeadsetClientService context, Looper looper,
NativeInterface nativeInterface) {
super(TAG, looper);
mService = context;
mNativeInterface = nativeInterface;
mAudioManager = mService.getAudioManager();
mVendorProcessor = new VendorCommandResponseProcessor(mService, mNativeInterface);
mAdapter = BluetoothAdapter.getDefaultAdapter();
mAudioState = BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED;
mAudioWbs = false;
mVoiceRecognitionActive = HeadsetClientHalConstants.VR_STATE_STOPPED;
mIndicatorNetworkState = HeadsetClientHalConstants.NETWORK_STATE_NOT_AVAILABLE;
mIndicatorNetworkType = HeadsetClientHalConstants.SERVICE_TYPE_HOME;
mIndicatorNetworkSignal = 0;
mIndicatorBatteryLevel = 0;
sMaxAmVcVol = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL);
sMinAmVcVol = mAudioManager.getStreamMinVolume(AudioManager.STREAM_VOICE_CALL);
mOperatorName = null;
mSubscriberInfo = null;
mQueuedActions = new LinkedList<Pair<Integer, Object>>();
clearPendingAction();
mCalls.clear();
mCallsUpdate.clear();
mDisconnected = new Disconnected();
mConnecting = new Connecting();
mConnected = new Connected();
mAudioOn = new AudioOn();
addState(mDisconnected);
addState(mConnecting);
addState(mConnected);
addState(mAudioOn, mConnected);
setInitialState(mDisconnected);
}
static HeadsetClientStateMachine make(HeadsetClientService context, Looper looper,
NativeInterface nativeInterface) {
logD("make");
HeadsetClientStateMachine hfcsm = new HeadsetClientStateMachine(context, looper,
nativeInterface);
hfcsm.start();
return hfcsm;
}
synchronized void routeHfpAudio(boolean enable) {
if (mAudioManager == null) {
Log.e(TAG, "AudioManager is null!");
return;
}
logD("hfp_enable=" + enable);
if (enable && !sAudioIsRouted) {
mAudioManager.setParameters("hfp_enable=true");
} else if (!enable) {
mAudioManager.setParameters("hfp_enable=false");
}
sAudioIsRouted = enable;
}
private AudioFocusRequest requestAudioFocus() {
AudioAttributes streamAttributes =
new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build();
AudioFocusRequest focusRequest =
new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
.setAudioAttributes(streamAttributes)
.build();
int focusRequestStatus = mAudioManager.requestAudioFocus(focusRequest);
String s = (focusRequestStatus == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
? "AudioFocus granted" : "AudioFocus NOT granted";
logD("AudioManager requestAudioFocus returned: " + s);
return focusRequest;
}
public void doQuit() {
logD("doQuit");
if (mCurrentDevice != null) {
mNativeInterface.disconnect(getByteAddress(mCurrentDevice));
}
routeHfpAudio(false);
returnAudioFocusIfNecessary();
quitNow();
}
private void returnAudioFocusIfNecessary() {
if (mAudioFocusRequest == null) return;
mAudioManager.abandonAudioFocusRequest(mAudioFocusRequest);
mAudioFocusRequest = null;
}
static int hfToAmVol(int hfVol) {
int amRange = sMaxAmVcVol - sMinAmVcVol;
int hfRange = MAX_HFP_SCO_VOICE_CALL_VOLUME - MIN_HFP_SCO_VOICE_CALL_VOLUME;
int amOffset = (amRange * (hfVol - MIN_HFP_SCO_VOICE_CALL_VOLUME)) / hfRange;
int amVol = sMinAmVcVol + amOffset;
logD("HF -> AM " + hfVol + " " + amVol);
return amVol;
}
static int amToHfVol(int amVol) {
int amRange = (sMaxAmVcVol > sMinAmVcVol) ? (sMaxAmVcVol - sMinAmVcVol) : 1;
int hfRange = MAX_HFP_SCO_VOICE_CALL_VOLUME - MIN_HFP_SCO_VOICE_CALL_VOLUME;
int hfOffset = (hfRange * (amVol - sMinAmVcVol)) / amRange;
int hfVol = MIN_HFP_SCO_VOICE_CALL_VOLUME + hfOffset;
logD("AM -> HF " + amVol + " " + hfVol);
return hfVol;
}
class Disconnected extends State {
@Override
public void enter() {
logD("Enter Disconnected: " + getCurrentMessage().what);
// cleanup
mIndicatorNetworkState = HeadsetClientHalConstants.NETWORK_STATE_NOT_AVAILABLE;
mIndicatorNetworkType = HeadsetClientHalConstants.SERVICE_TYPE_HOME;
mIndicatorNetworkSignal = 0;
mIndicatorBatteryLevel = 0;
mInBandRing = false;
mAudioWbs = false;
// will be set on connect
mOperatorName = null;
mSubscriberInfo = null;
mQueuedActions = new LinkedList<Pair<Integer, Object>>();
clearPendingAction();
mCalls.clear();
mCallsUpdate.clear();
mPeerFeatures = 0;
mChldFeatures = 0;
removeMessages(QUERY_CURRENT_CALLS);
if (mPrevState == mConnecting) {
broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
} else if (mPrevState == mConnected || mPrevState == mAudioOn) {
broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED);
} else if (mPrevState != null) { // null is the default state before Disconnected
Log.e(TAG, "Connected: Illegal state transition from " + mPrevState.getName()
+ " to Connecting, mCurrentDevice=" + mCurrentDevice);
}
mCurrentDevice = null;
}
@Override
public synchronized boolean processMessage(Message message) {
logD("Disconnected process message: " + message.what);
if (mCurrentDevice != null) {
Log.e(TAG, "ERROR: current device not null in Disconnected");
return NOT_HANDLED;
}
switch (message.what) {
case CONNECT:
BluetoothDevice device = (BluetoothDevice) message.obj;
if (!mNativeInterface.connect(getByteAddress(device))) {
// No state transition is involved, fire broadcast immediately
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
break;
}
mCurrentDevice = device;
transitionTo(mConnecting);
break;
case DISCONNECT:
// ignore
break;
case StackEvent.STACK_EVENT:
StackEvent event = (StackEvent) message.obj;
logD("Stack event type: " + event.type);
switch (event.type) {
case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
logD("Disconnected: Connection " + event.device
+ " state changed:" + event.valueInt);
processConnectionEvent(event.valueInt, event.device);
break;
default:
Log.e(TAG, "Disconnected: Unexpected stack event: " + event.type);
break;
}
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
// in Disconnected state
private void processConnectionEvent(int state, BluetoothDevice device) {
switch (state) {
case HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED:
Log.w(TAG, "HFPClient Connecting from Disconnected state");
if (okToConnect(device)) {
Log.i(TAG, "Incoming AG accepted");
mCurrentDevice = device;
transitionTo(mConnecting);
} else {
Log.i(TAG, "Incoming AG rejected. connectionPolicy="
+ mService.getConnectionPolicy(device) + " bondState="
+ device.getBondState());
// reject the connection and stay in Disconnected state
// itself
mNativeInterface.disconnect(getByteAddress(device));
// the other profile connection should be initiated
AdapterService adapterService = AdapterService.getAdapterService();
// No state transition is involved, fire broadcast immediately
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
}
break;
case HeadsetClientHalConstants.CONNECTION_STATE_CONNECTING:
case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED:
case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTING:
default:
Log.i(TAG, "ignoring state: " + state);
break;
}
}
@Override
public void exit() {
logD("Exit Disconnected: " + getCurrentMessage().what);
mPrevState = this;
}
}
class Connecting extends State {
@Override
public void enter() {
logD("Enter Connecting: " + getCurrentMessage().what);
// This message is either consumed in processMessage or
// removed in exit. It is safe to send a CONNECTING_TIMEOUT here since
// the only transition is when connection attempt is initiated.
sendMessageDelayed(CONNECTING_TIMEOUT, CONNECTING_TIMEOUT_MS);
if (mPrevState == mDisconnected) {
broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTED);
} else {
String prevStateName = mPrevState == null ? "null" : mPrevState.getName();
Log.e(TAG, "Connected: Illegal state transition from " + prevStateName
+ " to Connecting, mCurrentDevice=" + mCurrentDevice);
}
}
@Override
public synchronized boolean processMessage(Message message) {
logD("Connecting process message: " + message.what);
switch (message.what) {
case CONNECT:
case CONNECT_AUDIO:
case DISCONNECT:
deferMessage(message);
break;
case StackEvent.STACK_EVENT:
StackEvent event = (StackEvent) message.obj;
logD("Connecting: event type: " + event.type);
switch (event.type) {
case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
logD("Connecting: Connection " + event.device + " state changed:"
+ event.valueInt);
processConnectionEvent(event.valueInt, event.valueInt2, event.valueInt3,
event.device);
break;
case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
case StackEvent.EVENT_TYPE_NETWORK_STATE:
case StackEvent.EVENT_TYPE_ROAMING_STATE:
case StackEvent.EVENT_TYPE_NETWORK_SIGNAL:
case StackEvent.EVENT_TYPE_BATTERY_LEVEL:
case StackEvent.EVENT_TYPE_CALL:
case StackEvent.EVENT_TYPE_CALLSETUP:
case StackEvent.EVENT_TYPE_CALLHELD:
case StackEvent.EVENT_TYPE_RESP_AND_HOLD:
case StackEvent.EVENT_TYPE_CLIP:
case StackEvent.EVENT_TYPE_CALL_WAITING:
case StackEvent.EVENT_TYPE_VOLUME_CHANGED:
deferMessage(message);
break;
case StackEvent.EVENT_TYPE_CMD_RESULT:
case StackEvent.EVENT_TYPE_SUBSCRIBER_INFO:
case StackEvent.EVENT_TYPE_CURRENT_CALLS:
case StackEvent.EVENT_TYPE_OPERATOR_NAME:
default:
Log.e(TAG, "Connecting: ignoring stack event: " + event.type);
break;
}
break;
case CONNECTING_TIMEOUT:
// We timed out trying to connect, transition to disconnected.
Log.w(TAG, "Connection timeout for " + mCurrentDevice);
transitionTo(mDisconnected);
break;
default:
Log.w(TAG, "Message not handled " + message);
return NOT_HANDLED;
}
return HANDLED;
}
// in Connecting state
private void processConnectionEvent(int state, int peerFeat, int chldFeat,
BluetoothDevice device) {
switch (state) {
case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED:
transitionTo(mDisconnected);
break;
case HeadsetClientHalConstants.CONNECTION_STATE_SLC_CONNECTED:
logD("HFPClient Connected from Connecting state");
mPeerFeatures = peerFeat;
mChldFeatures = chldFeat;
// We do not support devices which do not support enhanced call status (ECS).
if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_ECS) == 0) {
mNativeInterface.disconnect(getByteAddress(device));
return;
}
// Send AT+NREC to remote if supported by audio
if (HeadsetClientHalConstants.HANDSFREECLIENT_NREC_SUPPORTED && (
(mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_ECNR)
== HeadsetClientHalConstants.PEER_FEAT_ECNR)) {
if (mNativeInterface.sendATCmd(getByteAddress(mCurrentDevice),
HeadsetClientHalConstants.HANDSFREECLIENT_AT_CMD_NREC, 1, 0,
null)) {
addQueuedAction(DISABLE_NREC);
} else {
Log.e(TAG, "Failed to send NREC");
}
}
int amVol = mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL);
deferMessage(
obtainMessage(HeadsetClientStateMachine.SET_SPEAKER_VOLUME, amVol, 0));
// Mic is either in ON state (full volume) or OFF state. There is no way in
// Android to change the MIC volume.
deferMessage(obtainMessage(HeadsetClientStateMachine.SET_MIC_VOLUME,
mAudioManager.isMicrophoneMute() ? 0 : 15, 0));
// query subscriber info
deferMessage(obtainMessage(HeadsetClientStateMachine.SUBSCRIBER_INFO));
transitionTo(mConnected);
break;
case HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED:
if (!mCurrentDevice.equals(device)) {
Log.w(TAG, "incoming connection event, device: " + device);
// No state transition is involved, fire broadcast immediately
broadcastConnectionState(mCurrentDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTED);
mCurrentDevice = device;
}
break;
case HeadsetClientHalConstants.CONNECTION_STATE_CONNECTING:
/* outgoing connecting started */
logD("outgoing connection started, ignore");
break;
case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTING:
default:
Log.e(TAG, "Incorrect state: " + state);
break;
}
}
@Override
public void exit() {
logD("Exit Connecting: " + getCurrentMessage().what);
removeMessages(CONNECTING_TIMEOUT);
mPrevState = this;
}
}
class Connected extends State {
int mCommandedSpeakerVolume = -1;
@Override
public void enter() {
logD("Enter Connected: " + getCurrentMessage().what);
mAudioWbs = false;
mCommandedSpeakerVolume = -1;
if (mPrevState == mConnecting) {
broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING);
MetricsLogger.logProfileConnectionEvent(
BluetoothMetricsProto.ProfileId.HEADSET_CLIENT);
} else if (mPrevState != mAudioOn) {
String prevStateName = mPrevState == null ? "null" : mPrevState.getName();
Log.e(TAG, "Connected: Illegal state transition from " + prevStateName
+ " to Connecting, mCurrentDevice=" + mCurrentDevice);
}
}
@Override
public synchronized boolean processMessage(Message message) {
logD("Connected process message: " + message.what);
if (DBG) {
if (mCurrentDevice == null) {
Log.e(TAG, "ERROR: mCurrentDevice is null in Connected");
return NOT_HANDLED;
}
}
switch (message.what) {
case CONNECT:
BluetoothDevice device = (BluetoothDevice) message.obj;
if (mCurrentDevice.equals(device)) {
// already connected to this device, do nothing
break;
}
mNativeInterface.connect(getByteAddress(device));
break;
case DISCONNECT:
BluetoothDevice dev = (BluetoothDevice) message.obj;
if (!mCurrentDevice.equals(dev)) {
break;
}
if (!mNativeInterface.disconnect(getByteAddress(dev))) {
Log.e(TAG, "disconnectNative failed for " + dev);
}
break;
case CONNECT_AUDIO:
if (!mNativeInterface.connectAudio(getByteAddress(mCurrentDevice))) {
Log.e(TAG, "ERROR: Couldn't connect Audio for device " + mCurrentDevice);
// No state transition is involved, fire broadcast immediately
broadcastAudioState(mCurrentDevice,
BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED,
BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED);
} else { // We have successfully sent a connect request!
mAudioState = BluetoothHeadsetClient.STATE_AUDIO_CONNECTING;
}
break;
case DISCONNECT_AUDIO:
if (!mNativeInterface.disconnectAudio(getByteAddress(mCurrentDevice))) {
Log.e(TAG, "ERROR: Couldn't disconnect Audio for device " + mCurrentDevice);
}
break;
case VOICE_RECOGNITION_START:
if (mVoiceRecognitionActive == HeadsetClientHalConstants.VR_STATE_STOPPED) {
if (mNativeInterface.startVoiceRecognition(
getByteAddress(mCurrentDevice))) {
addQueuedAction(VOICE_RECOGNITION_START);
} else {
Log.e(TAG, "ERROR: Couldn't start voice recognition");
}
}
break;
case VOICE_RECOGNITION_STOP:
if (mVoiceRecognitionActive == HeadsetClientHalConstants.VR_STATE_STARTED) {
if (mNativeInterface.stopVoiceRecognition(
getByteAddress(mCurrentDevice))) {
addQueuedAction(VOICE_RECOGNITION_STOP);
} else {
Log.e(TAG, "ERROR: Couldn't stop voice recognition");
}
}
break;
case SEND_VENDOR_AT_COMMAND: {
int vendorId = message.arg1;
String atCommand = (String) (message.obj);
mVendorProcessor.sendCommand(vendorId, atCommand, mCurrentDevice);
break;
}
// Called only for Mute/Un-mute - Mic volume change is not allowed.
case SET_MIC_VOLUME:
break;
case SET_SPEAKER_VOLUME:
// This message should always contain the volume in AudioManager max normalized.
int amVol = message.arg1;
int hfVol = amToHfVol(amVol);
if (amVol != mCommandedSpeakerVolume) {
logD("Volume" + amVol + ":" + mCommandedSpeakerVolume);
// Volume was changed by a 3rd party
mCommandedSpeakerVolume = -1;
if (mNativeInterface.setVolume(getByteAddress(mCurrentDevice),
HeadsetClientHalConstants.VOLUME_TYPE_SPK, hfVol)) {
addQueuedAction(SET_SPEAKER_VOLUME);
}
}
break;
case DIAL_NUMBER:
// Add the call as an outgoing call.
BluetoothHeadsetClientCall c = (BluetoothHeadsetClientCall) message.obj;
mCalls.put(HF_ORIGINATED_CALL_ID, c);
if (mNativeInterface.dial(getByteAddress(mCurrentDevice), c.getNumber())) {
addQueuedAction(DIAL_NUMBER, c.getNumber());
// Start looping on calling current calls.
sendMessage(QUERY_CURRENT_CALLS);
} else {
Log.e(TAG,
"ERROR: Cannot dial with a given number:" + c.toString(true));
// Set the call to terminated remove.
c.setState(BluetoothHeadsetClientCall.CALL_STATE_TERMINATED);
sendCallChangedIntent(c);
mCalls.remove(HF_ORIGINATED_CALL_ID);
}
break;
case ACCEPT_CALL:
acceptCall(message.arg1);
break;
case REJECT_CALL:
rejectCall();
break;
case HOLD_CALL:
holdCall();
break;
case TERMINATE_CALL:
terminateCall();
break;
case ENTER_PRIVATE_MODE:
enterPrivateMode(message.arg1);
break;
case EXPLICIT_CALL_TRANSFER:
explicitCallTransfer();
break;
case SEND_DTMF:
if (mNativeInterface.sendDtmf(getByteAddress(mCurrentDevice),
(byte) message.arg1)) {
addQueuedAction(SEND_DTMF);
} else {
Log.e(TAG, "ERROR: Couldn't send DTMF");
}
break;
case SUBSCRIBER_INFO:
if (mNativeInterface.retrieveSubscriberInfo(
getByteAddress(mCurrentDevice))) {
addQueuedAction(SUBSCRIBER_INFO);
} else {
Log.e(TAG, "ERROR: Couldn't retrieve subscriber info");
}
break;
case QUERY_CURRENT_CALLS:
removeMessages(QUERY_CURRENT_CALLS);
if (mCalls.size() > 0) {
// If there are ongoing calls periodically check their status.
sendMessageDelayed(QUERY_CURRENT_CALLS, QUERY_CURRENT_CALLS_WAIT_MILLIS);
}
queryCallsStart();
break;
case StackEvent.STACK_EVENT:
Intent intent = null;
StackEvent event = (StackEvent) message.obj;
logD("Connected: event type: " + event.type);
switch (event.type) {
case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
logD("Connected: Connection state changed: " + event.device
+ ": " + event.valueInt);
processConnectionEvent(event.valueInt, event.device);
break;
case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
logD("Connected: Audio state changed: " + event.device + ": "
+ event.valueInt);
processAudioEvent(event.valueInt, event.device);
break;
case StackEvent.EVENT_TYPE_NETWORK_STATE:
logD("Connected: Network state: " + event.valueInt);
mIndicatorNetworkState = event.valueInt;
intent = new Intent(BluetoothHeadsetClient.ACTION_AG_EVENT);
intent.putExtra(BluetoothHeadsetClient.EXTRA_NETWORK_STATUS,
event.valueInt);
if (mIndicatorNetworkState
== HeadsetClientHalConstants.NETWORK_STATE_NOT_AVAILABLE) {
mOperatorName = null;
intent.putExtra(BluetoothHeadsetClient.EXTRA_OPERATOR_NAME,
mOperatorName);
}
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
if (mIndicatorNetworkState
== HeadsetClientHalConstants.NETWORK_STATE_AVAILABLE) {
if (mNativeInterface.queryCurrentOperatorName(
getByteAddress(mCurrentDevice))) {
addQueuedAction(QUERY_OPERATOR_NAME);
} else {
Log.e(TAG, "ERROR: Couldn't querry operator name");
}
}
break;
case StackEvent.EVENT_TYPE_ROAMING_STATE:
mIndicatorNetworkType = event.valueInt;
intent = new Intent(BluetoothHeadsetClient.ACTION_AG_EVENT);
intent.putExtra(BluetoothHeadsetClient.EXTRA_NETWORK_ROAMING,
event.valueInt);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
break;
case StackEvent.EVENT_TYPE_NETWORK_SIGNAL:
mIndicatorNetworkSignal = event.valueInt;
intent = new Intent(BluetoothHeadsetClient.ACTION_AG_EVENT);
intent.putExtra(BluetoothHeadsetClient.EXTRA_NETWORK_SIGNAL_STRENGTH,
event.valueInt);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
break;
case StackEvent.EVENT_TYPE_BATTERY_LEVEL:
mIndicatorBatteryLevel = event.valueInt;
intent = new Intent(BluetoothHeadsetClient.ACTION_AG_EVENT);
intent.putExtra(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL,
event.valueInt);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
break;
case StackEvent.EVENT_TYPE_OPERATOR_NAME:
mOperatorName = event.valueString;
intent = new Intent(BluetoothHeadsetClient.ACTION_AG_EVENT);
intent.putExtra(BluetoothHeadsetClient.EXTRA_OPERATOR_NAME,
event.valueString);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
break;
case StackEvent.EVENT_TYPE_VR_STATE_CHANGED:
int oldState = mVoiceRecognitionActive;
mVoiceRecognitionActive = event.valueInt;
broadcastVoiceRecognitionStateChanged(event.device, oldState,
mVoiceRecognitionActive);
break;
case StackEvent.EVENT_TYPE_CALL:
case StackEvent.EVENT_TYPE_CALLSETUP:
case StackEvent.EVENT_TYPE_CALLHELD:
case StackEvent.EVENT_TYPE_RESP_AND_HOLD:
case StackEvent.EVENT_TYPE_CLIP:
case StackEvent.EVENT_TYPE_CALL_WAITING:
sendMessage(QUERY_CURRENT_CALLS);
break;
case StackEvent.EVENT_TYPE_CURRENT_CALLS:
queryCallsUpdate(event.valueInt, event.valueInt3, event.valueString,
event.valueInt4
== HeadsetClientHalConstants.CALL_MPTY_TYPE_MULTI,
event.valueInt2
== HeadsetClientHalConstants.CALL_DIRECTION_OUTGOING);
break;
case StackEvent.EVENT_TYPE_VOLUME_CHANGED:
if (event.valueInt == HeadsetClientHalConstants.VOLUME_TYPE_SPK) {
mCommandedSpeakerVolume = hfToAmVol(event.valueInt2);
logD("AM volume set to " + mCommandedSpeakerVolume);
mAudioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL,
+mCommandedSpeakerVolume, AudioManager.FLAG_SHOW_UI);
} else if (event.valueInt
== HeadsetClientHalConstants.VOLUME_TYPE_MIC) {
mAudioManager.setMicrophoneMute(event.valueInt2 == 0);
}
break;
case StackEvent.EVENT_TYPE_CMD_RESULT:
Pair<Integer, Object> queuedAction = mQueuedActions.poll();
// should not happen but...
if (queuedAction == null || queuedAction.first == NO_ACTION) {
clearPendingAction();
break;
}
logD("Connected: command result: " + event.valueInt
+ " queuedAction: " + queuedAction.first);
switch (queuedAction.first) {
case QUERY_CURRENT_CALLS:
queryCallsDone();
break;
case VOICE_RECOGNITION_START:
if (event.valueInt == AT_OK) {
oldState = mVoiceRecognitionActive;
mVoiceRecognitionActive =
HeadsetClientHalConstants.VR_STATE_STARTED;
broadcastVoiceRecognitionStateChanged(event.device,
oldState, mVoiceRecognitionActive);
}
break;
case VOICE_RECOGNITION_STOP:
if (event.valueInt == AT_OK) {
oldState = mVoiceRecognitionActive;
mVoiceRecognitionActive =
HeadsetClientHalConstants.VR_STATE_STOPPED;
broadcastVoiceRecognitionStateChanged(event.device,
oldState, mVoiceRecognitionActive);
}
break;
default:
Log.w(TAG, "Unhandled AT OK " + event);
break;
}
break;
case StackEvent.EVENT_TYPE_SUBSCRIBER_INFO:
mSubscriberInfo = event.valueString;
intent = new Intent(BluetoothHeadsetClient.ACTION_AG_EVENT);
intent.putExtra(BluetoothHeadsetClient.EXTRA_SUBSCRIBER_INFO,
mSubscriberInfo);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
break;
case StackEvent.EVENT_TYPE_IN_BAND_RINGTONE:
intent = new Intent(BluetoothHeadsetClient.ACTION_AG_EVENT);
mInBandRing = event.valueInt == IN_BAND_RING_ENABLED;
intent.putExtra(BluetoothHeadsetClient.EXTRA_IN_BAND_RING,
event.valueInt);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
logD(event.device.toString() + "onInBandRing" + event.valueInt);
break;
case StackEvent.EVENT_TYPE_RING_INDICATION:
// Ringing is not handled at this indication and rather should be
// implemented (by the client of this service). Use the
// CALL_STATE_INCOMING (and similar) handle ringing.
break;
case StackEvent.EVENT_TYPE_UNKNOWN_EVENT:
if (!mVendorProcessor.processEvent(event.valueString, event.device)) {
Log.e(TAG, "Unknown event :" + event.valueString
+ " for device " + event.device);
}
break;
default:
Log.e(TAG, "Unknown stack event: " + event.type);
break;
}
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
private void broadcastVoiceRecognitionStateChanged(BluetoothDevice device, int oldState,
int newState) {
if (oldState == newState) {
return;
}
Intent intent = new Intent(BluetoothHeadsetClient.ACTION_AG_EVENT);
intent.putExtra(BluetoothHeadsetClient.EXTRA_VOICE_RECOGNITION, newState);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
}
// in Connected state
private void processConnectionEvent(int state, BluetoothDevice device) {
switch (state) {
case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED:
logD("Connected disconnects.");
// AG disconnects
if (mCurrentDevice.equals(device)) {
transitionTo(mDisconnected);
} else {
Log.e(TAG, "Disconnected from unknown device: " + device);
}
break;
default:
Log.e(TAG, "Connection State Device: " + device + " bad state: " + state);
break;
}
}
// in Connected state
private void processAudioEvent(int state, BluetoothDevice device) {
// message from old device
if (!mCurrentDevice.equals(device)) {
Log.e(TAG, "Audio changed on disconnected device: " + device);
return;
}
switch (state) {
case HeadsetClientHalConstants.AUDIO_STATE_CONNECTED_MSBC:
mAudioWbs = true;
// fall through
case HeadsetClientHalConstants.AUDIO_STATE_CONNECTED:
// Audio state is split in two parts, the audio focus is maintained by the
// entity exercising this service (typically the Telecom stack) and audio
// routing is handled by the bluetooth stack itself. The only reason to do so is
// because Bluetooth SCO connection from the HF role is not entirely supported
// for routing and volume purposes.
// NOTE: All calls here are routed via the setParameters which changes the
// routing at the Audio HAL level.
if (mService.isScoRouted()) {
StackEvent event =
new StackEvent(StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED);
event.valueInt = state;
event.device = device;
sendMessageDelayed(StackEvent.STACK_EVENT, event, ROUTING_DELAY_MS);
break;
}
mAudioState = BluetoothHeadsetClient.STATE_AUDIO_CONNECTED;
// We need to set the volume after switching into HFP mode as some Audio HALs
// reset the volume to a known-default on mode switch.
final int amVol = mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL);
final int hfVol = amToHfVol(amVol);
logD("hfp_enable=true mAudioWbs is " + mAudioWbs);
if (mAudioWbs) {
logD("Setting sampling rate as 16000");
mAudioManager.setParameters("hfp_set_sampling_rate=16000");
} else {
logD("Setting sampling rate as 8000");
mAudioManager.setParameters("hfp_set_sampling_rate=8000");
}
logD("hf_volume " + hfVol);
routeHfpAudio(true);
mAudioFocusRequest = requestAudioFocus();
mAudioManager.setParameters("hfp_volume=" + hfVol);
transitionTo(mAudioOn);
break;
case HeadsetClientHalConstants.AUDIO_STATE_CONNECTING:
// No state transition is involved, fire broadcast immediately
broadcastAudioState(device, BluetoothHeadsetClient.STATE_AUDIO_CONNECTING,
mAudioState);
mAudioState = BluetoothHeadsetClient.STATE_AUDIO_CONNECTING;
break;
case HeadsetClientHalConstants.AUDIO_STATE_DISCONNECTED:
// No state transition is involved, fire broadcast immediately
broadcastAudioState(device, BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED,
mAudioState);
mAudioState = BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED;
break;
default:
Log.e(TAG, "Audio State Device: " + device + " bad state: " + state);
break;
}
}
@Override
public void exit() {
logD("Exit Connected: " + getCurrentMessage().what);
mPrevState = this;
}
}
class AudioOn extends State {
@Override
public void enter() {
logD("Enter AudioOn: " + getCurrentMessage().what);
broadcastAudioState(mCurrentDevice, BluetoothHeadsetClient.STATE_AUDIO_CONNECTED,
BluetoothHeadsetClient.STATE_AUDIO_CONNECTING);
}
@Override
public synchronized boolean processMessage(Message message) {
logD("AudioOn process message: " + message.what);
if (DBG) {
if (mCurrentDevice == null) {
Log.e(TAG, "ERROR: mCurrentDevice is null in Connected");
return NOT_HANDLED;
}
}
switch (message.what) {
case DISCONNECT:
BluetoothDevice device = (BluetoothDevice) message.obj;
if (!mCurrentDevice.equals(device)) {
break;
}
deferMessage(message);
/*
* fall through - disconnect audio first then expect
* deferred DISCONNECT message in Connected state
*/
case DISCONNECT_AUDIO:
/*
* just disconnect audio and wait for
* StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED, that triggers State
* Machines state changing
*/
if (mNativeInterface.disconnectAudio(getByteAddress(mCurrentDevice))) {
routeHfpAudio(false);
returnAudioFocusIfNecessary();
}
break;
case HOLD_CALL:
holdCall();
break;
case StackEvent.STACK_EVENT:
StackEvent event = (StackEvent) message.obj;
logD("AudioOn: event type: " + event.type);
switch (event.type) {
case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
logD("AudioOn connection state changed" + event.device + ": "
+ event.valueInt);
processConnectionEvent(event.valueInt, event.device);
break;
case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
logD("AudioOn audio state changed" + event.device + ": "
+ event.valueInt);
processAudioEvent(event.valueInt, event.device);
break;
default:
return NOT_HANDLED;
}
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
// in AudioOn state. Can AG disconnect RFCOMM prior to SCO? Handle this
private void processConnectionEvent(int state, BluetoothDevice device) {
switch (state) {
case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED:
if (mCurrentDevice.equals(device)) {
processAudioEvent(HeadsetClientHalConstants.AUDIO_STATE_DISCONNECTED,
device);
transitionTo(mDisconnected);
} else {
Log.e(TAG, "Disconnected from unknown device: " + device);
}
break;
default:
Log.e(TAG, "Connection State Device: " + device + " bad state: " + state);
break;
}
}
// in AudioOn state
private void processAudioEvent(int state, BluetoothDevice device) {
if (!mCurrentDevice.equals(device)) {
Log.e(TAG, "Audio changed on disconnected device: " + device);
return;
}
switch (state) {
case HeadsetClientHalConstants.AUDIO_STATE_DISCONNECTED:
removeMessages(DISCONNECT_AUDIO);
mAudioState = BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED;
// Audio focus may still be held by the entity controlling the actual call
// (such as Telecom) and hence this will still keep the call around, there
// is not much we can do here since dropping the call without user consent
// even if the audio connection snapped may not be a good idea.
routeHfpAudio(false);
returnAudioFocusIfNecessary();
transitionTo(mConnected);
break;
default:
Log.e(TAG, "Audio State Device: " + device + " bad state: " + state);
break;
}
}
@Override
public void exit() {
logD("Exit AudioOn: " + getCurrentMessage().what);
mPrevState = this;
broadcastAudioState(mCurrentDevice, BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED,
BluetoothHeadsetClient.STATE_AUDIO_CONNECTED);
}
}
/**
* @hide
*/
public synchronized int getConnectionState(BluetoothDevice device) {
if (mCurrentDevice == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
if (!mCurrentDevice.equals(device)) {
return BluetoothProfile.STATE_DISCONNECTED;
}
IState currentState = getCurrentState();
if (currentState == mConnecting) {
return BluetoothProfile.STATE_CONNECTING;
}
if (currentState == mConnected || currentState == mAudioOn) {
return BluetoothProfile.STATE_CONNECTED;
}
Log.e(TAG, "Bad currentState: " + currentState);
return BluetoothProfile.STATE_DISCONNECTED;
}
private void broadcastAudioState(BluetoothDevice device, int newState, int prevState) {
BluetoothStatsLog.write(BluetoothStatsLog.BLUETOOTH_SCO_CONNECTION_STATE_CHANGED,
AdapterService.getAdapterService().obfuscateAddress(device),
getConnectionStateFromAudioState(newState), mAudioWbs
? BluetoothHfpProtoEnums.SCO_CODEC_MSBC
: BluetoothHfpProtoEnums.SCO_CODEC_CVSD,
AdapterService.getAdapterService().getMetricId(device));
Intent intent = new Intent(BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, newState);
if (newState == BluetoothHeadsetClient.STATE_AUDIO_CONNECTED) {
intent.putExtra(BluetoothHeadsetClient.EXTRA_AUDIO_WBS, mAudioWbs);
}
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
logD("Audio state " + device + ": " + prevState + "->" + newState);
}
// This method does not check for error condition (newState == prevState)
private void broadcastConnectionState(BluetoothDevice device, int newState, int prevState) {
logD("Connection state " + device + ": " + prevState + "->" + newState);
/*
* Notifying the connection state change of the profile before sending
* the intent for connection state change, as it was causing a race
* condition, with the UI not being updated with the correct connection
* state.
*/
Intent intent = new Intent(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, newState);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
// add feature extras when connected
if (newState == BluetoothProfile.STATE_CONNECTED) {
if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_3WAY)
== HeadsetClientHalConstants.PEER_FEAT_3WAY) {
intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_3WAY_CALLING, true);
}
if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_VREC)
== HeadsetClientHalConstants.PEER_FEAT_VREC) {
intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_VOICE_RECOGNITION, true);
}
if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_REJECT)
== HeadsetClientHalConstants.PEER_FEAT_REJECT) {
intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_REJECT_CALL, true);
}
if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_ECC)
== HeadsetClientHalConstants.PEER_FEAT_ECC) {
intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_ECC, true);
}
// add individual CHLD support extras
if ((mChldFeatures & HeadsetClientHalConstants.CHLD_FEAT_HOLD_ACC)
== HeadsetClientHalConstants.CHLD_FEAT_HOLD_ACC) {
intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_ACCEPT_HELD_OR_WAITING_CALL,
true);
}
if ((mChldFeatures & HeadsetClientHalConstants.CHLD_FEAT_REL)
== HeadsetClientHalConstants.CHLD_FEAT_REL) {
intent.putExtra(
BluetoothHeadsetClient.EXTRA_AG_FEATURE_RELEASE_HELD_OR_WAITING_CALL, true);
}
if ((mChldFeatures & HeadsetClientHalConstants.CHLD_FEAT_REL_ACC)
== HeadsetClientHalConstants.CHLD_FEAT_REL_ACC) {
intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_RELEASE_AND_ACCEPT, true);
}
if ((mChldFeatures & HeadsetClientHalConstants.CHLD_FEAT_MERGE)
== HeadsetClientHalConstants.CHLD_FEAT_MERGE) {
intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_MERGE, true);
}
if ((mChldFeatures & HeadsetClientHalConstants.CHLD_FEAT_MERGE_DETACH)
== HeadsetClientHalConstants.CHLD_FEAT_MERGE_DETACH) {
intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_MERGE_AND_DETACH, true);
}
}
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
}
boolean isConnected() {
IState currentState = getCurrentState();
return (currentState == mConnected || currentState == mAudioOn);
}
List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>();
Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
int connectionState;
synchronized (this) {
for (BluetoothDevice device : bondedDevices) {
ParcelUuid[] featureUuids = device.getUuids();
if (!ArrayUtils.contains(featureUuids, BluetoothUuid.HFP_AG)) {
continue;
}
connectionState = getConnectionState(device);
for (int state : states) {
if (connectionState == state) {
deviceList.add(device);
}
}
}
}
return deviceList;
}
boolean okToConnect(BluetoothDevice device) {
int connectionPolicy = mService.getConnectionPolicy(device);
boolean ret = false;
// check connection policy and accept or reject the connection. if connection policy is
// undefined
// it is likely that our SDP has not completed and peer is initiating
// the
// connection. Allow this connection, provided the device is bonded
if ((BluetoothProfile.CONNECTION_POLICY_FORBIDDEN < connectionPolicy) || (
(BluetoothProfile.CONNECTION_POLICY_UNKNOWN == connectionPolicy)
&& (device.getBondState() != BluetoothDevice.BOND_NONE))) {
ret = true;
}
return ret;
}
boolean isAudioOn() {
return (getCurrentState() == mAudioOn);
}
synchronized int getAudioState(BluetoothDevice device) {
if (mCurrentDevice == null || !mCurrentDevice.equals(device)) {
return BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED;
}
return mAudioState;
}
List<BluetoothDevice> getConnectedDevices() {
List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
synchronized (this) {
if (isConnected()) {
devices.add(mCurrentDevice);
}
}
return devices;
}
private byte[] getByteAddress(BluetoothDevice device) {
return Utils.getBytesFromAddress(device.getAddress());
}
public List<BluetoothHeadsetClientCall> getCurrentCalls() {
return new ArrayList<BluetoothHeadsetClientCall>(mCalls.values());
}
public Bundle getCurrentAgEvents() {
Bundle b = new Bundle();
b.putInt(BluetoothHeadsetClient.EXTRA_NETWORK_STATUS, mIndicatorNetworkState);
b.putInt(BluetoothHeadsetClient.EXTRA_NETWORK_SIGNAL_STRENGTH, mIndicatorNetworkSignal);
b.putInt(BluetoothHeadsetClient.EXTRA_NETWORK_ROAMING, mIndicatorNetworkType);
b.putInt(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL, mIndicatorBatteryLevel);
b.putString(BluetoothHeadsetClient.EXTRA_OPERATOR_NAME, mOperatorName);
b.putString(BluetoothHeadsetClient.EXTRA_SUBSCRIBER_INFO, mSubscriberInfo);
return b;
}
private static int getConnectionStateFromAudioState(int audioState) {
switch (audioState) {
case BluetoothHeadsetClient.STATE_AUDIO_CONNECTED:
return BluetoothAdapter.STATE_CONNECTED;
case BluetoothHeadsetClient.STATE_AUDIO_CONNECTING:
return BluetoothAdapter.STATE_CONNECTING;
case BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED:
return BluetoothAdapter.STATE_DISCONNECTED;
}
return BluetoothAdapter.STATE_DISCONNECTED;
}
private static void logD(String message) {
if (DBG) {
Log.d(TAG, message);
}
}
}