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