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