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.statusbar.NotificationShadeWindowController; 38 import com.android.systemui.statusbar.policy.ConfigurationController; 39 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; 40 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; 41 42 import java.io.FileDescriptor; 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 60 private boolean mIsStatusBarExpanded = false; 61 private boolean mShouldAdjustInsets = false; 62 private StatusBar mStatusBar; 63 private View mNotificationShadeWindowView; 64 private View mNotificationPanelView; 65 private boolean mForceCollapsedUntilLayout = false; 66 67 private Region mTouchableRegion = new Region(); 68 private int mDisplayCutoutTouchableRegionSize; 69 private int mStatusBarHeight; 70 71 @Inject StatusBarTouchableRegionManager( Context context, NotificationShadeWindowController notificationShadeWindowController, ConfigurationController configurationController, HeadsUpManagerPhone headsUpManager )72 public StatusBarTouchableRegionManager( 73 Context context, 74 NotificationShadeWindowController notificationShadeWindowController, 75 ConfigurationController configurationController, 76 HeadsUpManagerPhone headsUpManager 77 ) { 78 mContext = context; 79 initResources(); 80 configurationController.addCallback(new ConfigurationListener() { 81 @Override 82 public void onDensityOrFontScaleChanged() { 83 initResources(); 84 } 85 86 @Override 87 public void onThemeChanged() { 88 initResources(); 89 } 90 }); 91 92 mHeadsUpManager = headsUpManager; 93 mHeadsUpManager.addListener( 94 new OnHeadsUpChangedListener() { 95 @Override 96 public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) { 97 if (Log.isLoggable(TAG, Log.WARN)) { 98 Log.w(TAG, "onHeadsUpPinnedModeChanged"); 99 } 100 updateTouchableRegion(); 101 } 102 }); 103 mHeadsUpManager.addHeadsUpPhoneListener( 104 new HeadsUpManagerPhone.OnHeadsUpPhoneListenerChange() { 105 @Override 106 public void onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway) { 107 if (!headsUpGoingAway) { 108 updateTouchableRegionAfterLayout(); 109 } else { 110 updateTouchableRegion(); 111 } 112 } 113 }); 114 115 mNotificationShadeWindowController = notificationShadeWindowController; 116 mNotificationShadeWindowController.setForcePluginOpenListener((forceOpen) -> { 117 updateTouchableRegion(); 118 }); 119 } 120 setup( @onNull StatusBar statusBar, @NonNull View notificationShadeWindowView)121 protected void setup( 122 @NonNull StatusBar statusBar, 123 @NonNull View notificationShadeWindowView) { 124 mStatusBar = statusBar; 125 mNotificationShadeWindowView = notificationShadeWindowView; 126 mNotificationPanelView = mNotificationShadeWindowView.findViewById(R.id.notification_panel); 127 } 128 129 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)130 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 131 pw.println("StatusBarTouchableRegionManager state:"); 132 pw.print(" mTouchableRegion="); 133 pw.println(mTouchableRegion); 134 } 135 136 /** 137 * Notify that the status bar panel gets expanded or collapsed. 138 * 139 * @param isExpanded True to notify expanded, false to notify collapsed. 140 */ setPanelExpanded(boolean isExpanded)141 void setPanelExpanded(boolean isExpanded) { 142 if (isExpanded != mIsStatusBarExpanded) { 143 mIsStatusBarExpanded = isExpanded; 144 if (isExpanded) { 145 // make sure our state is sane 146 mForceCollapsedUntilLayout = false; 147 } 148 updateTouchableRegion(); 149 } 150 } 151 152 /** 153 * Calculates the touch region needed for heads up notifications, taking into consideration 154 * any existing display cutouts (notch) 155 * @return the heads up notification touch area 156 */ calculateTouchableRegion()157 Region calculateTouchableRegion() { 158 // Update touchable region for HeadsUp notifications 159 final Region headsUpTouchableRegion = mHeadsUpManager.getTouchableRegion(); 160 if (headsUpTouchableRegion != null) { 161 mTouchableRegion.set(headsUpTouchableRegion); 162 } else { 163 // If there aren't any HUNs, update the touch region to the status bar 164 // width/height, potentially adjusting for a display cutout (notch) 165 mTouchableRegion.set(0, 0, mNotificationShadeWindowView.getWidth(), 166 mStatusBarHeight); 167 updateRegionForNotch(mTouchableRegion); 168 } 169 return mTouchableRegion; 170 } 171 initResources()172 private void initResources() { 173 Resources resources = mContext.getResources(); 174 mDisplayCutoutTouchableRegionSize = resources.getDimensionPixelSize( 175 com.android.internal.R.dimen.display_cutout_touchable_region_size); 176 mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext); 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