1 /* 2 * Copyright (C) 2023 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 android.virtualdevice.cts.common; 18 19 import static android.Manifest.permission.ADD_TRUSTED_DISPLAY; 20 import static android.Manifest.permission.CREATE_VIRTUAL_DEVICE; 21 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; 22 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; 23 import static android.content.pm.PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS; 24 import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT; 25 import static android.graphics.ImageFormat.YUV_420_888; 26 import static android.hardware.camera2.CameraMetadata.LENS_FACING_BACK; 27 28 import static androidx.test.core.app.ApplicationProvider.getApplicationContext; 29 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 30 31 import static com.google.common.truth.Truth.assertThat; 32 33 import static org.junit.Assume.assumeFalse; 34 import static org.junit.Assume.assumeNotNull; 35 import static org.junit.Assume.assumeTrue; 36 37 import android.annotation.TargetApi; 38 import android.app.Activity; 39 import android.app.ActivityOptions; 40 import android.app.UiAutomation; 41 import android.companion.AssociationInfo; 42 import android.companion.virtual.VirtualDeviceManager; 43 import android.companion.virtual.VirtualDeviceManager.VirtualDevice; 44 import android.companion.virtual.VirtualDeviceParams; 45 import android.companion.virtual.camera.VirtualCamera; 46 import android.companion.virtual.camera.VirtualCameraCallback; 47 import android.companion.virtual.camera.VirtualCameraConfig; 48 import android.content.ComponentName; 49 import android.content.Context; 50 import android.content.Intent; 51 import android.graphics.SurfaceTexture; 52 import android.hardware.display.DisplayManager; 53 import android.hardware.display.VirtualDisplay; 54 import android.hardware.display.VirtualDisplayConfig; 55 import android.os.Bundle; 56 import android.platform.test.flag.junit.DeviceFlagsValueProvider; 57 import android.server.wm.Condition; 58 import android.server.wm.WindowManagerState; 59 import android.server.wm.WindowManagerStateHelper; 60 import android.view.Display; 61 import android.view.Surface; 62 63 import androidx.annotation.NonNull; 64 import androidx.annotation.Nullable; 65 import androidx.core.os.BuildCompat; 66 67 import com.android.compatibility.common.util.AdoptShellPermissionsRule; 68 import com.android.compatibility.common.util.FeatureUtil; 69 70 import org.junit.rules.ExternalResource; 71 import org.junit.rules.RuleChain; 72 import org.junit.rules.TestRule; 73 import org.junit.runner.Description; 74 import org.junit.runners.model.Statement; 75 import org.mockito.Mock; 76 import org.mockito.MockitoAnnotations; 77 78 import java.util.ArrayList; 79 import java.util.Arrays; 80 import java.util.Set; 81 import java.util.function.BooleanSupplier; 82 import java.util.function.Supplier; 83 import java.util.stream.Stream; 84 85 /** 86 * A test rule that allows for testing VDM and virtual device features. 87 */ 88 @TargetApi(34) 89 public class VirtualDeviceRule implements TestRule { 90 91 /** General permissions needed for created virtual devices and displays. */ 92 private static final String[] REQUIRED_PERMISSIONS = new String[] { 93 CREATE_VIRTUAL_DEVICE, 94 ADD_TRUSTED_DISPLAY 95 }; 96 97 public static final VirtualDeviceParams DEFAULT_VIRTUAL_DEVICE_PARAMS = 98 new VirtualDeviceParams.Builder().build(); 99 public static final VirtualDisplayConfig DEFAULT_VIRTUAL_DISPLAY_CONFIG = 100 createDefaultVirtualDisplayConfigBuilder().build(); 101 public static final VirtualDisplayConfig TRUSTED_VIRTUAL_DISPLAY_CONFIG = 102 createDefaultVirtualDisplayConfigBuilder() 103 .setFlags(DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC 104 | DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED 105 | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY) 106 .build(); 107 108 public static final String DEFAULT_VIRTUAL_DISPLAY_NAME = "testVirtualDisplay"; 109 public static final int DEFAULT_VIRTUAL_DISPLAY_WIDTH = 640; 110 public static final int DEFAULT_VIRTUAL_DISPLAY_HEIGHT = 480; 111 public static final int DEFAULT_VIRTUAL_DISPLAY_DPI = 240; 112 113 public static final ComponentName BLOCKED_ACTIVITY_COMPONENT = 114 new ComponentName("android", "com.android.internal.app.BlockedAppStreamingActivity"); 115 116 private RuleChain mRuleChain; 117 private final FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule(); 118 private final VirtualDeviceTrackerRule mTrackerRule = new VirtualDeviceTrackerRule(); 119 120 private final Context mContext = getInstrumentation().getTargetContext(); 121 private final VirtualDeviceManager mVirtualDeviceManager = 122 mContext.getSystemService(VirtualDeviceManager.class); 123 private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); 124 125 /** Creates a rule with the required permissions for creating virtual devices and displays. */ createDefault()126 public static VirtualDeviceRule createDefault() { 127 return new VirtualDeviceRule(REQUIRED_PERMISSIONS); 128 } 129 130 /** Creates a rule with any additional permission needed for the specific test. */ withAdditionalPermissions(String... additionalPermissions)131 public static VirtualDeviceRule withAdditionalPermissions(String... additionalPermissions) { 132 return new VirtualDeviceRule(Stream.concat( 133 Arrays.stream(REQUIRED_PERMISSIONS), Arrays.stream(additionalPermissions)) 134 .toArray(String[]::new)); 135 } 136 VirtualDeviceRule(String... permissions)137 private VirtualDeviceRule(String... permissions) { 138 mRuleChain = RuleChain 139 .outerRule(mFakeAssociationRule) 140 .around(DeviceFlagsValueProvider.createCheckFlagsRule()) 141 .around(new AdoptShellPermissionsRule( 142 getInstrumentation().getUiAutomation(), permissions)) 143 .around(mTrackerRule); 144 } 145 146 /** Creates a rule with virtual camera support check before test execution. */ withVirtualCameraSupportCheck()147 public VirtualDeviceRule withVirtualCameraSupportCheck() { 148 mRuleChain = mRuleChain.around(new VirtualCameraSupportRule(this)); 149 return this; 150 } 151 152 @Override apply(final Statement base, final Description description)153 public Statement apply(final Statement base, final Description description) { 154 assumeNotNull(mVirtualDeviceManager); 155 return mRuleChain.apply(base, description); 156 } 157 158 /** 159 * Returns the VirtualDevice object for the given deviceId 160 */ getVirtualDevice(int deviceId)161 public android.companion.virtual.VirtualDevice getVirtualDevice(int deviceId) { 162 if (BuildCompat.isAtLeastV()) { 163 return mVirtualDeviceManager.getVirtualDevice(deviceId); 164 } else { 165 return mVirtualDeviceManager.getVirtualDevices().stream() 166 .filter(device -> device.getDeviceId() == deviceId).findFirst().orElse(null); 167 } 168 } 169 170 /** 171 * Creates a virtual device with default params that will be automatically closed when the 172 * test is torn down. 173 */ 174 @NonNull createManagedVirtualDevice()175 public VirtualDevice createManagedVirtualDevice() { 176 return createManagedVirtualDevice(DEFAULT_VIRTUAL_DEVICE_PARAMS); 177 } 178 179 /** 180 * Creates a virtual device with the given params that will be automatically closed when the 181 * test is torn down. 182 */ 183 @NonNull createManagedVirtualDevice(@onNull VirtualDeviceParams params)184 public VirtualDevice createManagedVirtualDevice(@NonNull VirtualDeviceParams params) { 185 final VirtualDevice virtualDevice = mVirtualDeviceManager.createVirtualDevice( 186 mFakeAssociationRule.getAssociationInfo().getId(), params); 187 mTrackerRule.mVirtualDevices.add(virtualDevice); 188 return virtualDevice; 189 } 190 191 /** 192 * Creates a virtual display associated with the given device that will be automatically 193 * released when the test is torn down. 194 */ 195 @Nullable createManagedVirtualDisplay(@onNull VirtualDevice virtualDevice)196 public VirtualDisplay createManagedVirtualDisplay(@NonNull VirtualDevice virtualDevice) { 197 return createManagedVirtualDisplay(virtualDevice, DEFAULT_VIRTUAL_DISPLAY_CONFIG); 198 } 199 200 /** 201 * Creates a virtual display associated with the given device and flags that will be 202 * automatically released when the test is torn down. 203 */ 204 @Nullable createManagedVirtualDisplayWithFlags( @onNull VirtualDevice virtualDevice, int flags)205 public VirtualDisplay createManagedVirtualDisplayWithFlags( 206 @NonNull VirtualDevice virtualDevice, int flags) { 207 return createManagedVirtualDisplay(virtualDevice, 208 createDefaultVirtualDisplayConfigBuilder().setFlags(flags).build()); 209 } 210 211 /** 212 * Creates a virtual display associated with the given device and config that will be 213 * automatically released when the test is torn down. 214 */ 215 @Nullable createManagedVirtualDisplay(@onNull VirtualDevice virtualDevice, @NonNull VirtualDisplayConfig config)216 public VirtualDisplay createManagedVirtualDisplay(@NonNull VirtualDevice virtualDevice, 217 @NonNull VirtualDisplayConfig config) { 218 final VirtualDisplay virtualDisplay = virtualDevice.createVirtualDisplay( 219 config, /* executor= */ null, /* callback= */ null); 220 if (virtualDisplay != null) { 221 assertDisplayExists(virtualDisplay.getDisplay().getDisplayId()); 222 // There's no need to track managed virtual displays to have them released on tear-down 223 // because they will be released automatically when the VirtualDevice is closed. 224 } 225 return virtualDisplay; 226 } 227 228 /** 229 * Creates a virtual display not associated with the any virtual device that will be 230 * automatically released when the test is torn down. 231 */ 232 @Nullable createManagedUnownedVirtualDisplay()233 public VirtualDisplay createManagedUnownedVirtualDisplay() { 234 return createManagedUnownedVirtualDisplay(DEFAULT_VIRTUAL_DISPLAY_CONFIG); 235 } 236 237 /** 238 * Creates a virtual display not associated with the any virtual device with the given flags 239 * that will be automatically released when the test is torn down. 240 */ 241 @Nullable createManagedUnownedVirtualDisplayWithFlags(int flags)242 public VirtualDisplay createManagedUnownedVirtualDisplayWithFlags(int flags) { 243 return createManagedUnownedVirtualDisplay( 244 createDefaultVirtualDisplayConfigBuilder().setFlags(flags).build()); 245 } 246 247 /** 248 * Creates a virtual display not associated with the any virtual device with the given config 249 * that will be automatically released when the test is torn down. 250 */ 251 @Nullable createManagedUnownedVirtualDisplay(@onNull VirtualDisplayConfig config)252 public VirtualDisplay createManagedUnownedVirtualDisplay(@NonNull VirtualDisplayConfig config) { 253 final VirtualDisplay virtualDisplay = 254 mContext.getSystemService(DisplayManager.class).createVirtualDisplay(config); 255 if (virtualDisplay != null) { 256 assertDisplayExists(virtualDisplay.getDisplay().getDisplayId()); 257 mTrackerRule.mVirtualDisplays.add(virtualDisplay); 258 } 259 return virtualDisplay; 260 } 261 262 /** 263 * Default config for virtual display creation, with a predefined name, dimensions and an empty 264 * surface. 265 */ 266 @NonNull createDefaultVirtualDisplayConfigBuilder()267 public static VirtualDisplayConfig.Builder createDefaultVirtualDisplayConfigBuilder() { 268 return createDefaultVirtualDisplayConfigBuilder( 269 DEFAULT_VIRTUAL_DISPLAY_WIDTH, DEFAULT_VIRTUAL_DISPLAY_HEIGHT); 270 } 271 272 /** 273 * Default config for virtual display creation with custom dimensions. 274 */ 275 @NonNull createDefaultVirtualDisplayConfigBuilder( int width, int height)276 public static VirtualDisplayConfig.Builder createDefaultVirtualDisplayConfigBuilder( 277 int width, int height) { 278 // VirtualDevice#close will cause this to be recycled. 279 //noinspection Recycle 280 SurfaceTexture texture = new SurfaceTexture(1); 281 texture.setDefaultBufferSize(width, height); 282 return new VirtualDisplayConfig.Builder( 283 DEFAULT_VIRTUAL_DISPLAY_NAME, width, height, DEFAULT_VIRTUAL_DISPLAY_DPI) 284 .setSurface(new Surface(texture)); 285 } 286 287 /** 288 * Blocks until the display with the given ID is available. 289 */ assertDisplayExists(int displayId)290 public void assertDisplayExists(int displayId) { 291 waitAndAssertWindowManagerState("Waiting for display to be available", 292 () -> mWmState.getDisplay(displayId) != null); 293 } 294 295 /** 296 * Blocks until the display with the given ID is removed. 297 */ assertDisplayDoesNotExist(int displayId)298 public void assertDisplayDoesNotExist(int displayId) { 299 waitAndAssertWindowManagerState("Waiting for display to be removed", 300 () -> mWmState.getDisplay(displayId) == null); 301 } 302 303 /** Returns the WM state helper. */ getWmState()304 public WindowManagerStateHelper getWmState() { 305 return mWmState; 306 } 307 308 /** Creates a new CDM association. */ createManagedAssociation()309 public AssociationInfo createManagedAssociation() { 310 return mFakeAssociationRule.createManagedAssociation(); 311 } 312 313 /** Drops the current CDM association. */ dropCompanionDeviceAssociation()314 public void dropCompanionDeviceAssociation() { 315 mFakeAssociationRule.disassociate(); 316 } 317 318 /** 319 * Temporarily assumes the given permissions and executes the given supplier. Reverts any 320 * permissions currently held after the execution. 321 */ runWithTemporaryPermission(Supplier<T> supplier, String... permissions)322 public <T> T runWithTemporaryPermission(Supplier<T> supplier, String... permissions) { 323 UiAutomation uiAutomation = getInstrumentation().getUiAutomation(); 324 final Set<String> currentPermissions = uiAutomation.getAdoptedShellPermissions(); 325 uiAutomation.adoptShellPermissionIdentity(permissions); 326 try { 327 return supplier.get(); 328 } finally { 329 // Revert the permissions needed for the test again. 330 uiAutomation.adoptShellPermissionIdentity( 331 currentPermissions.toArray(new String[0])); 332 } 333 } 334 335 /** 336 * Temporarily drops any permissions and executes the given supplier. Reverts any permissions 337 * currently held after the execution. 338 */ runWithoutPermissions(Supplier<T> supplier)339 public <T> T runWithoutPermissions(Supplier<T> supplier) { 340 UiAutomation uiAutomation = getInstrumentation().getUiAutomation(); 341 final Set<String> currentPermissions = uiAutomation.getAdoptedShellPermissions(); 342 uiAutomation.dropShellPermissionIdentity(); 343 try { 344 return supplier.get(); 345 } finally { 346 // Revert the permissions needed for the test again. 347 uiAutomation.adoptShellPermissionIdentity( 348 currentPermissions.toArray(new String[0])); 349 } 350 } 351 352 /** 353 * Starts the activity for the given class on the given virtual display and blocks until it is 354 * successfully launched there. 355 */ startActivityOnDisplaySync( VirtualDisplay virtualDisplay, Class<T> clazz)356 public <T extends Activity> T startActivityOnDisplaySync( 357 VirtualDisplay virtualDisplay, Class<T> clazz) { 358 final int displayId = virtualDisplay.getDisplay().getDisplayId(); 359 return startActivityOnDisplaySync(displayId, clazz); 360 } 361 362 /** 363 * Sends the given intent to the given virtual display. 364 */ sendIntentToDisplay(Intent intent, VirtualDisplay virtualDisplay)365 public void sendIntentToDisplay(Intent intent, VirtualDisplay virtualDisplay) { 366 sendIntentToDisplay(intent, virtualDisplay.getDisplay().getDisplayId()); 367 } 368 369 /** 370 * Sends the given intent to the given display. 371 */ sendIntentToDisplay(Intent intent, int displayId)372 public void sendIntentToDisplay(Intent intent, int displayId) { 373 assumeActivityLaunchSupported(displayId); 374 mContext.startActivity(intent, createActivityOptions(displayId)); 375 } 376 377 /** 378 * Starts the activity for the given class on the given display and blocks until it is 379 * successfully launched there. 380 */ startActivityOnDisplaySync(int displayId, Class<T> clazz)381 public <T extends Activity> T startActivityOnDisplaySync(int displayId, Class<T> clazz) { 382 return startActivityOnDisplaySync(displayId, new Intent(mContext, clazz) 383 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)); 384 } 385 386 /** 387 * Starts the activity for the given intent on the given virtual display and blocks until it is 388 * successfully launched there. 389 */ startActivityOnDisplaySync( VirtualDisplay virtualDisplay, Intent intent)390 public <T extends Activity> T startActivityOnDisplaySync( 391 VirtualDisplay virtualDisplay, Intent intent) { 392 return startActivityOnDisplaySync(virtualDisplay.getDisplay().getDisplayId(), intent); 393 } 394 395 /** 396 * Starts the activity for the given intent on the given display and blocks until it is 397 * successfully launched there. 398 */ startActivityOnDisplaySync(int displayId, Intent intent)399 public <T extends Activity> T startActivityOnDisplaySync(int displayId, Intent intent) { 400 assumeActivityLaunchSupported(displayId); 401 return (T) getInstrumentation().startActivitySync(intent, createActivityOptions(displayId)); 402 } 403 404 /** 405 * Creates activity options for launching activities on the given virtual display. 406 */ createActivityOptions(VirtualDisplay virtualDisplay)407 public static Bundle createActivityOptions(VirtualDisplay virtualDisplay) { 408 return createActivityOptions(virtualDisplay.getDisplay().getDisplayId()); 409 } 410 411 /** 412 * Creates activity options for launching activities on the given display. 413 */ createActivityOptions(int displayId)414 public static Bundle createActivityOptions(int displayId) { 415 return ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle(); 416 } 417 418 /** 419 * Skips the test if the device doesn't support virtual displays that can host activities. 420 */ assumeActivityLaunchSupported(int displayId)421 public void assumeActivityLaunchSupported(int displayId) { 422 if (displayId != Display.DEFAULT_DISPLAY) { 423 assumeTrue(FeatureUtil.hasSystemFeature(FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS)); 424 // TODO(b/261155110): Re-enable once freeform mode is supported on virtual displays. 425 assumeFalse(FeatureUtil.hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT)); 426 } 427 } 428 429 /** 430 * Blocks until the given activity is in resumed state. 431 */ waitAndAssertActivityResumed(ComponentName componentName)432 public void waitAndAssertActivityResumed(ComponentName componentName) { 433 waitAndAssertWindowManagerState("Waiting for activity to be resumed", 434 () -> mWmState.hasActivityState(componentName, WindowManagerState.STATE_RESUMED)); 435 } 436 437 /** 438 * Blocks until the given activity is gone. 439 */ waitAndAssertActivityRemoved(ComponentName componentName)440 public void waitAndAssertActivityRemoved(ComponentName componentName) { 441 waitAndAssertWindowManagerState("Waiting for activity to be removed", 442 () -> !mWmState.containsActivity(componentName)); 443 } 444 445 /** 446 * Override the default retry limit of WindowManagerStateHelper. 447 * Destroying activities on virtual displays and destroying the virtual displays themselves 448 * takes longer than the default timeout of 5s. 449 */ waitAndAssertWindowManagerState( String message, BooleanSupplier waitCondition)450 private void waitAndAssertWindowManagerState( 451 String message, BooleanSupplier waitCondition) { 452 final Condition<String> condition = 453 new Condition<>(message, () -> { 454 mWmState.computeState(); 455 return waitCondition.getAsBoolean(); 456 }); 457 condition.setRetryLimit(10); 458 assertThat(Condition.waitFor(condition)).isTrue(); 459 } 460 461 /** 462 * Internal rule that tracks all created virtual devices and displays and ensures they are 463 * properly closed and released after the test. 464 */ 465 private static final class VirtualDeviceTrackerRule extends ExternalResource { 466 467 final ArrayList<VirtualDevice> mVirtualDevices = new ArrayList<>(); 468 final ArrayList<VirtualDisplay> mVirtualDisplays = new ArrayList<>(); 469 470 @Override after()471 protected void after() { 472 for (VirtualDevice virtualDevice : mVirtualDevices) { 473 virtualDevice.close(); 474 } 475 mVirtualDevices.clear(); 476 for (VirtualDisplay virtualDisplay : mVirtualDisplays) { 477 virtualDisplay.release(); 478 } 479 mVirtualDisplays.clear(); 480 super.after(); 481 } 482 } 483 484 /** 485 * Internal rule that checks whether virtual camera is supported by the device, before executing 486 * any test. 487 */ 488 private static final class VirtualCameraSupportRule extends ExternalResource { 489 private static final int VIRTUAL_CAMERA_SUPPORT_UNKNOWN = 0; 490 private static final int VIRTUAL_CAMERA_SUPPORT_AVAILABLE = 1; 491 private static final int VIRTUAL_CAMERA_SUPPORT_NOT_AVAILABLE = 2; 492 493 @Mock 494 private VirtualCameraCallback mVirtualCameraCallback; 495 496 private final VirtualDeviceRule mVirtualDeviceRule; 497 private int mVirtualCameraSupport = VIRTUAL_CAMERA_SUPPORT_UNKNOWN; 498 VirtualCameraSupportRule(VirtualDeviceRule virtualDeviceRule)499 private VirtualCameraSupportRule(VirtualDeviceRule virtualDeviceRule) { 500 mVirtualDeviceRule = virtualDeviceRule; 501 } 502 503 @Override before()504 protected void before() { 505 MockitoAnnotations.initMocks(this); 506 assumeTrue("Virtual camera not available on this device", 507 getVirtualCameraSupport() == VIRTUAL_CAMERA_SUPPORT_AVAILABLE); 508 } 509 getVirtualCameraSupport()510 private int getVirtualCameraSupport() { 511 if (mVirtualCameraSupport != VIRTUAL_CAMERA_SUPPORT_UNKNOWN) { 512 return mVirtualCameraSupport; 513 } 514 515 try (VirtualDevice virtualDevice = mVirtualDeviceRule.createManagedVirtualDevice( 516 new VirtualDeviceParams.Builder().setDevicePolicy(POLICY_TYPE_CAMERA, 517 DEVICE_POLICY_CUSTOM).build())) { 518 VirtualCameraConfig config = new VirtualCameraConfig.Builder("dummycam") 519 .setVirtualCameraCallback(getApplicationContext().getMainExecutor(), 520 mVirtualCameraCallback) 521 .addStreamConfig(640, 480, YUV_420_888, 30) 522 .setLensFacing(LENS_FACING_BACK) 523 .build(); 524 try (VirtualCamera ignored = virtualDevice.createVirtualCamera(config)) { 525 mVirtualCameraSupport = VIRTUAL_CAMERA_SUPPORT_AVAILABLE; 526 } catch (UnsupportedOperationException e) { 527 mVirtualCameraSupport = VIRTUAL_CAMERA_SUPPORT_NOT_AVAILABLE; 528 } 529 } 530 return mVirtualCameraSupport; 531 } 532 } 533 } 534