1package com.android.systemui.statusbar.phone;
2
3import android.content.Context;
4import android.content.res.Resources;
5import android.graphics.Color;
6import android.graphics.Rect;
7import android.graphics.drawable.Icon;
8import android.support.annotation.NonNull;
9import android.support.v4.util.ArrayMap;
10import android.support.v4.util.ArraySet;
11import android.view.LayoutInflater;
12import android.view.View;
13import android.widget.FrameLayout;
14
15import com.android.internal.statusbar.StatusBarIcon;
16import com.android.internal.util.NotificationColorUtil;
17import com.android.systemui.R;
18import com.android.systemui.statusbar.ExpandableNotificationRow;
19import com.android.systemui.statusbar.NotificationData;
20import com.android.systemui.statusbar.NotificationShelf;
21import com.android.systemui.statusbar.StatusBarIconView;
22import com.android.systemui.statusbar.notification.NotificationUtils;
23import com.android.systemui.statusbar.policy.DarkIconDispatcher;
24import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
25import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
26
27import java.util.ArrayList;
28import java.util.function.Function;
29
30/**
31 * A controller for the space in the status bar to the left of the system icons. This area is
32 * normally reserved for notifications.
33 */
34public class NotificationIconAreaController implements DarkReceiver {
35    private final NotificationColorUtil mNotificationColorUtil;
36
37    private int mIconSize;
38    private int mIconHPadding;
39    private int mIconTint = Color.WHITE;
40
41    private StatusBar mStatusBar;
42    protected View mNotificationIconArea;
43    private NotificationIconContainer mNotificationIcons;
44    private NotificationIconContainer mShelfIcons;
45    private final Rect mTintArea = new Rect();
46    private NotificationStackScrollLayout mNotificationScrollLayout;
47    private Context mContext;
48
49    public NotificationIconAreaController(Context context, StatusBar statusBar) {
50        mStatusBar = statusBar;
51        mNotificationColorUtil = NotificationColorUtil.getInstance(context);
52        mContext = context;
53
54        initializeNotificationAreaViews(context);
55    }
56
57    protected View inflateIconArea(LayoutInflater inflater) {
58        return inflater.inflate(R.layout.notification_icon_area, null);
59    }
60
61    /**
62     * Initializes the views that will represent the notification area.
63     */
64    protected void initializeNotificationAreaViews(Context context) {
65        reloadDimens(context);
66
67        LayoutInflater layoutInflater = LayoutInflater.from(context);
68        mNotificationIconArea = inflateIconArea(layoutInflater);
69        mNotificationIcons = (NotificationIconContainer) mNotificationIconArea.findViewById(
70                R.id.notificationIcons);
71
72        mNotificationScrollLayout = mStatusBar.getNotificationScrollLayout();
73    }
74
75    public void setupShelf(NotificationShelf shelf) {
76        mShelfIcons = shelf.getShelfIcons();
77        shelf.setCollapsedIcons(mNotificationIcons);
78    }
79
80    public void onDensityOrFontScaleChanged(Context context) {
81        reloadDimens(context);
82        final FrameLayout.LayoutParams params = generateIconLayoutParams();
83        for (int i = 0; i < mNotificationIcons.getChildCount(); i++) {
84            View child = mNotificationIcons.getChildAt(i);
85            child.setLayoutParams(params);
86        }
87        for (int i = 0; i < mShelfIcons.getChildCount(); i++) {
88            View child = mShelfIcons.getChildAt(i);
89            child.setLayoutParams(params);
90        }
91    }
92
93    @NonNull
94    private FrameLayout.LayoutParams generateIconLayoutParams() {
95        return new FrameLayout.LayoutParams(
96                mIconSize + 2 * mIconHPadding, getHeight());
97    }
98
99    private void reloadDimens(Context context) {
100        Resources res = context.getResources();
101        mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size);
102        mIconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_padding);
103    }
104
105    /**
106     * Returns the view that represents the notification area.
107     */
108    public View getNotificationInnerAreaView() {
109        return mNotificationIconArea;
110    }
111
112    /**
113     * See {@link com.android.systemui.statusbar.policy.DarkIconDispatcher#setIconsDarkArea}.
114     * Sets the color that should be used to tint any icons in the notification area.
115     *
116     * @param tintArea the area in which to tint the icons, specified in screen coordinates
117     * @param darkIntensity
118     */
119    public void onDarkChanged(Rect tintArea, float darkIntensity, int iconTint) {
120        if (tintArea == null) {
121            mTintArea.setEmpty();
122        } else {
123            mTintArea.set(tintArea);
124        }
125        mIconTint = iconTint;
126        applyNotificationIconsTint();
127    }
128
129    protected int getHeight() {
130        return mStatusBar.getStatusBarHeight();
131    }
132
133    protected boolean shouldShowNotificationIcon(NotificationData.Entry entry,
134            NotificationData notificationData, boolean showAmbient) {
135        if (notificationData.isAmbient(entry.key) && !showAmbient) {
136            return false;
137        }
138        if (!StatusBar.isTopLevelChild(entry)) {
139            return false;
140        }
141        if (entry.row.getVisibility() == View.GONE) {
142            return false;
143        }
144
145        return true;
146    }
147
148    /**
149     * Updates the notifications with the given list of notifications to display.
150     */
151    public void updateNotificationIcons(NotificationData notificationData) {
152
153        updateIconsForLayout(notificationData, entry -> entry.icon, mNotificationIcons,
154                false /* showAmbient */);
155        updateIconsForLayout(notificationData, entry -> entry.expandedIcon, mShelfIcons,
156                NotificationShelf.SHOW_AMBIENT_ICONS);
157
158        applyNotificationIconsTint();
159    }
160
161    /**
162     * Updates the notification icons for a host layout. This will ensure that the notification
163     * host layout will have the same icons like the ones in here.
164     *
165     * @param notificationData the notification data to look up which notifications are relevant
166     * @param function A function to look up an icon view based on an entry
167     * @param hostLayout which layout should be updated
168     * @param showAmbient should ambient notification icons be shown
169     */
170    private void updateIconsForLayout(NotificationData notificationData,
171            Function<NotificationData.Entry, StatusBarIconView> function,
172            NotificationIconContainer hostLayout, boolean showAmbient) {
173        ArrayList<StatusBarIconView> toShow = new ArrayList<>(
174                mNotificationScrollLayout.getChildCount());
175
176        // Filter out ambient notifications and notification children.
177        for (int i = 0; i < mNotificationScrollLayout.getChildCount(); i++) {
178            View view = mNotificationScrollLayout.getChildAt(i);
179            if (view instanceof ExpandableNotificationRow) {
180                NotificationData.Entry ent = ((ExpandableNotificationRow) view).getEntry();
181                if (shouldShowNotificationIcon(ent, notificationData, showAmbient)) {
182                    toShow.add(function.apply(ent));
183                }
184            }
185        }
186
187        // In case we are changing the suppression of a group, the replacement shouldn't flicker
188        // and it should just be replaced instead. We therefore look for notifications that were
189        // just replaced by the child or vice-versa to suppress this.
190
191        ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons = new ArrayMap<>();
192        ArrayList<View> toRemove = new ArrayList<>();
193        for (int i = 0; i < hostLayout.getChildCount(); i++) {
194            View child = hostLayout.getChildAt(i);
195            if (!(child instanceof StatusBarIconView)) {
196                continue;
197            }
198            if (!toShow.contains(child)) {
199                boolean iconWasReplaced = false;
200                StatusBarIconView removedIcon = (StatusBarIconView) child;
201                String removedGroupKey = removedIcon.getNotification().getGroupKey();
202                for (int j = 0; j < toShow.size(); j++) {
203                    StatusBarIconView candidate = toShow.get(j);
204                    if (candidate.getSourceIcon().sameAs((removedIcon.getSourceIcon()))
205                            && candidate.getNotification().getGroupKey().equals(removedGroupKey)) {
206                        if (!iconWasReplaced) {
207                            iconWasReplaced = true;
208                        } else {
209                            iconWasReplaced = false;
210                            break;
211                        }
212                    }
213                }
214                if (iconWasReplaced) {
215                    ArrayList<StatusBarIcon> statusBarIcons = replacingIcons.get(removedGroupKey);
216                    if (statusBarIcons == null) {
217                        statusBarIcons = new ArrayList<>();
218                        replacingIcons.put(removedGroupKey, statusBarIcons);
219                    }
220                    statusBarIcons.add(removedIcon.getStatusBarIcon());
221                }
222                toRemove.add(removedIcon);
223            }
224        }
225        // removing all duplicates
226        ArrayList<String> duplicates = new ArrayList<>();
227        for (String key : replacingIcons.keySet()) {
228            ArrayList<StatusBarIcon> statusBarIcons = replacingIcons.get(key);
229            if (statusBarIcons.size() != 1) {
230                duplicates.add(key);
231            }
232        }
233        replacingIcons.removeAll(duplicates);
234        hostLayout.setReplacingIcons(replacingIcons);
235
236        final int toRemoveCount = toRemove.size();
237        for (int i = 0; i < toRemoveCount; i++) {
238            hostLayout.removeView(toRemove.get(i));
239        }
240
241        final FrameLayout.LayoutParams params = generateIconLayoutParams();
242        for (int i = 0; i < toShow.size(); i++) {
243            View v = toShow.get(i);
244            // The view might still be transiently added if it was just removed and added again
245            hostLayout.removeTransientView(v);
246            if (v.getParent() == null) {
247                hostLayout.addView(v, i, params);
248            }
249        }
250
251        hostLayout.setChangingViewPositions(true);
252        // Re-sort notification icons
253        final int childCount = hostLayout.getChildCount();
254        for (int i = 0; i < childCount; i++) {
255            View actual = hostLayout.getChildAt(i);
256            StatusBarIconView expected = toShow.get(i);
257            if (actual == expected) {
258                continue;
259            }
260            hostLayout.removeView(expected);
261            hostLayout.addView(expected, i);
262        }
263        hostLayout.setChangingViewPositions(false);
264        hostLayout.setReplacingIcons(null);
265    }
266
267    /**
268     * Applies {@link #mIconTint} to the notification icons.
269     */
270    private void applyNotificationIconsTint() {
271        for (int i = 0; i < mNotificationIcons.getChildCount(); i++) {
272            StatusBarIconView v = (StatusBarIconView) mNotificationIcons.getChildAt(i);
273            boolean isPreL = Boolean.TRUE.equals(v.getTag(R.id.icon_is_pre_L));
274            int color = StatusBarIconView.NO_COLOR;
275            boolean colorize = !isPreL || NotificationUtils.isGrayscale(v, mNotificationColorUtil);
276            if (colorize) {
277                color = DarkIconDispatcher.getTint(mTintArea, v, mIconTint);
278            }
279            v.setStaticDrawableColor(color);
280            v.setDecorColor(mIconTint);
281        }
282    }
283}
284