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 com.android.wm.shell.shared; 18 19 import static android.app.ActivityTaskManager.INVALID_TASK_ID; 20 import static android.view.RemoteAnimationTarget.MODE_CHANGING; 21 import static android.view.RemoteAnimationTarget.MODE_CLOSING; 22 import static android.view.RemoteAnimationTarget.MODE_OPENING; 23 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; 24 import static android.view.WindowManager.LayoutParams.LAST_SYSTEM_WINDOW; 25 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; 26 import static android.view.WindowManager.TRANSIT_CHANGE; 27 import static android.view.WindowManager.TRANSIT_CLOSE; 28 import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; 29 import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; 30 import static android.view.WindowManager.TRANSIT_OPEN; 31 import static android.view.WindowManager.TRANSIT_TO_BACK; 32 import static android.view.WindowManager.TRANSIT_TO_FRONT; 33 import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM; 34 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; 35 import static android.window.TransitionInfo.FLAG_IS_DISPLAY; 36 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; 37 import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; 38 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; 39 40 import android.annotation.NonNull; 41 import android.annotation.Nullable; 42 import android.annotation.SuppressLint; 43 import android.app.ActivityManager; 44 import android.app.WindowConfiguration; 45 import android.graphics.Rect; 46 import android.util.ArrayMap; 47 import android.util.SparseBooleanArray; 48 import android.view.RemoteAnimationTarget; 49 import android.view.SurfaceControl; 50 import android.view.WindowManager; 51 import android.window.TransitionInfo; 52 53 import java.util.function.Predicate; 54 55 /** Various utility functions for transitions. */ 56 public class TransitionUtil { 57 /** Flag applied to a transition change to identify it as a divider bar for animation. */ 58 public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM; 59 public static final int FLAG_IS_DIM_LAYER = FLAG_FIRST_CUSTOM << 1; 60 61 /** Flag applied to a transition change to identify it as a desktop wallpaper activity. */ 62 public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 2; 63 64 /** 65 * Applied to a {@link RemoteAnimationTarget} to identify dim layers for animation in Launcher. 66 */ 67 public static final int TYPE_SPLIT_SCREEN_DIM_LAYER = LAST_SYSTEM_WINDOW + 1; 68 69 /** @return true if the transition was triggered by opening something vs closing something */ isOpeningType(@indowManager.TransitionType int type)70 public static boolean isOpeningType(@WindowManager.TransitionType int type) { 71 return type == TRANSIT_OPEN 72 || type == TRANSIT_TO_FRONT 73 || type == TRANSIT_KEYGUARD_GOING_AWAY 74 || type == TRANSIT_PREPARE_BACK_NAVIGATION; 75 } 76 77 /** @return true if the transition was triggered by closing something vs opening something */ isClosingType(@indowManager.TransitionType int type)78 public static boolean isClosingType(@WindowManager.TransitionType int type) { 79 return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK; 80 } 81 82 /** Returns {@code true} if the transition is opening or closing mode. */ isOpenOrCloseMode(@ransitionInfo.TransitionMode int mode)83 public static boolean isOpenOrCloseMode(@TransitionInfo.TransitionMode int mode) { 84 return isOpeningMode(mode) || isClosingMode(mode); 85 } 86 87 /** Returns {@code true} if the transition is opening mode. */ isOpeningMode(@ransitionInfo.TransitionMode int mode)88 public static boolean isOpeningMode(@TransitionInfo.TransitionMode int mode) { 89 return mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT; 90 } 91 92 /** Returns {@code true} if the transition is closing mode. */ isClosingMode(@ransitionInfo.TransitionMode int mode)93 public static boolean isClosingMode(@TransitionInfo.TransitionMode int mode) { 94 return mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK; 95 } 96 97 /** Returns {@code true} if the transition has a display change. */ hasDisplayChange(@onNull TransitionInfo info)98 public static boolean hasDisplayChange(@NonNull TransitionInfo info) { 99 for (int i = info.getChanges().size() - 1; i >= 0; --i) { 100 final TransitionInfo.Change change = info.getChanges().get(i); 101 if (change.getMode() == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) { 102 return true; 103 } 104 } 105 return false; 106 } 107 108 /** Returns `true` if `change` is a wallpaper. */ isWallpaper(TransitionInfo.Change change)109 public static boolean isWallpaper(TransitionInfo.Change change) { 110 return (change.getTaskInfo() == null) 111 && change.hasFlags(FLAG_IS_WALLPAPER) 112 && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY); 113 } 114 115 /** Returns `true` if `change` is not an app window or wallpaper. */ isNonApp(TransitionInfo.Change change)116 public static boolean isNonApp(TransitionInfo.Change change) { 117 return (change.getTaskInfo() == null) 118 && !change.hasFlags(FLAG_IS_WALLPAPER) 119 && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY); 120 } 121 122 /** Returns `true` if `change` is the divider. */ isDividerBar(TransitionInfo.Change change)123 public static boolean isDividerBar(TransitionInfo.Change change) { 124 return isNonApp(change) && change.hasFlags(FLAG_IS_DIVIDER_BAR); 125 } 126 127 /** Returns `true` if `change` is an app's dim layer. */ isDimLayer(TransitionInfo.Change change)128 public static boolean isDimLayer(TransitionInfo.Change change) { 129 return isNonApp(change) && change.hasFlags(FLAG_IS_DIM_LAYER); 130 } 131 132 /** Returns `true` if `change` is only re-ordering. */ isOrderOnly(TransitionInfo.Change change)133 public static boolean isOrderOnly(TransitionInfo.Change change) { 134 return change.getMode() == TRANSIT_CHANGE 135 && (change.getFlags() & FLAG_MOVED_TO_TOP) != 0 136 && change.getStartAbsBounds().equals(change.getEndAbsBounds()) 137 && (change.getLastParent() == null 138 || change.getLastParent().equals(change.getParent())); 139 } 140 141 /** 142 * Check if all changes in this transition are only ordering changes. If so, we won't animate. 143 */ isAllOrderOnly(TransitionInfo info)144 public static boolean isAllOrderOnly(TransitionInfo info) { 145 for (int i = info.getChanges().size() - 1; i >= 0; --i) { 146 if (!isOrderOnly(info.getChanges().get(i))) return false; 147 } 148 return true; 149 } 150 151 /** 152 * Look through a transition and see if all non-closing changes are no-animation. If so, no 153 * animation should play. 154 */ isAllNoAnimation(TransitionInfo info)155 public static boolean isAllNoAnimation(TransitionInfo info) { 156 if (isClosingType(info.getType())) { 157 // no-animation is only relevant for launching (open) activities. 158 return false; 159 } 160 boolean hasNoAnimation = false; 161 final int changeSize = info.getChanges().size(); 162 for (int i = changeSize - 1; i >= 0; --i) { 163 final TransitionInfo.Change change = info.getChanges().get(i); 164 if (isClosingType(change.getMode())) { 165 // ignore closing apps since they are a side-effect of the transition and don't 166 // animate. 167 continue; 168 } 169 if (change.hasFlags(TransitionInfo.FLAG_NO_ANIMATION)) { 170 hasNoAnimation = true; 171 } else if (!isOrderOnly(change) && !change.hasFlags(TransitionInfo.FLAG_IS_OCCLUDED)) { 172 // Ignore the order only or occluded changes since they shouldn't be visible during 173 // animation. For anything else, we need to animate if at-least one relevant 174 // participant *is* animated, 175 return false; 176 } 177 } 178 return hasNoAnimation; 179 } 180 181 /** 182 * Filter that selects leaf-tasks only. THIS IS ORDER-DEPENDENT! For it to work properly, you 183 * MUST call `test` in the same order that the changes appear in the TransitionInfo. 184 */ 185 public static class LeafTaskFilter implements Predicate<TransitionInfo.Change> { 186 private final SparseBooleanArray mChildTaskTargets = new SparseBooleanArray(); 187 188 @Override test(TransitionInfo.Change change)189 public boolean test(TransitionInfo.Change change) { 190 final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); 191 if (taskInfo == null) return false; 192 // Children always come before parent since changes are in top-to-bottom z-order. 193 boolean hasChildren = mChildTaskTargets.get(taskInfo.taskId); 194 if (taskInfo.hasParentTask()) { 195 mChildTaskTargets.put(taskInfo.parentTaskId, true); 196 } 197 // If it has children, it's not a leaf. 198 return !hasChildren; 199 } 200 } 201 202 newModeToLegacyMode(int newMode)203 private static int newModeToLegacyMode(int newMode) { 204 switch (newMode) { 205 case WindowManager.TRANSIT_OPEN: 206 case WindowManager.TRANSIT_TO_FRONT: 207 return MODE_OPENING; 208 case WindowManager.TRANSIT_CLOSE: 209 case WindowManager.TRANSIT_TO_BACK: 210 return MODE_CLOSING; 211 default: 212 return MODE_CHANGING; 213 } 214 } 215 216 /** 217 * Very similar to Transitions#setupAnimHierarchy but specialized for leashes. 218 */ 219 @SuppressLint("NewApi") setupLeash(@onNull SurfaceControl leash, @NonNull TransitionInfo.Change change, int layer, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t)220 private static void setupLeash(@NonNull SurfaceControl leash, 221 @NonNull TransitionInfo.Change change, int layer, 222 @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { 223 final boolean isOpening = TransitionUtil.isOpeningType(info.getType()); 224 // Put animating stuff above this line and put static stuff below it. 225 int zSplitLine = info.getChanges().size(); 226 // changes should be ordered top-to-bottom in z 227 final int mode = change.getMode(); 228 229 final int rootIdx = TransitionUtil.rootIndexFor(change, info); 230 t.reparent(leash, info.getRoot(rootIdx).getLeash()); 231 final Rect absBounds = 232 (mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds(); 233 t.setPosition(leash, absBounds.left - info.getRoot(rootIdx).getOffset().x, 234 absBounds.top - info.getRoot(rootIdx).getOffset().y); 235 236 if (isDividerBar(change)) { 237 if (isOpeningType(mode)) { 238 t.setAlpha(leash, 0.f); 239 } 240 // Set the transition leash position to 0 in case the divider leash position being 241 // taking down. 242 t.setPosition(leash, 0, 0); 243 t.setLayer(leash, Integer.MAX_VALUE); 244 return; 245 } 246 if (isDimLayer(change)) { 247 // When a dim layer gets reparented onto the transition root, we need to zero out its 248 // position so that it's in line with everything else on the transition root. Also, 249 // we need to set a crop because we don't want it applying MATCH_PARENT on the whole 250 // root surface. 251 t.setPosition(leash, 0, 0); 252 t.setCrop(leash, change.getEndAbsBounds()); 253 } 254 255 // Put all the OPEN/SHOW on top 256 if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { 257 // Wallpaper is always at the bottom, opening wallpaper on top of closing one. 258 if (mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT) { 259 t.setLayer(leash, -zSplitLine + info.getChanges().size() - layer); 260 } else { 261 t.setLayer(leash, -zSplitLine - layer); 262 } 263 } else if (TransitionUtil.isOpeningType(mode)) { 264 if (isOpening) { 265 t.setLayer(leash, zSplitLine + info.getChanges().size() - layer); 266 if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) { 267 // if transferred, it should be left visible. 268 t.setAlpha(leash, 0.f); 269 } 270 } else { 271 // put on bottom and leave it visible 272 t.setLayer(leash, zSplitLine - layer); 273 } 274 } else if (TransitionUtil.isClosingType(mode)) { 275 if (isOpening) { 276 // put on bottom and leave visible 277 t.setLayer(leash, zSplitLine - layer); 278 } else { 279 // put on top 280 t.setLayer(leash, zSplitLine + info.getChanges().size() - layer); 281 } 282 } else { // CHANGE 283 t.setLayer(leash, zSplitLine + info.getChanges().size() - layer); 284 } 285 } 286 287 @SuppressLint("NewApi") createLeash(TransitionInfo info, TransitionInfo.Change change, int order, SurfaceControl.Transaction t)288 private static SurfaceControl createLeash(TransitionInfo info, TransitionInfo.Change change, 289 int order, SurfaceControl.Transaction t) { 290 // TODO: once we can properly sync transactions across process, then get rid of this leash. 291 if (change.getParent() != null && (change.getFlags() & FLAG_IS_WALLPAPER) != 0) { 292 // Special case for wallpaper atm. Normally these are left alone; but, a quirk of 293 // making leashes means we have to handle them specially. 294 return change.getLeash(); 295 } 296 final int rootIdx = TransitionUtil.rootIndexFor(change, info); 297 SurfaceControl leashSurface = new SurfaceControl.Builder() 298 .setName(change.getLeash().toString() + "_transition-leash") 299 .setContainerLayer() 300 // Initial the surface visible to respect the visibility of the original surface. 301 .setHidden(false) 302 .setParent(info.getRoot(rootIdx).getLeash()) 303 .build(); 304 // Copied Transitions setup code (which expects bottom-to-top order, so we swap here) 305 setupLeash(leashSurface, change, info.getChanges().size() - order, info, t); 306 t.reparent(change.getLeash(), leashSurface); 307 if (!isDimLayer(change)) { 308 // Most leashes going onto the transition root should have their alpha set here to make 309 // them visible. But dim layers should be left untouched (their alpha value is their 310 // actual dim value). 311 t.setAlpha(change.getLeash(), 1.0f); 312 } 313 if (!isDividerBar(change)) { 314 // For divider, don't modify its inner leash position when creating the outer leash 315 // for the transition. In case the position being wrong after the transition finished. 316 t.setPosition(change.getLeash(), 0, 0); 317 } 318 t.setLayer(change.getLeash(), 0); 319 t.show(change.getLeash()); 320 return leashSurface; 321 } 322 323 /** 324 * Creates a new RemoteAnimationTarget from the provided change info 325 */ newTarget(TransitionInfo.Change change, int order, TransitionInfo info, SurfaceControl.Transaction t, @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap)326 public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order, 327 TransitionInfo info, SurfaceControl.Transaction t, 328 @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) { 329 return newTarget(change, order, false /* forceTranslucent */, info, t, leashMap); 330 } 331 332 /** 333 * Creates a new RemoteAnimationTarget from the provided change info 334 */ newTarget(TransitionInfo.Change change, int order, boolean forceTranslucent, TransitionInfo info, SurfaceControl.Transaction t, @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap)335 public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order, 336 boolean forceTranslucent, TransitionInfo info, SurfaceControl.Transaction t, 337 @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) { 338 final SurfaceControl leash = createLeash(info, change, order, t); 339 if (leashMap != null) { 340 leashMap.put(change.getLeash(), leash); 341 } 342 return newTarget(change, order, leash, forceTranslucent); 343 } 344 345 /** 346 * Creates a new RemoteAnimationTarget from the provided change and leash 347 */ newTarget(TransitionInfo.Change change, int order, SurfaceControl leash)348 public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order, 349 SurfaceControl leash) { 350 return newTarget(change, order, leash, false /* forceTranslucent */); 351 } 352 353 /** 354 * Creates a new RemoteAnimationTarget from the provided change and leash 355 */ newTarget(TransitionInfo.Change change, int order, SurfaceControl leash, boolean forceTranslucent)356 public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order, 357 SurfaceControl leash, boolean forceTranslucent) { 358 if (isDividerBar(change)) { 359 return getDividerTarget(change, leash); 360 } 361 if (isDimLayer(change)) { 362 return getDimLayerTarget(change, leash); 363 } 364 365 int taskId; 366 boolean isNotInRecents; 367 ActivityManager.RunningTaskInfo taskInfo; 368 WindowConfiguration windowConfiguration; 369 370 taskInfo = change.getTaskInfo(); 371 if (taskInfo != null) { 372 taskId = taskInfo.taskId; 373 isNotInRecents = !taskInfo.isRunning; 374 windowConfiguration = taskInfo.configuration.windowConfiguration; 375 } else { 376 taskId = INVALID_TASK_ID; 377 isNotInRecents = true; 378 windowConfiguration = new WindowConfiguration(); 379 } 380 381 Rect localBounds = new Rect(change.getEndAbsBounds()); 382 localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y); 383 384 RemoteAnimationTarget target = new RemoteAnimationTarget( 385 taskId, 386 newModeToLegacyMode(change.getMode()), 387 // TODO: once we can properly sync transactions across process, 388 // then get rid of this leash. 389 leash, 390 forceTranslucent || (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0, 391 null, 392 // TODO(shell-transitions): we need to send content insets? evaluate how its used. 393 new Rect(0, 0, 0, 0), 394 order, 395 null, 396 localBounds, 397 new Rect(change.getEndAbsBounds()), 398 windowConfiguration, 399 isNotInRecents, 400 null, 401 new Rect(change.getStartAbsBounds()), 402 taskInfo, 403 change.isAllowEnterPip(), 404 INVALID_WINDOW_TYPE 405 ); 406 target.setWillShowImeOnTarget( 407 (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0); 408 target.setRotationChange(change.getEndRotation() - change.getStartRotation()); 409 target.backgroundColor = change.getBackgroundColor(); 410 return target; 411 } 412 413 /** 414 * Creates a new RemoteAnimationTarget from the provided change and leash 415 */ newSyntheticTarget(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, @TransitionInfo.TransitionMode int mode, int order, boolean isTranslucent)416 public static RemoteAnimationTarget newSyntheticTarget(ActivityManager.RunningTaskInfo taskInfo, 417 SurfaceControl leash, @TransitionInfo.TransitionMode int mode, int order, 418 boolean isTranslucent) { 419 int taskId; 420 boolean isNotInRecents; 421 WindowConfiguration windowConfiguration; 422 423 if (taskInfo != null) { 424 taskId = taskInfo.taskId; 425 isNotInRecents = !taskInfo.isRunning; 426 windowConfiguration = taskInfo.configuration.windowConfiguration; 427 } else { 428 taskId = INVALID_TASK_ID; 429 isNotInRecents = true; 430 windowConfiguration = new WindowConfiguration(); 431 } 432 433 Rect bounds = windowConfiguration.getBounds(); 434 RemoteAnimationTarget target = new RemoteAnimationTarget( 435 taskId, 436 newModeToLegacyMode(mode), 437 // TODO: once we can properly sync transactions across process, 438 // then get rid of this leash. 439 leash, 440 isTranslucent, 441 null, 442 // TODO(shell-transitions): we need to send content insets? evaluate how its used. 443 new Rect(0, 0, 0, 0), 444 order, 445 null, 446 bounds, 447 bounds, 448 windowConfiguration, 449 isNotInRecents, 450 null, 451 bounds, 452 taskInfo, 453 false, 454 INVALID_WINDOW_TYPE 455 ); 456 return target; 457 } 458 getDividerTarget(TransitionInfo.Change change, SurfaceControl leash)459 private static RemoteAnimationTarget getDividerTarget(TransitionInfo.Change change, 460 SurfaceControl leash) { 461 return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()), 462 leash, false /* isTranslucent */, null /* clipRect */, 463 null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, 464 new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(), 465 change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */, 466 null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */, 467 TYPE_DOCK_DIVIDER); 468 } 469 getDimLayerTarget(TransitionInfo.Change change, SurfaceControl leash)470 private static RemoteAnimationTarget getDimLayerTarget(TransitionInfo.Change change, 471 SurfaceControl leash) { 472 return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()), 473 leash, false /* isTranslucent */, null /* clipRect */, 474 null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, 475 new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(), 476 change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */, 477 null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */, 478 TYPE_SPLIT_SCREEN_DIM_LAYER); 479 } 480 481 /** 482 * Finds the "correct" root idx for a change. The change's end display is prioritized, then 483 * the start display. If there is no display, it will fallback on the 0th root in the 484 * transition. There MUST be at-least 1 root in the transition (ie. it's not a no-op). 485 */ rootIndexFor(@onNull TransitionInfo.Change change, @NonNull TransitionInfo info)486 public static int rootIndexFor(@NonNull TransitionInfo.Change change, 487 @NonNull TransitionInfo info) { 488 int rootIdx = info.findRootIndex(change.getEndDisplayId()); 489 if (rootIdx >= 0) return rootIdx; 490 rootIdx = info.findRootIndex(change.getStartDisplayId()); 491 if (rootIdx >= 0) return rootIdx; 492 return 0; 493 } 494 495 /** 496 * Gets the {@link TransitionInfo.Root} for the given {@link TransitionInfo.Change}. 497 * @see #rootIndexFor(TransitionInfo.Change, TransitionInfo) 498 */ 499 @NonNull getRootFor(@onNull TransitionInfo.Change change, @NonNull TransitionInfo info)500 public static TransitionInfo.Root getRootFor(@NonNull TransitionInfo.Change change, 501 @NonNull TransitionInfo info) { 502 return info.getRoot(rootIndexFor(change, info)); 503 } 504 } 505