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.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
20 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE;
21 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
22 
23 import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG;
24 import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen;
25 
26 import android.content.Context;
27 import android.content.pm.ActivityInfo;
28 import android.content.res.Configuration;
29 import android.hardware.display.DisplayManager;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.RemoteException;
33 import android.os.Trace;
34 import android.os.UserHandle;
35 import android.provider.Settings;
36 import android.util.Log;
37 import android.util.SparseArray;
38 import android.view.Display;
39 import android.view.IWindowManager;
40 import android.view.View;
41 import android.view.WindowManagerGlobal;
42 
43 import androidx.annotation.NonNull;
44 import androidx.annotation.Nullable;
45 
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.internal.statusbar.RegisterStatusBarResult;
48 import com.android.settingslib.applications.InterestingConfigChanges;
49 import com.android.systemui.Dumpable;
50 import com.android.systemui.dagger.SysUISingleton;
51 import com.android.systemui.dagger.qualifiers.Main;
52 import com.android.systemui.dump.DumpManager;
53 import com.android.systemui.flags.FeatureFlags;
54 import com.android.systemui.flags.Flags;
55 import com.android.systemui.model.SysUiState;
56 import com.android.systemui.recents.OverviewProxyService;
57 import com.android.systemui.settings.DisplayTracker;
58 import com.android.systemui.shared.system.QuickStepContract;
59 import com.android.systemui.shared.system.TaskStackChangeListeners;
60 import com.android.systemui.statusbar.CommandQueue;
61 import com.android.systemui.statusbar.CommandQueue.Callbacks;
62 import com.android.systemui.statusbar.phone.AutoHideController;
63 import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
64 import com.android.systemui.statusbar.phone.LightBarController;
65 import com.android.systemui.statusbar.policy.ConfigurationController;
66 import com.android.systemui.util.settings.SecureSettings;
67 import com.android.wm.shell.back.BackAnimation;
68 import com.android.wm.shell.pip.Pip;
69 
70 import java.io.PrintWriter;
71 import java.util.Optional;
72 
73 import javax.inject.Inject;
74 
75 /** A controller to handle navigation bars. */
76 @SysUISingleton
77 public class NavigationBarController implements
78         Callbacks,
79         ConfigurationController.ConfigurationListener,
80         NavigationModeController.ModeChangedListener,
81         Dumpable {
82 
83     private static final String TAG = NavigationBarController.class.getSimpleName();
84 
85     private final Context mContext;
86     private final Handler mHandler;
87     private final NavigationBarComponent.Factory mNavigationBarComponentFactory;
88     private FeatureFlags mFeatureFlags;
89     private final SecureSettings mSecureSettings;
90     private final DisplayTracker mDisplayTracker;
91     private final DisplayManager mDisplayManager;
92     private final TaskbarDelegate mTaskbarDelegate;
93     private final NavBarHelper mNavBarHelper;
94     private int mNavMode;
95     @VisibleForTesting boolean mIsLargeScreen;
96 
97     /** A displayId - nav bar maps. */
98     @VisibleForTesting
99     SparseArray<NavigationBar> mNavigationBars = new SparseArray<>();
100 
101     // Tracks config changes that will actually recreate the nav bar
102     private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges(
103             ActivityInfo.CONFIG_FONT_SCALE
104                     | ActivityInfo.CONFIG_UI_MODE);
105 
106     @Inject
NavigationBarController(Context context, OverviewProxyService overviewProxyService, NavigationModeController navigationModeController, SysUiState sysUiFlagsContainer, CommandQueue commandQueue, @Main Handler mainHandler, ConfigurationController configurationController, NavBarHelper navBarHelper, TaskbarDelegate taskbarDelegate, NavigationBarComponent.Factory navigationBarComponentFactory, DumpManager dumpManager, AutoHideController autoHideController, LightBarController lightBarController, TaskStackChangeListeners taskStackChangeListeners, Optional<Pip> pipOptional, Optional<BackAnimation> backAnimation, FeatureFlags featureFlags, SecureSettings secureSettings, DisplayTracker displayTracker)107     public NavigationBarController(Context context,
108             OverviewProxyService overviewProxyService,
109             NavigationModeController navigationModeController,
110             SysUiState sysUiFlagsContainer,
111             CommandQueue commandQueue,
112             @Main Handler mainHandler,
113             ConfigurationController configurationController,
114             NavBarHelper navBarHelper,
115             TaskbarDelegate taskbarDelegate,
116             NavigationBarComponent.Factory navigationBarComponentFactory,
117             DumpManager dumpManager,
118             AutoHideController autoHideController,
119             LightBarController lightBarController,
120             TaskStackChangeListeners taskStackChangeListeners,
121             Optional<Pip> pipOptional,
122             Optional<BackAnimation> backAnimation,
123             FeatureFlags featureFlags,
124             SecureSettings secureSettings,
125             DisplayTracker displayTracker) {
126         mContext = context;
127         mHandler = mainHandler;
128         mNavigationBarComponentFactory = navigationBarComponentFactory;
129         mFeatureFlags = featureFlags;
130         mSecureSettings = secureSettings;
131         mDisplayTracker = displayTracker;
132         mDisplayManager = mContext.getSystemService(DisplayManager.class);
133         commandQueue.addCallback(this);
134         configurationController.addCallback(this);
135         mConfigChanges.applyNewConfig(mContext.getResources());
136         mNavMode = navigationModeController.addListener(this);
137         mNavBarHelper = navBarHelper;
138         mTaskbarDelegate = taskbarDelegate;
139         mTaskbarDelegate.setDependencies(commandQueue, overviewProxyService,
140                 navBarHelper, navigationModeController, sysUiFlagsContainer,
141                 dumpManager, autoHideController, lightBarController, pipOptional,
142                 backAnimation.orElse(null), taskStackChangeListeners);
143         mIsLargeScreen = isLargeScreen(mContext);
144         dumpManager.registerDumpable(this);
145     }
146 
147     @Override
onConfigChanged(Configuration newConfig)148     public void onConfigChanged(Configuration newConfig) {
149         boolean isOldConfigLargeScreen = mIsLargeScreen;
150         mIsLargeScreen = isLargeScreen(mContext);
151         boolean willApplyConfig = mConfigChanges.applyNewConfig(mContext.getResources());
152         boolean largeScreenChanged = mIsLargeScreen != isOldConfigLargeScreen;
153         // TODO(b/243765256): Disable this logging once b/243765256 is fixed.
154         Log.i(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig
155                 + " mTaskbarDelegate initialized=" + mTaskbarDelegate.isInitialized()
156                 + " willApplyConfigToNavbars=" + willApplyConfig
157                 + " navBarCount=" + mNavigationBars.size());
158         if (mTaskbarDelegate.isInitialized()) {
159             mTaskbarDelegate.onConfigurationChanged(newConfig);
160         }
161         // If we folded/unfolded while in 3 button, show navbar in folded state, hide in unfolded
162         if (largeScreenChanged && updateNavbarForTaskbar()) {
163             return;
164         }
165 
166         if (willApplyConfig) {
167             for (int i = 0; i < mNavigationBars.size(); i++) {
168                 recreateNavigationBar(mNavigationBars.keyAt(i));
169             }
170         } else {
171             for (int i = 0; i < mNavigationBars.size(); i++) {
172                 mNavigationBars.valueAt(i).onConfigurationChanged(newConfig);
173             }
174         }
175     }
176 
177     @Override
onNavigationModeChanged(int mode)178     public void onNavigationModeChanged(int mode) {
179         if (mNavMode == mode) {
180             return;
181         }
182         final int oldMode = mNavMode;
183         mNavMode = mode;
184         updateAccessibilityButtonModeIfNeeded();
185 
186         mHandler.post(() -> {
187             // create/destroy nav bar based on nav mode only in unfolded state
188             if (oldMode != mNavMode) {
189                 updateNavbarForTaskbar();
190             }
191             for (int i = 0; i < mNavigationBars.size(); i++) {
192                 NavigationBar navBar = mNavigationBars.valueAt(i);
193                 if (navBar == null) {
194                     continue;
195                 }
196                 navBar.getView().updateStates();
197             }
198         });
199     }
200 
updateAccessibilityButtonModeIfNeeded()201     private void updateAccessibilityButtonModeIfNeeded() {
202         final int mode = mSecureSettings.getIntForUser(
203                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
204                 ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
205 
206         // ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU is compatible under gestural or non-gestural
207         // mode, so we don't need to update it.
208         if (mode == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU) {
209             return;
210         }
211 
212         // ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR is incompatible under gestural mode. Need to
213         // force update to ACCESSIBILITY_BUTTON_MODE_GESTURE.
214         if (QuickStepContract.isGesturalMode(mNavMode)
215                 && mode == ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR) {
216             mSecureSettings.putIntForUser(
217                     Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_GESTURE,
218                     UserHandle.USER_CURRENT);
219             // ACCESSIBILITY_BUTTON_MODE_GESTURE is incompatible under non gestural mode. Need to
220             // force update to ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR.
221         } else if (!QuickStepContract.isGesturalMode(mNavMode)
222                 && mode == ACCESSIBILITY_BUTTON_MODE_GESTURE) {
223             mSecureSettings.putIntForUser(
224                     Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
225                     ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
226         }
227     }
228 
shouldCreateNavBarAndTaskBar(int displayId)229     private boolean shouldCreateNavBarAndTaskBar(int displayId) {
230         final IWindowManager wms = WindowManagerGlobal.getWindowManagerService();
231 
232         try {
233             return wms.hasNavigationBar(displayId);
234         } catch (RemoteException e) {
235             // Cannot get wms, just return false with warning message.
236             Log.w(TAG, "Cannot get WindowManager.");
237             return false;
238         }
239     }
240 
241     /** @see #initializeTaskbarIfNecessary() */
updateNavbarForTaskbar()242     private boolean updateNavbarForTaskbar() {
243         boolean taskbarShown = initializeTaskbarIfNecessary();
244         if (!taskbarShown && mNavigationBars.get(mContext.getDisplayId()) == null) {
245             createNavigationBar(mContext.getDisplay(), null, null);
246         }
247         return taskbarShown;
248     }
249 
250     /** @return {@code true} if taskbar is enabled, false otherwise */
initializeTaskbarIfNecessary()251     private boolean initializeTaskbarIfNecessary() {
252         // Enable for large screens or (phone AND flag is set); assuming phone = !mIsLargeScreen
253         boolean taskbarEnabled = (mIsLargeScreen || mFeatureFlags.isEnabled(
254                 Flags.HIDE_NAVBAR_WINDOW)) && shouldCreateNavBarAndTaskBar(mContext.getDisplayId());
255 
256         if (taskbarEnabled) {
257             Trace.beginSection("NavigationBarController#initializeTaskbarIfNecessary");
258             final int displayId = mContext.getDisplayId();
259             // Hint to NavBarHelper if we are replacing an existing bar to skip extra work
260             mNavBarHelper.setTogglingNavbarTaskbar(mNavigationBars.contains(displayId));
261             // Remove navigation bar when taskbar is showing
262             removeNavigationBar(displayId);
263             mTaskbarDelegate.init(displayId);
264             mNavBarHelper.setTogglingNavbarTaskbar(false);
265             Trace.endSection();
266 
267         } else {
268             mTaskbarDelegate.destroy();
269         }
270         return taskbarEnabled;
271     }
272 
273     @Override
onDisplayRemoved(int displayId)274     public void onDisplayRemoved(int displayId) {
275         removeNavigationBar(displayId);
276     }
277 
278     @Override
onDisplayReady(int displayId)279     public void onDisplayReady(int displayId) {
280         Display display = mDisplayManager.getDisplay(displayId);
281         mIsLargeScreen = isLargeScreen(mContext);
282         createNavigationBar(display, null /* savedState */, null /* result */);
283     }
284 
285     @Override
setNavigationBarLumaSamplingEnabled(int displayId, boolean enable)286     public void setNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
287         final NavigationBar navigationBar = getNavigationBar(displayId);
288         if (navigationBar != null) {
289             navigationBar.setNavigationBarLumaSamplingEnabled(enable);
290         }
291     }
292 
293     /**
294      * Recreates the navigation bar for the given display.
295      */
recreateNavigationBar(int displayId)296     private void recreateNavigationBar(int displayId) {
297         // TODO: Improve this flow so that we don't need to create a new nav bar but just
298         //       the view
299         Bundle savedState = new Bundle();
300         NavigationBar bar = mNavigationBars.get(displayId);
301         if (bar != null) {
302             bar.onSaveInstanceState(savedState);
303         }
304         removeNavigationBar(displayId);
305         createNavigationBar(mDisplayManager.getDisplay(displayId), savedState, null /* result */);
306     }
307 
308     // TODO(b/117478341): I use {@code includeDefaultDisplay} to make this method compatible to
309     // CarStatusBar because they have their own nav bar. Think about a better way for it.
310     /**
311      * Creates navigation bars when car/status bar initializes.
312      *
313      * @param includeDefaultDisplay {@code true} to create navigation bar on default display.
314      */
createNavigationBars(final boolean includeDefaultDisplay, RegisterStatusBarResult result)315     public void createNavigationBars(final boolean includeDefaultDisplay,
316             RegisterStatusBarResult result) {
317         updateAccessibilityButtonModeIfNeeded();
318 
319         // Don't need to create nav bar on the default display if we initialize TaskBar.
320         final boolean shouldCreateDefaultNavbar = includeDefaultDisplay
321                 && !initializeTaskbarIfNecessary();
322         Display[] displays = mDisplayTracker.getAllDisplays();
323         for (Display display : displays) {
324             if (shouldCreateDefaultNavbar
325                     || display.getDisplayId() != mDisplayTracker.getDefaultDisplayId()) {
326                 createNavigationBar(display, null /* savedState */, result);
327             }
328         }
329     }
330 
331     /**
332      * Adds a navigation bar on default display or an external display if the display supports
333      * system decorations.
334      *
335      * @param display the display to add navigation bar on.
336      */
337     @VisibleForTesting
createNavigationBar(Display display, Bundle savedState, RegisterStatusBarResult result)338     void createNavigationBar(Display display, Bundle savedState, RegisterStatusBarResult result) {
339         if (display == null) {
340             return;
341         }
342 
343         final int displayId = display.getDisplayId();
344         final boolean isOnDefaultDisplay = displayId == mDisplayTracker.getDefaultDisplayId();
345 
346         if (!shouldCreateNavBarAndTaskBar(displayId)) {
347             return;
348         }
349 
350         // We may show TaskBar on the default display for large screen device. Don't need to create
351         // navigation bar for this case.
352         if (isOnDefaultDisplay && initializeTaskbarIfNecessary()) {
353             return;
354         }
355 
356         final Context context = isOnDefaultDisplay
357                 ? mContext
358                 : mContext.createDisplayContext(display);
359         NavigationBarComponent component = mNavigationBarComponentFactory.create(
360                 context, savedState);
361         NavigationBar navBar = component.getNavigationBar();
362         navBar.init();
363         mNavigationBars.put(displayId, navBar);
364 
365         navBar.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
366             @Override
367             public void onViewAttachedToWindow(View v) {
368                 if (result != null) {
369                     navBar.setImeWindowStatus(display.getDisplayId(), result.mImeToken,
370                             result.mImeWindowVis, result.mImeBackDisposition,
371                             result.mShowImeSwitcher);
372                 }
373             }
374 
375             @Override
376             public void onViewDetachedFromWindow(View v) {
377                 v.removeOnAttachStateChangeListener(this);
378             }
379         });
380     }
381 
removeNavigationBar(int displayId)382     void removeNavigationBar(int displayId) {
383         NavigationBar navBar = mNavigationBars.get(displayId);
384         if (navBar != null) {
385             navBar.destroyView();
386             mNavigationBars.remove(displayId);
387         }
388     }
389 
390     /** @see NavigationBar#checkNavBarModes() */
checkNavBarModes(int displayId)391     public void checkNavBarModes(int displayId) {
392         NavigationBar navBar = mNavigationBars.get(displayId);
393         if (navBar != null) {
394             navBar.checkNavBarModes();
395         }
396     }
397 
398     /** @see NavigationBar#finishBarAnimations() */
finishBarAnimations(int displayId)399     public void finishBarAnimations(int displayId) {
400         NavigationBar navBar = mNavigationBars.get(displayId);
401         if (navBar != null) {
402             navBar.finishBarAnimations();
403         }
404     }
405 
406     /** @see NavigationBar#touchAutoDim() */
touchAutoDim(int displayId)407     public void touchAutoDim(int displayId) {
408         NavigationBar navBar = mNavigationBars.get(displayId);
409         if (navBar != null) {
410             navBar.touchAutoDim();
411         }
412     }
413 
414     /** @see NavigationBar#transitionTo(int, boolean) */
transitionTo(int displayId, @TransitionMode int barMode, boolean animate)415     public void transitionTo(int displayId, @TransitionMode int barMode, boolean animate) {
416         NavigationBar navBar = mNavigationBars.get(displayId);
417         if (navBar != null) {
418             navBar.transitionTo(barMode, animate);
419         }
420     }
421 
422     /** @see NavigationBar#disableAnimationsDuringHide(long) */
disableAnimationsDuringHide(int displayId, long delay)423     public void disableAnimationsDuringHide(int displayId, long delay) {
424         NavigationBar navBar = mNavigationBars.get(displayId);
425         if (navBar != null) {
426             navBar.disableAnimationsDuringHide(delay);
427         }
428     }
429 
430     /** @return {@link NavigationBarView} on the default display. */
getDefaultNavigationBarView()431     public @Nullable NavigationBarView getDefaultNavigationBarView() {
432         return getNavigationBarView(mDisplayTracker.getDefaultDisplayId());
433     }
434 
435     /**
436      * @param displayId the ID of display which Navigation bar is on
437      * @return {@link NavigationBarView} on the display with {@code displayId}.
438      *         {@code null} if no navigation bar on that display.
439      */
getNavigationBarView(int displayId)440     public @Nullable NavigationBarView getNavigationBarView(int displayId) {
441         NavigationBar navBar = getNavigationBar(displayId);
442         return (navBar == null) ? null : navBar.getView();
443     }
444 
getNavigationBar(int displayId)445     private @Nullable NavigationBar getNavigationBar(int displayId) {
446         return mNavigationBars.get(displayId);
447     }
448 
showPinningEnterExitToast(int displayId, boolean entering)449     public void showPinningEnterExitToast(int displayId, boolean entering) {
450         final NavigationBarView navBarView = getNavigationBarView(displayId);
451         if (navBarView != null) {
452             navBarView.showPinningEnterExitToast(entering);
453         } else if (displayId == mDisplayTracker.getDefaultDisplayId()
454                 && mTaskbarDelegate.isInitialized()) {
455             mTaskbarDelegate.showPinningEnterExitToast(entering);
456         }
457     }
458 
showPinningEscapeToast(int displayId)459     public void showPinningEscapeToast(int displayId) {
460         final NavigationBarView navBarView = getNavigationBarView(displayId);
461         if (navBarView != null) {
462             navBarView.showPinningEscapeToast();
463         } else if (displayId == mDisplayTracker.getDefaultDisplayId()
464                 && mTaskbarDelegate.isInitialized()) {
465             mTaskbarDelegate.showPinningEscapeToast();
466         }
467     }
468 
isOverviewEnabled(int displayId)469     public boolean isOverviewEnabled(int displayId) {
470         final NavigationBarView navBarView = getNavigationBarView(displayId);
471         if (navBarView != null) {
472             return navBarView.isOverviewEnabled();
473         } else {
474             return mTaskbarDelegate.isOverviewEnabled();
475         }
476     }
477 
478     /** @return {@link NavigationBar} on the default display. */
479     @Nullable
getDefaultNavigationBar()480     public NavigationBar getDefaultNavigationBar() {
481         return mNavigationBars.get(mDisplayTracker.getDefaultDisplayId());
482     }
483 
484     @Override
dump(@onNull PrintWriter pw, @NonNull String[] args)485     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
486         pw.println("mIsLargeScreen=" + mIsLargeScreen);
487         pw.println("mNavMode=" + mNavMode);
488         for (int i = 0; i < mNavigationBars.size(); i++) {
489             if (i > 0) {
490                 pw.println();
491             }
492             mNavigationBars.valueAt(i).dump(pw);
493         }
494     }
495 }
496