1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.systemui.statusbar.phone;
18
19import android.animation.ArgbEvaluator;
20import android.animation.ValueAnimator;
21import android.content.Context;
22import android.content.res.ColorStateList;
23import android.content.res.Resources;
24import android.graphics.Color;
25import android.graphics.Rect;
26import android.graphics.drawable.Icon;
27import android.os.Bundle;
28import android.os.Handler;
29import android.os.SystemClock;
30import android.os.UserHandle;
31import android.text.TextUtils;
32import android.util.ArraySet;
33import android.util.TypedValue;
34import android.view.Gravity;
35import android.view.View;
36import android.view.ViewGroup;
37import android.widget.ImageView;
38import android.widget.LinearLayout;
39import android.widget.TextView;
40import com.android.internal.statusbar.StatusBarIcon;
41import com.android.systemui.BatteryMeterView;
42import com.android.systemui.FontSizeUtils;
43import com.android.systemui.Interpolators;
44import com.android.systemui.R;
45import com.android.systemui.SystemUIFactory;
46import com.android.systemui.statusbar.NotificationData;
47import com.android.systemui.statusbar.SignalClusterView;
48import com.android.systemui.statusbar.StatusBarIconView;
49import com.android.systemui.tuner.TunerService;
50import com.android.systemui.tuner.TunerService.Tunable;
51
52import java.io.PrintWriter;
53import java.util.ArrayList;
54
55/**
56 * Controls everything regarding the icons in the status bar and on Keyguard, including, but not
57 * limited to: notification icons, signal cluster, additional status icons, and clock in the status
58 * bar.
59 */
60public class StatusBarIconController extends StatusBarIconList implements Tunable {
61
62    public static final long DEFAULT_TINT_ANIMATION_DURATION = 120;
63    public static final String ICON_BLACKLIST = "icon_blacklist";
64    public static final int DEFAULT_ICON_TINT = Color.WHITE;
65
66    private Context mContext;
67    private PhoneStatusBar mPhoneStatusBar;
68    private DemoStatusIcons mDemoStatusIcons;
69
70    private LinearLayout mSystemIconArea;
71    private LinearLayout mStatusIcons;
72    private SignalClusterView mSignalCluster;
73    private LinearLayout mStatusIconsKeyguard;
74
75    private NotificationIconAreaController mNotificationIconAreaController;
76    private View mNotificationIconAreaInner;
77
78    private BatteryMeterView mBatteryMeterView;
79    private BatteryMeterView mBatteryMeterViewKeyguard;
80    private TextView mClock;
81
82    private int mIconSize;
83    private int mIconHPadding;
84
85    private int mIconTint = DEFAULT_ICON_TINT;
86    private float mDarkIntensity;
87    private final Rect mTintArea = new Rect();
88    private static final Rect sTmpRect = new Rect();
89    private static final int[] sTmpInt2 = new int[2];
90
91    private boolean mTransitionPending;
92    private boolean mTintChangePending;
93    private float mPendingDarkIntensity;
94    private ValueAnimator mTintAnimator;
95
96    private int mDarkModeIconColorSingleTone;
97    private int mLightModeIconColorSingleTone;
98
99    private final Handler mHandler;
100    private boolean mTransitionDeferring;
101    private long mTransitionDeferringStartTime;
102    private long mTransitionDeferringDuration;
103
104    private final ArraySet<String> mIconBlacklist = new ArraySet<>();
105
106    private final Runnable mTransitionDeferringDoneRunnable = new Runnable() {
107        @Override
108        public void run() {
109            mTransitionDeferring = false;
110        }
111    };
112
113    public StatusBarIconController(Context context, View statusBar, View keyguardStatusBar,
114            PhoneStatusBar phoneStatusBar) {
115        super(context.getResources().getStringArray(
116                com.android.internal.R.array.config_statusBarIcons));
117        mContext = context;
118        mPhoneStatusBar = phoneStatusBar;
119        mSystemIconArea = (LinearLayout) statusBar.findViewById(R.id.system_icon_area);
120        mStatusIcons = (LinearLayout) statusBar.findViewById(R.id.statusIcons);
121        mSignalCluster = (SignalClusterView) statusBar.findViewById(R.id.signal_cluster);
122
123        mNotificationIconAreaController = SystemUIFactory.getInstance()
124                .createNotificationIconAreaController(context, phoneStatusBar);
125        mNotificationIconAreaInner =
126                mNotificationIconAreaController.getNotificationInnerAreaView();
127
128        ViewGroup notificationIconArea =
129                (ViewGroup) statusBar.findViewById(R.id.notification_icon_area);
130        notificationIconArea.addView(mNotificationIconAreaInner);
131
132        mStatusIconsKeyguard = (LinearLayout) keyguardStatusBar.findViewById(R.id.statusIcons);
133
134        mBatteryMeterView = (BatteryMeterView) statusBar.findViewById(R.id.battery);
135        mBatteryMeterViewKeyguard = (BatteryMeterView) keyguardStatusBar.findViewById(R.id.battery);
136        scaleBatteryMeterViews(context);
137
138        mClock = (TextView) statusBar.findViewById(R.id.clock);
139        mDarkModeIconColorSingleTone = context.getColor(R.color.dark_mode_icon_color_single_tone);
140        mLightModeIconColorSingleTone = context.getColor(R.color.light_mode_icon_color_single_tone);
141        mHandler = new Handler();
142        loadDimens();
143
144        TunerService.get(mContext).addTunable(this, ICON_BLACKLIST);
145    }
146
147    public void setSignalCluster(SignalClusterView signalCluster) {
148        mSignalCluster = signalCluster;
149    }
150
151    /**
152     * Looks up the scale factor for status bar icons and scales the battery view by that amount.
153     */
154    private void scaleBatteryMeterViews(Context context) {
155        Resources res = context.getResources();
156        TypedValue typedValue = new TypedValue();
157
158        res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
159        float iconScaleFactor = typedValue.getFloat();
160
161        int batteryHeight = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height);
162        int batteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width);
163        int marginBottom = res.getDimensionPixelSize(R.dimen.battery_margin_bottom);
164
165        LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
166                (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor));
167        scaledLayoutParams.setMarginsRelative(0, 0, 0, marginBottom);
168
169        mBatteryMeterView.setLayoutParams(scaledLayoutParams);
170        mBatteryMeterViewKeyguard.setLayoutParams(scaledLayoutParams);
171    }
172
173    @Override
174    public void onTuningChanged(String key, String newValue) {
175        if (!ICON_BLACKLIST.equals(key)) {
176            return;
177        }
178        mIconBlacklist.clear();
179        mIconBlacklist.addAll(getIconBlacklist(newValue));
180        ArrayList<StatusBarIconView> views = new ArrayList<StatusBarIconView>();
181        // Get all the current views.
182        for (int i = 0; i < mStatusIcons.getChildCount(); i++) {
183            views.add((StatusBarIconView) mStatusIcons.getChildAt(i));
184        }
185        // Remove all the icons.
186        for (int i = views.size() - 1; i >= 0; i--) {
187            removeIcon(views.get(i).getSlot());
188        }
189        // Add them all back
190        for (int i = 0; i < views.size(); i++) {
191            setIcon(views.get(i).getSlot(), views.get(i).getStatusBarIcon());
192        }
193    }
194    private void loadDimens() {
195        mIconSize = mContext.getResources().getDimensionPixelSize(
196                com.android.internal.R.dimen.status_bar_icon_size);
197        mIconHPadding = mContext.getResources().getDimensionPixelSize(
198                R.dimen.status_bar_icon_padding);
199    }
200
201    private void addSystemIcon(int index, StatusBarIcon icon) {
202        String slot = getSlot(index);
203        int viewIndex = getViewIndex(index);
204        boolean blocked = mIconBlacklist.contains(slot);
205        StatusBarIconView view = new StatusBarIconView(mContext, slot, null, blocked);
206        view.set(icon);
207
208        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
209                ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
210        lp.setMargins(mIconHPadding, 0, mIconHPadding, 0);
211        mStatusIcons.addView(view, viewIndex, lp);
212
213        view = new StatusBarIconView(mContext, slot, null, blocked);
214        view.set(icon);
215        mStatusIconsKeyguard.addView(view, viewIndex, new LinearLayout.LayoutParams(
216                ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize));
217        applyIconTint();
218    }
219
220    public void setIcon(String slot, int resourceId, CharSequence contentDescription) {
221        int index = getSlotIndex(slot);
222        StatusBarIcon icon = getIcon(index);
223        if (icon == null) {
224            icon = new StatusBarIcon(UserHandle.SYSTEM, mContext.getPackageName(),
225                    Icon.createWithResource(mContext, resourceId), 0, 0, contentDescription);
226            setIcon(slot, icon);
227        } else {
228            icon.icon = Icon.createWithResource(mContext, resourceId);
229            icon.contentDescription = contentDescription;
230            handleSet(index, icon);
231        }
232    }
233
234    public void setExternalIcon(String slot) {
235        int viewIndex = getViewIndex(getSlotIndex(slot));
236        int height = mContext.getResources().getDimensionPixelSize(
237                R.dimen.status_bar_icon_drawing_size);
238        ImageView imageView = (ImageView) mStatusIcons.getChildAt(viewIndex);
239        imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
240        imageView.setAdjustViewBounds(true);
241        setHeightAndCenter(imageView, height);
242        imageView = (ImageView) mStatusIconsKeyguard.getChildAt(viewIndex);
243        imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
244        imageView.setAdjustViewBounds(true);
245        setHeightAndCenter(imageView, height);
246    }
247
248    private void setHeightAndCenter(ImageView imageView, int height) {
249        ViewGroup.LayoutParams params = imageView.getLayoutParams();
250        params.height = height;
251        if (params instanceof LinearLayout.LayoutParams) {
252            ((LinearLayout.LayoutParams) params).gravity = Gravity.CENTER_VERTICAL;
253        }
254        imageView.setLayoutParams(params);
255    }
256
257    public void setIcon(String slot, StatusBarIcon icon) {
258        setIcon(getSlotIndex(slot), icon);
259    }
260
261    public void removeIcon(String slot) {
262        int index = getSlotIndex(slot);
263        removeIcon(index);
264    }
265
266    public void setIconVisibility(String slot, boolean visibility) {
267        int index = getSlotIndex(slot);
268        StatusBarIcon icon = getIcon(index);
269        if (icon == null || icon.visible == visibility) {
270            return;
271        }
272        icon.visible = visibility;
273        handleSet(index, icon);
274    }
275
276    @Override
277    public void removeIcon(int index) {
278        if (getIcon(index) == null) {
279            return;
280        }
281        super.removeIcon(index);
282        int viewIndex = getViewIndex(index);
283        mStatusIcons.removeViewAt(viewIndex);
284        mStatusIconsKeyguard.removeViewAt(viewIndex);
285    }
286
287    @Override
288    public void setIcon(int index, StatusBarIcon icon) {
289        if (icon == null) {
290            removeIcon(index);
291            return;
292        }
293        boolean isNew = getIcon(index) == null;
294        super.setIcon(index, icon);
295        if (isNew) {
296            addSystemIcon(index, icon);
297        } else {
298            handleSet(index, icon);
299        }
300    }
301
302    private void handleSet(int index, StatusBarIcon icon) {
303        int viewIndex = getViewIndex(index);
304        StatusBarIconView view = (StatusBarIconView) mStatusIcons.getChildAt(viewIndex);
305        view.set(icon);
306        view = (StatusBarIconView) mStatusIconsKeyguard.getChildAt(viewIndex);
307        view.set(icon);
308        applyIconTint();
309    }
310
311    public void updateNotificationIcons(NotificationData notificationData) {
312        mNotificationIconAreaController.updateNotificationIcons(notificationData);
313    }
314
315    public void hideSystemIconArea(boolean animate) {
316        animateHide(mSystemIconArea, animate);
317    }
318
319    public void showSystemIconArea(boolean animate) {
320        animateShow(mSystemIconArea, animate);
321    }
322
323    public void hideNotificationIconArea(boolean animate) {
324        animateHide(mNotificationIconAreaInner, animate);
325    }
326
327    public void showNotificationIconArea(boolean animate) {
328        animateShow(mNotificationIconAreaInner, animate);
329    }
330
331    public void setClockVisibility(boolean visible) {
332        mClock.setVisibility(visible ? View.VISIBLE : View.GONE);
333    }
334
335    public void dump(PrintWriter pw) {
336        int N = mStatusIcons.getChildCount();
337        pw.println("  icon views: " + N);
338        for (int i=0; i<N; i++) {
339            StatusBarIconView ic = (StatusBarIconView) mStatusIcons.getChildAt(i);
340            pw.println("    [" + i + "] icon=" + ic);
341        }
342        super.dump(pw);
343    }
344
345    public void dispatchDemoCommand(String command, Bundle args) {
346        if (mDemoStatusIcons == null) {
347            mDemoStatusIcons = new DemoStatusIcons(mStatusIcons, mIconSize);
348        }
349        mDemoStatusIcons.dispatchDemoCommand(command, args);
350    }
351
352    /**
353     * Hides a view.
354     */
355    private void animateHide(final View v, boolean animate) {
356        v.animate().cancel();
357        if (!animate) {
358            v.setAlpha(0f);
359            v.setVisibility(View.INVISIBLE);
360            return;
361        }
362        v.animate()
363                .alpha(0f)
364                .setDuration(160)
365                .setStartDelay(0)
366                .setInterpolator(Interpolators.ALPHA_OUT)
367                .withEndAction(new Runnable() {
368                    @Override
369                    public void run() {
370                        v.setVisibility(View.INVISIBLE);
371                    }
372                });
373    }
374
375    /**
376     * Shows a view, and synchronizes the animation with Keyguard exit animations, if applicable.
377     */
378    private void animateShow(View v, boolean animate) {
379        v.animate().cancel();
380        v.setVisibility(View.VISIBLE);
381        if (!animate) {
382            v.setAlpha(1f);
383            return;
384        }
385        v.animate()
386                .alpha(1f)
387                .setDuration(320)
388                .setInterpolator(Interpolators.ALPHA_IN)
389                .setStartDelay(50)
390
391                // We need to clean up any pending end action from animateHide if we call
392                // both hide and show in the same frame before the animation actually gets started.
393                // cancel() doesn't really remove the end action.
394                .withEndAction(null);
395
396        // Synchronize the motion with the Keyguard fading if necessary.
397        if (mPhoneStatusBar.isKeyguardFadingAway()) {
398            v.animate()
399                    .setDuration(mPhoneStatusBar.getKeyguardFadingAwayDuration())
400                    .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
401                    .setStartDelay(mPhoneStatusBar.getKeyguardFadingAwayDelay())
402                    .start();
403        }
404    }
405
406    /**
407     * Sets the dark area so {@link #setIconsDark} only affects the icons in the specified area.
408     *
409     * @param darkArea the area in which icons should change it's tint, in logical screen
410     *                 coordinates
411     */
412    public void setIconsDarkArea(Rect darkArea) {
413        if (darkArea == null && mTintArea.isEmpty()) {
414            return;
415        }
416        if (darkArea == null) {
417            mTintArea.setEmpty();
418        } else {
419            mTintArea.set(darkArea);
420        }
421        applyIconTint();
422        mNotificationIconAreaController.setTintArea(darkArea);
423    }
424
425    public void setIconsDark(boolean dark, boolean animate) {
426        if (!animate) {
427            setIconTintInternal(dark ? 1.0f : 0.0f);
428        } else if (mTransitionPending) {
429            deferIconTintChange(dark ? 1.0f : 0.0f);
430        } else if (mTransitionDeferring) {
431            animateIconTint(dark ? 1.0f : 0.0f,
432                    Math.max(0, mTransitionDeferringStartTime - SystemClock.uptimeMillis()),
433                    mTransitionDeferringDuration);
434        } else {
435            animateIconTint(dark ? 1.0f : 0.0f, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION);
436        }
437    }
438
439    private void animateIconTint(float targetDarkIntensity, long delay,
440            long duration) {
441        if (mTintAnimator != null) {
442            mTintAnimator.cancel();
443        }
444        if (mDarkIntensity == targetDarkIntensity) {
445            return;
446        }
447        mTintAnimator = ValueAnimator.ofFloat(mDarkIntensity, targetDarkIntensity);
448        mTintAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
449            @Override
450            public void onAnimationUpdate(ValueAnimator animation) {
451                setIconTintInternal((Float) animation.getAnimatedValue());
452            }
453        });
454        mTintAnimator.setDuration(duration);
455        mTintAnimator.setStartDelay(delay);
456        mTintAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
457        mTintAnimator.start();
458    }
459
460    private void setIconTintInternal(float darkIntensity) {
461        mDarkIntensity = darkIntensity;
462        mIconTint = (int) ArgbEvaluator.getInstance().evaluate(darkIntensity,
463                mLightModeIconColorSingleTone, mDarkModeIconColorSingleTone);
464        mNotificationIconAreaController.setIconTint(mIconTint);
465        applyIconTint();
466    }
467
468    private void deferIconTintChange(float darkIntensity) {
469        if (mTintChangePending && darkIntensity == mPendingDarkIntensity) {
470            return;
471        }
472        mTintChangePending = true;
473        mPendingDarkIntensity = darkIntensity;
474    }
475
476    /**
477     * @return the tint to apply to {@param view} depending on the desired tint {@param color} and
478     *         the screen {@param tintArea} in which to apply that tint
479     */
480    public static int getTint(Rect tintArea, View view, int color) {
481        if (isInArea(tintArea, view)) {
482            return color;
483        } else {
484            return DEFAULT_ICON_TINT;
485        }
486    }
487
488    /**
489     * @return the dark intensity to apply to {@param view} depending on the desired dark
490     *         {@param intensity} and the screen {@param tintArea} in which to apply that intensity
491     */
492    public static float getDarkIntensity(Rect tintArea, View view, float intensity) {
493        if (isInArea(tintArea, view)) {
494            return intensity;
495        } else {
496            return 0f;
497        }
498    }
499
500    /**
501     * @return true if more than half of the {@param view} area are in {@param area}, false
502     *         otherwise
503     */
504    private static boolean isInArea(Rect area, View view) {
505        if (area.isEmpty()) {
506            return true;
507        }
508        sTmpRect.set(area);
509        view.getLocationOnScreen(sTmpInt2);
510        int left = sTmpInt2[0];
511
512        int intersectStart = Math.max(left, area.left);
513        int intersectEnd = Math.min(left + view.getWidth(), area.right);
514        int intersectAmount = Math.max(0, intersectEnd - intersectStart);
515
516        boolean coversFullStatusBar = area.top <= 0;
517        boolean majorityOfWidth = 2 * intersectAmount > view.getWidth();
518        return majorityOfWidth && coversFullStatusBar;
519    }
520
521    private void applyIconTint() {
522        for (int i = 0; i < mStatusIcons.getChildCount(); i++) {
523            StatusBarIconView v = (StatusBarIconView) mStatusIcons.getChildAt(i);
524            v.setImageTintList(ColorStateList.valueOf(getTint(mTintArea, v, mIconTint)));
525        }
526        mSignalCluster.setIconTint(mIconTint, mDarkIntensity, mTintArea);
527        mBatteryMeterView.setDarkIntensity(
528                isInArea(mTintArea, mBatteryMeterView) ? mDarkIntensity : 0);
529        mClock.setTextColor(getTint(mTintArea, mClock, mIconTint));
530    }
531
532    public void appTransitionPending() {
533        mTransitionPending = true;
534    }
535
536    public void appTransitionCancelled() {
537        if (mTransitionPending && mTintChangePending) {
538            mTintChangePending = false;
539            animateIconTint(mPendingDarkIntensity, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION);
540        }
541        mTransitionPending = false;
542    }
543
544    public void appTransitionStarting(long startTime, long duration) {
545        if (mTransitionPending && mTintChangePending) {
546            mTintChangePending = false;
547            animateIconTint(mPendingDarkIntensity,
548                    Math.max(0, startTime - SystemClock.uptimeMillis()),
549                    duration);
550
551        } else if (mTransitionPending) {
552
553            // If we don't have a pending tint change yet, the change might come in the future until
554            // startTime is reached.
555            mTransitionDeferring = true;
556            mTransitionDeferringStartTime = startTime;
557            mTransitionDeferringDuration = duration;
558            mHandler.removeCallbacks(mTransitionDeferringDoneRunnable);
559            mHandler.postAtTime(mTransitionDeferringDoneRunnable, startTime);
560        }
561        mTransitionPending = false;
562    }
563
564    public static ArraySet<String> getIconBlacklist(String blackListStr) {
565        ArraySet<String> ret = new ArraySet<String>();
566        if (blackListStr == null) {
567            blackListStr = "rotate,headset";
568        }
569        String[] blacklist = blackListStr.split(",");
570        for (String slot : blacklist) {
571            if (!TextUtils.isEmpty(slot)) {
572                ret.add(slot);
573            }
574        }
575        return ret;
576    }
577
578    public void onDensityOrFontScaleChanged() {
579        loadDimens();
580        mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
581        updateClock();
582        for (int i = 0; i < mStatusIcons.getChildCount(); i++) {
583            View child = mStatusIcons.getChildAt(i);
584            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
585                    ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
586            lp.setMargins(mIconHPadding, 0, mIconHPadding, 0);
587            child.setLayoutParams(lp);
588        }
589        for (int i = 0; i < mStatusIconsKeyguard.getChildCount(); i++) {
590            View child = mStatusIconsKeyguard.getChildAt(i);
591            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
592                    ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
593            child.setLayoutParams(lp);
594        }
595        scaleBatteryMeterViews(mContext);
596    }
597
598    private void updateClock() {
599        FontSizeUtils.updateFontSize(mClock, R.dimen.status_bar_clock_size);
600        mClock.setPaddingRelative(
601                mContext.getResources().getDimensionPixelSize(
602                        R.dimen.status_bar_clock_starting_padding),
603                0,
604                mContext.getResources().getDimensionPixelSize(
605                        R.dimen.status_bar_clock_end_padding),
606                0);
607    }
608}
609