2 * Copyright (C) 2008 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package android.server;
19 import android.bluetooth.BluetoothA2dp;
20 import android.bluetooth.BluetoothAdapter;
21 import android.bluetooth.BluetoothClass;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothUuid;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.os.Handler;
27 import android.os.Message;
28 import android.os.ParcelUuid;
29 import android.util.Log;
31 import java.util.HashMap;
36 * java/services/com/android/server/BluetoothEventLoop.java
37 * and make the constructor package private again.
41 class BluetoothEventLoop {
42 private static final String TAG = "BluetoothEventLoop";
43 private static final boolean DBG = false;
45 private int mNativeData;
46 private Thread mThread;
47 private boolean mStarted;
48 private boolean mInterrupted;
50 private final HashMap<String, Integer> mPasskeyAgentRequestData;
51 private final BluetoothService mBluetoothService;
52 private final BluetoothAdapter mAdapter;
53 private final Context mContext;
55 private static final int EVENT_RESTART_BLUETOOTH = 1;
56 private static final int EVENT_PAIRING_CONSENT_DELAYED_ACCEPT = 2;
57 private static final int EVENT_AGENT_CANCEL = 3;
59 private static final int CREATE_DEVICE_ALREADY_EXISTS = 1;
60 private static final int CREATE_DEVICE_SUCCESS = 0;
61 private static final int CREATE_DEVICE_FAILED = -1;
63 private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN;
64 private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH;
66 private final Handler mHandler = new Handler() {
68 public void handleMessage(Message msg) {
69 String address = null;
71 case EVENT_RESTART_BLUETOOTH:
72 mBluetoothService.restart();
74 case EVENT_PAIRING_CONSENT_DELAYED_ACCEPT:
75 address = (String)msg.obj;
76 if (address != null) {
77 mBluetoothService.setPairingConfirmation(address, true);
80 case EVENT_AGENT_CANCEL:
81 // Set the Bond State to BOND_NONE.
82 // We always have only 1 device in BONDING state.
83 String[] devices = mBluetoothService.listInState(BluetoothDevice.BOND_BONDING);
84 if (devices.length == 0) {
86 } else if (devices.length > 1) {
87 Log.e(TAG, " There is more than one device in the Bonding State");
91 mBluetoothService.setBondState(address,
92 BluetoothDevice.BOND_NONE,
93 BluetoothDevice.UNBOND_REASON_REMOTE_AUTH_CANCELED);
99 static { classInitNative(); }
100 private static native void classInitNative();
102 /* package */ BluetoothEventLoop(Context context, BluetoothAdapter adapter,
103 BluetoothService bluetoothService) {
104 mBluetoothService = bluetoothService;
106 mPasskeyAgentRequestData = new HashMap();
108 initializeNativeDataNative();
111 protected void finalize() throws Throwable {
113 cleanupNativeDataNative();
119 /* package */ HashMap<String, Integer> getPasskeyAgentRequestData() {
120 return mPasskeyAgentRequestData;
123 /* package */ void start() {
125 if (!isEventLoopRunningNative()) {
126 if (DBG) log("Starting Event Loop thread");
127 startEventLoopNative();
132 if (isEventLoopRunningNative()) {
133 if (DBG) log("Stopping Event Loop thread");
134 stopEventLoopNative();
138 public boolean isEventLoopRunning() {
139 return isEventLoopRunningNative();
142 private void addDevice(String address, String[] properties) {
143 mBluetoothService.addRemoteDeviceProperties(address, properties);
144 String rssi = mBluetoothService.getRemoteDeviceProperty(address, "RSSI");
145 String classValue = mBluetoothService.getRemoteDeviceProperty(address, "Class");
146 String name = mBluetoothService.getRemoteDeviceProperty(address, "Name");
148 // For incoming connections, we don't get the RSSI value. Use a default of MIN_VALUE.
149 // If we accept the pairing, we will automatically show it at the top of the list.
151 rssiValue = (short)Integer.valueOf(rssi).intValue();
153 rssiValue = Short.MIN_VALUE;
155 if (classValue != null) {
156 Intent intent = new Intent(BluetoothDevice.ACTION_FOUND);
157 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address));
158 intent.putExtra(BluetoothDevice.EXTRA_CLASS,
159 new BluetoothClass(Integer.valueOf(classValue)));
160 intent.putExtra(BluetoothDevice.EXTRA_RSSI, rssiValue);
161 intent.putExtra(BluetoothDevice.EXTRA_NAME, name);
163 mContext.sendBroadcast(intent, BLUETOOTH_PERM);
165 log ("ClassValue: " + classValue + " for remote device: " + address + " is null");
169 private void onDeviceFound(String address, String[] properties) {
170 if (properties == null) {
171 Log.e(TAG, "ERROR: Remote device properties are null");
174 addDevice(address, properties);
177 private void onDeviceDisappeared(String address) {
178 Intent intent = new Intent(BluetoothDevice.ACTION_DISAPPEARED);
179 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address));
180 mContext.sendBroadcast(intent, BLUETOOTH_PERM);
183 private void onDeviceDisconnectRequested(String deviceObjectPath) {
184 String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath);
185 if (address == null) {
186 Log.e(TAG, "onDeviceDisconnectRequested: Address of the remote device in null");
189 Intent intent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED);
190 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address));
191 mContext.sendBroadcast(intent, BLUETOOTH_PERM);
194 private void onCreatePairedDeviceResult(String address, int result) {
195 address = address.toUpperCase();
196 mBluetoothService.onCreatePairedDeviceResult(address, result);
199 private void onDeviceCreated(String deviceObjectPath) {
200 String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath);
201 if (!mBluetoothService.isRemoteDeviceInCache(address)) {
202 // Incoming connection, we haven't seen this device, add to cache.
203 String[] properties = mBluetoothService.getRemoteDeviceProperties(address);
204 if (properties != null) {
205 addDevice(address, properties);
211 private void onDeviceRemoved(String deviceObjectPath) {
212 String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath);
213 if (address != null) {
214 mBluetoothService.setBondState(address.toUpperCase(), BluetoothDevice.BOND_NONE,
215 BluetoothDevice.UNBOND_REASON_REMOVED);
216 mBluetoothService.setRemoteDeviceProperty(address, "UUIDs", null);
220 /*package*/ void onPropertyChanged(String[] propValues) {
221 if (mBluetoothService.isAdapterPropertiesEmpty()) {
222 // We have got a property change before
223 // we filled up our cache.
224 mBluetoothService.getAllProperties();
226 String name = propValues[0];
227 if (name.equals("Name")) {
228 mBluetoothService.setProperty(name, propValues[1]);
229 Intent intent = new Intent(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED);
230 intent.putExtra(BluetoothAdapter.EXTRA_LOCAL_NAME, propValues[1]);
231 mContext.sendBroadcast(intent, BLUETOOTH_PERM);
232 } else if (name.equals("Pairable") || name.equals("Discoverable")) {
233 String pairable = name.equals("Pairable") ? propValues[1] :
234 mBluetoothService.getPropertyInternal("Pairable");
235 String discoverable = name.equals("Discoverable") ? propValues[1] :
236 mBluetoothService.getPropertyInternal("Discoverable");
238 // This shouldn't happen, unless Adapter Properties are null.
239 if (pairable == null || discoverable == null)
242 mBluetoothService.setProperty(name, propValues[1]);
243 int mode = BluetoothService.bluezStringToScanMode(
244 pairable.equals("true"),
245 discoverable.equals("true"));
247 Intent intent = new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
248 intent.putExtra(BluetoothAdapter.EXTRA_SCAN_MODE, mode);
249 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
250 mContext.sendBroadcast(intent, BLUETOOTH_PERM);
252 } else if (name.equals("Discovering")) {
254 mBluetoothService.setProperty(name, propValues[1]);
255 if (propValues[1].equals("true")) {
256 mBluetoothService.setIsDiscovering(true);
257 intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
259 // Stop the discovery.
260 mBluetoothService.cancelDiscovery();
261 mBluetoothService.setIsDiscovering(false);
262 intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
264 mContext.sendBroadcast(intent, BLUETOOTH_PERM);
265 } else if (name.equals("Devices")) {
267 int len = Integer.valueOf(propValues[1]);
269 StringBuilder str = new StringBuilder();
270 for (int i = 2; i < propValues.length; i++) {
271 str.append(propValues[i]);
274 value = str.toString();
276 mBluetoothService.setProperty(name, value);
277 } else if (name.equals("Powered")) {
278 // bluetoothd has restarted, re-read all our properties.
279 // Note: bluez only sends this property change when it restarts.
280 if (propValues[1].equals("true"))
285 private void onDevicePropertyChanged(String deviceObjectPath, String[] propValues) {
286 String name = propValues[0];
287 String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath);
288 if (address == null) {
289 Log.e(TAG, "onDevicePropertyChanged: Address of the remote device in null");
293 log("Device property changed:" + address + "property:" + name);
295 BluetoothDevice device = mAdapter.getRemoteDevice(address);
296 if (name.equals("Name")) {
297 mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]);
298 Intent intent = new Intent(BluetoothDevice.ACTION_NAME_CHANGED);
299 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
300 intent.putExtra(BluetoothDevice.EXTRA_NAME, propValues[1]);
301 mContext.sendBroadcast(intent, BLUETOOTH_PERM);
302 } else if (name.equals("Class")) {
303 mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]);
304 Intent intent = new Intent(BluetoothDevice.ACTION_CLASS_CHANGED);
305 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
306 intent.putExtra(BluetoothDevice.EXTRA_CLASS,
307 new BluetoothClass(Integer.valueOf(propValues[1])));
308 mContext.sendBroadcast(intent, BLUETOOTH_PERM);
309 } else if (name.equals("Connected")) {
310 mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]);
311 Intent intent = null;
312 if (propValues[1].equals("true")) {
313 intent = new Intent(BluetoothDevice.ACTION_ACL_CONNECTED);
314 // Set the link timeout to 8000 slots (5 sec timeout)
315 // for bluetooth docks.
316 if (mBluetoothService.isBluetoothDock(address)) {
317 mBluetoothService.setLinkTimeout(address, 8000);
320 intent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECTED);
322 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
323 mContext.sendBroadcast(intent, BLUETOOTH_PERM);
324 } else if (name.equals("UUIDs")) {
326 int len = Integer.valueOf(propValues[1]);
328 StringBuilder str = new StringBuilder();
329 for (int i = 2; i < propValues.length; i++) {
330 str.append(propValues[i]);
333 uuid = str.toString();
335 mBluetoothService.setRemoteDeviceProperty(address, name, uuid);
337 // UUIDs have changed, query remote service channel and update cache.
338 mBluetoothService.updateDeviceServiceChannelCache(address);
340 mBluetoothService.sendUuidIntent(address);
341 } else if (name.equals("Paired")) {
342 if (propValues[1].equals("true")) {
343 // If locally initiated pairing, we will
344 // not go to BOND_BONDED state until we have received a
345 // successful return value in onCreatePairedDeviceResult
346 if (null == mBluetoothService.getPendingOutgoingBonding()) {
347 mBluetoothService.setBondState(address, BluetoothDevice.BOND_BONDED);
350 mBluetoothService.setBondState(address, BluetoothDevice.BOND_NONE);
351 mBluetoothService.setRemoteDeviceProperty(address, "Trusted", "false");
353 } else if (name.equals("Trusted")) {
355 log("set trust state succeded, value is " + propValues[1]);
356 mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]);
360 private String checkPairingRequestAndGetAddress(String objectPath, int nativeData) {
361 String address = mBluetoothService.getAddressFromObjectPath(objectPath);
362 if (address == null) {
363 Log.e(TAG, "Unable to get device address in checkPairingRequestAndGetAddress, " +
367 address = address.toUpperCase();
368 mPasskeyAgentRequestData.put(address, new Integer(nativeData));
370 if (mBluetoothService.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF) {
372 mBluetoothService.cancelPairingUserInput(address);
375 // Set state to BONDING. For incoming connections it will be set here.
376 // For outgoing connections, it gets set when we call createBond.
377 // Also set it only when the state is not already Bonded, we can sometimes
378 // get an authorization request from the remote end if it doesn't have the link key
379 // while we still have it.
380 if (mBluetoothService.getBondState(address) != BluetoothDevice.BOND_BONDED)
381 mBluetoothService.setBondState(address, BluetoothDevice.BOND_BONDING);
385 private void onRequestPairingConsent(String objectPath, int nativeData) {
386 String address = checkPairingRequestAndGetAddress(objectPath, nativeData);
387 if (address == null) return;
389 /* The link key will not be stored if the incoming request has MITM
390 * protection switched on. Unfortunately, some devices have MITM
391 * switched on even though their capabilities are NoInputNoOutput,
392 * so we may get this request many times. Also if we respond immediately,
393 * the other end is unable to handle it. Delay sending the message.
395 if (mBluetoothService.getBondState(address) == BluetoothDevice.BOND_BONDED) {
396 Message message = mHandler.obtainMessage(EVENT_PAIRING_CONSENT_DELAYED_ACCEPT);
397 message.obj = address;
398 mHandler.sendMessageDelayed(message, 1500);
402 Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST);
403 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address));
404 intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT,
405 BluetoothDevice.PAIRING_VARIANT_CONSENT);
406 mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM);
410 private void onRequestPasskeyConfirmation(String objectPath, int passkey, int nativeData) {
411 String address = checkPairingRequestAndGetAddress(objectPath, nativeData);
412 if (address == null) return;
414 Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST);
415 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address));
416 intent.putExtra(BluetoothDevice.EXTRA_PASSKEY, passkey);
417 intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT,
418 BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION);
419 mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM);
423 private void onRequestPasskey(String objectPath, int nativeData) {
424 String address = checkPairingRequestAndGetAddress(objectPath, nativeData);
425 if (address == null) return;
427 Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST);
428 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address));
429 intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT,
430 BluetoothDevice.PAIRING_VARIANT_PASSKEY);
431 mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM);
435 private void onRequestPinCode(String objectPath, int nativeData) {
436 String address = checkPairingRequestAndGetAddress(objectPath, nativeData);
437 if (address == null) return;
439 String pendingOutgoingAddress =
440 mBluetoothService.getPendingOutgoingBonding();
441 if (address.equals(pendingOutgoingAddress)) {
442 // we initiated the bonding
444 // Check if its a dock
445 if (mBluetoothService.isBluetoothDock(address)) {
446 String pin = mBluetoothService.getDockPin();
447 mBluetoothService.setPin(address, BluetoothDevice.convertPinToBytes(pin));
451 BluetoothClass btClass = new BluetoothClass(mBluetoothService.getRemoteClass(address));
453 // try 0000 once if the device looks dumb
454 switch (btClass.getDeviceClass()) {
455 case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
456 case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE:
457 case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES:
458 case BluetoothClass.Device.AUDIO_VIDEO_PORTABLE_AUDIO:
459 case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO:
460 case BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO:
461 if (mBluetoothService.attemptAutoPair(address)) return;
464 Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST);
465 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address));
466 intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_PIN);
467 mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM);
471 private void onDisplayPasskey(String objectPath, int passkey, int nativeData) {
472 String address = checkPairingRequestAndGetAddress(objectPath, nativeData);
473 if (address == null) return;
475 Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST);
476 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address));
477 intent.putExtra(BluetoothDevice.EXTRA_PASSKEY, passkey);
478 intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT,
479 BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY);
480 mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM);
483 private void onRequestOobData(String objectPath , int nativeData) {
484 String address = checkPairingRequestAndGetAddress(objectPath, nativeData);
485 if (address == null) return;
487 Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST);
488 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address));
489 intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT,
490 BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT);
491 mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM);
494 private boolean onAgentAuthorize(String objectPath, String deviceUuid) {
495 String address = mBluetoothService.getAddressFromObjectPath(objectPath);
496 if (address == null) {
497 Log.e(TAG, "Unable to get device address in onAuthAgentAuthorize");
501 boolean authorized = false;
502 ParcelUuid uuid = ParcelUuid.fromString(deviceUuid);
503 BluetoothA2dp a2dp = new BluetoothA2dp(mContext);
505 // Bluez sends the UUID of the local service being accessed, _not_ the
507 if (mBluetoothService.isEnabled() &&
508 (BluetoothUuid.isAudioSource(uuid) || BluetoothUuid.isAvrcpTarget(uuid)
509 || BluetoothUuid.isAdvAudioDist(uuid)) &&
510 !isOtherSinkInNonDisconnectingState(address)) {
511 BluetoothDevice device = mAdapter.getRemoteDevice(address);
512 authorized = a2dp.getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF;
514 Log.i(TAG, "Allowing incoming A2DP / AVRCP connection from " + address);
515 // Some headsets try to connect AVCTP before AVDTP - against the recommendation
516 // If AVCTP connection fails, we get stuck in IncomingA2DP state in the state
517 // machine. We don't handle AVCTP signals currently. We only send
518 // intents for AVDTP state changes. We need to handle both of them in
519 // some cases. For now, just don't move to incoming state in this case.
520 if (!BluetoothUuid.isAvrcpTarget(uuid)) {
521 mBluetoothService.notifyIncomingA2dpConnection(address);
524 Log.i(TAG, "Rejecting incoming A2DP / AVRCP connection from " + address);
527 Log.i(TAG, "Rejecting incoming " + deviceUuid + " connection from " + address);
529 log("onAgentAuthorize(" + objectPath + ", " + deviceUuid + ") = " + authorized);
533 private boolean onAgentOutOfBandDataAvailable(String objectPath) {
534 if (!mBluetoothService.isEnabled()) return false;
536 String address = mBluetoothService.getAddressFromObjectPath(objectPath);
537 if (address == null) return false;
539 if (mBluetoothService.getDeviceOutOfBandData(
540 mAdapter.getRemoteDevice(address)) != null) {
547 private boolean isOtherSinkInNonDisconnectingState(String address) {
548 BluetoothA2dp a2dp = new BluetoothA2dp(mContext);
549 Set<BluetoothDevice> devices = a2dp.getNonDisconnectedSinks();
550 if (devices.size() == 0) return false;
551 for(BluetoothDevice dev: devices) {
552 if (!dev.getAddress().equals(address)) return true;
557 private void onAgentCancel() {
558 Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_CANCEL);
559 mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM);
561 mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_AGENT_CANCEL),
567 private void onDiscoverServicesResult(String deviceObjectPath, boolean result) {
568 String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath);
569 // We don't parse the xml here, instead just query Bluez for the properties.
571 mBluetoothService.updateRemoteDevicePropertiesCache(address);
573 mBluetoothService.sendUuidIntent(address);
574 mBluetoothService.makeServiceChannelCallbacks(address);
577 private void onCreateDeviceResult(String address, int result) {
578 if (DBG) log("Result of onCreateDeviceResult:" + result);
581 case CREATE_DEVICE_ALREADY_EXISTS:
582 String path = mBluetoothService.getObjectPathFromAddress(address);
584 mBluetoothService.discoverServicesNative(path, "");
587 Log.w(TAG, "Device exists, but we dont have the bluez path, failing");
589 case CREATE_DEVICE_FAILED:
590 mBluetoothService.sendUuidIntent(address);
591 mBluetoothService.makeServiceChannelCallbacks(address);
593 case CREATE_DEVICE_SUCCESS:
594 // nothing to do, UUID intent's will be sent via property changed
598 private void onRestartRequired() {
599 if (mBluetoothService.isEnabled()) {
600 Log.e(TAG, "*** A serious error occurred (did bluetoothd crash?) - " +
601 "restarting Bluetooth ***");
602 mHandler.sendEmptyMessage(EVENT_RESTART_BLUETOOTH);
606 private static void log(String msg) {
610 private native void initializeNativeDataNative();
611 private native void startEventLoopNative();
612 private native void stopEventLoopNative();
613 private native boolean isEventLoopRunningNative();
614 private native void cleanupNativeDataNative();