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