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