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