/* * 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.BluetoothUuid; import android.os.ParcelUuid; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.Message; import android.util.Log; import java.util.HashMap; /** * TODO: Move this to * java/services/com/android/server/BluetoothEventLoop.java * and make the contructor package private again. * * @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 BluetoothService mBluetoothService; private final BluetoothAdapter mAdapter; private final Context mContext; private static final int EVENT_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY = 1; private static final int EVENT_RESTART_BLUETOOTH = 2; private static final int EVENT_PAIRING_CONSENT_DELAYED_ACCEPT = 3; private static final int EVENT_AGENT_CANCEL = 4; 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; // The time (in millisecs) to delay the pairing attempt after the first // auto pairing attempt fails. We use an exponential delay with // INIT_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY as the initial value and // MAX_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY as the max value. private static final long INIT_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY = 3000; private static final long MAX_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY = 12000; 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_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY: address = (String)msg.obj; if (address != null) { mBluetoothService.createBond(address); return; } break; case EVENT_RESTART_BLUETOOTH: mBluetoothService.restart(); break; 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.getBondState().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.getBondState().setBondState(address, BluetoothDevice.BOND_NONE, BluetoothDevice.UNBOND_REASON_REMOTE_AUTH_CANCELED); break; } } }; static { classInitNative(); } private static native void classInitNative(); /* pacakge */ BluetoothEventLoop(Context context, BluetoothAdapter adapter, BluetoothService bluetoothService) { mBluetoothService = bluetoothService; mContext = context; mPasskeyAgentRequestData = new HashMap(); mAdapter = adapter; initializeNativeDataNative(); } protected void finalize() throws Throwable { try { cleanupNativeDataNative(); } finally { super.finalize(); } } /* package */ HashMap<String, Integer> getPasskeyAgentRequestData() { return mPasskeyAgentRequestData; } /* 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) { mBluetoothService.addRemoteDeviceProperties(address, properties); String rssi = mBluetoothService.getRemoteDeviceProperty(address, "RSSI"); String classValue = mBluetoothService.getRemoteDeviceProperty(address, "Class"); String name = mBluetoothService.getRemoteDeviceProperty(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"); } } private void onDeviceFound(String address, String[] properties) { if (properties == null) { Log.e(TAG, "ERROR: Remote device properties are null"); return; } addDevice(address, properties); } 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); } 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); } private void onCreatePairedDeviceResult(String address, int result) { address = address.toUpperCase(); if (result == BluetoothDevice.BOND_SUCCESS) { mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_BONDED); if (mBluetoothService.getBondState().isAutoPairingAttemptsInProgress(address)) { mBluetoothService.getBondState().clearPinAttempts(address); } } else if (result == BluetoothDevice.UNBOND_REASON_AUTH_FAILED && mBluetoothService.getBondState().getAttempt(address) == 1) { mBluetoothService.getBondState().addAutoPairingFailure(address); pairingAttempt(address, result); } else if (result == BluetoothDevice.UNBOND_REASON_REMOTE_DEVICE_DOWN && mBluetoothService.getBondState().isAutoPairingAttemptsInProgress(address)) { pairingAttempt(address, result); } else { mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_NONE, result); if (mBluetoothService.getBondState().isAutoPairingAttemptsInProgress(address)) { mBluetoothService.getBondState().clearPinAttempts(address); } } } private void pairingAttempt(String address, int result) { // This happens when our initial guess of "0000" as the pass key // fails. Try to create the bond again and display the pin dialog // to the user. Use back-off while posting the delayed // message. The initial value is // INIT_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY and the max value is // MAX_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY. If the max value is // reached, display an error to the user. int attempt = mBluetoothService.getBondState().getAttempt(address); if (attempt * INIT_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY > MAX_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY) { mBluetoothService.getBondState().clearPinAttempts(address); mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_NONE, result); return; } Message message = mHandler.obtainMessage(EVENT_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY); message.obj = address; boolean postResult = mHandler.sendMessageDelayed(message, attempt * INIT_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY); if (!postResult) { mBluetoothService.getBondState().clearPinAttempts(address); mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_NONE, result); return; } mBluetoothService.getBondState().attempt(address); } 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; } private void onDeviceRemoved(String deviceObjectPath) { String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); if (address != null) mBluetoothService.getBondState().setBondState(address.toUpperCase(), BluetoothDevice.BOND_NONE, BluetoothDevice.UNBOND_REASON_REMOVED); } /*package*/ void onPropertyChanged(String[] propValues) { if (mBluetoothService.isAdapterPropertiesEmpty()) { // We have got a property change before // we filled up our cache. mBluetoothService.getAllProperties(); } String name = propValues[0]; if (name.equals("Name")) { Intent intent = new Intent(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); intent.putExtra(BluetoothAdapter.EXTRA_LOCAL_NAME, propValues[1]); mContext.sendBroadcast(intent, BLUETOOTH_PERM); mBluetoothService.setProperty(name, propValues[1]); } else if (name.equals("Pairable") || name.equals("Discoverable")) { String pairable = name.equals("Pairable") ? propValues[1] : mBluetoothService.getProperty("Pairable"); String discoverable = name.equals("Discoverable") ? propValues[1] : mBluetoothService.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); } mBluetoothService.setProperty(name, propValues[1]); } else if (name.equals("Discovering")) { Intent intent; if (propValues[1].equals("true")) { mBluetoothService.setIsDiscovering(true); intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_STARTED); } else { // Stop the discovery. mBluetoothService.cancelDiscovery(); mBluetoothService.setIsDiscovering(false); intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); } mContext.sendBroadcast(intent, BLUETOOTH_PERM); mBluetoothService.setProperty(name, propValues[1]); } else if (name.equals("Devices")) { 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(); } mBluetoothService.setProperty(name, value); } else if (name.equals("Powered")) { // bluetoothd has restarted, re-read all our properties. // Note: bluez only sends this property change when it restarts. if (propValues[1].equals("true")) onRestartRequired(); } } 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; } if (DBG) { log("Device property changed:" + address + "property:" + name); } BluetoothDevice device = mAdapter.getRemoteDevice(address); if (name.equals("Name")) { Intent intent = new Intent(BluetoothDevice.ACTION_NAME_CHANGED); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); intent.putExtra(BluetoothDevice.EXTRA_NAME, propValues[1]); mContext.sendBroadcast(intent, BLUETOOTH_PERM); mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); } else if (name.equals("Class")) { 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]))); mContext.sendBroadcast(intent, BLUETOOTH_PERM); mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); } else if (name.equals("Connected")) { Intent intent = null; if (propValues[1].equals("true")) { intent = new Intent(BluetoothDevice.ACTION_ACL_CONNECTED); } else { intent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECTED); } intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); mContext.sendBroadcast(intent, BLUETOOTH_PERM); mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); } 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")) { mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_BONDED); } else { mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_NONE); mBluetoothService.setRemoteDeviceProperty(address, "Trusted", "false"); } } else if (name.equals("Trusted")) { if (DBG) log("set trust state succeded, value is " + propValues[1]); mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); } } 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().getBondState(address) != BluetoothDevice.BOND_BONDED) mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_BONDING); return address; } 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().getBondState(address) == BluetoothDevice.BOND_BONDED) { Message message = mHandler.obtainMessage(EVENT_PAIRING_CONSENT_DELAYED_ACCEPT); message.obj = address; mHandler.sendMessageDelayed(message, 1500); 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_CONSENT); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); return; } private void onRequestPasskeyConfirmation(String objectPath, int passkey, 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_PASSKEY, passkey); intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); return; } private void onRequestPasskey(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_PASSKEY); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); return; } private void onRequestPinCode(String objectPath, int nativeData) { String address = checkPairingRequestAndGetAddress(objectPath, nativeData); if (address == null) return; String pendingOutgoingAddress = mBluetoothService.getBondState().getPendingOutgoingBonding(); if (address.equals(pendingOutgoingAddress)) { // we initiated the bonding BluetoothClass btClass = new BluetoothClass(mBluetoothService.getRemoteClass(address)); // try 0000 once if the device looks dumb switch (btClass.getDeviceClass()) { 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_CAR_AUDIO: case BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO: if (!mBluetoothService.getBondState().hasAutoPairingFailed(address) && !mBluetoothService.getBondState().isAutoPairingBlacklisted(address)) { mBluetoothService.getBondState().attempt(address); mBluetoothService.setPin(address, BluetoothDevice.convertPinToBytes("0000")); 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_PIN); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); return; } private void onDisplayPasskey(String objectPath, int passkey, 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_PASSKEY, passkey); intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); } private boolean onAgentAuthorize(String objectPath, String deviceUuid) { String address = mBluetoothService.getAddressFromObjectPath(objectPath); if (address == null) { Log.e(TAG, "Unable to get device address in onAuthAgentAuthorize"); return false; } boolean authorized = false; ParcelUuid uuid = ParcelUuid.fromString(deviceUuid); // Bluez sends the UUID of the local service being accessed, _not_ the // remote service if (mBluetoothService.isEnabled() && (BluetoothUuid.isAudioSource(uuid) || BluetoothUuid.isAvrcpTarget(uuid) || BluetoothUuid.isAdvAudioDist(uuid))) { BluetoothA2dp a2dp = new BluetoothA2dp(mContext); BluetoothDevice device = mAdapter.getRemoteDevice(address); authorized = a2dp.getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF; if (authorized) { Log.i(TAG, "Allowing incoming A2DP / AVRCP connection from " + address); } else { Log.i(TAG, "Rejecting incoming A2DP / AVRCP connection from " + address); } } else { Log.i(TAG, "Rejecting incoming " + deviceUuid + " connection from " + address); } log("onAgentAuthorize(" + objectPath + ", " + deviceUuid + ") = " + authorized); return authorized; } 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; } private void onDiscoverServicesResult(String deviceObjectPath, boolean result) { String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); // 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); } 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 dont 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 } } private void onRestartRequired() { if (mBluetoothService.isEnabled()) { Log.e(TAG, "*** A serious error occured (did bluetoothd crash?) - " + "restarting Bluetooth ***"); mHandler.sendEmptyMessage(EVENT_RESTART_BLUETOOTH); } } 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(); }