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