1 /* 2 * Copyright (C) 2013 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 package android.support.v7.media; 17 18 import android.content.IntentFilter; 19 import android.content.IntentSender; 20 import android.net.Uri; 21 import android.os.Bundle; 22 import android.text.TextUtils; 23 24 import java.util.ArrayList; 25 import java.util.Arrays; 26 import java.util.Collection; 27 import java.util.Collections; 28 import java.util.List; 29 30 /** 31 * Describes the properties of a route. 32 * <p> 33 * Each route is uniquely identified by an opaque id string. This token 34 * may take any form as long as it is unique within the media route provider. 35 * </p><p> 36 * This object is immutable once created using a {@link Builder} instance. 37 * </p> 38 */ 39 public final class MediaRouteDescriptor { 40 private static final String KEY_ID = "id"; 41 private static final String KEY_GROUP_MEMBER_IDS = "groupMemberIds"; 42 private static final String KEY_NAME = "name"; 43 private static final String KEY_DESCRIPTION = "status"; 44 private static final String KEY_ICON_URI = "iconUri"; 45 private static final String KEY_ENABLED = "enabled"; 46 private static final String KEY_CONNECTING = "connecting"; 47 private static final String KEY_CONNECTION_STATE = "connectionState"; 48 private static final String KEY_CONTROL_FILTERS = "controlFilters"; 49 private static final String KEY_PLAYBACK_TYPE = "playbackType"; 50 private static final String KEY_PLAYBACK_STREAM = "playbackStream"; 51 private static final String KEY_DEVICE_TYPE = "deviceType"; 52 private static final String KEY_VOLUME = "volume"; 53 private static final String KEY_VOLUME_MAX = "volumeMax"; 54 private static final String KEY_VOLUME_HANDLING = "volumeHandling"; 55 private static final String KEY_PRESENTATION_DISPLAY_ID = "presentationDisplayId"; 56 private static final String KEY_EXTRAS = "extras"; 57 private static final String KEY_CAN_DISCONNECT = "canDisconnect"; 58 private static final String KEY_SETTINGS_INTENT = "settingsIntent"; 59 private static final String KEY_MIN_CLIENT_VERSION = "minClientVersion"; 60 private static final String KEY_MAX_CLIENT_VERSION = "maxClientVersion"; 61 62 private final Bundle mBundle; 63 private List<IntentFilter> mControlFilters; 64 MediaRouteDescriptor(Bundle bundle, List<IntentFilter> controlFilters)65 private MediaRouteDescriptor(Bundle bundle, List<IntentFilter> controlFilters) { 66 mBundle = bundle; 67 mControlFilters = controlFilters; 68 } 69 70 /** 71 * Gets the unique id of the route. 72 * <p> 73 * The route id associated with a route descriptor functions as a stable 74 * identifier for the route and must be unique among all routes offered 75 * by the provider. 76 * </p> 77 */ getId()78 public String getId() { 79 return mBundle.getString(KEY_ID); 80 } 81 82 /** 83 * Gets the group member ids of the route. 84 * <p> 85 * A route descriptor that has one or more group member route ids 86 * represents a route group. A member route may belong to another group. 87 * </p> 88 * @hide 89 */ getGroupMemberIds()90 public List<String> getGroupMemberIds() { 91 return mBundle.getStringArrayList(KEY_GROUP_MEMBER_IDS); 92 } 93 94 /** 95 * Gets the user-visible name of the route. 96 * <p> 97 * The route name identifies the destination represented by the route. 98 * It may be a user-supplied name, an alias, or device serial number. 99 * </p> 100 */ getName()101 public String getName() { 102 return mBundle.getString(KEY_NAME); 103 } 104 105 /** 106 * Gets the user-visible description of the route. 107 * <p> 108 * The route description describes the kind of destination represented by the route. 109 * It may be a user-supplied string, a model number or brand of device. 110 * </p> 111 */ getDescription()112 public String getDescription() { 113 return mBundle.getString(KEY_DESCRIPTION); 114 } 115 116 /** 117 * Gets the URI of the icon representing this route. 118 * <p> 119 * This icon will be used in picker UIs if available. 120 * </p> 121 */ getIconUri()122 public Uri getIconUri() { 123 String iconUri = mBundle.getString(KEY_ICON_URI); 124 return iconUri == null ? null : Uri.parse(iconUri); 125 } 126 127 /** 128 * Gets whether the route is enabled. 129 */ isEnabled()130 public boolean isEnabled() { 131 return mBundle.getBoolean(KEY_ENABLED, true); 132 } 133 134 /** 135 * Gets whether the route is connecting. 136 * @deprecated Use {@link #getConnectionState} instead 137 */ 138 @Deprecated isConnecting()139 public boolean isConnecting() { 140 return mBundle.getBoolean(KEY_CONNECTING, false); 141 } 142 143 /** 144 * Gets the connection state of the route. 145 * 146 * @return The connection state of this route: 147 * {@link MediaRouter.RouteInfo#CONNECTION_STATE_DISCONNECTED}, 148 * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTING}, or 149 * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTED}. 150 */ getConnectionState()151 public int getConnectionState() { 152 return mBundle.getInt(KEY_CONNECTION_STATE, 153 MediaRouter.RouteInfo.CONNECTION_STATE_DISCONNECTED); 154 } 155 156 /** 157 * Gets whether the route can be disconnected without stopping playback. 158 * <p> 159 * The route can normally be disconnected without stopping playback when 160 * the destination device on the route is connected to two or more source 161 * devices. The route provider should update the route immediately when the 162 * number of connected devices changes. 163 * </p><p> 164 * To specify that the route should disconnect without stopping use 165 * {@link MediaRouter#unselect(int)} with 166 * {@link MediaRouter#UNSELECT_REASON_DISCONNECTED}. 167 * </p> 168 */ canDisconnectAndKeepPlaying()169 public boolean canDisconnectAndKeepPlaying() { 170 return mBundle.getBoolean(KEY_CAN_DISCONNECT, false); 171 } 172 173 /** 174 * Gets an {@link IntentSender} for starting a settings activity for this 175 * route. The activity may have specific route settings or general settings 176 * for the connected device or route provider. 177 * 178 * @return An {@link IntentSender} to start a settings activity. 179 */ getSettingsActivity()180 public IntentSender getSettingsActivity() { 181 return mBundle.getParcelable(KEY_SETTINGS_INTENT); 182 } 183 184 /** 185 * Gets the route's {@link MediaControlIntent media control intent} filters. 186 */ getControlFilters()187 public List<IntentFilter> getControlFilters() { 188 ensureControlFilters(); 189 return mControlFilters; 190 } 191 ensureControlFilters()192 private void ensureControlFilters() { 193 if (mControlFilters == null) { 194 mControlFilters = mBundle.<IntentFilter>getParcelableArrayList(KEY_CONTROL_FILTERS); 195 if (mControlFilters == null) { 196 mControlFilters = Collections.<IntentFilter>emptyList(); 197 } 198 } 199 } 200 201 /** 202 * Gets the type of playback associated with this route. 203 * 204 * @return The type of playback associated with this route: 205 * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_LOCAL} or 206 * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_REMOTE}. 207 */ getPlaybackType()208 public int getPlaybackType() { 209 return mBundle.getInt(KEY_PLAYBACK_TYPE, MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE); 210 } 211 212 /** 213 * Gets the route's playback stream. 214 */ getPlaybackStream()215 public int getPlaybackStream() { 216 return mBundle.getInt(KEY_PLAYBACK_STREAM, -1); 217 } 218 219 /** 220 * Gets the type of the receiver device associated with this route. 221 * 222 * @return The type of the receiver device associated with this route: 223 * {@link MediaRouter.RouteInfo#DEVICE_TYPE_TV} or 224 * {@link MediaRouter.RouteInfo#DEVICE_TYPE_SPEAKER}. 225 */ getDeviceType()226 public int getDeviceType() { 227 return mBundle.getInt(KEY_DEVICE_TYPE); 228 } 229 230 /** 231 * Gets the route's current volume, or 0 if unknown. 232 */ getVolume()233 public int getVolume() { 234 return mBundle.getInt(KEY_VOLUME); 235 } 236 237 /** 238 * Gets the route's maximum volume, or 0 if unknown. 239 */ getVolumeMax()240 public int getVolumeMax() { 241 return mBundle.getInt(KEY_VOLUME_MAX); 242 } 243 244 /** 245 * Gets information about how volume is handled on the route. 246 * 247 * @return How volume is handled on the route: 248 * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_FIXED} or 249 * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_VARIABLE}. 250 */ getVolumeHandling()251 public int getVolumeHandling() { 252 return mBundle.getInt(KEY_VOLUME_HANDLING, 253 MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED); 254 } 255 256 /** 257 * Gets the route's presentation display id, or -1 if none. 258 */ getPresentationDisplayId()259 public int getPresentationDisplayId() { 260 return mBundle.getInt( 261 KEY_PRESENTATION_DISPLAY_ID, MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE); 262 } 263 264 /** 265 * Gets a bundle of extras for this route descriptor. 266 * The extras will be ignored by the media router but they may be used 267 * by applications. 268 */ getExtras()269 public Bundle getExtras() { 270 return mBundle.getBundle(KEY_EXTRAS); 271 } 272 273 /** 274 * Gets the minimum client version required for this route. 275 * @hide 276 */ getMinClientVersion()277 public int getMinClientVersion() { 278 return mBundle.getInt(KEY_MIN_CLIENT_VERSION, 279 MediaRouteProviderProtocol.CLIENT_VERSION_START); 280 } 281 282 /** 283 * Gets the maximum client version required for this route. 284 * @hide 285 */ getMaxClientVersion()286 public int getMaxClientVersion() { 287 return mBundle.getInt(KEY_MAX_CLIENT_VERSION, Integer.MAX_VALUE); 288 } 289 290 /** 291 * Returns true if the route descriptor has all of the required fields. 292 */ isValid()293 public boolean isValid() { 294 ensureControlFilters(); 295 if (TextUtils.isEmpty(getId()) 296 || TextUtils.isEmpty(getName()) 297 || mControlFilters.contains(null)) { 298 return false; 299 } 300 return true; 301 } 302 303 @Override toString()304 public String toString() { 305 StringBuilder result = new StringBuilder(); 306 result.append("MediaRouteDescriptor{ "); 307 result.append("id=").append(getId()); 308 result.append(", groupMemberIds=").append(getGroupMemberIds()); 309 result.append(", name=").append(getName()); 310 result.append(", description=").append(getDescription()); 311 result.append(", iconUri=").append(getIconUri()); 312 result.append(", isEnabled=").append(isEnabled()); 313 result.append(", isConnecting=").append(isConnecting()); 314 result.append(", connectionState=").append(getConnectionState()); 315 result.append(", controlFilters=").append(Arrays.toString(getControlFilters().toArray())); 316 result.append(", playbackType=").append(getPlaybackType()); 317 result.append(", playbackStream=").append(getPlaybackStream()); 318 result.append(", deviceType=").append(getDeviceType()); 319 result.append(", volume=").append(getVolume()); 320 result.append(", volumeMax=").append(getVolumeMax()); 321 result.append(", volumeHandling=").append(getVolumeHandling()); 322 result.append(", presentationDisplayId=").append(getPresentationDisplayId()); 323 result.append(", extras=").append(getExtras()); 324 result.append(", isValid=").append(isValid()); 325 result.append(", minClientVersion=").append(getMinClientVersion()); 326 result.append(", maxClientVersion=").append(getMaxClientVersion()); 327 result.append(" }"); 328 return result.toString(); 329 } 330 331 /** 332 * Converts this object to a bundle for serialization. 333 * 334 * @return The contents of the object represented as a bundle. 335 */ asBundle()336 public Bundle asBundle() { 337 return mBundle; 338 } 339 340 /** 341 * Creates an instance from a bundle. 342 * 343 * @param bundle The bundle, or null if none. 344 * @return The new instance, or null if the bundle was null. 345 */ fromBundle(Bundle bundle)346 public static MediaRouteDescriptor fromBundle(Bundle bundle) { 347 return bundle != null ? new MediaRouteDescriptor(bundle, null) : null; 348 } 349 350 /** 351 * Builder for {@link MediaRouteDescriptor media route descriptors}. 352 */ 353 public static final class Builder { 354 private final Bundle mBundle; 355 private ArrayList<String> mGroupMemberIds; 356 private ArrayList<IntentFilter> mControlFilters; 357 358 /** 359 * Creates a media route descriptor builder. 360 * 361 * @param id The unique id of the route. 362 * @param name The user-visible name of the route. 363 */ Builder(String id, String name)364 public Builder(String id, String name) { 365 mBundle = new Bundle(); 366 setId(id); 367 setName(name); 368 } 369 370 /** 371 * Creates a media route descriptor builder whose initial contents are 372 * copied from an existing descriptor. 373 */ Builder(MediaRouteDescriptor descriptor)374 public Builder(MediaRouteDescriptor descriptor) { 375 if (descriptor == null) { 376 throw new IllegalArgumentException("descriptor must not be null"); 377 } 378 379 mBundle = new Bundle(descriptor.mBundle); 380 381 descriptor.ensureControlFilters(); 382 if (!descriptor.mControlFilters.isEmpty()) { 383 mControlFilters = new ArrayList<IntentFilter>(descriptor.mControlFilters); 384 } 385 } 386 387 /** 388 * Sets the unique id of the route. 389 * <p> 390 * The route id associated with a route descriptor functions as a stable 391 * identifier for the route and must be unique among all routes offered 392 * by the provider. 393 * </p> 394 */ setId(String id)395 public Builder setId(String id) { 396 mBundle.putString(KEY_ID, id); 397 return this; 398 } 399 400 /** 401 * Adds a group member id of the route. 402 * <p> 403 * A route descriptor that has one or more group member route ids 404 * represents a route group. A member route may belong to another group. 405 * </p> 406 * @hide 407 */ addGroupMemberId(String groupMemberId)408 public Builder addGroupMemberId(String groupMemberId) { 409 if (TextUtils.isEmpty(groupMemberId)) { 410 throw new IllegalArgumentException("groupMemberId must not be empty"); 411 } 412 413 if (mGroupMemberIds == null) { 414 mGroupMemberIds = new ArrayList<>(); 415 } 416 if (!mGroupMemberIds.contains(groupMemberId)) { 417 mGroupMemberIds.add(groupMemberId); 418 } 419 return this; 420 } 421 422 /** 423 * Adds a list of group member ids of the route. 424 * <p> 425 * A route descriptor that has one or more group member route ids 426 * represents a route group. A member route may belong to another group. 427 * </p> 428 * @hide 429 */ addGroupMemberIds(Collection<String> groupMemberIds)430 public Builder addGroupMemberIds(Collection<String> groupMemberIds) { 431 if (groupMemberIds == null) { 432 throw new IllegalArgumentException("groupMemberIds must not be null"); 433 } 434 435 if (!groupMemberIds.isEmpty()) { 436 for (String groupMemberId : groupMemberIds) { 437 addGroupMemberId(groupMemberId); 438 } 439 } 440 return this; 441 } 442 443 /** 444 * Sets the user-visible name of the route. 445 * <p> 446 * The route name identifies the destination represented by the route. 447 * It may be a user-supplied name, an alias, or device serial number. 448 * </p> 449 */ setName(String name)450 public Builder setName(String name) { 451 mBundle.putString(KEY_NAME, name); 452 return this; 453 } 454 455 /** 456 * Sets the user-visible description of the route. 457 * <p> 458 * The route description describes the kind of destination represented by the route. 459 * It may be a user-supplied string, a model number or brand of device. 460 * </p> 461 */ setDescription(String description)462 public Builder setDescription(String description) { 463 mBundle.putString(KEY_DESCRIPTION, description); 464 return this; 465 } 466 467 /** 468 * Sets the URI of the icon representing this route. 469 * <p> 470 * This icon will be used in picker UIs if available. 471 * </p><p> 472 * The URI must be one of the following formats: 473 * <ul> 474 * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li> 475 * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE}) 476 * </li> 477 * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li> 478 * </ul> 479 * </p> 480 */ setIconUri(Uri iconUri)481 public Builder setIconUri(Uri iconUri) { 482 if (iconUri == null) { 483 throw new IllegalArgumentException("iconUri must not be null"); 484 } 485 mBundle.putString(KEY_ICON_URI, iconUri.toString()); 486 return this; 487 } 488 489 /** 490 * Sets whether the route is enabled. 491 * <p> 492 * Disabled routes represent routes that a route provider knows about, such as paired 493 * Wifi Display receivers, but that are not currently available for use. 494 * </p> 495 */ setEnabled(boolean enabled)496 public Builder setEnabled(boolean enabled) { 497 mBundle.putBoolean(KEY_ENABLED, enabled); 498 return this; 499 } 500 501 /** 502 * Sets whether the route is in the process of connecting and is not yet 503 * ready for use. 504 * @deprecated Use {@link #setConnectionState} instead. 505 */ 506 @Deprecated setConnecting(boolean connecting)507 public Builder setConnecting(boolean connecting) { 508 mBundle.putBoolean(KEY_CONNECTING, connecting); 509 return this; 510 } 511 512 /** 513 * Sets the route's connection state. 514 * 515 * @param connectionState The connection state of the route: 516 * {@link MediaRouter.RouteInfo#CONNECTION_STATE_DISCONNECTED}, 517 * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTING}, or 518 * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTED}. 519 */ setConnectionState(int connectionState)520 public Builder setConnectionState(int connectionState) { 521 mBundle.putInt(KEY_CONNECTION_STATE, connectionState); 522 return this; 523 } 524 525 /** 526 * Sets whether the route can be disconnected without stopping playback. 527 */ setCanDisconnect(boolean canDisconnect)528 public Builder setCanDisconnect(boolean canDisconnect) { 529 mBundle.putBoolean(KEY_CAN_DISCONNECT, canDisconnect); 530 return this; 531 } 532 533 /** 534 * Sets an intent sender for launching the settings activity for this 535 * route. 536 */ setSettingsActivity(IntentSender is)537 public Builder setSettingsActivity(IntentSender is) { 538 mBundle.putParcelable(KEY_SETTINGS_INTENT, is); 539 return this; 540 } 541 542 /** 543 * Adds a {@link MediaControlIntent media control intent} filter for the route. 544 */ addControlFilter(IntentFilter filter)545 public Builder addControlFilter(IntentFilter filter) { 546 if (filter == null) { 547 throw new IllegalArgumentException("filter must not be null"); 548 } 549 550 if (mControlFilters == null) { 551 mControlFilters = new ArrayList<IntentFilter>(); 552 } 553 if (!mControlFilters.contains(filter)) { 554 mControlFilters.add(filter); 555 } 556 return this; 557 } 558 559 /** 560 * Adds a list of {@link MediaControlIntent media control intent} filters for the route. 561 */ addControlFilters(Collection<IntentFilter> filters)562 public Builder addControlFilters(Collection<IntentFilter> filters) { 563 if (filters == null) { 564 throw new IllegalArgumentException("filters must not be null"); 565 } 566 567 if (!filters.isEmpty()) { 568 for (IntentFilter filter : filters) { 569 addControlFilter(filter); 570 } 571 } 572 return this; 573 } 574 575 /** 576 * Sets the route's playback type. 577 * 578 * @param playbackType The playback type of the route: 579 * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_LOCAL} or 580 * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_REMOTE}. 581 */ setPlaybackType(int playbackType)582 public Builder setPlaybackType(int playbackType) { 583 mBundle.putInt(KEY_PLAYBACK_TYPE, playbackType); 584 return this; 585 } 586 587 /** 588 * Sets the route's playback stream. 589 */ setPlaybackStream(int playbackStream)590 public Builder setPlaybackStream(int playbackStream) { 591 mBundle.putInt(KEY_PLAYBACK_STREAM, playbackStream); 592 return this; 593 } 594 595 /** 596 * Sets the route's receiver device type. 597 * 598 * @param deviceType The receive device type of the route: 599 * {@link MediaRouter.RouteInfo#DEVICE_TYPE_TV} or 600 * {@link MediaRouter.RouteInfo#DEVICE_TYPE_SPEAKER}. 601 */ setDeviceType(int deviceType)602 public Builder setDeviceType(int deviceType) { 603 mBundle.putInt(KEY_DEVICE_TYPE, deviceType); 604 return this; 605 } 606 607 /** 608 * Sets the route's current volume, or 0 if unknown. 609 */ setVolume(int volume)610 public Builder setVolume(int volume) { 611 mBundle.putInt(KEY_VOLUME, volume); 612 return this; 613 } 614 615 /** 616 * Sets the route's maximum volume, or 0 if unknown. 617 */ setVolumeMax(int volumeMax)618 public Builder setVolumeMax(int volumeMax) { 619 mBundle.putInt(KEY_VOLUME_MAX, volumeMax); 620 return this; 621 } 622 623 /** 624 * Sets the route's volume handling. 625 * 626 * @param volumeHandling how volume is handled on the route: 627 * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_FIXED} or 628 * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_VARIABLE}. 629 */ setVolumeHandling(int volumeHandling)630 public Builder setVolumeHandling(int volumeHandling) { 631 mBundle.putInt(KEY_VOLUME_HANDLING, volumeHandling); 632 return this; 633 } 634 635 /** 636 * Sets the route's presentation display id, or -1 if none. 637 */ setPresentationDisplayId(int presentationDisplayId)638 public Builder setPresentationDisplayId(int presentationDisplayId) { 639 mBundle.putInt(KEY_PRESENTATION_DISPLAY_ID, presentationDisplayId); 640 return this; 641 } 642 643 /** 644 * Sets a bundle of extras for this route descriptor. 645 * The extras will be ignored by the media router but they may be used 646 * by applications. 647 */ setExtras(Bundle extras)648 public Builder setExtras(Bundle extras) { 649 mBundle.putBundle(KEY_EXTRAS, extras); 650 return this; 651 } 652 653 /** 654 * Sets the route's minimum client version. 655 * A router whose version is lower than this will not be able to connect to this route. 656 * @hide 657 */ setMinClientVersion(int minVersion)658 public Builder setMinClientVersion(int minVersion) { 659 mBundle.putInt(KEY_MIN_CLIENT_VERSION, minVersion); 660 return this; 661 } 662 663 /** 664 * Sets the route's maximum client version. 665 * A router whose version is higher than this will not be able to connect to this route. 666 * @hide 667 */ setMaxClientVersion(int maxVersion)668 public Builder setMaxClientVersion(int maxVersion) { 669 mBundle.putInt(KEY_MAX_CLIENT_VERSION, maxVersion); 670 return this; 671 } 672 673 /** 674 * Builds the {@link MediaRouteDescriptor media route descriptor}. 675 */ build()676 public MediaRouteDescriptor build() { 677 if (mControlFilters != null) { 678 mBundle.putParcelableArrayList(KEY_CONTROL_FILTERS, mControlFilters); 679 } 680 if (mGroupMemberIds != null) { 681 mBundle.putStringArrayList(KEY_GROUP_MEMBER_IDS, mGroupMemberIds); 682 } 683 return new MediaRouteDescriptor(mBundle, mControlFilters); 684 } 685 } 686 } 687