/* * 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 com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; import android.annotation.ColorInt; import android.app.ActivityManager; import android.app.AlarmManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Rect; import android.media.AudioManager; import android.os.Handler; import android.provider.AlarmClock; import android.provider.Settings; import android.service.notification.ZenModeConfig; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; import android.util.Pair; import android.view.ContextThemeWrapper; import android.view.DisplayCutout; import android.view.View; import android.view.WindowInsets; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.VisibleForTesting; import com.android.settingslib.Utils; import com.android.systemui.BatteryMeterView; import com.android.systemui.DualToneHandler; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; import com.android.systemui.qs.QSDetail.Callback; import com.android.systemui.statusbar.phone.PhoneStatusBarView; import com.android.systemui.statusbar.phone.StatusBarIconController; 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.DateView; import com.android.systemui.statusbar.policy.NextAlarmController; import com.android.systemui.statusbar.policy.ZenModeController; import java.util.Locale; import java.util.Objects; import javax.inject.Inject; import javax.inject.Named; /** * View that contains the top-most bits of the screen (primarily the status bar with date, time, and * battery) and also contains the {@link QuickQSPanel} along with some of the panel's inner * contents. */ public class QuickStatusBarHeader extends RelativeLayout implements View.OnClickListener, NextAlarmController.NextAlarmChangeCallback, ZenModeController.Callback { private static final String TAG = "QuickStatusBarHeader"; private static final boolean DEBUG = false; /** Delay for auto fading out the long press tooltip after it's fully visible (in ms). */ private static final long AUTO_FADE_OUT_DELAY_MS = DateUtils.SECOND_IN_MILLIS * 6; private static final int FADE_ANIMATION_DURATION_MS = 300; private static final int TOOLTIP_NOT_YET_SHOWN_COUNT = 0; public static final int MAX_TOOLTIP_SHOWN_COUNT = 2; private final Handler mHandler = new Handler(); private final NextAlarmController mAlarmController; private final ZenModeController mZenController; private final StatusBarIconController mStatusBarIconController; private final ActivityStarter mActivityStarter; private QSPanel mQsPanel; private boolean mExpanded; private boolean mListening; private boolean mQsDisabled; private QSCarrierGroup mCarrierGroup; protected QuickQSPanel mHeaderQsPanel; protected QSTileHost mHost; private TintedIconManager mIconManager; private TouchAnimator mStatusIconsAlphaAnimator; private TouchAnimator mHeaderTextContainerAlphaAnimator; private DualToneHandler mDualToneHandler; private View mSystemIconsView; private View mQuickQsStatusIcons; private View mHeaderTextContainerView; private int mRingerMode = AudioManager.RINGER_MODE_NORMAL; private AlarmManager.AlarmClockInfo mNextAlarm; private ImageView mNextAlarmIcon; /** {@link TextView} containing the actual text indicating when the next alarm will go off. */ private TextView mNextAlarmTextView; private View mNextAlarmContainer; private View mStatusSeparator; private ImageView mRingerModeIcon; private TextView mRingerModeTextView; private View mRingerContainer; private Clock mClockView; private DateView mDateView; private BatteryMeterView mBatteryRemainingIcon; private final BroadcastReceiver mRingerReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mRingerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1); updateStatusText(); } }; private boolean mHasTopCutout = false; @Inject public QuickStatusBarHeader(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs, NextAlarmController nextAlarmController, ZenModeController zenModeController, StatusBarIconController statusBarIconController, ActivityStarter activityStarter) { super(context, attrs); mAlarmController = nextAlarmController; mZenController = zenModeController; mStatusBarIconController = statusBarIconController; mActivityStarter = activityStarter; mDualToneHandler = new DualToneHandler( new ContextThemeWrapper(context, R.style.QSHeaderTheme)); } @Override protected void onFinishInflate() { super.onFinishInflate(); mHeaderQsPanel = findViewById(R.id.quick_qs_panel); mSystemIconsView = findViewById(R.id.quick_status_bar_system_icons); mQuickQsStatusIcons = findViewById(R.id.quick_qs_status_icons); StatusIconContainer iconContainer = findViewById(R.id.statusIcons); iconContainer.setShouldRestrictIcons(false); mIconManager = new TintedIconManager(iconContainer); // Views corresponding to the header info section (e.g. ringer and next alarm). mHeaderTextContainerView = findViewById(R.id.header_text_container); mStatusSeparator = findViewById(R.id.status_separator); mNextAlarmIcon = findViewById(R.id.next_alarm_icon); mNextAlarmTextView = findViewById(R.id.next_alarm_text); mNextAlarmContainer = findViewById(R.id.alarm_container); mNextAlarmContainer.setOnClickListener(this::onClick); mRingerModeIcon = findViewById(R.id.ringer_mode_icon); mRingerModeTextView = findViewById(R.id.ringer_mode_text); mRingerContainer = findViewById(R.id.ringer_container); mCarrierGroup = findViewById(R.id.carrier_group); updateResources(); Rect tintArea = new Rect(0, 0, 0, 0); int colorForeground = Utils.getColorAttrDefaultColor(getContext(), android.R.attr.colorForeground); float intensity = getColorIntensity(colorForeground); int fillColor = mDualToneHandler.getSingleColor(intensity); // Set light text on the header icons because they will always be on a black background applyDarkness(R.id.clock, tintArea, 0, DarkIconDispatcher.DEFAULT_ICON_TINT); // Set the correct tint for the status icons so they contrast mIconManager.setTint(fillColor); mNextAlarmIcon.setImageTintList(ColorStateList.valueOf(fillColor)); mRingerModeIcon.setImageTintList(ColorStateList.valueOf(fillColor)); mClockView = findViewById(R.id.clock); mClockView.setOnClickListener(this); mDateView = findViewById(R.id.date); // Tint for the battery icons are handled in setupHost() mBatteryRemainingIcon = findViewById(R.id.batteryRemainingIcon); // Don't need to worry about tuner settings for this icon mBatteryRemainingIcon.setIgnoreTunerUpdates(true); // QS will always show the estimate, and BatteryMeterView handles the case where // it's unavailable or charging mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE); mRingerModeTextView.setSelected(true); mNextAlarmTextView.setSelected(true); } private void updateStatusText() { boolean changed = updateRingerStatus() || updateAlarmStatus(); if (changed) { boolean alarmVisible = mNextAlarmTextView.getVisibility() == View.VISIBLE; boolean ringerVisible = mRingerModeTextView.getVisibility() == View.VISIBLE; mStatusSeparator.setVisibility(alarmVisible && ringerVisible ? View.VISIBLE : View.GONE); } } private boolean updateRingerStatus() { boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE; CharSequence originalRingerText = mRingerModeTextView.getText(); boolean ringerVisible = false; if (!ZenModeConfig.isZenOverridingRinger(mZenController.getZen(), mZenController.getConsolidatedPolicy())) { if (mRingerMode == AudioManager.RINGER_MODE_VIBRATE) { mRingerModeIcon.setImageResource(R.drawable.ic_volume_ringer_vibrate); mRingerModeTextView.setText(R.string.qs_status_phone_vibrate); ringerVisible = true; } else if (mRingerMode == AudioManager.RINGER_MODE_SILENT) { mRingerModeIcon.setImageResource(R.drawable.ic_volume_ringer_mute); mRingerModeTextView.setText(R.string.qs_status_phone_muted); ringerVisible = true; } } mRingerModeIcon.setVisibility(ringerVisible ? View.VISIBLE : View.GONE); mRingerModeTextView.setVisibility(ringerVisible ? View.VISIBLE : View.GONE); mRingerContainer.setVisibility(ringerVisible ? View.VISIBLE : View.GONE); return isOriginalVisible != ringerVisible || !Objects.equals(originalRingerText, mRingerModeTextView.getText()); } private boolean updateAlarmStatus() { boolean isOriginalVisible = mNextAlarmTextView.getVisibility() == View.VISIBLE; CharSequence originalAlarmText = mNextAlarmTextView.getText(); boolean alarmVisible = false; if (mNextAlarm != null) { alarmVisible = true; mNextAlarmTextView.setText(formatNextAlarm(mNextAlarm)); } mNextAlarmIcon.setVisibility(alarmVisible ? View.VISIBLE : View.GONE); mNextAlarmTextView.setVisibility(alarmVisible ? View.VISIBLE : View.GONE); mNextAlarmContainer.setVisibility(alarmVisible ? View.VISIBLE : View.GONE); return isOriginalVisible != alarmVisible || !Objects.equals(originalAlarmText, mNextAlarmTextView.getText()); } private void applyDarkness(int id, Rect tintArea, float intensity, int color) { View v = findViewById(id); if (v instanceof DarkReceiver) { ((DarkReceiver) v).onDarkChanged(tintArea, intensity, color); } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateResources(); // Update color schemes in landscape to use wallpaperTextColor boolean shouldUseWallpaperTextColor = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE; mClockView.useWallpaperTextColor(shouldUseWallpaperTextColor); } @Override public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); updateResources(); } /** * The height of QQS should always be the status bar height + 128dp. This is normally easy, but * when there is a notch involved the status bar can remain a fixed pixel size. */ private void updateMinimumHeight() { int sbHeight = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_height); int qqsHeight = mContext.getResources().getDimensionPixelSize( R.dimen.qs_quick_header_panel_height); setMinimumHeight(sbHeight + qqsHeight); } private void updateResources() { Resources resources = mContext.getResources(); updateMinimumHeight(); // Update height for a few views, especially due to landscape mode restricting space. mHeaderTextContainerView.getLayoutParams().height = resources.getDimensionPixelSize(R.dimen.qs_header_tooltip_height); mHeaderTextContainerView.setLayoutParams(mHeaderTextContainerView.getLayoutParams()); mSystemIconsView.getLayoutParams().height = resources.getDimensionPixelSize( com.android.internal.R.dimen.quick_qs_offset_height); mSystemIconsView.setLayoutParams(mSystemIconsView.getLayoutParams()); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); if (mQsDisabled) { lp.height = resources.getDimensionPixelSize( com.android.internal.R.dimen.quick_qs_offset_height); } else { lp.height = Math.max(getMinimumHeight(), resources.getDimensionPixelSize( com.android.internal.R.dimen.quick_qs_total_height)); } setLayoutParams(lp); updateStatusIconAlphaAnimator(); updateHeaderTextContainerAlphaAnimator(); } private void updateStatusIconAlphaAnimator() { mStatusIconsAlphaAnimator = new TouchAnimator.Builder() .addFloat(mQuickQsStatusIcons, "alpha", 1, 0, 0) .build(); } private void updateHeaderTextContainerAlphaAnimator() { mHeaderTextContainerAlphaAnimator = new TouchAnimator.Builder() .addFloat(mHeaderTextContainerView, "alpha", 0, 0, 1) .build(); } public void setExpanded(boolean expanded) { if (mExpanded == expanded) return; mExpanded = expanded; mHeaderQsPanel.setExpanded(expanded); updateEverything(); } /** * Animates the inner contents based on the given expansion details. * * @param isKeyguardShowing whether or not we're showing the keyguard (a.k.a. lockscreen) * @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 isKeyguardShowing, float expansionFraction, float panelTranslationY) { final float keyguardExpansionFraction = isKeyguardShowing ? 1f : expansionFraction; if (mStatusIconsAlphaAnimator != null) { mStatusIconsAlphaAnimator.setPosition(keyguardExpansionFraction); } if (isKeyguardShowing) { // If the keyguard is showing, we want to offset the text so that it comes in at the // same time as the panel as it slides down. mHeaderTextContainerView.setTranslationY(panelTranslationY); } else { mHeaderTextContainerView.setTranslationY(0f); } if (mHeaderTextContainerAlphaAnimator != null) { mHeaderTextContainerAlphaAnimator.setPosition(keyguardExpansionFraction); if (keyguardExpansionFraction > 0) { mHeaderTextContainerView.setVisibility(VISIBLE); } else { mHeaderTextContainerView.setVisibility(INVISIBLE); } } } 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); mHeaderTextContainerView.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE); mQuickQsStatusIcons.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE); updateResources(); } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); mStatusBarIconController.addIconGroup(mIconManager); requestApplyInsets(); } @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { DisplayCutout cutout = insets.getDisplayCutout(); Pair padding = PhoneStatusBarView.cornerCutoutMargins( cutout, getDisplay()); if (padding == null) { mSystemIconsView.setPaddingRelative( getResources().getDimensionPixelSize(R.dimen.status_bar_padding_start), 0, getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end), 0); } else { mSystemIconsView.setPadding(padding.first, 0, padding.second, 0); } return super.onApplyWindowInsets(insets); } @Override @VisibleForTesting public void onDetachedFromWindow() { setListening(false); mStatusBarIconController.removeIconGroup(mIconManager); super.onDetachedFromWindow(); } public void setListening(boolean listening) { if (listening == mListening) { return; } mHeaderQsPanel.setListening(listening); mListening = listening; mCarrierGroup.setListening(mListening); if (listening) { mZenController.addCallback(this); mAlarmController.addCallback(this); mContext.registerReceiver(mRingerReceiver, new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)); } else { mZenController.removeCallback(this); mAlarmController.removeCallback(this); mContext.unregisterReceiver(mRingerReceiver); } } @Override public void onClick(View v) { if (v == mClockView) { mActivityStarter.postStartActivityDismissingKeyguard(new Intent( AlarmClock.ACTION_SHOW_ALARMS), 0); } else if (v == mNextAlarmContainer && mNextAlarmContainer.isVisibleToUser()) { if (mNextAlarm.getShowIntent() != null) { mActivityStarter.postStartActivityDismissingKeyguard( mNextAlarm.getShowIntent()); } else { Log.d(TAG, "No PendingIntent for next alarm. Using default intent"); mActivityStarter.postStartActivityDismissingKeyguard(new Intent( AlarmClock.ACTION_SHOW_ALARMS), 0); } } else if (v == mRingerContainer && mRingerContainer.isVisibleToUser()) { mActivityStarter.postStartActivityDismissingKeyguard(new Intent( Settings.ACTION_SOUND_SETTINGS), 0); } } @Override public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) { mNextAlarm = nextAlarm; updateStatusText(); } @Override public void onZenChanged(int zen) { updateStatusText(); } @Override public void onConfigChanged(ZenModeConfig config) { updateStatusText(); } public void updateEverything() { post(() -> setClickable(!mExpanded)); } public void setQSPanel(final QSPanel qsPanel) { mQsPanel = qsPanel; setupHost(qsPanel.getHost()); } public void setupHost(final QSTileHost host) { mHost = host; //host.setHeaderView(mExpandIndicator); mHeaderQsPanel.setQSPanelAndHeader(mQsPanel, this); mHeaderQsPanel.setHost(host, null /* No customization in header */); Rect tintArea = new Rect(0, 0, 0, 0); int colorForeground = Utils.getColorAttrDefaultColor(getContext(), android.R.attr.colorForeground); float intensity = getColorIntensity(colorForeground); int fillColor = mDualToneHandler.getSingleColor(intensity); mBatteryRemainingIcon.onDarkChanged(tintArea, intensity, fillColor); } public void setCallback(Callback qsPanelCallback) { mHeaderQsPanel.setCallback(qsPanelCallback); } private String formatNextAlarm(AlarmManager.AlarmClockInfo info) { if (info == null) { return ""; } String skeleton = android.text.format.DateFormat .is24HourFormat(mContext, ActivityManager.getCurrentUser()) ? "EHm" : "Ehma"; String pattern = android.text.format.DateFormat .getBestDateTimePattern(Locale.getDefault(), skeleton); return android.text.format.DateFormat.format(pattern, info.getTriggerTime()).toString(); } public static float getColorIntensity(@ColorInt int color) { return color == Color.WHITE ? 0 : 1; } public void setMargins(int sideMargins) { for (int i = 0; i < getChildCount(); i++) { View v = getChildAt(i); // Prevents these views from getting set a margin. // The Icon views all have the same padding set in XML to be aligned. if (v == mSystemIconsView || v == mQuickQsStatusIcons || v == mHeaderQsPanel || v == mHeaderTextContainerView) { continue; } RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) v.getLayoutParams(); lp.leftMargin = sideMargins; lp.rightMargin = sideMargins; } } }