| /* |
| * Copyright (c) 2014 The Android Open Source Project |
| * Copyright (C) 2012 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.os.Bundle; |
| import android.os.Message; |
| import android.os.ParcelUuid; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.media.AudioManager; |
| import android.media.Ringtone; |
| import android.media.RingtoneManager; |
| import android.net.Uri; |
| import android.telecom.TelecomManager; |
| |
| import com.android.bluetooth.Utils; |
| import com.android.bluetooth.btservice.AdapterService; |
| import com.android.bluetooth.btservice.ProfileService; |
| import com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService; |
| import com.android.internal.util.IState; |
| import com.android.internal.util.State; |
| import com.android.internal.util.StateMachine; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Hashtable; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Queue; |
| import java.util.Set; |
| |
| import com.android.bluetooth.R; |
| |
| final class HeadsetClientStateMachine extends StateMachine { |
| private static final String TAG = "HeadsetClientStateMachine"; |
| private static final boolean DBG = false; |
| |
| static final int NO_ACTION = 0; |
| |
| // external actions |
| static final int CONNECT = 1; |
| static final int DISCONNECT = 2; |
| static final int CONNECT_AUDIO = 3; |
| static final int DISCONNECT_AUDIO = 4; |
| static final int VOICE_RECOGNITION_START = 5; |
| static final int VOICE_RECOGNITION_STOP = 6; |
| static final int SET_MIC_VOLUME = 7; |
| static final int SET_SPEAKER_VOLUME = 8; |
| static final int REDIAL = 9; |
| static final int DIAL_NUMBER = 10; |
| static final int DIAL_MEMORY = 11; |
| static final int ACCEPT_CALL = 12; |
| static final int REJECT_CALL = 13; |
| static final int HOLD_CALL = 14; |
| static final int TERMINATE_CALL = 15; |
| static final int ENTER_PRIVATE_MODE = 16; |
| static final int SEND_DTMF = 17; |
| static final int EXPLICIT_CALL_TRANSFER = 18; |
| static final int LAST_VTAG_NUMBER = 19; |
| static final int DISABLE_NREC = 20; |
| |
| // internal actions |
| static final int QUERY_CURRENT_CALLS = 50; |
| static final int QUERY_OPERATOR_NAME = 51; |
| static final int SUBSCRIBER_INFO = 52; |
| // special action to handle terminating specific call from multiparty call |
| static final int TERMINATE_SPECIFIC_CALL = 53; |
| |
| static final int MAX_HFP_SCO_VOICE_CALL_VOLUME = 15; // HFP 1.5 spec. |
| static final int MIN_HFP_SCO_VOICE_CALL_VOLUME = 1; // HFP 1.5 spec. |
| |
| private static final int STACK_EVENT = 100; |
| |
| private final Disconnected mDisconnected; |
| private final Connecting mConnecting; |
| private final Connected mConnected; |
| private final AudioOn mAudioOn; |
| |
| private final HeadsetClientService mService; |
| |
| private Hashtable<Integer, BluetoothHeadsetClientCall> mCalls; |
| private Hashtable<Integer, BluetoothHeadsetClientCall> mCallsUpdate; |
| private boolean mQueryCallsSupported; |
| |
| private int mIndicatorNetworkState; |
| private int mIndicatorNetworkType; |
| private int mIndicatorNetworkSignal; |
| private int mIndicatorBatteryLevel; |
| |
| private int mIndicatorCall; |
| private int mIndicatorCallSetup; |
| private int mIndicatorCallHeld; |
| private boolean mVgsFromStack = false; |
| private boolean mVgmFromStack = false; |
| |
| private String mOperatorName; |
| private String mSubscriberInfo; |
| |
| private int mVoiceRecognitionActive; |
| private int mInBandRingtone; |
| |
| private int mMaxAmVcVol; |
| private int mMinAmVcVol; |
| |
| // 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 final AudioManager mAudioManager; |
| private int mAudioState; |
| // Indicates whether audio can be routed to the device. |
| private boolean mAudioRouteAllowed; |
| private boolean mAudioWbs; |
| private final BluetoothAdapter mAdapter; |
| private boolean mNativeAvailable; |
| private TelecomManager mTelecomManager; |
| |
| // currently connected device |
| private BluetoothDevice mCurrentDevice = null; |
| |
| // general peer features and call handling features |
| private int mPeerFeatures; |
| private int mChldFeatures; |
| |
| static { |
| classInitNative(); |
| } |
| |
| public void dump(StringBuilder sb) { |
| ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice); |
| ProfileService.println(sb, "mAudioOn: " + mAudioOn); |
| 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, "mIndicatorCall: " + mIndicatorCall); |
| ProfileService.println(sb, "mIndicatorCallSetup: " + mIndicatorCallSetup); |
| ProfileService.println(sb, "mIndicatorCallHeld: " + mIndicatorCallHeld); |
| ProfileService.println(sb, "mVgsFromStack: " + mVgsFromStack); |
| ProfileService.println(sb, "mVgmFromStack: " + mVgmFromStack); |
| ProfileService.println(sb, "mOperatorName: " + mOperatorName); |
| ProfileService.println(sb, "mSubscriberInfo: " + mSubscriberInfo); |
| ProfileService.println(sb, "mVoiceRecognitionActive: " + mVoiceRecognitionActive); |
| ProfileService.println(sb, "mInBandRingtone: " + mInBandRingtone); |
| |
| 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); |
| } |
| } |
| } |
| |
| 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 void addCall(int state, String number) { |
| Log.d(TAG, "addToCalls state:" + state + " number:" + number); |
| |
| boolean outgoing = state == BluetoothHeadsetClientCall.CALL_STATE_DIALING || |
| state == BluetoothHeadsetClientCall.CALL_STATE_ALERTING; |
| |
| // new call always takes lowest possible id, starting with 1 |
| Integer id = 1; |
| while (mCalls.containsKey(id)) { |
| id++; |
| } |
| |
| BluetoothHeadsetClientCall c = new BluetoothHeadsetClientCall(mCurrentDevice, id, state, |
| number, false, outgoing); |
| mCalls.put(id, c); |
| |
| sendCallChangedIntent(c); |
| } |
| |
| private void removeCalls(int... states) { |
| Log.d(TAG, "removeFromCalls states:" + Arrays.toString(states)); |
| |
| Iterator<Hashtable.Entry<Integer, BluetoothHeadsetClientCall>> it; |
| |
| it = mCalls.entrySet().iterator(); |
| while (it.hasNext()) { |
| BluetoothHeadsetClientCall c = it.next().getValue(); |
| |
| for (int s : states) { |
| if (c.getState() == s) { |
| it.remove(); |
| setCallState(c, BluetoothHeadsetClientCall.CALL_STATE_TERMINATED); |
| break; |
| } |
| } |
| } |
| } |
| |
| private void changeCallsState(int old_state, int new_state) { |
| Log.d(TAG, "changeStateFromCalls old:" + old_state + " new: " + new_state); |
| |
| for (BluetoothHeadsetClientCall c : mCalls.values()) { |
| if (c.getState() == old_state) { |
| setCallState(c, new_state); |
| } |
| } |
| } |
| |
| private BluetoothHeadsetClientCall getCall(int... states) { |
| Log.d(TAG, "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 updateCallsMultiParty() { |
| boolean multi = callsInState(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) > 1; |
| |
| for (BluetoothHeadsetClientCall c : mCalls.values()) { |
| if (c.getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) { |
| if (c.isMultiParty() == multi) { |
| continue; |
| } |
| |
| c.setMultiParty(multi); |
| sendCallChangedIntent(c); |
| } else { |
| if (c.isMultiParty()) { |
| c.setMultiParty(false); |
| sendCallChangedIntent(c); |
| } |
| } |
| } |
| } |
| |
| private void setCallState(BluetoothHeadsetClientCall c, int state) { |
| if (state == c.getState()) { |
| return; |
| } |
| c.setState(state); |
| sendCallChangedIntent(c); |
| } |
| |
| private void sendCallChangedIntent(BluetoothHeadsetClientCall c) { |
| Log.d(TAG, "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 waitForIndicators(int call, int callsetup, int callheld) { |
| // all indicators initial values received |
| if (mIndicatorCall != -1 && mIndicatorCallSetup != -1 && |
| mIndicatorCallHeld != -1) { |
| return false; |
| } |
| |
| if (call != -1) { |
| mIndicatorCall = call; |
| } else if (callsetup != -1) { |
| mIndicatorCallSetup = callsetup; |
| } else if (callheld != -1) { |
| mIndicatorCallHeld = callheld; |
| } |
| |
| // still waiting for some indicators |
| if (mIndicatorCall == -1 || mIndicatorCallSetup == -1 || |
| mIndicatorCallHeld == -1) { |
| return true; |
| } |
| |
| // for start always query calls to define if it is supported |
| mQueryCallsSupported = queryCallsStart(); |
| |
| if (mQueryCallsSupported) { |
| return true; |
| } |
| |
| // no support for querying calls |
| |
| switch (mIndicatorCallSetup) { |
| case HeadsetClientHalConstants.CALLSETUP_INCOMING: |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING, ""); |
| break; |
| case HeadsetClientHalConstants.CALLSETUP_OUTGOING: |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_DIALING, ""); |
| break; |
| case HeadsetClientHalConstants.CALLSETUP_ALERTING: |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_ALERTING, ""); |
| break; |
| case HeadsetClientHalConstants.CALLSETUP_NONE: |
| default: |
| break; |
| } |
| |
| switch (mIndicatorCall) { |
| case HeadsetClientHalConstants.CALL_CALLS_IN_PROGRESS: |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE, ""); |
| break; |
| case HeadsetClientHalConstants.CALL_NO_CALLS_IN_PROGRESS: |
| default: |
| break; |
| } |
| |
| switch (mIndicatorCallHeld) { |
| case HeadsetClientHalConstants.CALLHELD_HOLD_AND_ACTIVE: |
| case HeadsetClientHalConstants.CALLHELD_HOLD: |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_HELD, ""); |
| break; |
| case HeadsetClientHalConstants.CALLHELD_NONE: |
| default: |
| break; |
| } |
| |
| return true; |
| } |
| |
| private void updateCallIndicator(int call) { |
| Log.d(TAG, "updateCallIndicator " + call); |
| |
| if (waitForIndicators(call, -1, -1)) { |
| return; |
| } |
| |
| if (mQueryCallsSupported) { |
| sendMessage(QUERY_CURRENT_CALLS); |
| return; |
| } |
| |
| BluetoothHeadsetClientCall c = null; |
| |
| switch (call) { |
| case HeadsetClientHalConstants.CALL_NO_CALLS_IN_PROGRESS: |
| removeCalls(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE, |
| BluetoothHeadsetClientCall.CALL_STATE_HELD, |
| BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD); |
| |
| break; |
| case HeadsetClientHalConstants.CALL_CALLS_IN_PROGRESS: |
| if (mIndicatorCall == HeadsetClientHalConstants.CALL_CALLS_IN_PROGRESS) { |
| // WP7.8 is sending call=1 before setup=0 when rejecting |
| // waiting call |
| if (mIndicatorCallSetup != HeadsetClientHalConstants.CALLSETUP_NONE) { |
| c = getCall(BluetoothHeadsetClientCall.CALL_STATE_WAITING); |
| if (c != null) { |
| setCallState(c, BluetoothHeadsetClientCall.CALL_STATE_TERMINATED); |
| mCalls.remove(c.getId()); |
| } |
| } |
| |
| break; |
| } |
| |
| // if there is only waiting call it is changed to incoming so |
| // don't |
| // handle it here |
| if (mIndicatorCallSetup != HeadsetClientHalConstants.CALLSETUP_NONE) { |
| c = getCall(BluetoothHeadsetClientCall.CALL_STATE_DIALING, |
| BluetoothHeadsetClientCall.CALL_STATE_ALERTING, |
| BluetoothHeadsetClientCall.CALL_STATE_INCOMING); |
| if (c != null) { |
| setCallState(c, BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| } |
| } |
| |
| updateCallsMultiParty(); |
| break; |
| default: |
| break; |
| } |
| |
| mIndicatorCall = call; |
| } |
| |
| private void updateCallSetupIndicator(int callsetup) { |
| Log.d(TAG, "updateCallSetupIndicator " + callsetup + " " + mPendingAction.first); |
| |
| if (waitForIndicators(-1, callsetup, -1)) { |
| return; |
| } |
| |
| if (mQueryCallsSupported) { |
| sendMessage(QUERY_CURRENT_CALLS); |
| return; |
| } |
| |
| switch (callsetup) { |
| case HeadsetClientHalConstants.CALLSETUP_NONE: |
| switch (mPendingAction.first) { |
| case ACCEPT_CALL: |
| switch ((Integer) mPendingAction.second) { |
| case HeadsetClientHalConstants.CALL_ACTION_ATA: |
| removeCalls(BluetoothHeadsetClientCall.CALL_STATE_DIALING, |
| BluetoothHeadsetClientCall.CALL_STATE_ALERTING); |
| clearPendingAction(); |
| break; |
| case HeadsetClientHalConstants.CALL_ACTION_CHLD_1: |
| removeCalls(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| changeCallsState(BluetoothHeadsetClientCall.CALL_STATE_WAITING, |
| BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| clearPendingAction(); |
| break; |
| case HeadsetClientHalConstants.CALL_ACTION_CHLD_2: |
| // no specific order for callsetup=0 and |
| // callheld=1 |
| if (mIndicatorCallHeld == |
| HeadsetClientHalConstants.CALLHELD_HOLD_AND_ACTIVE) { |
| clearPendingAction(); |
| } |
| break; |
| case HeadsetClientHalConstants.CALL_ACTION_CHLD_3: |
| if (mIndicatorCallHeld == |
| HeadsetClientHalConstants.CALLHELD_NONE) { |
| clearPendingAction(); |
| } |
| break; |
| default: |
| Log.e(TAG, "Unexpected callsetup=0 while in action ACCEPT_CALL"); |
| break; |
| } |
| break; |
| case REJECT_CALL: |
| switch ((Integer) mPendingAction.second) { |
| case HeadsetClientHalConstants.CALL_ACTION_CHUP: |
| removeCalls(BluetoothHeadsetClientCall.CALL_STATE_INCOMING); |
| clearPendingAction(); |
| break; |
| case HeadsetClientHalConstants.CALL_ACTION_CHLD_0: |
| removeCalls(BluetoothHeadsetClientCall.CALL_STATE_WAITING); |
| clearPendingAction(); |
| break; |
| default: |
| Log.e(TAG, "Unexpected callsetup=0 while in action REJECT_CALL"); |
| break; |
| } |
| break; |
| case DIAL_NUMBER: |
| case DIAL_MEMORY: |
| case REDIAL: |
| case NO_ACTION: |
| case TERMINATE_CALL: |
| removeCalls(BluetoothHeadsetClientCall.CALL_STATE_INCOMING, |
| BluetoothHeadsetClientCall.CALL_STATE_DIALING, |
| BluetoothHeadsetClientCall.CALL_STATE_WAITING, |
| BluetoothHeadsetClientCall.CALL_STATE_ALERTING); |
| clearPendingAction(); |
| break; |
| default: |
| Log.e(TAG, "Unexpected callsetup=0 while in action " + |
| mPendingAction.first); |
| break; |
| } |
| break; |
| case HeadsetClientHalConstants.CALLSETUP_ALERTING: |
| BluetoothHeadsetClientCall c = |
| getCall(BluetoothHeadsetClientCall.CALL_STATE_DIALING); |
| if (c == null) { |
| if (mPendingAction.first == DIAL_NUMBER) { |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_ALERTING, |
| (String) mPendingAction.second); |
| } else { |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_ALERTING, ""); |
| } |
| } else { |
| setCallState(c, BluetoothHeadsetClientCall.CALL_STATE_ALERTING); |
| } |
| |
| switch (mPendingAction.first) { |
| case DIAL_NUMBER: |
| case DIAL_MEMORY: |
| case REDIAL: |
| clearPendingAction(); |
| break; |
| default: |
| break; |
| } |
| break; |
| case HeadsetClientHalConstants.CALLSETUP_OUTGOING: |
| if (mPendingAction.first == DIAL_NUMBER) { |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_DIALING, |
| (String) mPendingAction.second); |
| } else { |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_DIALING, ""); |
| } |
| break; |
| case HeadsetClientHalConstants.CALLSETUP_INCOMING: |
| if (getCall(BluetoothHeadsetClientCall.CALL_STATE_WAITING) == null) |
| { |
| // will get number in clip if known |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING, ""); |
| } |
| break; |
| default: |
| break; |
| } |
| |
| updateCallsMultiParty(); |
| |
| mIndicatorCallSetup = callsetup; |
| } |
| |
| private void updateCallHeldIndicator(int callheld) { |
| Log.d(TAG, "updateCallHeld " + callheld); |
| |
| if (waitForIndicators(-1, -1, callheld)) { |
| return; |
| } |
| |
| if (mQueryCallsSupported) { |
| sendMessage(QUERY_CURRENT_CALLS); |
| return; |
| } |
| |
| switch (callheld) { |
| case HeadsetClientHalConstants.CALLHELD_NONE: |
| switch (mPendingAction.first) { |
| case REJECT_CALL: |
| removeCalls(BluetoothHeadsetClientCall.CALL_STATE_HELD); |
| clearPendingAction(); |
| break; |
| case ACCEPT_CALL: |
| switch ((Integer) mPendingAction.second) { |
| case HeadsetClientHalConstants.CALL_ACTION_CHLD_1: |
| removeCalls(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| changeCallsState(BluetoothHeadsetClientCall.CALL_STATE_HELD, |
| BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| clearPendingAction(); |
| break; |
| case HeadsetClientHalConstants.CALL_ACTION_CHLD_3: |
| changeCallsState(BluetoothHeadsetClientCall.CALL_STATE_HELD, |
| BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| clearPendingAction(); |
| break; |
| default: |
| break; |
| } |
| break; |
| case NO_ACTION: |
| if (mIndicatorCall == HeadsetClientHalConstants.CALL_CALLS_IN_PROGRESS && |
| mIndicatorCallHeld == HeadsetClientHalConstants.CALLHELD_HOLD) { |
| changeCallsState(BluetoothHeadsetClientCall.CALL_STATE_HELD, |
| BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| break; |
| } |
| |
| removeCalls(BluetoothHeadsetClientCall.CALL_STATE_HELD); |
| break; |
| default: |
| Log.e(TAG, "Unexpected callheld=0 while in action " + mPendingAction.first); |
| break; |
| } |
| break; |
| case HeadsetClientHalConstants.CALLHELD_HOLD_AND_ACTIVE: |
| switch (mPendingAction.first) { |
| case ACCEPT_CALL: |
| if ((Integer) mPendingAction.second == |
| HeadsetClientHalConstants.CALL_ACTION_CHLD_2) { |
| BluetoothHeadsetClientCall c = |
| getCall(BluetoothHeadsetClientCall.CALL_STATE_WAITING); |
| if (c != null) { // accept |
| changeCallsState(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE, |
| BluetoothHeadsetClientCall.CALL_STATE_HELD); |
| setCallState(c, BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| } else { // swap |
| for (BluetoothHeadsetClientCall cc : mCalls.values()) { |
| if (cc.getState() == |
| BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) { |
| setCallState(cc, |
| BluetoothHeadsetClientCall.CALL_STATE_HELD); |
| } else if (cc.getState() == |
| BluetoothHeadsetClientCall.CALL_STATE_HELD) { |
| setCallState(cc, |
| BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| } |
| } |
| } |
| clearPendingAction(); |
| } |
| break; |
| case NO_ACTION: |
| BluetoothHeadsetClientCall c = |
| getCall(BluetoothHeadsetClientCall.CALL_STATE_WAITING); |
| if (c != null) { // accept |
| changeCallsState(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE, |
| BluetoothHeadsetClientCall.CALL_STATE_HELD); |
| setCallState(c, BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| break; |
| } |
| |
| // swap |
| for (BluetoothHeadsetClientCall cc : mCalls.values()) { |
| if (cc.getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) { |
| setCallState(cc, BluetoothHeadsetClientCall.CALL_STATE_HELD); |
| } else if (cc.getState() == BluetoothHeadsetClientCall.CALL_STATE_HELD) { |
| setCallState(cc, BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| } |
| } |
| break; |
| case ENTER_PRIVATE_MODE: |
| for (BluetoothHeadsetClientCall cc : mCalls.values()) { |
| if (cc != (BluetoothHeadsetClientCall) mPendingAction.second) { |
| setCallState(cc, BluetoothHeadsetClientCall.CALL_STATE_HELD); |
| } |
| } |
| clearPendingAction(); |
| break; |
| default: |
| Log.e(TAG, "Unexpected callheld=0 while in action " + mPendingAction.first); |
| break; |
| } |
| break; |
| case HeadsetClientHalConstants.CALLHELD_HOLD: |
| switch (mPendingAction.first) { |
| case DIAL_NUMBER: |
| case DIAL_MEMORY: |
| case REDIAL: |
| changeCallsState(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE, |
| BluetoothHeadsetClientCall.CALL_STATE_HELD); |
| break; |
| case REJECT_CALL: |
| switch ((Integer) mPendingAction.second) { |
| case HeadsetClientHalConstants.CALL_ACTION_CHLD_1: |
| removeCalls(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| changeCallsState(BluetoothHeadsetClientCall.CALL_STATE_HELD, |
| BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| clearPendingAction(); |
| break; |
| case HeadsetClientHalConstants.CALL_ACTION_CHLD_3: |
| changeCallsState(BluetoothHeadsetClientCall.CALL_STATE_HELD, |
| BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| clearPendingAction(); |
| break; |
| default: |
| break; |
| } |
| break; |
| case TERMINATE_CALL: |
| case NO_ACTION: |
| removeCalls(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| break; |
| default: |
| Log.e(TAG, "Unexpected callheld=0 while in action " + mPendingAction.first); |
| break; |
| } |
| break; |
| default: |
| break; |
| } |
| |
| updateCallsMultiParty(); |
| |
| mIndicatorCallHeld = callheld; |
| } |
| |
| private void updateRespAndHold(int resp_and_hold) { |
| Log.d(TAG, "updatRespAndHold " + resp_and_hold); |
| |
| if (mQueryCallsSupported) { |
| sendMessage(QUERY_CURRENT_CALLS); |
| return; |
| } |
| |
| BluetoothHeadsetClientCall c = null; |
| |
| switch (resp_and_hold) { |
| case HeadsetClientHalConstants.RESP_AND_HOLD_HELD: |
| // might be active if it was resp-and-hold before SLC created |
| c = getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING, |
| BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| if (c != null) { |
| setCallState(c, |
| BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD); |
| } else { |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD, ""); |
| } |
| break; |
| case HeadsetClientHalConstants.RESP_AND_HOLD_ACCEPT: |
| c = getCall(BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD); |
| if (c != null) { |
| setCallState(c, BluetoothHeadsetClientCall.CALL_STATE_ACTIVE); |
| } |
| if (mPendingAction.first == ACCEPT_CALL && |
| (Integer) mPendingAction.second == |
| HeadsetClientHalConstants.CALL_ACTION_BTRH_1) { |
| clearPendingAction(); |
| } |
| break; |
| case HeadsetClientHalConstants.RESP_AND_HOLD_REJECT: |
| removeCalls(BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| private void updateClip(String number) { |
| BluetoothHeadsetClientCall c = getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING); |
| |
| if (c == null) { |
| // MeeGo sends CLCC indicating waiting call followed by CLIP when call state changes |
| // from waiting to incoming in 3WC scenarios. Handle this call state transfer here. |
| BluetoothHeadsetClientCall cw = getCall(BluetoothHeadsetClientCall.CALL_STATE_WAITING); |
| if(cw != null) { |
| setCallState(cw, BluetoothHeadsetClientCall.CALL_STATE_INCOMING); |
| } |
| else { |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING, number); |
| } |
| } else { |
| c.setNumber(number); |
| sendCallChangedIntent(c); |
| } |
| } |
| |
| private void addCallWaiting(String number) { |
| if (getCall(BluetoothHeadsetClientCall.CALL_STATE_WAITING) == null) { |
| addCall(BluetoothHeadsetClientCall.CALL_STATE_WAITING, number); |
| } |
| } |
| |
| // use ECS |
| private boolean queryCallsStart() { |
| Log.d(TAG, "queryCallsStart"); |
| |
| // not supported |
| if (mQueryCallsSupported == false) { |
| return false; |
| } |
| |
| clearPendingAction(); |
| |
| // already started |
| if (mCallsUpdate != null) { |
| return true; |
| } |
| |
| if (queryCurrentCallsNative()) { |
| mCallsUpdate = new Hashtable<Integer, BluetoothHeadsetClientCall>(); |
| addQueuedAction(QUERY_CURRENT_CALLS, 0); |
| return true; |
| } |
| |
| Log.i(TAG, "updateCallsStart queryCurrentCallsNative failed"); |
| mQueryCallsSupported = false; |
| mCallsUpdate = null; |
| return false; |
| } |
| |
| private void queryCallsDone() { |
| Log.d(TAG, "queryCallsDone"); |
| Iterator<Hashtable.Entry<Integer, BluetoothHeadsetClientCall>> it; |
| |
| // check if any call was removed |
| it = mCalls.entrySet().iterator(); |
| while (it.hasNext()) { |
| Hashtable.Entry<Integer, BluetoothHeadsetClientCall> entry = it.next(); |
| |
| if (mCallsUpdate.containsKey(entry.getKey())) { |
| continue; |
| } |
| |
| Log.d(TAG, "updateCallsDone call removed id:" + entry.getValue().getId()); |
| BluetoothHeadsetClientCall c = entry.getValue(); |
| |
| setCallState(c, BluetoothHeadsetClientCall.CALL_STATE_TERMINATED); |
| } |
| |
| /* check if any calls changed or new call is present */ |
| it = mCallsUpdate.entrySet().iterator(); |
| while (it.hasNext()) { |
| Hashtable.Entry<Integer, BluetoothHeadsetClientCall> entry = it.next(); |
| |
| if (mCalls.containsKey(entry.getKey())) { |
| // avoid losing number if was not present in clcc |
| if (entry.getValue().getNumber().equals("")) { |
| entry.getValue().setNumber(mCalls.get(entry.getKey()).getNumber()); |
| } |
| |
| if (mCalls.get(entry.getKey()).equals(entry.getValue())) { |
| continue; |
| } |
| |
| Log.d(TAG, "updateCallsDone call changed id:" + entry.getValue().getId()); |
| sendCallChangedIntent(entry.getValue()); |
| } else { |
| Log.d(TAG, "updateCallsDone new call id:" + entry.getValue().getId()); |
| sendCallChangedIntent(entry.getValue()); |
| } |
| } |
| |
| mCalls = mCallsUpdate; |
| mCallsUpdate = null; |
| |
| if (loopQueryCalls()) { |
| Log.d(TAG, "queryCallsDone ambigious calls, starting call query loop"); |
| sendMessageDelayed(QUERY_CURRENT_CALLS, 1523); |
| } |
| } |
| |
| private void queryCallsUpdate(int id, int state, String number, boolean multiParty, |
| boolean outgoing) { |
| Log.d(TAG, "queryCallsUpdate: " + id); |
| |
| // should not happen |
| if (mCallsUpdate == null) { |
| return; |
| } |
| |
| mCallsUpdate.put(id, new BluetoothHeadsetClientCall(mCurrentDevice, id, state, number, |
| multiParty, outgoing)); |
| } |
| |
| // helper function for determining if query calls should be looped |
| private boolean loopQueryCalls() { |
| if (callsInState(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) > 1) { |
| return true; |
| } |
| |
| // Workaround for Windows Phone 7.8 not sending callsetup=0 after |
| // rejecting incoming call in 3WC use case (when no active calls present). |
| // Fixes both, AG and HF rejecting the call. |
| BluetoothHeadsetClientCall c = getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING); |
| if (c != null && mIndicatorCallSetup == HeadsetClientHalConstants.CALLSETUP_NONE) |
| return true; |
| |
| return false; |
| } |
| |
| private void acceptCall(int flag, boolean retry) { |
| int action; |
| |
| Log.d(TAG, "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; |
| } |
| } |
| |
| switch (c.getState()) { |
| case BluetoothHeadsetClientCall.CALL_STATE_INCOMING: |
| if (flag != BluetoothHeadsetClient.CALL_ACCEPT_NONE) { |
| return; |
| } |
| |
| // Some NOKIA phones with Windows Phone 7.8 and MeeGo requires CHLD=1 |
| // for accepting incoming call if it is the only call present after |
| // second active remote has disconnected (3WC scenario - call state |
| // changes from waiting to incoming). On the other hand some Android |
| // phones and iPhone requires ATA. Try to handle those gently by |
| // first issuing ATA. Failing means that AG is probably one of those |
| // phones that requires CHLD=1. Handle this case when we are retrying. |
| // Accepting incoming calls when there is held one and |
| // no active should also be handled by ATA. |
| action = HeadsetClientHalConstants.CALL_ACTION_ATA; |
| |
| if (mCalls.size() == 1 && retry) { |
| action = HeadsetClientHalConstants.CALL_ACTION_CHLD_1; |
| } |
| 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; |
| } |
| |
| // Some phones (WP7) require ATA instead of CHLD=2 |
| // to accept waiting call if no active calls are present. |
| if (retry) { |
| action = HeadsetClientHalConstants.CALL_ACTION_ATA; |
| } else { |
| action = HeadsetClientHalConstants.CALL_ACTION_CHLD_2; |
| } |
| break; |
| } |
| |
| // if active calls are present action must be selected |
| 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 { |
| 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 { |
| action = HeadsetClientHalConstants.CALL_ACTION_CHLD_2; |
| } |
| break; |
| case BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD: |
| if (flag != BluetoothHeadsetClient.CALL_ACCEPT_NONE) { |
| return; |
| } |
| 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) { |
| // HFP is disabled when a call is put on hold to ensure correct audio routing for |
| // cellular calls accepted while an HFP call is in progress. Reenable HFP when the HFP |
| // call is put off hold. |
| Log.d(TAG,"hfp_enable=true"); |
| mAudioManager.setParameters("hfp_enable=true"); |
| } |
| |
| if (handleCallActionNative(action, 0)) { |
| addQueuedAction(ACCEPT_CALL, action); |
| } else { |
| Log.e(TAG, "ERROR: Couldn't accept a call, action:" + action); |
| } |
| } |
| |
| private void rejectCall() { |
| int action; |
| |
| Log.d(TAG, "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) { |
| 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 (handleCallActionNative(action, 0)) { |
| addQueuedAction(REJECT_CALL, action); |
| } else { |
| Log.e(TAG, "ERROR: Couldn't reject a call, action:" + action); |
| } |
| } |
| |
| private void holdCall() { |
| int action; |
| |
| Log.d(TAG, "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; |
| } |
| |
| // Set HFP enable to false in case the call is being held to accept a cellular call. This |
| // allows the cellular call's audio to be correctly routed. |
| Log.d(TAG,"hfp_enable=false"); |
| mAudioManager.setParameters("hfp_enable=false"); |
| |
| if (handleCallActionNative(action, 0)) { |
| addQueuedAction(HOLD_CALL, action); |
| } else { |
| Log.e(TAG, "ERROR: Couldn't hold a call, action:" + action); |
| } |
| } |
| |
| private void terminateCall(int idx) { |
| Log.d(TAG, "terminateCall: " + idx); |
| |
| if (idx == 0) { |
| int action = HeadsetClientHalConstants.CALL_ACTION_CHUP; |
| |
| BluetoothHeadsetClientCall c = getCall( |
| BluetoothHeadsetClientCall.CALL_STATE_DIALING, |
| BluetoothHeadsetClientCall.CALL_STATE_ALERTING); |
| if (c != null) { |
| if (handleCallActionNative(action, 0)) { |
| addQueuedAction(TERMINATE_CALL, action); |
| } else { |
| Log.e(TAG, "ERROR: Couldn't terminate outgoing call"); |
| } |
| } |
| |
| if (callsInState(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) > 0) { |
| if (handleCallActionNative(action, 0)) { |
| addQueuedAction(TERMINATE_CALL, action); |
| } else { |
| Log.e(TAG, "ERROR: Couldn't terminate active calls"); |
| } |
| } |
| } else { |
| int action; |
| BluetoothHeadsetClientCall c = mCalls.get(idx); |
| |
| if (c == null) { |
| return; |
| } |
| |
| switch (c.getState()) { |
| case BluetoothHeadsetClientCall.CALL_STATE_ACTIVE: |
| action = HeadsetClientHalConstants.CALL_ACTION_CHLD_1x; |
| break; |
| case BluetoothHeadsetClientCall.CALL_STATE_DIALING: |
| case BluetoothHeadsetClientCall.CALL_STATE_ALERTING: |
| action = HeadsetClientHalConstants.CALL_ACTION_CHUP; |
| break; |
| default: |
| return; |
| } |
| |
| if (handleCallActionNative(action, idx)) { |
| if (action == HeadsetClientHalConstants.CALL_ACTION_CHLD_1x) { |
| addQueuedAction(TERMINATE_SPECIFIC_CALL, c); |
| } else { |
| addQueuedAction(TERMINATE_CALL, action); |
| } |
| } else { |
| Log.e(TAG, "ERROR: Couldn't terminate a call, action:" + action + " id:" + idx); |
| } |
| } |
| } |
| |
| private void enterPrivateMode(int idx) { |
| Log.d(TAG, "enterPrivateMode: " + idx); |
| |
| BluetoothHeadsetClientCall c = mCalls.get(idx); |
| |
| if (c == null) { |
| return; |
| } |
| |
| if (c.getState() != BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) { |
| return; |
| } |
| |
| if (!c.isMultiParty()) { |
| return; |
| } |
| |
| if (handleCallActionNative(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() { |
| Log.d(TAG, "explicitCallTransfer"); |
| |
| // can't transfer call if there is not enough call parties |
| if (mCalls.size() < 2) { |
| return; |
| } |
| |
| if (handleCallActionNative(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_VTAG) == |
| HeadsetClientHalConstants.PEER_FEAT_VTAG) { |
| b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_ATTACH_NUMBER_TO_VT, 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; |
| } |
| |
| private HeadsetClientStateMachine(HeadsetClientService context) { |
| super(TAG); |
| mService = context; |
| |
| mAdapter = BluetoothAdapter.getDefaultAdapter(); |
| mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); |
| mAudioState = BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED; |
| mAudioWbs = false; |
| |
| mAudioRouteAllowed = context.getResources().getBoolean( |
| R.bool.headset_client_initial_audio_route_allowed); |
| |
| mTelecomManager = (TelecomManager) context.getSystemService(context.TELECOM_SERVICE); |
| |
| mIndicatorNetworkState = HeadsetClientHalConstants.NETWORK_STATE_NOT_AVAILABLE; |
| mIndicatorNetworkType = HeadsetClientHalConstants.SERVICE_TYPE_HOME; |
| mIndicatorNetworkSignal = 0; |
| mIndicatorBatteryLevel = 0; |
| |
| // all will be set on connected |
| mIndicatorCall = -1; |
| mIndicatorCallSetup = -1; |
| mIndicatorCallHeld = -1; |
| |
| mMaxAmVcVol = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL); |
| mMinAmVcVol = mAudioManager.getStreamMinVolume(AudioManager.STREAM_VOICE_CALL); |
| |
| mOperatorName = null; |
| mSubscriberInfo = null; |
| |
| mVoiceRecognitionActive = HeadsetClientHalConstants.VR_STATE_STOPPED; |
| mInBandRingtone = HeadsetClientHalConstants.IN_BAND_RING_NOT_PROVIDED; |
| |
| mQueuedActions = new LinkedList<Pair<Integer, Object>>(); |
| clearPendingAction(); |
| |
| mCalls = new Hashtable<Integer, BluetoothHeadsetClientCall>(); |
| mCallsUpdate = null; |
| mQueryCallsSupported = true; |
| |
| initializeNative(); |
| mNativeAvailable = true; |
| |
| 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) { |
| Log.d(TAG, "make"); |
| HeadsetClientStateMachine hfcsm = new HeadsetClientStateMachine(context); |
| hfcsm.start(); |
| return hfcsm; |
| } |
| |
| public void doQuit() { |
| quitNow(); |
| } |
| |
| public void cleanup() { |
| if (mNativeAvailable) { |
| cleanupNative(); |
| mNativeAvailable = false; |
| } |
| } |
| |
| private int hfToAmVol(int hfVol) { |
| int amRange = mMaxAmVcVol - mMinAmVcVol; |
| 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 = mMinAmVcVol + amOffset; |
| Log.d(TAG, "HF -> AM " + hfVol + " " + amVol); |
| return amVol; |
| } |
| |
| private int amToHfVol(int amVol) { |
| int amRange = mMaxAmVcVol - mMinAmVcVol; |
| int hfRange = MAX_HFP_SCO_VOICE_CALL_VOLUME - MIN_HFP_SCO_VOICE_CALL_VOLUME; |
| int hfOffset = (hfRange * (amVol - mMinAmVcVol)) / amRange; |
| int hfVol = MIN_HFP_SCO_VOICE_CALL_VOLUME + hfOffset; |
| Log.d(TAG, "AM -> HF " + amVol + " " + hfVol); |
| return hfVol; |
| } |
| |
| private class Disconnected extends State { |
| @Override |
| public void enter() { |
| Log.d(TAG, "Enter Disconnected: " + getCurrentMessage().what); |
| |
| // cleanup |
| mIndicatorNetworkState = HeadsetClientHalConstants.NETWORK_STATE_NOT_AVAILABLE; |
| mIndicatorNetworkType = HeadsetClientHalConstants.SERVICE_TYPE_HOME; |
| mIndicatorNetworkSignal = 0; |
| mIndicatorBatteryLevel = 0; |
| |
| mAudioWbs = false; |
| |
| // will be set on connect |
| mIndicatorCall = -1; |
| mIndicatorCallSetup = -1; |
| mIndicatorCallHeld = -1; |
| |
| mOperatorName = null; |
| mSubscriberInfo = null; |
| |
| mQueuedActions = new LinkedList<Pair<Integer, Object>>(); |
| clearPendingAction(); |
| |
| mVoiceRecognitionActive = HeadsetClientHalConstants.VR_STATE_STOPPED; |
| mInBandRingtone = HeadsetClientHalConstants.IN_BAND_RING_NOT_PROVIDED; |
| |
| mCalls = new Hashtable<Integer, BluetoothHeadsetClientCall>(); |
| mCallsUpdate = null; |
| mQueryCallsSupported = true; |
| |
| mPeerFeatures = 0; |
| mChldFeatures = 0; |
| |
| removeMessages(QUERY_CURRENT_CALLS); |
| } |
| |
| @Override |
| public synchronized boolean processMessage(Message message) { |
| Log.d(TAG, "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; |
| |
| broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING, |
| BluetoothProfile.STATE_DISCONNECTED); |
| |
| if (!connectNative(getByteAddress(device))) { |
| broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, |
| BluetoothProfile.STATE_CONNECTING); |
| break; |
| } |
| |
| mCurrentDevice = device; |
| transitionTo(mConnecting); |
| break; |
| case DISCONNECT: |
| // ignore |
| break; |
| case STACK_EVENT: |
| StackEvent event = (StackEvent) message.obj; |
| if (DBG) { |
| Log.d(TAG, "Stack event type: " + event.type); |
| } |
| switch (event.type) { |
| case EVENT_TYPE_CONNECTION_STATE_CHANGED: |
| Log.d(TAG, "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"); |
| broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING, |
| BluetoothProfile.STATE_DISCONNECTED); |
| mCurrentDevice = device; |
| transitionTo(mConnecting); |
| } else { |
| Log.i(TAG, "Incoming AG rejected. priority=" + mService.getPriority(device) |
| + |
| " bondState=" + device.getBondState()); |
| // reject the connection and stay in Disconnected state |
| // itself |
| disconnectNative(getByteAddress(device)); |
| // the other profile connection should be initiated |
| AdapterService adapterService = AdapterService.getAdapterService(); |
| if (adapterService != null) { |
| adapterService.connectOtherProfile(device, |
| AdapterService.PROFILE_CONN_REJECTED); |
| } |
| } |
| 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() { |
| Log.d(TAG, "Exit Disconnected: " + getCurrentMessage().what); |
| } |
| } |
| |
| private class Connecting extends State { |
| @Override |
| public void enter() { |
| Log.d(TAG, "Enter Connecting: " + getCurrentMessage().what); |
| } |
| |
| @Override |
| public synchronized boolean processMessage(Message message) { |
| Log.d(TAG, "Connecting process message: " + message.what); |
| |
| boolean retValue = HANDLED; |
| switch (message.what) { |
| case CONNECT: |
| case CONNECT_AUDIO: |
| case DISCONNECT: |
| deferMessage(message); |
| break; |
| case STACK_EVENT: |
| StackEvent event = (StackEvent) message.obj; |
| if (DBG) { |
| Log.d(TAG, "Connecting: event type: " + event.type); |
| } |
| switch (event.type) { |
| case EVENT_TYPE_CONNECTION_STATE_CHANGED: |
| Log.d(TAG, "Connecting: Connection " + event.device + " state changed:" |
| + event.valueInt); |
| processConnectionEvent(event.valueInt, event.valueInt2, |
| event.valueInt3, event.device); |
| break; |
| case EVENT_TYPE_AUDIO_STATE_CHANGED: |
| case EVENT_TYPE_VR_STATE_CHANGED: |
| case EVENT_TYPE_NETWORK_STATE: |
| case EVENT_TYPE_ROAMING_STATE: |
| case EVENT_TYPE_NETWORK_SIGNAL: |
| case EVENT_TYPE_BATTERY_LEVEL: |
| case EVENT_TYPE_CALL: |
| case EVENT_TYPE_CALLSETUP: |
| case EVENT_TYPE_CALLHELD: |
| case EVENT_TYPE_RESP_AND_HOLD: |
| case EVENT_TYPE_CLIP: |
| case EVENT_TYPE_CALL_WAITING: |
| case EVENT_TYPE_VOLUME_CHANGED: |
| case EVENT_TYPE_IN_BAND_RING: |
| deferMessage(message); |
| break; |
| case EVENT_TYPE_CMD_RESULT: |
| case EVENT_TYPE_SUBSCRIBER_INFO: |
| case EVENT_TYPE_CURRENT_CALLS: |
| case EVENT_TYPE_OPERATOR_NAME: |
| default: |
| Log.e(TAG, "Connecting: ignoring stack event: " + event.type); |
| break; |
| } |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return retValue; |
| } |
| |
| // in Connecting state |
| private void processConnectionEvent(int state, int peer_feat, int chld_feat, BluetoothDevice device) { |
| switch (state) { |
| case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED: |
| broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED, |
| BluetoothProfile.STATE_CONNECTING); |
| mCurrentDevice = null; |
| transitionTo(mDisconnected); |
| break; |
| case HeadsetClientHalConstants.CONNECTION_STATE_SLC_CONNECTED: |
| Log.w(TAG, "HFPClient Connected from Connecting state"); |
| |
| mPeerFeatures = peer_feat; |
| mChldFeatures = chld_feat; |
| |
| broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_CONNECTING); |
| // Send AT+NREC to remote if supported by audio |
| if (HeadsetClientHalConstants.HANDSFREECLIENT_NREC_SUPPORTED && |
| ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_ECNR) == |
| HeadsetClientHalConstants.PEER_FEAT_ECNR)) { |
| if (sendATCmdNative(HeadsetClientHalConstants.HANDSFREECLIENT_AT_CMD_NREC, |
| 1 , 0, null)) { |
| addQueuedAction(DISABLE_NREC); |
| } else { |
| Log.e(TAG, "Failed to send NREC"); |
| } |
| } |
| transitionTo(mConnected); |
| |
| int amVol = mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL); |
| sendMessage( |
| 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. |
| sendMessage(obtainMessage(HeadsetClientStateMachine.SET_MIC_VOLUME, |
| mAudioManager.isMicrophoneMute() ? 0 : 15, 0)); |
| |
| // query subscriber info |
| sendMessage(HeadsetClientStateMachine.SUBSCRIBER_INFO); |
| break; |
| case HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED: |
| if (!mCurrentDevice.equals(device)) { |
| Log.w(TAG, "incoming connection event, device: " + device); |
| |
| 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 */ |
| Log.d(TAG, "outgoing connection started, ignore"); |
| break; |
| case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTING: |
| default: |
| Log.e(TAG, "Incorrect state: " + state); |
| break; |
| } |
| } |
| |
| @Override |
| public void exit() { |
| Log.d(TAG, "Exit Connecting: " + getCurrentMessage().what); |
| } |
| } |
| |
| private class Connected extends State { |
| @Override |
| public void enter() { |
| Log.d(TAG, "Enter Connected: " + getCurrentMessage().what); |
| |
| mAudioWbs = false; |
| } |
| |
| @Override |
| public synchronized boolean processMessage(Message message) { |
| Log.d(TAG, "Connected process message: " + message.what); |
| if (DBG) { |
| if (mCurrentDevice == null) { |
| Log.d(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; |
| } |
| |
| if (!disconnectNative(getByteAddress(mCurrentDevice))) { |
| // if succeed this will be handled from disconnected |
| // state |
| broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING, |
| BluetoothProfile.STATE_DISCONNECTED); |
| broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, |
| BluetoothProfile.STATE_CONNECTING); |
| break; |
| } |
| |
| // will be handled when entered disconnected |
| deferMessage(message); |
| break; |
| case DISCONNECT: |
| BluetoothDevice dev = (BluetoothDevice) message.obj; |
| if (!mCurrentDevice.equals(dev)) { |
| break; |
| } |
| broadcastConnectionState(dev, BluetoothProfile.STATE_DISCONNECTING, |
| BluetoothProfile.STATE_CONNECTED); |
| if (!disconnectNative(getByteAddress(dev))) { |
| // disconnecting failed |
| broadcastConnectionState(dev, BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_DISCONNECTED); |
| break; |
| } |
| break; |
| case CONNECT_AUDIO: |
| // TODO: handle audio connection failure |
| if (!connectAudioNative(getByteAddress(mCurrentDevice))) { |
| Log.e(TAG, "ERROR: Couldn't connect Audio."); |
| } |
| break; |
| case DISCONNECT_AUDIO: |
| // TODO: handle audio disconnection failure |
| if (!disconnectAudioNative(getByteAddress(mCurrentDevice))) { |
| Log.e(TAG, "ERROR: Couldn't connect Audio."); |
| } |
| break; |
| case VOICE_RECOGNITION_START: |
| if (mVoiceRecognitionActive == HeadsetClientHalConstants.VR_STATE_STOPPED) { |
| if (startVoiceRecognitionNative()) { |
| 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 (stopVoiceRecognitionNative()) { |
| addQueuedAction(VOICE_RECOGNITION_STOP); |
| } else { |
| Log.e(TAG, "ERROR: Couldn't stop voice recognition"); |
| } |
| } |
| break; |
| // Called only for Mute/Un-mute - Mic volume change is not allowed. |
| case SET_MIC_VOLUME: |
| if (mVgmFromStack) { |
| mVgmFromStack = false; |
| break; |
| } |
| if (setVolumeNative(HeadsetClientHalConstants.VOLUME_TYPE_MIC, message.arg1)) { |
| addQueuedAction(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); |
| Log.d(TAG,"HF volume is set to " + hfVol); |
| mAudioManager.setParameters("hfp_volume=" + hfVol); |
| if (mVgsFromStack) { |
| mVgsFromStack = false; |
| break; |
| } |
| if (setVolumeNative(HeadsetClientHalConstants.VOLUME_TYPE_SPK, hfVol)) { |
| addQueuedAction(SET_SPEAKER_VOLUME); |
| } |
| break; |
| case REDIAL: |
| if (dialNative(null)) { |
| addQueuedAction(REDIAL); |
| } else { |
| Log.e(TAG, "ERROR: Cannot redial"); |
| } |
| break; |
| case DIAL_NUMBER: |
| if (dialNative((String) message.obj)) { |
| addQueuedAction(DIAL_NUMBER, message.obj); |
| } else { |
| Log.e(TAG, "ERROR: Cannot dial with a given number:" + (String) message.obj); |
| } |
| break; |
| case DIAL_MEMORY: |
| if (dialMemoryNative(message.arg1)) { |
| addQueuedAction(DIAL_MEMORY); |
| } else { |
| Log.e(TAG, "ERROR: Cannot dial with a given location:" + message.arg1); |
| } |
| break; |
| case ACCEPT_CALL: |
| acceptCall(message.arg1, false); |
| break; |
| case REJECT_CALL: |
| rejectCall(); |
| break; |
| case HOLD_CALL: |
| holdCall(); |
| break; |
| case TERMINATE_CALL: |
| terminateCall(message.arg1); |
| break; |
| case ENTER_PRIVATE_MODE: |
| enterPrivateMode(message.arg1); |
| break; |
| case EXPLICIT_CALL_TRANSFER: |
| explicitCallTransfer(); |
| break; |
| case SEND_DTMF: |
| if (sendDtmfNative((byte) message.arg1)) { |
| addQueuedAction(SEND_DTMF); |
| } else { |
| Log.e(TAG, "ERROR: Couldn't send DTMF"); |
| } |
| break; |
| case SUBSCRIBER_INFO: |
| if (retrieveSubscriberInfoNative()) { |
| addQueuedAction(SUBSCRIBER_INFO); |
| } else { |
| Log.e(TAG, "ERROR: Couldn't retrieve subscriber info"); |
| } |
| break; |
| case LAST_VTAG_NUMBER: |
| if (requestLastVoiceTagNumberNative()) { |
| addQueuedAction(LAST_VTAG_NUMBER); |
| } else { |
| Log.e(TAG, "ERROR: Couldn't get last VTAG number"); |
| } |
| break; |
| case QUERY_CURRENT_CALLS: |
| queryCallsStart(); |
| break; |
| case STACK_EVENT: |
| Intent intent = null; |
| StackEvent event = (StackEvent) message.obj; |
| if (DBG) { |
| Log.d(TAG, "Connected: event type: " + event.type); |
| } |
| |
| switch (event.type) { |
| case EVENT_TYPE_CONNECTION_STATE_CHANGED: |
| Log.d(TAG, "Connected: Connection state changed: " + event.device |
| + ": " + event.valueInt); |
| processConnectionEvent(event.valueInt, event.device); |
| break; |
| case EVENT_TYPE_AUDIO_STATE_CHANGED: |
| Log.d(TAG, "Connected: Audio state changed: " + event.device + ": " |
| + event.valueInt); |
| processAudioEvent(event.valueInt, event.device); |
| break; |
| case EVENT_TYPE_NETWORK_STATE: |
| Log.d(TAG, "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 (queryCurrentOperatorNameNative()) { |
| addQueuedAction(QUERY_OPERATOR_NAME); |
| } else { |
| Log.e(TAG, "ERROR: Couldn't querry operator name"); |
| } |
| } |
| break; |
| case EVENT_TYPE_ROAMING_STATE: |
| Log.d(TAG, "Connected: Roaming state: " + event.valueInt); |
| |
| 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 EVENT_TYPE_NETWORK_SIGNAL: |
| Log.d(TAG, "Connected: Signal level: " + event.valueInt); |
| |
| 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 EVENT_TYPE_BATTERY_LEVEL: |
| Log.d(TAG, "Connected: Battery level: " + event.valueInt); |
| |
| 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 EVENT_TYPE_OPERATOR_NAME: |
| Log.d(TAG, "Connected: Operator name: " + event.valueString); |
| |
| 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 EVENT_TYPE_VR_STATE_CHANGED: |
| Log.d(TAG, "Connected: Voice recognition state: " + event.valueInt); |
| |
| if (mVoiceRecognitionActive != event.valueInt) { |
| mVoiceRecognitionActive = event.valueInt; |
| |
| intent = new Intent(BluetoothHeadsetClient.ACTION_AG_EVENT); |
| intent.putExtra(BluetoothHeadsetClient.EXTRA_VOICE_RECOGNITION, |
| mVoiceRecognitionActive); |
| intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device); |
| mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); |
| } |
| break; |
| case EVENT_TYPE_CALL: |
| updateCallIndicator(event.valueInt); |
| break; |
| case EVENT_TYPE_CALLSETUP: |
| updateCallSetupIndicator(event.valueInt); |
| break; |
| case EVENT_TYPE_CALLHELD: |
| updateCallHeldIndicator(event.valueInt); |
| break; |
| case EVENT_TYPE_RESP_AND_HOLD: |
| updateRespAndHold(event.valueInt); |
| break; |
| case EVENT_TYPE_CLIP: |
| updateClip(event.valueString); |
| break; |
| case EVENT_TYPE_CALL_WAITING: |
| addCallWaiting(event.valueString); |
| break; |
| case EVENT_TYPE_IN_BAND_RING: |
| if (mInBandRingtone != event.valueInt) { |
| mInBandRingtone = event.valueInt; |
| intent = new Intent(BluetoothHeadsetClient.ACTION_AG_EVENT); |
| intent.putExtra(BluetoothHeadsetClient.EXTRA_IN_BAND_RING, |
| mInBandRingtone); |
| intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device); |
| mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); |
| } |
| break; |
| case 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 EVENT_TYPE_VOLUME_CHANGED: |
| if (event.valueInt == HeadsetClientHalConstants.VOLUME_TYPE_SPK) { |
| Log.d(TAG, "AM volume set to " + |
| hfToAmVol(event.valueInt2)); |
| mAudioManager.setStreamVolume( |
| AudioManager.STREAM_VOICE_CALL, |
| hfToAmVol(event.valueInt2), |
| AudioManager.FLAG_SHOW_UI); |
| mVgsFromStack = true; |
| } else if (event.valueInt == |
| HeadsetClientHalConstants.VOLUME_TYPE_MIC) { |
| mAudioManager.setMicrophoneMute(event.valueInt2 == 0); |
| |
| mVgmFromStack = true; |
| } |
| break; |
| case EVENT_TYPE_CMD_RESULT: |
| Pair<Integer, Object> queuedAction = mQueuedActions.poll(); |
| |
| // should not happen but... |
| if (queuedAction == null || queuedAction.first == NO_ACTION) { |
| clearPendingAction(); |
| break; |
| } |
| |
| Log.d(TAG, "Connected: command result: " + event.valueInt |
| + " queuedAction: " + queuedAction.first); |
| |
| switch (queuedAction.first) { |
| case VOICE_RECOGNITION_STOP: |
| case VOICE_RECOGNITION_START: |
| if (event.valueInt == HeadsetClientHalConstants.CMD_COMPLETE_OK) { |
| if (queuedAction.first == VOICE_RECOGNITION_STOP) { |
| mVoiceRecognitionActive = |
| HeadsetClientHalConstants.VR_STATE_STOPPED; |
| } else { |
| mVoiceRecognitionActive = |
| HeadsetClientHalConstants.VR_STATE_STARTED; |
| } |
| } |
| intent = new Intent(BluetoothHeadsetClient.ACTION_AG_EVENT); |
| intent.putExtra( |
| BluetoothHeadsetClient.EXTRA_VOICE_RECOGNITION, |
| mVoiceRecognitionActive); |
| intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device); |
| mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); |
| break; |
| case QUERY_CURRENT_CALLS: |
| queryCallsDone(); |
| break; |
| case ACCEPT_CALL: |
| if (event.valueInt == BluetoothHeadsetClient.ACTION_RESULT_OK) { |
| mPendingAction = queuedAction; |
| } else { |
| if (callsInState(BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) == 0) { |
| if(getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING) != null && |
| (Integer) mPendingAction.second == HeadsetClientHalConstants.CALL_ACTION_ATA) { |
| acceptCall(BluetoothHeadsetClient.CALL_ACCEPT_NONE, true); |
| break; |
| } else if(getCall(BluetoothHeadsetClientCall.CALL_STATE_WAITING) != null && |
| (Integer) mPendingAction.second == HeadsetClientHalConstants.CALL_ACTION_CHLD_2) { |
| acceptCall(BluetoothHeadsetClient.CALL_ACCEPT_NONE, true); |
| break; |
| } |
| } |
| sendActionResultIntent(event); |
| } |
| break; |
| case REJECT_CALL: |
| case HOLD_CALL: |
| case TERMINATE_CALL: |
| case ENTER_PRIVATE_MODE: |
| case DIAL_NUMBER: |
| case DIAL_MEMORY: |
| case REDIAL: |
| if (event.valueInt == BluetoothHeadsetClient.ACTION_RESULT_OK) { |
| mPendingAction = queuedAction; |
| } else { |
| sendActionResultIntent(event); |
| } |
| break; |
| case TERMINATE_SPECIFIC_CALL: |
| // if terminating specific succeed no other |
| // event is send |
| if (event.valueInt == BluetoothHeadsetClient.ACTION_RESULT_OK) { |
| BluetoothHeadsetClientCall c = |
| (BluetoothHeadsetClientCall) queuedAction.second; |
| setCallState(c, |
| BluetoothHeadsetClientCall.CALL_STATE_TERMINATED); |
| mCalls.remove(c.getId()); |
| } else { |
| sendActionResultIntent(event); |
| } |
| break; |
| case LAST_VTAG_NUMBER: |
| if (event.valueInt != BluetoothHeadsetClient.ACTION_RESULT_OK) { |
| sendActionResultIntent(event); |
| } |
| break; |
| case DISABLE_NREC: |
| if (event.valueInt != HeadsetClientHalConstants.CMD_COMPLETE_OK) { |
| Log.w(TAG, "Failed to disable AG's EC and NR"); |
| } |
| break; |
| case SET_MIC_VOLUME: |
| case SET_SPEAKER_VOLUME: |
| case SUBSCRIBER_INFO: |
| case QUERY_OPERATOR_NAME: |
| break; |
| default: |
| sendActionResultIntent(event); |
| break; |
| } |
| |
| break; |
| case EVENT_TYPE_SUBSCRIBER_INFO: |
| /* TODO should we handle type as well? */ |
| 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 EVENT_TYPE_LAST_VOICE_TAG_NUMBER: |
| intent = new Intent(BluetoothHeadsetClient.ACTION_LAST_VTAG); |
| intent.putExtra(BluetoothHeadsetClient.EXTRA_NUMBER, |
| event.valueString); |
| intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device); |
| mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); |
| break; |
| case 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; |
| default: |
| Log.e(TAG, "Unknown stack event: " + event.type); |
| break; |
| } |
| |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return HANDLED; |
| } |
| |
| private void sendActionResultIntent(StackEvent event) { |
| Intent intent = new Intent(BluetoothHeadsetClient.ACTION_RESULT); |
| intent.putExtra(BluetoothHeadsetClient.EXTRA_RESULT_CODE, event.valueInt); |
| if (event.valueInt == BluetoothHeadsetClient.ACTION_RESULT_ERROR_CME) { |
| intent.putExtra(BluetoothHeadsetClient.EXTRA_CME_CODE, event.valueInt2); |
| } |
| intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device); |
| mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); |
| } |
| |
| // in Connected state |
| private void processConnectionEvent(int state, BluetoothDevice device) { |
| switch (state) { |
| case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED: |
| Log.d(TAG, "Connected disconnects."); |
| // AG disconnects |
| if (mCurrentDevice.equals(device)) { |
| broadcastConnectionState(mCurrentDevice, |
| BluetoothProfile.STATE_DISCONNECTED, |
| BluetoothProfile.STATE_CONNECTED); |
| mCurrentDevice = null; |
| 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: |
| if (!mAudioRouteAllowed) { |
| sendMessage(HeadsetClientStateMachine.DISCONNECT_AUDIO); |
| break; |
| } |
| |
| // 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. |
| 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); |
| |
| Log.d(TAG,"hfp_enable=true"); |
| Log.d(TAG,"mAudioWbs is " + mAudioWbs); |
| if (mAudioWbs) { |
| Log.d(TAG,"Setting sampling rate as 16000"); |
| mAudioManager.setParameters("hfp_set_sampling_rate=16000"); |
| } |
| else { |
| Log.d(TAG,"Setting sampling rate as 8000"); |
| mAudioManager.setParameters("hfp_set_sampling_rate=8000"); |
| } |
| Log.d(TAG, "hf_volume " + hfVol); |
| mAudioManager.setParameters("hfp_enable=true"); |
| mAudioManager.setParameters("hfp_volume=" + hfVol); |
| transitionTo(mAudioOn); |
| break; |
| case HeadsetClientHalConstants.AUDIO_STATE_CONNECTING: |
| mAudioState = BluetoothHeadsetClient.STATE_AUDIO_CONNECTING; |
| broadcastAudioState(device, BluetoothHeadsetClient.STATE_AUDIO_CONNECTING, |
| BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED); |
| break; |
| case HeadsetClientHalConstants.AUDIO_STATE_DISCONNECTED: |
| if (mAudioState == BluetoothHeadsetClient.STATE_AUDIO_CONNECTING) { |
| mAudioState = BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED; |
| broadcastAudioState(device, |
| BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED, |
| BluetoothHeadsetClient.STATE_AUDIO_CONNECTING); |
| } |
| break; |
| default: |
| Log.e(TAG, "Audio State Device: " + device + " bad state: " + state); |
| break; |
| } |
| } |
| |
| @Override |
| public void exit() { |
| Log.d(TAG, "Exit Connected: " + getCurrentMessage().what); |
| } |
| } |
| |
| private class AudioOn extends State { |
| @Override |
| public void enter() { |
| Log.d(TAG, "Enter AudioOn: " + getCurrentMessage().what); |
| broadcastAudioState(mCurrentDevice, BluetoothHeadsetClient.STATE_AUDIO_CONNECTED, |
| BluetoothHeadsetClient.STATE_AUDIO_CONNECTING); |
| } |
| |
| @Override |
| public synchronized boolean processMessage(Message message) { |
| Log.d(TAG, "AudioOn process message: " + message.what); |
| if (DBG) { |
| if (mCurrentDevice == null) { |
| Log.d(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 |
| * EVENT_TYPE_AUDIO_STATE_CHANGED, that triggers State |
| * Machines state changing |
| */ |
| if (disconnectAudioNative(getByteAddress(mCurrentDevice))) { |
| mAudioState = BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED; |
| Log.d(TAG,"hfp_enable=false"); |
| mAudioManager.setParameters("hfp_enable=false"); |
| broadcastAudioState(mCurrentDevice, |
| BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED, |
| BluetoothHeadsetClient.STATE_AUDIO_CONNECTED); |
| } |
| break; |
| case STACK_EVENT: |
| StackEvent event = (StackEvent) message.obj; |
| if (DBG) { |
| Log.d(TAG, "AudioOn: event type: " + event.type); |
| } |
| switch (event.type) { |
| case EVENT_TYPE_CONNECTION_STATE_CHANGED: |
| Log.d(TAG, "AudioOn connection state changed" + event.device + ": " |
| + event.valueInt); |
| processConnectionEvent(event.valueInt, event.device); |
| break; |
| case EVENT_TYPE_AUDIO_STATE_CHANGED: |
| Log.d(TAG, "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); |
| broadcastConnectionState(mCurrentDevice, |
| BluetoothProfile.STATE_DISCONNECTED, |
| BluetoothProfile.STATE_CONNECTED); |
| mCurrentDevice = null; |
| 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: |
| if (mAudioState != BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED) { |
| 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. |
| Log.d(TAG,"hfp_enable=false"); |
| mAudioManager.setParameters("hfp_enable=false"); |
| broadcastAudioState(device, |
| BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED, |
| BluetoothHeadsetClient.STATE_AUDIO_CONNECTED); |
| } |
| |
| transitionTo(mConnected); |
| break; |
| default: |
| Log.e(TAG, "Audio State Device: " + device + " bad state: " + state); |
| break; |
| } |
| } |
| |
| @Override |
| public void exit() { |
| Log.d(TAG, "Exit AudioOn: " + getCurrentMessage().what); |
| } |
| } |
| |
| /** |
| * @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) { |
| 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); |
| Log.d(TAG, "Audio state " + device + ": " + prevState + "->" + newState); |
| } |
| |
| // This method does not check for error condition (newState == prevState) |
| private void broadcastConnectionState(BluetoothDevice device, int newState, int prevState) { |
| Log.d(TAG, "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. |
| */ |
| mService.notifyProfileConnectionStateChanged(device, BluetoothProfile.HEADSET_CLIENT, |
| newState, prevState); |
| 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_VTAG) == |
| HeadsetClientHalConstants.PEER_FEAT_VTAG) { |
| intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_ATTACH_NUMBER_TO_VT, 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 (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.Handsfree_AG)) { |
| continue; |
| } |
| connectionState = getConnectionState(device); |
| for (int state : states) { |
| if (connectionState == state) { |
| deviceList.add(device); |
| } |
| } |
| } |
| } |
| return deviceList; |
| } |
| |
| boolean okToConnect(BluetoothDevice device) { |
| int priority = mService.getPriority(device); |
| boolean ret = false; |
| // check priority and accept or reject the connection. if priority 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.PRIORITY_OFF < priority) || |
| ((BluetoothProfile.PRIORITY_UNDEFINED == priority) && |
| (device.getBondState() != BluetoothDevice.BOND_NONE))) { |
| ret = true; |
| } |
| return ret; |
| } |
| |
| boolean isAudioOn() { |
| return (getCurrentState() == mAudioOn); |
| } |
| |
| public void setAudioRouteAllowed(boolean allowed) { |
| mAudioRouteAllowed = allowed; |
| } |
| |
| public boolean getAudioRouteAllowed() { |
| return mAudioRouteAllowed; |
| } |
| |
| synchronized int getAudioState(BluetoothDevice device) { |
| if (mCurrentDevice == null || !mCurrentDevice.equals(device)) { |
| return BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED; |
| } |
| return mAudioState; |
| } |
| |
| /** |
| * @hide |
| */ |
| List<BluetoothDevice> getConnectedDevices() { |
| List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>(); |
| synchronized (this) { |
| if (isConnected()) { |
| devices.add(mCurrentDevice); |
| } |
| } |
| return devices; |
| } |
| |
| private BluetoothDevice getDevice(byte[] address) { |
| return mAdapter.getRemoteDevice(Utils.getAddressStringFromByte(address)); |
| } |
| |
| private void onConnectionStateChanged(int state, int peer_feat, int chld_feat, byte[] address) { |
| StackEvent event = new StackEvent(EVENT_TYPE_CONNECTION_STATE_CHANGED); |
| event.valueInt = state; |
| event.valueInt2 = peer_feat; |
| event.valueInt3 = chld_feat; |
| event.device = getDevice(address); |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onAudioStateChanged(int state, byte[] address) { |
| StackEvent event = new StackEvent(EVENT_TYPE_AUDIO_STATE_CHANGED); |
| event.valueInt = state; |
| event.device = getDevice(address); |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onVrStateChanged(int state) { |
| StackEvent event = new StackEvent(EVENT_TYPE_VR_STATE_CHANGED); |
| event.valueInt = state; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onNetworkState(int state) { |
| StackEvent event = new StackEvent(EVENT_TYPE_NETWORK_STATE); |
| event.valueInt = state; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onNetworkRoaming(int state) { |
| StackEvent event = new StackEvent(EVENT_TYPE_ROAMING_STATE); |
| event.valueInt = state; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onNetworkSignal(int signal) { |
| StackEvent event = new StackEvent(EVENT_TYPE_NETWORK_SIGNAL); |
| event.valueInt = signal; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onBatteryLevel(int level) { |
| StackEvent event = new StackEvent(EVENT_TYPE_BATTERY_LEVEL); |
| event.valueInt = level; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onCurrentOperator(String name) { |
| StackEvent event = new StackEvent(EVENT_TYPE_OPERATOR_NAME); |
| event.valueString = name; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onCall(int call) { |
| StackEvent event = new StackEvent(EVENT_TYPE_CALL); |
| event.valueInt = call; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onCallSetup(int callsetup) { |
| StackEvent event = new StackEvent(EVENT_TYPE_CALLSETUP); |
| event.valueInt = callsetup; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onCallHeld(int callheld) { |
| StackEvent event = new StackEvent(EVENT_TYPE_CALLHELD); |
| event.valueInt = callheld; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onRespAndHold(int resp_and_hold) { |
| StackEvent event = new StackEvent(EVENT_TYPE_RESP_AND_HOLD); |
| event.valueInt = resp_and_hold; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onClip(String number) { |
| StackEvent event = new StackEvent(EVENT_TYPE_CLIP); |
| event.valueString = number; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onCallWaiting(String number) { |
| StackEvent event = new StackEvent(EVENT_TYPE_CALL_WAITING); |
| event.valueString = number; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onCurrentCalls(int index, int dir, int state, int mparty, String number) { |
| StackEvent event = new StackEvent(EVENT_TYPE_CURRENT_CALLS); |
| event.valueInt = index; |
| event.valueInt2 = dir; |
| event.valueInt3 = state; |
| event.valueInt4 = mparty; |
| event.valueString = number; |
| Log.d(TAG, "incoming " + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onVolumeChange(int type, int volume) { |
| StackEvent event = new StackEvent(EVENT_TYPE_VOLUME_CHANGED); |
| event.valueInt = type; |
| event.valueInt2 = volume; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onCmdResult(int type, int cme) { |
| StackEvent event = new StackEvent(EVENT_TYPE_CMD_RESULT); |
| event.valueInt = type; |
| event.valueInt2 = cme; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onSubscriberInfo(String number, int type) { |
| StackEvent event = new StackEvent(EVENT_TYPE_SUBSCRIBER_INFO); |
| event.valueInt = type; |
| event.valueString = number; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onInBandRing(int in_band) { |
| StackEvent event = new StackEvent(EVENT_TYPE_IN_BAND_RING); |
| event.valueInt = in_band; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private void onLastVoiceTagNumber(String number) { |
| StackEvent event = new StackEvent(EVENT_TYPE_LAST_VOICE_TAG_NUMBER); |
| event.valueString = number; |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| private void onRingIndication() { |
| StackEvent event = new StackEvent(EVENT_TYPE_RING_INDICATION); |
| Log.d(TAG, "incoming" + event); |
| sendMessage(STACK_EVENT, event); |
| } |
| |
| private String getCurrentDeviceName() { |
| String defaultName = "<unknown>"; |
| if (mCurrentDevice == null) { |
| return defaultName; |
| } |
| String deviceName = mCurrentDevice.getName(); |
| if (deviceName == null) { |
| return defaultName; |
| } |
| return deviceName; |
| } |
| |
| private byte[] getByteAddress(BluetoothDevice device) { |
| return Utils.getBytesFromAddress(device.getAddress()); |
| } |
| |
| // Event types for STACK_EVENT message |
| final private static int EVENT_TYPE_NONE = 0; |
| final private static int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1; |
| final private static int EVENT_TYPE_AUDIO_STATE_CHANGED = 2; |
| final private static int EVENT_TYPE_VR_STATE_CHANGED = 3; |
| final private static int EVENT_TYPE_NETWORK_STATE = 4; |
| final private static int EVENT_TYPE_ROAMING_STATE = 5; |
| final private static int EVENT_TYPE_NETWORK_SIGNAL = 6; |
| final private static int EVENT_TYPE_BATTERY_LEVEL = 7; |
| final private static int EVENT_TYPE_OPERATOR_NAME = 8; |
| final private static int EVENT_TYPE_CALL = 9; |
| final private static int EVENT_TYPE_CALLSETUP = 10; |
| final private static int EVENT_TYPE_CALLHELD = 11; |
| final private static int EVENT_TYPE_CLIP = 12; |
| final private static int EVENT_TYPE_CALL_WAITING = 13; |
| final private static int EVENT_TYPE_CURRENT_CALLS = 14; |
| final private static int EVENT_TYPE_VOLUME_CHANGED = 15; |
| final private static int EVENT_TYPE_CMD_RESULT = 16; |
| final private static int EVENT_TYPE_SUBSCRIBER_INFO = 17; |
| final private static int EVENT_TYPE_RESP_AND_HOLD = 18; |
| final private static int EVENT_TYPE_IN_BAND_RING = 19; |
| final private static int EVENT_TYPE_LAST_VOICE_TAG_NUMBER = 20; |
| final private static int EVENT_TYPE_RING_INDICATION= 21; |
| |
| // for debugging only |
| private final String EVENT_TYPE_NAMES[] = |
| { |
| "EVENT_TYPE_NONE", |
| "EVENT_TYPE_CONNECTION_STATE_CHANGED", |
| "EVENT_TYPE_AUDIO_STATE_CHANGED", |
| "EVENT_TYPE_VR_STATE_CHANGED", |
| "EVENT_TYPE_NETWORK_STATE", |
| "EVENT_TYPE_ROAMING_STATE", |
| "EVENT_TYPE_NETWORK_SIGNAL", |
| "EVENT_TYPE_BATTERY_LEVEL", |
| "EVENT_TYPE_OPERATOR_NAME", |
| "EVENT_TYPE_CALL", |
| "EVENT_TYPE_CALLSETUP", |
| "EVENT_TYPE_CALLHELD", |
| "EVENT_TYPE_CLIP", |
| "EVENT_TYPE_CALL_WAITING", |
| "EVENT_TYPE_CURRENT_CALLS", |
| "EVENT_TYPE_VOLUME_CHANGED", |
| "EVENT_TYPE_CMD_RESULT", |
| "EVENT_TYPE_SUBSCRIBER_INFO", |
| "EVENT_TYPE_RESP_AND_HOLD", |
| "EVENT_TYPE_IN_BAND_RING", |
| "EVENT_TYPE_LAST_VOICE_TAG_NUMBER", |
| "EVENT_TYPE_RING_INDICATION", |
| }; |
| |
| private class StackEvent { |
| int type = EVENT_TYPE_NONE; |
| int valueInt = 0; |
| int valueInt2 = 0; |
| int valueInt3 = 0; |
| int valueInt4 = 0; |
| String valueString = null; |
| BluetoothDevice device = null; |
| |
| private StackEvent(int type) { |
| this.type = type; |
| } |
| |
| @Override |
| public String toString() { |
| // event dump |
| StringBuilder result = new StringBuilder(); |
| result.append("StackEvent {type:" + EVENT_TYPE_NAMES[type]); |
| result.append(", value1:" + valueInt); |
| result.append(", value2:" + valueInt2); |
| result.append(", value3:" + valueInt3); |
| result.append(", value4:" + valueInt4); |
| result.append(", string: \"" + valueString + "\""); |
| result.append(", device:" + device + "}"); |
| return result.toString(); |
| } |
| } |
| |
| private native static void classInitNative(); |
| |
| private native void initializeNative(); |
| |
| private native void cleanupNative(); |
| |
| private native boolean connectNative(byte[] address); |
| |
| private native boolean disconnectNative(byte[] address); |
| |
| private native boolean connectAudioNative(byte[] address); |
| |
| private native boolean disconnectAudioNative(byte[] address); |
| |
| private native boolean startVoiceRecognitionNative(); |
| |
| private native boolean stopVoiceRecognitionNative(); |
| |
| private native boolean setVolumeNative(int volumeType, int volume); |
| |
| private native boolean dialNative(String number); |
| |
| private native boolean dialMemoryNative(int location); |
| |
| private native boolean handleCallActionNative(int action, int index); |
| |
| private native boolean queryCurrentCallsNative(); |
| |
| private native boolean queryCurrentOperatorNameNative(); |
| |
| private native boolean retrieveSubscriberInfoNative(); |
| |
| private native boolean sendDtmfNative(byte code); |
| |
| private native boolean requestLastVoiceTagNumberNative(); |
| |
| private native boolean sendATCmdNative(int ATCmd, int val1, |
| int val2, String arg); |
| |
| 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.putInt(BluetoothHeadsetClient.EXTRA_VOICE_RECOGNITION, mVoiceRecognitionActive); |
| b.putInt(BluetoothHeadsetClient.EXTRA_IN_BAND_RING, mInBandRingtone); |
| b.putString(BluetoothHeadsetClient.EXTRA_SUBSCRIBER_INFO, mSubscriberInfo); |
| return b; |
| } |
| } |