1 /* 2 * Copyright (C) 2021 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.bedstead.nene.users; 18 19 import static android.cts.testapisreflection.TestApisReflectionKt.setStopUserOnSwitch; 20 import static android.cts.testapisreflection.TestApisReflectionKt.getVisibleBackgroundUsersSupported; 21 import static android.cts.testapisreflection.TestApisReflectionKt.getVisibleBackgroundUsersOnDefaultDisplaySupported; 22 import static android.Manifest.permission.CREATE_USERS; 23 import static android.Manifest.permission.INTERACT_ACROSS_USERS; 24 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; 25 import static android.Manifest.permission.QUERY_USERS; 26 import static android.os.Build.VERSION.SDK_INT; 27 import static android.os.Build.VERSION_CODES.S; 28 import static android.os.Build.VERSION_CODES.S_V2; 29 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; 30 import static android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM; 31 import static android.os.Process.myUserHandle; 32 33 import static com.android.bedstead.nene.users.UserType.MANAGED_PROFILE_TYPE_NAME; 34 import static com.android.bedstead.nene.users.UserType.SECONDARY_USER_TYPE_NAME; 35 import static com.android.bedstead.nene.users.UserType.SYSTEM_USER_TYPE_NAME; 36 import static com.android.bedstead.testapisreflection.TestApisConstants.STOP_USER_ON_SWITCH_DEFAULT; 37 import static com.android.bedstead.testapisreflection.TestApisConstants.STOP_USER_ON_SWITCH_FALSE; 38 import static com.android.bedstead.testapisreflection.TestApisConstants.STOP_USER_ON_SWITCH_TRUE; 39 40 import android.annotation.SuppressLint; 41 import android.app.ActivityManager; 42 import android.content.Context; 43 import android.cts.testapisreflection.TestApisReflectionKt; 44 import android.os.Build; 45 import android.os.UserHandle; 46 import android.os.UserManager; 47 import android.util.Log; 48 49 import androidx.annotation.CheckResult; 50 import androidx.annotation.Nullable; 51 52 import com.android.bedstead.nene.TestApis; 53 import com.android.bedstead.nene.annotations.Experimental; 54 import com.android.bedstead.nene.exceptions.AdbException; 55 import com.android.bedstead.nene.exceptions.AdbParseException; 56 import com.android.bedstead.nene.exceptions.NeneException; 57 import com.android.bedstead.nene.types.OptionalBoolean; 58 import com.android.bedstead.nene.utils.Poll; 59 import com.android.bedstead.nene.utils.ShellCommand; 60 import com.android.bedstead.nene.utils.Versions; 61 import com.android.bedstead.permissions.PermissionContext; 62 import com.android.bedstead.permissions.Permissions; 63 import com.google.errorprone.annotations.CanIgnoreReturnValue; 64 65 import java.time.Duration; 66 import java.util.ArrayList; 67 import java.util.Arrays; 68 import java.util.Collection; 69 import java.util.Comparator; 70 import java.util.HashMap; 71 import java.util.HashSet; 72 import java.util.Iterator; 73 import java.util.List; 74 import java.util.Map; 75 import java.util.Set; 76 import java.util.concurrent.ConcurrentHashMap; 77 import java.util.function.Function; 78 import java.util.stream.Collectors; 79 import java.util.stream.Stream; 80 81 public final class Users { 82 83 private static final String LOG_TAG = "Users"; 84 85 static final int SYSTEM_USER_ID = 0; 86 private static final Duration WAIT_FOR_USER_TIMEOUT = Duration.ofMinutes(4); 87 88 private Map<Integer, AdbUser> mCachedUsers = null; 89 private Map<String, UserType> mCachedUserTypes = null; 90 private Set<UserType> mCachedUserTypeValues = null; 91 private final AdbUserParser mParser; 92 private static final UserManager sUserManager = 93 TestApis.context().instrumentedContext().getSystemService(UserManager.class); 94 private Map<Integer, UserReference> mUsers = new ConcurrentHashMap<>(); 95 96 public static final Users sInstance = new Users(); 97 Users()98 private Users() { 99 mParser = AdbUserParser.get(SDK_INT); 100 } 101 102 /** Get all {@link UserReference}s on the device. */ 103 @CanIgnoreReturnValue all()104 public Collection<UserReference> all() { 105 if (!Versions.meetsMinimumSdkVersionRequirement(S)) { 106 fillCache(); 107 return mCachedUsers.keySet().stream().map(UserReference::new) 108 .collect(Collectors.toSet()); 109 } 110 111 return users().map( 112 ui -> find(ui.getId()) 113 ).collect(Collectors.toSet()); 114 } 115 116 /** Get all {@link UserReference}s in the instrumented user's profile group. */ 117 @Experimental profileGroup()118 public Collection<UserReference> profileGroup() { 119 return profileGroup(TestApis.users().instrumented()); 120 } 121 122 /** Get all {@link UserReference}s in the given profile group. */ 123 @Experimental profileGroup(UserReference user)124 public Collection<UserReference> profileGroup(UserReference user) { 125 return users().filter(ui -> ui.getProfileGroupId() == user.id()) 126 .map(ui -> find(ui.getId())).collect(Collectors.toSet()); 127 } 128 129 /** 130 * Gets a {@link UserReference} of the first human user on the device. 131 * 132 * @deprecated Use {@link #initial()} to ensure compatibility with Headless System User 133 * Mode devices. 134 */ 135 @Deprecated primary()136 public UserReference primary() { 137 return all() 138 .stream() 139 .filter(UserReference::isPrimary) 140 .findFirst() 141 .orElseThrow(IllegalStateException::new); 142 } 143 144 /** 145 * Gets a {@link UserReference} of the first admin user on the device. 146 * 147 * @throws IllegalStateException when there's no admin 148 */ admin()149 public UserReference admin() { 150 return all() 151 .stream() 152 .sorted(Comparator.comparing(UserReference::id)) 153 .filter(UserReference::isAdmin) 154 .findFirst() 155 .orElseThrow(() -> new IllegalStateException("No admin user on device")); 156 } 157 158 /** 159 * Gets a {@link UserReference} for the initial user for the device. 160 * 161 * <p>This will be the {@link #system()} user on most systems.</p> 162 */ initial()163 public UserReference initial() { 164 if (!isHeadlessSystemUserMode()) { 165 return system(); 166 } 167 if (TestApis.packages().features().contains("android.hardware.type.automotive")) { 168 try { 169 UserReference user = 170 ShellCommand.builder("cmd car_service get-initial-user") 171 .executeAndParseOutput(i -> find(Integer.parseInt(i.trim()))); 172 173 if (user.exists()) { 174 return user; 175 } else { 176 Log.d(LOG_TAG, "Initial user " + user + " does not exist." 177 + "Finding first non-system full user"); 178 } 179 } catch (AdbException e) { 180 throw new NeneException("Error finding initial user on Auto", e); 181 } 182 } 183 184 List<UserReference> users = new ArrayList<>(all()); 185 users.sort(Comparator.comparingInt(UserReference::id)); 186 187 for (UserReference user : users) { 188 if (user.parent() != null) { 189 continue; 190 } 191 if (user.id() == 0) { 192 continue; 193 } 194 195 return user; 196 } 197 198 throw new NeneException("No initial user available"); 199 } 200 201 /** Get a {@link UserReference} for the user currently switched to. */ current()202 public UserReference current() { 203 if (Versions.meetsMinimumSdkVersionRequirement(S)) { 204 try (PermissionContext p = 205 TestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) { 206 int currentUserId = ActivityManager.getCurrentUser(); 207 Log.d(LOG_TAG, "current(): finding " + currentUserId); 208 return find(currentUserId); 209 } 210 } 211 212 try { 213 return find((int) ShellCommand.builder("am get-current-user") 214 .executeAndParseOutput(i -> Integer.parseInt(i.trim()))); 215 } catch (AdbException e) { 216 throw new NeneException("Error getting current user", e); 217 } 218 } 219 220 /** Get a {@link UserReference} for the user running the current test process. */ instrumented()221 public UserReference instrumented() { 222 return find(myUserHandle()); 223 } 224 225 /** Get a {@link UserReference} for the system user. */ system()226 public UserReference system() { 227 return find(0); 228 } 229 230 /** Get a {@link UserReference} for the main user, if one exists. Null otherwise. */ 231 @Nullable 232 @SuppressLint("NewApi") main()233 public UserReference main() { 234 UserHandle mainUser; 235 try (PermissionContext p = 236 TestApis.permissions().withPermission(QUERY_USERS)) { 237 mainUser = sUserManager.getMainUser(); 238 } 239 if (mainUser == null) { 240 return null; 241 } 242 return find(mainUser); 243 } 244 245 /** Get a {@link UserReference} by {@code id}. */ find(int id)246 public UserReference find(int id) { 247 if (!mUsers.containsKey(id)) { 248 mUsers.put(id, new UserReference(id)); 249 } 250 return mUsers.get(id); 251 } 252 253 /** Get a {@link UserReference} by {@code userHandle}. */ find(UserHandle userHandle)254 public UserReference find(UserHandle userHandle) { 255 return find(userHandle.getIdentifier()); 256 } 257 258 /** Get all supported {@link UserType}s. */ supportedTypes()259 public Set<UserType> supportedTypes() { 260 // TODO(b/203557600): Stop using adb 261 ensureSupportedTypesCacheFilled(); 262 return mCachedUserTypeValues; 263 } 264 265 /** Get a {@link UserType} with the given {@code typeName}, or {@code null} */ 266 @Nullable supportedType(String typeName)267 public UserType supportedType(String typeName) { 268 ensureSupportedTypesCacheFilled(); 269 return mCachedUserTypes.get(typeName); 270 } 271 272 /** 273 * Find all users which have the given {@link UserType}. 274 */ findUsersOfType(UserType userType)275 public Set<UserReference> findUsersOfType(UserType userType) { 276 if (userType == null) { 277 throw new NullPointerException(); 278 } 279 280 if (userType.baseType().contains(UserType.BaseType.PROFILE)) { 281 throw new NeneException("Cannot use findUsersOfType with profile type " + userType); 282 } 283 284 return all().stream() 285 .filter(u -> { 286 try { 287 return u.type().equals(userType); 288 } catch (NeneException e) { 289 return false; 290 } 291 }) 292 .collect(Collectors.toSet()); 293 } 294 295 /** 296 * Find a single user which has the given {@link UserType}. 297 * 298 * <p>If there are no users of the given type, {@code Null} will be returned. 299 * 300 * <p>If there is more than one user of the given type, {@link NeneException} will be thrown. 301 */ 302 @Nullable 303 public UserReference findUserOfType(UserType userType) { 304 Set<UserReference> users = findUsersOfType(userType); 305 306 if (users.isEmpty()) { 307 return null; 308 } else if (users.size() > 1) { 309 throw new NeneException("findUserOfType called but there is more than 1 user of type " 310 + userType + ". Found: " + users); 311 } 312 313 return users.iterator().next(); 314 } 315 316 /** 317 * Find all users which have the given {@link UserType} and the given parent. 318 */ 319 public Set<UserReference> findProfilesOfType(UserType userType, UserReference parent) { 320 if (userType == null || parent == null) { 321 throw new NullPointerException(); 322 } 323 324 if (!userType.baseType().contains(UserType.BaseType.PROFILE)) { 325 throw new NeneException("Cannot use findProfilesOfType with non-profile type " 326 + userType); 327 } 328 329 return all().stream() 330 .filter(u -> parent.equals(u.parent()) 331 && u.type().equals(userType)) 332 .collect(Collectors.toSet()); 333 } 334 335 /** 336 * Find all users which have the given {@link UserType} and the given parent. 337 * 338 * <p>If there are no users of the given type and parent, {@code Null} will be returned. 339 * 340 * <p>If there is more than one user of the given type and parent, {@link NeneException} will 341 * be thrown. 342 */ 343 @Nullable 344 public UserReference findProfileOfType(UserType userType, UserReference parent) { 345 Set<UserReference> profiles = findProfilesOfType(userType, parent); 346 347 if (profiles.isEmpty()) { 348 return null; 349 } else if (profiles.size() > 1) { 350 throw new NeneException("findProfileOfType called but there is more than 1 user of " 351 + "type " + userType + " with parent " + parent + ". Found: " + profiles); 352 } 353 354 return profiles.iterator().next(); 355 } 356 357 358 /** 359 * Find all users which have the given {@link UserType} and the instrumented user as parent. 360 * 361 * <p>If there are no users of the given type and parent, {@code Null} will be returned. 362 * 363 * <p>If there is more than one user of the given type and parent, {@link NeneException} will 364 * be thrown. 365 */ 366 @Nullable 367 public UserReference findProfileOfType(UserType userType) { 368 return findProfileOfType(userType, TestApis.users().instrumented()); 369 } 370 371 private void ensureSupportedTypesCacheFilled() { 372 if (mCachedUserTypes != null) { 373 // SupportedTypes don't change so don't need to be refreshed 374 return; 375 } 376 if (SDK_INT < Build.VERSION_CODES.R) { 377 mCachedUserTypes = new HashMap<>(); 378 mCachedUserTypes.put(MANAGED_PROFILE_TYPE_NAME, managedProfileUserType()); 379 mCachedUserTypes.put(SYSTEM_USER_TYPE_NAME, systemUserType()); 380 mCachedUserTypes.put(SECONDARY_USER_TYPE_NAME, secondaryUserType()); 381 mCachedUserTypeValues = new HashSet<>(); 382 mCachedUserTypeValues.addAll(mCachedUserTypes.values()); 383 return; 384 } 385 386 fillCache(); 387 } 388 389 private UserType managedProfileUserType() { 390 UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType(); 391 managedProfileMutableUserType.mName = MANAGED_PROFILE_TYPE_NAME; 392 managedProfileMutableUserType.mBaseType = new HashSet<>(Arrays.asList(UserType.BaseType.PROFILE)); 393 managedProfileMutableUserType.mEnabled = true; 394 managedProfileMutableUserType.mMaxAllowed = -1; 395 managedProfileMutableUserType.mMaxAllowedPerParent = 1; 396 return new UserType(managedProfileMutableUserType); 397 } 398 399 private UserType systemUserType() { 400 UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType(); 401 managedProfileMutableUserType.mName = SYSTEM_USER_TYPE_NAME; 402 managedProfileMutableUserType.mBaseType = 403 new HashSet<>(Arrays.asList(UserType.BaseType.FULL, UserType.BaseType.SYSTEM)); 404 managedProfileMutableUserType.mEnabled = true; 405 managedProfileMutableUserType.mMaxAllowed = -1; 406 managedProfileMutableUserType.mMaxAllowedPerParent = -1; 407 return new UserType(managedProfileMutableUserType); 408 } 409 410 private UserType secondaryUserType() { 411 UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType(); 412 managedProfileMutableUserType.mName = SECONDARY_USER_TYPE_NAME; 413 managedProfileMutableUserType.mBaseType = new HashSet<>(Arrays.asList(UserType.BaseType.FULL)); 414 managedProfileMutableUserType.mEnabled = true; 415 managedProfileMutableUserType.mMaxAllowed = -1; 416 managedProfileMutableUserType.mMaxAllowedPerParent = -1; 417 return new UserType(managedProfileMutableUserType); 418 } 419 420 /** 421 * Create a new user. 422 */ 423 @CheckResult 424 public UserBuilder createUser() { 425 return new UserBuilder(); 426 } 427 428 /** 429 * Get a {@link UserReference} to a user who does not exist. 430 */ 431 public UserReference nonExisting() { 432 Set<Integer> userIds; 433 if (Versions.meetsMinimumSdkVersionRequirement(S)) { 434 userIds = users().map(ui -> ui.getId()).collect(Collectors.toSet()); 435 } else { 436 fillCache(); 437 userIds = mCachedUsers.keySet(); 438 } 439 440 int id = 0; 441 442 while (userIds.contains(id)) { 443 id++; 444 } 445 446 return find(id); 447 } 448 449 private void fillCache() { 450 try { 451 // TODO: Replace use of adb on supported versions of Android 452 String userDumpsysOutput = ShellCommand.builder("dumpsys user").execute(); 453 AdbUserParser.ParseResult result = mParser.parse(userDumpsysOutput); 454 455 mCachedUsers = result.mUsers; 456 if (result.mUserTypes != null) { 457 mCachedUserTypes = result.mUserTypes; 458 } else { 459 ensureSupportedTypesCacheFilled(); 460 } 461 462 Iterator<Map.Entry<Integer, AdbUser>> iterator = mCachedUsers.entrySet().iterator(); 463 464 while (iterator.hasNext()) { 465 Map.Entry<Integer, AdbUser> entry = iterator.next(); 466 467 if (entry.getValue().isRemoving()) { 468 // We don't expose users who are currently being removed 469 iterator.remove(); 470 continue; 471 } 472 473 AdbUser.MutableUser mutableUser = entry.getValue().mMutableUser; 474 475 if (SDK_INT < Build.VERSION_CODES.R) { 476 if (entry.getValue().id() == SYSTEM_USER_ID) { 477 mutableUser.mType = supportedType(SYSTEM_USER_TYPE_NAME); 478 mutableUser.mIsPrimary = true; 479 } else if (entry.getValue().hasFlag(AdbUser.FLAG_MANAGED_PROFILE)) { 480 mutableUser.mType = 481 supportedType(MANAGED_PROFILE_TYPE_NAME); 482 mutableUser.mIsPrimary = false; 483 } else { 484 mutableUser.mType = 485 supportedType(SECONDARY_USER_TYPE_NAME); 486 mutableUser.mIsPrimary = false; 487 } 488 } 489 490 if (SDK_INT < S) { 491 if (mutableUser.mType.baseType() 492 .contains(UserType.BaseType.PROFILE)) { 493 // We assume that all profiles before S were on the System User 494 mutableUser.mParent = find(SYSTEM_USER_ID); 495 } 496 } 497 } 498 499 mCachedUserTypeValues = new HashSet<>(); 500 mCachedUserTypeValues.addAll(mCachedUserTypes.values()); 501 502 } catch (AdbException | AdbParseException e) { 503 throw new RuntimeException("Error filling cache", e); 504 } 505 } 506 507 /** 508 * Block until the user with the given {@code userReference} to not exist or to be in the 509 * correct state. 510 * 511 * <p>If this cannot be met before a timeout, a {@link NeneException} will be thrown. 512 */ 513 @Nullable 514 UserReference waitForUserToNotExistOrMatch( 515 UserReference userReference, Function<UserReference, Boolean> userChecker) { 516 return waitForUserToMatch(userReference, userChecker, /* waitForExist= */ false); 517 } 518 519 @Nullable 520 private UserReference waitForUserToMatch( 521 UserReference userReference, Function<UserReference, Boolean> userChecker, 522 boolean waitForExist) { 523 // TODO(scottjonathan): This is pretty heavy because we resolve everything when we know we 524 // are throwing away everything except one user. Optimise 525 try { 526 return Poll.forValue("user", () -> userReference) 527 .toMeet((user) -> { 528 if (user == null) { 529 return !waitForExist; 530 } 531 return userChecker.apply(user); 532 }).timeout(WAIT_FOR_USER_TIMEOUT) 533 .errorOnFail("Expected user to meet requirement") 534 .await(); 535 } catch (AssertionError e) { 536 if (!userReference.exists()) { 537 throw new NeneException( 538 "Timed out waiting for user state for user " 539 + userReference + ". User does not exist.", e); 540 } 541 throw new NeneException( 542 "Timed out waiting for user state, current state " + userReference, e 543 ); 544 } 545 } 546 547 /** Checks if private profile usertupe is supported on the device */ 548 public boolean canAddPrivateProfile() { 549 if (Versions.meetsMinimumSdkVersionRequirement(VANILLA_ICE_CREAM)) { 550 try (PermissionContext p = TestApis.permissions().withPermission(CREATE_USERS)) { 551 return TestApisReflectionKt.canAddPrivateProfile(sUserManager); 552 } 553 } 554 return false; 555 } 556 557 /** See {@link UserManager#isHeadlessSystemUserMode()}. */ 558 @SuppressWarnings("NewApi") 559 public boolean isHeadlessSystemUserMode() { 560 if (Versions.meetsMinimumSdkVersionRequirement(S)) { 561 boolean value = UserManager.isHeadlessSystemUserMode(); 562 Log.d(LOG_TAG, "isHeadlessSystemUserMode: " + value); 563 return value; 564 } 565 566 Log.d(LOG_TAG, "isHeadlessSystemUserMode pre-S: false"); 567 return false; 568 } 569 570 /** See {@link UserManager#isVisibleBackgroundUsersSupported()}. */ 571 @SuppressWarnings("NewApi") 572 public boolean isVisibleBackgroundUsersSupported() { 573 if (Versions.meetsMinimumSdkVersionRequirement(UPSIDE_DOWN_CAKE)) { 574 return getVisibleBackgroundUsersSupported(sUserManager); 575 } 576 577 return false; 578 } 579 580 /** See {@link UserManager#isVisibleBackgroundUsersOnDefaultDisplaySupported()}. */ 581 @SuppressWarnings("NewApi") 582 public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported() { 583 if (Versions.meetsMinimumSdkVersionRequirement(UPSIDE_DOWN_CAKE)) { 584 return getVisibleBackgroundUsersOnDefaultDisplaySupported(sUserManager); 585 } 586 587 return false; 588 } 589 590 /** 591 * Set the stopBgUsersOnSwitch property. 592 * 593 * <p>This affects if background users will be swapped when switched away from on some devices. 594 */ 595 public void setStopBgUsersOnSwitch(OptionalBoolean value) { 596 int intValue = 597 (value == OptionalBoolean.TRUE) 598 ? STOP_USER_ON_SWITCH_TRUE 599 : (value == OptionalBoolean.FALSE) 600 ? STOP_USER_ON_SWITCH_FALSE 601 : STOP_USER_ON_SWITCH_DEFAULT; 602 if (!Versions.meetsMinimumSdkVersionRequirement(S_V2)) { 603 return; 604 } 605 Context context = TestApis.context().instrumentedContext(); 606 try (PermissionContext p = TestApis.permissions() 607 .withPermission(INTERACT_ACROSS_USERS)) { 608 setStopUserOnSwitch(context.getSystemService(ActivityManager.class), intValue); 609 } 610 } 611 612 @Nullable 613 AdbUser fetchUser(int id) { 614 fillCache(); 615 return mCachedUsers.get(id); 616 } 617 618 @Experimental 619 public boolean supportsMultipleUsers() { 620 return UserManager.supportsMultipleUsers(); 621 } 622 623 /** 624 * Note: This method should not be run on < S. 625 */ 626 static Stream<UserInfo> users() { 627 Versions.requireMinimumVersion(S); 628 629 if (Permissions.sIgnorePermissions.get()) { 630 return getUsers(); 631 } 632 633 try (PermissionContext p = 634 TestApis.permissions().withPermission(CREATE_USERS) 635 .withPermissionOnVersionAtLeast(Versions.U, QUERY_USERS)) { 636 return getUsers(); 637 } 638 } 639 640 private static Stream<UserInfo> getUsers() { 641 return TestApisReflectionKt.getUsers(sUserManager, 642 /* excludePartial= */ false, 643 /* excludeDying= */ true, 644 /* excludePreCreated= */ false).stream() 645 .map(ui -> new UserInfo(ui)); 646 } 647 648 /** 649 * Gets the maximum number of users supported by the device 650 */ 651 public int getMaxNumberOfUsersSupported() { 652 try { 653 return ShellCommand.builder("pm get-max-users") 654 .validate((output) -> output.startsWith("Maximum supported users:")) 655 .executeAndParseOutput((output) -> 656 Integer.parseInt(output.split(": ", 2)[1].trim()) 657 ); 658 } catch (AdbException e) { 659 throw new IllegalStateException("Invalid command output", e); 660 } 661 } 662 } 663