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