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