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.statusbar.NotificationShadeWindowController;
38 import com.android.systemui.statusbar.policy.ConfigurationController;
39 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
40 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
41 
42 import java.io.FileDescriptor;
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 
60     private boolean mIsStatusBarExpanded = false;
61     private boolean mShouldAdjustInsets = false;
62     private StatusBar mStatusBar;
63     private View mNotificationShadeWindowView;
64     private View mNotificationPanelView;
65     private boolean mForceCollapsedUntilLayout = false;
66 
67     private Region mTouchableRegion = new Region();
68     private int mDisplayCutoutTouchableRegionSize;
69     private int mStatusBarHeight;
70 
71     @Inject
StatusBarTouchableRegionManager( Context context, NotificationShadeWindowController notificationShadeWindowController, ConfigurationController configurationController, HeadsUpManagerPhone headsUpManager )72     public StatusBarTouchableRegionManager(
73             Context context,
74             NotificationShadeWindowController notificationShadeWindowController,
75             ConfigurationController configurationController,
76             HeadsUpManagerPhone headsUpManager
77     ) {
78         mContext = context;
79         initResources();
80         configurationController.addCallback(new ConfigurationListener() {
81             @Override
82             public void onDensityOrFontScaleChanged() {
83                 initResources();
84             }
85 
86             @Override
87             public void onThemeChanged() {
88                 initResources();
89             }
90         });
91 
92         mHeadsUpManager = headsUpManager;
93         mHeadsUpManager.addListener(
94                 new OnHeadsUpChangedListener() {
95                     @Override
96                     public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) {
97                         if (Log.isLoggable(TAG, Log.WARN)) {
98                             Log.w(TAG, "onHeadsUpPinnedModeChanged");
99                         }
100                         updateTouchableRegion();
101                     }
102                 });
103         mHeadsUpManager.addHeadsUpPhoneListener(
104                 new HeadsUpManagerPhone.OnHeadsUpPhoneListenerChange() {
105                     @Override
106                     public void onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway) {
107                         if (!headsUpGoingAway) {
108                             updateTouchableRegionAfterLayout();
109                         } else {
110                             updateTouchableRegion();
111                         }
112                     }
113                 });
114 
115         mNotificationShadeWindowController = notificationShadeWindowController;
116         mNotificationShadeWindowController.setForcePluginOpenListener((forceOpen) -> {
117             updateTouchableRegion();
118         });
119     }
120 
setup( @onNull StatusBar statusBar, @NonNull View notificationShadeWindowView)121     protected void setup(
122             @NonNull StatusBar statusBar,
123             @NonNull View notificationShadeWindowView) {
124         mStatusBar = statusBar;
125         mNotificationShadeWindowView = notificationShadeWindowView;
126         mNotificationPanelView = mNotificationShadeWindowView.findViewById(R.id.notification_panel);
127     }
128 
129     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)130     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
131         pw.println("StatusBarTouchableRegionManager state:");
132         pw.print("  mTouchableRegion=");
133         pw.println(mTouchableRegion);
134     }
135 
136     /**
137      * Notify that the status bar panel gets expanded or collapsed.
138      *
139      * @param isExpanded True to notify expanded, false to notify collapsed.
140      */
setPanelExpanded(boolean isExpanded)141     void setPanelExpanded(boolean isExpanded) {
142         if (isExpanded != mIsStatusBarExpanded) {
143             mIsStatusBarExpanded = isExpanded;
144             if (isExpanded) {
145                 // make sure our state is sane
146                 mForceCollapsedUntilLayout = false;
147             }
148             updateTouchableRegion();
149         }
150     }
151 
152     /**
153      * Calculates the touch region needed for heads up notifications, taking into consideration
154      * any existing display cutouts (notch)
155      * @return the heads up notification touch area
156      */
calculateTouchableRegion()157     Region calculateTouchableRegion() {
158         // Update touchable region for HeadsUp notifications
159         final Region headsUpTouchableRegion = mHeadsUpManager.getTouchableRegion();
160         if (headsUpTouchableRegion != null) {
161             mTouchableRegion.set(headsUpTouchableRegion);
162         } else {
163             // If there aren't any HUNs, update the touch region to the status bar
164             // width/height, potentially adjusting for a display cutout (notch)
165             mTouchableRegion.set(0, 0, mNotificationShadeWindowView.getWidth(),
166                     mStatusBarHeight);
167             updateRegionForNotch(mTouchableRegion);
168         }
169         return mTouchableRegion;
170     }
171 
initResources()172     private void initResources() {
173         Resources resources = mContext.getResources();
174         mDisplayCutoutTouchableRegionSize = resources.getDimensionPixelSize(
175                 com.android.internal.R.dimen.display_cutout_touchable_region_size);
176         mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
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