1 /* 2 * Copyright (C) 2008 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 static android.content.res.Configuration.ORIENTATION_PORTRAIT; 20 21 import static com.android.systemui.ScreenDecorations.DisplayCutoutView.boundsFromDirection; 22 import static com.android.systemui.SysUiServiceProvider.getComponent; 23 24 import static java.lang.Float.isNaN; 25 26 import android.annotation.Nullable; 27 import android.content.Context; 28 import android.content.res.Configuration; 29 import android.graphics.Point; 30 import android.graphics.Rect; 31 import android.util.AttributeSet; 32 import android.util.EventLog; 33 import android.util.Pair; 34 import android.view.Display; 35 import android.view.DisplayCutout; 36 import android.view.Gravity; 37 import android.view.MotionEvent; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.WindowInsets; 41 import android.view.accessibility.AccessibilityEvent; 42 import android.widget.FrameLayout; 43 import android.widget.LinearLayout; 44 45 import com.android.systemui.Dependency; 46 import com.android.systemui.EventLogTags; 47 import com.android.systemui.R; 48 import com.android.systemui.plugins.DarkIconDispatcher; 49 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; 50 import com.android.systemui.statusbar.CommandQueue; 51 import com.android.systemui.util.leak.RotationUtils; 52 53 import java.util.Objects; 54 55 public class PhoneStatusBarView extends PanelBar { 56 private static final String TAG = "PhoneStatusBarView"; 57 private static final boolean DEBUG = StatusBar.DEBUG; 58 private static final boolean DEBUG_GESTURES = false; 59 private static final int NO_VALUE = Integer.MIN_VALUE; 60 private final CommandQueue mCommandQueue; 61 62 StatusBar mBar; 63 64 boolean mIsFullyOpenedPanel = false; 65 private ScrimController mScrimController; 66 private float mMinFraction; 67 private Runnable mHideExpandedRunnable = new Runnable() { 68 @Override 69 public void run() { 70 if (mPanelFraction == 0.0f) { 71 mBar.makeExpandedInvisible(); 72 } 73 } 74 }; 75 private DarkReceiver mBattery; 76 private int mLastOrientation; 77 private int mRotationOrientation; 78 @Nullable 79 private View mCenterIconSpace; 80 @Nullable 81 private View mCutoutSpace; 82 @Nullable 83 private DisplayCutout mDisplayCutout; 84 /** 85 * Draw this many pixels into the left/right side of the cutout to optimally use the space 86 */ 87 private int mCutoutSideNudge = 0; 88 private int mStatusBarHeight; 89 private boolean mHeadsUpVisible; 90 PhoneStatusBarView(Context context, AttributeSet attrs)91 public PhoneStatusBarView(Context context, AttributeSet attrs) { 92 super(context, attrs); 93 94 mCommandQueue = getComponent(context, CommandQueue.class); 95 } 96 setBar(StatusBar bar)97 public void setBar(StatusBar bar) { 98 mBar = bar; 99 } 100 setScrimController(ScrimController scrimController)101 public void setScrimController(ScrimController scrimController) { 102 mScrimController = scrimController; 103 } 104 105 @Override onFinishInflate()106 public void onFinishInflate() { 107 mBattery = findViewById(R.id.battery); 108 mCutoutSpace = findViewById(R.id.cutout_space_view); 109 mCenterIconSpace = findViewById(R.id.centered_icon_area); 110 111 updateResources(); 112 } 113 114 @Override onAttachedToWindow()115 protected void onAttachedToWindow() { 116 super.onAttachedToWindow(); 117 // Always have Battery meters in the status bar observe the dark/light modes. 118 Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mBattery); 119 if (updateOrientationAndCutout(getResources().getConfiguration().orientation)) { 120 updateLayoutForCutout(); 121 } 122 } 123 124 @Override onDetachedFromWindow()125 protected void onDetachedFromWindow() { 126 super.onDetachedFromWindow(); 127 Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mBattery); 128 mDisplayCutout = null; 129 } 130 131 @Override onConfigurationChanged(Configuration newConfig)132 protected void onConfigurationChanged(Configuration newConfig) { 133 super.onConfigurationChanged(newConfig); 134 updateResources(); 135 136 // May trigger cutout space layout-ing 137 if (updateOrientationAndCutout(newConfig.orientation)) { 138 updateLayoutForCutout(); 139 requestLayout(); 140 } 141 } 142 143 @Override onApplyWindowInsets(WindowInsets insets)144 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 145 if (updateOrientationAndCutout(mLastOrientation)) { 146 updateLayoutForCutout(); 147 requestLayout(); 148 } 149 return super.onApplyWindowInsets(insets); 150 } 151 152 /** 153 * 154 * @param newOrientation may pass NO_VALUE for no change 155 * @return boolean indicating if we need to update the cutout location / margins 156 */ updateOrientationAndCutout(int newOrientation)157 private boolean updateOrientationAndCutout(int newOrientation) { 158 boolean changed = false; 159 if (newOrientation != NO_VALUE) { 160 if (mLastOrientation != newOrientation) { 161 changed = true; 162 mLastOrientation = newOrientation; 163 } 164 mRotationOrientation = RotationUtils.getExactRotation(mContext); 165 } 166 167 if (!Objects.equals(getRootWindowInsets().getDisplayCutout(), mDisplayCutout)) { 168 changed = true; 169 mDisplayCutout = getRootWindowInsets().getDisplayCutout(); 170 } 171 172 return changed; 173 } 174 175 @Override panelEnabled()176 public boolean panelEnabled() { 177 return mCommandQueue.panelsEnabled(); 178 } 179 180 @Override onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event)181 public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) { 182 if (super.onRequestSendAccessibilityEventInternal(child, event)) { 183 // The status bar is very small so augment the view that the user is touching 184 // with the content of the status bar a whole. This way an accessibility service 185 // may announce the current item as well as the entire content if appropriate. 186 AccessibilityEvent record = AccessibilityEvent.obtain(); 187 onInitializeAccessibilityEvent(record); 188 dispatchPopulateAccessibilityEvent(record); 189 event.appendRecord(record); 190 return true; 191 } 192 return false; 193 } 194 195 @Override onPanelPeeked()196 public void onPanelPeeked() { 197 super.onPanelPeeked(); 198 mBar.makeExpandedVisible(false); 199 } 200 201 @Override onPanelCollapsed()202 public void onPanelCollapsed() { 203 super.onPanelCollapsed(); 204 // Close the status bar in the next frame so we can show the end of the animation. 205 post(mHideExpandedRunnable); 206 mIsFullyOpenedPanel = false; 207 } 208 removePendingHideExpandedRunnables()209 public void removePendingHideExpandedRunnables() { 210 removeCallbacks(mHideExpandedRunnable); 211 } 212 213 @Override onPanelFullyOpened()214 public void onPanelFullyOpened() { 215 super.onPanelFullyOpened(); 216 if (!mIsFullyOpenedPanel) { 217 mPanel.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 218 } 219 mIsFullyOpenedPanel = true; 220 } 221 222 @Override onTouchEvent(MotionEvent event)223 public boolean onTouchEvent(MotionEvent event) { 224 boolean barConsumedEvent = mBar.interceptTouchEvent(event); 225 226 if (DEBUG_GESTURES) { 227 if (event.getActionMasked() != MotionEvent.ACTION_MOVE) { 228 EventLog.writeEvent(EventLogTags.SYSUI_PANELBAR_TOUCH, 229 event.getActionMasked(), (int) event.getX(), (int) event.getY(), 230 barConsumedEvent ? 1 : 0); 231 } 232 } 233 234 return barConsumedEvent || super.onTouchEvent(event); 235 } 236 237 @Override onTrackingStarted()238 public void onTrackingStarted() { 239 super.onTrackingStarted(); 240 mBar.onTrackingStarted(); 241 mScrimController.onTrackingStarted(); 242 removePendingHideExpandedRunnables(); 243 } 244 245 @Override onClosingFinished()246 public void onClosingFinished() { 247 super.onClosingFinished(); 248 mBar.onClosingFinished(); 249 } 250 251 @Override onTrackingStopped(boolean expand)252 public void onTrackingStopped(boolean expand) { 253 super.onTrackingStopped(expand); 254 mBar.onTrackingStopped(expand); 255 } 256 257 @Override onExpandingFinished()258 public void onExpandingFinished() { 259 super.onExpandingFinished(); 260 mScrimController.onExpandingFinished(); 261 } 262 263 @Override onInterceptTouchEvent(MotionEvent event)264 public boolean onInterceptTouchEvent(MotionEvent event) { 265 return mBar.interceptTouchEvent(event) || super.onInterceptTouchEvent(event); 266 } 267 268 @Override panelScrimMinFractionChanged(float minFraction)269 public void panelScrimMinFractionChanged(float minFraction) { 270 if (isNaN(minFraction)) { 271 throw new IllegalArgumentException("minFraction cannot be NaN"); 272 } 273 if (mMinFraction != minFraction) { 274 mMinFraction = minFraction; 275 updateScrimFraction(); 276 } 277 } 278 279 @Override panelExpansionChanged(float frac, boolean expanded)280 public void panelExpansionChanged(float frac, boolean expanded) { 281 super.panelExpansionChanged(frac, expanded); 282 updateScrimFraction(); 283 if ((frac == 0 || frac == 1) && mBar.getNavigationBarView() != null) { 284 mBar.getNavigationBarView().onStatusBarPanelStateChanged(); 285 } 286 } 287 updateScrimFraction()288 private void updateScrimFraction() { 289 float scrimFraction = mPanelFraction; 290 if (mMinFraction < 1.0f) { 291 scrimFraction = Math.max((mPanelFraction - mMinFraction) / (1.0f - mMinFraction), 292 0); 293 } 294 mScrimController.setPanelExpansion(scrimFraction); 295 } 296 updateResources()297 public void updateResources() { 298 mCutoutSideNudge = getResources().getDimensionPixelSize( 299 R.dimen.display_cutout_margin_consumption); 300 301 boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 302 303 int statusBarPaddingTop = getResources().getDimensionPixelSize( 304 R.dimen.status_bar_padding_top); 305 int statusBarPaddingStart = getResources().getDimensionPixelSize( 306 R.dimen.status_bar_padding_start); 307 int statusBarPaddingEnd = getResources().getDimensionPixelSize( 308 R.dimen.status_bar_padding_end); 309 310 ViewGroup.LayoutParams layoutParams = getLayoutParams(); 311 mStatusBarHeight = getResources().getDimensionPixelSize(R.dimen.status_bar_height); 312 layoutParams.height = mStatusBarHeight; 313 314 View sbContents = findViewById(R.id.status_bar_contents); 315 sbContents.setPadding( 316 isRtl ? statusBarPaddingEnd : statusBarPaddingStart, 317 statusBarPaddingTop, 318 isRtl ? statusBarPaddingStart : statusBarPaddingEnd, 319 0); 320 321 findViewById(R.id.notification_lights_out).setPadding(0, statusBarPaddingStart, 0, 0); 322 323 setLayoutParams(layoutParams); 324 } 325 updateLayoutForCutout()326 private void updateLayoutForCutout() { 327 Pair<Integer, Integer> cornerCutoutMargins = cornerCutoutMargins(mDisplayCutout, 328 getDisplay(), mRotationOrientation, mStatusBarHeight); 329 updateCutoutLocation(cornerCutoutMargins); 330 updateSafeInsets(cornerCutoutMargins); 331 } 332 updateCutoutLocation(Pair<Integer, Integer> cornerCutoutMargins)333 private void updateCutoutLocation(Pair<Integer, Integer> cornerCutoutMargins) { 334 // Not all layouts have a cutout (e.g., Car) 335 if (mCutoutSpace == null) { 336 return; 337 } 338 339 if (mDisplayCutout == null || mDisplayCutout.isEmpty() 340 || mLastOrientation != ORIENTATION_PORTRAIT || cornerCutoutMargins != null) { 341 mCenterIconSpace.setVisibility(View.VISIBLE); 342 mCutoutSpace.setVisibility(View.GONE); 343 return; 344 } 345 346 mCenterIconSpace.setVisibility(View.GONE); 347 mCutoutSpace.setVisibility(View.VISIBLE); 348 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mCutoutSpace.getLayoutParams(); 349 350 Rect bounds = new Rect(); 351 boundsFromDirection(mDisplayCutout, Gravity.TOP, bounds); 352 353 bounds.left = bounds.left + mCutoutSideNudge; 354 bounds.right = bounds.right - mCutoutSideNudge; 355 lp.width = bounds.width(); 356 lp.height = bounds.height(); 357 } 358 updateSafeInsets(Pair<Integer, Integer> cornerCutoutMargins)359 private void updateSafeInsets(Pair<Integer, Integer> cornerCutoutMargins) { 360 // Depending on our rotation, we may have to work around a cutout in the middle of the view, 361 // or letterboxing from the right or left sides. 362 363 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 364 if (mDisplayCutout == null || mDisplayCutout.isEmpty() || cornerCutoutMargins == null) { 365 lp.leftMargin = 0; 366 lp.rightMargin = 0; 367 return; 368 } 369 lp.leftMargin = cornerCutoutMargins.first; 370 lp.rightMargin = cornerCutoutMargins.second; 371 372 // If we're already inset enough (e.g. on the status bar side), we can have 0 margin 373 WindowInsets insets = getRootWindowInsets(); 374 int leftInset = insets.getSystemWindowInsetLeft(); 375 int rightInset = insets.getSystemWindowInsetRight(); 376 if (lp.leftMargin <= leftInset) { 377 lp.leftMargin = 0; 378 } 379 if (lp.rightMargin <= rightInset) { 380 lp.rightMargin = 0; 381 } 382 } 383 384 /** 385 * Returns a Pair of integers where 386 * - Pair.first is the left margin inset 387 * - Pair.second is the right margin inset 388 */ cornerCutoutMargins(DisplayCutout cutout, Display display)389 public static Pair<Integer, Integer> cornerCutoutMargins(DisplayCutout cutout, 390 Display display) { 391 return cornerCutoutMargins(cutout, display, RotationUtils.ROTATION_NONE, -1); 392 } 393 cornerCutoutMargins(DisplayCutout cutout, Display display, int rotationOrientation, int statusBarHeight)394 private static Pair<Integer, Integer> cornerCutoutMargins(DisplayCutout cutout, 395 Display display, int rotationOrientation, int statusBarHeight) { 396 if (cutout == null) { 397 return null; 398 } 399 Point size = new Point(); 400 display.getRealSize(size); 401 402 if (rotationOrientation != RotationUtils.ROTATION_NONE) { 403 return new Pair<>(cutout.getSafeInsetLeft(), cutout.getSafeInsetRight()); 404 } 405 406 Rect bounds = new Rect(); 407 boundsFromDirection(cutout, Gravity.TOP, bounds); 408 409 if (statusBarHeight >= 0 && bounds.top > statusBarHeight) { 410 return null; 411 } 412 413 if (bounds.left <= 0) { 414 return new Pair<>(bounds.right, 0); 415 } 416 417 if (bounds.right >= size.x) { 418 return new Pair<>(0, size.x - bounds.left); 419 } 420 421 return null; 422 } 423 setHeadsUpVisible(boolean headsUpVisible)424 public void setHeadsUpVisible(boolean headsUpVisible) { 425 mHeadsUpVisible = headsUpVisible; 426 updateVisibility(); 427 } 428 429 @Override shouldPanelBeVisible()430 protected boolean shouldPanelBeVisible() { 431 return mHeadsUpVisible || super.shouldPanelBeVisible(); 432 } 433 } 434