1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 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 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 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. 15 */ 16 17 package com.googlecode.android_scripting.facade.bluetooth; 18 19 import android.app.Service; 20 import android.bluetooth.BluetoothActivityEnergyInfo; 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.os.BatteryStats; 28 import android.os.Bundle; 29 import android.os.ParcelUuid; 30 import android.os.SynchronousResultReceiver; 31 32 import com.googlecode.android_scripting.Log; 33 import com.googlecode.android_scripting.MainThread; 34 import com.googlecode.android_scripting.facade.EventFacade; 35 import com.googlecode.android_scripting.facade.FacadeManager; 36 import com.googlecode.android_scripting.jsonrpc.RpcReceiver; 37 import com.googlecode.android_scripting.rpc.Rpc; 38 import com.googlecode.android_scripting.rpc.RpcDefault; 39 import com.googlecode.android_scripting.rpc.RpcOptional; 40 import com.googlecode.android_scripting.rpc.RpcParameter; 41 42 import java.time.Duration; 43 import java.util.Collection; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Set; 48 import java.util.concurrent.Callable; 49 import java.util.concurrent.ConcurrentHashMap; 50 import java.util.concurrent.Executor; 51 import java.util.concurrent.TimeoutException; 52 53 /** 54 * Basic Bluetooth functions. 55 */ 56 public class BluetoothFacade extends RpcReceiver { 57 private final Service mService; 58 private final BroadcastReceiver mDiscoveryReceiver; 59 private final IntentFilter discoveryFilter; 60 private final EventFacade mEventFacade; 61 private final BluetoothStateReceiver mStateReceiver; 62 private static final Object mReceiverLock = new Object(); 63 private BluetoothStateReceiver mMultiStateReceiver; 64 private final BleStateReceiver mBleStateReceiver; 65 private Map<String, BluetoothConnection> connections = 66 new HashMap<String, BluetoothConnection>(); 67 private BluetoothAdapter mBluetoothAdapter; 68 69 public static ConcurrentHashMap<String, BluetoothDevice> DiscoveredDevices; 70 BluetoothFacade(FacadeManager manager)71 public BluetoothFacade(FacadeManager manager) { 72 super(manager); 73 mBluetoothAdapter = MainThread.run(manager.getService(), 74 new Callable<BluetoothAdapter>() { 75 @Override 76 public BluetoothAdapter call() throws Exception { 77 return BluetoothAdapter.getDefaultAdapter(); 78 } 79 }); 80 mEventFacade = manager.getReceiver(EventFacade.class); 81 mService = manager.getService(); 82 83 DiscoveredDevices = new ConcurrentHashMap<String, BluetoothDevice>(); 84 discoveryFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND); 85 discoveryFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); 86 mDiscoveryReceiver = new DiscoveryCacheReceiver(); 87 mStateReceiver = new BluetoothStateReceiver(); 88 mMultiStateReceiver = null; 89 mBleStateReceiver = new BleStateReceiver(); 90 } 91 92 class DiscoveryCacheReceiver extends BroadcastReceiver { 93 @Override onReceive(Context context, Intent intent)94 public void onReceive(Context context, Intent intent) { 95 String action = intent.getAction(); 96 if (action.equals(BluetoothDevice.ACTION_FOUND)) { 97 BluetoothDevice device = intent.getParcelableExtra( 98 BluetoothDevice.EXTRA_DEVICE); 99 Log.d("Found device " + device.getAlias()); 100 if (!DiscoveredDevices.containsKey(device.getAddress())) { 101 String name = device.getAlias(); 102 if (name != null) { 103 DiscoveredDevices.put(device.getAlias(), device); 104 } 105 DiscoveredDevices.put(device.getAddress(), device); 106 } 107 } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) { 108 mEventFacade.postEvent("BluetoothDiscoveryFinished", new Bundle()); 109 mService.unregisterReceiver(mDiscoveryReceiver); 110 } 111 } 112 } 113 114 class BluetoothStateReceiver extends BroadcastReceiver { 115 116 private final boolean mIsMultiBroadcast; 117 BluetoothStateReceiver()118 public BluetoothStateReceiver() { 119 mIsMultiBroadcast = false; 120 } 121 BluetoothStateReceiver(boolean isMultiBroadcast)122 public BluetoothStateReceiver(boolean isMultiBroadcast) { 123 mIsMultiBroadcast = isMultiBroadcast; 124 } 125 126 @Override onReceive(Context context, Intent intent)127 public void onReceive(Context context, Intent intent) { 128 String action = intent.getAction(); 129 if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { 130 final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); 131 Bundle msg = new Bundle(); 132 if (state == BluetoothAdapter.STATE_ON) { 133 msg.putString("State", "ON"); 134 mEventFacade.postEvent("BluetoothStateChangedOn", msg.clone()); 135 if (!mIsMultiBroadcast) { 136 mService.unregisterReceiver(mStateReceiver); 137 } 138 } else if(state == BluetoothAdapter.STATE_OFF) { 139 msg.putString("State", "OFF"); 140 mEventFacade.postEvent("BluetoothStateChangedOff", msg.clone()); 141 if (!mIsMultiBroadcast) { 142 mService.unregisterReceiver(mStateReceiver); 143 } 144 } 145 } 146 } 147 } 148 149 class BleStateReceiver extends BroadcastReceiver { 150 151 @Override onReceive(Context context, Intent intent)152 public void onReceive(Context context, Intent intent) { 153 String action = intent.getAction(); 154 if (action.equals(BluetoothAdapter.ACTION_BLE_STATE_CHANGED)) { 155 int state = mBluetoothAdapter.getLeState(); 156 if (state == BluetoothAdapter.STATE_BLE_ON) { 157 mEventFacade.postEvent("BleStateChangedOn", new Bundle()); 158 mService.unregisterReceiver(mBleStateReceiver); 159 } else if (state == BluetoothAdapter.STATE_OFF) { 160 mEventFacade.postEvent("BleStateChangedOff", new Bundle()); 161 mService.unregisterReceiver(mBleStateReceiver); 162 } 163 } 164 } 165 } 166 167 deviceMatch(BluetoothDevice device, String deviceID)168 public static boolean deviceMatch(BluetoothDevice device, String deviceID) { 169 return deviceID.equals(device.getAlias()) || deviceID.equals( 170 device.getAddress()); 171 } 172 173 /** 174 * Get Bluetooth device. 175 * @param devices - HashMap of Device Address and Bluetooth device name. 176 * @param device - name of the device. 177 * @return the device name if it exits. 178 */ getDevice( ConcurrentHashMap<String, T> devices, String device)179 public static <T> BluetoothDevice getDevice( 180 ConcurrentHashMap<String, T> devices, String device) 181 throws Exception { 182 if (devices.containsKey(device)) { 183 return (BluetoothDevice) devices.get(device); 184 } else { 185 throw new Exception("Can't find device " + device); 186 } 187 } 188 189 /** 190 * Get Bluetooth device. 191 * @param devices - Collection of device IDs. 192 * @param deviceID - ID of the desired device. 193 * @return the Bluetooth device if the device ID is matched. 194 */ getDevice( Collection<BluetoothDevice> devices, String deviceID)195 public static BluetoothDevice getDevice( 196 Collection<BluetoothDevice> devices, String deviceID) 197 throws Exception { 198 Log.d("Looking for " + deviceID); 199 for (BluetoothDevice bd : devices) { 200 Log.d(bd.getAlias() + " " + bd.getAddress()); 201 if (deviceMatch(bd, deviceID)) { 202 Log.d("Found match " + bd.getAlias() + " " + bd.getAddress()); 203 return bd; 204 } 205 } 206 throw new Exception("Can't find device " + deviceID); 207 } 208 209 /** 210 * Verify device existence. 211 * @param devices - Collection of device IDs. 212 * @param deviceID - ID of the desired device. 213 * @return if the device Exists or not. 214 */ deviceExists( Collection<BluetoothDevice> devices, String deviceID)215 public static boolean deviceExists( 216 Collection<BluetoothDevice> devices, String deviceID) { 217 for (BluetoothDevice bd : devices) { 218 if (deviceMatch(bd, deviceID)) { 219 Log.d("Found match " + bd.getAlias() + " " + bd.getAddress()); 220 return true; 221 } 222 } 223 return false; 224 } 225 226 @Rpc(description = "Requests that the device be made connectable.") bluetoothMakeConnectable()227 public void bluetoothMakeConnectable() { 228 mBluetoothAdapter 229 .setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); 230 } 231 232 @Rpc(description = "Requests that the device be discoverable for Bluetooth connections.") bluetoothMakeDiscoverable( @pcParametername = "duration", description = "period of time, in milliseconds," + "during which the device should be discoverable") @pcDefault"300000") Long duration)233 public void bluetoothMakeDiscoverable( 234 @RpcParameter(name = "duration", 235 description = "period of time, in milliseconds," 236 + "during which the device should be discoverable") 237 @RpcDefault("300000") 238 Long duration) { 239 Duration finalDuration = Duration.ofMillis(duration); 240 if (finalDuration.toSeconds() <= Integer.MAX_VALUE) { 241 mBluetoothAdapter.setDiscoverableTimeout(finalDuration); 242 mBluetoothAdapter.setScanMode( 243 BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); 244 Log.d("Making discoverable for " + duration + " milliseconds.\n"); 245 } else { 246 Log.e("setScanMode: Duration in seconds outside of the bounds of an int"); 247 throw new IllegalArgumentException("Duration not in bounds. In seconds, the " 248 + "duration must be in the range of an int"); 249 } 250 } 251 252 @Rpc(description = "Requests that the device be not discoverable.") bluetoothMakeUndiscoverable()253 public void bluetoothMakeUndiscoverable() { 254 Log.d("Making undiscoverable\n"); 255 mBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_NONE); 256 } 257 258 @Rpc(description = "Queries a remote device for it's name or null if it can't be resolved") bluetoothGetRemoteDeviceName( @pcParametername = "address", description = "Bluetooth Address For Target Device") String address)259 public String bluetoothGetRemoteDeviceName( 260 @RpcParameter(name = "address", description = "Bluetooth Address For Target Device") 261 String address) { 262 try { 263 BluetoothDevice mDevice; 264 mDevice = mBluetoothAdapter.getRemoteDevice(address); 265 return mDevice.getName(); 266 } catch (Exception e) { 267 return null; 268 } 269 } 270 271 @Rpc(description = "Fetch UUIDS with SDP") bluetoothFetchUuidsWithSdp( @pcParametername = "address", description = "Bluetooth Address For Target Device") String address)272 public boolean bluetoothFetchUuidsWithSdp( 273 @RpcParameter(name = "address", description = "Bluetooth Address For Target Device") 274 String address) { 275 try { 276 BluetoothDevice mDevice; 277 mDevice = mBluetoothAdapter.getRemoteDevice(address); 278 return mDevice.fetchUuidsWithSdp(); 279 } catch (Exception e) { 280 return false; 281 } 282 } 283 284 @Rpc(description = "Get local Bluetooth device name") bluetoothGetLocalName()285 public String bluetoothGetLocalName() { 286 return mBluetoothAdapter.getName(); 287 } 288 289 @Rpc(description = "Sets the Bluetooth visible device name", returns = "true on success") bluetoothSetLocalName( @pcParametername = "name", description = "New local name") String name)290 public boolean bluetoothSetLocalName( 291 @RpcParameter(name = "name", description = "New local name") 292 String name) { 293 return mBluetoothAdapter.setName(name); 294 } 295 296 @Rpc(description = "Returns the hardware address of the local Bluetooth adapter. ") bluetoothGetLocalAddress()297 public String bluetoothGetLocalAddress() { 298 return mBluetoothAdapter.getAddress(); 299 } 300 301 @Rpc(description = "Returns the UUIDs supported by local Bluetooth adapter.") bluetoothGetLocalUuids()302 public ParcelUuid[] bluetoothGetLocalUuids() { 303 List<ParcelUuid> uuidsList = mBluetoothAdapter.getUuidsList(); 304 ParcelUuid[] uuidsArray = new ParcelUuid[uuidsList.size()]; 305 uuidsList.toArray(uuidsArray); 306 return uuidsArray; 307 } 308 309 @Rpc(description = "Gets the scan mode for the local dongle.\r\n" + "Return values:\r\n" 310 + "\t-1 when Bluetooth is disabled.\r\n" 311 + "\t0 if non discoverable and non connectable.\r\n" 312 + "\r1 connectable non discoverable." + "\r3 connectable and discoverable.") bluetoothGetScanMode()313 public int bluetoothGetScanMode() { 314 if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF 315 || mBluetoothAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF) { 316 return -1; 317 } 318 switch (mBluetoothAdapter.getScanMode()) { 319 case BluetoothAdapter.SCAN_MODE_NONE: 320 return 0; 321 case BluetoothAdapter.SCAN_MODE_CONNECTABLE: 322 return 1; 323 case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE: 324 return 3; 325 default: 326 return mBluetoothAdapter.getScanMode() - 20; 327 } 328 } 329 330 @Rpc(description = "Return the set of BluetoothDevice that are paired to the local adapter.") bluetoothGetBondedDevices()331 public Set<BluetoothDevice> bluetoothGetBondedDevices() { 332 return mBluetoothAdapter.getBondedDevices(); 333 } 334 335 @Rpc(description = "Checks Bluetooth state.", returns = "True if Bluetooth is enabled.") bluetoothCheckState()336 public Boolean bluetoothCheckState() { 337 return mBluetoothAdapter.isEnabled(); 338 } 339 340 @Rpc(description = "Factory reset bluetooth settings.", returns = "True if successful.") bluetoothFactoryReset()341 public boolean bluetoothFactoryReset() { 342 return mBluetoothAdapter.clearBluetooth(); 343 } 344 345 @Rpc(description = "Toggle Bluetooth on and off.", returns = "True if Bluetooth is enabled.") bluetoothToggleState(@pcParametername = "enabled") @pcOptional Boolean enabled, @RpcParameter(name = "prompt", description = "Prompt the user to confirm changing the Bluetooth state.") @RpcDefault("false") Boolean prompt)346 public Boolean bluetoothToggleState(@RpcParameter(name = "enabled") 347 @RpcOptional 348 Boolean enabled, 349 @RpcParameter(name = "prompt", 350 description = "Prompt the user to confirm changing the Bluetooth state.") 351 @RpcDefault("false") 352 Boolean prompt) { 353 mService.registerReceiver(mStateReceiver, 354 new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); 355 if (enabled == null) { 356 enabled = !bluetoothCheckState(); 357 } 358 if (enabled) { 359 return mBluetoothAdapter.enable(); 360 } else { 361 shutdown(); 362 return mBluetoothAdapter.disable(); 363 } 364 } 365 366 367 @Rpc(description = "Start the remote device discovery process. ", 368 returns = "true on success, false on error") bluetoothStartDiscovery()369 public Boolean bluetoothStartDiscovery() { 370 DiscoveredDevices.clear(); 371 mService.registerReceiver(mDiscoveryReceiver, discoveryFilter); 372 return mBluetoothAdapter.startDiscovery(); 373 } 374 375 @Rpc(description = "Cancel the current device discovery process.", 376 returns = "true on success, false on error") bluetoothCancelDiscovery()377 public Boolean bluetoothCancelDiscovery() { 378 try { 379 mService.unregisterReceiver(mDiscoveryReceiver); 380 } catch (IllegalArgumentException e) { 381 Log.d("IllegalArgumentExeption found when trying to unregister reciever"); 382 } 383 return mBluetoothAdapter.cancelDiscovery(); 384 } 385 386 @Rpc(description = "If the local Bluetooth adapter is currently" 387 + "in the device discovery process.") bluetoothIsDiscovering()388 public Boolean bluetoothIsDiscovering() { 389 return mBluetoothAdapter.isDiscovering(); 390 } 391 392 @Rpc(description = "Get all the discovered bluetooth devices.") bluetoothGetDiscoveredDevices()393 public Collection<BluetoothDevice> bluetoothGetDiscoveredDevices() { 394 while (bluetoothIsDiscovering()) 395 ; 396 return DiscoveredDevices.values(); 397 } 398 399 @Rpc(description = "Get Bluetooth controller activity energy info.") bluetoothGetControllerActivityEnergyInfo()400 public String bluetoothGetControllerActivityEnergyInfo() { 401 SynchronousResultReceiver receiver = new SynchronousResultReceiver(); 402 mBluetoothAdapter.requestControllerActivityEnergyInfo( 403 new Executor() { 404 @Override 405 public void execute(Runnable runnable) { 406 runnable.run(); 407 } 408 }, 409 new BluetoothAdapter.OnBluetoothActivityEnergyInfoCallback() { 410 @Override 411 public void onBluetoothActivityEnergyInfoAvailable( 412 BluetoothActivityEnergyInfo info) { 413 Bundle bundle = new Bundle(); 414 bundle.putParcelable( 415 BatteryStats.RESULT_RECEIVER_CONTROLLER_KEY, info); 416 receiver.send(0, bundle); 417 } 418 419 @Override 420 public void onBluetoothActivityEnergyInfoError(int errorCode) { 421 Bundle bundle = new Bundle(); 422 bundle.putParcelable( 423 BatteryStats.RESULT_RECEIVER_CONTROLLER_KEY, null); 424 receiver.send(0, bundle); 425 } 426 } 427 ); 428 try { 429 SynchronousResultReceiver.Result result = receiver.awaitResult(1000); 430 if (result.bundle != null) { 431 return result.bundle.getParcelable(BatteryStats.RESULT_RECEIVER_CONTROLLER_KEY) 432 .toString(); 433 } 434 } catch (TimeoutException e) { 435 Log.e("getControllerActivityEnergyInfo timed out"); 436 } 437 return null; 438 } 439 440 @Rpc(description = "Return true if hardware has entries" + 441 "available for matching beacons.") bluetoothIsHardwareTrackingFiltersAvailable()442 public boolean bluetoothIsHardwareTrackingFiltersAvailable() { 443 return mBluetoothAdapter.isHardwareTrackingFiltersAvailable(); 444 } 445 446 /** 447 * Return true if LE 2M PHY feature is supported. 448 * 449 * @return true if chipset supports LE 2M PHY feature 450 */ 451 @Rpc(description = "Return true if LE 2M PHY feature is supported") bluetoothIsLe2MPhySupported()452 public boolean bluetoothIsLe2MPhySupported() { 453 return mBluetoothAdapter.isLe2MPhySupported(); 454 } 455 456 /** 457 * Return true if LE Coded PHY feature is supported. 458 * 459 * @return true if chipset supports LE Coded PHY feature 460 */ 461 @Rpc(description = "Return true if LE Coded PHY feature is supported") bluetoothIsLeCodedPhySupported()462 public boolean bluetoothIsLeCodedPhySupported() { 463 return mBluetoothAdapter.isLeCodedPhySupported(); 464 } 465 466 /** 467 * Return true if LE Extended Advertising feature is supported. 468 * 469 * @return true if chipset supports LE Extended Advertising feature 470 */ 471 @Rpc(description = "Return true if LE Extended Advertising is supported") bluetoothIsLeExtendedAdvertisingSupported()472 public boolean bluetoothIsLeExtendedAdvertisingSupported() { 473 return mBluetoothAdapter.isLeExtendedAdvertisingSupported(); 474 } 475 476 /** 477 * Return true if LE Periodic Advertising feature is supported. 478 * 479 * @return true if chipset supports LE Periodic Advertising feature 480 */ 481 @Rpc(description = "Return true if LE Periodic Advertising is supported") bluetoothIsLePeriodicAdvertisingSupported()482 public boolean bluetoothIsLePeriodicAdvertisingSupported() { 483 return mBluetoothAdapter.isLePeriodicAdvertisingSupported(); 484 } 485 486 /** 487 * Return the maximum LE advertising data length, 488 * if LE Extended Advertising feature is supported. 489 * 490 * @return the maximum LE advertising data length. 491 */ 492 @Rpc(description = "Return the maximum LE advertising data length") bluetoothGetLeMaximumAdvertisingDataLength()493 public int bluetoothGetLeMaximumAdvertisingDataLength() { 494 return mBluetoothAdapter.getLeMaximumAdvertisingDataLength(); 495 } 496 497 @Rpc(description = "Gets the current state of LE.") bluetoothGetLeState()498 public int bluetoothGetLeState() { 499 return mBluetoothAdapter.getLeState(); 500 } 501 502 @Rpc(description = "Enables BLE functionalities.") bluetoothEnableBLE()503 public boolean bluetoothEnableBLE() { 504 mService.registerReceiver(mBleStateReceiver, 505 new IntentFilter(BluetoothAdapter.ACTION_BLE_STATE_CHANGED)); 506 return mBluetoothAdapter.enableBLE(); 507 } 508 509 @Rpc(description = "Disables BLE functionalities.") bluetoothDisableBLE()510 public boolean bluetoothDisableBLE() { 511 mService.registerReceiver(mBleStateReceiver, 512 new IntentFilter(BluetoothAdapter.ACTION_BLE_STATE_CHANGED)); 513 return mBluetoothAdapter.disableBLE(); 514 } 515 516 @Rpc(description = "Listen for a Bluetooth LE State Change.") bluetoothListenForBleStateChange()517 public boolean bluetoothListenForBleStateChange() { 518 mService.registerReceiver(mBleStateReceiver, 519 new IntentFilter(BluetoothAdapter.ACTION_BLE_STATE_CHANGED)); 520 return true; 521 } 522 523 @Rpc(description = "Stop Listening for a Bluetooth LE State Change.") bluetoothStopListeningForBleStateChange()524 public boolean bluetoothStopListeningForBleStateChange() { 525 mService.unregisterReceiver(mBleStateReceiver); 526 return true; 527 } 528 529 @Rpc(description = "Listen for Bluetooth State Changes.") bluetoothStartListeningForAdapterStateChange()530 public boolean bluetoothStartListeningForAdapterStateChange() { 531 synchronized (mReceiverLock) { 532 if (mMultiStateReceiver != null) { 533 Log.e("Persistent Bluetooth Receiver State Change Listener Already Active"); 534 return false; 535 } 536 mMultiStateReceiver = new BluetoothStateReceiver(true); 537 mService.registerReceiver(mMultiStateReceiver, 538 new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); 539 } 540 return true; 541 } 542 543 @Rpc(description = "Stop Listening for Bluetooth State Changes.") bluetoothStopListeningForAdapterStateChange()544 public boolean bluetoothStopListeningForAdapterStateChange() { 545 synchronized (mReceiverLock) { 546 if (mMultiStateReceiver == null) { 547 Log.d("No Persistent Bluetooth Receiever State Change Listener Found to Stop"); 548 return false; 549 } 550 mService.unregisterReceiver(mMultiStateReceiver); 551 mMultiStateReceiver = null; 552 } 553 return true; 554 } 555 556 @Override shutdown()557 public void shutdown() { 558 for (Map.Entry<String, 559 BluetoothConnection> entry : connections.entrySet()) { 560 entry.getValue().stop(); 561 } 562 if (mMultiStateReceiver != null ) bluetoothStopListeningForAdapterStateChange(); 563 connections.clear(); 564 } 565 } 566