1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.qs; 16 17 import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; 18 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 19 20 import android.content.Context; 21 import android.content.res.Configuration; 22 import android.content.res.Resources; 23 import android.graphics.Color; 24 import android.graphics.Rect; 25 import android.util.AttributeSet; 26 import android.util.Pair; 27 import android.view.DisplayCutout; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.WindowInsets; 31 import android.widget.FrameLayout; 32 import android.widget.LinearLayout; 33 import android.widget.Space; 34 35 import androidx.annotation.NonNull; 36 37 import com.android.settingslib.Utils; 38 import com.android.systemui.BatteryMeterView; 39 import com.android.systemui.R; 40 import com.android.systemui.qs.QSDetail.Callback; 41 import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager; 42 import com.android.systemui.statusbar.phone.StatusBarWindowView; 43 import com.android.systemui.statusbar.phone.StatusIconContainer; 44 import com.android.systemui.statusbar.policy.Clock; 45 46 import java.util.List; 47 48 /** 49 * View that contains the top-most bits of the QS panel (primarily the status bar with date, time, 50 * battery, carrier info and privacy icons) and also contains the {@link QuickQSPanel}. 51 */ 52 public class QuickStatusBarHeader extends FrameLayout { 53 54 private boolean mExpanded; 55 private boolean mQsDisabled; 56 57 private TouchAnimator mAlphaAnimator; 58 private TouchAnimator mTranslationAnimator; 59 private TouchAnimator mIconsAlphaAnimator; 60 private TouchAnimator mIconsAlphaAnimatorFixed; 61 62 protected QuickQSPanel mHeaderQsPanel; 63 private View mDatePrivacyView; 64 private View mDateView; 65 private View mSecurityHeaderView; 66 private View mClockIconsView; 67 private View mContainer; 68 69 private View mQSCarriers; 70 private Clock mClockView; 71 private Space mDatePrivacySeparator; 72 private View mClockIconsSeparator; 73 private boolean mShowClockIconsSeparator; 74 private View mRightLayout; 75 private View mDateContainer; 76 private View mPrivacyContainer; 77 78 private BatteryMeterView mBatteryRemainingIcon; 79 private StatusIconContainer mIconContainer; 80 private View mPrivacyChip; 81 82 private TintedIconManager mTintedIconManager; 83 private QSExpansionPathInterpolator mQSExpansionPathInterpolator; 84 85 private int mRoundedCornerPadding = 0; 86 private int mWaterfallTopInset; 87 private int mCutOutPaddingLeft; 88 private int mCutOutPaddingRight; 89 private float mViewAlpha = 1.0f; 90 private float mKeyguardExpansionFraction; 91 private int mTextColorPrimary = Color.TRANSPARENT; 92 private int mTopViewMeasureHeight; 93 94 @NonNull 95 private List<String> mRssiIgnoredSlots; 96 private boolean mIsSingleCarrier; 97 98 private boolean mHasCenterCutout; 99 private boolean mConfigShowBatteryEstimate; 100 QuickStatusBarHeader(Context context, AttributeSet attrs)101 public QuickStatusBarHeader(Context context, AttributeSet attrs) { 102 super(context, attrs); 103 } 104 105 /** 106 * How much the view containing the clock and QQS will translate down when QS is fully expanded. 107 * 108 * This matches the measured height of the view containing the date and privacy icons. 109 */ getOffsetTranslation()110 public int getOffsetTranslation() { 111 return mTopViewMeasureHeight; 112 } 113 114 @Override onFinishInflate()115 protected void onFinishInflate() { 116 super.onFinishInflate(); 117 118 mHeaderQsPanel = findViewById(R.id.quick_qs_panel); 119 mDatePrivacyView = findViewById(R.id.quick_status_bar_date_privacy); 120 mClockIconsView = findViewById(R.id.quick_qs_status_icons); 121 mQSCarriers = findViewById(R.id.carrier_group); 122 mContainer = findViewById(R.id.qs_container); 123 mIconContainer = findViewById(R.id.statusIcons); 124 mPrivacyChip = findViewById(R.id.privacy_chip); 125 mDateView = findViewById(R.id.date); 126 mSecurityHeaderView = findViewById(R.id.header_text_container); 127 mClockIconsSeparator = findViewById(R.id.separator); 128 mRightLayout = findViewById(R.id.rightLayout); 129 mDateContainer = findViewById(R.id.date_container); 130 mPrivacyContainer = findViewById(R.id.privacy_container); 131 132 mClockView = findViewById(R.id.clock); 133 mDatePrivacySeparator = findViewById(R.id.space); 134 // Tint for the battery icons are handled in setupHost() 135 mBatteryRemainingIcon = findViewById(R.id.batteryRemainingIcon); 136 137 updateResources(); 138 Configuration config = mContext.getResources().getConfiguration(); 139 setDatePrivacyContainersWidth(config.orientation == Configuration.ORIENTATION_LANDSCAPE); 140 setSecurityHeaderContainerVisibility( 141 config.orientation == Configuration.ORIENTATION_LANDSCAPE); 142 143 // Don't need to worry about tuner settings for this icon 144 mBatteryRemainingIcon.setIgnoreTunerUpdates(true); 145 // QS will always show the estimate, and BatteryMeterView handles the case where 146 // it's unavailable or charging 147 mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE); 148 149 mIconsAlphaAnimatorFixed = new TouchAnimator.Builder() 150 .addFloat(mIconContainer, "alpha", 0, 1) 151 .addFloat(mBatteryRemainingIcon, "alpha", 0, 1) 152 .build(); 153 } 154 onAttach(TintedIconManager iconManager, QSExpansionPathInterpolator qsExpansionPathInterpolator, List<String> rssiIgnoredSlots)155 void onAttach(TintedIconManager iconManager, 156 QSExpansionPathInterpolator qsExpansionPathInterpolator, 157 List<String> rssiIgnoredSlots) { 158 mTintedIconManager = iconManager; 159 mRssiIgnoredSlots = rssiIgnoredSlots; 160 int fillColor = Utils.getColorAttrDefaultColor(getContext(), 161 android.R.attr.textColorPrimary); 162 163 // Set the correct tint for the status icons so they contrast 164 iconManager.setTint(fillColor); 165 166 mQSExpansionPathInterpolator = qsExpansionPathInterpolator; 167 updateAnimators(); 168 } 169 setIsSingleCarrier(boolean isSingleCarrier)170 void setIsSingleCarrier(boolean isSingleCarrier) { 171 mIsSingleCarrier = isSingleCarrier; 172 updateAlphaAnimator(); 173 } 174 getHeaderQsPanel()175 public QuickQSPanel getHeaderQsPanel() { 176 return mHeaderQsPanel; 177 } 178 179 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)180 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 181 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 182 if (mDatePrivacyView.getMeasuredHeight() != mTopViewMeasureHeight) { 183 mTopViewMeasureHeight = mDatePrivacyView.getMeasuredHeight(); 184 updateAnimators(); 185 } 186 } 187 188 @Override onConfigurationChanged(Configuration newConfig)189 protected void onConfigurationChanged(Configuration newConfig) { 190 super.onConfigurationChanged(newConfig); 191 updateResources(); 192 setDatePrivacyContainersWidth(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE); 193 setSecurityHeaderContainerVisibility( 194 newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE); 195 } 196 197 @Override onRtlPropertiesChanged(int layoutDirection)198 public void onRtlPropertiesChanged(int layoutDirection) { 199 super.onRtlPropertiesChanged(layoutDirection); 200 updateResources(); 201 } 202 setDatePrivacyContainersWidth(boolean landscape)203 private void setDatePrivacyContainersWidth(boolean landscape) { 204 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mDateContainer.getLayoutParams(); 205 lp.width = landscape ? WRAP_CONTENT : 0; 206 lp.weight = landscape ? 0f : 1f; 207 mDateContainer.setLayoutParams(lp); 208 209 lp = (LinearLayout.LayoutParams) mPrivacyContainer.getLayoutParams(); 210 lp.width = landscape ? WRAP_CONTENT : 0; 211 lp.weight = landscape ? 0f : 1f; 212 mPrivacyContainer.setLayoutParams(lp); 213 } 214 setSecurityHeaderContainerVisibility(boolean landscape)215 private void setSecurityHeaderContainerVisibility(boolean landscape) { 216 mSecurityHeaderView.setVisibility(landscape ? VISIBLE : GONE); 217 } 218 updateBatteryMode()219 private void updateBatteryMode() { 220 if (mConfigShowBatteryEstimate && !mHasCenterCutout) { 221 mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE); 222 } else { 223 mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ON); 224 } 225 } 226 updateResources()227 void updateResources() { 228 Resources resources = mContext.getResources(); 229 230 mConfigShowBatteryEstimate = resources.getBoolean(R.bool.config_showBatteryEstimateQSBH); 231 232 mRoundedCornerPadding = resources.getDimensionPixelSize( 233 R.dimen.rounded_corner_content_padding); 234 235 int qsOffsetHeight = resources.getDimensionPixelSize( 236 com.android.internal.R.dimen.quick_qs_offset_height); 237 238 mDatePrivacyView.getLayoutParams().height = 239 Math.max(qsOffsetHeight, mDatePrivacyView.getMinimumHeight()); 240 mDatePrivacyView.setLayoutParams(mDatePrivacyView.getLayoutParams()); 241 242 mClockIconsView.getLayoutParams().height = 243 Math.max(qsOffsetHeight, mClockIconsView.getMinimumHeight()); 244 mClockIconsView.setLayoutParams(mClockIconsView.getLayoutParams()); 245 246 ViewGroup.LayoutParams lp = getLayoutParams(); 247 if (mQsDisabled) { 248 lp.height = mClockIconsView.getLayoutParams().height; 249 } else { 250 lp.height = WRAP_CONTENT; 251 } 252 setLayoutParams(lp); 253 254 int textColor = Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorPrimary); 255 if (textColor != mTextColorPrimary) { 256 int textColorSecondary = Utils.getColorAttrDefaultColor(mContext, 257 android.R.attr.textColorSecondary); 258 mTextColorPrimary = textColor; 259 mClockView.setTextColor(textColor); 260 if (mTintedIconManager != null) { 261 mTintedIconManager.setTint(textColor); 262 } 263 mBatteryRemainingIcon.updateColors(mTextColorPrimary, textColorSecondary, 264 mTextColorPrimary); 265 } 266 267 MarginLayoutParams qqsLP = (MarginLayoutParams) mHeaderQsPanel.getLayoutParams(); 268 qqsLP.topMargin = mContext.getResources() 269 .getDimensionPixelSize(R.dimen.qqs_layout_margin_top); 270 mHeaderQsPanel.setLayoutParams(qqsLP); 271 272 updateBatteryMode(); 273 updateHeadersPadding(); 274 updateAnimators(); 275 } 276 updateAnimators()277 private void updateAnimators() { 278 updateAlphaAnimator(); 279 int offset = mTopViewMeasureHeight; 280 281 mTranslationAnimator = new TouchAnimator.Builder() 282 .addFloat(mContainer, "translationY", 0, offset) 283 .setInterpolator(mQSExpansionPathInterpolator != null 284 ? mQSExpansionPathInterpolator.getYInterpolator() 285 : null) 286 .build(); 287 } 288 updateAlphaAnimator()289 private void updateAlphaAnimator() { 290 TouchAnimator.Builder builder = new TouchAnimator.Builder() 291 .addFloat(mSecurityHeaderView, "alpha", 0, 1) 292 // These views appear on expanding down 293 .addFloat(mClockView, "alpha", 0, 1) 294 .addFloat(mQSCarriers, "alpha", 0, 1) 295 .setListener(new TouchAnimator.ListenerAdapter() { 296 @Override 297 public void onAnimationAtEnd() { 298 super.onAnimationAtEnd(); 299 if (!mIsSingleCarrier) { 300 mIconContainer.addIgnoredSlots(mRssiIgnoredSlots); 301 } 302 } 303 304 @Override 305 public void onAnimationStarted() { 306 setSeparatorVisibility(false); 307 if (!mIsSingleCarrier) { 308 mIconContainer.addIgnoredSlots(mRssiIgnoredSlots); 309 } 310 } 311 312 @Override 313 public void onAnimationAtStart() { 314 super.onAnimationAtStart(); 315 setSeparatorVisibility(mShowClockIconsSeparator); 316 // In QQS we never ignore RSSI. 317 mIconContainer.removeIgnoredSlots(mRssiIgnoredSlots); 318 } 319 }); 320 mAlphaAnimator = builder.build(); 321 } 322 setChipVisibility(boolean visibility)323 void setChipVisibility(boolean visibility) { 324 mPrivacyChip.setVisibility(visibility ? View.VISIBLE : View.GONE); 325 if (visibility) { 326 // Animates the icons and battery indicator from alpha 0 to 1, when the chip is visible 327 mIconsAlphaAnimator = mIconsAlphaAnimatorFixed; 328 mIconsAlphaAnimator.setPosition(mKeyguardExpansionFraction); 329 } else { 330 mIconsAlphaAnimator = null; 331 mIconContainer.setAlpha(1); 332 mBatteryRemainingIcon.setAlpha(1); 333 } 334 335 } 336 337 /** */ setExpanded(boolean expanded, QuickQSPanelController quickQSPanelController)338 public void setExpanded(boolean expanded, QuickQSPanelController quickQSPanelController) { 339 if (mExpanded == expanded) return; 340 mExpanded = expanded; 341 quickQSPanelController.setExpanded(expanded); 342 updateEverything(); 343 } 344 345 /** 346 * Animates the inner contents based on the given expansion details. 347 * 348 * @param forceExpanded whether we should show the state expanded forcibly 349 * @param expansionFraction how much the QS panel is expanded/pulled out (up to 1f) 350 * @param panelTranslationY how much the panel has physically moved down vertically (required 351 * for keyguard animations only) 352 */ setExpansion(boolean forceExpanded, float expansionFraction, float panelTranslationY)353 public void setExpansion(boolean forceExpanded, float expansionFraction, 354 float panelTranslationY) { 355 final float keyguardExpansionFraction = forceExpanded ? 1f : expansionFraction; 356 357 if (mAlphaAnimator != null) { 358 mAlphaAnimator.setPosition(keyguardExpansionFraction); 359 } 360 if (mTranslationAnimator != null) { 361 mTranslationAnimator.setPosition(keyguardExpansionFraction); 362 } 363 if (mIconsAlphaAnimator != null) { 364 mIconsAlphaAnimator.setPosition(keyguardExpansionFraction); 365 } 366 // If forceExpanded (we are opening QS from lockscreen), the animators have been set to 367 // position = 1f. 368 if (forceExpanded) { 369 setTranslationY(panelTranslationY); 370 } else { 371 setTranslationY(0); 372 } 373 374 mKeyguardExpansionFraction = keyguardExpansionFraction; 375 } 376 disable(int state1, int state2, boolean animate)377 public void disable(int state1, int state2, boolean animate) { 378 final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0; 379 if (disabled == mQsDisabled) return; 380 mQsDisabled = disabled; 381 mHeaderQsPanel.setDisabledByPolicy(disabled); 382 mClockIconsView.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE); 383 updateResources(); 384 } 385 386 @Override onApplyWindowInsets(WindowInsets insets)387 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 388 // Handle padding of the views 389 DisplayCutout cutout = insets.getDisplayCutout(); 390 Pair<Integer, Integer> cornerCutoutPadding = StatusBarWindowView.cornerCutoutMargins( 391 cutout, getDisplay()); 392 Pair<Integer, Integer> padding = 393 StatusBarWindowView.paddingNeededForCutoutAndRoundedCorner( 394 cutout, cornerCutoutPadding, -1); 395 mDatePrivacyView.setPadding(padding.first, 0, padding.second, 0); 396 mClockIconsView.setPadding(padding.first, 0, padding.second, 0); 397 LinearLayout.LayoutParams datePrivacySeparatorLayoutParams = 398 (LinearLayout.LayoutParams) mDatePrivacySeparator.getLayoutParams(); 399 LinearLayout.LayoutParams mClockIconsSeparatorLayoutParams = 400 (LinearLayout.LayoutParams) mClockIconsSeparator.getLayoutParams(); 401 boolean cornerCutout = cornerCutoutPadding != null 402 && (cornerCutoutPadding.first == 0 || cornerCutoutPadding.second == 0); 403 if (cutout != null) { 404 Rect topCutout = cutout.getBoundingRectTop(); 405 if (topCutout.isEmpty() || cornerCutout) { 406 datePrivacySeparatorLayoutParams.width = 0; 407 mDatePrivacySeparator.setVisibility(View.GONE); 408 mClockIconsSeparatorLayoutParams.width = 0; 409 setSeparatorVisibility(false); 410 mShowClockIconsSeparator = false; 411 mHasCenterCutout = false; 412 } else { 413 datePrivacySeparatorLayoutParams.width = topCutout.width(); 414 mDatePrivacySeparator.setVisibility(View.VISIBLE); 415 mClockIconsSeparatorLayoutParams.width = topCutout.width(); 416 mShowClockIconsSeparator = true; 417 setSeparatorVisibility(mKeyguardExpansionFraction == 0f); 418 mHasCenterCutout = true; 419 } 420 } 421 mDatePrivacySeparator.setLayoutParams(datePrivacySeparatorLayoutParams); 422 mClockIconsSeparator.setLayoutParams(mClockIconsSeparatorLayoutParams); 423 mCutOutPaddingLeft = padding.first; 424 mCutOutPaddingRight = padding.second; 425 mWaterfallTopInset = cutout == null ? 0 : cutout.getWaterfallInsets().top; 426 427 updateBatteryMode(); 428 updateHeadersPadding(); 429 return super.onApplyWindowInsets(insets); 430 } 431 432 /** 433 * Sets the visibility of the separator between clock and icons. 434 * 435 * This separator is "visible" when there is a center cutout, to block that space. In that 436 * case, the clock and the layout on the right (containing the icons and the battery meter) are 437 * set to weight 1 to take the available space. 438 * @param visible whether the separator between clock and icons should be visible. 439 */ setSeparatorVisibility(boolean visible)440 private void setSeparatorVisibility(boolean visible) { 441 int newVisibility = visible ? View.VISIBLE : View.GONE; 442 if (mClockIconsSeparator.getVisibility() == newVisibility) return; 443 444 mClockIconsSeparator.setVisibility(visible ? View.VISIBLE : View.GONE); 445 mQSCarriers.setVisibility(visible ? View.GONE : View.VISIBLE); 446 447 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mClockView.getLayoutParams(); 448 lp.width = visible ? 0 : WRAP_CONTENT; 449 lp.weight = visible ? 1f : 0f; 450 mClockView.setLayoutParams(lp); 451 452 lp = (LinearLayout.LayoutParams) mRightLayout.getLayoutParams(); 453 lp.width = visible ? 0 : WRAP_CONTENT; 454 lp.weight = visible ? 1f : 0f; 455 mRightLayout.setLayoutParams(lp); 456 } 457 updateHeadersPadding()458 private void updateHeadersPadding() { 459 setContentMargins(mDatePrivacyView, 0, 0); 460 setContentMargins(mClockIconsView, 0, 0); 461 int paddingLeft = 0; 462 int paddingRight = 0; 463 464 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 465 int leftMargin = lp.leftMargin; 466 int rightMargin = lp.rightMargin; 467 468 // The clock might collide with cutouts, let's shift it out of the way. 469 // We only do that if the inset is bigger than our own padding, since it's nicer to 470 // align with 471 if (mCutOutPaddingLeft > 0) { 472 // if there's a cutout, let's use at least the rounded corner inset 473 int cutoutPadding = Math.max(mCutOutPaddingLeft, mRoundedCornerPadding); 474 paddingLeft = Math.max(cutoutPadding - leftMargin, 0); 475 } 476 if (mCutOutPaddingRight > 0) { 477 // if there's a cutout, let's use at least the rounded corner inset 478 int cutoutPadding = Math.max(mCutOutPaddingRight, mRoundedCornerPadding); 479 paddingRight = Math.max(cutoutPadding - rightMargin, 0); 480 } 481 482 mDatePrivacyView.setPadding(paddingLeft, 483 mWaterfallTopInset, 484 paddingRight, 485 0); 486 mClockIconsView.setPadding(paddingLeft, 487 mWaterfallTopInset, 488 paddingRight, 489 0); 490 } 491 updateEverything()492 public void updateEverything() { 493 post(() -> setClickable(!mExpanded)); 494 } 495 setCallback(Callback qsPanelCallback)496 public void setCallback(Callback qsPanelCallback) { 497 mHeaderQsPanel.setCallback(qsPanelCallback); 498 } 499 setContentMargins(View view, int marginStart, int marginEnd)500 private void setContentMargins(View view, int marginStart, int marginEnd) { 501 MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); 502 lp.setMarginStart(marginStart); 503 lp.setMarginEnd(marginEnd); 504 view.setLayoutParams(lp); 505 } 506 507 /** 508 * Scroll the headers away. 509 * 510 * @param scrollY the scroll of the QSPanel container 511 */ setExpandedScrollAmount(int scrollY)512 public void setExpandedScrollAmount(int scrollY) { 513 mClockIconsView.setScrollY(scrollY); 514 mDatePrivacyView.setScrollY(scrollY); 515 } 516 } 517