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_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 
     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 
    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 
    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 
    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 
    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 
    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 
    248         public static HeaderProcessor forTextView(ExpandableNotificationRow row, int id) {
    249             return new HeaderProcessor(row, id, null, sTextViewComparator, sVisibilityApplicator);
    250         }
    251 
    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 
    262         public void init() {
    263             mParentView = mParentRow.getNotificationHeader().findViewById(mId);
    264             mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
    265             mApply = !mComparator.isEmpty(mParentView);
    266         }
    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 
    282         public void apply(ExpandableNotificationRow row) {
    283             apply(row, false /* reset */);
    284         }
    285 
    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 
    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          */
    315         boolean compare(View parent, View child, Object parentData, Object childData);
    316         boolean isEmpty(View view);
    317     }
    318 
    319     private interface DataExtractor {
    320         Object extractData(ExpandableNotificationRow row);
    321     }
    322 
    323     private static class TextViewComparator implements ViewComparator {
    324         @Override
    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
    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
    339         public boolean compare(View parent, View child, Object parentData, Object childData) {
    340             return false;
    341         }
    342 
    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          */
    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
    359         public boolean isEmpty(View view) {
    360             return false;
    361         }
    362     }
    363 
    364     private interface ResultApplicator {
    365         void apply(View view, boolean apply);
    366     }
    367 
    368     private static class VisibilityApplicator implements ResultApplicator {
    369 
    370         @Override
    371         public void apply(View view, boolean apply) {
    372             view.setVisibility(apply ? View.GONE : View.VISIBLE);
    373         }
    374     }
    375 }
    376