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