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