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.graphics.Color;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.SystemClock;
27import android.text.TextUtils;
28import android.util.ArraySet;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.animation.AnimationUtils;
32import android.view.animation.Interpolator;
33import android.widget.ImageView;
34import android.widget.LinearLayout;
35import android.widget.TextView;
36
37import com.android.internal.statusbar.StatusBarIcon;
38import com.android.internal.util.NotificationColorUtil;
39import com.android.systemui.BatteryMeterView;
40import com.android.systemui.FontSizeUtils;
41import com.android.systemui.R;
42import com.android.systemui.statusbar.NotificationData;
43import com.android.systemui.statusbar.SignalClusterView;
44import com.android.systemui.statusbar.StatusBarIconView;
45import com.android.systemui.tuner.TunerService;
46import com.android.systemui.tuner.TunerService.Tunable;
47
48import java.io.PrintWriter;
49import java.util.ArrayList;
50
51/**
52 * Controls everything regarding the icons in the status bar and on Keyguard, including, but not
53 * limited to: notification icons, signal cluster, additional status icons, and clock in the status
54 * bar.
55 */
56public class StatusBarIconController implements Tunable {
57
58    public static final long DEFAULT_TINT_ANIMATION_DURATION = 120;
59
60    public static final String ICON_BLACKLIST = "icon_blacklist";
61
62    private Context mContext;
63    private PhoneStatusBar mPhoneStatusBar;
64    private Interpolator mLinearOutSlowIn;
65    private Interpolator mFastOutSlowIn;
66    private DemoStatusIcons mDemoStatusIcons;
67    private NotificationColorUtil mNotificationColorUtil;
68
69    private LinearLayout mSystemIconArea;
70    private LinearLayout mStatusIcons;
71    private SignalClusterView mSignalCluster;
72    private LinearLayout mStatusIconsKeyguard;
73    private IconMerger mNotificationIcons;
74    private View mNotificationIconArea;
75    private ImageView mMoreIcon;
76    private BatteryMeterView mBatteryMeterView;
77    private TextView mClock;
78
79    private int mIconSize;
80    private int mIconHPadding;
81
82    private int mIconTint = Color.WHITE;
83    private float mDarkIntensity;
84
85    private boolean mTransitionPending;
86    private boolean mTintChangePending;
87    private float mPendingDarkIntensity;
88    private ValueAnimator mTintAnimator;
89
90    private int mDarkModeIconColorSingleTone;
91    private int mLightModeIconColorSingleTone;
92
93    private final Handler mHandler;
94    private boolean mTransitionDeferring;
95    private long mTransitionDeferringStartTime;
96    private long mTransitionDeferringDuration;
97
98    private final ArraySet<String> mIconBlacklist = new ArraySet<>();
99
100    private final Runnable mTransitionDeferringDoneRunnable = new Runnable() {
101        @Override
102        public void run() {
103            mTransitionDeferring = false;
104        }
105    };
106
107    public StatusBarIconController(Context context, View statusBar, View keyguardStatusBar,
108            PhoneStatusBar phoneStatusBar) {
109        mContext = context;
110        mPhoneStatusBar = phoneStatusBar;
111        mNotificationColorUtil = NotificationColorUtil.getInstance(context);
112        mSystemIconArea = (LinearLayout) statusBar.findViewById(R.id.system_icon_area);
113        mStatusIcons = (LinearLayout) statusBar.findViewById(R.id.statusIcons);
114        mSignalCluster = (SignalClusterView) statusBar.findViewById(R.id.signal_cluster);
115        mNotificationIconArea = statusBar.findViewById(R.id.notification_icon_area_inner);
116        mNotificationIcons = (IconMerger) statusBar.findViewById(R.id.notificationIcons);
117        mMoreIcon = (ImageView) statusBar.findViewById(R.id.moreIcon);
118        mNotificationIcons.setOverflowIndicator(mMoreIcon);
119        mStatusIconsKeyguard = (LinearLayout) keyguardStatusBar.findViewById(R.id.statusIcons);
120        mBatteryMeterView = (BatteryMeterView) statusBar.findViewById(R.id.battery);
121        mClock = (TextView) statusBar.findViewById(R.id.clock);
122        mLinearOutSlowIn = AnimationUtils.loadInterpolator(mContext,
123                android.R.interpolator.linear_out_slow_in);
124        mFastOutSlowIn = AnimationUtils.loadInterpolator(mContext,
125                android.R.interpolator.fast_out_slow_in);
126        mDarkModeIconColorSingleTone = context.getColor(R.color.dark_mode_icon_color_single_tone);
127        mLightModeIconColorSingleTone = context.getColor(R.color.light_mode_icon_color_single_tone);
128        mHandler = new Handler();
129        updateResources();
130
131        TunerService.get(mContext).addTunable(this, ICON_BLACKLIST);
132    }
133
134    @Override
135    public void onTuningChanged(String key, String newValue) {
136        if (!ICON_BLACKLIST.equals(key)) {
137            return;
138        }
139        mIconBlacklist.clear();
140        mIconBlacklist.addAll(getIconBlacklist(newValue));
141        ArrayList<StatusBarIconView> views = new ArrayList<StatusBarIconView>();
142        // Get all the current views.
143        for (int i = 0; i < mStatusIcons.getChildCount(); i++) {
144            views.add((StatusBarIconView) mStatusIcons.getChildAt(i));
145        }
146        // Remove all the icons.
147        for (int i = views.size() - 1; i >= 0; i--) {
148            removeSystemIcon(views.get(i).getSlot(), i, i);
149        }
150        // Add them all back
151        for (int i = 0; i < views.size(); i++) {
152            addSystemIcon(views.get(i).getSlot(), i, i, views.get(i).getStatusBarIcon());
153        }
154    };
155
156    public void updateResources() {
157        mIconSize = mContext.getResources().getDimensionPixelSize(
158                com.android.internal.R.dimen.status_bar_icon_size);
159        mIconHPadding = mContext.getResources().getDimensionPixelSize(
160                R.dimen.status_bar_icon_padding);
161        FontSizeUtils.updateFontSize(mClock, R.dimen.status_bar_clock_size);
162    }
163
164    public void addSystemIcon(String slot, int index, int viewIndex, StatusBarIcon icon) {
165        boolean blocked = mIconBlacklist.contains(slot);
166        StatusBarIconView view = new StatusBarIconView(mContext, slot, null, blocked);
167        view.set(icon);
168        mStatusIcons.addView(view, viewIndex, new LinearLayout.LayoutParams(
169                ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize));
170        view = new StatusBarIconView(mContext, slot, null, blocked);
171        view.set(icon);
172        mStatusIconsKeyguard.addView(view, viewIndex, new LinearLayout.LayoutParams(
173                ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize));
174        applyIconTint();
175    }
176
177    public void updateSystemIcon(String slot, int index, int viewIndex,
178            StatusBarIcon old, StatusBarIcon icon) {
179        StatusBarIconView view = (StatusBarIconView) mStatusIcons.getChildAt(viewIndex);
180        view.set(icon);
181        view = (StatusBarIconView) mStatusIconsKeyguard.getChildAt(viewIndex);
182        view.set(icon);
183        applyIconTint();
184    }
185
186    public void removeSystemIcon(String slot, int index, int viewIndex) {
187        mStatusIcons.removeViewAt(viewIndex);
188        mStatusIconsKeyguard.removeViewAt(viewIndex);
189    }
190
191    public void updateNotificationIcons(NotificationData notificationData) {
192        final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
193                mIconSize + 2*mIconHPadding, mPhoneStatusBar.getStatusBarHeight());
194
195        ArrayList<NotificationData.Entry> activeNotifications =
196                notificationData.getActiveNotifications();
197        final int N = activeNotifications.size();
198        ArrayList<StatusBarIconView> toShow = new ArrayList<>(N);
199
200        // Filter out ambient notifications and notification children.
201        for (int i = 0; i < N; i++) {
202            NotificationData.Entry ent = activeNotifications.get(i);
203            if (notificationData.isAmbient(ent.key)
204                    && !NotificationData.showNotificationEvenIfUnprovisioned(ent.notification)) {
205                continue;
206            }
207            if (!PhoneStatusBar.isTopLevelChild(ent)) {
208                continue;
209            }
210            toShow.add(ent.icon);
211        }
212
213        ArrayList<View> toRemove = new ArrayList<>();
214        for (int i=0; i<mNotificationIcons.getChildCount(); i++) {
215            View child = mNotificationIcons.getChildAt(i);
216            if (!toShow.contains(child)) {
217                toRemove.add(child);
218            }
219        }
220
221        final int toRemoveCount = toRemove.size();
222        for (int i = 0; i < toRemoveCount; i++) {
223            mNotificationIcons.removeView(toRemove.get(i));
224        }
225
226        for (int i=0; i<toShow.size(); i++) {
227            View v = toShow.get(i);
228            if (v.getParent() == null) {
229                mNotificationIcons.addView(v, i, params);
230            }
231        }
232
233        // Resort notification icons
234        final int childCount = mNotificationIcons.getChildCount();
235        for (int i = 0; i < childCount; i++) {
236            View actual = mNotificationIcons.getChildAt(i);
237            StatusBarIconView expected = toShow.get(i);
238            if (actual == expected) {
239                continue;
240            }
241            mNotificationIcons.removeView(expected);
242            mNotificationIcons.addView(expected, i);
243        }
244
245        applyNotificationIconsTint();
246    }
247
248    public void hideSystemIconArea(boolean animate) {
249        animateHide(mSystemIconArea, animate);
250    }
251
252    public void showSystemIconArea(boolean animate) {
253        animateShow(mSystemIconArea, animate);
254    }
255
256    public void hideNotificationIconArea(boolean animate) {
257        animateHide(mNotificationIconArea, animate);
258    }
259
260    public void showNotificationIconArea(boolean animate) {
261        animateShow(mNotificationIconArea, animate);
262    }
263
264    public void setClockVisibility(boolean visible) {
265        mClock.setVisibility(visible ? View.VISIBLE : View.GONE);
266    }
267
268    public void dump(PrintWriter pw) {
269        int N = mStatusIcons.getChildCount();
270        pw.println("  system icons: " + N);
271        for (int i=0; i<N; i++) {
272            StatusBarIconView ic = (StatusBarIconView) mStatusIcons.getChildAt(i);
273            pw.println("    [" + i + "] icon=" + ic);
274        }
275    }
276
277    public void dispatchDemoCommand(String command, Bundle args) {
278        if (mDemoStatusIcons == null) {
279            mDemoStatusIcons = new DemoStatusIcons(mStatusIcons, mIconSize);
280        }
281        mDemoStatusIcons.dispatchDemoCommand(command, args);
282    }
283
284    /**
285     * Hides a view.
286     */
287    private void animateHide(final View v, boolean animate) {
288        v.animate().cancel();
289        if (!animate) {
290            v.setAlpha(0f);
291            v.setVisibility(View.INVISIBLE);
292            return;
293        }
294        v.animate()
295                .alpha(0f)
296                .setDuration(160)
297                .setStartDelay(0)
298                .setInterpolator(PhoneStatusBar.ALPHA_OUT)
299                .withEndAction(new Runnable() {
300                    @Override
301                    public void run() {
302                        v.setVisibility(View.INVISIBLE);
303                    }
304                });
305    }
306
307    /**
308     * Shows a view, and synchronizes the animation with Keyguard exit animations, if applicable.
309     */
310    private void animateShow(View v, boolean animate) {
311        v.animate().cancel();
312        v.setVisibility(View.VISIBLE);
313        if (!animate) {
314            v.setAlpha(1f);
315            return;
316        }
317        v.animate()
318                .alpha(1f)
319                .setDuration(320)
320                .setInterpolator(PhoneStatusBar.ALPHA_IN)
321                .setStartDelay(50)
322
323                // We need to clean up any pending end action from animateHide if we call
324                // both hide and show in the same frame before the animation actually gets started.
325                // cancel() doesn't really remove the end action.
326                .withEndAction(null);
327
328        // Synchronize the motion with the Keyguard fading if necessary.
329        if (mPhoneStatusBar.isKeyguardFadingAway()) {
330            v.animate()
331                    .setDuration(mPhoneStatusBar.getKeyguardFadingAwayDuration())
332                    .setInterpolator(mLinearOutSlowIn)
333                    .setStartDelay(mPhoneStatusBar.getKeyguardFadingAwayDelay())
334                    .start();
335        }
336    }
337
338    public void setIconsDark(boolean dark, boolean animate) {
339        if (!animate) {
340            setIconTintInternal(dark ? 1.0f : 0.0f);
341        } else if (mTransitionPending) {
342            deferIconTintChange(dark ? 1.0f : 0.0f);
343        } else if (mTransitionDeferring) {
344            animateIconTint(dark ? 1.0f : 0.0f,
345                    Math.max(0, mTransitionDeferringStartTime - SystemClock.uptimeMillis()),
346                    mTransitionDeferringDuration);
347        } else {
348            animateIconTint(dark ? 1.0f : 0.0f, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION);
349        }
350    }
351
352    private void animateIconTint(float targetDarkIntensity, long delay,
353            long duration) {
354        if (mTintAnimator != null) {
355            mTintAnimator.cancel();
356        }
357        if (mDarkIntensity == targetDarkIntensity) {
358            return;
359        }
360        mTintAnimator = ValueAnimator.ofFloat(mDarkIntensity, targetDarkIntensity);
361        mTintAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
362            @Override
363            public void onAnimationUpdate(ValueAnimator animation) {
364                setIconTintInternal((Float) animation.getAnimatedValue());
365            }
366        });
367        mTintAnimator.setDuration(duration);
368        mTintAnimator.setStartDelay(delay);
369        mTintAnimator.setInterpolator(mFastOutSlowIn);
370        mTintAnimator.start();
371    }
372
373    private void setIconTintInternal(float darkIntensity) {
374        mDarkIntensity = darkIntensity;
375        mIconTint = (int) ArgbEvaluator.getInstance().evaluate(darkIntensity,
376                mLightModeIconColorSingleTone, mDarkModeIconColorSingleTone);
377        applyIconTint();
378    }
379
380    private void deferIconTintChange(float darkIntensity) {
381        if (mTintChangePending && darkIntensity == mPendingDarkIntensity) {
382            return;
383        }
384        mTintChangePending = true;
385        mPendingDarkIntensity = darkIntensity;
386    }
387
388    private void applyIconTint() {
389        for (int i = 0; i < mStatusIcons.getChildCount(); i++) {
390            StatusBarIconView v = (StatusBarIconView) mStatusIcons.getChildAt(i);
391            v.setImageTintList(ColorStateList.valueOf(mIconTint));
392        }
393        mSignalCluster.setIconTint(mIconTint, mDarkIntensity);
394        mMoreIcon.setImageTintList(ColorStateList.valueOf(mIconTint));
395        mBatteryMeterView.setDarkIntensity(mDarkIntensity);
396        mClock.setTextColor(mIconTint);
397        applyNotificationIconsTint();
398    }
399
400    private void applyNotificationIconsTint() {
401        for (int i = 0; i < mNotificationIcons.getChildCount(); i++) {
402            StatusBarIconView v = (StatusBarIconView) mNotificationIcons.getChildAt(i);
403            boolean isPreL = Boolean.TRUE.equals(v.getTag(R.id.icon_is_pre_L));
404            boolean colorize = !isPreL || isGrayscale(v);
405            if (colorize) {
406                v.setImageTintList(ColorStateList.valueOf(mIconTint));
407            }
408        }
409    }
410
411    private boolean isGrayscale(StatusBarIconView v) {
412        Object isGrayscale = v.getTag(R.id.icon_is_grayscale);
413        if (isGrayscale != null) {
414            return Boolean.TRUE.equals(isGrayscale);
415        }
416        boolean grayscale = mNotificationColorUtil.isGrayscaleIcon(v.getDrawable());
417        v.setTag(R.id.icon_is_grayscale, grayscale);
418        return grayscale;
419    }
420
421    public void appTransitionPending() {
422        mTransitionPending = true;
423    }
424
425    public void appTransitionCancelled() {
426        if (mTransitionPending && mTintChangePending) {
427            mTintChangePending = false;
428            animateIconTint(mPendingDarkIntensity, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION);
429        }
430        mTransitionPending = false;
431    }
432
433    public void appTransitionStarting(long startTime, long duration) {
434        if (mTransitionPending && mTintChangePending) {
435            mTintChangePending = false;
436            animateIconTint(mPendingDarkIntensity,
437                    Math.max(0, startTime - SystemClock.uptimeMillis()),
438                    duration);
439
440        } else if (mTransitionPending) {
441
442            // If we don't have a pending tint change yet, the change might come in the future until
443            // startTime is reached.
444            mTransitionDeferring = true;
445            mTransitionDeferringStartTime = startTime;
446            mTransitionDeferringDuration = duration;
447            mHandler.removeCallbacks(mTransitionDeferringDoneRunnable);
448            mHandler.postAtTime(mTransitionDeferringDoneRunnable, startTime);
449        }
450        mTransitionPending = false;
451    }
452
453    public static ArraySet<String> getIconBlacklist(String blackListStr) {
454        ArraySet<String> ret = new ArraySet<String>();
455        if (blackListStr != null) {
456            String[] blacklist = blackListStr.split(",");
457            for (String slot : blacklist) {
458                if (!TextUtils.isEmpty(slot)) {
459                    ret.add(slot);
460                }
461            }
462        }
463        return ret;
464    }
465}
466