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