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