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