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.getNotificationHeader(); 271 if (header == null) { 272 mApply = false; 273 return; 274 } 275 Object childData = mExtractor == null ? null : mExtractor.extractData(row); 276 mApply = mComparator.compare(mParentView, header.findViewById(mId), 277 mParentData, childData); 278 } 279 280 public void apply(ExpandableNotificationRow row) { 281 apply(row, false /* reset */); 282 } 283 284 public void apply(ExpandableNotificationRow row, boolean reset) { 285 boolean apply = mApply && !reset; 286 if (row.isSummaryWithChildren()) { 287 applyToView(apply, row.getNotificationHeader()); 288 return; 289 } 290 applyToView(apply, row.getPrivateLayout().getContractedChild()); 291 applyToView(apply, row.getPrivateLayout().getHeadsUpChild()); 292 applyToView(apply, row.getPrivateLayout().getExpandedChild()); 293 } 294 295 private void applyToView(boolean apply, View parent) { 296 if (parent != null) { 297 View view = parent.findViewById(mId); 298 if (view != null && !mComparator.isEmpty(view)) { 299 mApplicator.apply(view, apply); 300 } 301 } 302 } 303 } 304 305 private interface ViewComparator { 306 /** 307 * @param parent the parent view 308 * @param child the child view 309 * @param parentData optional data for the parent 310 * @param childData optional data for the child 311 * @return whether to views are the same 312 */ 313 boolean compare(View parent, View child, Object parentData, Object childData); 314 boolean isEmpty(View view); 315 } 316 317 private interface DataExtractor { 318 Object extractData(ExpandableNotificationRow row); 319 } 320 321 private static class TextViewComparator implements ViewComparator { 322 @Override 323 public boolean compare(View parent, View child, Object parentData, Object childData) { 324 TextView parentView = (TextView) parent; 325 TextView childView = (TextView) child; 326 return parentView.getText().equals(childView.getText()); 327 } 328 329 @Override 330 public boolean isEmpty(View view) { 331 return TextUtils.isEmpty(((TextView) view).getText()); 332 } 333 } 334 335 private static abstract class IconComparator implements ViewComparator { 336 @Override 337 public boolean compare(View parent, View child, Object parentData, Object childData) { 338 return false; 339 } 340 341 protected boolean hasSameIcon(Object parentData, Object childData) { 342 Icon parentIcon = ((Notification) parentData).getSmallIcon(); 343 Icon childIcon = ((Notification) childData).getSmallIcon(); 344 return parentIcon.sameAs(childIcon); 345 } 346 347 /** 348 * @return whether two ImageViews have the same colorFilterSet or none at all 349 */ 350 protected boolean hasSameColor(Object parentData, Object childData) { 351 int parentColor = ((Notification) parentData).color; 352 int childColor = ((Notification) childData).color; 353 return parentColor == childColor; 354 } 355 356 @Override 357 public boolean isEmpty(View view) { 358 return false; 359 } 360 } 361 362 private interface ResultApplicator { 363 void apply(View view, boolean apply); 364 } 365 366 private static class VisibilityApplicator implements ResultApplicator { 367 368 @Override 369 public void apply(View view, boolean apply) { 370 view.setVisibility(apply ? View.GONE : View.VISIBLE); 371 } 372 } 373 } 374