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