/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the specific language governing * permissions and limitations under the License. */ package com.android.systemui.qs; import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Pair; import android.view.DisplayCutout; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.Space; import androidx.annotation.NonNull; import com.android.internal.policy.SystemBarUtils; import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.battery.BatteryMeterView; import com.android.systemui.qs.QSDetail.Callback; import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider; import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager; import com.android.systemui.statusbar.phone.StatusIconContainer; import com.android.systemui.statusbar.policy.Clock; import com.android.systemui.statusbar.policy.VariableDateView; import java.util.List; /** * View that contains the top-most bits of the QS panel (primarily the status bar with date, time, * battery, carrier info and privacy icons) and also contains the {@link QuickQSPanel}. */ public class QuickStatusBarHeader extends FrameLayout { private boolean mExpanded; private boolean mQsDisabled; private TouchAnimator mAlphaAnimator; private TouchAnimator mTranslationAnimator; private TouchAnimator mIconsAlphaAnimator; private TouchAnimator mIconsAlphaAnimatorFixed; protected QuickQSPanel mHeaderQsPanel; private View mDatePrivacyView; private View mDateView; // DateView next to clock. Visible on QQS private VariableDateView mClockDateView; private View mSecurityHeaderView; private View mStatusIconsView; private View mContainer; private View mQSCarriers; private ViewGroup mClockContainer; private Clock mClockView; private Space mDatePrivacySeparator; private View mClockIconsSeparator; private boolean mShowClockIconsSeparator; private View mRightLayout; private View mDateContainer; private View mPrivacyContainer; private BatteryMeterView mBatteryRemainingIcon; private StatusIconContainer mIconContainer; private View mPrivacyChip; private TintedIconManager mTintedIconManager; private QSExpansionPathInterpolator mQSExpansionPathInterpolator; private StatusBarContentInsetsProvider mInsetsProvider; private int mRoundedCornerPadding = 0; private int mWaterfallTopInset; private int mCutOutPaddingLeft; private int mCutOutPaddingRight; private float mKeyguardExpansionFraction; private int mTextColorPrimary = Color.TRANSPARENT; private int mTopViewMeasureHeight; @NonNull private List mRssiIgnoredSlots; private boolean mIsSingleCarrier; private boolean mHasCenterCutout; private boolean mConfigShowBatteryEstimate; private boolean mUseCombinedQSHeader; public QuickStatusBarHeader(Context context, AttributeSet attrs) { super(context, attrs); } /** * How much the view containing the clock and QQS will translate down when QS is fully expanded. * * This matches the measured height of the view containing the date and privacy icons. */ public int getOffsetTranslation() { return mTopViewMeasureHeight; } @Override protected void onFinishInflate() { super.onFinishInflate(); mHeaderQsPanel = findViewById(R.id.quick_qs_panel); mDatePrivacyView = findViewById(R.id.quick_status_bar_date_privacy); mStatusIconsView = findViewById(R.id.quick_qs_status_icons); mQSCarriers = findViewById(R.id.carrier_group); mContainer = findViewById(R.id.qs_container); mIconContainer = findViewById(R.id.statusIcons); mPrivacyChip = findViewById(R.id.privacy_chip); mDateView = findViewById(R.id.date); mClockDateView = findViewById(R.id.date_clock); mSecurityHeaderView = findViewById(R.id.header_text_container); mClockIconsSeparator = findViewById(R.id.separator); mRightLayout = findViewById(R.id.rightLayout); mDateContainer = findViewById(R.id.date_container); mPrivacyContainer = findViewById(R.id.privacy_container); mClockContainer = findViewById(R.id.clock_container); mClockView = findViewById(R.id.clock); mDatePrivacySeparator = findViewById(R.id.space); // Tint for the battery icons are handled in setupHost() mBatteryRemainingIcon = findViewById(R.id.batteryRemainingIcon); updateResources(); Configuration config = mContext.getResources().getConfiguration(); setDatePrivacyContainersWidth(config.orientation == Configuration.ORIENTATION_LANDSCAPE); setSecurityHeaderContainerVisibility( config.orientation == Configuration.ORIENTATION_LANDSCAPE); // QS will always show the estimate, and BatteryMeterView handles the case where // it's unavailable or charging mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE); mIconsAlphaAnimatorFixed = new TouchAnimator.Builder() .addFloat(mIconContainer, "alpha", 0, 1) .addFloat(mBatteryRemainingIcon, "alpha", 0, 1) .build(); } void onAttach(TintedIconManager iconManager, QSExpansionPathInterpolator qsExpansionPathInterpolator, List rssiIgnoredSlots, boolean useCombinedQSHeader, StatusBarContentInsetsProvider insetsProvider) { mUseCombinedQSHeader = useCombinedQSHeader; mTintedIconManager = iconManager; mRssiIgnoredSlots = rssiIgnoredSlots; mInsetsProvider = insetsProvider; int fillColor = Utils.getColorAttrDefaultColor(getContext(), android.R.attr.textColorPrimary); // Set the correct tint for the status icons so they contrast iconManager.setTint(fillColor); mQSExpansionPathInterpolator = qsExpansionPathInterpolator; updateAnimators(); } void setIsSingleCarrier(boolean isSingleCarrier) { mIsSingleCarrier = isSingleCarrier; updateAlphaAnimator(); } public QuickQSPanel getHeaderQsPanel() { return mHeaderQsPanel; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mDatePrivacyView.getMeasuredHeight() != mTopViewMeasureHeight) { mTopViewMeasureHeight = mDatePrivacyView.getMeasuredHeight(); post(this::updateAnimators); } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateResources(); setDatePrivacyContainersWidth(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE); setSecurityHeaderContainerVisibility( newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE); } @Override public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); updateResources(); } private void setDatePrivacyContainersWidth(boolean landscape) { LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mDateContainer.getLayoutParams(); lp.width = landscape ? WRAP_CONTENT : 0; lp.weight = landscape ? 0f : 1f; mDateContainer.setLayoutParams(lp); lp = (LinearLayout.LayoutParams) mPrivacyContainer.getLayoutParams(); lp.width = landscape ? WRAP_CONTENT : 0; lp.weight = landscape ? 0f : 1f; mPrivacyContainer.setLayoutParams(lp); } private void setSecurityHeaderContainerVisibility(boolean landscape) { mSecurityHeaderView.setVisibility(landscape ? VISIBLE : GONE); } private void updateBatteryMode() { if (mConfigShowBatteryEstimate && !mHasCenterCutout) { mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE); } else { mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ON); } } void updateResources() { Resources resources = mContext.getResources(); // status bar is already displayed out of QS in split shade boolean shouldUseSplitShade = resources.getBoolean(R.bool.config_use_split_notification_shade); boolean gone = shouldUseSplitShade || mUseCombinedQSHeader || mQsDisabled; mStatusIconsView.setVisibility(gone ? View.GONE : View.VISIBLE); mDatePrivacyView.setVisibility(gone ? View.GONE : View.VISIBLE); mConfigShowBatteryEstimate = resources.getBoolean(R.bool.config_showBatteryEstimateQSBH); mRoundedCornerPadding = resources.getDimensionPixelSize( R.dimen.rounded_corner_content_padding); int qsOffsetHeight = SystemBarUtils.getQuickQsOffsetHeight(mContext); mDatePrivacyView.getLayoutParams().height = Math.max(qsOffsetHeight, mDatePrivacyView.getMinimumHeight()); mDatePrivacyView.setLayoutParams(mDatePrivacyView.getLayoutParams()); mStatusIconsView.getLayoutParams().height = Math.max(qsOffsetHeight, mStatusIconsView.getMinimumHeight()); mStatusIconsView.setLayoutParams(mStatusIconsView.getLayoutParams()); ViewGroup.LayoutParams lp = getLayoutParams(); if (mQsDisabled) { lp.height = mStatusIconsView.getLayoutParams().height; } else { lp.height = WRAP_CONTENT; } setLayoutParams(lp); int textColor = Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorPrimary); if (textColor != mTextColorPrimary) { int textColorSecondary = Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorSecondary); mTextColorPrimary = textColor; mClockView.setTextColor(textColor); if (mTintedIconManager != null) { mTintedIconManager.setTint(textColor); } mBatteryRemainingIcon.updateColors(mTextColorPrimary, textColorSecondary, mTextColorPrimary); } MarginLayoutParams qqsLP = (MarginLayoutParams) mHeaderQsPanel.getLayoutParams(); qqsLP.topMargin = shouldUseSplitShade || !mUseCombinedQSHeader ? mContext.getResources() .getDimensionPixelSize(R.dimen.qqs_layout_margin_top) : qsOffsetHeight; mHeaderQsPanel.setLayoutParams(qqsLP); updateBatteryMode(); updateHeadersPadding(); updateAnimators(); updateClockDatePadding(); } private void updateClockDatePadding() { int startPadding = mContext.getResources() .getDimensionPixelSize(R.dimen.status_bar_left_clock_starting_padding); int endPadding = mContext.getResources() .getDimensionPixelSize(R.dimen.status_bar_left_clock_end_padding); mClockView.setPaddingRelative( startPadding, mClockView.getPaddingTop(), endPadding, mClockView.getPaddingBottom() ); MarginLayoutParams lp = (MarginLayoutParams) mClockDateView.getLayoutParams(); lp.setMarginStart(endPadding); mClockDateView.setLayoutParams(lp); } private void updateAnimators() { if (mUseCombinedQSHeader) { mTranslationAnimator = null; return; } updateAlphaAnimator(); int offset = mTopViewMeasureHeight; mTranslationAnimator = new TouchAnimator.Builder() .addFloat(mContainer, "translationY", 0, offset) .setInterpolator(mQSExpansionPathInterpolator != null ? mQSExpansionPathInterpolator.getYInterpolator() : null) .build(); } private void updateAlphaAnimator() { if (mUseCombinedQSHeader) { mAlphaAnimator = null; return; } TouchAnimator.Builder builder = new TouchAnimator.Builder() .addFloat(mSecurityHeaderView, "alpha", 0, 1) // These views appear on expanding down .addFloat(mDateView, "alpha", 0, 0, 1) .addFloat(mClockDateView, "alpha", 1, 0, 0) .addFloat(mQSCarriers, "alpha", 0, 1) .setListener(new TouchAnimator.ListenerAdapter() { @Override public void onAnimationAtEnd() { super.onAnimationAtEnd(); if (!mIsSingleCarrier) { mIconContainer.addIgnoredSlots(mRssiIgnoredSlots); } // Make it gone so there's enough room for carrier names mClockDateView.setVisibility(View.GONE); } @Override public void onAnimationStarted() { mClockDateView.setVisibility(View.VISIBLE); mClockDateView.setFreezeSwitching(true); setSeparatorVisibility(false); if (!mIsSingleCarrier) { mIconContainer.addIgnoredSlots(mRssiIgnoredSlots); } } @Override public void onAnimationAtStart() { super.onAnimationAtStart(); mClockDateView.setFreezeSwitching(false); mClockDateView.setVisibility(View.VISIBLE); setSeparatorVisibility(mShowClockIconsSeparator); // In QQS we never ignore RSSI. mIconContainer.removeIgnoredSlots(mRssiIgnoredSlots); } }); mAlphaAnimator = builder.build(); } void setChipVisibility(boolean visibility) { if (visibility) { // Animates the icons and battery indicator from alpha 0 to 1, when the chip is visible mIconsAlphaAnimator = mIconsAlphaAnimatorFixed; mIconsAlphaAnimator.setPosition(mKeyguardExpansionFraction); } else { mIconsAlphaAnimator = null; mIconContainer.setAlpha(1); mBatteryRemainingIcon.setAlpha(1); } } /** */ public void setExpanded(boolean expanded, QuickQSPanelController quickQSPanelController) { if (mExpanded == expanded) return; mExpanded = expanded; quickQSPanelController.setExpanded(expanded); updateEverything(); } /** * Animates the inner contents based on the given expansion details. * * @param forceExpanded whether we should show the state expanded forcibly * @param expansionFraction how much the QS panel is expanded/pulled out (up to 1f) * @param panelTranslationY how much the panel has physically moved down vertically (required * for keyguard animations only) */ public void setExpansion(boolean forceExpanded, float expansionFraction, float panelTranslationY) { final float keyguardExpansionFraction = forceExpanded ? 1f : expansionFraction; if (mAlphaAnimator != null) { mAlphaAnimator.setPosition(keyguardExpansionFraction); } if (mTranslationAnimator != null) { mTranslationAnimator.setPosition(keyguardExpansionFraction); } if (mIconsAlphaAnimator != null) { mIconsAlphaAnimator.setPosition(keyguardExpansionFraction); } // If forceExpanded (we are opening QS from lockscreen), the animators have been set to // position = 1f. if (forceExpanded) { setTranslationY(panelTranslationY); } else { setTranslationY(0); } mKeyguardExpansionFraction = keyguardExpansionFraction; } public void disable(int state1, int state2, boolean animate) { final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0; if (disabled == mQsDisabled) return; mQsDisabled = disabled; mHeaderQsPanel.setDisabledByPolicy(disabled); mStatusIconsView.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE); updateResources(); } @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { // Handle padding of the views DisplayCutout cutout = insets.getDisplayCutout(); Pair sbInsets = mInsetsProvider .getStatusBarContentInsetsForCurrentRotation(); boolean hasCornerCutout = mInsetsProvider.currentRotationHasCornerCutout(); mDatePrivacyView.setPadding(sbInsets.first, 0, sbInsets.second, 0); mStatusIconsView.setPadding(sbInsets.first, 0, sbInsets.second, 0); LinearLayout.LayoutParams datePrivacySeparatorLayoutParams = (LinearLayout.LayoutParams) mDatePrivacySeparator.getLayoutParams(); LinearLayout.LayoutParams mClockIconsSeparatorLayoutParams = (LinearLayout.LayoutParams) mClockIconsSeparator.getLayoutParams(); if (cutout != null) { Rect topCutout = cutout.getBoundingRectTop(); if (topCutout.isEmpty() || hasCornerCutout) { datePrivacySeparatorLayoutParams.width = 0; mDatePrivacySeparator.setVisibility(View.GONE); mClockIconsSeparatorLayoutParams.width = 0; setSeparatorVisibility(false); mShowClockIconsSeparator = false; mHasCenterCutout = false; } else { datePrivacySeparatorLayoutParams.width = topCutout.width(); mDatePrivacySeparator.setVisibility(View.VISIBLE); mClockIconsSeparatorLayoutParams.width = topCutout.width(); mShowClockIconsSeparator = true; setSeparatorVisibility(mKeyguardExpansionFraction == 0f); mHasCenterCutout = true; } } mDatePrivacySeparator.setLayoutParams(datePrivacySeparatorLayoutParams); mClockIconsSeparator.setLayoutParams(mClockIconsSeparatorLayoutParams); mCutOutPaddingLeft = sbInsets.first; mCutOutPaddingRight = sbInsets.second; mWaterfallTopInset = cutout == null ? 0 : cutout.getWaterfallInsets().top; updateBatteryMode(); updateHeadersPadding(); return super.onApplyWindowInsets(insets); } /** * Sets the visibility of the separator between clock and icons. * * This separator is "visible" when there is a center cutout, to block that space. In that * case, the clock and the layout on the right (containing the icons and the battery meter) are * set to weight 1 to take the available space. * @param visible whether the separator between clock and icons should be visible. */ private void setSeparatorVisibility(boolean visible) { int newVisibility = visible ? View.VISIBLE : View.GONE; if (mClockIconsSeparator.getVisibility() == newVisibility) return; mClockIconsSeparator.setVisibility(visible ? View.VISIBLE : View.GONE); mQSCarriers.setVisibility(visible ? View.GONE : View.VISIBLE); LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mClockContainer.getLayoutParams(); lp.width = visible ? 0 : WRAP_CONTENT; lp.weight = visible ? 1f : 0f; mClockContainer.setLayoutParams(lp); lp = (LinearLayout.LayoutParams) mRightLayout.getLayoutParams(); lp.width = visible ? 0 : WRAP_CONTENT; lp.weight = visible ? 1f : 0f; mRightLayout.setLayoutParams(lp); } private void updateHeadersPadding() { setContentMargins(mDatePrivacyView, 0, 0); setContentMargins(mStatusIconsView, 0, 0); int paddingLeft = 0; int paddingRight = 0; FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); int leftMargin = lp.leftMargin; int rightMargin = lp.rightMargin; // The clock might collide with cutouts, let's shift it out of the way. // We only do that if the inset is bigger than our own padding, since it's nicer to // align with if (mCutOutPaddingLeft > 0) { // if there's a cutout, let's use at least the rounded corner inset int cutoutPadding = Math.max(mCutOutPaddingLeft, mRoundedCornerPadding); paddingLeft = Math.max(cutoutPadding - leftMargin, 0); } if (mCutOutPaddingRight > 0) { // if there's a cutout, let's use at least the rounded corner inset int cutoutPadding = Math.max(mCutOutPaddingRight, mRoundedCornerPadding); paddingRight = Math.max(cutoutPadding - rightMargin, 0); } mDatePrivacyView.setPadding(paddingLeft, mWaterfallTopInset, paddingRight, 0); mStatusIconsView.setPadding(paddingLeft, mWaterfallTopInset, paddingRight, 0); } public void updateEverything() { post(() -> setClickable(!mExpanded)); } public void setCallback(Callback qsPanelCallback) { mHeaderQsPanel.setCallback(qsPanelCallback); } private void setContentMargins(View view, int marginStart, int marginEnd) { MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); lp.setMarginStart(marginStart); lp.setMarginEnd(marginEnd); view.setLayoutParams(lp); } /** * Scroll the headers away. * * @param scrollY the scroll of the QSPanel container */ public void setExpandedScrollAmount(int scrollY) { mStatusIconsView.setScrollY(scrollY); mDatePrivacyView.setScrollY(scrollY); } }