• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.drawable.Drawable;
21 import android.graphics.drawable.Icon;
22 import android.text.TextUtils;
23 import android.util.DisplayMetrics;
24 import android.util.TypedValue;
25 import android.view.NotificationHeaderView;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.widget.ImageView;
29 import android.widget.TextView;
30 
31 import com.android.internal.R;
32 import com.android.internal.widget.CachingIconView;
33 import com.android.internal.widget.ConversationLayout;
34 import com.android.internal.widget.ImageFloatingTextView;
35 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
36 import com.android.systemui.statusbar.notification.row.NotificationContentView;
37 
38 import java.util.ArrayList;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Objects;
42 
43 /**
44  * A utility to manage notification views when they are placed in a group by adjusting elements
45  * to reduce redundancies and occasionally tweak layouts to highlight the unique content.
46  */
47 public class NotificationGroupingUtil {
48 
49     private static final TextViewComparator TEXT_VIEW_COMPARATOR = new TextViewComparator();
50     private static final TextViewComparator APP_NAME_COMPARATOR = new AppNameComparator();
51     private static final ViewComparator BADGE_COMPARATOR = new BadgeComparator();
52     private static final VisibilityApplicator VISIBILITY_APPLICATOR = new VisibilityApplicator();
53     private static final VisibilityApplicator APP_NAME_APPLICATOR = new AppNameApplicator();
54     private static final ResultApplicator LEFT_ICON_APPLICATOR = new LeftIconApplicator();
55     private static final DataExtractor ICON_EXTRACTOR = new DataExtractor() {
56         @Override
57         public Object extractData(ExpandableNotificationRow row) {
58             return row.getEntry().getSbn().getNotification();
59         }
60     };
61     private static final IconComparator ICON_VISIBILITY_COMPARATOR = new IconComparator() {
62         public boolean compare(View parent, View child, Object parentData,
63                 Object childData) {
64             return hasSameIcon(parentData, childData)
65                     && hasSameColor(parentData, childData);
66         }
67     };
68     private static final IconComparator GREY_COMPARATOR = new IconComparator() {
69         public boolean compare(View parent, View child, Object parentData,
70                 Object childData) {
71             return !hasSameIcon(parentData, childData)
72                     || hasSameColor(parentData, childData);
73         }
74     };
75     private static final ResultApplicator GREY_APPLICATOR = new ResultApplicator() {
76         @Override
77         public void apply(View parent, View view, boolean apply, boolean reset) {
78             CachingIconView icon = view.findViewById(com.android.internal.R.id.icon);
79             if (icon != null) {
80                 icon.setGrayedOut(apply);
81             }
82         }
83     };
84 
85     private final ExpandableNotificationRow mRow;
86     private final ArrayList<Processor> mProcessors = new ArrayList<>();
87     private final HashSet<Integer> mDividers = new HashSet<>();
88 
NotificationGroupingUtil(ExpandableNotificationRow row)89     public NotificationGroupingUtil(ExpandableNotificationRow row) {
90         mRow = row;
91         // To hide the icons if they are the same and the color is the same
92         mProcessors.add(new Processor(mRow,
93                 com.android.internal.R.id.icon,
94                 ICON_EXTRACTOR,
95                 ICON_VISIBILITY_COMPARATOR,
96                 VISIBILITY_APPLICATOR));
97         // To grey them out the icons and expand button when the icons are not the same
98         mProcessors.add(new Processor(mRow,
99                 com.android.internal.R.id.status_bar_latest_event_content,
100                 ICON_EXTRACTOR,
101                 GREY_COMPARATOR,
102                 GREY_APPLICATOR));
103         mProcessors.add(new Processor(mRow,
104                 com.android.internal.R.id.status_bar_latest_event_content,
105                 ICON_EXTRACTOR,
106                 ICON_VISIBILITY_COMPARATOR,
107                 LEFT_ICON_APPLICATOR));
108         mProcessors.add(new Processor(mRow,
109                 com.android.internal.R.id.profile_badge,
110                 null /* Extractor */,
111                 BADGE_COMPARATOR,
112                 VISIBILITY_APPLICATOR));
113         mProcessors.add(new Processor(mRow,
114                 com.android.internal.R.id.app_name_text,
115                 null,
116                 APP_NAME_COMPARATOR,
117                 APP_NAME_APPLICATOR));
118         mProcessors.add(Processor.forTextView(mRow, com.android.internal.R.id.header_text));
119         mDividers.add(com.android.internal.R.id.header_text_divider);
120         mDividers.add(com.android.internal.R.id.header_text_secondary_divider);
121         mDividers.add(com.android.internal.R.id.time_divider);
122     }
123 
124     /**
125      * Update the appearance of the children in this group to reduce redundancies.
126      */
updateChildrenAppearance()127     public void updateChildrenAppearance() {
128         List<ExpandableNotificationRow> notificationChildren = mRow.getAttachedChildren();
129         if (notificationChildren == null || !mRow.isSummaryWithChildren()) {
130             return;
131         }
132         // Initialize the processors
133         for (int compI = 0; compI < mProcessors.size(); compI++) {
134             mProcessors.get(compI).init();
135         }
136 
137         // Compare all notification headers
138         for (int i = 0; i < notificationChildren.size(); i++) {
139             ExpandableNotificationRow row = notificationChildren.get(i);
140             for (int compI = 0; compI < mProcessors.size(); compI++) {
141                 mProcessors.get(compI).compareToGroupParent(row);
142             }
143         }
144 
145         // Apply the comparison to the row
146         for (int i = 0; i < notificationChildren.size(); i++) {
147             ExpandableNotificationRow row = notificationChildren.get(i);
148             for (int compI = 0; compI < mProcessors.size(); compI++) {
149                 mProcessors.get(compI).apply(row);
150             }
151             // We need to sanitize the dividers since they might be off-balance now
152             sanitizeTopLineViews(row);
153         }
154     }
155 
sanitizeTopLineViews(ExpandableNotificationRow row)156     private void sanitizeTopLineViews(ExpandableNotificationRow row) {
157         if (row.isSummaryWithChildren()) {
158             sanitizeTopLine(row.getNotificationViewWrapper().getNotificationHeader(), row);
159             return;
160         }
161         final NotificationContentView layout = row.getPrivateLayout();
162         sanitizeChild(layout.getContractedChild(), row);
163         sanitizeChild(layout.getHeadsUpChild(), row);
164         sanitizeChild(layout.getExpandedChild(), row);
165     }
166 
sanitizeChild(View child, ExpandableNotificationRow row)167     private void sanitizeChild(View child, ExpandableNotificationRow row) {
168         if (child != null) {
169             sanitizeTopLine(child.findViewById(R.id.notification_top_line), row);
170         }
171     }
172 
sanitizeTopLine(ViewGroup rowHeader, ExpandableNotificationRow row)173     private void sanitizeTopLine(ViewGroup rowHeader, ExpandableNotificationRow row) {
174         if (rowHeader == null) {
175             return;
176         }
177         final int childCount = rowHeader.getChildCount();
178         View time = rowHeader.findViewById(com.android.internal.R.id.time);
179         boolean hasVisibleText = false;
180         for (int i = 0; i < childCount; i++) {
181             View child = rowHeader.getChildAt(i);
182             if (child instanceof TextView
183                     && child.getVisibility() != View.GONE
184                     && !mDividers.contains(child.getId())
185                     && child != time) {
186                 hasVisibleText = true;
187                 break;
188             }
189         }
190         // in case no view is visible we make sure the time is visible
191         int timeVisibility = !hasVisibleText
192                 || row.getEntry().getSbn().getNotification().showsTime()
193                 ? View.VISIBLE : View.GONE;
194         time.setVisibility(timeVisibility);
195         View left = null;
196         View right;
197         for (int i = 0; i < childCount; i++) {
198             View child = rowHeader.getChildAt(i);
199             if (mDividers.contains(child.getId())) {
200                 boolean visible = false;
201                 // Lets find the item to the right
202                 for (i++; i < childCount; i++) {
203                     right = rowHeader.getChildAt(i);
204                     if (mDividers.contains(right.getId())) {
205                         // A divider was found, this needs to be hidden
206                         i--;
207                         break;
208                     } else if (right.getVisibility() != View.GONE && right instanceof TextView) {
209                         visible = left != null;
210                         left = right;
211                         break;
212                     }
213                 }
214                 child.setVisibility(visible ? View.VISIBLE : View.GONE);
215             } else if (child.getVisibility() != View.GONE && child instanceof TextView) {
216                 left = child;
217             }
218         }
219     }
220 
221     /**
222      * Reset the modifications to this row for removing it from the group.
223      */
restoreChildNotification(ExpandableNotificationRow row)224     public void restoreChildNotification(ExpandableNotificationRow row) {
225         for (int compI = 0; compI < mProcessors.size(); compI++) {
226             mProcessors.get(compI).apply(row, true /* reset */);
227         }
228         sanitizeTopLineViews(row);
229     }
230 
231     private static class Processor {
232         private final int mId;
233         private final DataExtractor mExtractor;
234         private final ViewComparator mComparator;
235         private final ResultApplicator mApplicator;
236         private final ExpandableNotificationRow mParentRow;
237         private boolean mApply;
238         private View mParentView;
239         private Object mParentData;
240 
forTextView(ExpandableNotificationRow row, int id)241         public static Processor forTextView(ExpandableNotificationRow row, int id) {
242             return new Processor(row, id, null, TEXT_VIEW_COMPARATOR, VISIBILITY_APPLICATOR);
243         }
244 
Processor(ExpandableNotificationRow row, int id, DataExtractor extractor, ViewComparator comparator, ResultApplicator applicator)245         Processor(ExpandableNotificationRow row, int id, DataExtractor extractor,
246                 ViewComparator comparator,
247                 ResultApplicator applicator) {
248             mId = id;
249             mExtractor = extractor;
250             mApplicator = applicator;
251             mComparator = comparator;
252             mParentRow = row;
253         }
254 
init()255         public void init() {
256             View header = mParentRow.getNotificationViewWrapper().getNotificationHeader();
257             mParentView = header == null ? null : header.findViewById(mId);
258             mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
259             mApply = !mComparator.isEmpty(mParentView);
260         }
compareToGroupParent(ExpandableNotificationRow row)261         public void compareToGroupParent(ExpandableNotificationRow row) {
262             if (!mApply) {
263                 return;
264             }
265             View contractedChild = row.getPrivateLayout().getContractedChild();
266             if (contractedChild == null) {
267                 return;
268             }
269             View ownView = contractedChild.findViewById(mId);
270             if (ownView == null) {
271                 // No view found. We still consider this to be the same to avoid weird flickering
272                 // when for example showing an undo notification
273                 return;
274             }
275             Object childData = mExtractor == null ? null : mExtractor.extractData(row);
276             mApply = mComparator.compare(mParentView, ownView,
277                     mParentData, childData);
278         }
279 
apply(ExpandableNotificationRow row)280         public void apply(ExpandableNotificationRow row) {
281             apply(row, false /* reset */);
282         }
283 
apply(ExpandableNotificationRow row, boolean reset)284         public void apply(ExpandableNotificationRow row, boolean reset) {
285             boolean apply = mApply && !reset;
286             if (row.isSummaryWithChildren()) {
287                 applyToView(apply, reset, row.getNotificationViewWrapper().getNotificationHeader());
288                 return;
289             }
290             applyToView(apply, reset, row.getPrivateLayout().getContractedChild());
291             applyToView(apply, reset, row.getPrivateLayout().getHeadsUpChild());
292             applyToView(apply, reset, row.getPrivateLayout().getExpandedChild());
293         }
294 
applyToView(boolean apply, boolean reset, View parent)295         private void applyToView(boolean apply, boolean reset, View parent) {
296             if (parent != null) {
297                 View view = parent.findViewById(mId);
298                 if (view != null && !mComparator.isEmpty(view)) {
299                     mApplicator.apply(parent, view, apply, reset);
300                 }
301             }
302         }
303     }
304 
305     private interface ViewComparator {
306         /**
307          * @param parent the view with the given id in the group header
308          * @param child the view with the given id in the child notification
309          * @param parentData optional data for the parent
310          * @param childData optional data for the child
311          * @return whether to views are the same
312          */
compare(View parent, View child, Object parentData, Object childData)313         boolean compare(View parent, View child, Object parentData, Object childData);
isEmpty(View view)314         boolean isEmpty(View view);
315     }
316 
317     private interface DataExtractor {
extractData(ExpandableNotificationRow row)318         Object extractData(ExpandableNotificationRow row);
319     }
320 
321     private static class BadgeComparator implements ViewComparator {
322         @Override
compare(View parent, View child, Object parentData, Object childData)323         public boolean compare(View parent, View child, Object parentData, Object childData) {
324             return parent.getVisibility() != View.GONE;
325         }
326 
327         @Override
isEmpty(View view)328         public boolean isEmpty(View view) {
329             if (view instanceof ImageView) {
330                 return ((ImageView) view).getDrawable() == null;
331             }
332             return false;
333         }
334     }
335 
336     private static class TextViewComparator implements ViewComparator {
337         @Override
compare(View parent, View child, Object parentData, Object childData)338         public boolean compare(View parent, View child, Object parentData, Object childData) {
339             TextView parentView = (TextView) parent;
340             CharSequence parentText = parentView == null ? "" : parentView.getText();
341             TextView childView = (TextView) child;
342             CharSequence childText = childView == null ? "" : childView.getText();
343             return Objects.equals(parentText, childText);
344         }
345 
346         @Override
isEmpty(View view)347         public boolean isEmpty(View view) {
348             return view == null || TextUtils.isEmpty(((TextView) view).getText());
349         }
350     }
351 
352     private abstract static class IconComparator implements ViewComparator {
353         @Override
compare(View parent, View child, Object parentData, Object childData)354         public boolean compare(View parent, View child, Object parentData, Object childData) {
355             return false;
356         }
357 
hasSameIcon(Object parentData, Object childData)358         protected boolean hasSameIcon(Object parentData, Object childData) {
359             Icon parentIcon = ((Notification) parentData).getSmallIcon();
360             Icon childIcon = ((Notification) childData).getSmallIcon();
361             return parentIcon.sameAs(childIcon);
362         }
363 
364         /**
365          * @return whether two ImageViews have the same colorFilterSet or none at all
366          */
hasSameColor(Object parentData, Object childData)367         protected boolean hasSameColor(Object parentData, Object childData) {
368             int parentColor = ((Notification) parentData).color;
369             int childColor = ((Notification) childData).color;
370             return parentColor == childColor;
371         }
372 
373         @Override
isEmpty(View view)374         public boolean isEmpty(View view) {
375             return false;
376         }
377     }
378 
379     private interface ResultApplicator {
380         /**
381          * @param parent the root view of the child notification
382          * @param view the view with the given id in the child notification
383          * @param apply whether the state should be applied or removed
384          * @param reset if [de]application is the result of a reset
385          */
apply(View parent, View view, boolean apply, boolean reset)386         void apply(View parent, View view, boolean apply, boolean reset);
387     }
388 
389     private static class VisibilityApplicator implements ResultApplicator {
390 
391         @Override
apply(View parent, View view, boolean apply, boolean reset)392         public void apply(View parent, View view, boolean apply, boolean reset) {
393             if (view != null) {
394                 view.setVisibility(apply ? View.GONE : View.VISIBLE);
395             }
396         }
397     }
398 
399     private static class AppNameApplicator extends VisibilityApplicator {
400 
401         @Override
apply(View parent, View view, boolean apply, boolean reset)402         public void apply(View parent, View view, boolean apply, boolean reset) {
403             if (reset && parent instanceof ConversationLayout) {
404                 ConversationLayout layout = (ConversationLayout) parent;
405                 apply = layout.shouldHideAppName();
406             }
407             super.apply(parent, view, apply, reset);
408         }
409     }
410 
411     private static class AppNameComparator extends TextViewComparator {
412         @Override
compare(View parent, View child, Object parentData, Object childData)413         public boolean compare(View parent, View child, Object parentData, Object childData) {
414             if (isEmpty(child)) {
415                 // In headerless notifications the AppName view exists but is usually GONE (and not
416                 // populated).  We need to treat this case as equal to the header in order to
417                 // deduplicate the view.
418                 return true;
419             }
420             return super.compare(parent, child, parentData, childData);
421         }
422     }
423 
424     private static class LeftIconApplicator implements ResultApplicator {
425 
426         public static final int[] MARGIN_ADJUSTED_VIEWS = {
427                 R.id.text,
428                 R.id.big_text,
429                 R.id.title,
430                 R.id.notification_main_column,
431                 R.id.notification_header};
432 
433         @Override
apply(View parent, View child, boolean showLeftIcon, boolean reset)434         public void apply(View parent, View child, boolean showLeftIcon, boolean reset) {
435             ImageView leftIcon = child.findViewById(com.android.internal.R.id.left_icon);
436             if (leftIcon == null) {
437                 return;
438             }
439             ImageView rightIcon = child.findViewById(com.android.internal.R.id.right_icon);
440             boolean keepRightIcon = rightIcon != null && Integer.valueOf(1).equals(
441                     rightIcon.getTag(R.id.tag_keep_when_showing_left_icon));
442             boolean leftIconUsesRightIconDrawable = Integer.valueOf(1).equals(
443                     leftIcon.getTag(R.id.tag_uses_right_icon_drawable));
444             if (leftIconUsesRightIconDrawable) {
445                 // Use the right drawable when showing the left, unless the right is being kept
446                 Drawable rightDrawable = rightIcon == null ? null : rightIcon.getDrawable();
447                 leftIcon.setImageDrawable(showLeftIcon && !keepRightIcon ? rightDrawable : null);
448             }
449             leftIcon.setVisibility(showLeftIcon ? View.VISIBLE : View.GONE);
450 
451             // update the right icon as well
452             if (rightIcon != null) {
453                 boolean showRightIcon = (keepRightIcon || !showLeftIcon)
454                         && rightIcon.getDrawable() != null;
455                 rightIcon.setVisibility(showRightIcon ? View.VISIBLE : View.GONE);
456                 for (int viewId : MARGIN_ADJUSTED_VIEWS) {
457                     adjustMargins(showRightIcon, child.findViewById(viewId));
458                 }
459             }
460         }
461 
adjustMargins(boolean iconVisible, View target)462         void adjustMargins(boolean iconVisible, View target) {
463             if (target == null) {
464                 return;
465             }
466             if (target instanceof ImageFloatingTextView) {
467                 ((ImageFloatingTextView) target).setHasImage(iconVisible);
468                 return;
469             }
470             final Integer data = (Integer) target.getTag(iconVisible
471                     ? com.android.internal.R.id.tag_margin_end_when_icon_visible
472                     : com.android.internal.R.id.tag_margin_end_when_icon_gone);
473             if (data == null) {
474                 return;
475             }
476             final DisplayMetrics metrics = target.getResources().getDisplayMetrics();
477             final int value = TypedValue.complexToDimensionPixelOffset(data, metrics);
478             if (target instanceof NotificationHeaderView) {
479                 ((NotificationHeaderView) target).setTopLineExtraMarginEnd(value);
480             } else {
481                 ViewGroup.LayoutParams layoutParams = target.getLayoutParams();
482                 if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
483                     ((ViewGroup.MarginLayoutParams) layoutParams).setMarginEnd(value);
484                     target.setLayoutParams(layoutParams);
485                 }
486             }
487         }
488     }
489 }
490