/* * 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. */ /** * TODO: Move this to * java/services/com/android/server/BluetoothDeviceService.java * and make the contructor package private again. * * @hide */ package android.server; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothError; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothIntent; import android.bluetooth.IBluetoothDevice; import android.bluetooth.IBluetoothDeviceCallback; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Binder; import android.os.Handler; import android.os.Message; import android.os.RemoteException; import android.os.SystemService; import android.provider.Settings; import android.util.Log; import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Map; public class BluetoothDeviceService extends IBluetoothDevice.Stub { private static final String TAG = "BluetoothDeviceService"; private static final boolean DBG = true; private int mNativeData; private BluetoothEventLoop mEventLoop; private IntentFilter mIntentFilter; private boolean mIsAirplaneSensitive; private final BondState mBondState = new BondState(); // local cache of bondings private volatile boolean mIsEnabled; // local cache of isEnabledNative() private boolean mIsDiscovering; private final Context mContext; private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; static { classInitNative(); } private native static void classInitNative(); public BluetoothDeviceService(Context context) { mContext = context; } /** Must be called after construction, and before any other method. */ public synchronized void init() { initializeNativeDataNative(); mIsEnabled = (isEnabledNative() == 1); if (mIsEnabled) { mBondState.loadBondState(); } mIsDiscovering = false; mEventLoop = new BluetoothEventLoop(mContext, this); registerForAirplaneMode(); } private native void initializeNativeDataNative(); @Override protected void finalize() throws Throwable { if (mIsAirplaneSensitive) { mContext.unregisterReceiver(mReceiver); } try { cleanupNativeDataNative(); } finally { super.finalize(); } } private native void cleanupNativeDataNative(); public boolean isEnabled() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return mIsEnabled; } private native int isEnabledNative(); /** * Bring down bluetooth and disable BT in settings. Returns true on success. */ public boolean disable() { return disable(true); } /** * Bring down bluetooth. Returns true on success. * * @param saveSetting If true, disable BT in settings * */ public synchronized boolean disable(boolean saveSetting) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); if (mEnableThread != null && mEnableThread.isAlive()) { return false; } if (!mIsEnabled) { return true; } mEventLoop.stop(); disableNative(); // mark in progress bondings as cancelled for (String address : mBondState.listInState(BluetoothDevice.BOND_BONDING)) { mBondState.setBondState(address, BluetoothDevice.BOND_NOT_BONDED, BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); } // update mode Intent intent = new Intent(BluetoothIntent.SCAN_MODE_CHANGED_ACTION); intent.putExtra(BluetoothIntent.SCAN_MODE, BluetoothDevice.SCAN_MODE_NONE); mContext.sendBroadcast(intent, BLUETOOTH_PERM); mIsEnabled = false; if (saveSetting) { persistBluetoothOnSetting(false); } mIsDiscovering = false; intent = new Intent(BluetoothIntent.DISABLED_ACTION); mContext.sendBroadcast(intent, BLUETOOTH_PERM); return true; } /** * Bring up bluetooth, asynchronously, and enable BT in settings. * This turns on/off the underlying hardware. * * @return True on success (so far), guaranteeing the callback with be * notified when complete. */ public boolean enable(IBluetoothDeviceCallback callback) { return enable(callback, true); } /** * Enable this Bluetooth device, asynchronously. * This turns on/off the underlying hardware. * * @param saveSetting If true, enable BT in settings * * @return True on success (so far), guaranteeing the callback with be * notified when complete. */ public synchronized boolean enable(IBluetoothDeviceCallback callback, boolean saveSetting) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); // Airplane mode can prevent Bluetooth radio from being turned on. if (mIsAirplaneSensitive && isAirplaneModeOn()) { return false; } if (mIsEnabled) { return false; } if (mEnableThread != null && mEnableThread.isAlive()) { return false; } mEnableThread = new EnableThread(callback, saveSetting); mEnableThread.start(); return true; } private static final int REGISTER_SDP_RECORDS = 1; private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case REGISTER_SDP_RECORDS: //TODO: Don't assume HSP/HFP is running, don't use sdptool, if (isEnabled()) { SystemService.start("hsag"); SystemService.start("hfag"); } } } }; private EnableThread mEnableThread; private class EnableThread extends Thread { private final IBluetoothDeviceCallback mEnableCallback; private final boolean mSaveSetting; public EnableThread(IBluetoothDeviceCallback callback, boolean saveSetting) { mEnableCallback = callback; mSaveSetting = saveSetting; } public void run() { boolean res = (enableNative() == 0); if (res) { mEventLoop.start(); } if (mEnableCallback != null) { try { mEnableCallback.onEnableResult(res ? BluetoothDevice.RESULT_SUCCESS : BluetoothDevice.RESULT_FAILURE); } catch (RemoteException e) {} } if (res) { mIsEnabled = true; if (mSaveSetting) { persistBluetoothOnSetting(true); } mIsDiscovering = false; Intent intent = new Intent(BluetoothIntent.ENABLED_ACTION); mBondState.loadBondState(); mContext.sendBroadcast(intent, BLUETOOTH_PERM); mHandler.sendMessageDelayed(mHandler.obtainMessage(REGISTER_SDP_RECORDS), 3000); // Update mode mEventLoop.onModeChanged(getModeNative()); } mEnableThread = null; } } private void persistBluetoothOnSetting(boolean bluetoothOn) { long origCallerIdentityToken = Binder.clearCallingIdentity(); Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.BLUETOOTH_ON, bluetoothOn ? 1 : 0); Binder.restoreCallingIdentity(origCallerIdentityToken); } private native int enableNative(); private native int disableNative(); /* package */ BondState getBondState() { return mBondState; } /** local cache of bonding state. /* we keep our own state to track the intermediate state BONDING, which /* bluez does not track. * All addreses must be passed in upper case. */ public class BondState { private final HashMap mState = new HashMap(); private final HashMap mPinAttempt = new HashMap(); private final ArrayList mAutoPairingFailures = new ArrayList(); // List of all the vendor_id prefix of Bluetooth addresses for which // auto pairing is not attempted private final ArrayList mAutoPairingBlacklisted = new ArrayList(Arrays.asList( "00:02:C7", "00:16:FE", "00:19:C1", "00:1B:FB", "00:1E:3D", //ALPS "00:21:4F", "00:23:06", "00:24:33", "00:A0:79", // ALPS "00:0E:6D", "00:13:E0", "00:21:E8", "00:60:57"// Murata for Prius 2007 )); public synchronized void loadBondState() { if (!mIsEnabled) { return; } String[] bonds = listBondingsNative(); if (bonds == null) { return; } mState.clear(); if (DBG) log("found " + bonds.length + " bonded devices"); for (String address : bonds) { mState.put(address.toUpperCase(), BluetoothDevice.BOND_BONDED); } } public synchronized void setBondState(String address, int state) { setBondState(address, state, 0); } /** reason is ignored unless state == BOND_NOT_BONDED */ public synchronized void setBondState(String address, int state, int reason) { int oldState = getBondState(address); if (oldState == state) { return; } if (DBG) log(address + " bond state " + oldState + " -> " + state + " (" + reason + ")"); Intent intent = new Intent(BluetoothIntent.BOND_STATE_CHANGED_ACTION); intent.putExtra(BluetoothIntent.ADDRESS, address); intent.putExtra(BluetoothIntent.BOND_STATE, state); intent.putExtra(BluetoothIntent.BOND_PREVIOUS_STATE, oldState); if (state == BluetoothDevice.BOND_NOT_BONDED) { if (reason <= 0) { Log.w(TAG, "setBondState() called to unbond device, but reason code is " + "invalid. Overriding reason code with BOND_RESULT_REMOVED"); reason = BluetoothDevice.UNBOND_REASON_REMOVED; } intent.putExtra(BluetoothIntent.REASON, reason); mState.remove(address); } else { mState.put(address, state); } mContext.sendBroadcast(intent, BLUETOOTH_PERM); } public boolean isAutoPairingBlacklisted(String address) { for (String blacklistAddress : mAutoPairingBlacklisted) { if (address.startsWith(blacklistAddress)) return true; } return false; } public synchronized int getBondState(String address) { Integer state = mState.get(address); if (state == null) { return BluetoothDevice.BOND_NOT_BONDED; } return state.intValue(); } private synchronized String[] listInState(int state) { ArrayList result = new ArrayList(mState.size()); for (Map.Entry e : mState.entrySet()) { if (e.getValue().intValue() == state) { result.add(e.getKey()); } } return result.toArray(new String[result.size()]); } public synchronized void addAutoPairingFailure(String address) { if (!mAutoPairingFailures.contains(address)) { mAutoPairingFailures.add(address); } } public synchronized boolean isAutoPairingAttemptsInProgress(String address) { return getAttempt(address) != 0; } public synchronized void clearPinAttempts(String address) { mPinAttempt.remove(address); } public synchronized boolean hasAutoPairingFailed(String address) { return mAutoPairingFailures.contains(address); } public synchronized int getAttempt(String address) { Integer attempt = mPinAttempt.get(address); if (attempt == null) { return 0; } return attempt.intValue(); } public synchronized void attempt(String address) { Integer attempt = mPinAttempt.get(address); int newAttempt; if (attempt == null) { newAttempt = 1; } else { newAttempt = attempt.intValue() + 1; } mPinAttempt.put(address, new Integer(newAttempt)); } } private native String[] listBondingsNative(); private static String toBondStateString(int bondState) { switch (bondState) { case BluetoothDevice.BOND_NOT_BONDED: return "not bonded"; case BluetoothDevice.BOND_BONDING: return "bonding"; case BluetoothDevice.BOND_BONDED: return "bonded"; default: return "??????"; } } public synchronized String getAddress() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getAddressNative(); } private native String getAddressNative(); public synchronized String getName() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getNameNative(); } private native String getNameNative(); public synchronized boolean setName(String name) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); if (name == null) { return false; } // hcid handles persistance of the bluetooth name return setNameNative(name); } private native boolean setNameNative(String name); /** * Returns the user-friendly name of a remote device. This value is * retrned from our local cache, which is updated during device discovery. * Do not expect to retrieve the updated remote name immediately after * changing the name on the remote device. * * @param address Bluetooth address of remote device. * * @return The user-friendly name of the specified remote device. */ public synchronized String getRemoteName(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } return getRemoteNameNative(address); } private native String getRemoteNameNative(String address); /* pacakge */ native String getAdapterPathNative(); public synchronized boolean startDiscovery(boolean resolveNames) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); return startDiscoveryNative(resolveNames); } private native boolean startDiscoveryNative(boolean resolveNames); public synchronized boolean cancelDiscovery() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); return cancelDiscoveryNative(); } private native boolean cancelDiscoveryNative(); public synchronized boolean isDiscovering() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return mIsDiscovering; } /* package */ void setIsDiscovering(boolean isDiscovering) { mIsDiscovering = isDiscovering; } public synchronized boolean startPeriodicDiscovery() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); return startPeriodicDiscoveryNative(); } private native boolean startPeriodicDiscoveryNative(); public synchronized boolean stopPeriodicDiscovery() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); return stopPeriodicDiscoveryNative(); } private native boolean stopPeriodicDiscoveryNative(); public synchronized boolean isPeriodicDiscovery() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return isPeriodicDiscoveryNative(); } private native boolean isPeriodicDiscoveryNative(); /** * Set the discoverability window for the device. A timeout of zero * makes the device permanently discoverable (if the device is * discoverable). Setting the timeout to a nonzero value does not make * a device discoverable; you need to call setMode() to make the device * explicitly discoverable. * * @param timeout_s The discoverable timeout in seconds. */ public synchronized boolean setDiscoverableTimeout(int timeout) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); return setDiscoverableTimeoutNative(timeout); } private native boolean setDiscoverableTimeoutNative(int timeout_s); /** * Get the discoverability window for the device. A timeout of zero * means that the device is permanently discoverable (if the device is * in the discoverable mode). * * @return The discoverability window of the device, in seconds. A negative * value indicates an error. */ public synchronized int getDiscoverableTimeout() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getDiscoverableTimeoutNative(); } private native int getDiscoverableTimeoutNative(); public synchronized boolean isAclConnected(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } return isConnectedNative(address); } private native boolean isConnectedNative(String address); public synchronized int getScanMode() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return bluezStringToScanMode(getModeNative()); } private native String getModeNative(); public synchronized boolean setScanMode(int mode) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); String bluezMode = scanModeToBluezString(mode); if (bluezMode != null) { return setModeNative(bluezMode); } return false; } private native boolean setModeNative(String mode); public synchronized boolean disconnectRemoteDeviceAcl(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } return disconnectRemoteDeviceNative(address); } private native boolean disconnectRemoteDeviceNative(String address); public synchronized boolean createBond(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } address = address.toUpperCase(); // Check for bond state only if we are not performing auto // pairing exponential back-off attempts. if (!mBondState.isAutoPairingAttemptsInProgress(address) && mBondState.getBondState(address) != BluetoothDevice.BOND_NOT_BONDED) { return false; } if (!createBondingNative(address, 60000 /* 1 minute */)) { return false; } mBondState.setBondState(address, BluetoothDevice.BOND_BONDING); return true; } private native boolean createBondingNative(String address, int timeout_ms); public synchronized boolean cancelBondProcess(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } address = address.toUpperCase(); if (mBondState.getBondState(address) != BluetoothDevice.BOND_BONDING) { return false; } mBondState.setBondState(address, BluetoothDevice.BOND_NOT_BONDED, BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); cancelBondingProcessNative(address); return true; } private native boolean cancelBondingProcessNative(String address); public synchronized boolean removeBond(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } return removeBondingNative(address); } private native boolean removeBondingNative(String address); public synchronized String[] listBonds() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return mBondState.listInState(BluetoothDevice.BOND_BONDED); } public synchronized int getBondState(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return BluetoothError.ERROR; } return mBondState.getBondState(address.toUpperCase()); } public synchronized String[] listAclConnections() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return listConnectionsNative(); } private native String[] listConnectionsNative(); /** * This method lists all remote devices that this adapter is aware of. * This is a list not only of all most-recently discovered devices, but of * all devices discovered by this adapter up to some point in the past. * Note that many of these devices may not be in the neighborhood anymore, * and attempting to connect to them will result in an error. * * @return An array of strings representing the Bluetooth addresses of all * remote devices that this adapter is aware of. */ public synchronized String[] listRemoteDevices() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return listRemoteDevicesNative(); } private native String[] listRemoteDevicesNative(); /** * Returns the version of the Bluetooth chip. This version is compiled from * the LMP version. In case of EDR the features attribute must be checked. * Example: "Bluetooth 2.0 + EDR". * * @return a String representation of the this Adapter's underlying * Bluetooth-chip version. */ public synchronized String getVersion() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getVersionNative(); } private native String getVersionNative(); /** * Returns the revision of the Bluetooth chip. This is a vendor-specific * value and in most cases it represents the firmware version. This might * derive from the HCI revision and LMP subversion values or via extra * vendord specific commands. * In case the revision of a chip is not available. This method should * return the LMP subversion value as a string. * Example: "HCI 19.2" * * @return The HCI revision of this adapter. */ public synchronized String getRevision() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getRevisionNative(); } private native String getRevisionNative(); /** * Returns the manufacturer of the Bluetooth chip. If the company id is not * known the sting "Company ID %d" where %d should be replaced with the * numeric value from the manufacturer field. * Example: "Cambridge Silicon Radio" * * @return Manufacturer name. */ public synchronized String getManufacturer() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getManufacturerNative(); } private native String getManufacturerNative(); /** * Returns the company name from the OUI database of the Bluetooth device * address. This function will need a valid and up-to-date oui.txt from * the IEEE. This value will be different from the manufacturer string in * the most cases. * If the oui.txt file is not present or the OUI part of the Bluetooth * address is not listed, it should return the string "OUI %s" where %s is * the actual OUI. * * Example: "Apple Computer" * * @return company name */ public synchronized String getCompany() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return getCompanyNative(); } private native String getCompanyNative(); /** * Like getVersion(), but for a remote device. * * @param address The Bluetooth address of the remote device. * * @return remote-device Bluetooth version * * @see #getVersion */ public synchronized String getRemoteVersion(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } return getRemoteVersionNative(address); } private native String getRemoteVersionNative(String address); /** * Like getRevision(), but for a remote device. * * @param address The Bluetooth address of the remote device. * * @return remote-device HCI revision * * @see #getRevision */ public synchronized String getRemoteRevision(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } return getRemoteRevisionNative(address); } private native String getRemoteRevisionNative(String address); /** * Like getManufacturer(), but for a remote device. * * @param address The Bluetooth address of the remote device. * * @return remote-device Bluetooth chip manufacturer * * @see #getManufacturer */ public synchronized String getRemoteManufacturer(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } return getRemoteManufacturerNative(address); } private native String getRemoteManufacturerNative(String address); /** * Like getCompany(), but for a remote device. * * @param address The Bluetooth address of the remote device. * * @return remote-device company * * @see #getCompany */ public synchronized String getRemoteCompany(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } return getRemoteCompanyNative(address); } private native String getRemoteCompanyNative(String address); /** * Returns the date and time when the specified remote device has been seen * by a discover procedure. * Example: "2006-02-08 12:00:00 GMT" * * @return a String with the timestamp. */ public synchronized String lastSeen(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } return lastSeenNative(address); } private native String lastSeenNative(String address); /** * Returns the date and time when the specified remote device has last been * connected to * Example: "2006-02-08 12:00:00 GMT" * * @return a String with the timestamp. */ public synchronized String lastUsed(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } return lastUsedNative(address); } private native String lastUsedNative(String address); /** * Gets the remote major, minor, and service classes encoded as a 32-bit * integer. * * Note: this value is retrieved from cache, because we get it during * remote-device discovery. * * @return 32-bit integer encoding the remote major, minor, and service * classes. * * @see #getRemoteMajorClass * @see #getRemoteMinorClass * @see #getRemoteServiceClasses */ public synchronized int getRemoteClass(String address) { if (!BluetoothDevice.checkBluetoothAddress(address)) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return -1; } return getRemoteClassNative(address); } private native int getRemoteClassNative(String address); /** * Gets the remote features encoded as bit mask. * * Note: This method may be obsoleted soon. * * @return byte array of features. */ public synchronized byte[] getRemoteFeatures(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } return getRemoteFeaturesNative(address); } private native byte[] getRemoteFeaturesNative(String address); /** * This method and {@link #getRemoteServiceRecord} query the SDP service * on a remote device. They do not interpret the data, but simply return * it raw to the user. To read more about SDP service handles and records, * consult the Bluetooth core documentation (www.bluetooth.com). * * @param address Bluetooth address of remote device. * @param match a String match to narrow down the service-handle search. * The only supported value currently is "hsp" for the headset * profile. To retrieve all service handles, simply pass an empty * match string. * * @return all service handles corresponding to the string match. * * @see #getRemoteServiceRecord */ public synchronized int[] getRemoteServiceHandles(String address, String match) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } if (match == null) { match = ""; } return getRemoteServiceHandlesNative(address, match); } private native int[] getRemoteServiceHandlesNative(String address, String match); /** * This method retrieves the service records corresponding to a given * service handle (method {@link #getRemoteServiceHandles} retrieves the * service handles.) * * This method and {@link #getRemoteServiceHandles} do not interpret their * data, but simply return it raw to the user. To read more about SDP * service handles and records, consult the Bluetooth core documentation * (www.bluetooth.com). * * @param address Bluetooth address of remote device. * @param handle Service handle returned by {@link #getRemoteServiceHandles} * * @return a byte array of all service records corresponding to the * specified service handle. * * @see #getRemoteServiceHandles */ public synchronized byte[] getRemoteServiceRecord(String address, int handle) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return null; } return getRemoteServiceRecordNative(address, handle); } private native byte[] getRemoteServiceRecordNative(String address, int handle); private static final int MAX_OUTSTANDING_ASYNC = 32; // AIDL does not yet support short's public synchronized boolean getRemoteServiceChannel(String address, int uuid16, IBluetoothDeviceCallback callback) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } HashMap callbacks = mEventLoop.getRemoteServiceChannelCallbacks(); if (callbacks.containsKey(address)) { Log.w(TAG, "SDP request already in progress for " + address); return false; } // Protect from malicious clients - only allow 32 bonding requests per minute. if (callbacks.size() > MAX_OUTSTANDING_ASYNC) { Log.w(TAG, "Too many outstanding SDP requests, dropping request for " + address); return false; } callbacks.put(address, callback); if (!getRemoteServiceChannelNative(address, (short)uuid16)) { callbacks.remove(address); return false; } return true; } private native boolean getRemoteServiceChannelNative(String address, short uuid16); public synchronized boolean setPin(String address, byte[] pin) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); if (pin == null || pin.length <= 0 || pin.length > 16 || !BluetoothDevice.checkBluetoothAddress(address)) { return false; } address = address.toUpperCase(); Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); if (data == null) { Log.w(TAG, "setPin(" + address + ") called but no native data available, " + "ignoring. Maybe the PasskeyAgent Request was cancelled by the remote device" + " or by bluez.\n"); return false; } // bluez API wants pin as a string String pinString; try { pinString = new String(pin, "UTF8"); } catch (UnsupportedEncodingException uee) { Log.e(TAG, "UTF8 not supported?!?"); return false; } return setPinNative(address, pinString, data.intValue()); } private native boolean setPinNative(String address, String pin, int nativeData); public synchronized boolean cancelPin(String address) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); if (!BluetoothDevice.checkBluetoothAddress(address)) { return false; } address = address.toUpperCase(); Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); if (data == null) { Log.w(TAG, "cancelPin(" + address + ") called but no native data available, " + "ignoring. Maybe the PasskeyAgent Request was already cancelled by the remote " + "or by bluez.\n"); return false; } return cancelPinNative(address, data.intValue()); } private native boolean cancelPinNative(String address, int natveiData); private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) { ContentResolver resolver = context.getContentResolver(); // Query the airplane mode from Settings.System just to make sure that // some random app is not sending this intent and disabling bluetooth boolean enabled = !isAirplaneModeOn(); // If bluetooth is currently expected to be on, then enable or disable bluetooth if (Settings.Secure.getInt(resolver, Settings.Secure.BLUETOOTH_ON, 0) > 0) { if (enabled) { enable(null, false); } else { disable(false); } } } } }; private void registerForAirplaneMode() { String airplaneModeRadios = Settings.System.getString(mContext.getContentResolver(), Settings.System.AIRPLANE_MODE_RADIOS); mIsAirplaneSensitive = airplaneModeRadios == null ? true : airplaneModeRadios.contains(Settings.System.RADIO_BLUETOOTH); if (mIsAirplaneSensitive) { mIntentFilter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); mContext.registerReceiver(mReceiver, mIntentFilter); } } /* Returns true if airplane mode is currently on */ private final boolean isAirplaneModeOn() { return Settings.System.getInt(mContext.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) == 1; } @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mIsEnabled) { pw.println("\nBluetooth ENABLED: " + getAddress() + " (" + getName() + ")"); pw.println("\nisDiscovering() = " + isDiscovering()); BluetoothHeadset headset = new BluetoothHeadset(mContext, null); String[] addresses = listRemoteDevices(); pw.println("\n--Known devices--"); for (String address : addresses) { pw.printf("%s %10s (%d) %s\n", address, toBondStateString(mBondState.getBondState(address)), mBondState.getAttempt(address), getRemoteName(address)); } addresses = listAclConnections(); pw.println("\n--ACL connected devices--"); for (String address : addresses) { pw.println(address); } // Rather not do this from here, but no-where else and I need this // dump pw.println("\n--Headset Service--"); switch (headset.getState()) { case BluetoothHeadset.STATE_DISCONNECTED: pw.println("getState() = STATE_DISCONNECTED"); break; case BluetoothHeadset.STATE_CONNECTING: pw.println("getState() = STATE_CONNECTING"); break; case BluetoothHeadset.STATE_CONNECTED: pw.println("getState() = STATE_CONNECTED"); break; case BluetoothHeadset.STATE_ERROR: pw.println("getState() = STATE_ERROR"); break; } pw.println("getHeadsetAddress() = " + headset.getHeadsetAddress()); headset.close(); } else { pw.println("\nBluetooth DISABLED"); } pw.println("\nmIsAirplaneSensitive = " + mIsAirplaneSensitive); } /* package */ static int bluezStringToScanMode(String mode) { if (mode == null) { return BluetoothError.ERROR; } mode = mode.toLowerCase(); if (mode.equals("off")) { return BluetoothDevice.SCAN_MODE_NONE; } else if (mode.equals("connectable")) { return BluetoothDevice.SCAN_MODE_CONNECTABLE; } else if (mode.equals("discoverable")) { return BluetoothDevice.SCAN_MODE_CONNECTABLE_DISCOVERABLE; } else { return BluetoothError.ERROR; } } /* package */ static String scanModeToBluezString(int mode) { switch (mode) { case BluetoothDevice.SCAN_MODE_NONE: return "off"; case BluetoothDevice.SCAN_MODE_CONNECTABLE: return "connectable"; case BluetoothDevice.SCAN_MODE_CONNECTABLE_DISCOVERABLE: return "discoverable"; } return null; } private static void log(String msg) { Log.d(TAG, msg); } }