1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.statusbar.phone; 16 17 import static com.android.systemui.statusbar.phone.StatusBar.CLOSE_PANEL_WHEN_EMPTIED; 18 import static com.android.systemui.statusbar.phone.StatusBar.DEBUG; 19 import static com.android.systemui.statusbar.phone.StatusBar.MULTIUSER_DEBUG; 20 import static com.android.systemui.statusbar.phone.StatusBar.SPEW; 21 22 import android.annotation.Nullable; 23 import android.app.KeyguardManager; 24 import android.content.Context; 25 import android.os.RemoteException; 26 import android.os.ServiceManager; 27 import android.service.notification.StatusBarNotification; 28 import android.service.vr.IVrManager; 29 import android.service.vr.IVrStateCallbacks; 30 import android.util.Log; 31 import android.util.Slog; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.accessibility.AccessibilityManager; 35 import android.widget.TextView; 36 37 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 38 import com.android.internal.statusbar.IStatusBarService; 39 import com.android.internal.statusbar.NotificationVisibility; 40 import com.android.internal.widget.MessagingGroup; 41 import com.android.internal.widget.MessagingMessage; 42 import com.android.keyguard.KeyguardUpdateMonitor; 43 import com.android.systemui.Dependency; 44 import com.android.systemui.ForegroundServiceNotificationListener; 45 import com.android.systemui.InitController; 46 import com.android.systemui.R; 47 import com.android.systemui.plugins.ActivityStarter; 48 import com.android.systemui.plugins.ActivityStarter.OnDismissAction; 49 import com.android.systemui.plugins.statusbar.StatusBarStateController; 50 import com.android.systemui.statusbar.CommandQueue; 51 import com.android.systemui.statusbar.KeyguardIndicationController; 52 import com.android.systemui.statusbar.NotificationLockscreenUserManager; 53 import com.android.systemui.statusbar.NotificationMediaManager; 54 import com.android.systemui.statusbar.NotificationPresenter; 55 import com.android.systemui.statusbar.NotificationRemoteInputManager; 56 import com.android.systemui.statusbar.NotificationViewHierarchyManager; 57 import com.android.systemui.statusbar.StatusBarState; 58 import com.android.systemui.statusbar.SysuiStatusBarStateController; 59 import com.android.systemui.statusbar.notification.AboveShelfObserver; 60 import com.android.systemui.statusbar.notification.ActivityLaunchAnimator; 61 import com.android.systemui.statusbar.notification.DynamicPrivacyController; 62 import com.android.systemui.statusbar.notification.NotificationEntryListener; 63 import com.android.systemui.statusbar.notification.NotificationEntryManager; 64 import com.android.systemui.statusbar.notification.VisualStabilityManager; 65 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 66 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; 67 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; 68 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptSuppressor; 69 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; 70 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 71 import com.android.systemui.statusbar.notification.row.NotificationGutsManager; 72 import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener; 73 import com.android.systemui.statusbar.notification.row.NotificationInfo.CheckSaveListener; 74 import com.android.systemui.statusbar.notification.stack.NotificationListContainer; 75 import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent; 76 import com.android.systemui.statusbar.policy.ConfigurationController; 77 import com.android.systemui.statusbar.policy.KeyguardStateController; 78 79 import java.util.List; 80 81 public class StatusBarNotificationPresenter implements NotificationPresenter, 82 ConfigurationController.ConfigurationListener, 83 NotificationRowBinderImpl.BindRowCallback, 84 CommandQueue.Callbacks { 85 86 private final LockscreenGestureLogger mLockscreenGestureLogger = 87 Dependency.get(LockscreenGestureLogger.class); 88 89 private static final String TAG = "StatusBarNotificationPresenter"; 90 91 private final ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class); 92 private final KeyguardStateController mKeyguardStateController; 93 private final NotificationViewHierarchyManager mViewHierarchyManager = 94 Dependency.get(NotificationViewHierarchyManager.class); 95 private final NotificationLockscreenUserManager mLockscreenUserManager = 96 Dependency.get(NotificationLockscreenUserManager.class); 97 private final SysuiStatusBarStateController mStatusBarStateController = 98 (SysuiStatusBarStateController) Dependency.get(StatusBarStateController.class); 99 private final NotificationEntryManager mEntryManager = 100 Dependency.get(NotificationEntryManager.class); 101 private final NotificationMediaManager mMediaManager = 102 Dependency.get(NotificationMediaManager.class); 103 private final VisualStabilityManager mVisualStabilityManager = 104 Dependency.get(VisualStabilityManager.class); 105 private final NotificationGutsManager mGutsManager = 106 Dependency.get(NotificationGutsManager.class); 107 108 private final NotificationPanelViewController mNotificationPanel; 109 private final HeadsUpManagerPhone mHeadsUpManager; 110 private final AboveShelfObserver mAboveShelfObserver; 111 private final DozeScrimController mDozeScrimController; 112 private final ScrimController mScrimController; 113 private final Context mContext; 114 private final KeyguardIndicationController mKeyguardIndicationController; 115 private final StatusBar mStatusBar; 116 private final ShadeController mShadeController; 117 private final CommandQueue mCommandQueue; 118 119 private final AccessibilityManager mAccessibilityManager; 120 private final KeyguardManager mKeyguardManager; 121 private final ActivityLaunchAnimator mActivityLaunchAnimator; 122 private final int mMaxAllowedKeyguardNotifications; 123 private final IStatusBarService mBarService; 124 private final DynamicPrivacyController mDynamicPrivacyController; 125 private boolean mReinflateNotificationsOnUserSwitched; 126 private boolean mDispatchUiModeChangeOnUserSwitched; 127 private TextView mNotificationPanelDebugText; 128 129 protected boolean mVrMode; 130 private int mMaxKeyguardNotifications; 131 StatusBarNotificationPresenter(Context context, NotificationPanelViewController panel, HeadsUpManagerPhone headsUp, NotificationShadeWindowView statusBarWindow, ViewGroup stackScroller, DozeScrimController dozeScrimController, ScrimController scrimController, ActivityLaunchAnimator activityLaunchAnimator, DynamicPrivacyController dynamicPrivacyController, KeyguardStateController keyguardStateController, KeyguardIndicationController keyguardIndicationController, StatusBar statusBar, ShadeController shadeController, CommandQueue commandQueue, InitController initController, NotificationInterruptStateProvider notificationInterruptStateProvider)132 public StatusBarNotificationPresenter(Context context, 133 NotificationPanelViewController panel, 134 HeadsUpManagerPhone headsUp, 135 NotificationShadeWindowView statusBarWindow, 136 ViewGroup stackScroller, 137 DozeScrimController dozeScrimController, 138 ScrimController scrimController, 139 ActivityLaunchAnimator activityLaunchAnimator, 140 DynamicPrivacyController dynamicPrivacyController, 141 KeyguardStateController keyguardStateController, 142 KeyguardIndicationController keyguardIndicationController, 143 StatusBar statusBar, 144 ShadeController shadeController, 145 CommandQueue commandQueue, 146 InitController initController, 147 NotificationInterruptStateProvider notificationInterruptStateProvider) { 148 mContext = context; 149 mKeyguardStateController = keyguardStateController; 150 mNotificationPanel = panel; 151 mHeadsUpManager = headsUp; 152 mDynamicPrivacyController = dynamicPrivacyController; 153 mKeyguardIndicationController = keyguardIndicationController; 154 // TODO: use KeyguardStateController#isOccluded to remove this dependency 155 mStatusBar = statusBar; 156 mShadeController = shadeController; 157 mCommandQueue = commandQueue; 158 mAboveShelfObserver = new AboveShelfObserver(stackScroller); 159 mActivityLaunchAnimator = activityLaunchAnimator; 160 mAboveShelfObserver.setListener(statusBarWindow.findViewById( 161 R.id.notification_container_parent)); 162 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 163 mDozeScrimController = dozeScrimController; 164 mScrimController = scrimController; 165 mKeyguardManager = context.getSystemService(KeyguardManager.class); 166 mMaxAllowedKeyguardNotifications = context.getResources().getInteger( 167 R.integer.keyguard_max_notification_count); 168 mBarService = IStatusBarService.Stub.asInterface( 169 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 170 171 if (MULTIUSER_DEBUG) { 172 mNotificationPanelDebugText = mNotificationPanel.getHeaderDebugInfo(); 173 mNotificationPanelDebugText.setVisibility(View.VISIBLE); 174 } 175 176 IVrManager vrManager = IVrManager.Stub.asInterface(ServiceManager.getService( 177 Context.VR_SERVICE)); 178 if (vrManager != null) { 179 try { 180 vrManager.registerListener(mVrStateCallbacks); 181 } catch (RemoteException e) { 182 Slog.e(TAG, "Failed to register VR mode state listener: " + e); 183 } 184 } 185 NotificationRemoteInputManager remoteInputManager = 186 Dependency.get(NotificationRemoteInputManager.class); 187 remoteInputManager.setUpWithCallback( 188 Dependency.get(NotificationRemoteInputManager.Callback.class), 189 mNotificationPanel.createRemoteInputDelegate()); 190 remoteInputManager.getController().addCallback( 191 Dependency.get(NotificationShadeWindowController.class)); 192 193 NotificationListContainer notifListContainer = (NotificationListContainer) stackScroller; 194 initController.addPostInitTask(() -> { 195 NotificationEntryListener notificationEntryListener = new NotificationEntryListener() { 196 @Override 197 public void onEntryRemoved( 198 @Nullable NotificationEntry entry, 199 NotificationVisibility visibility, 200 boolean removedByUser, 201 int reason) { 202 StatusBarNotificationPresenter.this.onNotificationRemoved( 203 entry.getKey(), entry.getSbn()); 204 if (removedByUser) { 205 maybeEndAmbientPulse(); 206 } 207 } 208 }; 209 210 mViewHierarchyManager.setUpWithPresenter(this, notifListContainer); 211 mEntryManager.setUpWithPresenter(this); 212 mEntryManager.addNotificationEntryListener(notificationEntryListener); 213 mEntryManager.addNotificationLifetimeExtender(mHeadsUpManager); 214 mEntryManager.addNotificationLifetimeExtender(mGutsManager); 215 mEntryManager.addNotificationLifetimeExtenders( 216 remoteInputManager.getLifetimeExtenders()); 217 notificationInterruptStateProvider.addSuppressor(mInterruptSuppressor); 218 mLockscreenUserManager.setUpWithPresenter(this); 219 mMediaManager.setUpWithPresenter(this); 220 mVisualStabilityManager.setUpWithPresenter(this); 221 mGutsManager.setUpWithPresenter(this, 222 notifListContainer, mCheckSaveListener, mOnSettingsClickListener); 223 // ForegroundServiceNotificationListener adds its listener in its constructor 224 // but we need to request it here in order for it to be instantiated. 225 // TODO: figure out how to do this correctly once Dependency.get() is gone. 226 Dependency.get(ForegroundServiceNotificationListener.class); 227 228 onUserSwitched(mLockscreenUserManager.getCurrentUserId()); 229 }); 230 Dependency.get(ConfigurationController.class).addCallback(this); 231 } 232 233 @Override onDensityOrFontScaleChanged()234 public void onDensityOrFontScaleChanged() { 235 MessagingMessage.dropCache(); 236 MessagingGroup.dropCache(); 237 if (!Dependency.get(KeyguardUpdateMonitor.class).isSwitchingUser()) { 238 updateNotificationsOnDensityOrFontScaleChanged(); 239 } else { 240 mReinflateNotificationsOnUserSwitched = true; 241 } 242 } 243 244 @Override onUiModeChanged()245 public void onUiModeChanged() { 246 if (!Dependency.get(KeyguardUpdateMonitor.class).isSwitchingUser()) { 247 updateNotificationOnUiModeChanged(); 248 } else { 249 mDispatchUiModeChangeOnUserSwitched = true; 250 } 251 } 252 253 @Override onOverlayChanged()254 public void onOverlayChanged() { 255 onDensityOrFontScaleChanged(); 256 } 257 updateNotificationOnUiModeChanged()258 private void updateNotificationOnUiModeChanged() { 259 List<NotificationEntry> userNotifications = 260 mEntryManager.getActiveNotificationsForCurrentUser(); 261 for (int i = 0; i < userNotifications.size(); i++) { 262 NotificationEntry entry = userNotifications.get(i); 263 ExpandableNotificationRow row = entry.getRow(); 264 if (row != null) { 265 row.onUiModeChanged(); 266 } 267 } 268 } 269 updateNotificationsOnDensityOrFontScaleChanged()270 private void updateNotificationsOnDensityOrFontScaleChanged() { 271 List<NotificationEntry> userNotifications = 272 mEntryManager.getActiveNotificationsForCurrentUser(); 273 for (int i = 0; i < userNotifications.size(); i++) { 274 NotificationEntry entry = userNotifications.get(i); 275 entry.onDensityOrFontScaleChanged(); 276 boolean exposedGuts = entry.areGutsExposed(); 277 if (exposedGuts) { 278 mGutsManager.onDensityOrFontScaleChanged(entry); 279 } 280 } 281 } 282 283 @Override isCollapsing()284 public boolean isCollapsing() { 285 return mNotificationPanel.isCollapsing() 286 || mActivityLaunchAnimator.isAnimationPending() 287 || mActivityLaunchAnimator.isAnimationRunning(); 288 } 289 maybeEndAmbientPulse()290 private void maybeEndAmbientPulse() { 291 if (mNotificationPanel.hasPulsingNotifications() && 292 !mHeadsUpManager.hasNotifications()) { 293 // We were showing a pulse for a notification, but no notifications are pulsing anymore. 294 // Finish the pulse. 295 mDozeScrimController.pulseOutNow(); 296 } 297 } 298 299 @Override updateNotificationViews(final String reason)300 public void updateNotificationViews(final String reason) { 301 // The function updateRowStates depends on both of these being non-null, so check them here. 302 // We may be called before they are set from DeviceProvisionedController's callback. 303 if (mScrimController == null) return; 304 305 // Do not modify the notifications during collapse. 306 if (isCollapsing()) { 307 mShadeController.addPostCollapseAction(() -> updateNotificationViews(reason)); 308 return; 309 } 310 311 mViewHierarchyManager.updateNotificationViews(); 312 313 mNotificationPanel.updateNotificationViews(reason); 314 } 315 onNotificationRemoved(String key, StatusBarNotification old)316 public void onNotificationRemoved(String key, StatusBarNotification old) { 317 if (SPEW) Log.d(TAG, "removeNotification key=" + key + " old=" + old); 318 319 if (old != null) { 320 if (CLOSE_PANEL_WHEN_EMPTIED && !hasActiveNotifications() 321 && !mNotificationPanel.isTracking() && !mNotificationPanel.isQsExpanded()) { 322 if (mStatusBarStateController.getState() == StatusBarState.SHADE) { 323 mCommandQueue.animateCollapsePanels(); 324 } else if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED 325 && !isCollapsing()) { 326 mStatusBarStateController.setState(StatusBarState.KEYGUARD); 327 } 328 } 329 } 330 } 331 hasActiveNotifications()332 public boolean hasActiveNotifications() { 333 return mEntryManager.hasActiveNotifications(); 334 } 335 336 @Override onUserSwitched(int newUserId)337 public void onUserSwitched(int newUserId) { 338 // Begin old BaseStatusBar.userSwitched 339 mHeadsUpManager.setUser(newUserId); 340 // End old BaseStatusBar.userSwitched 341 if (MULTIUSER_DEBUG) mNotificationPanelDebugText.setText("USER " + newUserId); 342 mCommandQueue.animateCollapsePanels(); 343 if (mReinflateNotificationsOnUserSwitched) { 344 updateNotificationsOnDensityOrFontScaleChanged(); 345 mReinflateNotificationsOnUserSwitched = false; 346 } 347 if (mDispatchUiModeChangeOnUserSwitched) { 348 updateNotificationOnUiModeChanged(); 349 mDispatchUiModeChangeOnUserSwitched = false; 350 } 351 updateNotificationViews("user switched"); 352 mMediaManager.clearCurrentMediaNotification(); 353 mStatusBar.setLockscreenUser(newUserId); 354 updateMediaMetaData(true, false); 355 } 356 357 @Override onBindRow(ExpandableNotificationRow row)358 public void onBindRow(ExpandableNotificationRow row) { 359 row.setAboveShelfChangedListener(mAboveShelfObserver); 360 row.setSecureStateProvider(mKeyguardStateController::canDismissLockScreen); 361 } 362 363 @Override isPresenterFullyCollapsed()364 public boolean isPresenterFullyCollapsed() { 365 return mNotificationPanel.isFullyCollapsed(); 366 } 367 368 @Override onActivated(ActivatableNotificationView view)369 public void onActivated(ActivatableNotificationView view) { 370 onActivated(); 371 if (view != null) mNotificationPanel.setActivatedChild(view); 372 } 373 onActivated()374 public void onActivated() { 375 mLockscreenGestureLogger.write( 376 MetricsEvent.ACTION_LS_NOTE, 377 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); 378 mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_NOTIFICATION_FALSE_TOUCH); 379 mNotificationPanel.showTransientIndication(R.string.notification_tap_again); 380 ActivatableNotificationView previousView = mNotificationPanel.getActivatedChild(); 381 if (previousView != null) { 382 previousView.makeInactive(true /* animate */); 383 } 384 } 385 386 @Override onActivationReset(ActivatableNotificationView view)387 public void onActivationReset(ActivatableNotificationView view) { 388 if (view == mNotificationPanel.getActivatedChild()) { 389 mNotificationPanel.setActivatedChild(null); 390 mKeyguardIndicationController.hideTransientIndication(); 391 } 392 } 393 394 @Override updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation)395 public void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation) { 396 mMediaManager.updateMediaMetaData(metaDataChanged, allowEnterAnimation); 397 } 398 399 @Override getMaxNotificationsWhileLocked(boolean recompute)400 public int getMaxNotificationsWhileLocked(boolean recompute) { 401 if (recompute) { 402 mMaxKeyguardNotifications = Math.max(1, 403 mNotificationPanel.computeMaxKeyguardNotifications( 404 mMaxAllowedKeyguardNotifications)); 405 return mMaxKeyguardNotifications; 406 } 407 return mMaxKeyguardNotifications; 408 } 409 410 @Override onUpdateRowStates()411 public void onUpdateRowStates() { 412 mNotificationPanel.onUpdateRowStates(); 413 } 414 415 @Override onExpandClicked(NotificationEntry clickedEntry, boolean nowExpanded)416 public void onExpandClicked(NotificationEntry clickedEntry, boolean nowExpanded) { 417 mHeadsUpManager.setExpanded(clickedEntry, nowExpanded); 418 if (nowExpanded) { 419 if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { 420 mShadeController.goToLockedShade(clickedEntry.getRow()); 421 } else if (clickedEntry.isSensitive() 422 && mDynamicPrivacyController.isInLockedDownShade()) { 423 mStatusBarStateController.setLeaveOpenOnKeyguardHide(true); 424 mActivityStarter.dismissKeyguardThenExecute(() -> false /* dismissAction */ 425 , null /* cancelRunnable */, false /* afterKeyguardGone */); 426 } 427 } 428 } 429 430 @Override isDeviceInVrMode()431 public boolean isDeviceInVrMode() { 432 return mVrMode; 433 } 434 onLockedNotificationImportanceChange(OnDismissAction dismissAction)435 private void onLockedNotificationImportanceChange(OnDismissAction dismissAction) { 436 mStatusBarStateController.setLeaveOpenOnKeyguardHide(true); 437 mActivityStarter.dismissKeyguardThenExecute(dismissAction, null, 438 true /* afterKeyguardGone */); 439 } 440 441 private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() { 442 @Override 443 public void onVrStateChanged(boolean enabled) { 444 mVrMode = enabled; 445 } 446 }; 447 448 private final CheckSaveListener mCheckSaveListener = new CheckSaveListener() { 449 @Override 450 public void checkSave(Runnable saveImportance, StatusBarNotification sbn) { 451 // If the user has security enabled, show challenge if the setting is changed. 452 if (mLockscreenUserManager.isLockscreenPublicMode(sbn.getUser().getIdentifier()) 453 && mKeyguardManager.isKeyguardLocked()) { 454 onLockedNotificationImportanceChange(() -> { 455 saveImportance.run(); 456 return true; 457 }); 458 } else { 459 saveImportance.run(); 460 } 461 } 462 }; 463 464 private final OnSettingsClickListener mOnSettingsClickListener = new OnSettingsClickListener() { 465 @Override 466 public void onSettingsClick(String key) { 467 try { 468 mBarService.onNotificationSettingsViewed(key); 469 } catch (RemoteException e) { 470 // if we're here we're dead 471 } 472 } 473 }; 474 475 private final NotificationInterruptSuppressor mInterruptSuppressor = 476 new NotificationInterruptSuppressor() { 477 @Override 478 public String getName() { 479 return TAG; 480 } 481 482 @Override 483 public boolean suppressAwakeHeadsUp(NotificationEntry entry) { 484 final StatusBarNotification sbn = entry.getSbn(); 485 if (mStatusBar.isOccluded()) { 486 boolean devicePublic = mLockscreenUserManager 487 .isLockscreenPublicMode(mLockscreenUserManager.getCurrentUserId()); 488 boolean userPublic = devicePublic 489 || mLockscreenUserManager.isLockscreenPublicMode(sbn.getUserId()); 490 boolean needsRedaction = mLockscreenUserManager.needsRedaction(entry); 491 if (userPublic && needsRedaction) { 492 // TODO(b/135046837): we can probably relax this with dynamic privacy 493 return true; 494 } 495 } 496 497 if (!mCommandQueue.panelsEnabled()) { 498 if (DEBUG) { 499 Log.d(TAG, "No heads up: disabled panel : " + sbn.getKey()); 500 } 501 return true; 502 } 503 504 if (sbn.getNotification().fullScreenIntent != null) { 505 // we don't allow head-up on the lockscreen (unless there's a 506 // "showWhenLocked" activity currently showing) if 507 // the potential HUN has a fullscreen intent 508 if (mKeyguardStateController.isShowing() && !mStatusBar.isOccluded()) { 509 if (DEBUG) { 510 Log.d(TAG, "No heads up: entry has fullscreen intent on lockscreen " 511 + sbn.getKey()); 512 } 513 return true; 514 } 515 516 if (mAccessibilityManager.isTouchExplorationEnabled()) { 517 if (DEBUG) { 518 Log.d(TAG, "No heads up: accessible fullscreen: " + sbn.getKey()); 519 } 520 return true; 521 } 522 } 523 return false; 524 } 525 526 @Override 527 public boolean suppressAwakeInterruptions(NotificationEntry entry) { 528 return isDeviceInVrMode(); 529 } 530 531 @Override 532 public boolean suppressInterruptions(NotificationEntry entry) { 533 return mStatusBar.areNotificationAlertsDisabled(); 534 } 535 }; 536 } 537