1 /*
2  * Copyright (C) 2020 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.NAVIGATION_HINT_BACK_ALT;
20 import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
21 import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
22 import static android.inputmethodservice.InputMethodService.IME_INVISIBLE;
23 import static android.inputmethodservice.InputMethodService.IME_VISIBLE;
24 import static android.view.Display.DEFAULT_DISPLAY;
25 import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
26 import static android.view.WindowInsets.Type.ime;
27 
28 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS;
29 import static com.android.systemui.navigationbar.NavigationBar.NavBarActionEvent.NAVBAR_ASSIST_LONGPRESS;
30 
31 import static org.junit.Assert.assertEquals;
32 import static org.junit.Assert.assertFalse;
33 import static org.junit.Assert.assertTrue;
34 import static org.mockito.ArgumentMatchers.any;
35 import static org.mockito.ArgumentMatchers.anyBoolean;
36 import static org.mockito.ArgumentMatchers.anyInt;
37 import static org.mockito.Matchers.eq;
38 import static org.mockito.Mockito.doNothing;
39 import static org.mockito.Mockito.doReturn;
40 import static org.mockito.Mockito.mock;
41 import static org.mockito.Mockito.spy;
42 import static org.mockito.Mockito.times;
43 import static org.mockito.Mockito.verify;
44 import static org.mockito.Mockito.when;
45 
46 import android.content.BroadcastReceiver;
47 import android.content.Context;
48 import android.content.IntentFilter;
49 import android.hardware.display.DisplayManagerGlobal;
50 import android.os.Handler;
51 import android.os.SystemClock;
52 import android.os.UserHandle;
53 import android.provider.DeviceConfig;
54 import android.provider.Settings;
55 import android.telecom.TelecomManager;
56 import android.testing.AndroidTestingRunner;
57 import android.testing.TestableLooper;
58 import android.testing.TestableLooper.RunWithLooper;
59 import android.view.Display;
60 import android.view.DisplayInfo;
61 import android.view.MotionEvent;
62 import android.view.View;
63 import android.view.WindowInsets;
64 import android.view.WindowManager;
65 import android.view.WindowMetrics;
66 import android.view.accessibility.AccessibilityManager;
67 import android.view.inputmethod.InputMethodManager;
68 
69 import androidx.test.filters.SmallTest;
70 
71 import com.android.internal.logging.MetricsLogger;
72 import com.android.internal.logging.UiEventLogger;
73 import com.android.systemui.SysuiTestCase;
74 import com.android.systemui.SysuiTestableContext;
75 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
76 import com.android.systemui.accessibility.SystemActions;
77 import com.android.systemui.assist.AssistManager;
78 import com.android.systemui.broadcast.BroadcastDispatcher;
79 import com.android.systemui.dump.DumpManager;
80 import com.android.systemui.model.SysUiState;
81 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
82 import com.android.systemui.plugins.statusbar.StatusBarStateController;
83 import com.android.systemui.recents.OverviewProxyService;
84 import com.android.systemui.recents.Recents;
85 import com.android.systemui.settings.UserTracker;
86 import com.android.systemui.statusbar.CommandQueue;
87 import com.android.systemui.statusbar.NotificationRemoteInputManager;
88 import com.android.systemui.statusbar.NotificationShadeDepthController;
89 import com.android.systemui.statusbar.phone.AutoHideController;
90 import com.android.systemui.statusbar.phone.LightBarController;
91 import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
92 import com.android.systemui.statusbar.phone.ShadeController;
93 import com.android.systemui.statusbar.phone.StatusBar;
94 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
95 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
96 import com.android.systemui.statusbar.policy.KeyguardStateController;
97 import com.android.systemui.utils.leaks.LeakCheckedTest;
98 import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
99 import com.android.wm.shell.pip.Pip;
100 
101 import org.junit.After;
102 import org.junit.Before;
103 import org.junit.Rule;
104 import org.junit.Test;
105 import org.junit.runner.RunWith;
106 import org.mockito.Mock;
107 import org.mockito.MockitoAnnotations;
108 import org.mockito.Spy;
109 
110 import java.util.Optional;
111 
112 @RunWith(AndroidTestingRunner.class)
113 @RunWithLooper(setAsMainLooper = true)
114 @SmallTest
115 public class NavigationBarTest extends SysuiTestCase {
116     private static final int EXTERNAL_DISPLAY_ID = 2;
117 
118     private NavigationBar mNavigationBar;
119     private NavigationBar mExternalDisplayNavigationBar;
120 
121     private SysuiTestableContext mSysuiTestableContextExternal;
122     @Mock
123     private OverviewProxyService mOverviewProxyService;
124     @Mock
125     private StatusBarStateController mStatusBarStateController;
126     @Mock
127     private NavigationModeController mNavigationModeController;
128     @Mock
129     private CommandQueue mCommandQueue;
130     private SysUiState mMockSysUiState;
131     @Mock
132     private Handler mHandler;
133     @Mock
134     private BroadcastDispatcher mBroadcastDispatcher;
135     @Mock
136     private UiEventLogger mUiEventLogger;
137     @Mock
138     EdgeBackGestureHandler.Factory mEdgeBackGestureHandlerFactory;
139     @Mock
140     EdgeBackGestureHandler mEdgeBackGestureHandler;
141     NavBarHelper mNavBarHelper;
142     @Mock
143     private LightBarController mLightBarController;
144     @Mock
145     private LightBarController.Factory mLightBarcontrollerFactory;
146     @Mock
147     private AutoHideController mAutoHideController;
148     @Mock
149     private AutoHideController.Factory mAutoHideControllerFactory;
150     @Mock
151     private WindowManager mWindowManager;
152     @Mock
153     private TelecomManager mTelecomManager;
154     @Mock
155     private InputMethodManager mInputMethodManager;
156     @Mock
157     private AssistManager mAssistManager;
158     @Mock
159     private StatusBar mStatusBar;
160 
161     @Rule
162     public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck();
163 
164     @Before
setup()165     public void setup() throws Exception {
166         MockitoAnnotations.initMocks(this);
167 
168         when(mEdgeBackGestureHandlerFactory.create(any(Context.class)))
169                 .thenReturn(mEdgeBackGestureHandler);
170         when(mLightBarcontrollerFactory.create(any(Context.class))).thenReturn(mLightBarController);
171         when(mAutoHideControllerFactory.create(any(Context.class))).thenReturn(mAutoHideController);
172         setupSysuiDependency();
173         // This class inflates views that call Dependency.get, thus these injections are still
174         // necessary.
175         mDependency.injectTestDependency(AssistManager.class, mAssistManager);
176         mDependency.injectMockDependency(KeyguardStateController.class);
177         mDependency.injectTestDependency(StatusBarStateController.class, mStatusBarStateController);
178         mDependency.injectMockDependency(NavigationBarController.class);
179         mDependency.injectTestDependency(EdgeBackGestureHandler.Factory.class,
180                 mEdgeBackGestureHandlerFactory);
181         mDependency.injectTestDependency(OverviewProxyService.class, mOverviewProxyService);
182         mDependency.injectTestDependency(NavigationModeController.class, mNavigationModeController);
183         TestableLooper.get(this).runWithLooper(() -> {
184             mNavBarHelper = spy(new NavBarHelper(mContext, mock(AccessibilityManager.class),
185                     mock(AccessibilityManagerWrapper.class),
186                     mock(AccessibilityButtonModeObserver.class), mOverviewProxyService,
187                     () -> mock(AssistManager.class), () -> Optional.of(mStatusBar),
188                     mock(NavigationModeController.class), mock(UserTracker.class),
189                     mock(DumpManager.class)));
190             mNavigationBar = createNavBar(mContext);
191             mExternalDisplayNavigationBar = createNavBar(mSysuiTestableContextExternal);
192         });
193     }
194 
195     @After
tearDown()196     public void tearDown() throws Exception {
197         DeviceConfig.resetToDefaults(
198                 Settings.RESET_MODE_PACKAGE_DEFAULTS, DeviceConfig.NAMESPACE_SYSTEMUI);
199     }
200 
setupSysuiDependency()201     private void setupSysuiDependency() {
202         Display display = new Display(DisplayManagerGlobal.getInstance(), EXTERNAL_DISPLAY_ID,
203                 new DisplayInfo(), DEFAULT_DISPLAY_ADJUSTMENTS);
204         mSysuiTestableContextExternal = (SysuiTestableContext) getContext().createDisplayContext(
205                 display);
206 
207         Display defaultDisplay = mContext.getDisplay();
208         when(mWindowManager.getDefaultDisplay()).thenReturn(defaultDisplay);
209         WindowMetrics metrics = mContext.getSystemService(WindowManager.class)
210                 .getMaximumWindowMetrics();
211         when(mWindowManager.getMaximumWindowMetrics()).thenReturn(metrics);
212         WindowMetrics currentWindowMetrics = mContext.getSystemService(WindowManager.class)
213                 .getCurrentWindowMetrics();
214         when(mWindowManager.getCurrentWindowMetrics()).thenReturn(currentWindowMetrics);
215         doNothing().when(mWindowManager).addView(any(), any());
216         doNothing().when(mWindowManager).removeViewImmediate(any());
217         mMockSysUiState = mock(SysUiState.class);
218         when(mMockSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mMockSysUiState);
219 
220         mContext.addMockSystemService(WindowManager.class, mWindowManager);
221         mSysuiTestableContextExternal.addMockSystemService(WindowManager.class, mWindowManager);
222     }
223 
224     @Test
testHomeLongPress()225     public void testHomeLongPress() {
226         mNavigationBar.onViewAttachedToWindow(mNavigationBar.createView(null));
227         mNavigationBar.onHomeLongClick(mNavigationBar.getView());
228 
229         verify(mUiEventLogger, times(1)).log(NAVBAR_ASSIST_LONGPRESS);
230     }
231 
232     @Test
testHomeLongPressWithCustomDuration()233     public void testHomeLongPressWithCustomDuration() throws Exception {
234         DeviceConfig.setProperties(
235                 new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_SYSTEMUI)
236                     .setLong(HOME_BUTTON_LONG_PRESS_DURATION_MS, 100)
237                     .build());
238         when(mNavBarHelper.getLongPressHomeEnabled()).thenReturn(true);
239         mNavigationBar.onViewAttachedToWindow(mNavigationBar.createView(null));
240 
241         mNavigationBar.onHomeTouch(mNavigationBar.getView(), MotionEvent.obtain(
242                 /*downTime=*/SystemClock.uptimeMillis(),
243                 /*eventTime=*/SystemClock.uptimeMillis(),
244                 /*action=*/MotionEvent.ACTION_DOWN,
245                 0, 0, 0
246         ));
247         verify(mHandler, times(1)).postDelayed(any(), eq(100L));
248 
249         mNavigationBar.onHomeTouch(mNavigationBar.getView(), MotionEvent.obtain(
250                 /*downTime=*/SystemClock.uptimeMillis(),
251                 /*eventTime=*/SystemClock.uptimeMillis(),
252                 /*action=*/MotionEvent.ACTION_UP,
253                 0, 0, 0
254         ));
255 
256         verify(mHandler, times(1)).removeCallbacks(any());
257     }
258 
259     @Test
testRegisteredWithDispatcher()260     public void testRegisteredWithDispatcher() {
261         mNavigationBar.onViewAttachedToWindow(mNavigationBar.createView(null));
262         verify(mBroadcastDispatcher).registerReceiverWithHandler(
263                 any(BroadcastReceiver.class),
264                 any(IntentFilter.class),
265                 any(Handler.class),
266                 any(UserHandle.class));
267     }
268 
269     @Test
testSetImeWindowStatusWhenImeSwitchOnDisplay()270     public void testSetImeWindowStatusWhenImeSwitchOnDisplay() {
271         // Create default & external NavBar fragment.
272         NavigationBar defaultNavBar = mNavigationBar;
273         NavigationBar externalNavBar = mExternalDisplayNavigationBar;
274         NotificationShadeWindowView mockShadeWindowView = mock(NotificationShadeWindowView.class);
275         WindowInsets windowInsets = new WindowInsets.Builder().setVisible(ime(), false).build();
276         doReturn(windowInsets).when(mockShadeWindowView).getRootWindowInsets();
277         doReturn(mockShadeWindowView).when(mStatusBar).getNotificationShadeWindowView();
278         doReturn(true).when(mockShadeWindowView).isAttachedToWindow();
279         doNothing().when(defaultNavBar).checkNavBarModes();
280         doNothing().when(externalNavBar).checkNavBarModes();
281         defaultNavBar.createView(null);
282         externalNavBar.createView(null);
283 
284         defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE,
285                 BACK_DISPOSITION_DEFAULT, true);
286 
287         // Verify IME window state will be updated in default NavBar & external NavBar state reset.
288         assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN,
289                 defaultNavBar.getNavigationIconHints());
290         assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
291         assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
292 
293         externalNavBar.setImeWindowStatus(EXTERNAL_DISPLAY_ID, null, IME_VISIBLE,
294                 BACK_DISPOSITION_DEFAULT, true);
295         defaultNavBar.setImeWindowStatus(
296                 DEFAULT_DISPLAY, null, IME_INVISIBLE, BACK_DISPOSITION_DEFAULT, false);
297         // Verify IME window state will be updated in external NavBar & default NavBar state reset.
298         assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN,
299                 externalNavBar.getNavigationIconHints());
300         assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
301         assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
302     }
303 
304     @Test
testSetImeWindowStatusWhenKeyguardLockingAndImeInsetsChange()305     public void testSetImeWindowStatusWhenKeyguardLockingAndImeInsetsChange() {
306         NotificationShadeWindowView mockShadeWindowView = mock(NotificationShadeWindowView.class);
307         doReturn(mockShadeWindowView).when(mStatusBar).getNotificationShadeWindowView();
308         doReturn(true).when(mockShadeWindowView).isAttachedToWindow();
309         doNothing().when(mNavigationBar).checkNavBarModes();
310         mNavigationBar.createView(null);
311         WindowInsets windowInsets = new WindowInsets.Builder().setVisible(ime(), false).build();
312         doReturn(windowInsets).when(mockShadeWindowView).getRootWindowInsets();
313 
314         // Verify navbar altered back icon when an app is showing IME
315         mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE,
316                 BACK_DISPOSITION_DEFAULT, true);
317         assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
318         assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
319 
320         // Verify navbar didn't alter and showing back icon when the keyguard is showing without
321         // requesting IME insets visible.
322         doReturn(true).when(mStatusBar).isKeyguardShowing();
323         mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE,
324                 BACK_DISPOSITION_DEFAULT, true);
325         assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
326         assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
327 
328         // Verify navbar altered and showing back icon when the keyguard is showing and
329         // requesting IME insets visible.
330         windowInsets = new WindowInsets.Builder().setVisible(ime(), true).build();
331         doReturn(windowInsets).when(mockShadeWindowView).getRootWindowInsets();
332         mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE,
333                 BACK_DISPOSITION_DEFAULT, true);
334         assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
335         assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
336     }
337 
338     @Test
testA11yEventAfterDetach()339     public void testA11yEventAfterDetach() {
340         View v = mNavigationBar.createView(null);
341         mNavigationBar.onViewAttachedToWindow(v);
342         verify(mNavBarHelper).registerNavTaskStateUpdater(any(
343                 NavBarHelper.NavbarTaskbarStateUpdater.class));
344         mNavigationBar.onViewDetachedFromWindow(v);
345         verify(mNavBarHelper).removeNavTaskStateUpdater(any(
346                 NavBarHelper.NavbarTaskbarStateUpdater.class));
347 
348         // Should be safe even though the internal view is now null.
349         mNavigationBar.updateAcessibilityStateFlags();
350     }
351 
createNavBar(Context context)352     private NavigationBar createNavBar(Context context) {
353         DeviceProvisionedController deviceProvisionedController =
354                 mock(DeviceProvisionedController.class);
355         when(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
356         NavigationBar.Factory factory = new NavigationBar.Factory(
357                 () -> mAssistManager,
358                 mock(AccessibilityManager.class),
359                 deviceProvisionedController,
360                 new MetricsLogger(),
361                 mOverviewProxyService,
362                 mNavigationModeController,
363                 mock(AccessibilityButtonModeObserver.class),
364                 mStatusBarStateController,
365                 mMockSysUiState,
366                 mBroadcastDispatcher,
367                 mCommandQueue,
368                 Optional.of(mock(Pip.class)),
369                 Optional.of(mock(LegacySplitScreen.class)),
370                 Optional.of(mock(Recents.class)),
371                 () -> Optional.of(mStatusBar),
372                 mock(ShadeController.class),
373                 mock(NotificationRemoteInputManager.class),
374                 mock(NotificationShadeDepthController.class),
375                 mock(SystemActions.class),
376                 mHandler,
377                 mock(NavigationBarOverlayController.class),
378                 mUiEventLogger,
379                 mNavBarHelper,
380                 mock(UserTracker.class),
381                 mLightBarController,
382                 mLightBarcontrollerFactory,
383                 mAutoHideController,
384                 mAutoHideControllerFactory,
385                 Optional.of(mTelecomManager),
386                 mInputMethodManager);
387         return spy(factory.create(context));
388     }
389 
processAllMessages()390     private void processAllMessages() {
391         TestableLooper.get(this).processAllMessages();
392     }
393 }
394