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