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