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.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 
    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 
    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 
    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 
    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 
    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 
    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 
    256         public static HeaderProcessor forTextView(ExpandableNotificationRow row, int id) {
    257             return new HeaderProcessor(row, id, null, sTextViewComparator, sVisibilityApplicator);
    258         }
    259 
    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 
    270         public void init() {
    271             mParentView = mParentRow.getNotificationHeader().findViewById(mId);
    272             mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
    273             mApply = !mComparator.isEmpty(mParentView);
    274         }
    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 
    290         public void apply(ExpandableNotificationRow row) {
    291             apply(row, false /* reset */);
    292         }
    293 
    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 
    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          */
    323         boolean compare(View parent, View child, Object parentData, Object childData);
    324         boolean isEmpty(View view);
    325     }
    326 
    327     private interface DataExtractor {
    328         Object extractData(ExpandableNotificationRow row);
    329     }
    330 
    331     private static class TextViewComparator implements ViewComparator {
    332         @Override
    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
    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
    347         public boolean compare(View parent, View child, Object parentData, Object childData) {
    348             return false;
    349         }
    350 
    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          */
    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
    367         public boolean isEmpty(View view) {
    368             return false;
    369         }
    370     }
    371 
    372     private interface ResultApplicator {
    373         void apply(View view, boolean apply);
    374     }
    375 
    376     private static class VisibilityApplicator implements ResultApplicator {
    377 
    378         @Override
    379         public void apply(View view, boolean apply) {
    380             view.setVisibility(apply ? View.GONE : View.VISIBLE);
    381         }
    382     }
    383 }
    384