| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.bluetooth; |
| |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.os.Message; |
| import android.bluetooth.BluetoothAdapter; |
| import android.os.PowerManager; |
| import android.server.BluetoothA2dpService; |
| import android.server.BluetoothService; |
| import android.util.Log; |
| import android.util.Pair; |
| |
| import com.android.internal.util.State; |
| import com.android.internal.util.StateMachine; |
| |
| import java.util.Set; |
| |
| /** |
| * This class is the Profile connection state machine associated with a remote |
| * device. When the device bonds an instance of this class is created. |
| * This tracks incoming and outgoing connections of all the profiles. Incoming |
| * connections are preferred over outgoing connections and HFP preferred over |
| * A2DP. When the device is unbonded, the instance is removed. |
| * |
| * States: |
| * {@link BondedDevice}: This state represents a bonded device. When in this |
| * state none of the profiles are in transition states. |
| * |
| * {@link OutgoingHandsfree}: Handsfree profile connection is in a transition |
| * state because of a outgoing Connect or Disconnect. |
| * |
| * {@link IncomingHandsfree}: Handsfree profile connection is in a transition |
| * state because of a incoming Connect or Disconnect. |
| * |
| * {@link IncomingA2dp}: A2dp profile connection is in a transition |
| * state because of a incoming Connect or Disconnect. |
| * |
| * {@link OutgoingA2dp}: A2dp profile connection is in a transition |
| * state because of a outgoing Connect or Disconnect. |
| * |
| * Todo(): Write tests for this class, when the Android Mock support is completed. |
| * @hide |
| */ |
| public final class BluetoothDeviceProfileState extends StateMachine { |
| private static final String TAG = "BluetoothDeviceProfileState"; |
| private static final boolean DBG = false; |
| |
| // TODO(): Restructure the state machine to make it scalable with regard to profiles. |
| public static final int CONNECT_HFP_OUTGOING = 1; |
| public static final int CONNECT_HFP_INCOMING = 2; |
| public static final int CONNECT_A2DP_OUTGOING = 3; |
| public static final int CONNECT_A2DP_INCOMING = 4; |
| public static final int CONNECT_HID_OUTGOING = 5; |
| public static final int CONNECT_HID_INCOMING = 6; |
| |
| public static final int DISCONNECT_HFP_OUTGOING = 50; |
| private static final int DISCONNECT_HFP_INCOMING = 51; |
| public static final int DISCONNECT_A2DP_OUTGOING = 52; |
| public static final int DISCONNECT_A2DP_INCOMING = 53; |
| public static final int DISCONNECT_HID_OUTGOING = 54; |
| public static final int DISCONNECT_HID_INCOMING = 55; |
| public static final int DISCONNECT_PBAP_OUTGOING = 56; |
| |
| public static final int UNPAIR = 100; |
| public static final int AUTO_CONNECT_PROFILES = 101; |
| public static final int TRANSITION_TO_STABLE = 102; |
| public static final int CONNECT_OTHER_PROFILES = 103; |
| private static final int CONNECTION_ACCESS_REQUEST_REPLY = 104; |
| private static final int CONNECTION_ACCESS_REQUEST_EXPIRY = 105; |
| |
| private static final int CONNECT_OTHER_PROFILES_DELAY = 4000; // 4 secs |
| private static final int CONNECTION_ACCESS_REQUEST_EXPIRY_TIMEOUT = 7000; // 7 secs |
| private static final int CONNECTION_ACCESS_UNDEFINED = -1; |
| private static final long INIT_INCOMING_REJECT_TIMER = 1000; // 1 sec |
| private static final long MAX_INCOMING_REJECT_TIMER = 3600 * 1000 * 4; // 4 hours |
| |
| private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings"; |
| private static final String ACCESS_AUTHORITY_CLASS = |
| "com.android.settings.bluetooth.BluetoothPermissionRequest"; |
| |
| private BondedDevice mBondedDevice = new BondedDevice(); |
| private OutgoingHandsfree mOutgoingHandsfree = new OutgoingHandsfree(); |
| private IncomingHandsfree mIncomingHandsfree = new IncomingHandsfree(); |
| private IncomingA2dp mIncomingA2dp = new IncomingA2dp(); |
| private OutgoingA2dp mOutgoingA2dp = new OutgoingA2dp(); |
| private OutgoingHid mOutgoingHid = new OutgoingHid(); |
| private IncomingHid mIncomingHid = new IncomingHid(); |
| |
| private Context mContext; |
| private BluetoothService mService; |
| private BluetoothA2dpService mA2dpService; |
| private BluetoothHeadset mHeadsetService; |
| private BluetoothPbap mPbapService; |
| private boolean mPbapServiceConnected; |
| private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; |
| |
| private BluetoothDevice mDevice; |
| private int mHeadsetState = BluetoothProfile.STATE_DISCONNECTED; |
| private int mA2dpState = BluetoothProfile.STATE_DISCONNECTED; |
| private long mIncomingRejectTimer; |
| private boolean mConnectionAccessReplyReceived = false; |
| private Pair<Integer, String> mIncomingConnections; |
| private PowerManager.WakeLock mWakeLock; |
| private PowerManager mPowerManager; |
| private boolean mPairingRequestRcvd = false; |
| |
| private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| if (!device.equals(mDevice)) return; |
| |
| if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) { |
| int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0); |
| int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0); |
| // We trust this device now |
| if (newState == BluetoothHeadset.STATE_CONNECTED) { |
| setTrust(BluetoothDevice.CONNECTION_ACCESS_YES); |
| } |
| mA2dpState = newState; |
| if (oldState == BluetoothA2dp.STATE_CONNECTED && |
| newState == BluetoothA2dp.STATE_DISCONNECTED) { |
| sendMessage(DISCONNECT_A2DP_INCOMING); |
| } |
| if (newState == BluetoothProfile.STATE_CONNECTED || |
| newState == BluetoothProfile.STATE_DISCONNECTED) { |
| sendMessage(TRANSITION_TO_STABLE); |
| } |
| } else if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { |
| int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0); |
| int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0); |
| // We trust this device now |
| if (newState == BluetoothHeadset.STATE_CONNECTED) { |
| setTrust(BluetoothDevice.CONNECTION_ACCESS_YES); |
| } |
| mHeadsetState = newState; |
| if (oldState == BluetoothHeadset.STATE_CONNECTED && |
| newState == BluetoothHeadset.STATE_DISCONNECTED) { |
| sendMessage(DISCONNECT_HFP_INCOMING); |
| } |
| if (newState == BluetoothProfile.STATE_CONNECTED || |
| newState == BluetoothProfile.STATE_DISCONNECTED) { |
| sendMessage(TRANSITION_TO_STABLE); |
| } |
| } else if (action.equals(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED)) { |
| int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0); |
| int oldState = |
| intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0); |
| // We trust this device now |
| if (newState == BluetoothHeadset.STATE_CONNECTED) { |
| setTrust(BluetoothDevice.CONNECTION_ACCESS_YES); |
| } |
| if (oldState == BluetoothProfile.STATE_CONNECTED && |
| newState == BluetoothProfile.STATE_DISCONNECTED) { |
| sendMessage(DISCONNECT_HID_INCOMING); |
| } |
| if (newState == BluetoothProfile.STATE_CONNECTED || |
| newState == BluetoothProfile.STATE_DISCONNECTED) { |
| sendMessage(TRANSITION_TO_STABLE); |
| } |
| } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { |
| // This is technically not needed, but we can get stuck sometimes. |
| // For example, if incoming A2DP fails, we are not informed by Bluez |
| sendMessage(TRANSITION_TO_STABLE); |
| } else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) { |
| mWakeLock.release(); |
| int val = intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT, |
| BluetoothDevice.CONNECTION_ACCESS_NO); |
| Message msg = obtainMessage(CONNECTION_ACCESS_REQUEST_REPLY); |
| msg.arg1 = val; |
| sendMessage(msg); |
| } else if (action.equals(BluetoothDevice.ACTION_PAIRING_REQUEST)) { |
| mPairingRequestRcvd = true; |
| } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { |
| int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, |
| BluetoothDevice.ERROR); |
| if (state == BluetoothDevice.BOND_BONDED && mPairingRequestRcvd) { |
| setTrust(BluetoothDevice.CONNECTION_ACCESS_YES); |
| mPairingRequestRcvd = false; |
| } else if (state == BluetoothDevice.BOND_NONE) { |
| mPairingRequestRcvd = false; |
| } |
| } |
| } |
| }; |
| |
| private boolean isPhoneDocked(BluetoothDevice autoConnectDevice) { |
| // This works only because these broadcast intents are "sticky" |
| Intent i = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT)); |
| if (i != null) { |
| int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED); |
| if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) { |
| BluetoothDevice device = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| if (device != null && autoConnectDevice.equals(device)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| public BluetoothDeviceProfileState(Context context, String address, |
| BluetoothService service, BluetoothA2dpService a2dpService, boolean setTrust) { |
| super(address); |
| mContext = context; |
| mDevice = new BluetoothDevice(address); |
| mService = service; |
| mA2dpService = a2dpService; |
| |
| addState(mBondedDevice); |
| addState(mOutgoingHandsfree); |
| addState(mIncomingHandsfree); |
| addState(mIncomingA2dp); |
| addState(mOutgoingA2dp); |
| addState(mOutgoingHid); |
| addState(mIncomingHid); |
| setInitialState(mBondedDevice); |
| |
| IntentFilter filter = new IntentFilter(); |
| // Fine-grained state broadcasts |
| filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); |
| filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); |
| filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); |
| filter.addAction(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED); |
| filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); |
| filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY); |
| filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST); |
| filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); |
| |
| mContext.registerReceiver(mBroadcastReceiver, filter); |
| |
| BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); |
| adapter.getProfileProxy(mContext, mBluetoothProfileServiceListener, |
| BluetoothProfile.HEADSET); |
| // TODO(): Convert PBAP to the new Profile APIs. |
| PbapServiceListener p = new PbapServiceListener(); |
| |
| mIncomingConnections = mService.getIncomingState(address); |
| mIncomingRejectTimer = readTimerValue(); |
| mPowerManager = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE); |
| mWakeLock = mPowerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | |
| PowerManager.ACQUIRE_CAUSES_WAKEUP | |
| PowerManager.ON_AFTER_RELEASE, TAG); |
| mWakeLock.setReferenceCounted(false); |
| |
| if (setTrust) { |
| setTrust(BluetoothDevice.CONNECTION_ACCESS_YES); |
| } |
| } |
| |
| private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener = |
| new BluetoothProfile.ServiceListener() { |
| public void onServiceConnected(int profile, BluetoothProfile proxy) { |
| synchronized(BluetoothDeviceProfileState.this) { |
| mHeadsetService = (BluetoothHeadset) proxy; |
| } |
| } |
| public void onServiceDisconnected(int profile) { |
| synchronized(BluetoothDeviceProfileState.this) { |
| mHeadsetService = null; |
| } |
| } |
| }; |
| |
| private class PbapServiceListener implements BluetoothPbap.ServiceListener { |
| public PbapServiceListener() { |
| mPbapService = new BluetoothPbap(mContext, this); |
| } |
| public void onServiceConnected() { |
| synchronized(BluetoothDeviceProfileState.this) { |
| mPbapServiceConnected = true; |
| } |
| } |
| public void onServiceDisconnected() { |
| synchronized(BluetoothDeviceProfileState.this) { |
| mPbapServiceConnected = false; |
| } |
| } |
| } |
| |
| private class BondedDevice extends State { |
| @Override |
| public void enter() { |
| Log.i(TAG, "Entering ACL Connected state with: " + getCurrentMessage().what); |
| Message m = new Message(); |
| m.copyFrom(getCurrentMessage()); |
| sendMessageAtFrontOfQueue(m); |
| } |
| @Override |
| public boolean processMessage(Message message) { |
| log("ACL Connected State -> Processing Message: " + message.what); |
| switch(message.what) { |
| case CONNECT_HFP_OUTGOING: |
| case DISCONNECT_HFP_OUTGOING: |
| transitionTo(mOutgoingHandsfree); |
| break; |
| case CONNECT_HFP_INCOMING: |
| transitionTo(mIncomingHandsfree); |
| break; |
| case DISCONNECT_HFP_INCOMING: |
| transitionTo(mIncomingHandsfree); |
| break; |
| case CONNECT_A2DP_OUTGOING: |
| case DISCONNECT_A2DP_OUTGOING: |
| transitionTo(mOutgoingA2dp); |
| break; |
| case CONNECT_A2DP_INCOMING: |
| case DISCONNECT_A2DP_INCOMING: |
| transitionTo(mIncomingA2dp); |
| break; |
| case CONNECT_HID_OUTGOING: |
| case DISCONNECT_HID_OUTGOING: |
| transitionTo(mOutgoingHid); |
| break; |
| case CONNECT_HID_INCOMING: |
| case DISCONNECT_HID_INCOMING: |
| transitionTo(mIncomingHid); |
| break; |
| case DISCONNECT_PBAP_OUTGOING: |
| processCommand(DISCONNECT_PBAP_OUTGOING); |
| break; |
| case UNPAIR: |
| if (mHeadsetState != BluetoothHeadset.STATE_DISCONNECTED) { |
| sendMessage(DISCONNECT_HFP_OUTGOING); |
| deferMessage(message); |
| break; |
| } else if (mA2dpState != BluetoothA2dp.STATE_DISCONNECTED) { |
| sendMessage(DISCONNECT_A2DP_OUTGOING); |
| deferMessage(message); |
| break; |
| } else if (mService.getInputDeviceConnectionState(mDevice) != |
| BluetoothInputDevice.STATE_DISCONNECTED) { |
| sendMessage(DISCONNECT_HID_OUTGOING); |
| deferMessage(message); |
| break; |
| } |
| processCommand(UNPAIR); |
| break; |
| case AUTO_CONNECT_PROFILES: |
| if (isPhoneDocked(mDevice)) { |
| // Don't auto connect to docks. |
| break; |
| } else { |
| if (mHeadsetService != null && |
| mHeadsetService.getPriority(mDevice) == |
| BluetoothHeadset.PRIORITY_AUTO_CONNECT && |
| mHeadsetService.getDevicesMatchingConnectionStates( |
| new int[] {BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_CONNECTING, |
| BluetoothProfile.STATE_DISCONNECTING}).size() == 0) { |
| mHeadsetService.connect(mDevice); |
| } |
| if (mA2dpService != null && |
| mA2dpService.getPriority(mDevice) == |
| BluetoothA2dp.PRIORITY_AUTO_CONNECT && |
| mA2dpService.getDevicesMatchingConnectionStates( |
| new int[] {BluetoothA2dp.STATE_CONNECTED, |
| BluetoothProfile.STATE_CONNECTING, |
| BluetoothProfile.STATE_DISCONNECTING}).size() == 0) { |
| mA2dpService.connect(mDevice); |
| } |
| if (mService.getInputDevicePriority(mDevice) == |
| BluetoothInputDevice.PRIORITY_AUTO_CONNECT) { |
| mService.connectInputDevice(mDevice); |
| } |
| } |
| break; |
| case CONNECT_OTHER_PROFILES: |
| if (isPhoneDocked(mDevice)) { |
| break; |
| } |
| if (message.arg1 == CONNECT_A2DP_OUTGOING) { |
| if (mA2dpService != null && |
| mA2dpService.getConnectedDevices().size() == 0) { |
| Log.i(TAG, "A2dp:Connect Other Profiles"); |
| mA2dpService.connect(mDevice); |
| } |
| } else if (message.arg1 == CONNECT_HFP_OUTGOING) { |
| if (mHeadsetService == null) { |
| deferMessage(message); |
| } else { |
| if (mHeadsetService.getConnectedDevices().size() == 0) { |
| Log.i(TAG, "Headset:Connect Other Profiles"); |
| mHeadsetService.connect(mDevice); |
| } |
| } |
| } |
| break; |
| case TRANSITION_TO_STABLE: |
| // ignore. |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return HANDLED; |
| } |
| } |
| |
| private class OutgoingHandsfree extends State { |
| private boolean mStatus = false; |
| private int mCommand; |
| |
| @Override |
| public void enter() { |
| Log.i(TAG, "Entering OutgoingHandsfree state with: " + getCurrentMessage().what); |
| mCommand = getCurrentMessage().what; |
| if (mCommand != CONNECT_HFP_OUTGOING && |
| mCommand != DISCONNECT_HFP_OUTGOING) { |
| Log.e(TAG, "Error: OutgoingHandsfree state with command:" + mCommand); |
| } |
| mStatus = processCommand(mCommand); |
| if (!mStatus) { |
| sendMessage(TRANSITION_TO_STABLE); |
| mService.sendProfileStateMessage(BluetoothProfileState.HFP, |
| BluetoothProfileState.TRANSITION_TO_STABLE); |
| } |
| } |
| |
| @Override |
| public boolean processMessage(Message message) { |
| log("OutgoingHandsfree State -> Processing Message: " + message.what); |
| Message deferMsg = new Message(); |
| int command = message.what; |
| switch(command) { |
| case CONNECT_HFP_OUTGOING: |
| if (command != mCommand) { |
| // Disconnect followed by a connect - defer |
| deferMessage(message); |
| } |
| break; |
| case CONNECT_HFP_INCOMING: |
| if (mCommand == CONNECT_HFP_OUTGOING) { |
| // Cancel outgoing connect, accept incoming |
| cancelCommand(CONNECT_HFP_OUTGOING); |
| transitionTo(mIncomingHandsfree); |
| } else { |
| // We have done the disconnect but we are not |
| // sure which state we are in at this point. |
| deferMessage(message); |
| } |
| break; |
| case CONNECT_A2DP_INCOMING: |
| // accept incoming A2DP, retry HFP_OUTGOING |
| transitionTo(mIncomingA2dp); |
| |
| if (mStatus) { |
| deferMsg.what = mCommand; |
| deferMessage(deferMsg); |
| } |
| break; |
| case CONNECT_A2DP_OUTGOING: |
| deferMessage(message); |
| break; |
| case DISCONNECT_HFP_OUTGOING: |
| if (mCommand == CONNECT_HFP_OUTGOING) { |
| // Cancel outgoing connect |
| cancelCommand(CONNECT_HFP_OUTGOING); |
| processCommand(DISCONNECT_HFP_OUTGOING); |
| } |
| // else ignore |
| break; |
| case DISCONNECT_HFP_INCOMING: |
| // When this happens the socket would be closed and the headset |
| // state moved to DISCONNECTED, cancel the outgoing thread. |
| // if it still is in CONNECTING state |
| cancelCommand(CONNECT_HFP_OUTGOING); |
| break; |
| case DISCONNECT_A2DP_OUTGOING: |
| deferMessage(message); |
| break; |
| case DISCONNECT_A2DP_INCOMING: |
| // Bluez will handle the disconnect. If because of this the outgoing |
| // handsfree connection has failed, then retry. |
| if (mStatus) { |
| deferMsg.what = mCommand; |
| deferMessage(deferMsg); |
| } |
| break; |
| case CONNECT_HID_OUTGOING: |
| case DISCONNECT_HID_OUTGOING: |
| deferMessage(message); |
| break; |
| case CONNECT_HID_INCOMING: |
| transitionTo(mIncomingHid); |
| if (mStatus) { |
| deferMsg.what = mCommand; |
| deferMessage(deferMsg); |
| } |
| break; |
| case DISCONNECT_HID_INCOMING: |
| if (mStatus) { |
| deferMsg.what = mCommand; |
| deferMessage(deferMsg); |
| } |
| break; // ignore |
| case DISCONNECT_PBAP_OUTGOING: |
| case UNPAIR: |
| case AUTO_CONNECT_PROFILES: |
| case CONNECT_OTHER_PROFILES: |
| deferMessage(message); |
| break; |
| case TRANSITION_TO_STABLE: |
| transitionTo(mBondedDevice); |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return HANDLED; |
| } |
| } |
| |
| private class IncomingHandsfree extends State { |
| private boolean mStatus = false; |
| private int mCommand; |
| |
| @Override |
| public void enter() { |
| Log.i(TAG, "Entering IncomingHandsfree state with: " + getCurrentMessage().what); |
| mCommand = getCurrentMessage().what; |
| if (mCommand != CONNECT_HFP_INCOMING && |
| mCommand != DISCONNECT_HFP_INCOMING) { |
| Log.e(TAG, "Error: IncomingHandsfree state with command:" + mCommand); |
| } |
| mStatus = processCommand(mCommand); |
| if (!mStatus) { |
| sendMessage(TRANSITION_TO_STABLE); |
| mService.sendProfileStateMessage(BluetoothProfileState.HFP, |
| BluetoothProfileState.TRANSITION_TO_STABLE); |
| } |
| } |
| |
| @Override |
| public boolean processMessage(Message message) { |
| log("IncomingHandsfree State -> Processing Message: " + message.what); |
| switch(message.what) { |
| case CONNECT_HFP_OUTGOING: |
| deferMessage(message); |
| break; |
| case CONNECT_HFP_INCOMING: |
| // Ignore |
| Log.e(TAG, "Error: Incoming connection with a pending incoming connection"); |
| break; |
| case CONNECTION_ACCESS_REQUEST_REPLY: |
| int val = message.arg1; |
| mConnectionAccessReplyReceived = true; |
| boolean value = false; |
| if (val == BluetoothDevice.CONNECTION_ACCESS_YES) { |
| value = true; |
| } |
| setTrust(val); |
| |
| handleIncomingConnection(CONNECT_HFP_INCOMING, value); |
| break; |
| case CONNECTION_ACCESS_REQUEST_EXPIRY: |
| if (!mConnectionAccessReplyReceived) { |
| handleIncomingConnection(CONNECT_HFP_INCOMING, false); |
| sendConnectionAccessRemovalIntent(); |
| sendMessage(TRANSITION_TO_STABLE); |
| } |
| break; |
| case CONNECT_A2DP_INCOMING: |
| // Serialize the commands. |
| deferMessage(message); |
| break; |
| case CONNECT_A2DP_OUTGOING: |
| deferMessage(message); |
| break; |
| case DISCONNECT_HFP_OUTGOING: |
| // We don't know at what state we are in the incoming HFP connection state. |
| // We can be changing from DISCONNECTED to CONNECTING, or |
| // from CONNECTING to CONNECTED, so serializing this command is |
| // the safest option. |
| deferMessage(message); |
| break; |
| case DISCONNECT_HFP_INCOMING: |
| // Nothing to do here, we will already be DISCONNECTED |
| // by this point. |
| break; |
| case DISCONNECT_A2DP_OUTGOING: |
| deferMessage(message); |
| break; |
| case DISCONNECT_A2DP_INCOMING: |
| // Bluez handles incoming A2DP disconnect. |
| // If this causes incoming HFP to fail, it is more of a headset problem |
| // since both connections are incoming ones. |
| break; |
| case CONNECT_HID_OUTGOING: |
| case DISCONNECT_HID_OUTGOING: |
| deferMessage(message); |
| break; |
| case CONNECT_HID_INCOMING: |
| case DISCONNECT_HID_INCOMING: |
| break; // ignore |
| case DISCONNECT_PBAP_OUTGOING: |
| case UNPAIR: |
| case AUTO_CONNECT_PROFILES: |
| case CONNECT_OTHER_PROFILES: |
| deferMessage(message); |
| break; |
| case TRANSITION_TO_STABLE: |
| transitionTo(mBondedDevice); |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return HANDLED; |
| } |
| } |
| |
| private class OutgoingA2dp extends State { |
| private boolean mStatus = false; |
| private int mCommand; |
| |
| @Override |
| public void enter() { |
| Log.i(TAG, "Entering OutgoingA2dp state with: " + getCurrentMessage().what); |
| mCommand = getCurrentMessage().what; |
| if (mCommand != CONNECT_A2DP_OUTGOING && |
| mCommand != DISCONNECT_A2DP_OUTGOING) { |
| Log.e(TAG, "Error: OutgoingA2DP state with command:" + mCommand); |
| } |
| mStatus = processCommand(mCommand); |
| if (!mStatus) { |
| sendMessage(TRANSITION_TO_STABLE); |
| mService.sendProfileStateMessage(BluetoothProfileState.A2DP, |
| BluetoothProfileState.TRANSITION_TO_STABLE); |
| } |
| } |
| |
| @Override |
| public boolean processMessage(Message message) { |
| log("OutgoingA2dp State->Processing Message: " + message.what); |
| Message deferMsg = new Message(); |
| switch(message.what) { |
| case CONNECT_HFP_OUTGOING: |
| processCommand(CONNECT_HFP_OUTGOING); |
| |
| // Don't cancel A2DP outgoing as there is no guarantee it |
| // will get canceled. |
| // It might already be connected but we might not have got the |
| // A2DP_SINK_STATE_CHANGE. Hence, no point disconnecting here. |
| // The worst case, the connection will fail, retry. |
| // The same applies to Disconnecting an A2DP connection. |
| if (mStatus) { |
| deferMsg.what = mCommand; |
| deferMessage(deferMsg); |
| } |
| break; |
| case CONNECT_HFP_INCOMING: |
| processCommand(CONNECT_HFP_INCOMING); |
| |
| // Don't cancel A2DP outgoing as there is no guarantee |
| // it will get canceled. |
| // The worst case, the connection will fail, retry. |
| if (mStatus) { |
| deferMsg.what = mCommand; |
| deferMessage(deferMsg); |
| } |
| break; |
| case CONNECT_A2DP_INCOMING: |
| // Bluez will take care of conflicts between incoming and outgoing |
| // connections. |
| transitionTo(mIncomingA2dp); |
| break; |
| case CONNECT_A2DP_OUTGOING: |
| // Ignore |
| break; |
| case DISCONNECT_HFP_OUTGOING: |
| deferMessage(message); |
| break; |
| case DISCONNECT_HFP_INCOMING: |
| // At this point, we are already disconnected |
| // with HFP. Sometimes A2DP connection can |
| // fail due to the disconnection of HFP. So add a retry |
| // for the A2DP. |
| if (mStatus) { |
| deferMsg.what = mCommand; |
| deferMessage(deferMsg); |
| } |
| break; |
| case DISCONNECT_A2DP_OUTGOING: |
| deferMessage(message); |
| break; |
| case DISCONNECT_A2DP_INCOMING: |
| // Ignore, will be handled by Bluez |
| break; |
| case CONNECT_HID_OUTGOING: |
| case DISCONNECT_HID_OUTGOING: |
| deferMessage(message); |
| break; |
| case CONNECT_HID_INCOMING: |
| transitionTo(mIncomingHid); |
| if (mStatus) { |
| deferMsg.what = mCommand; |
| deferMessage(deferMsg); |
| } |
| break; |
| case DISCONNECT_HID_INCOMING: |
| if (mStatus) { |
| deferMsg.what = mCommand; |
| deferMessage(deferMsg); |
| } |
| break; // ignore |
| case DISCONNECT_PBAP_OUTGOING: |
| case UNPAIR: |
| case AUTO_CONNECT_PROFILES: |
| case CONNECT_OTHER_PROFILES: |
| deferMessage(message); |
| break; |
| case TRANSITION_TO_STABLE: |
| transitionTo(mBondedDevice); |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return HANDLED; |
| } |
| } |
| |
| private class IncomingA2dp extends State { |
| private boolean mStatus = false; |
| private int mCommand; |
| |
| @Override |
| public void enter() { |
| Log.i(TAG, "Entering IncomingA2dp state with: " + getCurrentMessage().what); |
| mCommand = getCurrentMessage().what; |
| if (mCommand != CONNECT_A2DP_INCOMING && |
| mCommand != DISCONNECT_A2DP_INCOMING) { |
| Log.e(TAG, "Error: IncomingA2DP state with command:" + mCommand); |
| } |
| mStatus = processCommand(mCommand); |
| if (!mStatus) { |
| sendMessage(TRANSITION_TO_STABLE); |
| mService.sendProfileStateMessage(BluetoothProfileState.A2DP, |
| BluetoothProfileState.TRANSITION_TO_STABLE); |
| } |
| } |
| |
| @Override |
| public boolean processMessage(Message message) { |
| log("IncomingA2dp State->Processing Message: " + message.what); |
| switch(message.what) { |
| case CONNECT_HFP_OUTGOING: |
| deferMessage(message); |
| break; |
| case CONNECT_HFP_INCOMING: |
| // Shouldn't happen, but serialize the commands. |
| deferMessage(message); |
| break; |
| case CONNECT_A2DP_INCOMING: |
| // ignore |
| break; |
| case CONNECTION_ACCESS_REQUEST_REPLY: |
| int val = message.arg1; |
| mConnectionAccessReplyReceived = true; |
| boolean value = false; |
| if (val == BluetoothDevice.CONNECTION_ACCESS_YES) { |
| value = true; |
| } |
| setTrust(val); |
| handleIncomingConnection(CONNECT_A2DP_INCOMING, value); |
| break; |
| case CONNECTION_ACCESS_REQUEST_EXPIRY: |
| // The check protects the race condition between REQUEST_REPLY |
| // and the timer expiry. |
| if (!mConnectionAccessReplyReceived) { |
| handleIncomingConnection(CONNECT_A2DP_INCOMING, false); |
| sendConnectionAccessRemovalIntent(); |
| sendMessage(TRANSITION_TO_STABLE); |
| } |
| break; |
| case CONNECT_A2DP_OUTGOING: |
| // Defer message and retry |
| deferMessage(message); |
| break; |
| case DISCONNECT_HFP_OUTGOING: |
| deferMessage(message); |
| break; |
| case DISCONNECT_HFP_INCOMING: |
| // Shouldn't happen but if does, we can handle it. |
| // Depends if the headset can handle it. |
| // Incoming A2DP will be handled by Bluez, Disconnect HFP |
| // the socket would have already been closed. |
| // ignore |
| break; |
| case DISCONNECT_A2DP_OUTGOING: |
| deferMessage(message); |
| break; |
| case DISCONNECT_A2DP_INCOMING: |
| // Ignore, will be handled by Bluez |
| break; |
| case CONNECT_HID_OUTGOING: |
| case DISCONNECT_HID_OUTGOING: |
| deferMessage(message); |
| break; |
| case CONNECT_HID_INCOMING: |
| case DISCONNECT_HID_INCOMING: |
| break; // ignore |
| case DISCONNECT_PBAP_OUTGOING: |
| case UNPAIR: |
| case AUTO_CONNECT_PROFILES: |
| case CONNECT_OTHER_PROFILES: |
| deferMessage(message); |
| break; |
| case TRANSITION_TO_STABLE: |
| transitionTo(mBondedDevice); |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return HANDLED; |
| } |
| } |
| |
| |
| private class OutgoingHid extends State { |
| private boolean mStatus = false; |
| private int mCommand; |
| |
| @Override |
| public void enter() { |
| log("Entering OutgoingHid state with: " + getCurrentMessage().what); |
| mCommand = getCurrentMessage().what; |
| if (mCommand != CONNECT_HID_OUTGOING && |
| mCommand != DISCONNECT_HID_OUTGOING) { |
| Log.e(TAG, "Error: OutgoingHid state with command:" + mCommand); |
| } |
| mStatus = processCommand(mCommand); |
| if (!mStatus) sendMessage(TRANSITION_TO_STABLE); |
| } |
| |
| @Override |
| public boolean processMessage(Message message) { |
| log("OutgoingHid State->Processing Message: " + message.what); |
| Message deferMsg = new Message(); |
| switch(message.what) { |
| // defer all outgoing messages |
| case CONNECT_HFP_OUTGOING: |
| case CONNECT_A2DP_OUTGOING: |
| case CONNECT_HID_OUTGOING: |
| case DISCONNECT_HFP_OUTGOING: |
| case DISCONNECT_A2DP_OUTGOING: |
| case DISCONNECT_HID_OUTGOING: |
| deferMessage(message); |
| break; |
| |
| case CONNECT_HFP_INCOMING: |
| transitionTo(mIncomingHandsfree); |
| break; |
| case CONNECT_A2DP_INCOMING: |
| transitionTo(mIncomingA2dp); |
| |
| // Don't cancel HID outgoing as there is no guarantee it |
| // will get canceled. |
| // It might already be connected but we might not have got the |
| // INPUT_DEVICE_STATE_CHANGE. Hence, no point disconnecting here. |
| // The worst case, the connection will fail, retry. |
| if (mStatus) { |
| deferMsg.what = mCommand; |
| deferMessage(deferMsg); |
| } |
| break; |
| case CONNECT_HID_INCOMING: |
| // Bluez will take care of the conflicts |
| transitionTo(mIncomingHid); |
| break; |
| |
| case DISCONNECT_HFP_INCOMING: |
| case DISCONNECT_A2DP_INCOMING: |
| // At this point, we are already disconnected |
| // with HFP. Sometimes HID connection can |
| // fail due to the disconnection of HFP. So add a retry |
| // for the HID. |
| if (mStatus) { |
| deferMsg.what = mCommand; |
| deferMessage(deferMsg); |
| } |
| break; |
| case DISCONNECT_HID_INCOMING: |
| // Ignore, will be handled by Bluez |
| break; |
| case DISCONNECT_PBAP_OUTGOING: |
| case UNPAIR: |
| case AUTO_CONNECT_PROFILES: |
| deferMessage(message); |
| break; |
| case TRANSITION_TO_STABLE: |
| transitionTo(mBondedDevice); |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return HANDLED; |
| } |
| } |
| |
| private class IncomingHid extends State { |
| private boolean mStatus = false; |
| private int mCommand; |
| |
| @Override |
| public void enter() { |
| log("Entering IncomingHid state with: " + getCurrentMessage().what); |
| mCommand = getCurrentMessage().what; |
| if (mCommand != CONNECT_HID_INCOMING && |
| mCommand != DISCONNECT_HID_INCOMING) { |
| Log.e(TAG, "Error: IncomingHid state with command:" + mCommand); |
| } |
| mStatus = processCommand(mCommand); |
| if (!mStatus) sendMessage(TRANSITION_TO_STABLE); |
| } |
| |
| @Override |
| public boolean processMessage(Message message) { |
| log("IncomingHid State->Processing Message: " + message.what); |
| Message deferMsg = new Message(); |
| switch(message.what) { |
| case CONNECT_HFP_OUTGOING: |
| case CONNECT_HFP_INCOMING: |
| case DISCONNECT_HFP_OUTGOING: |
| case CONNECT_A2DP_INCOMING: |
| case CONNECT_A2DP_OUTGOING: |
| case DISCONNECT_A2DP_OUTGOING: |
| case CONNECT_HID_OUTGOING: |
| case CONNECT_HID_INCOMING: |
| case DISCONNECT_HID_OUTGOING: |
| deferMessage(message); |
| break; |
| case CONNECTION_ACCESS_REQUEST_REPLY: |
| mConnectionAccessReplyReceived = true; |
| int val = message.arg1; |
| setTrust(val); |
| handleIncomingConnection(CONNECT_HID_INCOMING, |
| val == BluetoothDevice.CONNECTION_ACCESS_YES); |
| break; |
| case CONNECTION_ACCESS_REQUEST_EXPIRY: |
| if (!mConnectionAccessReplyReceived) { |
| handleIncomingConnection(CONNECT_HID_INCOMING, false); |
| sendConnectionAccessRemovalIntent(); |
| sendMessage(TRANSITION_TO_STABLE); |
| } |
| break; |
| case DISCONNECT_HFP_INCOMING: |
| // Shouldn't happen but if does, we can handle it. |
| // Depends if the headset can handle it. |
| // Incoming HID will be handled by Bluez, Disconnect HFP |
| // the socket would have already been closed. |
| // ignore |
| break; |
| case DISCONNECT_HID_INCOMING: |
| case DISCONNECT_A2DP_INCOMING: |
| // Ignore, will be handled by Bluez |
| break; |
| case DISCONNECT_PBAP_OUTGOING: |
| case UNPAIR: |
| case AUTO_CONNECT_PROFILES: |
| deferMessage(message); |
| break; |
| case TRANSITION_TO_STABLE: |
| transitionTo(mBondedDevice); |
| break; |
| default: |
| return NOT_HANDLED; |
| } |
| return HANDLED; |
| } |
| } |
| |
| |
| synchronized void cancelCommand(int command) { |
| if (command == CONNECT_HFP_OUTGOING ) { |
| // Cancel the outgoing thread. |
| if (mHeadsetService != null) { |
| mHeadsetService.cancelConnectThread(); |
| } |
| // HeadsetService is down. Phone process most likely crashed. |
| // The thread would have got killed. |
| } |
| } |
| |
| synchronized void deferProfileServiceMessage(int command) { |
| Message msg = new Message(); |
| msg.what = command; |
| deferMessage(msg); |
| } |
| |
| private void updateIncomingAllowedTimer() { |
| // Not doing a perfect exponential backoff because |
| // we want two different rates. For all practical |
| // purposes, this is good enough. |
| if (mIncomingRejectTimer == 0) mIncomingRejectTimer = INIT_INCOMING_REJECT_TIMER; |
| |
| mIncomingRejectTimer *= 5; |
| if (mIncomingRejectTimer > MAX_INCOMING_REJECT_TIMER) { |
| mIncomingRejectTimer = MAX_INCOMING_REJECT_TIMER; |
| } |
| writeTimerValue(mIncomingRejectTimer); |
| } |
| |
| private boolean handleIncomingConnection(int command, boolean accept) { |
| boolean ret = false; |
| Log.i(TAG, "handleIncomingConnection:" + command + ":" + accept); |
| switch (command) { |
| case CONNECT_HFP_INCOMING: |
| if (!accept) { |
| ret = mHeadsetService.rejectIncomingConnect(mDevice); |
| sendMessage(TRANSITION_TO_STABLE); |
| updateIncomingAllowedTimer(); |
| } else if (mHeadsetState == BluetoothHeadset.STATE_CONNECTING) { |
| writeTimerValue(0); |
| ret = mHeadsetService.acceptIncomingConnect(mDevice); |
| } else if (mHeadsetState == BluetoothHeadset.STATE_DISCONNECTED) { |
| writeTimerValue(0); |
| handleConnectionOfOtherProfiles(command); |
| ret = mHeadsetService.createIncomingConnect(mDevice); |
| } |
| break; |
| case CONNECT_A2DP_INCOMING: |
| if (!accept) { |
| ret = mA2dpService.allowIncomingConnect(mDevice, false); |
| sendMessage(TRANSITION_TO_STABLE); |
| updateIncomingAllowedTimer(); |
| } else { |
| writeTimerValue(0); |
| ret = mA2dpService.allowIncomingConnect(mDevice, true); |
| handleConnectionOfOtherProfiles(command); |
| } |
| break; |
| case CONNECT_HID_INCOMING: |
| if (!accept) { |
| ret = mService.allowIncomingHidConnect(mDevice, false); |
| sendMessage(TRANSITION_TO_STABLE); |
| updateIncomingAllowedTimer(); |
| } else { |
| writeTimerValue(0); |
| ret = mService.allowIncomingHidConnect(mDevice, true); |
| } |
| break; |
| default: |
| Log.e(TAG, "Waiting for incoming connection but state changed to:" + command); |
| break; |
| } |
| return ret; |
| } |
| |
| private void sendConnectionAccessIntent() { |
| mConnectionAccessReplyReceived = false; |
| |
| if (!mPowerManager.isScreenOn()) mWakeLock.acquire(); |
| |
| Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST); |
| intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS); |
| intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, |
| BluetoothDevice.REQUEST_TYPE_PROFILE_CONNECTION); |
| intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); |
| mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); |
| } |
| |
| private void sendConnectionAccessRemovalIntent() { |
| mWakeLock.release(); |
| Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL); |
| intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); |
| mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); |
| } |
| |
| private int getTrust() { |
| String address = mDevice.getAddress(); |
| if (mIncomingConnections != null) return mIncomingConnections.first; |
| return CONNECTION_ACCESS_UNDEFINED; |
| } |
| |
| |
| private String getStringValue(long value) { |
| StringBuilder sbr = new StringBuilder(); |
| sbr.append(Long.toString(System.currentTimeMillis())); |
| sbr.append("-"); |
| sbr.append(Long.toString(value)); |
| return sbr.toString(); |
| } |
| |
| private void setTrust(int value) { |
| String second; |
| if (mIncomingConnections == null) { |
| second = getStringValue(INIT_INCOMING_REJECT_TIMER); |
| } else { |
| second = mIncomingConnections.second; |
| } |
| |
| mIncomingConnections = new Pair(value, second); |
| mService.writeIncomingConnectionState(mDevice.getAddress(), mIncomingConnections); |
| } |
| |
| private void writeTimerValue(long value) { |
| Integer first; |
| if (mIncomingConnections == null) { |
| first = CONNECTION_ACCESS_UNDEFINED; |
| } else { |
| first = mIncomingConnections.first; |
| } |
| mIncomingConnections = new Pair(first, getStringValue(value)); |
| mService.writeIncomingConnectionState(mDevice.getAddress(), mIncomingConnections); |
| } |
| |
| private long readTimerValue() { |
| if (mIncomingConnections == null) |
| return 0; |
| String value = mIncomingConnections.second; |
| String[] splits = value.split("-"); |
| if (splits != null && splits.length == 2) { |
| return Long.parseLong(splits[1]); |
| } |
| return 0; |
| } |
| |
| private boolean readIncomingAllowedValue() { |
| if (readTimerValue() == 0) return true; |
| String value = mIncomingConnections.second; |
| String[] splits = value.split("-"); |
| if (splits != null && splits.length == 2) { |
| long val1 = Long.parseLong(splits[0]); |
| long val2 = Long.parseLong(splits[1]); |
| if (val1 + val2 <= System.currentTimeMillis()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| synchronized boolean processCommand(int command) { |
| log("Processing command:" + command); |
| switch(command) { |
| case CONNECT_HFP_OUTGOING: |
| if (mHeadsetService == null) { |
| deferProfileServiceMessage(command); |
| } else { |
| return mHeadsetService.connectHeadsetInternal(mDevice); |
| } |
| break; |
| case CONNECT_HFP_INCOMING: |
| if (mHeadsetService == null) { |
| deferProfileServiceMessage(command); |
| } else { |
| processIncomingConnectCommand(command); |
| return true; |
| } |
| break; |
| case CONNECT_A2DP_OUTGOING: |
| if (mA2dpService != null) { |
| return mA2dpService.connectSinkInternal(mDevice); |
| } |
| break; |
| case CONNECT_A2DP_INCOMING: |
| processIncomingConnectCommand(command); |
| return true; |
| case CONNECT_HID_OUTGOING: |
| return mService.connectInputDeviceInternal(mDevice); |
| case CONNECT_HID_INCOMING: |
| processIncomingConnectCommand(command); |
| return true; |
| case DISCONNECT_HFP_OUTGOING: |
| if (mHeadsetService == null) { |
| deferProfileServiceMessage(command); |
| } else { |
| // Disconnect PBAP |
| // TODO(): Add PBAP to the state machine. |
| Message m = new Message(); |
| m.what = DISCONNECT_PBAP_OUTGOING; |
| deferMessage(m); |
| if (mHeadsetService.getPriority(mDevice) == |
| BluetoothHeadset.PRIORITY_AUTO_CONNECT) { |
| mHeadsetService.setPriority(mDevice, BluetoothHeadset.PRIORITY_ON); |
| } |
| return mHeadsetService.disconnectHeadsetInternal(mDevice); |
| } |
| break; |
| case DISCONNECT_HFP_INCOMING: |
| // ignore |
| return true; |
| case DISCONNECT_A2DP_INCOMING: |
| // ignore |
| return true; |
| case DISCONNECT_A2DP_OUTGOING: |
| if (mA2dpService != null) { |
| if (mA2dpService.getPriority(mDevice) == |
| BluetoothA2dp.PRIORITY_AUTO_CONNECT) { |
| mA2dpService.setPriority(mDevice, BluetoothHeadset.PRIORITY_ON); |
| } |
| return mA2dpService.disconnectSinkInternal(mDevice); |
| } |
| break; |
| case DISCONNECT_HID_INCOMING: |
| // ignore |
| return true; |
| case DISCONNECT_HID_OUTGOING: |
| if (mService.getInputDevicePriority(mDevice) == |
| BluetoothInputDevice.PRIORITY_AUTO_CONNECT) { |
| mService.setInputDevicePriority(mDevice, BluetoothInputDevice.PRIORITY_ON); |
| } |
| return mService.disconnectInputDeviceInternal(mDevice); |
| case DISCONNECT_PBAP_OUTGOING: |
| if (!mPbapServiceConnected) { |
| deferProfileServiceMessage(command); |
| } else { |
| return mPbapService.disconnect(); |
| } |
| break; |
| case UNPAIR: |
| writeTimerValue(INIT_INCOMING_REJECT_TIMER); |
| setTrust(CONNECTION_ACCESS_UNDEFINED); |
| return mService.removeBondInternal(mDevice.getAddress()); |
| default: |
| Log.e(TAG, "Error: Unknown Command"); |
| } |
| return false; |
| } |
| |
| private void processIncomingConnectCommand(int command) { |
| // Check if device is already trusted |
| int access = getTrust(); |
| if (access == BluetoothDevice.CONNECTION_ACCESS_YES) { |
| handleIncomingConnection(command, true); |
| } else if (access == BluetoothDevice.CONNECTION_ACCESS_NO && |
| !readIncomingAllowedValue()) { |
| handleIncomingConnection(command, false); |
| } else { |
| sendConnectionAccessIntent(); |
| Message msg = obtainMessage(CONNECTION_ACCESS_REQUEST_EXPIRY); |
| sendMessageDelayed(msg, |
| CONNECTION_ACCESS_REQUEST_EXPIRY_TIMEOUT); |
| } |
| } |
| |
| private void handleConnectionOfOtherProfiles(int command) { |
| // The white paper recommendations mentions that when there is a |
| // link loss, it is the responsibility of the remote device to connect. |
| // Many connect only 1 profile - and they connect the second profile on |
| // some user action (like play being pressed) and so we need this code. |
| // Auto Connect code only connects to the last connected device - which |
| // is useful in cases like when the phone reboots. But consider the |
| // following case: |
| // User is connected to the car's phone and A2DP profile. |
| // User comes to the desk and places the phone in the dock |
| // (or any speaker or music system or even another headset) and thus |
| // gets connected to the A2DP profile. User goes back to the car. |
| // Ideally the car's system is supposed to send incoming connections |
| // from both Handsfree and A2DP profile. But they don't. The Auto |
| // connect code, will not work here because we only auto connect to the |
| // last connected device for that profile which in this case is the dock. |
| // Now suppose a user is using 2 headsets simultaneously, one for the |
| // phone profile one for the A2DP profile. If this is the use case, we |
| // expect the user to use the preference to turn off the A2DP profile in |
| // the Settings screen for the first headset. Else, after link loss, |
| // there can be an incoming connection from the first headset which |
| // might result in the connection of the A2DP profile (if the second |
| // headset is slower) and thus the A2DP profile on the second headset |
| // will never get connected. |
| // |
| // TODO(): Handle other profiles here. |
| switch (command) { |
| case CONNECT_HFP_INCOMING: |
| // Connect A2DP if there is no incoming connection |
| // If the priority is OFF - don't auto connect. |
| if (mA2dpService.getPriority(mDevice) == BluetoothProfile.PRIORITY_ON || |
| mA2dpService.getPriority(mDevice) == |
| BluetoothProfile.PRIORITY_AUTO_CONNECT) { |
| Message msg = new Message(); |
| msg.what = CONNECT_OTHER_PROFILES; |
| msg.arg1 = CONNECT_A2DP_OUTGOING; |
| sendMessageDelayed(msg, CONNECT_OTHER_PROFILES_DELAY); |
| } |
| break; |
| case CONNECT_A2DP_INCOMING: |
| // This is again against spec. HFP incoming connections should be made |
| // before A2DP, so we should not hit this case. But many devices |
| // don't follow this. |
| if (mHeadsetService != null && |
| (mHeadsetService.getPriority(mDevice) == BluetoothProfile.PRIORITY_ON || |
| mHeadsetService.getPriority(mDevice) == |
| BluetoothProfile.PRIORITY_AUTO_CONNECT)) { |
| Message msg = new Message(); |
| msg.what = CONNECT_OTHER_PROFILES; |
| msg.arg1 = CONNECT_HFP_OUTGOING; |
| sendMessageDelayed(msg, CONNECT_OTHER_PROFILES_DELAY); |
| } |
| break; |
| default: |
| break; |
| } |
| |
| } |
| |
| /*package*/ BluetoothDevice getDevice() { |
| return mDevice; |
| } |
| |
| private void log(String message) { |
| if (DBG) { |
| Log.i(TAG, "Device:" + mDevice + " Message:" + message); |
| } |
| } |
| } |