/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.statusbar; import android.app.Notification; import android.graphics.PorterDuff; import android.graphics.drawable.Icon; import android.text.TextUtils; import android.view.NotificationHeaderView; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import java.util.ArrayList; import java.util.HashSet; import java.util.List; /** * A Util to manage {@link android.view.NotificationHeaderView} objects and their redundancies. */ public class NotificationHeaderUtil { private static final TextViewComparator sTextViewComparator = new TextViewComparator(); private static final VisibilityApplicator sVisibilityApplicator = new VisibilityApplicator(); private static final DataExtractor sIconExtractor = new DataExtractor() { @Override public Object extractData(ExpandableNotificationRow row) { return row.getStatusBarNotification().getNotification(); } }; private static final IconComparator sIconVisibilityComparator = new IconComparator() { public boolean compare(View parent, View child, Object parentData, Object childData) { return hasSameIcon(parentData, childData) && hasSameColor(parentData, childData); } }; private static final IconComparator sGreyComparator = new IconComparator() { public boolean compare(View parent, View child, Object parentData, Object childData) { return !hasSameIcon(parentData, childData) || hasSameColor(parentData, childData); } }; private final static ResultApplicator mGreyApplicator = new ResultApplicator() { @Override public void apply(View view, boolean apply) { NotificationHeaderView header = (NotificationHeaderView) view; ImageView icon = (ImageView) view.findViewById( com.android.internal.R.id.icon); ImageView expand = (ImageView) view.findViewById( com.android.internal.R.id.expand_button); applyToChild(icon, apply, header.getOriginalIconColor()); applyToChild(expand, apply, header.getOriginalNotificationColor()); } private void applyToChild(View view, boolean shouldApply, int originalColor) { if (originalColor != NotificationHeaderView.NO_COLOR) { ImageView imageView = (ImageView) view; imageView.getDrawable().mutate(); if (shouldApply) { // lets gray it out int grey = view.getContext().getColor( com.android.internal.R.color.notification_icon_default_color); imageView.getDrawable().setColorFilter(grey, PorterDuff.Mode.SRC_ATOP); } else { // lets reset it imageView.getDrawable().setColorFilter(originalColor, PorterDuff.Mode.SRC_ATOP); } } } }; private final ExpandableNotificationRow mRow; private final ArrayList mComparators = new ArrayList<>(); private final HashSet mDividers = new HashSet<>(); public NotificationHeaderUtil(ExpandableNotificationRow row) { mRow = row; // To hide the icons if they are the same and the color is the same mComparators.add(new HeaderProcessor(mRow, com.android.internal.R.id.icon, sIconExtractor, sIconVisibilityComparator, sVisibilityApplicator)); // To grey them out the icons and expand button when the icons are not the same mComparators.add(new HeaderProcessor(mRow, com.android.internal.R.id.notification_header, sIconExtractor, sGreyComparator, mGreyApplicator)); mComparators.add(new HeaderProcessor(mRow, com.android.internal.R.id.profile_badge, null /* Extractor */, new ViewComparator() { @Override public boolean compare(View parent, View child, Object parentData, Object childData) { return parent.getVisibility() != View.GONE; } @Override public boolean isEmpty(View view) { if (view instanceof ImageView) { return ((ImageView) view).getDrawable() == null; } return false; } }, sVisibilityApplicator)); mComparators.add(HeaderProcessor.forTextView(mRow, com.android.internal.R.id.app_name_text)); mComparators.add(HeaderProcessor.forTextView(mRow, com.android.internal.R.id.header_text)); mDividers.add(com.android.internal.R.id.header_text_divider); mDividers.add(com.android.internal.R.id.time_divider); } public void updateChildrenHeaderAppearance() { List notificationChildren = mRow.getNotificationChildren(); if (notificationChildren == null) { return; } // Initialize the comparators for (int compI = 0; compI < mComparators.size(); compI++) { mComparators.get(compI).init(); } // Compare all notification headers for (int i = 0; i < notificationChildren.size(); i++) { ExpandableNotificationRow row = notificationChildren.get(i); for (int compI = 0; compI < mComparators.size(); compI++) { mComparators.get(compI).compareToHeader(row); } } // Apply the comparison to the row for (int i = 0; i < notificationChildren.size(); i++) { ExpandableNotificationRow row = notificationChildren.get(i); for (int compI = 0; compI < mComparators.size(); compI++) { mComparators.get(compI).apply(row); } // We need to sanitize the dividers since they might be off-balance now sanitizeHeaderViews(row); } } private void sanitizeHeaderViews(ExpandableNotificationRow row) { if (row.isSummaryWithChildren()) { sanitizeHeader(row.getNotificationHeader()); return; } final NotificationContentView layout = row.getPrivateLayout(); sanitizeChild(layout.getContractedChild()); sanitizeChild(layout.getHeadsUpChild()); sanitizeChild(layout.getExpandedChild()); } private void sanitizeChild(View child) { if (child != null) { NotificationHeaderView header = (NotificationHeaderView) child.findViewById( com.android.internal.R.id.notification_header); sanitizeHeader(header); } } private void sanitizeHeader(NotificationHeaderView rowHeader) { if (rowHeader == null) { return; } final int childCount = rowHeader.getChildCount(); View time = rowHeader.findViewById(com.android.internal.R.id.time); boolean hasVisibleText = false; for (int i = 1; i < childCount - 1 ; i++) { View child = rowHeader.getChildAt(i); if (child instanceof TextView && child.getVisibility() != View.GONE && !mDividers.contains(Integer.valueOf(child.getId())) && child != time) { hasVisibleText = true; break; } } // in case no view is visible we make sure the time is visible int timeVisibility = !hasVisibleText || mRow.getStatusBarNotification().getNotification().showsTime() ? View.VISIBLE : View.GONE; time.setVisibility(timeVisibility); View left = null; View right; for (int i = 1; i < childCount - 1 ; i++) { View child = rowHeader.getChildAt(i); if (mDividers.contains(Integer.valueOf(child.getId()))) { boolean visible = false; // Lets find the item to the right for (i++; i < childCount - 1; i++) { right = rowHeader.getChildAt(i); if (mDividers.contains(Integer.valueOf(right.getId()))) { // A divider was found, this needs to be hidden i--; break; } else if (right.getVisibility() != View.GONE && right instanceof TextView) { visible = left != null; left = right; break; } } child.setVisibility(visible ? View.VISIBLE : View.GONE); } else if (child.getVisibility() != View.GONE && child instanceof TextView) { left = child; } } } public void restoreNotificationHeader(ExpandableNotificationRow row) { for (int compI = 0; compI < mComparators.size(); compI++) { mComparators.get(compI).apply(row, true /* reset */); } sanitizeHeaderViews(row); } private static class HeaderProcessor { private final int mId; private final DataExtractor mExtractor; private final ResultApplicator mApplicator; private final ExpandableNotificationRow mParentRow; private boolean mApply; private View mParentView; private ViewComparator mComparator; private Object mParentData; public static HeaderProcessor forTextView(ExpandableNotificationRow row, int id) { return new HeaderProcessor(row, id, null, sTextViewComparator, sVisibilityApplicator); } HeaderProcessor(ExpandableNotificationRow row, int id, DataExtractor extractor, ViewComparator comparator, ResultApplicator applicator) { mId = id; mExtractor = extractor; mApplicator = applicator; mComparator = comparator; mParentRow = row; } public void init() { mParentView = mParentRow.getNotificationHeader().findViewById(mId); mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow); mApply = !mComparator.isEmpty(mParentView); } public void compareToHeader(ExpandableNotificationRow row) { if (!mApply) { return; } NotificationHeaderView header = row.getContractedNotificationHeader(); if (header == null) { // No header found. We still consider this to be the same to avoid weird flickering // when for example showing an undo notification return; } Object childData = mExtractor == null ? null : mExtractor.extractData(row); mApply = mComparator.compare(mParentView, header.findViewById(mId), mParentData, childData); } public void apply(ExpandableNotificationRow row) { apply(row, false /* reset */); } public void apply(ExpandableNotificationRow row, boolean reset) { boolean apply = mApply && !reset; if (row.isSummaryWithChildren()) { applyToView(apply, row.getNotificationHeader()); return; } applyToView(apply, row.getPrivateLayout().getContractedChild()); applyToView(apply, row.getPrivateLayout().getHeadsUpChild()); applyToView(apply, row.getPrivateLayout().getExpandedChild()); } private void applyToView(boolean apply, View parent) { if (parent != null) { View view = parent.findViewById(mId); if (view != null && !mComparator.isEmpty(view)) { mApplicator.apply(view, apply); } } } } private interface ViewComparator { /** * @param parent the parent view * @param child the child view * @param parentData optional data for the parent * @param childData optional data for the child * @return whether to views are the same */ boolean compare(View parent, View child, Object parentData, Object childData); boolean isEmpty(View view); } private interface DataExtractor { Object extractData(ExpandableNotificationRow row); } private static class TextViewComparator implements ViewComparator { @Override public boolean compare(View parent, View child, Object parentData, Object childData) { TextView parentView = (TextView) parent; TextView childView = (TextView) child; return parentView.getText().equals(childView.getText()); } @Override public boolean isEmpty(View view) { return TextUtils.isEmpty(((TextView) view).getText()); } } private static abstract class IconComparator implements ViewComparator { @Override public boolean compare(View parent, View child, Object parentData, Object childData) { return false; } protected boolean hasSameIcon(Object parentData, Object childData) { Icon parentIcon = ((Notification) parentData).getSmallIcon(); Icon childIcon = ((Notification) childData).getSmallIcon(); return parentIcon.sameAs(childIcon); } /** * @return whether two ImageViews have the same colorFilterSet or none at all */ protected boolean hasSameColor(Object parentData, Object childData) { int parentColor = ((Notification) parentData).color; int childColor = ((Notification) childData).color; return parentColor == childColor; } @Override public boolean isEmpty(View view) { return false; } } private interface ResultApplicator { void apply(View view, boolean apply); } private static class VisibilityApplicator implements ResultApplicator { @Override public void apply(View view, boolean apply) { view.setVisibility(apply ? View.GONE : View.VISIBLE); } } }