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