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