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.systemui.navigationbar; 18 19 import static android.app.StatusBarManager.WINDOW_NAVIGATION_BAR; 20 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; 21 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE; 22 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR; 23 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; 24 25 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE; 26 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; 27 28 import static com.google.common.truth.Truth.assertThat; 29 30 import static org.mockito.ArgumentMatchers.any; 31 import static org.mockito.ArgumentMatchers.anyBoolean; 32 import static org.mockito.ArgumentMatchers.anyInt; 33 import static org.mockito.Mockito.doAnswer; 34 import static org.mockito.Mockito.mock; 35 import static org.mockito.Mockito.times; 36 import static org.mockito.Mockito.verify; 37 import static org.mockito.Mockito.when; 38 39 import android.content.ComponentName; 40 import android.content.res.Configuration; 41 import android.os.Handler; 42 import android.platform.test.annotations.DisableFlags; 43 import android.platform.test.annotations.EnableFlags; 44 import android.provider.Flags; 45 import android.view.IWindowManager; 46 import android.view.accessibility.AccessibilityManager; 47 48 import androidx.test.filters.SmallTest; 49 import androidx.test.runner.AndroidJUnit4; 50 51 import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; 52 import com.android.systemui.SysuiTestCase; 53 import com.android.systemui.accessibility.AccessibilityButtonModeObserver; 54 import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver; 55 import com.android.systemui.accessibility.AccessibilityGestureTargetsObserver; 56 import com.android.systemui.accessibility.SystemActions; 57 import com.android.systemui.assist.AssistManager; 58 import com.android.systemui.dump.DumpManager; 59 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; 60 import com.android.systemui.recents.LauncherProxyService; 61 import com.android.systemui.settings.DisplayTracker; 62 import com.android.systemui.settings.UserTracker; 63 import com.android.systemui.statusbar.CommandQueue; 64 import com.android.systemui.statusbar.NotificationShadeWindowController; 65 import com.android.systemui.statusbar.phone.CentralSurfaces; 66 import com.android.systemui.statusbar.policy.ConfigurationController; 67 import com.android.systemui.statusbar.policy.FakeConfigurationController; 68 import com.android.systemui.statusbar.policy.KeyguardStateController; 69 70 import dagger.Lazy; 71 72 import org.junit.Before; 73 import org.junit.Test; 74 import org.junit.runner.RunWith; 75 import org.mockito.ArgumentCaptor; 76 import org.mockito.Captor; 77 import org.mockito.Mock; 78 import org.mockito.MockitoAnnotations; 79 80 import java.util.ArrayList; 81 import java.util.List; 82 import java.util.Optional; 83 import java.util.concurrent.Executor; 84 85 /** 86 * Tests for {@link NavBarHelper}. 87 */ 88 @RunWith(AndroidJUnit4.class) 89 @SmallTest 90 public class NavBarHelperTest extends SysuiTestCase { 91 92 private static final int DISPLAY_ID = 0; 93 private static final int WINDOW = WINDOW_NAVIGATION_BAR; 94 private static final int STATE_ID = 0; 95 96 @Mock 97 AccessibilityManager mAccessibilityManager; 98 @Mock 99 AccessibilityButtonModeObserver mAccessibilityButtonModeObserver; 100 @Mock 101 AccessibilityButtonTargetsObserver mAccessibilityButtonTargetObserver; 102 @Mock 103 AccessibilityGestureTargetsObserver mAccessibilityGestureTargetObserver; 104 @Mock 105 SystemActions mSystemActions; 106 @Mock 107 LauncherProxyService mLauncherProxyService; 108 @Mock 109 Lazy<AssistManager> mAssistManagerLazy; 110 @Mock 111 AssistManager mAssistManager; 112 @Mock 113 NavigationModeController mNavigationModeController; 114 @Mock 115 UserTracker mUserTracker; 116 @Mock 117 ComponentName mAssistantComponent; 118 @Mock 119 DumpManager mDumpManager; 120 @Mock 121 NavBarHelper.NavbarTaskbarStateUpdater mNavbarTaskbarStateUpdater; 122 @Mock 123 CommandQueue mCommandQueue; 124 @Mock 125 IWindowManager mWm; 126 @Mock 127 DisplayTracker mDisplayTracker; 128 @Mock 129 EdgeBackGestureHandler mEdgeBackGestureHandler; 130 @Mock 131 EdgeBackGestureHandler.Factory mEdgeBackGestureHandlerFactory; 132 @Mock 133 NotificationShadeWindowController mNotificationShadeWindowController; 134 @Mock 135 Handler mBgHandler; 136 137 @Captor ArgumentCaptor<Runnable> mRunnableArgumentCaptor; 138 ConfigurationController mConfigurationController = new FakeConfigurationController(); 139 140 private AccessibilityManager.AccessibilityServicesStateChangeListener 141 mAccessibilityServicesStateChangeListener; 142 143 private static final long ACCESSIBILITY_BUTTON_CLICKABLE_STATE = 144 SYSUI_STATE_A11Y_BUTTON_CLICKABLE | SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; 145 private NavBarHelper mNavBarHelper; 146 147 private final Executor mSynchronousExecutor = runnable -> runnable.run(); 148 149 @Before setup()150 public void setup() { 151 MockitoAnnotations.initMocks(this); 152 when(mAssistManagerLazy.get()).thenReturn(mAssistManager); 153 when(mAssistManager.getAssistInfoForUser(anyInt())).thenReturn(mAssistantComponent); 154 when(mUserTracker.getUserId()).thenReturn(1); 155 when(mDisplayTracker.getDefaultDisplayId()).thenReturn(0); 156 when(mEdgeBackGestureHandlerFactory.create(any())).thenReturn(mEdgeBackGestureHandler); 157 158 doAnswer((invocation) -> mAccessibilityServicesStateChangeListener = 159 invocation.getArgument(0)).when( 160 mAccessibilityManager).addAccessibilityServicesStateChangeListener(any()); 161 mNavBarHelper = new NavBarHelper(mContext, mAccessibilityManager, 162 mAccessibilityButtonModeObserver, mAccessibilityButtonTargetObserver, 163 mAccessibilityGestureTargetObserver, 164 mSystemActions, mLauncherProxyService, mAssistManagerLazy, 165 () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardStateController.class), 166 mNavigationModeController, mEdgeBackGestureHandlerFactory, mWm, mUserTracker, 167 mDisplayTracker, mNotificationShadeWindowController, mConfigurationController, 168 mDumpManager, mCommandQueue, mSynchronousExecutor, mBgHandler); 169 } 170 171 @Test registerListenersInCtor()172 public void registerListenersInCtor() { 173 verify(mNavigationModeController, times(1)).addListener(mNavBarHelper); 174 verify(mLauncherProxyService, times(1)).addCallback(mNavBarHelper); 175 verify(mCommandQueue, times(1)).addCallback(any()); 176 } 177 178 @Test testSetupBarsRegistersListeners()179 public void testSetupBarsRegistersListeners() throws Exception { 180 mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 181 verify(mAccessibilityButtonModeObserver, times(1)).addListener(mNavBarHelper); 182 verify(mAccessibilityButtonTargetObserver, times(1)).addListener(mNavBarHelper); 183 verify(mAccessibilityGestureTargetObserver, times(1)).addListener(mNavBarHelper); 184 verify(mAccessibilityManager, times(1)).addAccessibilityServicesStateChangeListener( 185 mNavBarHelper); 186 verify(mAssistManager, times(1)).getAssistInfoForUser(anyInt()); 187 verify(mWm, times(1)).watchRotation(any(), anyInt()); 188 verify(mWm, times(1)).registerWallpaperVisibilityListener(any(), anyInt()); 189 verify(mEdgeBackGestureHandler, times(1)).onNavBarAttached(); 190 } 191 192 @Test testCleanupBarsUnregistersListeners()193 public void testCleanupBarsUnregistersListeners() throws Exception { 194 mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 195 mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 196 verify(mAccessibilityButtonModeObserver, times(1)).removeListener(mNavBarHelper); 197 verify(mAccessibilityButtonTargetObserver, times(1)).removeListener(mNavBarHelper); 198 verify(mAccessibilityGestureTargetObserver, times(1)).removeListener(mNavBarHelper); 199 verify(mAccessibilityManager, times(1)).removeAccessibilityServicesStateChangeListener( 200 mNavBarHelper); 201 verify(mWm, times(1)).removeRotationWatcher(any()); 202 verify(mWm, times(1)).unregisterWallpaperVisibilityListener(any(), anyInt()); 203 verify(mEdgeBackGestureHandler, times(1)).onNavBarDetached(); 204 } 205 206 @Test replacingBarsHint()207 public void replacingBarsHint() { 208 mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 209 mNavBarHelper.setTogglingNavbarTaskbar(true); 210 mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 211 mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 212 mNavBarHelper.setTogglingNavbarTaskbar(false); 213 // Use any state in cleanup to verify it was not called 214 verify(mAccessibilityButtonModeObserver, times(0)).removeListener(mNavBarHelper); 215 } 216 217 @Test callbacksFiredWhenRegistering()218 public void callbacksFiredWhenRegistering() { 219 mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 220 verify(mNavbarTaskbarStateUpdater, times(1)) 221 .updateAccessibilityServicesState(); 222 verify(mNavbarTaskbarStateUpdater, times(1)) 223 .updateAssistantAvailable(anyBoolean(), anyBoolean()); 224 verify(mBgHandler).post(mRunnableArgumentCaptor.capture()); 225 mRunnableArgumentCaptor.getValue().run(); 226 verify(mNavbarTaskbarStateUpdater, times(1)) 227 .updateRotationWatcherState(anyInt(), anyBoolean()); 228 verify(mNavbarTaskbarStateUpdater, times(1)) 229 .updateWallpaperVisibility(anyBoolean(), anyInt()); 230 } 231 232 @Test assistantCallbacksFiredAfterConnecting()233 public void assistantCallbacksFiredAfterConnecting() { 234 // 1st set of callbacks get called when registering 235 mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 236 237 mNavBarHelper.onConnectionChanged(false); 238 // assert no more callbacks fired 239 verify(mNavbarTaskbarStateUpdater, times(1)) 240 .updateAccessibilityServicesState(); 241 verify(mNavbarTaskbarStateUpdater, times(1)) 242 .updateAssistantAvailable(anyBoolean(), anyBoolean()); 243 244 mNavBarHelper.onConnectionChanged(true); 245 // assert no more callbacks fired 246 verify(mNavbarTaskbarStateUpdater, times(1)) 247 .updateAccessibilityServicesState(); 248 verify(mNavbarTaskbarStateUpdater, times(2)) 249 .updateAssistantAvailable(anyBoolean(), anyBoolean()); 250 } 251 252 @Test a11yCallbacksFiredAfterModeChange()253 public void a11yCallbacksFiredAfterModeChange() { 254 // 1st set of callbacks get called when registering 255 mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 256 257 mNavBarHelper.onAccessibilityButtonModeChanged(0); 258 verify(mNavbarTaskbarStateUpdater, times(2)) 259 .updateAccessibilityServicesState(); 260 verify(mNavbarTaskbarStateUpdater, times(1)) 261 .updateAssistantAvailable(anyBoolean(), anyBoolean()); 262 } 263 264 @Test assistantCallbacksFiredAfterNavModeChange()265 public void assistantCallbacksFiredAfterNavModeChange() { 266 // 1st set of callbacks get called when registering 267 mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 268 269 mNavBarHelper.onNavigationModeChanged(0); 270 verify(mNavbarTaskbarStateUpdater, times(1)) 271 .updateAccessibilityServicesState(); 272 verify(mNavbarTaskbarStateUpdater, times(2)) 273 .updateAssistantAvailable(anyBoolean(), anyBoolean()); 274 } 275 276 @Test removeListenerNoCallbacksFired()277 public void removeListenerNoCallbacksFired() { 278 // 1st set of callbacks get called when registering 279 mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 280 281 // Remove listener 282 mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 283 284 // Would have fired 2nd callback if not removed 285 mNavBarHelper.onAccessibilityButtonModeChanged(0); 286 287 // assert no more callbacks fired 288 verify(mNavbarTaskbarStateUpdater, times(1)) 289 .updateAccessibilityServicesState(); 290 verify(mNavbarTaskbarStateUpdater, times(1)) 291 .updateAssistantAvailable(anyBoolean(), anyBoolean()); 292 } 293 294 @Test initNavBarHelper_buttonModeNavBar_a11yButtonClickableState()295 public void initNavBarHelper_buttonModeNavBar_a11yButtonClickableState() { 296 when(mAccessibilityManager.getAccessibilityShortcutTargets(UserShortcutType.SOFTWARE)) 297 .thenReturn(createFakeShortcutTargets()); 298 299 mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 300 301 assertThat(mNavBarHelper.getA11yButtonState()).isEqualTo( 302 ACCESSIBILITY_BUTTON_CLICKABLE_STATE); 303 } 304 305 @Test initAccessibilityStateWithFloatingMenuModeAndTargets_disableClickableState()306 public void initAccessibilityStateWithFloatingMenuModeAndTargets_disableClickableState() { 307 when(mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()).thenReturn( 308 ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU); 309 310 mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 311 312 assertThat(mNavBarHelper.getA11yButtonState()).isEqualTo(/* disable_clickable_state */ 0); 313 } 314 315 @Test onA11yServicesStateChangedWithMultipleServices_a11yButtonClickableState()316 public void onA11yServicesStateChangedWithMultipleServices_a11yButtonClickableState() { 317 mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); 318 when(mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()).thenReturn( 319 ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR); 320 321 when(mAccessibilityManager.getAccessibilityShortcutTargets(UserShortcutType.SOFTWARE)) 322 .thenReturn(createFakeShortcutTargets()); 323 mAccessibilityServicesStateChangeListener.onAccessibilityServicesStateChanged( 324 mAccessibilityManager); 325 326 assertThat(mNavBarHelper.getA11yButtonState()).isEqualTo( 327 ACCESSIBILITY_BUTTON_CLICKABLE_STATE); 328 } 329 330 @Test saveMostRecentSysuiState()331 public void saveMostRecentSysuiState() { 332 mNavBarHelper.setWindowState(DISPLAY_ID, WINDOW, STATE_ID); 333 NavBarHelper.CurrentSysuiState state1 = mNavBarHelper.getCurrentSysuiState(); 334 335 // Update window state 336 int newState = STATE_ID + 1; 337 mNavBarHelper.setWindowState(DISPLAY_ID, WINDOW, newState); 338 NavBarHelper.CurrentSysuiState state2 = mNavBarHelper.getCurrentSysuiState(); 339 340 // Ensure we get most recent state back 341 assertThat(state1.mWindowState).isNotEqualTo(state2.mWindowState); 342 assertThat(state1.mWindowStateDisplayId).isEqualTo(state2.mWindowStateDisplayId); 343 assertThat(state2.mWindowState).isEqualTo(newState); 344 } 345 346 @Test ignoreNonNavbarSysuiState()347 public void ignoreNonNavbarSysuiState() { 348 mNavBarHelper.setWindowState(DISPLAY_ID, WINDOW, STATE_ID); 349 NavBarHelper.CurrentSysuiState state1 = mNavBarHelper.getCurrentSysuiState(); 350 351 // Update window state for other window type 352 int newState = STATE_ID + 1; 353 mNavBarHelper.setWindowState(DISPLAY_ID, WINDOW + 1, newState); 354 NavBarHelper.CurrentSysuiState state2 = mNavBarHelper.getCurrentSysuiState(); 355 356 // Ensure we get first state back 357 assertThat(state2.mWindowState).isEqualTo(state1.mWindowState); 358 assertThat(state2.mWindowState).isNotEqualTo(newState); 359 } 360 361 @Test configUpdatePropagatesToEdgeBackGestureHandler()362 public void configUpdatePropagatesToEdgeBackGestureHandler() { 363 mConfigurationController.onConfigurationChanged(Configuration.EMPTY); 364 verify(mEdgeBackGestureHandler, times(1)).onConfigurationChanged(any()); 365 } 366 367 @Test updateA11yState_navBarMode_softwareTargets_isClickable()368 public void updateA11yState_navBarMode_softwareTargets_isClickable() { 369 when(mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()).thenReturn( 370 ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR); 371 when(mAccessibilityManager.getAccessibilityShortcutTargets(UserShortcutType.SOFTWARE)) 372 .thenReturn(createFakeShortcutTargets()); 373 374 mNavBarHelper.updateA11yState(); 375 long state = mNavBarHelper.getA11yButtonState(); 376 assertThat(state & SYSUI_STATE_A11Y_BUTTON_CLICKABLE).isEqualTo( 377 SYSUI_STATE_A11Y_BUTTON_CLICKABLE); 378 assertThat(state & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE).isEqualTo( 379 SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE); 380 } 381 382 @Test 383 @DisableFlags(Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) updateA11yState_gestureMode_softwareTargets_isClickable()384 public void updateA11yState_gestureMode_softwareTargets_isClickable() { 385 when(mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()).thenReturn( 386 ACCESSIBILITY_BUTTON_MODE_GESTURE); 387 when(mAccessibilityManager.getAccessibilityShortcutTargets(UserShortcutType.SOFTWARE)) 388 .thenReturn(createFakeShortcutTargets()); 389 390 mNavBarHelper.updateA11yState(); 391 long state = mNavBarHelper.getA11yButtonState(); 392 assertThat(state & SYSUI_STATE_A11Y_BUTTON_CLICKABLE).isEqualTo( 393 SYSUI_STATE_A11Y_BUTTON_CLICKABLE); 394 assertThat(state & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE).isEqualTo( 395 SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE); 396 } 397 398 @Test 399 @EnableFlags(Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) updateA11yState_gestureNavMode_floatingButtonMode_gestureTargets_isClickable()400 public void updateA11yState_gestureNavMode_floatingButtonMode_gestureTargets_isClickable() { 401 mNavBarHelper.onNavigationModeChanged(NAV_BAR_MODE_GESTURAL); 402 when(mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()).thenReturn( 403 ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU); 404 when(mAccessibilityManager.getAccessibilityShortcutTargets(UserShortcutType.GESTURE)) 405 .thenReturn(createFakeShortcutTargets()); 406 407 mNavBarHelper.updateA11yState(); 408 long state = mNavBarHelper.getA11yButtonState(); 409 assertThat(state & SYSUI_STATE_A11Y_BUTTON_CLICKABLE).isEqualTo( 410 SYSUI_STATE_A11Y_BUTTON_CLICKABLE); 411 assertThat(state & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE).isEqualTo( 412 SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE); 413 } 414 415 @Test 416 @EnableFlags(Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) updateA11yState_navBarMode_gestureTargets_isNotClickable()417 public void updateA11yState_navBarMode_gestureTargets_isNotClickable() { 418 when(mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()).thenReturn( 419 ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR); 420 when(mAccessibilityManager.getAccessibilityShortcutTargets(UserShortcutType.GESTURE)) 421 .thenReturn(createFakeShortcutTargets()); 422 423 mNavBarHelper.updateA11yState(); 424 long state = mNavBarHelper.getA11yButtonState(); 425 assertThat(state & SYSUI_STATE_A11Y_BUTTON_CLICKABLE).isEqualTo(0); 426 assertThat(state & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE).isEqualTo(0); 427 } 428 429 @Test 430 @EnableFlags(Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) updateA11yState_singleTarget_clickableButNotLongClickable()431 public void updateA11yState_singleTarget_clickableButNotLongClickable() { 432 when(mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()).thenReturn( 433 ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR); 434 when(mAccessibilityManager.getAccessibilityShortcutTargets(UserShortcutType.SOFTWARE)) 435 .thenReturn(new ArrayList<>(List.of("a"))); 436 437 mNavBarHelper.updateA11yState(); 438 long state = mNavBarHelper.getA11yButtonState(); 439 assertThat(state & SYSUI_STATE_A11Y_BUTTON_CLICKABLE).isEqualTo( 440 SYSUI_STATE_A11Y_BUTTON_CLICKABLE); 441 assertThat(state & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE).isEqualTo(0); 442 } 443 createFakeShortcutTargets()444 private List<String> createFakeShortcutTargets() { 445 return new ArrayList<>(List.of("a", "b", "c", "d")); 446 } 447 } 448