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 20 import static com.android.systemui.Flags.truncatedStatusBarIconsFix; 21 22 import android.annotation.Nullable; 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.graphics.Insets; 26 import android.graphics.Rect; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.view.DisplayCutout; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.view.WindowInsets; 34 import android.view.accessibility.AccessibilityEvent; 35 import android.widget.FrameLayout; 36 import android.widget.LinearLayout; 37 38 import com.android.internal.policy.SystemBarUtils; 39 import com.android.systemui.Dependency; 40 import com.android.systemui.Gefingerpoken; 41 import com.android.systemui.plugins.DarkIconDispatcher; 42 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; 43 import com.android.systemui.res.R; 44 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; 45 import com.android.systemui.statusbar.policy.Clock; 46 import com.android.systemui.statusbar.window.StatusBarWindowController; 47 import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; 48 import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; 49 import com.android.systemui.util.leak.RotationUtils; 50 51 import java.util.Objects; 52 53 public class PhoneStatusBarView extends FrameLayout { 54 private static final String TAG = "PhoneStatusBarView"; 55 private final StatusBarContentInsetsProvider mContentInsetsProvider; 56 private final StatusBarWindowController mStatusBarWindowController; 57 58 private DarkReceiver mBattery; 59 private Clock mClock; 60 private int mRotationOrientation = -1; 61 @Nullable 62 private View mCutoutSpace; 63 @Nullable 64 private DisplayCutout mDisplayCutout; 65 @Nullable 66 private Rect mDisplaySize; 67 private int mStatusBarHeight; 68 @Nullable 69 private Gefingerpoken mTouchEventHandler; 70 private int mDensity; 71 private float mFontScale; 72 73 /** 74 * Draw this many pixels into the left/right side of the cutout to optimally use the space 75 */ 76 private int mCutoutSideNudge = 0; 77 PhoneStatusBarView(Context context, AttributeSet attrs)78 public PhoneStatusBarView(Context context, AttributeSet attrs) { 79 super(context, attrs); 80 mContentInsetsProvider = Dependency.get(StatusBarContentInsetsProvider.class); 81 mStatusBarWindowController = Dependency.get(StatusBarWindowController.class); 82 } 83 setTouchEventHandler(Gefingerpoken handler)84 void setTouchEventHandler(Gefingerpoken handler) { 85 mTouchEventHandler = handler; 86 } 87 init(StatusBarUserChipViewModel viewModel)88 void init(StatusBarUserChipViewModel viewModel) { 89 StatusBarUserSwitcherContainer container = findViewById(R.id.user_switcher_container); 90 StatusBarUserChipViewBinder.bind(container, viewModel); 91 } 92 93 @Override onFinishInflate()94 public void onFinishInflate() { 95 super.onFinishInflate(); 96 mBattery = findViewById(R.id.battery); 97 mClock = findViewById(R.id.clock); 98 mCutoutSpace = findViewById(R.id.cutout_space_view); 99 100 updateResources(); 101 } 102 103 @Override onAttachedToWindow()104 protected void onAttachedToWindow() { 105 super.onAttachedToWindow(); 106 // Always have Battery meters in the status bar observe the dark/light modes. 107 Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mBattery); 108 Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mClock); 109 if (updateDisplayParameters()) { 110 updateLayoutForCutout(); 111 if (truncatedStatusBarIconsFix()) { 112 updateWindowHeight(); 113 } 114 } 115 } 116 117 @Override onDetachedFromWindow()118 protected void onDetachedFromWindow() { 119 super.onDetachedFromWindow(); 120 Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mBattery); 121 Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mClock); 122 mDisplayCutout = null; 123 } 124 125 // Per b/300629388, we let the PhoneStatusBarView detect onConfigurationChanged to 126 // updateResources, instead of letting the PhoneStatusBarViewController detect onConfigChanged 127 // then notify PhoneStatusBarView. 128 @Override onConfigurationChanged(Configuration newConfig)129 protected void onConfigurationChanged(Configuration newConfig) { 130 super.onConfigurationChanged(newConfig); 131 updateResources(); 132 133 // May trigger cutout space layout-ing 134 if (updateDisplayParameters()) { 135 updateLayoutForCutout(); 136 requestLayout(); 137 } 138 if (truncatedStatusBarIconsFix()) { 139 updateWindowHeight(); 140 } 141 } 142 onDensityOrFontScaleChanged()143 void onDensityOrFontScaleChanged() { 144 mClock.onDensityOrFontScaleChanged(); 145 } 146 147 @Override onApplyWindowInsets(WindowInsets insets)148 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 149 if (updateDisplayParameters()) { 150 updateLayoutForCutout(); 151 requestLayout(); 152 } 153 return super.onApplyWindowInsets(insets); 154 } 155 156 /** 157 * @return boolean indicating if we need to update the cutout location / margins 158 */ updateDisplayParameters()159 private boolean updateDisplayParameters() { 160 boolean changed = false; 161 int newRotation = RotationUtils.getExactRotation(mContext); 162 if (newRotation != mRotationOrientation) { 163 changed = true; 164 mRotationOrientation = newRotation; 165 } 166 167 if (!Objects.equals(getRootWindowInsets().getDisplayCutout(), mDisplayCutout)) { 168 changed = true; 169 mDisplayCutout = getRootWindowInsets().getDisplayCutout(); 170 } 171 172 Configuration newConfiguration = mContext.getResources().getConfiguration(); 173 final Rect newSize = newConfiguration.windowConfiguration.getMaxBounds(); 174 if (!Objects.equals(newSize, mDisplaySize)) { 175 changed = true; 176 mDisplaySize = newSize; 177 } 178 179 int density = newConfiguration.densityDpi; 180 if (density != mDensity) { 181 changed = true; 182 mDensity = density; 183 } 184 float fontScale = newConfiguration.fontScale; 185 if (fontScale != mFontScale) { 186 changed = true; 187 mFontScale = fontScale; 188 } 189 return changed; 190 } 191 192 @Override onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event)193 public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) { 194 if (super.onRequestSendAccessibilityEventInternal(child, event)) { 195 // The status bar is very small so augment the view that the user is touching 196 // with the content of the status bar a whole. This way an accessibility service 197 // may announce the current item as well as the entire content if appropriate. 198 AccessibilityEvent record = AccessibilityEvent.obtain(); 199 onInitializeAccessibilityEvent(record); 200 dispatchPopulateAccessibilityEvent(record); 201 event.appendRecord(record); 202 return true; 203 } 204 return false; 205 } 206 207 @Override onTouchEvent(MotionEvent event)208 public boolean onTouchEvent(MotionEvent event) { 209 if (mTouchEventHandler == null) { 210 Log.w( 211 TAG, 212 String.format( 213 "onTouch: No touch handler provided; eating gesture at (%d,%d)", 214 (int) event.getX(), 215 (int) event.getY() 216 ) 217 ); 218 return true; 219 } 220 return mTouchEventHandler.onTouchEvent(event); 221 } 222 223 @Override onInterceptTouchEvent(MotionEvent event)224 public boolean onInterceptTouchEvent(MotionEvent event) { 225 mTouchEventHandler.onInterceptTouchEvent(event); 226 return super.onInterceptTouchEvent(event); 227 } 228 updateResources()229 public void updateResources() { 230 mCutoutSideNudge = getResources().getDimensionPixelSize( 231 R.dimen.display_cutout_margin_consumption); 232 233 updateStatusBarHeight(); 234 } 235 updateStatusBarHeight()236 private void updateStatusBarHeight() { 237 final int waterfallTopInset = 238 mDisplayCutout == null ? 0 : mDisplayCutout.getWaterfallInsets().top; 239 ViewGroup.LayoutParams layoutParams = getLayoutParams(); 240 mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext); 241 layoutParams.height = mStatusBarHeight - waterfallTopInset; 242 updateSystemIconsContainerHeight(); 243 updatePaddings(); 244 setLayoutParams(layoutParams); 245 } 246 updateSystemIconsContainerHeight()247 private void updateSystemIconsContainerHeight() { 248 View systemIconsContainer = findViewById(R.id.system_icons); 249 ViewGroup.LayoutParams layoutParams = systemIconsContainer.getLayoutParams(); 250 int newSystemIconsHeight = 251 getResources().getDimensionPixelSize(R.dimen.status_bar_system_icons_height); 252 if (layoutParams.height != newSystemIconsHeight) { 253 layoutParams.height = newSystemIconsHeight; 254 systemIconsContainer.setLayoutParams(layoutParams); 255 } 256 } 257 updatePaddings()258 private void updatePaddings() { 259 int statusBarPaddingStart = getResources().getDimensionPixelSize( 260 R.dimen.status_bar_padding_start); 261 262 findViewById(R.id.status_bar_contents).setPaddingRelative( 263 statusBarPaddingStart, 264 getResources().getDimensionPixelSize(R.dimen.status_bar_padding_top), 265 getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end), 266 0); 267 268 findViewById(R.id.notification_lights_out) 269 .setPaddingRelative(0, statusBarPaddingStart, 0, 0); 270 271 findViewById(R.id.system_icons).setPaddingRelative( 272 getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_start), 273 getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_top), 274 getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_end), 275 getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_bottom) 276 ); 277 } 278 updateLayoutForCutout()279 private void updateLayoutForCutout() { 280 updateStatusBarHeight(); 281 updateCutoutLocation(); 282 updateSafeInsets(); 283 } 284 updateCutoutLocation()285 private void updateCutoutLocation() { 286 // Not all layouts have a cutout (e.g., Car) 287 if (mCutoutSpace == null) { 288 return; 289 } 290 291 boolean hasCornerCutout = mContentInsetsProvider.currentRotationHasCornerCutout(); 292 if (mDisplayCutout == null || mDisplayCutout.isEmpty() || hasCornerCutout) { 293 mCutoutSpace.setVisibility(View.GONE); 294 return; 295 } 296 297 mCutoutSpace.setVisibility(View.VISIBLE); 298 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mCutoutSpace.getLayoutParams(); 299 300 Rect bounds = mDisplayCutout.getBoundingRectTop(); 301 302 bounds.left = bounds.left + mCutoutSideNudge; 303 bounds.right = bounds.right - mCutoutSideNudge; 304 lp.width = bounds.width(); 305 lp.height = bounds.height(); 306 } 307 updateSafeInsets()308 private void updateSafeInsets() { 309 Insets insets = mContentInsetsProvider 310 .getStatusBarContentInsetsForCurrentRotation(); 311 setPadding( 312 insets.left, 313 insets.top, 314 insets.right, 315 getPaddingBottom()); 316 } 317 updateWindowHeight()318 private void updateWindowHeight() { 319 mStatusBarWindowController.refreshStatusBarHeight(); 320 } 321 } 322