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