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