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