• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.statusbar.phone;
18 
19 import android.annotation.NonNull;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.graphics.Rect;
23 import android.graphics.Region;
24 import android.util.Log;
25 import android.view.DisplayCutout;
26 import android.view.Gravity;
27 import android.view.View;
28 import android.view.ViewTreeObserver;
29 import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
30 import android.view.WindowInsets;
31 
32 import com.android.internal.policy.SystemBarUtils;
33 import com.android.systemui.Dumpable;
34 import com.android.systemui.R;
35 import com.android.systemui.ScreenDecorations;
36 import com.android.systemui.dagger.SysUISingleton;
37 import com.android.systemui.shade.ShadeExpansionStateManager;
38 import com.android.systemui.statusbar.NotificationShadeWindowController;
39 import com.android.systemui.statusbar.policy.ConfigurationController;
40 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
41 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
42 
43 import java.io.PrintWriter;
44 
45 import javax.inject.Inject;
46 
47 /**
48  * Manages what parts of the status bar are touchable. Clients are primarily UI that display in the
49  * status bar even though the UI doesn't look like part of the status bar. Currently this consists
50  * of HeadsUpNotifications.
51  */
52 @SysUISingleton
53 public final class StatusBarTouchableRegionManager implements Dumpable {
54     private static final String TAG = "TouchableRegionManager";
55 
56     private final Context mContext;
57     private final HeadsUpManagerPhone mHeadsUpManager;
58     private final NotificationShadeWindowController mNotificationShadeWindowController;
59     private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
60 
61     private boolean mIsStatusBarExpanded = false;
62     private boolean mShouldAdjustInsets = false;
63     private CentralSurfaces mCentralSurfaces;
64     private View mNotificationShadeWindowView;
65     private View mNotificationPanelView;
66     private boolean mForceCollapsedUntilLayout = false;
67 
68     private Region mTouchableRegion = new Region();
69     private int mDisplayCutoutTouchableRegionSize;
70     private int mStatusBarHeight;
71 
72     private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener;
73 
74     @Inject
StatusBarTouchableRegionManager( Context context, NotificationShadeWindowController notificationShadeWindowController, ConfigurationController configurationController, HeadsUpManagerPhone headsUpManager, ShadeExpansionStateManager shadeExpansionStateManager, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController )75     public StatusBarTouchableRegionManager(
76             Context context,
77             NotificationShadeWindowController notificationShadeWindowController,
78             ConfigurationController configurationController,
79             HeadsUpManagerPhone headsUpManager,
80             ShadeExpansionStateManager shadeExpansionStateManager,
81             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController
82     ) {
83         mContext = context;
84         initResources();
85         configurationController.addCallback(new ConfigurationListener() {
86             @Override
87             public void onDensityOrFontScaleChanged() {
88                 initResources();
89             }
90 
91             @Override
92             public void onThemeChanged() {
93                 initResources();
94             }
95         });
96 
97         mHeadsUpManager = headsUpManager;
98         mHeadsUpManager.addListener(
99                 new OnHeadsUpChangedListener() {
100                     @Override
101                     public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) {
102                         if (Log.isLoggable(TAG, Log.WARN)) {
103                             Log.w(TAG, "onHeadsUpPinnedModeChanged");
104                         }
105                         updateTouchableRegion();
106                     }
107                 });
108         mHeadsUpManager.addHeadsUpPhoneListener(this::onHeadsUpGoingAwayStateChanged);
109 
110         mNotificationShadeWindowController = notificationShadeWindowController;
111         mNotificationShadeWindowController.setForcePluginOpenListener((forceOpen) -> {
112             updateTouchableRegion();
113         });
114 
115         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
116         shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
117 
118         mOnComputeInternalInsetsListener = this::onComputeInternalInsets;
119     }
120 
setup( @onNull CentralSurfaces centralSurfaces, @NonNull View notificationShadeWindowView)121     protected void setup(
122             @NonNull CentralSurfaces centralSurfaces,
123             @NonNull View notificationShadeWindowView) {
124         mCentralSurfaces = centralSurfaces;
125         mNotificationShadeWindowView = notificationShadeWindowView;
126         mNotificationPanelView = mNotificationShadeWindowView.findViewById(R.id.notification_panel);
127     }
128 
129     @Override
dump(PrintWriter pw, String[] args)130     public void dump(PrintWriter pw, String[] args) {
131         pw.println("StatusBarTouchableRegionManager state:");
132         pw.print("  mTouchableRegion=");
133         pw.println(mTouchableRegion);
134     }
135 
onShadeExpansionFullyChanged(Boolean isExpanded)136     private void onShadeExpansionFullyChanged(Boolean isExpanded) {
137         if (isExpanded != mIsStatusBarExpanded) {
138             mIsStatusBarExpanded = isExpanded;
139             if (isExpanded) {
140                 // make sure our state is sensible
141                 mForceCollapsedUntilLayout = false;
142             }
143             updateTouchableRegion();
144         }
145     }
146 
147     /**
148      * Calculates the touch region needed for heads up notifications, taking into consideration
149      * any existing display cutouts (notch)
150      * @return the heads up notification touch area
151      */
calculateTouchableRegion()152     public Region calculateTouchableRegion() {
153         // Update touchable region for HeadsUp notifications
154         final Region headsUpTouchableRegion = mHeadsUpManager.getTouchableRegion();
155         if (headsUpTouchableRegion != null) {
156             mTouchableRegion.set(headsUpTouchableRegion);
157         } else {
158             // If there aren't any HUNs, update the touch region to the status bar
159             // width/height, potentially adjusting for a display cutout (notch)
160             mTouchableRegion.set(0, 0, mNotificationShadeWindowView.getWidth(),
161                     mStatusBarHeight);
162             updateRegionForNotch(mTouchableRegion);
163         }
164         return mTouchableRegion;
165     }
166 
initResources()167     private void initResources() {
168         Resources resources = mContext.getResources();
169         mDisplayCutoutTouchableRegionSize = resources.getDimensionPixelSize(
170                 com.android.internal.R.dimen.display_cutout_touchable_region_size);
171         mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
172     }
173 
174     /**
175      * Set the touchable portion of the status bar based on what elements are visible.
176      */
updateTouchableRegion()177     public void updateTouchableRegion() {
178         boolean hasCutoutInset = (mNotificationShadeWindowView != null)
179                 && (mNotificationShadeWindowView.getRootWindowInsets() != null)
180                 && (mNotificationShadeWindowView.getRootWindowInsets().getDisplayCutout() != null);
181         boolean shouldObserve = mHeadsUpManager.hasPinnedHeadsUp()
182                         || mHeadsUpManager.isHeadsUpGoingAway()
183                         || mForceCollapsedUntilLayout
184                         || hasCutoutInset
185                         || mNotificationShadeWindowController.getForcePluginOpen();
186         if (shouldObserve == mShouldAdjustInsets) {
187             return;
188         }
189 
190         if (shouldObserve) {
191             mNotificationShadeWindowView.getViewTreeObserver()
192                     .addOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
193             mNotificationShadeWindowView.requestLayout();
194         } else {
195             mNotificationShadeWindowView.getViewTreeObserver()
196                     .removeOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
197         }
198         mShouldAdjustInsets = shouldObserve;
199     }
200 
201     /**
202      * Calls {@code updateTouchableRegion()} after a layout pass completes.
203      */
updateTouchableRegionAfterLayout()204     private void updateTouchableRegionAfterLayout() {
205         if (mNotificationPanelView != null) {
206             mForceCollapsedUntilLayout = true;
207             mNotificationPanelView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
208                 @Override
209                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
210                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
211                     if (!mNotificationPanelView.isVisibleToUser()) {
212                         mNotificationPanelView.removeOnLayoutChangeListener(this);
213                         mForceCollapsedUntilLayout = false;
214                         updateTouchableRegion();
215                     }
216                 }
217             });
218         }
219     }
220 
updateRegionForNotch(Region touchableRegion)221     public void updateRegionForNotch(Region touchableRegion) {
222         WindowInsets windowInsets = mNotificationShadeWindowView.getRootWindowInsets();
223         if (windowInsets == null) {
224             Log.w(TAG, "StatusBarWindowView is not attached.");
225             return;
226         }
227         DisplayCutout cutout = windowInsets.getDisplayCutout();
228         if (cutout == null) {
229             return;
230         }
231 
232         // Expand touchable region such that we also catch touches that just start below the notch
233         // area.
234         Rect bounds = new Rect();
235         ScreenDecorations.DisplayCutoutView.boundsFromDirection(cutout, Gravity.TOP, bounds);
236         bounds.offset(0, mDisplayCutoutTouchableRegionSize);
237         touchableRegion.union(bounds);
238     }
239 
240     /**
241      * Helper to let us know when calculating the region is not needed because we know the entire
242      * screen needs to be touchable.
243      */
shouldMakeEntireScreenTouchable()244     private boolean shouldMakeEntireScreenTouchable() {
245         // The touchable region is always the full area when expanded, whether we're showing the
246         // shade or the bouncer. It's also fully touchable when the screen off animation is playing
247         // since we don't want stray touches to go through the light reveal scrim to whatever is
248         // underneath.
249         return mIsStatusBarExpanded
250                 || mCentralSurfaces.isBouncerShowing()
251                 || mUnlockedScreenOffAnimationController.isAnimationPlaying();
252     }
253 
onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway)254     private void onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway) {
255         if (!headsUpGoingAway) {
256             updateTouchableRegionAfterLayout();
257         } else {
258             updateTouchableRegion();
259         }
260     }
261 
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info)262     private void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
263         if (shouldMakeEntireScreenTouchable()) {
264             return;
265         }
266 
267         // Update touch insets to include any area needed for touching features that live in
268         // the status bar (ie: heads up notifications)
269         info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
270         info.touchableRegion.set(calculateTouchableRegion());
271     }
272 }
273