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