QuickStatusBarHeader.java revision 7d86b04f30690605d18d44bfb0f1f2d9a9f0ee64
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
15package com.android.systemui.qs;
16
17import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.annotation.ColorInt;
22import android.app.ActivityManager;
23import android.app.AlarmManager;
24import android.content.BroadcastReceiver;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.content.res.Configuration;
29import android.content.res.Resources;
30import android.graphics.Color;
31import android.graphics.Rect;
32import android.media.AudioManager;
33import android.os.Handler;
34import android.provider.AlarmClock;
35import android.service.notification.ZenModeConfig;
36import android.support.annotation.VisibleForTesting;
37import android.text.format.DateUtils;
38import android.util.AttributeSet;
39import android.util.Log;
40import android.util.Pair;
41import android.view.View;
42import android.view.WindowInsets;
43import android.widget.ImageView;
44import android.widget.RelativeLayout;
45import android.widget.TextView;
46
47import com.android.settingslib.Utils;
48import com.android.systemui.BatteryMeterView;
49import com.android.systemui.Dependency;
50import com.android.systemui.Prefs;
51import com.android.systemui.R;
52import com.android.systemui.plugins.ActivityStarter;
53import com.android.systemui.qs.QSDetail.Callback;
54import com.android.systemui.statusbar.phone.PhoneStatusBarView;
55import com.android.systemui.statusbar.phone.StatusBarIconController;
56import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager;
57import com.android.systemui.statusbar.policy.Clock;
58import com.android.systemui.statusbar.phone.StatusIconContainer;
59import com.android.systemui.statusbar.policy.DarkIconDispatcher;
60import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
61import com.android.systemui.statusbar.policy.DateView;
62import com.android.systemui.statusbar.policy.NextAlarmController;
63import com.android.systemui.statusbar.policy.ZenModeController;
64
65import java.util.Locale;
66import java.util.Objects;
67
68/**
69 * View that contains the top-most bits of the screen (primarily the status bar with date, time, and
70 * battery) and also contains the {@link QuickQSPanel} along with some of the panel's inner
71 * contents.
72 */
73public class QuickStatusBarHeader extends RelativeLayout implements
74        View.OnClickListener, NextAlarmController.NextAlarmChangeCallback,
75        ZenModeController.Callback {
76    private static final String TAG = "QuickStatusBarHeader";
77    private static final boolean DEBUG = false;
78
79    /** Delay for auto fading out the long press tooltip after it's fully visible (in ms). */
80    private static final long AUTO_FADE_OUT_DELAY_MS = DateUtils.SECOND_IN_MILLIS * 6;
81    private static final int FADE_ANIMATION_DURATION_MS = 300;
82    private static final int TOOLTIP_NOT_YET_SHOWN_COUNT = 0;
83    public static final int MAX_TOOLTIP_SHOWN_COUNT = 2;
84
85    private final Handler mHandler = new Handler();
86
87    private QSPanel mQsPanel;
88
89    private boolean mExpanded;
90    private boolean mListening;
91    private boolean mQsDisabled;
92
93    protected QuickQSPanel mHeaderQsPanel;
94    protected QSTileHost mHost;
95    private TintedIconManager mIconManager;
96    private TouchAnimator mStatusIconsAlphaAnimator;
97    private TouchAnimator mHeaderTextContainerAlphaAnimator;
98
99    private View mSystemIconsView;
100    private View mQuickQsStatusIcons;
101    private View mDate;
102    private View mHeaderTextContainerView;
103    /** View containing the next alarm and ringer mode info. */
104    private View mStatusContainer;
105    /** Tooltip for educating users that they can long press on icons to see more details. */
106    private View mLongPressTooltipView;
107
108    private int mRingerMode = AudioManager.RINGER_MODE_NORMAL;
109    private AlarmManager.AlarmClockInfo mNextAlarm;
110
111    private ImageView mNextAlarmIcon;
112    /** {@link TextView} containing the actual text indicating when the next alarm will go off. */
113    private TextView mNextAlarmTextView;
114    private View mStatusSeparator;
115    private ImageView mRingerModeIcon;
116    private TextView mRingerModeTextView;
117    private BatteryMeterView mBatteryMeterView;
118    private Clock mClockView;
119    private DateView mDateView;
120
121    private NextAlarmController mAlarmController;
122    private ZenModeController mZenController;
123    /** Counts how many times the long press tooltip has been shown to the user. */
124    private int mShownCount;
125
126    private final BroadcastReceiver mRingerReceiver = new BroadcastReceiver() {
127        @Override
128        public void onReceive(Context context, Intent intent) {
129            mRingerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1);
130            updateStatusText();
131        }
132    };
133
134    /**
135     * Runnable for automatically fading out the long press tooltip (as if it were animating away).
136     */
137    private final Runnable mAutoFadeOutTooltipRunnable = () -> hideLongPressTooltip(false);
138
139    public QuickStatusBarHeader(Context context, AttributeSet attrs) {
140        super(context, attrs);
141        mAlarmController = Dependency.get(NextAlarmController.class);
142        mZenController = Dependency.get(ZenModeController.class);
143        mShownCount = getStoredShownCount();
144    }
145
146    @Override
147    protected void onFinishInflate() {
148        super.onFinishInflate();
149
150        mHeaderQsPanel = findViewById(R.id.quick_qs_panel);
151        mDate = findViewById(R.id.date);
152        mDate.setOnClickListener(this);
153        mSystemIconsView = findViewById(R.id.quick_status_bar_system_icons);
154        mQuickQsStatusIcons = findViewById(R.id.quick_qs_status_icons);
155        StatusIconContainer iconContainer = findViewById(R.id.statusIcons);
156        iconContainer.setShouldRestrictIcons(false);
157        mIconManager = new TintedIconManager(iconContainer);
158
159        // Views corresponding to the header info section (e.g. tooltip and next alarm).
160        mHeaderTextContainerView = findViewById(R.id.header_text_container);
161        mLongPressTooltipView = findViewById(R.id.long_press_tooltip);
162        mStatusContainer = findViewById(R.id.status_container);
163        mStatusSeparator = findViewById(R.id.status_separator);
164        mNextAlarmIcon = findViewById(R.id.next_alarm_icon);
165        mNextAlarmTextView = findViewById(R.id.next_alarm_text);
166        mRingerModeIcon = findViewById(R.id.ringer_mode_icon);
167        mRingerModeTextView = findViewById(R.id.ringer_mode_text);
168
169        updateResources();
170
171        Rect tintArea = new Rect(0, 0, 0, 0);
172        int colorForeground = Utils.getColorAttr(getContext(), android.R.attr.colorForeground);
173        float intensity = getColorIntensity(colorForeground);
174        int fillColor = fillColorForIntensity(intensity, getContext());
175
176        // Set light text on the header icons because they will always be on a black background
177        applyDarkness(R.id.clock, tintArea, 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
178
179        // Set the correct tint for the status icons so they contrast
180        mIconManager.setTint(fillColor);
181
182        mBatteryMeterView = findViewById(R.id.battery);
183        mBatteryMeterView.setForceShowPercent(true);
184        mClockView = findViewById(R.id.clock);
185        mDateView = findViewById(R.id.date);
186    }
187
188    private void updateStatusText() {
189        boolean changed = updateRingerStatus() || updateAlarmStatus();
190
191        if (changed) {
192            boolean alarmVisible = mNextAlarmTextView.getVisibility() == View.VISIBLE;
193            boolean ringerVisible = mRingerModeTextView.getVisibility() == View.VISIBLE;
194            mStatusSeparator.setVisibility(alarmVisible && ringerVisible ? View.VISIBLE
195                    : View.GONE);
196            updateTooltipShow();
197        }
198    }
199
200    private boolean updateRingerStatus() {
201        boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE;
202        CharSequence originalRingerText = mRingerModeTextView.getText();
203
204        boolean ringerVisible = false;
205        if (!ZenModeConfig.isZenOverridingRinger(mZenController.getZen(),
206                mZenController.getConfig())) {
207            if (mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
208                mRingerModeIcon.setImageResource(R.drawable.stat_sys_ringer_vibrate);
209                mRingerModeTextView.setText(R.string.qs_status_phone_vibrate);
210                ringerVisible = true;
211            } else if (mRingerMode == AudioManager.RINGER_MODE_SILENT) {
212                mRingerModeIcon.setImageResource(R.drawable.stat_sys_ringer_silent);
213                mRingerModeTextView.setText(R.string.qs_status_phone_muted);
214                ringerVisible = true;
215            }
216        }
217        mRingerModeIcon.setVisibility(ringerVisible ? View.VISIBLE : View.GONE);
218        mRingerModeTextView.setVisibility(ringerVisible ? View.VISIBLE : View.GONE);
219
220        return isOriginalVisible != ringerVisible ||
221                !Objects.equals(originalRingerText, mRingerModeTextView.getText());
222    }
223
224    private boolean updateAlarmStatus() {
225        boolean isOriginalVisible = mNextAlarmTextView.getVisibility() == View.VISIBLE;
226        CharSequence originalAlarmText = mNextAlarmTextView.getText();
227
228        boolean alarmVisible = false;
229        if (mNextAlarm != null) {
230            alarmVisible = true;
231            mNextAlarmTextView.setText(formatNextAlarm(mNextAlarm));
232        }
233        mNextAlarmIcon.setVisibility(alarmVisible ? View.VISIBLE : View.GONE);
234        mNextAlarmTextView.setVisibility(alarmVisible ? View.VISIBLE : View.GONE);
235
236        return isOriginalVisible != alarmVisible ||
237                !Objects.equals(originalAlarmText, mNextAlarmTextView.getText());
238    }
239
240    private void applyDarkness(int id, Rect tintArea, float intensity, int color) {
241        View v = findViewById(id);
242        if (v instanceof DarkReceiver) {
243            ((DarkReceiver) v).onDarkChanged(tintArea, intensity, color);
244        }
245    }
246
247    private int fillColorForIntensity(float intensity, Context context) {
248        if (intensity == 0) {
249            return context.getColor(R.color.light_mode_icon_color_single_tone);
250        }
251        return context.getColor(R.color.dark_mode_icon_color_single_tone);
252    }
253
254    @Override
255    protected void onConfigurationChanged(Configuration newConfig) {
256        super.onConfigurationChanged(newConfig);
257        updateResources();
258
259        // Update color schemes in landscape to use wallpaperTextColor
260        boolean shouldUseWallpaperTextColor =
261                newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
262        mBatteryMeterView.useWallpaperTextColor(shouldUseWallpaperTextColor);
263        mClockView.useWallpaperTextColor(shouldUseWallpaperTextColor);
264        mDateView.useWallpaperTextColor(shouldUseWallpaperTextColor);
265    }
266
267    @Override
268    public void onRtlPropertiesChanged(int layoutDirection) {
269        super.onRtlPropertiesChanged(layoutDirection);
270        updateResources();
271    }
272
273    private void updateResources() {
274        Resources resources = mContext.getResources();
275
276        // Update height for a few views, especially due to landscape mode restricting space.
277        mHeaderTextContainerView.getLayoutParams().height =
278                resources.getDimensionPixelSize(R.dimen.qs_header_tooltip_height);
279        mHeaderTextContainerView.setLayoutParams(mHeaderTextContainerView.getLayoutParams());
280
281        mSystemIconsView.getLayoutParams().height = resources.getDimensionPixelSize(
282                com.android.internal.R.dimen.quick_qs_offset_height);
283        mSystemIconsView.setLayoutParams(mSystemIconsView.getLayoutParams());
284
285        getLayoutParams().height = resources.getDimensionPixelSize(mQsDisabled
286                ? com.android.internal.R.dimen.quick_qs_offset_height
287                : com.android.internal.R.dimen.quick_qs_total_height);
288        setLayoutParams(getLayoutParams());
289
290        updateStatusIconAlphaAnimator();
291        updateHeaderTextContainerAlphaAnimator();
292    }
293
294    private void updateStatusIconAlphaAnimator() {
295        mStatusIconsAlphaAnimator = new TouchAnimator.Builder()
296                .addFloat(mQuickQsStatusIcons, "alpha", 1, 0)
297                .build();
298    }
299
300    private void updateHeaderTextContainerAlphaAnimator() {
301        mHeaderTextContainerAlphaAnimator = new TouchAnimator.Builder()
302                .addFloat(mHeaderTextContainerView, "alpha", 0, 1)
303                .setStartDelay(.5f)
304                .build();
305    }
306
307    public void setExpanded(boolean expanded) {
308        if (mExpanded == expanded) return;
309        mExpanded = expanded;
310        mHeaderQsPanel.setExpanded(expanded);
311        updateEverything();
312    }
313
314    /**
315     * Animates the inner contents based on the given expansion details.
316     *
317     * @param isKeyguardShowing whether or not we're showing the keyguard (a.k.a. lockscreen)
318     * @param expansionFraction how much the QS panel is expanded/pulled out (up to 1f)
319     * @param panelTranslationY how much the panel has physically moved down vertically (required
320     *                          for keyguard animations only)
321     */
322    public void setExpansion(boolean isKeyguardShowing, float expansionFraction,
323                             float panelTranslationY) {
324        final float keyguardExpansionFraction = isKeyguardShowing ? 1f : expansionFraction;
325        if (mStatusIconsAlphaAnimator != null) {
326            mStatusIconsAlphaAnimator.setPosition(keyguardExpansionFraction);
327        }
328
329        if (isKeyguardShowing) {
330            // If the keyguard is showing, we want to offset the text so that it comes in at the
331            // same time as the panel as it slides down.
332            mHeaderTextContainerView.setTranslationY(panelTranslationY);
333        } else {
334            mHeaderTextContainerView.setTranslationY(0f);
335        }
336
337        if (mHeaderTextContainerAlphaAnimator != null) {
338            mHeaderTextContainerAlphaAnimator.setPosition(keyguardExpansionFraction);
339        }
340
341        // Check the original expansion fraction - we don't want to show the tooltip until the
342        // panel is pulled all the way out.
343        if (expansionFraction == 1f) {
344            // QS is fully expanded, bring in the tooltip.
345            showLongPressTooltip();
346        }
347    }
348
349    /** Returns the latest stored tooltip shown count from SharedPreferences. */
350    private int getStoredShownCount() {
351        return Prefs.getInt(
352                mContext,
353                Prefs.Key.QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT,
354                TOOLTIP_NOT_YET_SHOWN_COUNT);
355    }
356
357    public void disable(int state1, int state2, boolean animate) {
358        final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
359        if (disabled == mQsDisabled) return;
360        mQsDisabled = disabled;
361        mHeaderQsPanel.setDisabledByPolicy(disabled);
362        mHeaderTextContainerView.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
363        mQuickQsStatusIcons.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
364        updateResources();
365    }
366
367    @Override
368    public void onAttachedToWindow() {
369        Dependency.get(StatusBarIconController.class).addIconGroup(mIconManager);
370        requestApplyInsets();
371    }
372
373    @Override
374    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
375        Pair<Integer, Integer> padding = PhoneStatusBarView.cornerCutoutMargins(
376                insets.getDisplayCutout(), getDisplay());
377        if (padding == null) {
378            mSystemIconsView.setPaddingRelative(
379                    getResources().getDimensionPixelSize(R.dimen.status_bar_padding_start), 0,
380                    getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end), 0);
381        } else {
382            mSystemIconsView.setPadding(padding.first, 0, padding.second, 0);
383
384        }
385        return super.onApplyWindowInsets(insets);
386    }
387
388    @Override
389    @VisibleForTesting
390    public void onDetachedFromWindow() {
391        setListening(false);
392        Dependency.get(StatusBarIconController.class).removeIconGroup(mIconManager);
393        super.onDetachedFromWindow();
394    }
395
396    public void setListening(boolean listening) {
397        if (listening == mListening) {
398            return;
399        }
400        mHeaderQsPanel.setListening(listening);
401        mListening = listening;
402
403        if (listening) {
404            mZenController.addCallback(this);
405            mAlarmController.addCallback(this);
406            mContext.registerReceiver(mRingerReceiver,
407                    new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
408        } else {
409            mZenController.removeCallback(this);
410            mAlarmController.removeCallback(this);
411            mContext.unregisterReceiver(mRingerReceiver);
412        }
413    }
414
415    @Override
416    public void onClick(View v) {
417        if(v == mDate){
418            Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(new Intent(
419                    AlarmClock.ACTION_SHOW_ALARMS),0);
420        }
421    }
422
423    @Override
424    public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) {
425        mNextAlarm = nextAlarm;
426        updateStatusText();
427    }
428
429    @Override
430    public void onZenChanged(int zen) {
431        updateStatusText();
432
433    }
434
435    @Override
436    public void onConfigChanged(ZenModeConfig config) {
437        updateStatusText();
438    }
439
440    private void updateTooltipShow() {
441        if (hasStatusText()) {
442            hideLongPressTooltip(true /* shouldShowStatusText */);
443        } else {
444            hideStatusText();
445        }
446        updateHeaderTextContainerAlphaAnimator();
447    }
448
449    private boolean hasStatusText() {
450        return mNextAlarmTextView.getVisibility() == View.VISIBLE
451                || mRingerModeTextView.getVisibility() == View.VISIBLE;
452    }
453
454    /**
455     * Animates in the long press tooltip (as long as the next alarm text isn't currently occupying
456     * the space).
457     */
458    public void showLongPressTooltip() {
459        // If we have status text to show, don't bother fading in the tooltip.
460        if (hasStatusText()) {
461            return;
462        }
463
464        if (mShownCount < MAX_TOOLTIP_SHOWN_COUNT) {
465            mLongPressTooltipView.animate().cancel();
466            mLongPressTooltipView.setVisibility(View.VISIBLE);
467            mLongPressTooltipView.animate()
468                    .alpha(1f)
469                    .setDuration(FADE_ANIMATION_DURATION_MS)
470                    .setListener(new AnimatorListenerAdapter() {
471                        @Override
472                        public void onAnimationEnd(Animator animation) {
473                            mHandler.postDelayed(
474                                    mAutoFadeOutTooltipRunnable, AUTO_FADE_OUT_DELAY_MS);
475                        }
476                    })
477                    .start();
478
479            // Increment and drop the shown count in prefs for the next time we're deciding to
480            // fade in the tooltip. We first sanity check that the tooltip count hasn't changed yet
481            // in prefs (say, from a long press).
482            if (getStoredShownCount() <= mShownCount) {
483                Prefs.putInt(mContext, Prefs.Key.QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT, ++mShownCount);
484            }
485        }
486    }
487
488    /**
489     * Fades out the long press tooltip if it's partially visible - short circuits any running
490     * animation. Additionally has the ability to fade in the status info text.
491     *
492     * @param shouldShowStatusText whether we should fade in the status text
493     */
494    private void hideLongPressTooltip(boolean shouldShowStatusText) {
495        mLongPressTooltipView.animate().cancel();
496        if (mLongPressTooltipView.getVisibility() == View.VISIBLE
497                && mLongPressTooltipView.getAlpha() != 0f) {
498            mHandler.removeCallbacks(mAutoFadeOutTooltipRunnable);
499            mLongPressTooltipView.animate()
500                    .alpha(0f)
501                    .setDuration(FADE_ANIMATION_DURATION_MS)
502                    .setListener(new AnimatorListenerAdapter() {
503                        @Override
504                        public void onAnimationEnd(Animator animation) {
505                            if (DEBUG) Log.d(TAG, "hideLongPressTooltip: Hid long press tip");
506                            mLongPressTooltipView.setVisibility(View.INVISIBLE);
507
508                            if (shouldShowStatusText) {
509                                showStatus();
510                            }
511                        }
512                    })
513                    .start();
514        } else {
515            mLongPressTooltipView.setVisibility(View.INVISIBLE);
516            if (shouldShowStatusText) {
517                showStatus();
518            }
519        }
520    }
521
522    /**
523     * Fades in the updated status text. Note that if there's already a status showing, this will
524     * immediately hide it and fade in the updated status.
525     */
526    private void showStatus() {
527        mStatusContainer.setAlpha(0f);
528        mStatusContainer.setVisibility(View.VISIBLE);
529
530        // Animate the alarm back in. Make sure to clear the animator listener for the animation!
531        mStatusContainer.animate()
532                .alpha(1f)
533                .setDuration(FADE_ANIMATION_DURATION_MS)
534                .setListener(null)
535                .start();
536    }
537
538    /** Fades out and hides the status text. */
539    private void hideStatusText() {
540        if (mStatusContainer.getVisibility() == View.VISIBLE) {
541            mStatusContainer.animate()
542                    .alpha(0f)
543                    .setListener(new AnimatorListenerAdapter() {
544                        @Override
545                        public void onAnimationEnd(Animator animation) {
546                            if (DEBUG) Log.d(TAG, "hideAlarmText: Hid alarm text");
547
548                            // Reset the alpha regardless of how the animation ends for the next
549                            // time we show this view/want to animate it.
550                            mStatusContainer.setVisibility(View.INVISIBLE);
551                            mStatusContainer.setAlpha(1f);
552                        }
553                    })
554                    .start();
555        }
556    }
557
558    public void updateEverything() {
559        post(() -> setClickable(false));
560    }
561
562    public void setQSPanel(final QSPanel qsPanel) {
563        mQsPanel = qsPanel;
564        setupHost(qsPanel.getHost());
565    }
566
567    public void setupHost(final QSTileHost host) {
568        mHost = host;
569        //host.setHeaderView(mExpandIndicator);
570        mHeaderQsPanel.setQSPanelAndHeader(mQsPanel, this);
571        mHeaderQsPanel.setHost(host, null /* No customization in header */);
572
573        // Use SystemUI context to get battery meter colors, and let it use the default tint (white)
574        mBatteryMeterView.setColorsFromContext(mHost.getContext());
575        mBatteryMeterView.onDarkChanged(new Rect(), 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
576    }
577
578    public void setCallback(Callback qsPanelCallback) {
579        mHeaderQsPanel.setCallback(qsPanelCallback);
580    }
581
582    private String formatNextAlarm(AlarmManager.AlarmClockInfo info) {
583        if (info == null) {
584            return "";
585        }
586        String skeleton = android.text.format.DateFormat
587                .is24HourFormat(mContext, ActivityManager.getCurrentUser()) ? "EHm" : "Ehma";
588        String pattern = android.text.format.DateFormat
589                .getBestDateTimePattern(Locale.getDefault(), skeleton);
590        return android.text.format.DateFormat.format(pattern, info.getTriggerTime()).toString();
591    }
592
593    public static float getColorIntensity(@ColorInt int color) {
594        return color == Color.WHITE ? 0 : 1;
595    }
596}
597