Home | History | Annotate | Download | only in qs
      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 
     19 import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
     20 
     21 import android.annotation.ColorInt;
     22 import android.app.ActivityManager;
     23 import android.app.AlarmManager;
     24 import android.content.BroadcastReceiver;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.IntentFilter;
     28 import android.content.res.ColorStateList;
     29 import android.content.res.Configuration;
     30 import android.content.res.Resources;
     31 import android.graphics.Color;
     32 import android.graphics.Rect;
     33 import android.media.AudioManager;
     34 import android.os.Handler;
     35 import android.provider.AlarmClock;
     36 import android.provider.Settings;
     37 import android.service.notification.ZenModeConfig;
     38 import android.text.format.DateUtils;
     39 import android.util.AttributeSet;
     40 import android.util.Log;
     41 import android.util.Pair;
     42 import android.view.ContextThemeWrapper;
     43 import android.view.DisplayCutout;
     44 import android.view.View;
     45 import android.view.WindowInsets;
     46 import android.widget.FrameLayout;
     47 import android.widget.ImageView;
     48 import android.widget.RelativeLayout;
     49 import android.widget.TextView;
     50 
     51 import androidx.annotation.VisibleForTesting;
     52 
     53 import com.android.settingslib.Utils;
     54 import com.android.systemui.BatteryMeterView;
     55 import com.android.systemui.DualToneHandler;
     56 import com.android.systemui.R;
     57 import com.android.systemui.plugins.ActivityStarter;
     58 import com.android.systemui.plugins.DarkIconDispatcher;
     59 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
     60 import com.android.systemui.qs.QSDetail.Callback;
     61 import com.android.systemui.statusbar.phone.PhoneStatusBarView;
     62 import com.android.systemui.statusbar.phone.StatusBarIconController;
     63 import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager;
     64 import com.android.systemui.statusbar.phone.StatusIconContainer;
     65 import com.android.systemui.statusbar.policy.Clock;
     66 import com.android.systemui.statusbar.policy.DateView;
     67 import com.android.systemui.statusbar.policy.NextAlarmController;
     68 import com.android.systemui.statusbar.policy.ZenModeController;
     69 
     70 import java.util.Locale;
     71 import java.util.Objects;
     72 
     73 import javax.inject.Inject;
     74 import javax.inject.Named;
     75 
     76 /**
     77  * View that contains the top-most bits of the screen (primarily the status bar with date, time, and
     78  * battery) and also contains the {@link QuickQSPanel} along with some of the panel's inner
     79  * contents.
     80  */
     81 public class QuickStatusBarHeader extends RelativeLayout implements
     82         View.OnClickListener, NextAlarmController.NextAlarmChangeCallback,
     83         ZenModeController.Callback {
     84     private static final String TAG = "QuickStatusBarHeader";
     85     private static final boolean DEBUG = false;
     86 
     87     /** Delay for auto fading out the long press tooltip after it's fully visible (in ms). */
     88     private static final long AUTO_FADE_OUT_DELAY_MS = DateUtils.SECOND_IN_MILLIS * 6;
     89     private static final int FADE_ANIMATION_DURATION_MS = 300;
     90     private static final int TOOLTIP_NOT_YET_SHOWN_COUNT = 0;
     91     public static final int MAX_TOOLTIP_SHOWN_COUNT = 2;
     92 
     93     private final Handler mHandler = new Handler();
     94     private final NextAlarmController mAlarmController;
     95     private final ZenModeController mZenController;
     96     private final StatusBarIconController mStatusBarIconController;
     97     private final ActivityStarter mActivityStarter;
     98 
     99     private QSPanel mQsPanel;
    100 
    101     private boolean mExpanded;
    102     private boolean mListening;
    103     private boolean mQsDisabled;
    104 
    105     private QSCarrierGroup mCarrierGroup;
    106     protected QuickQSPanel mHeaderQsPanel;
    107     protected QSTileHost mHost;
    108     private TintedIconManager mIconManager;
    109     private TouchAnimator mStatusIconsAlphaAnimator;
    110     private TouchAnimator mHeaderTextContainerAlphaAnimator;
    111     private DualToneHandler mDualToneHandler;
    112 
    113     private View mSystemIconsView;
    114     private View mQuickQsStatusIcons;
    115     private View mHeaderTextContainerView;
    116 
    117     private int mRingerMode = AudioManager.RINGER_MODE_NORMAL;
    118     private AlarmManager.AlarmClockInfo mNextAlarm;
    119 
    120     private ImageView mNextAlarmIcon;
    121     /** {@link TextView} containing the actual text indicating when the next alarm will go off. */
    122     private TextView mNextAlarmTextView;
    123     private View mNextAlarmContainer;
    124     private View mStatusSeparator;
    125     private ImageView mRingerModeIcon;
    126     private TextView mRingerModeTextView;
    127     private View mRingerContainer;
    128     private Clock mClockView;
    129     private DateView mDateView;
    130     private BatteryMeterView mBatteryRemainingIcon;
    131 
    132     private final BroadcastReceiver mRingerReceiver = new BroadcastReceiver() {
    133         @Override
    134         public void onReceive(Context context, Intent intent) {
    135             mRingerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1);
    136             updateStatusText();
    137         }
    138     };
    139     private boolean mHasTopCutout = false;
    140 
    141     @Inject
    142     public QuickStatusBarHeader(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs,
    143             NextAlarmController nextAlarmController, ZenModeController zenModeController,
    144             StatusBarIconController statusBarIconController,
    145             ActivityStarter activityStarter) {
    146         super(context, attrs);
    147         mAlarmController = nextAlarmController;
    148         mZenController = zenModeController;
    149         mStatusBarIconController = statusBarIconController;
    150         mActivityStarter = activityStarter;
    151         mDualToneHandler = new DualToneHandler(
    152                 new ContextThemeWrapper(context, R.style.QSHeaderTheme));
    153     }
    154 
    155     @Override
    156     protected void onFinishInflate() {
    157         super.onFinishInflate();
    158 
    159         mHeaderQsPanel = findViewById(R.id.quick_qs_panel);
    160         mSystemIconsView = findViewById(R.id.quick_status_bar_system_icons);
    161         mQuickQsStatusIcons = findViewById(R.id.quick_qs_status_icons);
    162         StatusIconContainer iconContainer = findViewById(R.id.statusIcons);
    163         iconContainer.setShouldRestrictIcons(false);
    164         mIconManager = new TintedIconManager(iconContainer);
    165 
    166         // Views corresponding to the header info section (e.g. ringer and next alarm).
    167         mHeaderTextContainerView = findViewById(R.id.header_text_container);
    168         mStatusSeparator = findViewById(R.id.status_separator);
    169         mNextAlarmIcon = findViewById(R.id.next_alarm_icon);
    170         mNextAlarmTextView = findViewById(R.id.next_alarm_text);
    171         mNextAlarmContainer = findViewById(R.id.alarm_container);
    172         mNextAlarmContainer.setOnClickListener(this::onClick);
    173         mRingerModeIcon = findViewById(R.id.ringer_mode_icon);
    174         mRingerModeTextView = findViewById(R.id.ringer_mode_text);
    175         mRingerContainer = findViewById(R.id.ringer_container);
    176         mCarrierGroup = findViewById(R.id.carrier_group);
    177 
    178 
    179         updateResources();
    180 
    181         Rect tintArea = new Rect(0, 0, 0, 0);
    182         int colorForeground = Utils.getColorAttrDefaultColor(getContext(),
    183                 android.R.attr.colorForeground);
    184         float intensity = getColorIntensity(colorForeground);
    185         int fillColor = mDualToneHandler.getSingleColor(intensity);
    186 
    187         // Set light text on the header icons because they will always be on a black background
    188         applyDarkness(R.id.clock, tintArea, 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
    189 
    190         // Set the correct tint for the status icons so they contrast
    191         mIconManager.setTint(fillColor);
    192         mNextAlarmIcon.setImageTintList(ColorStateList.valueOf(fillColor));
    193         mRingerModeIcon.setImageTintList(ColorStateList.valueOf(fillColor));
    194 
    195         mClockView = findViewById(R.id.clock);
    196         mClockView.setOnClickListener(this);
    197         mDateView = findViewById(R.id.date);
    198 
    199         // Tint for the battery icons are handled in setupHost()
    200         mBatteryRemainingIcon = findViewById(R.id.batteryRemainingIcon);
    201         // Don't need to worry about tuner settings for this icon
    202         mBatteryRemainingIcon.setIgnoreTunerUpdates(true);
    203         // QS will always show the estimate, and BatteryMeterView handles the case where
    204         // it's unavailable or charging
    205         mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
    206         mRingerModeTextView.setSelected(true);
    207         mNextAlarmTextView.setSelected(true);
    208     }
    209 
    210     private void updateStatusText() {
    211         boolean changed = updateRingerStatus() || updateAlarmStatus();
    212 
    213         if (changed) {
    214             boolean alarmVisible = mNextAlarmTextView.getVisibility() == View.VISIBLE;
    215             boolean ringerVisible = mRingerModeTextView.getVisibility() == View.VISIBLE;
    216             mStatusSeparator.setVisibility(alarmVisible && ringerVisible ? View.VISIBLE
    217                     : View.GONE);
    218         }
    219     }
    220 
    221     private boolean updateRingerStatus() {
    222         boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE;
    223         CharSequence originalRingerText = mRingerModeTextView.getText();
    224 
    225         boolean ringerVisible = false;
    226         if (!ZenModeConfig.isZenOverridingRinger(mZenController.getZen(),
    227                 mZenController.getConsolidatedPolicy())) {
    228             if (mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
    229                 mRingerModeIcon.setImageResource(R.drawable.ic_volume_ringer_vibrate);
    230                 mRingerModeTextView.setText(R.string.qs_status_phone_vibrate);
    231                 ringerVisible = true;
    232             } else if (mRingerMode == AudioManager.RINGER_MODE_SILENT) {
    233                 mRingerModeIcon.setImageResource(R.drawable.ic_volume_ringer_mute);
    234                 mRingerModeTextView.setText(R.string.qs_status_phone_muted);
    235                 ringerVisible = true;
    236             }
    237         }
    238         mRingerModeIcon.setVisibility(ringerVisible ? View.VISIBLE : View.GONE);
    239         mRingerModeTextView.setVisibility(ringerVisible ? View.VISIBLE : View.GONE);
    240         mRingerContainer.setVisibility(ringerVisible ? View.VISIBLE : View.GONE);
    241 
    242         return isOriginalVisible != ringerVisible ||
    243                 !Objects.equals(originalRingerText, mRingerModeTextView.getText());
    244     }
    245 
    246     private boolean updateAlarmStatus() {
    247         boolean isOriginalVisible = mNextAlarmTextView.getVisibility() == View.VISIBLE;
    248         CharSequence originalAlarmText = mNextAlarmTextView.getText();
    249 
    250         boolean alarmVisible = false;
    251         if (mNextAlarm != null) {
    252             alarmVisible = true;
    253             mNextAlarmTextView.setText(formatNextAlarm(mNextAlarm));
    254         }
    255         mNextAlarmIcon.setVisibility(alarmVisible ? View.VISIBLE : View.GONE);
    256         mNextAlarmTextView.setVisibility(alarmVisible ? View.VISIBLE : View.GONE);
    257         mNextAlarmContainer.setVisibility(alarmVisible ? View.VISIBLE : View.GONE);
    258 
    259         return isOriginalVisible != alarmVisible ||
    260                 !Objects.equals(originalAlarmText, mNextAlarmTextView.getText());
    261     }
    262 
    263     private void applyDarkness(int id, Rect tintArea, float intensity, int color) {
    264         View v = findViewById(id);
    265         if (v instanceof DarkReceiver) {
    266             ((DarkReceiver) v).onDarkChanged(tintArea, intensity, color);
    267         }
    268     }
    269 
    270     @Override
    271     protected void onConfigurationChanged(Configuration newConfig) {
    272         super.onConfigurationChanged(newConfig);
    273         updateResources();
    274 
    275         // Update color schemes in landscape to use wallpaperTextColor
    276         boolean shouldUseWallpaperTextColor =
    277                 newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
    278         mClockView.useWallpaperTextColor(shouldUseWallpaperTextColor);
    279     }
    280 
    281     @Override
    282     public void onRtlPropertiesChanged(int layoutDirection) {
    283         super.onRtlPropertiesChanged(layoutDirection);
    284         updateResources();
    285     }
    286 
    287     /**
    288      * The height of QQS should always be the status bar height + 128dp. This is normally easy, but
    289      * when there is a notch involved the status bar can remain a fixed pixel size.
    290      */
    291     private void updateMinimumHeight() {
    292         int sbHeight = mContext.getResources().getDimensionPixelSize(
    293                 com.android.internal.R.dimen.status_bar_height);
    294         int qqsHeight = mContext.getResources().getDimensionPixelSize(
    295                 R.dimen.qs_quick_header_panel_height);
    296 
    297         setMinimumHeight(sbHeight + qqsHeight);
    298     }
    299 
    300     private void updateResources() {
    301         Resources resources = mContext.getResources();
    302         updateMinimumHeight();
    303 
    304         // Update height for a few views, especially due to landscape mode restricting space.
    305         mHeaderTextContainerView.getLayoutParams().height =
    306                 resources.getDimensionPixelSize(R.dimen.qs_header_tooltip_height);
    307         mHeaderTextContainerView.setLayoutParams(mHeaderTextContainerView.getLayoutParams());
    308 
    309         mSystemIconsView.getLayoutParams().height = resources.getDimensionPixelSize(
    310                 com.android.internal.R.dimen.quick_qs_offset_height);
    311         mSystemIconsView.setLayoutParams(mSystemIconsView.getLayoutParams());
    312 
    313         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
    314         if (mQsDisabled) {
    315             lp.height = resources.getDimensionPixelSize(
    316                     com.android.internal.R.dimen.quick_qs_offset_height);
    317         } else {
    318             lp.height = Math.max(getMinimumHeight(),
    319                     resources.getDimensionPixelSize(
    320                             com.android.internal.R.dimen.quick_qs_total_height));
    321         }
    322 
    323         setLayoutParams(lp);
    324 
    325         updateStatusIconAlphaAnimator();
    326         updateHeaderTextContainerAlphaAnimator();
    327     }
    328 
    329     private void updateStatusIconAlphaAnimator() {
    330         mStatusIconsAlphaAnimator = new TouchAnimator.Builder()
    331                 .addFloat(mQuickQsStatusIcons, "alpha", 1, 0, 0)
    332                 .build();
    333     }
    334 
    335     private void updateHeaderTextContainerAlphaAnimator() {
    336         mHeaderTextContainerAlphaAnimator = new TouchAnimator.Builder()
    337                 .addFloat(mHeaderTextContainerView, "alpha", 0, 0, 1)
    338                 .build();
    339     }
    340 
    341     public void setExpanded(boolean expanded) {
    342         if (mExpanded == expanded) return;
    343         mExpanded = expanded;
    344         mHeaderQsPanel.setExpanded(expanded);
    345         updateEverything();
    346     }
    347 
    348     /**
    349      * Animates the inner contents based on the given expansion details.
    350      *
    351      * @param isKeyguardShowing whether or not we're showing the keyguard (a.k.a. lockscreen)
    352      * @param expansionFraction how much the QS panel is expanded/pulled out (up to 1f)
    353      * @param panelTranslationY how much the panel has physically moved down vertically (required
    354      *                          for keyguard animations only)
    355      */
    356     public void setExpansion(boolean isKeyguardShowing, float expansionFraction,
    357                              float panelTranslationY) {
    358         final float keyguardExpansionFraction = isKeyguardShowing ? 1f : expansionFraction;
    359         if (mStatusIconsAlphaAnimator != null) {
    360             mStatusIconsAlphaAnimator.setPosition(keyguardExpansionFraction);
    361         }
    362 
    363         if (isKeyguardShowing) {
    364             // If the keyguard is showing, we want to offset the text so that it comes in at the
    365             // same time as the panel as it slides down.
    366             mHeaderTextContainerView.setTranslationY(panelTranslationY);
    367         } else {
    368             mHeaderTextContainerView.setTranslationY(0f);
    369         }
    370 
    371         if (mHeaderTextContainerAlphaAnimator != null) {
    372             mHeaderTextContainerAlphaAnimator.setPosition(keyguardExpansionFraction);
    373             if (keyguardExpansionFraction > 0) {
    374                 mHeaderTextContainerView.setVisibility(VISIBLE);
    375             } else {
    376                 mHeaderTextContainerView.setVisibility(INVISIBLE);
    377             }
    378         }
    379     }
    380 
    381     public void disable(int state1, int state2, boolean animate) {
    382         final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
    383         if (disabled == mQsDisabled) return;
    384         mQsDisabled = disabled;
    385         mHeaderQsPanel.setDisabledByPolicy(disabled);
    386         mHeaderTextContainerView.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
    387         mQuickQsStatusIcons.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
    388         updateResources();
    389     }
    390 
    391     @Override
    392     public void onAttachedToWindow() {
    393         super.onAttachedToWindow();
    394         mStatusBarIconController.addIconGroup(mIconManager);
    395         requestApplyInsets();
    396     }
    397 
    398     @Override
    399     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
    400         DisplayCutout cutout = insets.getDisplayCutout();
    401         Pair<Integer, Integer> padding = PhoneStatusBarView.cornerCutoutMargins(
    402                 cutout, getDisplay());
    403         if (padding == null) {
    404             mSystemIconsView.setPaddingRelative(
    405                     getResources().getDimensionPixelSize(R.dimen.status_bar_padding_start), 0,
    406                     getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end), 0);
    407         } else {
    408             mSystemIconsView.setPadding(padding.first, 0, padding.second, 0);
    409 
    410         }
    411         return super.onApplyWindowInsets(insets);
    412     }
    413 
    414     @Override
    415     @VisibleForTesting
    416     public void onDetachedFromWindow() {
    417         setListening(false);
    418         mStatusBarIconController.removeIconGroup(mIconManager);
    419         super.onDetachedFromWindow();
    420     }
    421 
    422     public void setListening(boolean listening) {
    423         if (listening == mListening) {
    424             return;
    425         }
    426         mHeaderQsPanel.setListening(listening);
    427         mListening = listening;
    428         mCarrierGroup.setListening(mListening);
    429 
    430         if (listening) {
    431             mZenController.addCallback(this);
    432             mAlarmController.addCallback(this);
    433             mContext.registerReceiver(mRingerReceiver,
    434                     new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
    435         } else {
    436             mZenController.removeCallback(this);
    437             mAlarmController.removeCallback(this);
    438             mContext.unregisterReceiver(mRingerReceiver);
    439         }
    440     }
    441 
    442     @Override
    443     public void onClick(View v) {
    444         if (v == mClockView) {
    445             mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
    446                     AlarmClock.ACTION_SHOW_ALARMS), 0);
    447         } else if (v == mNextAlarmContainer && mNextAlarmContainer.isVisibleToUser()) {
    448             if (mNextAlarm.getShowIntent() != null) {
    449                 mActivityStarter.postStartActivityDismissingKeyguard(
    450                         mNextAlarm.getShowIntent());
    451             } else {
    452                 Log.d(TAG, "No PendingIntent for next alarm. Using default intent");
    453                 mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
    454                         AlarmClock.ACTION_SHOW_ALARMS), 0);
    455             }
    456         } else if (v == mRingerContainer && mRingerContainer.isVisibleToUser()) {
    457             mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
    458                     Settings.ACTION_SOUND_SETTINGS), 0);
    459         }
    460     }
    461 
    462     @Override
    463     public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) {
    464         mNextAlarm = nextAlarm;
    465         updateStatusText();
    466     }
    467 
    468     @Override
    469     public void onZenChanged(int zen) {
    470         updateStatusText();
    471     }
    472 
    473     @Override
    474     public void onConfigChanged(ZenModeConfig config) {
    475         updateStatusText();
    476     }
    477 
    478     public void updateEverything() {
    479         post(() -> setClickable(!mExpanded));
    480     }
    481 
    482     public void setQSPanel(final QSPanel qsPanel) {
    483         mQsPanel = qsPanel;
    484         setupHost(qsPanel.getHost());
    485     }
    486 
    487     public void setupHost(final QSTileHost host) {
    488         mHost = host;
    489         //host.setHeaderView(mExpandIndicator);
    490         mHeaderQsPanel.setQSPanelAndHeader(mQsPanel, this);
    491         mHeaderQsPanel.setHost(host, null /* No customization in header */);
    492 
    493 
    494         Rect tintArea = new Rect(0, 0, 0, 0);
    495         int colorForeground = Utils.getColorAttrDefaultColor(getContext(),
    496                 android.R.attr.colorForeground);
    497         float intensity = getColorIntensity(colorForeground);
    498         int fillColor = mDualToneHandler.getSingleColor(intensity);
    499         mBatteryRemainingIcon.onDarkChanged(tintArea, intensity, fillColor);
    500     }
    501 
    502     public void setCallback(Callback qsPanelCallback) {
    503         mHeaderQsPanel.setCallback(qsPanelCallback);
    504     }
    505 
    506     private String formatNextAlarm(AlarmManager.AlarmClockInfo info) {
    507         if (info == null) {
    508             return "";
    509         }
    510         String skeleton = android.text.format.DateFormat
    511                 .is24HourFormat(mContext, ActivityManager.getCurrentUser()) ? "EHm" : "Ehma";
    512         String pattern = android.text.format.DateFormat
    513                 .getBestDateTimePattern(Locale.getDefault(), skeleton);
    514         return android.text.format.DateFormat.format(pattern, info.getTriggerTime()).toString();
    515     }
    516 
    517     public static float getColorIntensity(@ColorInt int color) {
    518         return color == Color.WHITE ? 0 : 1;
    519     }
    520 
    521     public void setMargins(int sideMargins) {
    522         for (int i = 0; i < getChildCount(); i++) {
    523             View v = getChildAt(i);
    524             // Prevents these views from getting set a margin.
    525             // The Icon views all have the same padding set in XML to be aligned.
    526             if (v == mSystemIconsView || v == mQuickQsStatusIcons || v == mHeaderQsPanel
    527                     || v == mHeaderTextContainerView) {
    528                 continue;
    529             }
    530             RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) v.getLayoutParams();
    531             lp.leftMargin = sideMargins;
    532             lp.rightMargin = sideMargins;
    533         }
    534     }
    535 }
    536