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