1/* 2 * Copyright (C) 2009 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 17package android.view.accessibility; 18 19import android.Manifest; 20import android.accessibilityservice.AccessibilityServiceInfo; 21import android.annotation.NonNull; 22import android.content.Context; 23import android.content.pm.PackageManager; 24import android.content.pm.ServiceInfo; 25import android.os.Binder; 26import android.os.Handler; 27import android.os.IBinder; 28import android.os.Looper; 29import android.os.Message; 30import android.os.Process; 31import android.os.RemoteException; 32import android.os.ServiceManager; 33import android.os.SystemClock; 34import android.os.UserHandle; 35import android.util.Log; 36import android.view.IWindow; 37import android.view.View; 38 39import java.util.ArrayList; 40import java.util.Collections; 41import java.util.List; 42import java.util.concurrent.CopyOnWriteArrayList; 43 44/** 45 * System level service that serves as an event dispatch for {@link AccessibilityEvent}s, 46 * and provides facilities for querying the accessibility state of the system. 47 * Accessibility events are generated when something notable happens in the user interface, 48 * for example an {@link android.app.Activity} starts, the focus or selection of a 49 * {@link android.view.View} changes etc. Parties interested in handling accessibility 50 * events implement and register an accessibility service which extends 51 * {@link android.accessibilityservice.AccessibilityService}. 52 * <p> 53 * To obtain a handle to the accessibility manager do the following: 54 * </p> 55 * <p> 56 * <code> 57 * <pre>AccessibilityManager accessibilityManager = 58 * (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);</pre> 59 * </code> 60 * </p> 61 * 62 * @see AccessibilityEvent 63 * @see AccessibilityNodeInfo 64 * @see android.accessibilityservice.AccessibilityService 65 * @see Context#getSystemService 66 * @see Context#ACCESSIBILITY_SERVICE 67 */ 68public final class AccessibilityManager { 69 private static final boolean DEBUG = false; 70 71 private static final String LOG_TAG = "AccessibilityManager"; 72 73 /** @hide */ 74 public static final int STATE_FLAG_ACCESSIBILITY_ENABLED = 0x00000001; 75 76 /** @hide */ 77 public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002; 78 79 /** @hide */ 80 public static final int STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED = 0x00000004; 81 82 /** @hide */ 83 public static final int DALTONIZER_DISABLED = -1; 84 85 /** @hide */ 86 public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0; 87 88 /** @hide */ 89 public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12; 90 91 /** @hide */ 92 public static final int AUTOCLICK_DELAY_DEFAULT = 600; 93 94 static final Object sInstanceSync = new Object(); 95 96 private static AccessibilityManager sInstance; 97 98 private final Object mLock = new Object(); 99 100 private IAccessibilityManager mService; 101 102 final int mUserId; 103 104 final Handler mHandler; 105 106 boolean mIsEnabled; 107 108 boolean mIsTouchExplorationEnabled; 109 110 boolean mIsHighTextContrastEnabled; 111 112 private final CopyOnWriteArrayList<AccessibilityStateChangeListener> 113 mAccessibilityStateChangeListeners = new CopyOnWriteArrayList<>(); 114 115 private final CopyOnWriteArrayList<TouchExplorationStateChangeListener> 116 mTouchExplorationStateChangeListeners = new CopyOnWriteArrayList<>(); 117 118 private final CopyOnWriteArrayList<HighTextContrastChangeListener> 119 mHighTextContrastStateChangeListeners = new CopyOnWriteArrayList<>(); 120 121 /** 122 * Listener for the system accessibility state. To listen for changes to the 123 * accessibility state on the device, implement this interface and register 124 * it with the system by calling {@link #addAccessibilityStateChangeListener}. 125 */ 126 public interface AccessibilityStateChangeListener { 127 128 /** 129 * Called when the accessibility enabled state changes. 130 * 131 * @param enabled Whether accessibility is enabled. 132 */ 133 public void onAccessibilityStateChanged(boolean enabled); 134 } 135 136 /** 137 * Listener for the system touch exploration state. To listen for changes to 138 * the touch exploration state on the device, implement this interface and 139 * register it with the system by calling 140 * {@link #addTouchExplorationStateChangeListener}. 141 */ 142 public interface TouchExplorationStateChangeListener { 143 144 /** 145 * Called when the touch exploration enabled state changes. 146 * 147 * @param enabled Whether touch exploration is enabled. 148 */ 149 public void onTouchExplorationStateChanged(boolean enabled); 150 } 151 152 /** 153 * Listener for the system high text contrast state. To listen for changes to 154 * the high text contrast state on the device, implement this interface and 155 * register it with the system by calling 156 * {@link #addHighTextContrastStateChangeListener}. 157 * 158 * @hide 159 */ 160 public interface HighTextContrastChangeListener { 161 162 /** 163 * Called when the high text contrast enabled state changes. 164 * 165 * @param enabled Whether high text contrast is enabled. 166 */ 167 public void onHighTextContrastStateChanged(boolean enabled); 168 } 169 170 private final IAccessibilityManagerClient.Stub mClient = 171 new IAccessibilityManagerClient.Stub() { 172 public void setState(int state) { 173 // We do not want to change this immediately as the applicatoin may 174 // have already checked that accessibility is on and fired an event, 175 // that is now propagating up the view tree, Hence, if accessibility 176 // is now off an exception will be thrown. We want to have the exception 177 // enforcement to guard against apps that fire unnecessary accessibility 178 // events when accessibility is off. 179 mHandler.obtainMessage(MyHandler.MSG_SET_STATE, state, 0).sendToTarget(); 180 } 181 }; 182 183 /** 184 * Get an AccessibilityManager instance (create one if necessary). 185 * 186 * @param context Context in which this manager operates. 187 * 188 * @hide 189 */ 190 public static AccessibilityManager getInstance(Context context) { 191 synchronized (sInstanceSync) { 192 if (sInstance == null) { 193 final int userId; 194 if (Binder.getCallingUid() == Process.SYSTEM_UID 195 || context.checkCallingOrSelfPermission( 196 Manifest.permission.INTERACT_ACROSS_USERS) 197 == PackageManager.PERMISSION_GRANTED 198 || context.checkCallingOrSelfPermission( 199 Manifest.permission.INTERACT_ACROSS_USERS_FULL) 200 == PackageManager.PERMISSION_GRANTED) { 201 userId = UserHandle.USER_CURRENT; 202 } else { 203 userId = UserHandle.myUserId(); 204 } 205 sInstance = new AccessibilityManager(context, null, userId); 206 } 207 } 208 return sInstance; 209 } 210 211 /** 212 * Create an instance. 213 * 214 * @param context A {@link Context}. 215 * @param service An interface to the backing service. 216 * @param userId User id under which to run. 217 * 218 * @hide 219 */ 220 public AccessibilityManager(Context context, IAccessibilityManager service, int userId) { 221 mHandler = new MyHandler(context.getMainLooper()); 222 mUserId = userId; 223 synchronized (mLock) { 224 tryConnectToServiceLocked(service); 225 } 226 } 227 228 /** 229 * @hide 230 */ 231 public IAccessibilityManagerClient getClient() { 232 return mClient; 233 } 234 235 /** 236 * Returns if the accessibility in the system is enabled. 237 * 238 * @return True if accessibility is enabled, false otherwise. 239 */ 240 public boolean isEnabled() { 241 synchronized (mLock) { 242 IAccessibilityManager service = getServiceLocked(); 243 if (service == null) { 244 return false; 245 } 246 return mIsEnabled; 247 } 248 } 249 250 /** 251 * Returns if the touch exploration in the system is enabled. 252 * 253 * @return True if touch exploration is enabled, false otherwise. 254 */ 255 public boolean isTouchExplorationEnabled() { 256 synchronized (mLock) { 257 IAccessibilityManager service = getServiceLocked(); 258 if (service == null) { 259 return false; 260 } 261 return mIsTouchExplorationEnabled; 262 } 263 } 264 265 /** 266 * Returns if the high text contrast in the system is enabled. 267 * <p> 268 * <strong>Note:</strong> You need to query this only if you application is 269 * doing its own rendering and does not rely on the platform rendering pipeline. 270 * </p> 271 * 272 * @return True if high text contrast is enabled, false otherwise. 273 * 274 * @hide 275 */ 276 public boolean isHighTextContrastEnabled() { 277 synchronized (mLock) { 278 IAccessibilityManager service = getServiceLocked(); 279 if (service == null) { 280 return false; 281 } 282 return mIsHighTextContrastEnabled; 283 } 284 } 285 286 /** 287 * Sends an {@link AccessibilityEvent}. 288 * 289 * @param event The event to send. 290 * 291 * @throws IllegalStateException if accessibility is not enabled. 292 * 293 * <strong>Note:</strong> The preferred mechanism for sending custom accessibility 294 * events is through calling 295 * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)} 296 * instead of this method to allow predecessors to augment/filter events sent by 297 * their descendants. 298 */ 299 public void sendAccessibilityEvent(AccessibilityEvent event) { 300 final IAccessibilityManager service; 301 final int userId; 302 synchronized (mLock) { 303 service = getServiceLocked(); 304 if (service == null) { 305 return; 306 } 307 if (!mIsEnabled) { 308 throw new IllegalStateException("Accessibility off. Did you forget to check that?"); 309 } 310 userId = mUserId; 311 } 312 boolean doRecycle = false; 313 try { 314 event.setEventTime(SystemClock.uptimeMillis()); 315 // it is possible that this manager is in the same process as the service but 316 // client using it is called through Binder from another process. Example: MMS 317 // app adds a SMS notification and the NotificationManagerService calls this method 318 long identityToken = Binder.clearCallingIdentity(); 319 doRecycle = service.sendAccessibilityEvent(event, userId); 320 Binder.restoreCallingIdentity(identityToken); 321 if (DEBUG) { 322 Log.i(LOG_TAG, event + " sent"); 323 } 324 } catch (RemoteException re) { 325 Log.e(LOG_TAG, "Error during sending " + event + " ", re); 326 } finally { 327 if (doRecycle) { 328 event.recycle(); 329 } 330 } 331 } 332 333 /** 334 * Requests feedback interruption from all accessibility services. 335 */ 336 public void interrupt() { 337 final IAccessibilityManager service; 338 final int userId; 339 synchronized (mLock) { 340 service = getServiceLocked(); 341 if (service == null) { 342 return; 343 } 344 if (!mIsEnabled) { 345 throw new IllegalStateException("Accessibility off. Did you forget to check that?"); 346 } 347 userId = mUserId; 348 } 349 try { 350 service.interrupt(userId); 351 if (DEBUG) { 352 Log.i(LOG_TAG, "Requested interrupt from all services"); 353 } 354 } catch (RemoteException re) { 355 Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re); 356 } 357 } 358 359 /** 360 * Returns the {@link ServiceInfo}s of the installed accessibility services. 361 * 362 * @return An unmodifiable list with {@link ServiceInfo}s. 363 * 364 * @deprecated Use {@link #getInstalledAccessibilityServiceList()} 365 */ 366 @Deprecated 367 public List<ServiceInfo> getAccessibilityServiceList() { 368 List<AccessibilityServiceInfo> infos = getInstalledAccessibilityServiceList(); 369 List<ServiceInfo> services = new ArrayList<>(); 370 final int infoCount = infos.size(); 371 for (int i = 0; i < infoCount; i++) { 372 AccessibilityServiceInfo info = infos.get(i); 373 services.add(info.getResolveInfo().serviceInfo); 374 } 375 return Collections.unmodifiableList(services); 376 } 377 378 /** 379 * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services. 380 * 381 * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. 382 */ 383 public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() { 384 final IAccessibilityManager service; 385 final int userId; 386 synchronized (mLock) { 387 service = getServiceLocked(); 388 if (service == null) { 389 return Collections.emptyList(); 390 } 391 userId = mUserId; 392 } 393 394 List<AccessibilityServiceInfo> services = null; 395 try { 396 services = service.getInstalledAccessibilityServiceList(userId); 397 if (DEBUG) { 398 Log.i(LOG_TAG, "Installed AccessibilityServices " + services); 399 } 400 } catch (RemoteException re) { 401 Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); 402 } 403 if (services != null) { 404 return Collections.unmodifiableList(services); 405 } else { 406 return Collections.emptyList(); 407 } 408 } 409 410 /** 411 * Returns the {@link AccessibilityServiceInfo}s of the enabled accessibility services 412 * for a given feedback type. 413 * 414 * @param feedbackTypeFlags The feedback type flags. 415 * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. 416 * 417 * @see AccessibilityServiceInfo#FEEDBACK_AUDIBLE 418 * @see AccessibilityServiceInfo#FEEDBACK_GENERIC 419 * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC 420 * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN 421 * @see AccessibilityServiceInfo#FEEDBACK_VISUAL 422 * @see AccessibilityServiceInfo#FEEDBACK_BRAILLE 423 */ 424 public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList( 425 int feedbackTypeFlags) { 426 final IAccessibilityManager service; 427 final int userId; 428 synchronized (mLock) { 429 service = getServiceLocked(); 430 if (service == null) { 431 return Collections.emptyList(); 432 } 433 userId = mUserId; 434 } 435 436 List<AccessibilityServiceInfo> services = null; 437 try { 438 services = service.getEnabledAccessibilityServiceList(feedbackTypeFlags, userId); 439 if (DEBUG) { 440 Log.i(LOG_TAG, "Installed AccessibilityServices " + services); 441 } 442 } catch (RemoteException re) { 443 Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); 444 } 445 if (services != null) { 446 return Collections.unmodifiableList(services); 447 } else { 448 return Collections.emptyList(); 449 } 450 } 451 452 /** 453 * Registers an {@link AccessibilityStateChangeListener} for changes in 454 * the global accessibility state of the system. 455 * 456 * @param listener The listener. 457 * @return True if successfully registered. 458 */ 459 public boolean addAccessibilityStateChangeListener( 460 @NonNull AccessibilityStateChangeListener listener) { 461 // Final CopyOnWriteArrayList - no lock needed. 462 return mAccessibilityStateChangeListeners.add(listener); 463 } 464 465 /** 466 * Unregisters an {@link AccessibilityStateChangeListener}. 467 * 468 * @param listener The listener. 469 * @return True if successfully unregistered. 470 */ 471 public boolean removeAccessibilityStateChangeListener( 472 @NonNull AccessibilityStateChangeListener listener) { 473 // Final CopyOnWriteArrayList - no lock needed. 474 return mAccessibilityStateChangeListeners.remove(listener); 475 } 476 477 /** 478 * Registers a {@link TouchExplorationStateChangeListener} for changes in 479 * the global touch exploration state of the system. 480 * 481 * @param listener The listener. 482 * @return True if successfully registered. 483 */ 484 public boolean addTouchExplorationStateChangeListener( 485 @NonNull TouchExplorationStateChangeListener listener) { 486 // Final CopyOnWriteArrayList - no lock needed. 487 return mTouchExplorationStateChangeListeners.add(listener); 488 } 489 490 /** 491 * Unregisters a {@link TouchExplorationStateChangeListener}. 492 * 493 * @param listener The listener. 494 * @return True if successfully unregistered. 495 */ 496 public boolean removeTouchExplorationStateChangeListener( 497 @NonNull TouchExplorationStateChangeListener listener) { 498 // Final CopyOnWriteArrayList - no lock needed. 499 return mTouchExplorationStateChangeListeners.remove(listener); 500 } 501 502 /** 503 * Registers a {@link HighTextContrastChangeListener} for changes in 504 * the global high text contrast state of the system. 505 * 506 * @param listener The listener. 507 * @return True if successfully registered. 508 * 509 * @hide 510 */ 511 public boolean addHighTextContrastStateChangeListener( 512 @NonNull HighTextContrastChangeListener listener) { 513 // Final CopyOnWriteArrayList - no lock needed. 514 return mHighTextContrastStateChangeListeners.add(listener); 515 } 516 517 /** 518 * Unregisters a {@link HighTextContrastChangeListener}. 519 * 520 * @param listener The listener. 521 * @return True if successfully unregistered. 522 * 523 * @hide 524 */ 525 public boolean removeHighTextContrastStateChangeListener( 526 @NonNull HighTextContrastChangeListener listener) { 527 // Final CopyOnWriteArrayList - no lock needed. 528 return mHighTextContrastStateChangeListeners.remove(listener); 529 } 530 531 /** 532 * Sets the current state and notifies listeners, if necessary. 533 * 534 * @param stateFlags The state flags. 535 */ 536 private void setStateLocked(int stateFlags) { 537 final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0; 538 final boolean touchExplorationEnabled = 539 (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0; 540 final boolean highTextContrastEnabled = 541 (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0; 542 543 final boolean wasEnabled = mIsEnabled; 544 final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled; 545 final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled; 546 547 // Ensure listeners get current state from isZzzEnabled() calls. 548 mIsEnabled = enabled; 549 mIsTouchExplorationEnabled = touchExplorationEnabled; 550 mIsHighTextContrastEnabled = highTextContrastEnabled; 551 552 if (wasEnabled != enabled) { 553 mHandler.sendEmptyMessage(MyHandler.MSG_NOTIFY_ACCESSIBILITY_STATE_CHANGED); 554 } 555 556 if (wasTouchExplorationEnabled != touchExplorationEnabled) { 557 mHandler.sendEmptyMessage(MyHandler.MSG_NOTIFY_EXPLORATION_STATE_CHANGED); 558 } 559 560 if (wasHighTextContrastEnabled != highTextContrastEnabled) { 561 mHandler.sendEmptyMessage(MyHandler.MSG_NOTIFY_HIGH_TEXT_CONTRAST_STATE_CHANGED); 562 } 563 } 564 565 /** 566 * Adds an accessibility interaction connection interface for a given window. 567 * @param windowToken The window token to which a connection is added. 568 * @param connection The connection. 569 * 570 * @hide 571 */ 572 public int addAccessibilityInteractionConnection(IWindow windowToken, 573 IAccessibilityInteractionConnection connection) { 574 final IAccessibilityManager service; 575 final int userId; 576 synchronized (mLock) { 577 service = getServiceLocked(); 578 if (service == null) { 579 return View.NO_ID; 580 } 581 userId = mUserId; 582 } 583 try { 584 return service.addAccessibilityInteractionConnection(windowToken, connection, userId); 585 } catch (RemoteException re) { 586 Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re); 587 } 588 return View.NO_ID; 589 } 590 591 /** 592 * Removed an accessibility interaction connection interface for a given window. 593 * @param windowToken The window token to which a connection is removed. 594 * 595 * @hide 596 */ 597 public void removeAccessibilityInteractionConnection(IWindow windowToken) { 598 final IAccessibilityManager service; 599 synchronized (mLock) { 600 service = getServiceLocked(); 601 if (service == null) { 602 return; 603 } 604 } 605 try { 606 service.removeAccessibilityInteractionConnection(windowToken); 607 } catch (RemoteException re) { 608 Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re); 609 } 610 } 611 612 private IAccessibilityManager getServiceLocked() { 613 if (mService == null) { 614 tryConnectToServiceLocked(null); 615 } 616 return mService; 617 } 618 619 private void tryConnectToServiceLocked(IAccessibilityManager service) { 620 if (service == null) { 621 IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); 622 if (iBinder == null) { 623 return; 624 } 625 service = IAccessibilityManager.Stub.asInterface(iBinder); 626 } 627 628 try { 629 final int stateFlags = service.addClient(mClient, mUserId); 630 setStateLocked(stateFlags); 631 mService = service; 632 } catch (RemoteException re) { 633 Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); 634 } 635 } 636 637 /** 638 * Notifies the registered {@link AccessibilityStateChangeListener}s. 639 */ 640 private void handleNotifyAccessibilityStateChanged() { 641 final boolean isEnabled; 642 synchronized (mLock) { 643 isEnabled = mIsEnabled; 644 } 645 // Listeners are a final CopyOnWriteArrayList, hence no lock needed. 646 for (AccessibilityStateChangeListener listener :mAccessibilityStateChangeListeners) { 647 listener.onAccessibilityStateChanged(isEnabled); 648 } 649 } 650 651 /** 652 * Notifies the registered {@link TouchExplorationStateChangeListener}s. 653 */ 654 private void handleNotifyTouchExplorationStateChanged() { 655 final boolean isTouchExplorationEnabled; 656 synchronized (mLock) { 657 isTouchExplorationEnabled = mIsTouchExplorationEnabled; 658 } 659 // Listeners are a final CopyOnWriteArrayList, hence no lock needed. 660 for (TouchExplorationStateChangeListener listener :mTouchExplorationStateChangeListeners) { 661 listener.onTouchExplorationStateChanged(isTouchExplorationEnabled); 662 } 663 } 664 665 /** 666 * Notifies the registered {@link HighTextContrastChangeListener}s. 667 */ 668 private void handleNotifyHighTextContrastStateChanged() { 669 final boolean isHighTextContrastEnabled; 670 synchronized (mLock) { 671 isHighTextContrastEnabled = mIsHighTextContrastEnabled; 672 } 673 // Listeners are a final CopyOnWriteArrayList, hence no lock needed. 674 for (HighTextContrastChangeListener listener : mHighTextContrastStateChangeListeners) { 675 listener.onHighTextContrastStateChanged(isHighTextContrastEnabled); 676 } 677 } 678 679 private final class MyHandler extends Handler { 680 public static final int MSG_NOTIFY_ACCESSIBILITY_STATE_CHANGED = 1; 681 public static final int MSG_NOTIFY_EXPLORATION_STATE_CHANGED = 2; 682 public static final int MSG_NOTIFY_HIGH_TEXT_CONTRAST_STATE_CHANGED = 3; 683 public static final int MSG_SET_STATE = 4; 684 685 public MyHandler(Looper looper) { 686 super(looper, null, false); 687 } 688 689 @Override 690 public void handleMessage(Message message) { 691 switch (message.what) { 692 case MSG_NOTIFY_ACCESSIBILITY_STATE_CHANGED: { 693 handleNotifyAccessibilityStateChanged(); 694 } break; 695 696 case MSG_NOTIFY_EXPLORATION_STATE_CHANGED: { 697 handleNotifyTouchExplorationStateChanged(); 698 } break; 699 700 case MSG_NOTIFY_HIGH_TEXT_CONTRAST_STATE_CHANGED: { 701 handleNotifyHighTextContrastStateChanged(); 702 } break; 703 704 case MSG_SET_STATE: { 705 // See comment at mClient 706 final int state = message.arg1; 707 synchronized (mLock) { 708 setStateLocked(state); 709 } 710 } break; 711 } 712 } 713 } 714} 715