/* * Copyright (C) 2008 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.server; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHealth; import android.bluetooth.BluetoothInputDevice; import android.bluetooth.BluetoothPan; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.Message; import android.os.ParcelUuid; import android.os.PowerManager; import android.util.Log; import java.util.HashMap; import java.util.List; /** * @hide */ class BluetoothEventLoop { private static final String TAG = "BluetoothEventLoop"; private static final boolean DBG = false; private int mNativeData; private Thread mThread; private boolean mStarted; private boolean mInterrupted; private final HashMap<String, Integer> mPasskeyAgentRequestData; private final HashMap<String, Integer> mAuthorizationAgentRequestData; private final BluetoothService mBluetoothService; private final BluetoothAdapter mAdapter; private final BluetoothAdapterStateMachine mBluetoothState; private BluetoothA2dp mA2dp; private final Context mContext; // The WakeLock is used for bringing up the LCD during a pairing request // from remote device when Android is in Suspend state. private PowerManager.WakeLock mWakeLock; private static final int EVENT_PAIRING_CONSENT_DELAYED_ACCEPT = 1; private static final int EVENT_AGENT_CANCEL = 2; private static final int CREATE_DEVICE_ALREADY_EXISTS = 1; private static final int CREATE_DEVICE_SUCCESS = 0; private static final int CREATE_DEVICE_FAILED = -1; private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { String address = null; switch (msg.what) { case EVENT_PAIRING_CONSENT_DELAYED_ACCEPT: address = (String)msg.obj; if (address != null) { mBluetoothService.setPairingConfirmation(address, true); } break; case EVENT_AGENT_CANCEL: // Set the Bond State to BOND_NONE. // We always have only 1 device in BONDING state. String[] devices = mBluetoothService.listInState(BluetoothDevice.BOND_BONDING); if (devices.length == 0) { break; } else if (devices.length > 1) { Log.e(TAG, " There is more than one device in the Bonding State"); break; } address = devices[0]; mBluetoothService.setBondState(address, BluetoothDevice.BOND_NONE, BluetoothDevice.UNBOND_REASON_REMOTE_AUTH_CANCELED); break; } } }; static { classInitNative(); } private static native void classInitNative(); /* package */ BluetoothEventLoop(Context context, BluetoothAdapter adapter, BluetoothService bluetoothService, BluetoothAdapterStateMachine bluetoothState) { mBluetoothService = bluetoothService; mContext = context; mBluetoothState = bluetoothState; mPasskeyAgentRequestData = new HashMap<String, Integer>(); mAuthorizationAgentRequestData = new HashMap<String, Integer>(); mAdapter = adapter; //WakeLock instantiation in BluetoothEventLoop class PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, TAG); mWakeLock.setReferenceCounted(false); initializeNativeDataNative(); } /*package*/ void getProfileProxy() { mAdapter.getProfileProxy(mContext, mProfileServiceListener, BluetoothProfile.A2DP); mAdapter.getProfileProxy(mContext, mProfileServiceListener, BluetoothProfile.INPUT_DEVICE); } private BluetoothProfile.ServiceListener mProfileServiceListener = new BluetoothProfile.ServiceListener() { public void onServiceConnected(int profile, BluetoothProfile proxy) { if (profile == BluetoothProfile.A2DP) { mA2dp = (BluetoothA2dp) proxy; } } public void onServiceDisconnected(int profile) { if (profile == BluetoothProfile.A2DP) { mA2dp = null; } } }; protected void finalize() throws Throwable { try { cleanupNativeDataNative(); } finally { super.finalize(); } } /* package */ HashMap<String, Integer> getPasskeyAgentRequestData() { return mPasskeyAgentRequestData; } /* package */ HashMap<String, Integer> getAuthorizationAgentRequestData() { return mAuthorizationAgentRequestData; } /* package */ void start() { if (!isEventLoopRunningNative()) { if (DBG) log("Starting Event Loop thread"); startEventLoopNative(); } } public void stop() { if (isEventLoopRunningNative()) { if (DBG) log("Stopping Event Loop thread"); stopEventLoopNative(); } } public boolean isEventLoopRunning() { return isEventLoopRunningNative(); } private void addDevice(String address, String[] properties) { BluetoothDeviceProperties deviceProperties = mBluetoothService.getDeviceProperties(); deviceProperties.addProperties(address, properties); String rssi = deviceProperties.getProperty(address, "RSSI"); String classValue = deviceProperties.getProperty(address, "Class"); String name = deviceProperties.getProperty(address, "Name"); short rssiValue; // For incoming connections, we don't get the RSSI value. Use a default of MIN_VALUE. // If we accept the pairing, we will automatically show it at the top of the list. if (rssi != null) { rssiValue = (short)Integer.valueOf(rssi).intValue(); } else { rssiValue = Short.MIN_VALUE; } if (classValue != null) { Intent intent = new Intent(BluetoothDevice.ACTION_FOUND); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); intent.putExtra(BluetoothDevice.EXTRA_CLASS, new BluetoothClass(Integer.valueOf(classValue))); intent.putExtra(BluetoothDevice.EXTRA_RSSI, rssiValue); intent.putExtra(BluetoothDevice.EXTRA_NAME, name); mContext.sendBroadcast(intent, BLUETOOTH_PERM); } else { log ("ClassValue: " + classValue + " for remote device: " + address + " is null"); } } /** * Called by native code on a DeviceFound signal from org.bluez.Adapter. * * @param address the MAC address of the new device * @param properties an array of property keys and value strings * * @see BluetoothDeviceProperties#addProperties(String, String[]) */ private void onDeviceFound(String address, String[] properties) { if (properties == null) { Log.e(TAG, "ERROR: Remote device properties are null"); return; } addDevice(address, properties); } /** * Called by native code on a DeviceDisappeared signal from * org.bluez.Adapter. * * @param address the MAC address of the disappeared device */ private void onDeviceDisappeared(String address) { Intent intent = new Intent(BluetoothDevice.ACTION_DISAPPEARED); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); mContext.sendBroadcast(intent, BLUETOOTH_PERM); } /** * Called by native code on a DisconnectRequested signal from * org.bluez.Device. * * @param deviceObjectPath the object path for the disconnecting device */ private void onDeviceDisconnectRequested(String deviceObjectPath) { String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); if (address == null) { Log.e(TAG, "onDeviceDisconnectRequested: Address of the remote device in null"); return; } Intent intent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); mContext.sendBroadcast(intent, BLUETOOTH_PERM); } /** * Called by native code for the async response to a CreatePairedDevice * method call to org.bluez.Adapter. * * @param address the MAC address of the device to pair * @param result success or error result for the pairing operation */ private void onCreatePairedDeviceResult(String address, int result) { address = address.toUpperCase(); mBluetoothService.onCreatePairedDeviceResult(address, result); } /** * Called by native code on a DeviceCreated signal from org.bluez.Adapter. * * @param deviceObjectPath the object path for the created device */ private void onDeviceCreated(String deviceObjectPath) { String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); if (!mBluetoothService.isRemoteDeviceInCache(address)) { // Incoming connection, we haven't seen this device, add to cache. String[] properties = mBluetoothService.getRemoteDeviceProperties(address); if (properties != null) { addDevice(address, properties); } } return; } /** * Called by native code on a DeviceRemoved signal from org.bluez.Adapter. * * @param deviceObjectPath the object path for the removed device */ private void onDeviceRemoved(String deviceObjectPath) { String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); if (address != null) { mBluetoothService.setBondState(address.toUpperCase(), BluetoothDevice.BOND_NONE, BluetoothDevice.UNBOND_REASON_REMOVED); mBluetoothService.setRemoteDeviceProperty(address, "UUIDs", null); } } /** * Called by native code on a PropertyChanged signal from * org.bluez.Adapter. This method is also called from * {@link BluetoothAdapterStateMachine} to set the "Pairable" * property when Bluetooth is enabled. * * @param propValues a string array containing the key and one or more * values. */ /*package*/ void onPropertyChanged(String[] propValues) { BluetoothAdapterProperties adapterProperties = mBluetoothService.getAdapterProperties(); if (adapterProperties.isEmpty()) { // We have got a property change before // we filled up our cache. adapterProperties.getAllProperties(); } log("Property Changed: " + propValues[0] + " : " + propValues[1]); String name = propValues[0]; if (name.equals("Name")) { adapterProperties.setProperty(name, propValues[1]); Intent intent = new Intent(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); intent.putExtra(BluetoothAdapter.EXTRA_LOCAL_NAME, propValues[1]); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); mContext.sendBroadcast(intent, BLUETOOTH_PERM); } else if (name.equals("Pairable") || name.equals("Discoverable")) { adapterProperties.setProperty(name, propValues[1]); if (name.equals("Discoverable")) { mBluetoothState.sendMessage(BluetoothAdapterStateMachine.SCAN_MODE_CHANGED); } String pairable = name.equals("Pairable") ? propValues[1] : adapterProperties.getProperty("Pairable"); String discoverable = name.equals("Discoverable") ? propValues[1] : adapterProperties.getProperty("Discoverable"); // This shouldn't happen, unless Adapter Properties are null. if (pairable == null || discoverable == null) return; int mode = BluetoothService.bluezStringToScanMode( pairable.equals("true"), discoverable.equals("true")); if (mode >= 0) { Intent intent = new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); intent.putExtra(BluetoothAdapter.EXTRA_SCAN_MODE, mode); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); mContext.sendBroadcast(intent, BLUETOOTH_PERM); } } else if (name.equals("Discovering")) { Intent intent; adapterProperties.setProperty(name, propValues[1]); if (propValues[1].equals("true")) { intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_STARTED); } else { // Stop the discovery. mBluetoothService.cancelDiscovery(); intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); } mContext.sendBroadcast(intent, BLUETOOTH_PERM); } else if (name.equals("Devices") || name.equals("UUIDs")) { String value = null; int len = Integer.valueOf(propValues[1]); if (len > 0) { StringBuilder str = new StringBuilder(); for (int i = 2; i < propValues.length; i++) { str.append(propValues[i]); str.append(","); } value = str.toString(); } adapterProperties.setProperty(name, value); if (name.equals("UUIDs")) { mBluetoothService.updateBluetoothState(value); } } else if (name.equals("Powered")) { mBluetoothState.sendMessage(BluetoothAdapterStateMachine.POWER_STATE_CHANGED, propValues[1].equals("true") ? new Boolean(true) : new Boolean(false)); } else if (name.equals("DiscoverableTimeout")) { adapterProperties.setProperty(name, propValues[1]); } } /** * Called by native code on a PropertyChanged signal from * org.bluez.Device. * * @param deviceObjectPath the object path for the changed device * @param propValues a string array containing the key and one or more * values. */ private void onDevicePropertyChanged(String deviceObjectPath, String[] propValues) { String name = propValues[0]; String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); if (address == null) { Log.e(TAG, "onDevicePropertyChanged: Address of the remote device in null"); return; } log("Device property changed: " + address + " property: " + name + " value: " + propValues[1]); BluetoothDevice device = mAdapter.getRemoteDevice(address); if (name.equals("Name")) { mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); Intent intent = new Intent(BluetoothDevice.ACTION_NAME_CHANGED); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); intent.putExtra(BluetoothDevice.EXTRA_NAME, propValues[1]); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); mContext.sendBroadcast(intent, BLUETOOTH_PERM); } else if (name.equals("Alias")) { mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); } else if (name.equals("Class")) { mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); Intent intent = new Intent(BluetoothDevice.ACTION_CLASS_CHANGED); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); intent.putExtra(BluetoothDevice.EXTRA_CLASS, new BluetoothClass(Integer.valueOf(propValues[1]))); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); mContext.sendBroadcast(intent, BLUETOOTH_PERM); } else if (name.equals("Connected")) { mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); Intent intent = null; if (propValues[1].equals("true")) { intent = new Intent(BluetoothDevice.ACTION_ACL_CONNECTED); // Set the link timeout to 8000 slots (5 sec timeout) // for bluetooth docks. if (mBluetoothService.isBluetoothDock(address)) { mBluetoothService.setLinkTimeout(address, 8000); } } else { intent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECTED); } intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); mContext.sendBroadcast(intent, BLUETOOTH_PERM); } else if (name.equals("UUIDs")) { String uuid = null; int len = Integer.valueOf(propValues[1]); if (len > 0) { StringBuilder str = new StringBuilder(); for (int i = 2; i < propValues.length; i++) { str.append(propValues[i]); str.append(","); } uuid = str.toString(); } mBluetoothService.setRemoteDeviceProperty(address, name, uuid); // UUIDs have changed, query remote service channel and update cache. mBluetoothService.updateDeviceServiceChannelCache(address); mBluetoothService.sendUuidIntent(address); } else if (name.equals("Paired")) { if (propValues[1].equals("true")) { // If locally initiated pairing, we will // not go to BOND_BONDED state until we have received a // successful return value in onCreatePairedDeviceResult if (null == mBluetoothService.getPendingOutgoingBonding()) { mBluetoothService.setBondState(address, BluetoothDevice.BOND_BONDED); } } else { mBluetoothService.setBondState(address, BluetoothDevice.BOND_NONE); mBluetoothService.setRemoteDeviceProperty(address, "Trusted", "false"); } } else if (name.equals("Trusted")) { if (DBG) log("set trust state succeeded, value is: " + propValues[1]); mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); } } /** * Called by native code on a PropertyChanged signal from * org.bluez.Input. * * @param path the object path for the changed input device * @param propValues a string array containing the key and one or more * values. */ private void onInputDevicePropertyChanged(String path, String[] propValues) { String address = mBluetoothService.getAddressFromObjectPath(path); if (address == null) { Log.e(TAG, "onInputDevicePropertyChanged: Address of the remote device is null"); return; } log("Input Device : Name of Property is: " + propValues[0]); boolean state = false; if (propValues[1].equals("true")) { state = true; } mBluetoothService.handleInputDevicePropertyChange(address, state); } /** * Called by native code on a PropertyChanged signal from * org.bluez.Network. * * @param deviceObjectPath the object path for the changed PAN device * @param propValues a string array containing the key and one or more * values. */ private void onPanDevicePropertyChanged(String deviceObjectPath, String[] propValues) { String name = propValues[0]; String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); if (address == null) { Log.e(TAG, "onPanDevicePropertyChanged: Address of the remote device in null"); return; } if (DBG) { log("Pan Device property changed: " + address + " property: " + name + " value: "+ propValues[1]); } BluetoothDevice device = mAdapter.getRemoteDevice(address); if (name.equals("Connected")) { if (propValues[1].equals("false")) { mBluetoothService.handlePanDeviceStateChange(device, BluetoothPan.STATE_DISCONNECTED, BluetoothPan.LOCAL_PANU_ROLE); } } else if (name.equals("Interface")) { String iface = propValues[1]; if (!iface.equals("")) { mBluetoothService.handlePanDeviceStateChange(device, iface, BluetoothPan.STATE_CONNECTED, BluetoothPan.LOCAL_PANU_ROLE); } } } private String checkPairingRequestAndGetAddress(String objectPath, int nativeData) { String address = mBluetoothService.getAddressFromObjectPath(objectPath); if (address == null) { Log.e(TAG, "Unable to get device address in checkPairingRequestAndGetAddress, " + "returning null"); return null; } address = address.toUpperCase(); mPasskeyAgentRequestData.put(address, new Integer(nativeData)); if (mBluetoothService.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF) { // shutdown path mBluetoothService.cancelPairingUserInput(address); return null; } // Set state to BONDING. For incoming connections it will be set here. // For outgoing connections, it gets set when we call createBond. // Also set it only when the state is not already Bonded, we can sometimes // get an authorization request from the remote end if it doesn't have the link key // while we still have it. if (mBluetoothService.getBondState(address) != BluetoothDevice.BOND_BONDED) mBluetoothService.setBondState(address, BluetoothDevice.BOND_BONDING); return address; } /** * Called by native code on a RequestPairingConsent method call to * org.bluez.Agent. * * @param objectPath the path of the device to request pairing consent for * @param nativeData a native pointer to the original D-Bus message */ private void onRequestPairingConsent(String objectPath, int nativeData) { String address = checkPairingRequestAndGetAddress(objectPath, nativeData); if (address == null) return; /* The link key will not be stored if the incoming request has MITM * protection switched on. Unfortunately, some devices have MITM * switched on even though their capabilities are NoInputNoOutput, * so we may get this request many times. Also if we respond immediately, * the other end is unable to handle it. Delay sending the message. */ if (mBluetoothService.getBondState(address) == BluetoothDevice.BOND_BONDED) { Message message = mHandler.obtainMessage(EVENT_PAIRING_CONSENT_DELAYED_ACCEPT); message.obj = address; mHandler.sendMessageDelayed(message, 1500); return; } // Acquire wakelock during PIN code request to bring up LCD display mWakeLock.acquire(); Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_CONSENT); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); // Release wakelock to allow the LCD to go off after the PIN popup notification. mWakeLock.release(); return; } /** * Called by native code on a RequestConfirmation method call to * org.bluez.Agent. * * @param objectPath the path of the device to confirm the passkey for * @param passkey an integer containing the 6-digit passkey to confirm * @param nativeData a native pointer to the original D-Bus message */ private void onRequestPasskeyConfirmation(String objectPath, int passkey, int nativeData) { String address = checkPairingRequestAndGetAddress(objectPath, nativeData); if (address == null) return; // Acquire wakelock during PIN code request to bring up LCD display mWakeLock.acquire(); Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); intent.putExtra(BluetoothDevice.EXTRA_PAIRING_KEY, passkey); intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); // Release wakelock to allow the LCD to go off after the PIN popup notification. mWakeLock.release(); return; } /** * Called by native code on a RequestPasskey method call to * org.bluez.Agent. * * @param objectPath the path of the device requesting a passkey * @param nativeData a native pointer to the original D-Bus message */ private void onRequestPasskey(String objectPath, int nativeData) { String address = checkPairingRequestAndGetAddress(objectPath, nativeData); if (address == null) return; // Acquire wakelock during PIN code request to bring up LCD display mWakeLock.acquire(); Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_PASSKEY); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); // Release wakelock to allow the LCD to go off after the PIN popup notification. mWakeLock.release(); return; } /** * Called by native code on a RequestPinCode method call to * org.bluez.Agent. * * @param objectPath the path of the device requesting a PIN code * @param nativeData a native pointer to the original D-Bus message */ private void onRequestPinCode(String objectPath, int nativeData) { String address = checkPairingRequestAndGetAddress(objectPath, nativeData); if (address == null) return; String pendingOutgoingAddress = mBluetoothService.getPendingOutgoingBonding(); BluetoothClass btClass = new BluetoothClass(mBluetoothService.getRemoteClass(address)); int btDeviceClass = btClass.getDeviceClass(); if (address.equals(pendingOutgoingAddress)) { // we initiated the bonding // Check if its a dock if (mBluetoothService.isBluetoothDock(address)) { String pin = mBluetoothService.getDockPin(); mBluetoothService.setPin(address, BluetoothDevice.convertPinToBytes(pin)); return; } // try 0000 once if the device looks dumb switch (btDeviceClass) { case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET: case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE: case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES: case BluetoothClass.Device.AUDIO_VIDEO_PORTABLE_AUDIO: case BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO: if (mBluetoothService.attemptAutoPair(address)) return; } } if (btDeviceClass == BluetoothClass.Device.PERIPHERAL_KEYBOARD || btDeviceClass == BluetoothClass.Device.PERIPHERAL_KEYBOARD_POINTING) { // Its a keyboard. Follow the HID spec recommendation of creating the // passkey and displaying it to the user. If the keyboard doesn't follow // the spec recommendation, check if the keyboard has a fixed PIN zero // and pair. if (mBluetoothService.isFixedPinZerosAutoPairKeyboard(address)) { mBluetoothService.setPin(address, BluetoothDevice.convertPinToBytes("0000")); return; } // Generate a variable PIN. This is not truly random but good enough. int pin = (int) Math.floor(Math.random() * 10000); sendDisplayPinIntent(address, pin); return; } // Acquire wakelock during PIN code request to bring up LCD display mWakeLock.acquire(); Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_PIN); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); // Release wakelock to allow the LCD to go off after the PIN popup notification. mWakeLock.release(); return; } /** * Called by native code on a DisplayPasskey method call to * org.bluez.Agent. * * @param objectPath the path of the device to display the passkey for * @param passkey an integer containing the 6-digit passkey * @param nativeData a native pointer to the original D-Bus message */ private void onDisplayPasskey(String objectPath, int passkey, int nativeData) { String address = checkPairingRequestAndGetAddress(objectPath, nativeData); if (address == null) return; // Acquire wakelock during PIN code request to bring up LCD display mWakeLock.acquire(); Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); intent.putExtra(BluetoothDevice.EXTRA_PAIRING_KEY, passkey); intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); //Release wakelock to allow the LCD to go off after the PIN popup notification. mWakeLock.release(); } private void sendDisplayPinIntent(String address, int pin) { // Acquire wakelock during PIN code request to bring up LCD display mWakeLock.acquire(); Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); intent.putExtra(BluetoothDevice.EXTRA_PAIRING_KEY, pin); intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); //Release wakelock to allow the LCD to go off after the PIN popup notifcation. mWakeLock.release(); } /** * Called by native code on a RequestOobData method call to * org.bluez.Agent. * * @param objectPath the path of the device requesting OOB data * @param nativeData a native pointer to the original D-Bus message */ private void onRequestOobData(String objectPath, int nativeData) { String address = checkPairingRequestAndGetAddress(objectPath, nativeData); if (address == null) return; Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); } /** * Called by native code on an Authorize method call to org.bluez.Agent. * * @param objectPath the path of the device requesting to be authorized * @param deviceUuid the UUID of the requesting device * @param nativeData reference for native data */ private void onAgentAuthorize(String objectPath, String deviceUuid, int nativeData) { if (!mBluetoothService.isEnabled()) return; String address = mBluetoothService.getAddressFromObjectPath(objectPath); if (address == null) { Log.e(TAG, "Unable to get device address in onAuthAgentAuthorize"); return; } boolean authorized = false; ParcelUuid uuid = ParcelUuid.fromString(deviceUuid); BluetoothDevice device = mAdapter.getRemoteDevice(address); mAuthorizationAgentRequestData.put(address, new Integer(nativeData)); // Bluez sends the UUID of the local service being accessed, _not_ the // remote service if (mA2dp != null && (BluetoothUuid.isAudioSource(uuid) || BluetoothUuid.isAvrcpTarget(uuid) || BluetoothUuid.isAdvAudioDist(uuid)) && !isOtherSinkInNonDisconnectedState(address)) { authorized = mA2dp.getPriority(device) > BluetoothProfile.PRIORITY_OFF; if (authorized && !BluetoothUuid.isAvrcpTarget(uuid)) { Log.i(TAG, "First check pass for incoming A2DP / AVRCP connection from " + address); // Some headsets try to connect AVCTP before AVDTP - against the recommendation // If AVCTP connection fails, we get stuck in IncomingA2DP state in the state // machine. We don't handle AVCTP signals currently. We only send // intents for AVDTP state changes. We need to handle both of them in // some cases. For now, just don't move to incoming state in this case. mBluetoothService.notifyIncomingA2dpConnection(address, false); } else { Log.i(TAG, "" + authorized + "Incoming A2DP / AVRCP connection from " + address); mA2dp.allowIncomingConnect(device, authorized); mBluetoothService.notifyIncomingA2dpConnection(address, true); } } else if (BluetoothUuid.isInputDevice(uuid)) { // We can have more than 1 input device connected. authorized = mBluetoothService.getInputDevicePriority(device) > BluetoothInputDevice.PRIORITY_OFF; if (authorized) { Log.i(TAG, "First check pass for incoming HID connection from " + address); // notify profile state change mBluetoothService.notifyIncomingHidConnection(address); } else { Log.i(TAG, "Rejecting incoming HID connection from " + address); mBluetoothService.allowIncomingProfileConnect(device, authorized); } } else if (BluetoothUuid.isBnep(uuid)) { // PAN doesn't go to the state machine, accept or reject from here authorized = mBluetoothService.allowIncomingTethering(); mBluetoothService.allowIncomingProfileConnect(device, authorized); } else { Log.i(TAG, "Rejecting incoming " + deviceUuid + " connection from " + address); mBluetoothService.allowIncomingProfileConnect(device, authorized); } log("onAgentAuthorize(" + objectPath + ", " + deviceUuid + ") = " + authorized); } private boolean onAgentOutOfBandDataAvailable(String objectPath) { if (!mBluetoothService.isEnabled()) return false; String address = mBluetoothService.getAddressFromObjectPath(objectPath); if (address == null) return false; if (mBluetoothService.getDeviceOutOfBandData( mAdapter.getRemoteDevice(address)) != null) { return true; } return false; } private boolean isOtherSinkInNonDisconnectedState(String address) { List<BluetoothDevice> devices = mA2dp.getDevicesMatchingConnectionStates(new int[] {BluetoothA2dp.STATE_CONNECTED, BluetoothA2dp.STATE_CONNECTING, BluetoothA2dp.STATE_DISCONNECTING}); if (devices.size() == 0) return false; for (BluetoothDevice dev: devices) { if (!dev.getAddress().equals(address)) return true; } return false; } /** * Called by native code on a Cancel method call to org.bluez.Agent. */ private void onAgentCancel() { Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_CANCEL); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_AGENT_CANCEL), 1500); return; } /** * Called by native code for the async response to a DiscoverServices * method call to org.bluez.Adapter. * * @param deviceObjectPath the path for the specified device * @param result true for success; false on error */ private void onDiscoverServicesResult(String deviceObjectPath, boolean result) { String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); if (address == null) return; // We don't parse the xml here, instead just query Bluez for the properties. if (result) { mBluetoothService.updateRemoteDevicePropertiesCache(address); } mBluetoothService.sendUuidIntent(address); mBluetoothService.makeServiceChannelCallbacks(address); } /** * Called by native code for the async response to a CreateDevice * method call to org.bluez.Adapter. * * @param address the MAC address of the device to create * @param result {@link #CREATE_DEVICE_SUCCESS}, * {@link #CREATE_DEVICE_ALREADY_EXISTS} or {@link #CREATE_DEVICE_FAILED}} */ private void onCreateDeviceResult(String address, int result) { if (DBG) log("Result of onCreateDeviceResult:" + result); switch (result) { case CREATE_DEVICE_ALREADY_EXISTS: String path = mBluetoothService.getObjectPathFromAddress(address); if (path != null) { mBluetoothService.discoverServicesNative(path, ""); break; } Log.w(TAG, "Device exists, but we don't have the bluez path, failing"); // fall-through case CREATE_DEVICE_FAILED: mBluetoothService.sendUuidIntent(address); mBluetoothService.makeServiceChannelCallbacks(address); break; case CREATE_DEVICE_SUCCESS: // nothing to do, UUID intent's will be sent via property changed } } /** * Called by native code for the async response to a Connect * method call to org.bluez.Input. * * @param path the path of the specified input device * @param result Result code of the operation. */ private void onInputDeviceConnectionResult(String path, int result) { // Success case gets handled by Property Change signal if (result != BluetoothInputDevice.INPUT_OPERATION_SUCCESS) { String address = mBluetoothService.getAddressFromObjectPath(path); if (address == null) return; boolean connected = false; BluetoothDevice device = mAdapter.getRemoteDevice(address); int state = mBluetoothService.getInputDeviceConnectionState(device); if (state == BluetoothInputDevice.STATE_CONNECTING) { if (result == BluetoothInputDevice.INPUT_CONNECT_FAILED_ALREADY_CONNECTED) { connected = true; } else { connected = false; } } else if (state == BluetoothInputDevice.STATE_DISCONNECTING) { if (result == BluetoothInputDevice.INPUT_DISCONNECT_FAILED_NOT_CONNECTED) { connected = false; } else { // There is no better way to handle this, this shouldn't happen connected = true; } } else { Log.e(TAG, "Error onInputDeviceConnectionResult. State is:" + state); } mBluetoothService.handleInputDevicePropertyChange(address, connected); } } /** * Called by native code for the async response to a Connect * method call to org.bluez.Network. * * @param path the path of the specified PAN device * @param result Result code of the operation. */ private void onPanDeviceConnectionResult(String path, int result) { log ("onPanDeviceConnectionResult " + path + " " + result); // Success case gets handled by Property Change signal if (result != BluetoothPan.PAN_OPERATION_SUCCESS) { String address = mBluetoothService.getAddressFromObjectPath(path); if (address == null) return; boolean connected = false; BluetoothDevice device = mAdapter.getRemoteDevice(address); int state = mBluetoothService.getPanDeviceConnectionState(device); if (state == BluetoothPan.STATE_CONNECTING) { if (result == BluetoothPan.PAN_CONNECT_FAILED_ALREADY_CONNECTED) { connected = true; } else { connected = false; } } else if (state == BluetoothPan.STATE_DISCONNECTING) { if (result == BluetoothPan.PAN_DISCONNECT_FAILED_NOT_CONNECTED) { connected = false; } else { // There is no better way to handle this, this shouldn't happen connected = true; } } else { Log.e(TAG, "Error onPanDeviceConnectionResult. State is: " + state + " result: "+ result); } int newState = connected? BluetoothPan.STATE_CONNECTED : BluetoothPan.STATE_DISCONNECTED; mBluetoothService.handlePanDeviceStateChange(device, newState, BluetoothPan.LOCAL_PANU_ROLE); } } /** * Called by native code for the async response to a Connect * method call to org.bluez.Health * * @param chanCode The internal id of the channel * @param result Result code of the operation. */ private void onHealthDeviceConnectionResult(int chanCode, int result) { log ("onHealthDeviceConnectionResult " + chanCode + " " + result); // Success case gets handled by Property Change signal if (result != BluetoothHealth.HEALTH_OPERATION_SUCCESS) { mBluetoothService.onHealthDeviceChannelConnectionError(chanCode, BluetoothHealth.STATE_CHANNEL_DISCONNECTED); } } /** * Called by native code on a DeviceDisconnected signal from * org.bluez.NetworkServer. * * @param address the MAC address of the disconnected device */ private void onNetworkDeviceDisconnected(String address) { BluetoothDevice device = mAdapter.getRemoteDevice(address); mBluetoothService.handlePanDeviceStateChange(device, BluetoothPan.STATE_DISCONNECTED, BluetoothPan.LOCAL_NAP_ROLE); } /** * Called by native code on a DeviceConnected signal from * org.bluez.NetworkServer. * * @param address the MAC address of the connected device * @param iface interface of remote network * @param destUuid unused UUID parameter */ private void onNetworkDeviceConnected(String address, String iface, int destUuid) { BluetoothDevice device = mAdapter.getRemoteDevice(address); mBluetoothService.handlePanDeviceStateChange(device, iface, BluetoothPan.STATE_CONNECTED, BluetoothPan.LOCAL_NAP_ROLE); } /** * Called by native code on a PropertyChanged signal from * org.bluez.HealthDevice. * * @param devicePath the object path of the remote device * @param propValues Properties (Name-Value) of the Health Device. */ private void onHealthDevicePropertyChanged(String devicePath, String[] propValues) { log("Health Device : Name of Property is: " + propValues[0] + " Value:" + propValues[1]); mBluetoothService.onHealthDevicePropertyChanged(devicePath, propValues[1]); } /** * Called by native code on a ChannelCreated/Deleted signal from * org.bluez.HealthDevice. * * @param devicePath the object path of the remote device * @param channelPath the path of the health channel. * @param exists Boolean to indicate if the channel was created or deleted. */ private void onHealthDeviceChannelChanged(String devicePath, String channelPath, boolean exists) { log("Health Device : devicePath: " + devicePath + ":channelPath:" + channelPath + ":exists" + exists); mBluetoothService.onHealthDeviceChannelChanged(devicePath, channelPath, exists); } private static void log(String msg) { Log.d(TAG, msg); } private native void initializeNativeDataNative(); private native void startEventLoopNative(); private native void stopEventLoopNative(); private native boolean isEventLoopRunningNative(); private native void cleanupNativeDataNative(); }