/* * 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.BluetoothClass; 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.IBinder; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; 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.Iterator; import java.util.Map; import com.android.internal.app.IBatteryStats; 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 int mBluetoothState; private boolean mRestart = false; // need to call enable() after disable() private final BondState mBondState = new BondState(); // local cache of bondings private boolean mIsDiscovering; private final IBatteryStats mBatteryStats; 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; private static final int MESSAGE_REGISTER_SDP_RECORDS = 1; private static final int MESSAGE_FINISH_DISABLE = 2; static { classInitNative(); } private native static void classInitNative(); public BluetoothDeviceService(Context context) { mContext = context; // Need to do this in place of: // mBatteryStats = BatteryStatsService.getService(); // Since we can not import BatteryStatsService from here. This class really needs to be // moved to java/services/com/android/server/ mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService("batteryinfo")); } /** Must be called after construction, and before any other method. */ public synchronized void init() { initializeNativeDataNative(); if (isEnabledNative() == 1) { Log.w(TAG, "Bluetooth daemons already running - runtime restart? "); disableNative(); } setBluetoothState(BluetoothDevice.BLUETOOTH_STATE_OFF); 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 mBluetoothState == BluetoothDevice.BLUETOOTH_STATE_ON; } private native int isEnabledNative(); public int getBluetoothState() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); return mBluetoothState; } /** * 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"); switch (mBluetoothState) { case BluetoothDevice.BLUETOOTH_STATE_OFF: return true; case BluetoothDevice.BLUETOOTH_STATE_ON: break; default: return false; } if (mEnableThread != null && mEnableThread.isAlive()) { return false; } setBluetoothState(BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF); // Allow 3 seconds for profiles to gracefully disconnect // TODO: Introduce a callback mechanism so that each profile can notify // BluetoothDeviceService when it is done shutting down mHandler.sendMessageDelayed( mHandler.obtainMessage(MESSAGE_FINISH_DISABLE, saveSetting ? 1 : 0, 0), 3000); return true; } private synchronized void finishDisable(boolean saveSetting) { if (mBluetoothState != BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF) { return; } 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); } // Remove remoteServiceChannelCallbacks HashMap<String, IBluetoothDeviceCallback> callbacksMap = mEventLoop.getRemoteServiceChannelCallbacks(); for (Iterator<String> i = callbacksMap.keySet().iterator(); i.hasNext();) { String address = i.next(); IBluetoothDeviceCallback callback = callbacksMap.get(address); try { callback.onGetRemoteServiceChannelResult(address, BluetoothError.ERROR_DISABLED); } catch (RemoteException e) {} i.remove(); } // 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); mIsDiscovering = false; if (saveSetting) { persistBluetoothOnSetting(false); } setBluetoothState(BluetoothDevice.BLUETOOTH_STATE_OFF); // Log bluetooth off to battery stats. long ident = Binder.clearCallingIdentity(); try { mBatteryStats.noteBluetoothOff(); } catch (RemoteException e) { } finally { Binder.restoreCallingIdentity(ident); } if (mRestart) { mRestart = false; enable(); } } /** Bring up BT and persist BT on in settings */ public boolean enable() { return enable(true); } /** * Enable this Bluetooth device, asynchronously. * This turns on/off the underlying hardware. * * @param saveSetting If true, persist the new state of BT in settings * @return True on success (so far) */ public synchronized boolean enable(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 (mBluetoothState != BluetoothDevice.BLUETOOTH_STATE_OFF) { return false; } if (mEnableThread != null && mEnableThread.isAlive()) { return false; } setBluetoothState(BluetoothDevice.BLUETOOTH_STATE_TURNING_ON); mEnableThread = new EnableThread(saveSetting); mEnableThread.start(); return true; } /** Forcibly restart Bluetooth if it is on */ /* package */ synchronized void restart() { if (mBluetoothState != BluetoothDevice.BLUETOOTH_STATE_ON) { return; } mRestart = true; if (!disable(false)) { mRestart = false; } } private synchronized void setBluetoothState(int state) { if (state == mBluetoothState) { return; } if (DBG) log("Bluetooth state " + mBluetoothState + " -> " + state); Intent intent = new Intent(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION); intent.putExtra(BluetoothIntent.BLUETOOTH_PREVIOUS_STATE, mBluetoothState); intent.putExtra(BluetoothIntent.BLUETOOTH_STATE, state); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); mBluetoothState = state; mContext.sendBroadcast(intent, BLUETOOTH_PERM); } private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_REGISTER_SDP_RECORDS: //TODO: Don't assume HSP/HFP is running, don't use sdptool, if (isEnabled()) { SystemService.start("hsag"); SystemService.start("hfag"); } break; case MESSAGE_FINISH_DISABLE: finishDisable(msg.arg1 != 0); break; } } }; private EnableThread mEnableThread; private class EnableThread extends Thread { private final boolean mSaveSetting; public EnableThread(boolean saveSetting) { mSaveSetting = saveSetting; } public void run() { boolean res = (enableNative() == 0); if (res) { int retryCount = 2; boolean running = false; while ((retryCount-- > 0) && !running) { mEventLoop.start(); // it may take a momement for the other thread to do its // thing. Check periodically for a while. int pollCount = 5; while ((pollCount-- > 0) && !running) { if (mEventLoop.isEventLoopRunning()) { running = true; break; } try { Thread.sleep(100); } catch (InterruptedException e) {} } } if (!running) { log("bt EnableThread giving up"); res = false; disableNative(); } } if (res) { if (mSaveSetting) { persistBluetoothOnSetting(true); } mIsDiscovering = false; mBondState.loadBondState(); mHandler.sendMessageDelayed(mHandler.obtainMessage(MESSAGE_REGISTER_SDP_RECORDS), 3000); // Log bluetooth on to battery stats. long ident = Binder.clearCallingIdentity(); try { mBatteryStats.noteBluetoothOn(); } catch (RemoteException e) { } finally { Binder.restoreCallingIdentity(ident); } } mEnableThread = null; setBluetoothState(res ? BluetoothDevice.BLUETOOTH_STATE_ON : BluetoothDevice.BLUETOOTH_STATE_OFF); if (res) { // Update mode mEventLoop.onModeChanged(getModeNative()); } } } 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<String, Integer> mState = new HashMap<String, Integer>(); private final HashMap<String, Integer> mPinAttempt = new HashMap<String, Integer>(); private final ArrayList<String> mAutoPairingFailures = new ArrayList<String>(); // List of all the vendor_id prefix of Bluetooth addresses for // which auto pairing is not attempted. // The following companies are included in the list below: // ALPS (lexus), Murata (Prius 2007, Nokia 616), TEMIC SDS (Porsche, Audi), // Parrot, Zhongshan General K-mate Electronics, Great Well // Electronics, Flaircomm Electronics, Jatty Electronics, Delphi, // Clarion, Novero, Denso (Lexus, Toyota), Johnson Controls (Acura), // Continental Automotive, Harman/Becker private final ArrayList<String> mAutoPairingBlacklisted = new ArrayList<String>(Arrays.asList( "00:02:C7", "00:16:FE", "00:19:C1", "00:1B:FB", "00:1E:3D", "00:21:4F", "00:23:06", "00:24:33", "00:A0:79", "00:0E:6D", "00:13:E0", "00:21:E8", "00:60:57", "00:0E:9F", "00:12:1C", "00:18:91", "00:18:96", "00:13:04", "00:16:FD", "00:22:A0", "00:0B:4C", "00:60:6F", "00:23:3D", "00:C0:59", "00:0A:30", "00:1E:AE", "00:1C:D7" )); public synchronized void loadBondState() { if (mBluetoothState != BluetoothDevice.BLUETOOTH_STATE_TURNING_ON) { 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<String> result = new ArrayList<String>(mState.size()); for (Map.Entry<String, Integer> 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(); String[] bonding = mBondState.listInState(BluetoothDevice.BOND_BONDING); if (bonding.length > 0 && !bonding[0].equals(address)) { log("Ignoring createBond(): another device is bonding"); // a different device is currently bonding, fail return false; } // 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) { log("Ignoring createBond(): this device is already bonding or 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 BluetoothClass.ERROR; } 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<String, IBluetoothDeviceCallback> 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(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) { pw.println("\nmIsAirplaneSensitive = " + mIsAirplaneSensitive + "\n"); switch(mBluetoothState) { case BluetoothDevice.BLUETOOTH_STATE_OFF: pw.println("\nBluetooth OFF\n"); return; case BluetoothDevice.BLUETOOTH_STATE_TURNING_ON: pw.println("\nBluetooth TURNING ON\n"); return; case BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF: pw.println("\nBluetooth TURNING OFF\n"); return; case BluetoothDevice.BLUETOOTH_STATE_ON: pw.println("\nBluetooth ON\n"); } pw.println("\nLocal address = " + getAddress()); pw.println("\nLocal name = " + 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(); } /* 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); } }