Home | History | Annotate | Download | only in statusbar
      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_icon_default_color);
     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 
     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.time_divider);
    132     }
    133 
    134     public void updateChildrenHeaderAppearance() {
    135         List<ExpandableNotificationRow> notificationChildren = mRow.getNotificationChildren();
    136         if (notificationChildren == null) {
    137             return;
    138         }
    139         // Initialize the comparators
    140         for (int compI = 0; compI < mComparators.size(); compI++) {
    141             mComparators.get(compI).init();
    142         }
    143 
    144         // Compare all notification headers
    145         for (int i = 0; i < notificationChildren.size(); i++) {
    146             ExpandableNotificationRow row = notificationChildren.get(i);
    147             for (int compI = 0; compI < mComparators.size(); compI++) {
    148                 mComparators.get(compI).compareToHeader(row);
    149             }
    150         }
    151 
    152         // Apply the comparison to the row
    153         for (int i = 0; i < notificationChildren.size(); i++) {
    154             ExpandableNotificationRow row = notificationChildren.get(i);
    155             for (int compI = 0; compI < mComparators.size(); compI++) {
    156                 mComparators.get(compI).apply(row);
    157             }
    158             // We need to sanitize the dividers since they might be off-balance now
    159             sanitizeHeaderViews(row);
    160         }
    161     }
    162 
    163     private void sanitizeHeaderViews(ExpandableNotificationRow row) {
    164         if (row.isSummaryWithChildren()) {
    165             sanitizeHeader(row.getNotificationHeader());
    166             return;
    167         }
    168         final NotificationContentView layout = row.getPrivateLayout();
    169         sanitizeChild(layout.getContractedChild());
    170         sanitizeChild(layout.getHeadsUpChild());
    171         sanitizeChild(layout.getExpandedChild());
    172     }
    173 
    174     private void sanitizeChild(View child) {
    175         if (child != null) {
    176             NotificationHeaderView header = (NotificationHeaderView) child.findViewById(
    177                     com.android.internal.R.id.notification_header);
    178             sanitizeHeader(header);
    179         }
    180     }
    181 
    182     private void sanitizeHeader(NotificationHeaderView rowHeader) {
    183         if (rowHeader == null) {
    184             return;
    185         }
    186         final int childCount = rowHeader.getChildCount();
    187         View time = rowHeader.findViewById(com.android.internal.R.id.time);
    188         boolean hasVisibleText = false;
    189         for (int i = 1; i < childCount - 1 ; i++) {
    190             View child = rowHeader.getChildAt(i);
    191             if (child instanceof TextView
    192                     && child.getVisibility() != View.GONE
    193                     && !mDividers.contains(Integer.valueOf(child.getId()))
    194                     && child != time) {
    195                 hasVisibleText = true;
    196                 break;
    197             }
    198         }
    199         // in case no view is visible we make sure the time is visible
    200         int timeVisibility = !hasVisibleText
    201                 || mRow.getStatusBarNotification().getNotification().showsTime()
    202                 ? View.VISIBLE : View.GONE;
    203         time.setVisibility(timeVisibility);
    204         View left = null;
    205         View right;
    206         for (int i = 1; i < childCount - 1 ; i++) {
    207             View child = rowHeader.getChildAt(i);
    208             if (mDividers.contains(Integer.valueOf(child.getId()))) {
    209                 boolean visible = false;
    210                 // Lets find the item to the right
    211                 for (i++; i < childCount - 1; i++) {
    212                     right = rowHeader.getChildAt(i);
    213                     if (mDividers.contains(Integer.valueOf(right.getId()))) {
    214                         // A divider was found, this needs to be hidden
    215                         i--;
    216                         break;
    217                     } else if (right.getVisibility() != View.GONE && right instanceof TextView) {
    218                         visible = left != null;
    219                         left = right;
    220                         break;
    221                     }
    222                 }
    223                 child.setVisibility(visible ? View.VISIBLE : View.GONE);
    224             } else if (child.getVisibility() != View.GONE && child instanceof TextView) {
    225                 left = child;
    226             }
    227         }
    228     }
    229 
    230     public void restoreNotificationHeader(ExpandableNotificationRow row) {
    231         for (int compI = 0; compI < mComparators.size(); compI++) {
    232             mComparators.get(compI).apply(row, true /* reset */);
    233         }
    234         sanitizeHeaderViews(row);
    235     }
    236 
    237     private static class HeaderProcessor {
    238         private final int mId;
    239         private final DataExtractor mExtractor;
    240         private final ResultApplicator mApplicator;
    241         private final ExpandableNotificationRow mParentRow;
    242         private boolean mApply;
    243         private View mParentView;
    244         private ViewComparator mComparator;
    245         private Object mParentData;
    246 
    247         public static HeaderProcessor forTextView(ExpandableNotificationRow row, int id) {
    248             return new HeaderProcessor(row, id, null, sTextViewComparator, sVisibilityApplicator);
    249         }
    250 
    251         HeaderProcessor(ExpandableNotificationRow row, int id, DataExtractor extractor,
    252                 ViewComparator comparator,
    253                 ResultApplicator applicator) {
    254             mId = id;
    255             mExtractor = extractor;
    256             mApplicator = applicator;
    257             mComparator = comparator;
    258             mParentRow = row;
    259         }
    260 
    261         public void init() {
    262             mParentView = mParentRow.getNotificationHeader().findViewById(mId);
    263             mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
    264             mApply = !mComparator.isEmpty(mParentView);
    265         }
    266         public void compareToHeader(ExpandableNotificationRow row) {
    267             if (!mApply) {
    268                 return;
    269             }
    270             NotificationHeaderView header = row.getContractedNotificationHeader();
    271             if (header == null) {
    272                 // No header found. We still consider this to be the same to avoid weird flickering
    273                 // when for example showing an undo notification
    274                 return;
    275             }
    276             Object childData = mExtractor == null ? null : mExtractor.extractData(row);
    277             mApply = mComparator.compare(mParentView, header.findViewById(mId),
    278                     mParentData, childData);
    279         }
    280 
    281         public void apply(ExpandableNotificationRow row) {
    282             apply(row, false /* reset */);
    283         }
    284 
    285         public void apply(ExpandableNotificationRow row, boolean reset) {
    286             boolean apply = mApply && !reset;
    287             if (row.isSummaryWithChildren()) {
    288                 applyToView(apply, row.getNotificationHeader());
    289                 return;
    290             }
    291             applyToView(apply, row.getPrivateLayout().getContractedChild());
    292             applyToView(apply, row.getPrivateLayout().getHeadsUpChild());
    293             applyToView(apply, row.getPrivateLayout().getExpandedChild());
    294         }
    295 
    296         private void applyToView(boolean apply, View parent) {
    297             if (parent != null) {
    298                 View view = parent.findViewById(mId);
    299                 if (view != null && !mComparator.isEmpty(view)) {
    300                     mApplicator.apply(view, apply);
    301                 }
    302             }
    303         }
    304     }
    305 
    306     private interface ViewComparator {
    307         /**
    308          * @param parent the parent view
    309          * @param child the child view
    310          * @param parentData optional data for the parent
    311          * @param childData optional data for the child
    312          * @return whether to views are the same
    313          */
    314         boolean compare(View parent, View child, Object parentData, Object childData);
    315         boolean isEmpty(View view);
    316     }
    317 
    318     private interface DataExtractor {
    319         Object extractData(ExpandableNotificationRow row);
    320     }
    321 
    322     private static class TextViewComparator implements ViewComparator {
    323         @Override
    324         public boolean compare(View parent, View child, Object parentData, Object childData) {
    325             TextView parentView = (TextView) parent;
    326             TextView childView = (TextView) child;
    327             return parentView.getText().equals(childView.getText());
    328         }
    329 
    330         @Override
    331         public boolean isEmpty(View view) {
    332             return TextUtils.isEmpty(((TextView) view).getText());
    333         }
    334     }
    335 
    336     private static abstract class IconComparator implements ViewComparator {
    337         @Override
    338         public boolean compare(View parent, View child, Object parentData, Object childData) {
    339             return false;
    340         }
    341 
    342         protected boolean hasSameIcon(Object parentData, Object childData) {
    343             Icon parentIcon = ((Notification) parentData).getSmallIcon();
    344             Icon childIcon = ((Notification) childData).getSmallIcon();
    345             return parentIcon.sameAs(childIcon);
    346         }
    347 
    348         /**
    349          * @return whether two ImageViews have the same colorFilterSet or none at all
    350          */
    351         protected boolean hasSameColor(Object parentData, Object childData) {
    352             int parentColor = ((Notification) parentData).color;
    353             int childColor = ((Notification) childData).color;
    354             return parentColor == childColor;
    355         }
    356 
    357         @Override
    358         public boolean isEmpty(View view) {
    359             return false;
    360         }
    361     }
    362 
    363     private interface ResultApplicator {
    364         void apply(View view, boolean apply);
    365     }
    366 
    367     private static class VisibilityApplicator implements ResultApplicator {
    368 
    369         @Override
    370         public void apply(View view, boolean apply) {
    371             view.setVisibility(apply ? View.GONE : View.VISIBLE);
    372         }
    373     }
    374 }
    375