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