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