1 /* 2 * Copyright (C) 2016 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.android.bluetooth.hid; 18 19 import android.bluetooth.BluetoothDevice; 20 import android.bluetooth.BluetoothHidDeviceAppConfiguration; 21 import android.bluetooth.BluetoothHidDeviceAppQosSettings; 22 import android.bluetooth.BluetoothHidDeviceAppSdpSettings; 23 import android.bluetooth.BluetoothInputHost; 24 import android.bluetooth.BluetoothProfile; 25 import android.bluetooth.IBluetoothHidDeviceCallback; 26 import android.bluetooth.IBluetoothInputHost; 27 import android.content.Intent; 28 import android.os.Handler; 29 import android.os.IBinder; 30 import android.os.Message; 31 import android.os.RemoteException; 32 import android.util.Log; 33 34 import com.android.bluetooth.Utils; 35 import com.android.bluetooth.btservice.ProfileService; 36 37 import java.nio.ByteBuffer; 38 import java.util.Arrays; 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.NoSuchElementException; 42 43 /** @hide */ 44 public class HidDevService extends ProfileService { 45 private static final boolean DBG = false; 46 47 private static final String TAG = HidDevService.class.getSimpleName(); 48 49 private static final int MESSAGE_APPLICATION_STATE_CHANGED = 1; 50 private static final int MESSAGE_CONNECT_STATE_CHANGED = 2; 51 private static final int MESSAGE_GET_REPORT = 3; 52 private static final int MESSAGE_SET_REPORT = 4; 53 private static final int MESSAGE_SET_PROTOCOL = 5; 54 private static final int MESSAGE_INTR_DATA = 6; 55 private static final int MESSAGE_VC_UNPLUG = 7; 56 57 private boolean mNativeAvailable = false; 58 59 private BluetoothDevice mHidDevice = null; 60 61 private int mHidDeviceState = BluetoothInputHost.STATE_DISCONNECTED; 62 63 private BluetoothHidDeviceAppConfiguration mAppConfig = null; 64 65 private IBluetoothHidDeviceCallback mCallback = null; 66 67 private BluetoothHidDeviceDeathRecipient mDeathRcpt; 68 69 static { classInitNative()70 classInitNative(); 71 } 72 73 private final Handler mHandler = new Handler() { 74 75 @Override 76 public void handleMessage(Message msg) { 77 if (DBG) Log.v(TAG, "handleMessage(): msg.what=" + msg.what); 78 79 switch (msg.what) { 80 case MESSAGE_APPLICATION_STATE_CHANGED: { 81 BluetoothDevice device = msg.obj != null ? getDevice((byte[]) msg.obj) : null; 82 boolean success = (msg.arg1 != 0); 83 84 if (success) { 85 mHidDevice = device; 86 } else { 87 mHidDevice = null; 88 } 89 90 try { 91 if (mCallback != null) 92 mCallback.onAppStatusChanged(device, mAppConfig, success); 93 else 94 break; 95 } catch (RemoteException e) { 96 Log.e(TAG, "e=" + e.toString()); 97 e.printStackTrace(); 98 } 99 100 if (success) { 101 mDeathRcpt = new BluetoothHidDeviceDeathRecipient( 102 HidDevService.this, mAppConfig); 103 if (mCallback != null) { 104 IBinder binder = mCallback.asBinder(); 105 try { 106 binder.linkToDeath(mDeathRcpt, 0); 107 Log.i(TAG, "IBinder.linkToDeath() ok"); 108 } catch (RemoteException e) { 109 e.printStackTrace(); 110 } 111 } 112 } else if (mDeathRcpt != null) { 113 if (mCallback != null) { 114 IBinder binder = mCallback.asBinder(); 115 try { 116 binder.unlinkToDeath(mDeathRcpt, 0); 117 Log.i(TAG, "IBinder.unlinkToDeath() ok"); 118 } catch (NoSuchElementException e) { 119 e.printStackTrace(); 120 } 121 mDeathRcpt.cleanup(); 122 mDeathRcpt = null; 123 } 124 } 125 126 if (!success) { 127 mAppConfig = null; 128 mCallback = null; 129 } 130 131 break; 132 } 133 134 case MESSAGE_CONNECT_STATE_CHANGED: { 135 BluetoothDevice device = getDevice((byte[]) msg.obj); 136 int halState = msg.arg1; 137 int state = convertHalState(halState); 138 139 if (state != BluetoothInputHost.STATE_DISCONNECTED) { 140 mHidDevice = device; 141 } 142 143 broadcastConnectionState(device, state); 144 145 try { 146 if (mCallback != null) mCallback.onConnectionStateChanged(device, state); 147 } catch (RemoteException e) { 148 e.printStackTrace(); 149 } 150 break; 151 } 152 153 case MESSAGE_GET_REPORT: 154 byte type = (byte) msg.arg1; 155 byte id = (byte) msg.arg2; 156 int bufferSize = msg.obj == null ? 0 : ((Integer) msg.obj).intValue(); 157 158 try { 159 if (mCallback != null) 160 mCallback.onGetReport(mHidDevice, type, id, bufferSize); 161 } catch (RemoteException e) { 162 e.printStackTrace(); 163 } 164 break; 165 166 case MESSAGE_SET_REPORT: { 167 byte reportType = (byte) msg.arg1; 168 byte reportId = (byte) msg.arg2; 169 byte[] data = ((ByteBuffer) msg.obj).array(); 170 171 try { 172 if (mCallback != null) 173 mCallback.onSetReport(mHidDevice, reportType, reportId, data); 174 } catch (RemoteException e) { 175 e.printStackTrace(); 176 } 177 break; 178 } 179 180 case MESSAGE_SET_PROTOCOL: 181 byte protocol = (byte) msg.arg1; 182 183 try { 184 if (mCallback != null) mCallback.onSetProtocol(mHidDevice, protocol); 185 } catch (RemoteException e) { 186 e.printStackTrace(); 187 } 188 break; 189 190 case MESSAGE_INTR_DATA: 191 byte reportId = (byte) msg.arg1; 192 byte[] data = ((ByteBuffer) msg.obj).array(); 193 194 try { 195 if (mCallback != null) mCallback.onIntrData(mHidDevice, reportId, data); 196 } catch (RemoteException e) { 197 e.printStackTrace(); 198 } 199 break; 200 201 case MESSAGE_VC_UNPLUG: 202 try { 203 if (mCallback != null) mCallback.onVirtualCableUnplug(mHidDevice); 204 } catch (RemoteException e) { 205 e.printStackTrace(); 206 } 207 mHidDevice = null; 208 break; 209 } 210 } 211 }; 212 213 private static class BluetoothHidDeviceDeathRecipient implements IBinder.DeathRecipient { 214 private HidDevService mService; 215 private BluetoothHidDeviceAppConfiguration mAppConfig; 216 BluetoothHidDeviceDeathRecipient( HidDevService service, BluetoothHidDeviceAppConfiguration config)217 public BluetoothHidDeviceDeathRecipient( 218 HidDevService service, BluetoothHidDeviceAppConfiguration config) { 219 mService = service; 220 mAppConfig = config; 221 } 222 223 @Override binderDied()224 public void binderDied() { 225 Log.w(TAG, "Binder died, need to unregister app :("); 226 mService.unregisterApp(mAppConfig); 227 } 228 cleanup()229 public void cleanup() { 230 mService = null; 231 mAppConfig = null; 232 } 233 } 234 235 private static class BluetoothHidDeviceBinder 236 extends IBluetoothInputHost.Stub implements IProfileServiceBinder { 237 238 private static final String TAG = 239 BluetoothHidDeviceBinder.class.getSimpleName(); 240 241 private HidDevService mService; 242 BluetoothHidDeviceBinder(HidDevService service)243 public BluetoothHidDeviceBinder(HidDevService service) { 244 mService = service; 245 } 246 247 @Override cleanup()248 public boolean cleanup() { 249 mService = null; 250 return true; 251 } 252 getService()253 private HidDevService getService() { 254 if (!Utils.checkCaller()) { 255 Log.w(TAG, "HidDevice call not allowed for non-active user"); 256 return null; 257 } 258 259 if (mService != null && mService.isAvailable()) { 260 return mService; 261 } 262 263 return null; 264 } 265 266 @Override registerApp(BluetoothHidDeviceAppConfiguration config, BluetoothHidDeviceAppSdpSettings sdp, BluetoothHidDeviceAppQosSettings inQos, BluetoothHidDeviceAppQosSettings outQos, IBluetoothHidDeviceCallback callback)267 public boolean registerApp(BluetoothHidDeviceAppConfiguration config, 268 BluetoothHidDeviceAppSdpSettings sdp, 269 BluetoothHidDeviceAppQosSettings inQos, 270 BluetoothHidDeviceAppQosSettings outQos, 271 IBluetoothHidDeviceCallback callback) { 272 if (DBG) 273 Log.v(TAG, "registerApp()"); 274 275 HidDevService service = getService(); 276 if (service == null) { 277 return false; 278 } 279 280 return service.registerApp(config, sdp, inQos, outQos, callback); 281 } 282 283 @Override unregisterApp(BluetoothHidDeviceAppConfiguration config)284 public boolean unregisterApp(BluetoothHidDeviceAppConfiguration config) { 285 if (DBG) 286 Log.v(TAG, "unregisterApp()"); 287 288 HidDevService service = getService(); 289 if (service == null) { 290 return false; 291 } 292 293 return service.unregisterApp(config); 294 } 295 296 @Override sendReport(BluetoothDevice device, int id, byte[] data)297 public boolean sendReport(BluetoothDevice device, int id, byte[] data) { 298 if (DBG) Log.v(TAG, "sendReport(): device=" + device + " id=" + id); 299 300 HidDevService service = getService(); 301 if (service == null) { 302 return false; 303 } 304 305 return service.sendReport(device, id, data); 306 } 307 308 @Override replyReport(BluetoothDevice device, byte type, byte id, byte[] data)309 public boolean replyReport(BluetoothDevice device, byte type, byte id, byte[] data) { 310 if (DBG) Log.v(TAG, "replyReport(): device=" + device + " type=" + type + " id=" + id); 311 312 HidDevService service = getService(); 313 if (service == null) { 314 return false; 315 } 316 317 return service.replyReport(device, type, id, data); 318 } 319 320 @Override unplug(BluetoothDevice device)321 public boolean unplug(BluetoothDevice device) { 322 if (DBG) Log.v(TAG, "unplug(): device=" + device); 323 324 HidDevService service = getService(); 325 if (service == null) { 326 return false; 327 } 328 329 return service.unplug(device); 330 } 331 332 @Override connect(BluetoothDevice device)333 public boolean connect(BluetoothDevice device) { 334 if (DBG) Log.v(TAG, "connect(): device=" + device); 335 336 HidDevService service = getService(); 337 if (service == null) { 338 return false; 339 } 340 341 return service.connect(device); 342 } 343 344 @Override disconnect(BluetoothDevice device)345 public boolean disconnect(BluetoothDevice device) { 346 if (DBG) Log.v(TAG, "disconnect(): device=" + device); 347 348 HidDevService service = getService(); 349 if (service == null) { 350 return false; 351 } 352 353 return service.disconnect(device); 354 } 355 356 @Override reportError(BluetoothDevice device, byte error)357 public boolean reportError(BluetoothDevice device, byte error) { 358 if (DBG) Log.v(TAG, "reportError(): device=" + device + " error=" + error); 359 360 HidDevService service = getService(); 361 if (service == null) { 362 return false; 363 } 364 365 return service.reportError(device, error); 366 } 367 368 @Override getConnectionState(BluetoothDevice device)369 public int getConnectionState(BluetoothDevice device) { 370 if (DBG) Log.v(TAG, "getConnectionState(): device=" + device); 371 372 HidDevService service = getService(); 373 if (service == null) { 374 return BluetoothInputHost.STATE_DISCONNECTED; 375 } 376 377 return service.getConnectionState(device); 378 } 379 380 @Override getConnectedDevices()381 public List<BluetoothDevice> getConnectedDevices() { 382 if (DBG) Log.v(TAG, "getConnectedDevices()"); 383 384 return getDevicesMatchingConnectionStates(new int[] {BluetoothProfile.STATE_CONNECTED}); 385 } 386 387 @Override getDevicesMatchingConnectionStates(int[] states)388 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 389 if (DBG) 390 Log.v(TAG, "getDevicesMatchingConnectionStates(): states=" + Arrays.toString(states)); 391 392 HidDevService service = getService(); 393 if (service == null) { 394 return new ArrayList<BluetoothDevice>(0); 395 } 396 397 return service.getDevicesMatchingConnectionStates(states); 398 } 399 } 400 401 @Override initBinder()402 protected IProfileServiceBinder initBinder() { 403 return new BluetoothHidDeviceBinder(this); 404 } 405 checkDevice(BluetoothDevice device)406 private boolean checkDevice(BluetoothDevice device) { 407 if (mHidDevice == null || !mHidDevice.equals(device)) { 408 Log.w(TAG, "Unknown device: " + device); 409 return false; 410 } 411 return true; 412 } 413 registerApp(BluetoothHidDeviceAppConfiguration config, BluetoothHidDeviceAppSdpSettings sdp, BluetoothHidDeviceAppQosSettings inQos, BluetoothHidDeviceAppQosSettings outQos, IBluetoothHidDeviceCallback callback)414 synchronized boolean registerApp(BluetoothHidDeviceAppConfiguration config, 415 BluetoothHidDeviceAppSdpSettings sdp, 416 BluetoothHidDeviceAppQosSettings inQos, 417 BluetoothHidDeviceAppQosSettings outQos, 418 IBluetoothHidDeviceCallback callback) { 419 if (DBG) 420 Log.v(TAG, "registerApp()"); 421 422 if (mAppConfig != null) { 423 return false; 424 } 425 426 mAppConfig = config; 427 mCallback = callback; 428 429 return registerAppNative(sdp.name, sdp.description, sdp.provider, 430 sdp.subclass, sdp.descriptors, 431 inQos == null ? null : inQos.toArray(), 432 outQos == null ? null : outQos.toArray()); 433 } 434 435 synchronized boolean unregisterApp(BluetoothHidDeviceAppConfiguration config)436 unregisterApp(BluetoothHidDeviceAppConfiguration config) { 437 if (DBG) 438 Log.v(TAG, "unregisterApp()"); 439 440 if (mAppConfig == null || config == null || !config.equals(mAppConfig)) { 441 return false; 442 } 443 444 return unregisterAppNative(); 445 } 446 sendReport(BluetoothDevice device, int id, byte[] data)447 synchronized boolean sendReport(BluetoothDevice device, int id, byte[] data) { 448 if (DBG) Log.v(TAG, "sendReport(): device=" + device + " id=" + id); 449 450 if (!checkDevice(device)) { 451 return false; 452 } 453 454 return sendReportNative(id, data); 455 } 456 replyReport(BluetoothDevice device, byte type, byte id, byte[] data)457 synchronized boolean replyReport(BluetoothDevice device, byte type, byte id, byte[] data) { 458 if (DBG) Log.v(TAG, "replyReport(): device=" + device + " type=" + type + " id=" + id); 459 460 if (!checkDevice(device)) { 461 return false; 462 } 463 464 return replyReportNative(type, id, data); 465 } 466 unplug(BluetoothDevice device)467 synchronized boolean unplug(BluetoothDevice device) { 468 if (DBG) Log.v(TAG, "unplug(): device=" + device); 469 470 if (!checkDevice(device)) { 471 return false; 472 } 473 474 return unplugNative(); 475 } 476 connect(BluetoothDevice device)477 synchronized boolean connect(BluetoothDevice device) { 478 if (DBG) Log.v(TAG, "connect(): device=" + device); 479 480 return connectNative(Utils.getByteAddress(device)); 481 } 482 disconnect(BluetoothDevice device)483 synchronized boolean disconnect(BluetoothDevice device) { 484 if (DBG) Log.v(TAG, "disconnect(): device=" + device); 485 486 if (!checkDevice(device)) { 487 return false; 488 } 489 490 return disconnectNative(); 491 } 492 reportError(BluetoothDevice device, byte error)493 synchronized boolean reportError(BluetoothDevice device, byte error) { 494 if (DBG) Log.v(TAG, "reportError(): device=" + device + " error=" + error); 495 496 if (!checkDevice(device)) { 497 return false; 498 } 499 500 return reportErrorNative(error); 501 } 502 503 @Override start()504 protected boolean start() { 505 if (DBG) 506 Log.d(TAG, "start()"); 507 508 initNative(); 509 mNativeAvailable = true; 510 511 return true; 512 } 513 514 @Override stop()515 protected boolean stop() { 516 if (DBG) 517 Log.d(TAG, "stop()"); 518 519 return true; 520 } 521 522 @Override cleanup()523 protected boolean cleanup() { 524 if (DBG) 525 Log.d(TAG, "cleanup()"); 526 527 if (mNativeAvailable) { 528 cleanupNative(); 529 mNativeAvailable = false; 530 } 531 532 return true; 533 } 534 getConnectionState(BluetoothDevice device)535 int getConnectionState(BluetoothDevice device) { 536 if (mHidDevice != null && mHidDevice.equals(device)) { 537 return mHidDeviceState; 538 } 539 return BluetoothInputHost.STATE_DISCONNECTED; 540 } 541 getDevicesMatchingConnectionStates(int[] states)542 List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 543 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 544 List<BluetoothDevice> inputDevices = new ArrayList<BluetoothDevice>(); 545 546 if (mHidDevice != null) { 547 for (int state : states) { 548 if (state == mHidDeviceState) { 549 inputDevices.add(mHidDevice); 550 break; 551 } 552 } 553 } 554 return inputDevices; 555 } 556 onApplicationStateChanged(byte[] address, boolean registered)557 private synchronized void onApplicationStateChanged(byte[] address, 558 boolean registered) { 559 if (DBG) 560 Log.v(TAG, "onApplicationStateChanged(): registered=" + registered); 561 562 Message msg = mHandler.obtainMessage(MESSAGE_APPLICATION_STATE_CHANGED); 563 msg.obj = address; 564 msg.arg1 = registered ? 1 : 0; 565 mHandler.sendMessage(msg); 566 } 567 onConnectStateChanged(byte[] address, int state)568 private synchronized void onConnectStateChanged(byte[] address, int state) { 569 if (DBG) 570 Log.v(TAG, "onConnectStateChanged(): address=" + 571 Arrays.toString(address) + " state=" + state); 572 573 Message msg = mHandler.obtainMessage(MESSAGE_CONNECT_STATE_CHANGED); 574 msg.obj = address; 575 msg.arg1 = state; 576 mHandler.sendMessage(msg); 577 } 578 onGetReport(byte type, byte id, short bufferSize)579 private synchronized void onGetReport(byte type, byte id, short bufferSize) { 580 if (DBG) 581 Log.v(TAG, "onGetReport(): type=" + type + " id=" + id + " bufferSize=" + 582 bufferSize); 583 584 Message msg = mHandler.obtainMessage(MESSAGE_GET_REPORT); 585 msg.obj = bufferSize > 0 ? new Integer(bufferSize) : null; 586 msg.arg1 = type; 587 msg.arg2 = id; 588 mHandler.sendMessage(msg); 589 } 590 onSetReport(byte reportType, byte reportId, byte[] data)591 private synchronized void onSetReport(byte reportType, byte reportId, 592 byte[] data) { 593 if (DBG) 594 Log.v(TAG, "onSetReport(): reportType=" + reportType + " reportId=" + 595 reportId); 596 597 ByteBuffer bb = ByteBuffer.wrap(data); 598 599 Message msg = mHandler.obtainMessage(MESSAGE_SET_REPORT); 600 msg.arg1 = reportType; 601 msg.arg2 = reportId; 602 msg.obj = bb; 603 mHandler.sendMessage(msg); 604 } 605 onSetProtocol(byte protocol)606 private synchronized void onSetProtocol(byte protocol) { 607 if (DBG) 608 Log.v(TAG, "onSetProtocol(): protocol=" + protocol); 609 610 Message msg = mHandler.obtainMessage(MESSAGE_SET_PROTOCOL); 611 msg.arg1 = protocol; 612 mHandler.sendMessage(msg); 613 } 614 onIntrData(byte reportId, byte[] data)615 private synchronized void onIntrData(byte reportId, byte[] data) { 616 if (DBG) 617 Log.v(TAG, "onIntrData(): reportId=" + reportId); 618 619 ByteBuffer bb = ByteBuffer.wrap(data); 620 621 Message msg = mHandler.obtainMessage(MESSAGE_INTR_DATA); 622 msg.arg1 = reportId; 623 msg.obj = bb; 624 mHandler.sendMessage(msg); 625 } 626 onVirtualCableUnplug()627 private synchronized void onVirtualCableUnplug() { 628 if (DBG) 629 Log.v(TAG, "onVirtualCableUnplug()"); 630 631 Message msg = mHandler.obtainMessage(MESSAGE_VC_UNPLUG); 632 mHandler.sendMessage(msg); 633 } 634 broadcastConnectionState(BluetoothDevice device, int newState)635 private void broadcastConnectionState(BluetoothDevice device, int newState) { 636 if (DBG) 637 Log.v(TAG, "broadcastConnectionState(): device=" + device.getAddress() + 638 " newState=" + newState); 639 640 if (mHidDevice != null && !mHidDevice.equals(device)) { 641 Log.w(TAG, "Connection state changed for unknown device, ignoring"); 642 return; 643 } 644 645 int prevState = mHidDeviceState; 646 mHidDeviceState = newState; 647 648 Log.i(TAG, "connection state for " + device.getAddress() + ": " + 649 prevState + " -> " + newState); 650 651 if (prevState == newState) { 652 return; 653 } 654 655 Intent intent = 656 new Intent(BluetoothInputHost.ACTION_CONNECTION_STATE_CHANGED); 657 intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); 658 intent.putExtra(BluetoothProfile.EXTRA_STATE, newState); 659 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 660 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 661 sendBroadcast(intent, BLUETOOTH_PERM); 662 } 663 convertHalState(int halState)664 private static int convertHalState(int halState) { 665 switch (halState) { 666 case CONN_STATE_CONNECTED: 667 return BluetoothProfile.STATE_CONNECTED; 668 case CONN_STATE_CONNECTING: 669 return BluetoothProfile.STATE_CONNECTING; 670 case CONN_STATE_DISCONNECTED: 671 return BluetoothProfile.STATE_DISCONNECTED; 672 case CONN_STATE_DISCONNECTING: 673 return BluetoothProfile.STATE_DISCONNECTING; 674 default: 675 return BluetoothProfile.STATE_DISCONNECTED; 676 } 677 } 678 679 private final static int CONN_STATE_CONNECTED = 0; 680 private final static int CONN_STATE_CONNECTING = 1; 681 private final static int CONN_STATE_DISCONNECTED = 2; 682 private final static int CONN_STATE_DISCONNECTING = 3; 683 classInitNative()684 private native static void classInitNative(); initNative()685 private native void initNative(); cleanupNative()686 private native void cleanupNative(); registerAppNative(String name, String description, String provider, byte subclass, byte[] descriptors, int[] inQos, int[] outQos)687 private native boolean registerAppNative(String name, String description, 688 String provider, byte subclass, 689 byte[] descriptors, int[] inQos, 690 int[] outQos); unregisterAppNative()691 private native boolean unregisterAppNative(); sendReportNative(int id, byte[] data)692 private native boolean sendReportNative(int id, byte[] data); replyReportNative(byte type, byte id, byte[] data)693 private native boolean replyReportNative(byte type, byte id, byte[] data); unplugNative()694 private native boolean unplugNative(); connectNative(byte[] btAddress)695 private native boolean connectNative(byte[] btAddress); disconnectNative()696 private native boolean disconnectNative(); reportErrorNative(byte error)697 private native boolean reportErrorNative(byte error); 698 } 699