1 /* 2 * Copyright (C) 2014 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 com.android.systemui.Flags.centralizedStatusBarHeightFix; 20 import static com.android.systemui.ScreenDecorations.DisplayCutoutView.boundsFromDirection; 21 import static com.android.systemui.util.Utils.getStatusBarHeaderHeightKeyguard; 22 23 import android.annotation.ColorInt; 24 import android.content.Context; 25 import android.content.res.Configuration; 26 import android.content.res.Resources; 27 import android.graphics.Color; 28 import android.graphics.Insets; 29 import android.graphics.Rect; 30 import android.graphics.drawable.Drawable; 31 import android.os.Trace; 32 import android.util.AttributeSet; 33 import android.util.TypedValue; 34 import android.view.DisplayCutout; 35 import android.view.Gravity; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.view.WindowInsets; 39 import android.widget.ImageView; 40 import android.widget.LinearLayout; 41 import android.widget.RelativeLayout; 42 import android.widget.TextView; 43 44 import androidx.annotation.Nullable; 45 import androidx.annotation.VisibleForTesting; 46 47 import com.android.settingslib.Utils; 48 import com.android.systemui.battery.BatteryMeterView; 49 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; 50 import com.android.systemui.res.R; 51 import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange; 52 import com.android.systemui.statusbar.phone.ui.TintedIconManager; 53 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; 54 import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; 55 import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; 56 57 import kotlinx.coroutines.flow.FlowKt; 58 import kotlinx.coroutines.flow.MutableStateFlow; 59 import kotlinx.coroutines.flow.StateFlow; 60 import kotlinx.coroutines.flow.StateFlowKt; 61 62 import java.io.PrintWriter; 63 import java.util.ArrayList; 64 65 /** 66 * The header group on Keyguard. 67 */ 68 public class KeyguardStatusBarView extends RelativeLayout { 69 70 private static final int LAYOUT_NONE = 0; 71 private static final int LAYOUT_CUTOUT = 1; 72 private static final int LAYOUT_NO_CUTOUT = 2; 73 74 private final ArrayList<Rect> mEmptyTintRect = new ArrayList<>(); 75 76 private boolean mShowPercentAvailable; 77 private boolean mBatteryCharging; 78 79 private TextView mCarrierLabel; 80 private ImageView mMultiUserAvatar; 81 private BatteryMeterView mBatteryView; 82 private StatusIconContainer mStatusIconContainer; 83 private StatusBarUserSwitcherContainer mUserSwitcherContainer; 84 85 private boolean mKeyguardUserSwitcherEnabled; 86 private boolean mKeyguardUserAvatarEnabled; 87 88 private boolean mIsPrivacyDotEnabled; 89 private int mSystemIconsSwitcherHiddenExpandedMargin; 90 private int mStatusBarPaddingEnd; 91 private int mMinDotWidth; 92 private View mSystemIconsContainer; 93 private View mSystemIcons; 94 private final MutableStateFlow<DarkChange> mDarkChange = StateFlowKt.MutableStateFlow( 95 DarkChange.EMPTY); 96 97 private View mCutoutSpace; 98 private ViewGroup mStatusIconArea; 99 private int mLayoutState = LAYOUT_NONE; 100 101 /** 102 * Draw this many pixels into the left/right side of the cutout to optimally use the space 103 */ 104 private int mCutoutSideNudge = 0; 105 106 private DisplayCutout mDisplayCutout; 107 private int mRoundedCornerPadding = 0; 108 // right and left padding applied to this view to account for cutouts and rounded corners 109 private Insets mPadding = Insets.of(0, 0, 0, 0); 110 111 /** 112 * The clipping on the top 113 */ 114 private int mTopClipping; 115 private final Rect mClipRect = new Rect(0, 0, 0, 0); 116 private boolean mIsUserSwitcherEnabled; 117 KeyguardStatusBarView(Context context, AttributeSet attrs)118 public KeyguardStatusBarView(Context context, AttributeSet attrs) { 119 super(context, attrs); 120 } 121 122 @Override onFinishInflate()123 protected void onFinishInflate() { 124 super.onFinishInflate(); 125 mSystemIconsContainer = findViewById(R.id.system_icons_container); 126 mSystemIcons = findViewById(R.id.system_icons); 127 mMultiUserAvatar = findViewById(R.id.multi_user_avatar); 128 mCarrierLabel = findViewById(R.id.keyguard_carrier_text); 129 mBatteryView = mSystemIconsContainer.findViewById(R.id.battery); 130 mCutoutSpace = findViewById(R.id.cutout_space_view); 131 mStatusIconArea = findViewById(R.id.status_icon_area); 132 mStatusIconContainer = findViewById(R.id.statusIcons); 133 mUserSwitcherContainer = findViewById(R.id.user_switcher_container); 134 mIsPrivacyDotEnabled = mContext.getResources().getBoolean(R.bool.config_enablePrivacyDot); 135 loadDimens(); 136 if (!centralizedStatusBarHeightFix()) { 137 setGravity(Gravity.CENTER_VERTICAL); 138 } 139 } 140 141 /** 142 * Should only be called from {@link KeyguardStatusBarViewController} 143 * @param viewModel view model for the status bar user chip 144 */ init(StatusBarUserChipViewModel viewModel)145 void init(StatusBarUserChipViewModel viewModel) { 146 StatusBarUserChipViewBinder.bind(mUserSwitcherContainer, viewModel); 147 } 148 149 @Override onConfigurationChanged(Configuration newConfig)150 protected void onConfigurationChanged(Configuration newConfig) { 151 super.onConfigurationChanged(newConfig); 152 loadDimens(); 153 154 MarginLayoutParams lp = (MarginLayoutParams) mMultiUserAvatar.getLayoutParams(); 155 lp.width = lp.height = getResources().getDimensionPixelSize( 156 R.dimen.multi_user_avatar_keyguard_size); 157 mMultiUserAvatar.setLayoutParams(lp); 158 159 // System icons 160 updateSystemIconsLayoutParams(); 161 162 // mStatusIconArea 163 mStatusIconArea.setPaddingRelative( 164 mStatusIconArea.getPaddingStart(), 165 getResources().getDimensionPixelSize(R.dimen.status_bar_padding_top), 166 mStatusIconArea.getPaddingEnd(), 167 mStatusIconArea.getPaddingBottom() 168 ); 169 170 // mStatusIconContainer 171 mStatusIconContainer.setPaddingRelative( 172 mStatusIconContainer.getPaddingStart(), 173 mStatusIconContainer.getPaddingTop(), 174 getResources().getDimensionPixelSize(R.dimen.signal_cluster_battery_padding), 175 mStatusIconContainer.getPaddingBottom() 176 ); 177 178 mSystemIcons.setPaddingRelative( 179 getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_start), 180 getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_top), 181 getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_end), 182 getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_bottom) 183 ); 184 185 // Respect font size setting. 186 mCarrierLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, 187 getResources().getDimensionPixelSize( 188 com.android.internal.R.dimen.text_size_small_material)); 189 lp = (MarginLayoutParams) mCarrierLabel.getLayoutParams(); 190 191 int marginStart = calculateMargin( 192 getResources().getDimensionPixelSize(R.dimen.keyguard_carrier_text_margin), 193 mPadding.left); 194 lp.setMarginStart(marginStart); 195 196 mCarrierLabel.setLayoutParams(lp); 197 updateKeyguardStatusBarHeight(); 198 } 199 setUserSwitcherEnabled(boolean enabled)200 public void setUserSwitcherEnabled(boolean enabled) { 201 mIsUserSwitcherEnabled = enabled; 202 } 203 updateKeyguardStatusBarHeight()204 private void updateKeyguardStatusBarHeight() { 205 ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) getLayoutParams(); 206 lp.height = getStatusBarHeaderHeightKeyguard(mContext); 207 setLayoutParams(lp); 208 } 209 loadDimens()210 void loadDimens() { 211 Resources res = getResources(); 212 mSystemIconsSwitcherHiddenExpandedMargin = res.getDimensionPixelSize( 213 R.dimen.system_icons_switcher_hidden_expanded_margin); 214 mStatusBarPaddingEnd = res.getDimensionPixelSize( 215 R.dimen.status_bar_padding_end); 216 mMinDotWidth = res.getDimensionPixelSize( 217 R.dimen.ongoing_appops_dot_min_padding); 218 mCutoutSideNudge = getResources().getDimensionPixelSize( 219 R.dimen.display_cutout_margin_consumption); 220 mShowPercentAvailable = getContext().getResources().getBoolean( 221 com.android.internal.R.bool.config_battery_percentage_setting_available); 222 mRoundedCornerPadding = res.getDimensionPixelSize( 223 R.dimen.rounded_corner_content_padding); 224 } 225 updateVisibilities()226 private void updateVisibilities() { 227 // Multi user avatar is disabled in favor of the user switcher chip 228 if (!mKeyguardUserAvatarEnabled) { 229 if (mMultiUserAvatar.getParent() == mStatusIconArea) { 230 mStatusIconArea.removeView(mMultiUserAvatar); 231 } else if (mMultiUserAvatar.getParent() != null) { 232 getOverlay().remove(mMultiUserAvatar); 233 } 234 235 return; 236 } 237 238 if (mMultiUserAvatar.getParent() != mStatusIconArea 239 && !mKeyguardUserSwitcherEnabled) { 240 if (mMultiUserAvatar.getParent() != null) { 241 getOverlay().remove(mMultiUserAvatar); 242 } 243 mStatusIconArea.addView(mMultiUserAvatar, 0); 244 } else if (mMultiUserAvatar.getParent() == mStatusIconArea 245 && mKeyguardUserSwitcherEnabled) { 246 mStatusIconArea.removeView(mMultiUserAvatar); 247 } 248 if (!mKeyguardUserSwitcherEnabled) { 249 // If we have no keyguard switcher, the screen width is under 600dp. In this case, 250 // we only show the multi-user switch if it's enabled through UserManager as well as 251 // by the user. 252 if (mIsUserSwitcherEnabled) { 253 mMultiUserAvatar.setVisibility(View.VISIBLE); 254 } else { 255 mMultiUserAvatar.setVisibility(View.GONE); 256 } 257 } 258 mBatteryView.setForceShowPercent(mBatteryCharging && mShowPercentAvailable); 259 } 260 updateSystemIconsLayoutParams()261 private void updateSystemIconsLayoutParams() { 262 LinearLayout.LayoutParams lp = 263 (LinearLayout.LayoutParams) mSystemIconsContainer.getLayoutParams(); 264 265 // Use status_bar_padding_end to replace original 266 // system_icons_super_container_avatarless_margin_end to prevent different end alignment 267 // between PhoneStatusBarView and KeyguardStatusBarView 268 int baseMarginEnd = mStatusBarPaddingEnd; 269 int marginEnd = 270 mKeyguardUserSwitcherEnabled ? mSystemIconsSwitcherHiddenExpandedMargin 271 : baseMarginEnd; 272 273 // Align PhoneStatusBar right margin/padding, only use 274 // 1. status bar layout: mPadding(consider round_corner + privacy dot) 275 // 2. icon container: R.dimen.status_bar_padding_end 276 277 if (marginEnd != lp.getMarginEnd()) { 278 lp.setMarginEnd(marginEnd); 279 mSystemIconsContainer.setLayoutParams(lp); 280 } 281 } 282 283 /** Should only be called from {@link KeyguardStatusBarViewController}. */ updateWindowInsets( WindowInsets insets, StatusBarContentInsetsProvider insetsProvider)284 WindowInsets updateWindowInsets( 285 WindowInsets insets, 286 StatusBarContentInsetsProvider insetsProvider) { 287 mLayoutState = LAYOUT_NONE; 288 if (updateLayoutConsideringCutout(insetsProvider)) { 289 requestLayout(); 290 } 291 return super.onApplyWindowInsets(insets); 292 } 293 updateLayoutConsideringCutout(StatusBarContentInsetsProvider insetsProvider)294 private boolean updateLayoutConsideringCutout(StatusBarContentInsetsProvider insetsProvider) { 295 return setDisplayCutout( 296 getRootWindowInsets().getDisplayCutout(), 297 insetsProvider); 298 } 299 300 /** Sets the {@link DisplayCutout}, updating the view to render around the cutout. */ setDisplayCutout( @ullable DisplayCutout displayCutout, StatusBarContentInsetsProvider insetsProvider)301 public boolean setDisplayCutout( 302 @Nullable DisplayCutout displayCutout, 303 StatusBarContentInsetsProvider insetsProvider) { 304 mDisplayCutout = displayCutout; 305 updateKeyguardStatusBarHeight(); 306 updatePadding(insetsProvider); 307 if (mDisplayCutout == null || insetsProvider.currentRotationHasCornerCutout()) { 308 return updateLayoutParamsNoCutout(); 309 } else { 310 return updateLayoutParamsForCutout(); 311 } 312 } 313 updatePadding(StatusBarContentInsetsProvider insetsProvider)314 private void updatePadding(StatusBarContentInsetsProvider insetsProvider) { 315 final int waterfallTop = 316 mDisplayCutout == null ? 0 : mDisplayCutout.getWaterfallInsets().top; 317 mPadding = insetsProvider.getStatusBarContentInsetsForCurrentRotation(); 318 319 // consider privacy dot space 320 final int minLeft = (isLayoutRtl() && mIsPrivacyDotEnabled) 321 ? Math.max(mMinDotWidth, mPadding.left) : mPadding.left; 322 final int minRight = (!isLayoutRtl() && mIsPrivacyDotEnabled) 323 ? Math.max(mMinDotWidth, mPadding.right) : mPadding.right; 324 325 int top = centralizedStatusBarHeightFix() ? waterfallTop + mPadding.top : waterfallTop; 326 setPadding(minLeft, top, minRight, 0); 327 } 328 updateLayoutParamsNoCutout()329 private boolean updateLayoutParamsNoCutout() { 330 if (mLayoutState == LAYOUT_NO_CUTOUT) { 331 return false; 332 } 333 mLayoutState = LAYOUT_NO_CUTOUT; 334 335 if (mCutoutSpace != null) { 336 mCutoutSpace.setVisibility(View.GONE); 337 } 338 339 RelativeLayout.LayoutParams lp = (LayoutParams) mCarrierLabel.getLayoutParams(); 340 lp.addRule(RelativeLayout.START_OF, R.id.status_icon_area); 341 342 lp = (LayoutParams) mStatusIconArea.getLayoutParams(); 343 lp.removeRule(RelativeLayout.RIGHT_OF); 344 lp.width = LayoutParams.WRAP_CONTENT; 345 lp.setMarginStart(getResources().getDimensionPixelSize( 346 R.dimen.system_icons_super_container_margin_start)); 347 return true; 348 } 349 updateLayoutParamsForCutout()350 private boolean updateLayoutParamsForCutout() { 351 if (mLayoutState == LAYOUT_CUTOUT) { 352 return false; 353 } 354 mLayoutState = LAYOUT_CUTOUT; 355 356 if (mCutoutSpace == null) { 357 updateLayoutParamsNoCutout(); 358 } 359 360 Rect bounds = new Rect(); 361 boundsFromDirection(mDisplayCutout, Gravity.TOP, bounds); 362 363 mCutoutSpace.setVisibility(View.VISIBLE); 364 RelativeLayout.LayoutParams lp = (LayoutParams) mCutoutSpace.getLayoutParams(); 365 bounds.left = bounds.left + mCutoutSideNudge; 366 bounds.right = bounds.right - mCutoutSideNudge; 367 lp.width = bounds.width(); 368 lp.height = bounds.height(); 369 lp.addRule(RelativeLayout.CENTER_IN_PARENT); 370 371 lp = (LayoutParams) mCarrierLabel.getLayoutParams(); 372 lp.addRule(RelativeLayout.START_OF, R.id.cutout_space_view); 373 374 lp = (LayoutParams) mStatusIconArea.getLayoutParams(); 375 lp.addRule(RelativeLayout.RIGHT_OF, R.id.cutout_space_view); 376 lp.width = LayoutParams.MATCH_PARENT; 377 lp.setMarginStart(0); 378 return true; 379 } 380 381 /** Should only be called from {@link KeyguardStatusBarViewController}. */ onUserInfoChanged(Drawable picture)382 void onUserInfoChanged(Drawable picture) { 383 mMultiUserAvatar.setImageDrawable(picture); 384 } 385 386 /** 387 * Should only be called from {@link KeyguardStatusBarViewController} or 388 * {@link com.android.systemui.statusbar.ui.binder.KeyguardStatusBarViewBinder}. 389 */ onBatteryChargingChanged(boolean charging)390 public void onBatteryChargingChanged(boolean charging) { 391 if (mBatteryCharging != charging) { 392 mBatteryCharging = charging; 393 updateVisibilities(); 394 } 395 } 396 397 /** 398 * Should only be called from {@link KeyguardStatusBarViewController} or 399 * {@link com.android.systemui.statusbar.ui.binder.KeyguardStatusBarViewBinder}. 400 */ setKeyguardUserSwitcherEnabled(boolean enabled)401 public void setKeyguardUserSwitcherEnabled(boolean enabled) { 402 mKeyguardUserSwitcherEnabled = enabled; 403 } 404 setKeyguardUserAvatarEnabled(boolean enabled)405 void setKeyguardUserAvatarEnabled(boolean enabled) { 406 mKeyguardUserAvatarEnabled = enabled; 407 updateVisibilities(); 408 } 409 410 @VisibleForTesting isKeyguardUserAvatarEnabled()411 boolean isKeyguardUserAvatarEnabled() { 412 return mKeyguardUserAvatarEnabled; 413 } 414 415 @Override setVisibility(int visibility)416 public void setVisibility(int visibility) { 417 super.setVisibility(visibility); 418 if (visibility != View.VISIBLE) { 419 mSystemIconsContainer.animate().cancel(); 420 mSystemIconsContainer.setTranslationX(0); 421 mMultiUserAvatar.animate().cancel(); 422 mMultiUserAvatar.setAlpha(1f); 423 } else { 424 updateVisibilities(); 425 updateSystemIconsLayoutParams(); 426 } 427 } 428 429 @Override hasOverlappingRendering()430 public boolean hasOverlappingRendering() { 431 return false; 432 } 433 434 /** Should only be called from {@link KeyguardStatusBarViewController}. */ onThemeChanged(TintedIconManager iconManager)435 void onThemeChanged(TintedIconManager iconManager) { 436 mBatteryView.setColorsFromContext(mContext); 437 updateIconsAndTextColors(iconManager); 438 } 439 440 /** Should only be called from {@link KeyguardStatusBarViewController}. */ onOverlayChanged()441 void onOverlayChanged() { 442 int theme = Utils.getThemeAttr(mContext, com.android.internal.R.attr.textAppearanceSmall); 443 mCarrierLabel.setTextAppearance(theme); 444 mBatteryView.updatePercentView(); 445 446 TextView userSwitcherName = mUserSwitcherContainer.findViewById(R.id.current_user_name); 447 if (userSwitcherName != null) { 448 userSwitcherName.setTextAppearance(theme); 449 } 450 } 451 updateIconsAndTextColors(TintedIconManager iconManager)452 private void updateIconsAndTextColors(TintedIconManager iconManager) { 453 @ColorInt int textColor = Utils.getColorAttrDefaultColor(mContext, 454 R.attr.wallpaperTextColor); 455 float luminance = Color.luminance(textColor); 456 @ColorInt int iconColor = Utils.getColorStateListDefaultColor(mContext, 457 luminance < 0.5 458 ? com.android.settingslib.R.color.dark_mode_icon_color_single_tone 459 : com.android.settingslib.R.color.light_mode_icon_color_single_tone); 460 @ColorInt int contrastColor = luminance < 0.5 461 ? DarkIconDispatcherImpl.DEFAULT_ICON_TINT 462 : DarkIconDispatcherImpl.DEFAULT_INVERSE_ICON_TINT; 463 float intensity = textColor == Color.WHITE ? 0 : 1; 464 mCarrierLabel.setTextColor(iconColor); 465 466 TextView userSwitcherName = mUserSwitcherContainer.findViewById(R.id.current_user_name); 467 if (userSwitcherName != null) { 468 userSwitcherName.setTextColor(Utils.getColorStateListDefaultColor( 469 mContext, 470 com.android.settingslib.R.color.light_mode_icon_color_single_tone)); 471 } 472 473 if (iconManager != null) { 474 iconManager.setTint(iconColor, contrastColor); 475 } 476 477 mDarkChange.setValue(new DarkChange(mEmptyTintRect, intensity, iconColor)); 478 applyDarkness(R.id.battery, mEmptyTintRect, intensity, iconColor); 479 applyDarkness(R.id.clock, mEmptyTintRect, intensity, iconColor); 480 } 481 482 private void applyDarkness(int id, ArrayList<Rect> tintAreas, float intensity, int color) { 483 View v = findViewById(id); 484 if (v instanceof DarkReceiver) { 485 ((DarkReceiver) v).onDarkChanged(tintAreas, intensity, color); 486 } 487 } 488 489 /** 490 * Calculates the margin that isn't already accounted for in the view's padding. 491 */ 492 private int calculateMargin(int margin, int padding) { 493 if (padding >= margin) { 494 return 0; 495 } else { 496 return margin - padding; 497 } 498 } 499 500 /** Should only be called from {@link KeyguardStatusBarViewController}. */ 501 void dump(PrintWriter pw, String[] args) { 502 pw.println("KeyguardStatusBarView:"); 503 pw.println(" mBatteryCharging: " + mBatteryCharging); 504 pw.println(" mLayoutState: " + mLayoutState); 505 pw.println(" mKeyguardUserSwitcherEnabled: " + mKeyguardUserSwitcherEnabled); 506 if (mBatteryView != null) { 507 mBatteryView.dump(pw, args); 508 } 509 } 510 511 @Override 512 protected void onLayout(boolean changed, int l, int t, int r, int b) { 513 super.onLayout(changed, l, t, r, b); 514 updateClipping(); 515 } 516 517 /** 518 * Set the clipping on the top of the view. 519 * 520 * Should only be called from {@link KeyguardStatusBarViewController}. 521 */ 522 void setTopClipping(int topClipping) { 523 if (topClipping != mTopClipping) { 524 mTopClipping = topClipping; 525 updateClipping(); 526 } 527 } 528 529 private void updateClipping() { 530 mClipRect.set(0, mTopClipping, getWidth(), getHeight()); 531 setClipBounds(mClipRect); 532 } 533 534 @Override 535 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 536 Trace.beginSection("KeyguardStatusBarView#onMeasure"); 537 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 538 Trace.endSection(); 539 } 540 541 public StateFlow<DarkChange> darkChangeFlow() { 542 return FlowKt.asStateFlow(mDarkChange); 543 } 544 } 545