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 
17 package com.android.systemui.statusbar;
18 
19 import android.app.Notification;
20 import android.graphics.PorterDuff;
21 import android.graphics.drawable.Icon;
22 import android.text.TextUtils;
23 import android.view.NotificationHeaderView;
24 import android.view.View;
25 import android.widget.ImageView;
26 import android.widget.TextView;
27 
28 import java.util.ArrayList;
29 import java.util.HashSet;
30 import java.util.List;
31 
32 /**
33  * A Util to manage {@link android.view.NotificationHeaderView} objects and their redundancies.
34  */
35 public class NotificationHeaderUtil {
36 
37     private static final TextViewComparator sTextViewComparator = new TextViewComparator();
38     private static final VisibilityApplicator sVisibilityApplicator = new VisibilityApplicator();
39     private static  final DataExtractor sIconExtractor = new DataExtractor() {
40         @Override
41         public Object extractData(ExpandableNotificationRow row) {
42             return row.getStatusBarNotification().getNotification();
43         }
44     };
45     private static final IconComparator sIconVisibilityComparator = new IconComparator() {
46         public boolean compare(View parent, View child, Object parentData,
47                 Object childData) {
48             return hasSameIcon(parentData, childData)
49                     && hasSameColor(parentData, childData);
50         }
51     };
52     private static final IconComparator sGreyComparator = new IconComparator() {
53         public boolean compare(View parent, View child, Object parentData,
54                 Object childData) {
55             return !hasSameIcon(parentData, childData)
56                     || hasSameColor(parentData, childData);
57         }
58     };
59     private final static ResultApplicator mGreyApplicator = new ResultApplicator() {
60         @Override
61         public void apply(View view, boolean apply) {
62             NotificationHeaderView header = (NotificationHeaderView) view;
63             ImageView icon = (ImageView) view.findViewById(
64                     com.android.internal.R.id.icon);
65             ImageView expand = (ImageView) view.findViewById(
66                     com.android.internal.R.id.expand_button);
67             applyToChild(icon, apply, header.getOriginalIconColor());
68             applyToChild(expand, apply, header.getOriginalNotificationColor());
69         }
70 
71         private void applyToChild(View view, boolean shouldApply, int originalColor) {
72             if (originalColor != NotificationHeaderView.NO_COLOR) {
73                 ImageView imageView = (ImageView) view;
74                 imageView.getDrawable().mutate();
75                 if (shouldApply) {
76                     // lets gray it out
77                     int grey = view.getContext().getColor(
78                             com.android.internal.R.color.notification_default_color_light);
79                     imageView.getDrawable().setColorFilter(grey, PorterDuff.Mode.SRC_ATOP);
80                 } else {
81                     // lets reset it
82                     imageView.getDrawable().setColorFilter(originalColor,
83                             PorterDuff.Mode.SRC_ATOP);
84                 }
85             }
86         }
87     };
88 
89     private final ExpandableNotificationRow mRow;
90     private final ArrayList<HeaderProcessor> mComparators = new ArrayList<>();
91     private final HashSet<Integer> mDividers = new HashSet<>();
92 
NotificationHeaderUtil(ExpandableNotificationRow row)93     public NotificationHeaderUtil(ExpandableNotificationRow row) {
94         mRow = row;
95         // To hide the icons if they are the same and the color is the same
96         mComparators.add(new HeaderProcessor(mRow,
97                 com.android.internal.R.id.icon,
98                 sIconExtractor,
99                 sIconVisibilityComparator,
100                 sVisibilityApplicator));
101         // To grey them out the icons and expand button when the icons are not the same
102         mComparators.add(new HeaderProcessor(mRow,
103                 com.android.internal.R.id.notification_header,
104                 sIconExtractor,
105                 sGreyComparator,
106                 mGreyApplicator));
107         mComparators.add(new HeaderProcessor(mRow,
108                 com.android.internal.R.id.profile_badge,
109                 null /* Extractor */,
110                 new ViewComparator() {
111                     @Override
112                     public boolean compare(View parent, View child, Object parentData,
113                             Object childData) {
114                         return parent.getVisibility() != View.GONE;
115                     }
116 
117                     @Override
118                     public boolean isEmpty(View view) {
119                         if (view instanceof ImageView) {
120                             return ((ImageView) view).getDrawable() == null;
121                         }
122                         return false;
123                     }
124                 },
125                 sVisibilityApplicator));
126         mComparators.add(HeaderProcessor.forTextView(mRow,
127                 com.android.internal.R.id.app_name_text));
128         mComparators.add(HeaderProcessor.forTextView(mRow,
129                 com.android.internal.R.id.header_text));
130         mDividers.add(com.android.internal.R.id.header_text_divider);
131         mDividers.add(com.android.internal.R.id.header_text_secondary_divider);
132         mDividers.add(com.android.internal.R.id.time_divider);
133     }
134 
updateChildrenHeaderAppearance()135     public void updateChildrenHeaderAppearance() {
136         List<ExpandableNotificationRow> notificationChildren = mRow.getNotificationChildren();
137         if (notificationChildren == null) {
138             return;
139         }
140         // Initialize the comparators
141         for (int compI = 0; compI < mComparators.size(); compI++) {
142             mComparators.get(compI).init();
143         }
144 
145         // Compare all notification headers
146         for (int i = 0; i < notificationChildren.size(); i++) {
147             ExpandableNotificationRow row = notificationChildren.get(i);
148             for (int compI = 0; compI < mComparators.size(); compI++) {
149                 mComparators.get(compI).compareToHeader(row);
150             }
151         }
152 
153         // Apply the comparison to the row
154         for (int i = 0; i < notificationChildren.size(); i++) {
155             ExpandableNotificationRow row = notificationChildren.get(i);
156             for (int compI = 0; compI < mComparators.size(); compI++) {
157                 mComparators.get(compI).apply(row);
158             }
159             // We need to sanitize the dividers since they might be off-balance now
160             sanitizeHeaderViews(row);
161         }
162     }
163 
sanitizeHeaderViews(ExpandableNotificationRow row)164     private void sanitizeHeaderViews(ExpandableNotificationRow row) {
165         if (row.isSummaryWithChildren()) {
166             sanitizeHeader(row.getNotificationHeader());
167             return;
168         }
169         final NotificationContentView layout = row.getPrivateLayout();
170         sanitizeChild(layout.getContractedChild());
171         sanitizeChild(layout.getHeadsUpChild());
172         sanitizeChild(layout.getExpandedChild());
173     }
174 
sanitizeChild(View child)175     private void sanitizeChild(View child) {
176         if (child != null) {
177             NotificationHeaderView header = (NotificationHeaderView) child.findViewById(
178                     com.android.internal.R.id.notification_header);
179             sanitizeHeader(header);
180         }
181     }
182 
sanitizeHeader(NotificationHeaderView rowHeader)183     private void sanitizeHeader(NotificationHeaderView rowHeader) {
184         if (rowHeader == null) {
185             return;
186         }
187         final int childCount = rowHeader.getChildCount();
188         View time = rowHeader.findViewById(com.android.internal.R.id.time);
189         boolean hasVisibleText = false;
190         for (int i = 1; i < childCount - 1 ; i++) {
191             View child = rowHeader.getChildAt(i);
192             if (child instanceof TextView
193                     && child.getVisibility() != View.GONE
194                     && !mDividers.contains(Integer.valueOf(child.getId()))
195                     && child != time) {
196                 hasVisibleText = true;
197                 break;
198             }
199         }
200         // in case no view is visible we make sure the time is visible
201         int timeVisibility = !hasVisibleText
202                 || mRow.getStatusBarNotification().getNotification().showsTime()
203                 ? View.VISIBLE : View.GONE;
204         time.setVisibility(timeVisibility);
205         View left = null;
206         View right;
207         for (int i = 1; i < childCount - 1 ; i++) {
208             View child = rowHeader.getChildAt(i);
209             if (mDividers.contains(Integer.valueOf(child.getId()))) {
210                 boolean visible = false;
211                 // Lets find the item to the right
212                 for (i++; i < childCount - 1; i++) {
213                     right = rowHeader.getChildAt(i);
214                     if (mDividers.contains(Integer.valueOf(right.getId()))) {
215                         // A divider was found, this needs to be hidden
216                         i--;
217                         break;
218                     } else if (right.getVisibility() != View.GONE && right instanceof TextView) {
219                         visible = left != null;
220                         left = right;
221                         break;
222                     }
223                 }
224                 child.setVisibility(visible ? View.VISIBLE : View.GONE);
225             } else if (child.getVisibility() != View.GONE && child instanceof TextView) {
226                 left = child;
227             }
228         }
229     }
230 
restoreNotificationHeader(ExpandableNotificationRow row)231     public void restoreNotificationHeader(ExpandableNotificationRow row) {
232         for (int compI = 0; compI < mComparators.size(); compI++) {
233             mComparators.get(compI).apply(row, true /* reset */);
234         }
235         sanitizeHeaderViews(row);
236     }
237 
238     private static class HeaderProcessor {
239         private final int mId;
240         private final DataExtractor mExtractor;
241         private final ResultApplicator mApplicator;
242         private final ExpandableNotificationRow mParentRow;
243         private boolean mApply;
244         private View mParentView;
245         private ViewComparator mComparator;
246         private Object mParentData;
247 
forTextView(ExpandableNotificationRow row, int id)248         public static HeaderProcessor forTextView(ExpandableNotificationRow row, int id) {
249             return new HeaderProcessor(row, id, null, sTextViewComparator, sVisibilityApplicator);
250         }
251 
HeaderProcessor(ExpandableNotificationRow row, int id, DataExtractor extractor, ViewComparator comparator, ResultApplicator applicator)252         HeaderProcessor(ExpandableNotificationRow row, int id, DataExtractor extractor,
253                 ViewComparator comparator,
254                 ResultApplicator applicator) {
255             mId = id;
256             mExtractor = extractor;
257             mApplicator = applicator;
258             mComparator = comparator;
259             mParentRow = row;
260         }
261 
init()262         public void init() {
263             mParentView = mParentRow.getNotificationHeader().findViewById(mId);
264             mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
265             mApply = !mComparator.isEmpty(mParentView);
266         }
compareToHeader(ExpandableNotificationRow row)267         public void compareToHeader(ExpandableNotificationRow row) {
268             if (!mApply) {
269                 return;
270             }
271             NotificationHeaderView header = row.getContractedNotificationHeader();
272             if (header == null) {
273                 // No header found. We still consider this to be the same to avoid weird flickering
274                 // when for example showing an undo notification
275                 return;
276             }
277             Object childData = mExtractor == null ? null : mExtractor.extractData(row);
278             mApply = mComparator.compare(mParentView, header.findViewById(mId),
279                     mParentData, childData);
280         }
281 
apply(ExpandableNotificationRow row)282         public void apply(ExpandableNotificationRow row) {
283             apply(row, false /* reset */);
284         }
285 
apply(ExpandableNotificationRow row, boolean reset)286         public void apply(ExpandableNotificationRow row, boolean reset) {
287             boolean apply = mApply && !reset;
288             if (row.isSummaryWithChildren()) {
289                 applyToView(apply, row.getNotificationHeader());
290                 return;
291             }
292             applyToView(apply, row.getPrivateLayout().getContractedChild());
293             applyToView(apply, row.getPrivateLayout().getHeadsUpChild());
294             applyToView(apply, row.getPrivateLayout().getExpandedChild());
295         }
296 
applyToView(boolean apply, View parent)297         private void applyToView(boolean apply, View parent) {
298             if (parent != null) {
299                 View view = parent.findViewById(mId);
300                 if (view != null && !mComparator.isEmpty(view)) {
301                     mApplicator.apply(view, apply);
302                 }
303             }
304         }
305     }
306 
307     private interface ViewComparator {
308         /**
309          * @param parent the parent view
310          * @param child the child view
311          * @param parentData optional data for the parent
312          * @param childData optional data for the child
313          * @return whether to views are the same
314          */
compare(View parent, View child, Object parentData, Object childData)315         boolean compare(View parent, View child, Object parentData, Object childData);
isEmpty(View view)316         boolean isEmpty(View view);
317     }
318 
319     private interface DataExtractor {
extractData(ExpandableNotificationRow row)320         Object extractData(ExpandableNotificationRow row);
321     }
322 
323     private static class TextViewComparator implements ViewComparator {
324         @Override
compare(View parent, View child, Object parentData, Object childData)325         public boolean compare(View parent, View child, Object parentData, Object childData) {
326             TextView parentView = (TextView) parent;
327             TextView childView = (TextView) child;
328             return parentView.getText().equals(childView.getText());
329         }
330 
331         @Override
isEmpty(View view)332         public boolean isEmpty(View view) {
333             return TextUtils.isEmpty(((TextView) view).getText());
334         }
335     }
336 
337     private static abstract class IconComparator implements ViewComparator {
338         @Override
compare(View parent, View child, Object parentData, Object childData)339         public boolean compare(View parent, View child, Object parentData, Object childData) {
340             return false;
341         }
342 
hasSameIcon(Object parentData, Object childData)343         protected boolean hasSameIcon(Object parentData, Object childData) {
344             Icon parentIcon = ((Notification) parentData).getSmallIcon();
345             Icon childIcon = ((Notification) childData).getSmallIcon();
346             return parentIcon.sameAs(childIcon);
347         }
348 
349         /**
350          * @return whether two ImageViews have the same colorFilterSet or none at all
351          */
hasSameColor(Object parentData, Object childData)352         protected boolean hasSameColor(Object parentData, Object childData) {
353             int parentColor = ((Notification) parentData).color;
354             int childColor = ((Notification) childData).color;
355             return parentColor == childColor;
356         }
357 
358         @Override
isEmpty(View view)359         public boolean isEmpty(View view) {
360             return false;
361         }
362     }
363 
364     private interface ResultApplicator {
apply(View view, boolean apply)365         void apply(View view, boolean apply);
366     }
367 
368     private static class VisibilityApplicator implements ResultApplicator {
369 
370         @Override
apply(View view, boolean apply)371         public void apply(View view, boolean apply) {
372             view.setVisibility(apply ? View.GONE : View.VISIBLE);
373         }
374     }
375 }
376