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.view.ViewGroup; 27 import android.widget.ImageView; 28 import android.widget.TextView; 29 30 import com.android.internal.util.ContrastColorUtil; 31 import com.android.internal.widget.ConversationLayout; 32 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 33 import com.android.systemui.statusbar.notification.row.NotificationContentView; 34 35 import java.util.ArrayList; 36 import java.util.HashSet; 37 import java.util.List; 38 39 /** 40 * A Util to manage {@link android.view.NotificationHeaderView} objects and their redundancies. 41 */ 42 public class NotificationHeaderUtil { 43 44 private static final TextViewComparator sTextViewComparator = new TextViewComparator(); 45 private static final VisibilityApplicator sVisibilityApplicator = new VisibilityApplicator(); 46 private static final VisibilityApplicator sAppNameApplicator = new AppNameApplicator(); 47 private static final DataExtractor sIconExtractor = new DataExtractor() { 48 @Override 49 public Object extractData(ExpandableNotificationRow row) { 50 return row.getEntry().getSbn().getNotification(); 51 } 52 }; 53 private static final IconComparator sIconVisibilityComparator = new IconComparator() { 54 public boolean compare(View parent, View child, Object parentData, 55 Object childData) { 56 return hasSameIcon(parentData, childData) 57 && hasSameColor(parentData, childData); 58 } 59 }; 60 private static final IconComparator sGreyComparator = new IconComparator() { 61 public boolean compare(View parent, View child, Object parentData, 62 Object childData) { 63 return !hasSameIcon(parentData, childData) 64 || hasSameColor(parentData, childData); 65 } 66 }; 67 private final static ResultApplicator mGreyApplicator = new ResultApplicator() { 68 @Override 69 public void apply(View parent, View view, boolean apply, boolean reset) { 70 NotificationHeaderView header = (NotificationHeaderView) view; 71 ImageView icon = (ImageView) view.findViewById( 72 com.android.internal.R.id.icon); 73 ImageView expand = (ImageView) view.findViewById( 74 com.android.internal.R.id.expand_button); 75 applyToChild(icon, apply, header.getOriginalIconColor()); 76 applyToChild(expand, apply, header.getOriginalNotificationColor()); 77 } 78 79 private void applyToChild(View view, boolean shouldApply, int originalColor) { 80 if (originalColor != NotificationHeaderView.NO_COLOR) { 81 ImageView imageView = (ImageView) view; 82 imageView.getDrawable().mutate(); 83 if (shouldApply) { 84 // lets gray it out 85 Configuration config = view.getContext().getResources().getConfiguration(); 86 boolean inNightMode = (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) 87 == Configuration.UI_MODE_NIGHT_YES; 88 int grey = ContrastColorUtil.resolveColor(view.getContext(), 89 Notification.COLOR_DEFAULT, inNightMode); 90 imageView.getDrawable().setColorFilter(grey, PorterDuff.Mode.SRC_ATOP); 91 } else { 92 // lets reset it 93 imageView.getDrawable().setColorFilter(originalColor, 94 PorterDuff.Mode.SRC_ATOP); 95 } 96 } 97 } 98 }; 99 100 private final ExpandableNotificationRow mRow; 101 private final ArrayList<HeaderProcessor> mComparators = new ArrayList<>(); 102 private final HashSet<Integer> mDividers = new HashSet<>(); 103 NotificationHeaderUtil(ExpandableNotificationRow row)104 public NotificationHeaderUtil(ExpandableNotificationRow row) { 105 mRow = row; 106 // To hide the icons if they are the same and the color is the same 107 mComparators.add(new HeaderProcessor(mRow, 108 com.android.internal.R.id.icon, 109 sIconExtractor, 110 sIconVisibilityComparator, 111 sVisibilityApplicator)); 112 // To grey them out the icons and expand button when the icons are not the same 113 mComparators.add(new HeaderProcessor(mRow, 114 com.android.internal.R.id.notification_header, 115 sIconExtractor, 116 sGreyComparator, 117 mGreyApplicator)); 118 mComparators.add(new HeaderProcessor(mRow, 119 com.android.internal.R.id.profile_badge, 120 null /* Extractor */, 121 new ViewComparator() { 122 @Override 123 public boolean compare(View parent, View child, Object parentData, 124 Object childData) { 125 return parent.getVisibility() != View.GONE; 126 } 127 128 @Override 129 public boolean isEmpty(View view) { 130 if (view instanceof ImageView) { 131 return ((ImageView) view).getDrawable() == null; 132 } 133 return false; 134 } 135 }, 136 sVisibilityApplicator)); 137 mComparators.add(new HeaderProcessor( 138 mRow, 139 com.android.internal.R.id.app_name_text, 140 null, 141 sTextViewComparator, 142 sAppNameApplicator)); 143 mComparators.add(HeaderProcessor.forTextView(mRow, 144 com.android.internal.R.id.header_text)); 145 mDividers.add(com.android.internal.R.id.header_text_divider); 146 mDividers.add(com.android.internal.R.id.header_text_secondary_divider); 147 mDividers.add(com.android.internal.R.id.time_divider); 148 } 149 updateChildrenHeaderAppearance()150 public void updateChildrenHeaderAppearance() { 151 List<ExpandableNotificationRow> notificationChildren = mRow.getAttachedChildren(); 152 if (notificationChildren == null) { 153 return; 154 } 155 // Initialize the comparators 156 for (int compI = 0; compI < mComparators.size(); compI++) { 157 mComparators.get(compI).init(); 158 } 159 160 // Compare all notification headers 161 for (int i = 0; i < notificationChildren.size(); i++) { 162 ExpandableNotificationRow row = notificationChildren.get(i); 163 for (int compI = 0; compI < mComparators.size(); compI++) { 164 mComparators.get(compI).compareToHeader(row); 165 } 166 } 167 168 // Apply the comparison to the row 169 for (int i = 0; i < notificationChildren.size(); i++) { 170 ExpandableNotificationRow row = notificationChildren.get(i); 171 for (int compI = 0; compI < mComparators.size(); compI++) { 172 mComparators.get(compI).apply(row); 173 } 174 // We need to sanitize the dividers since they might be off-balance now 175 sanitizeHeaderViews(row); 176 } 177 } 178 sanitizeHeaderViews(ExpandableNotificationRow row)179 private void sanitizeHeaderViews(ExpandableNotificationRow row) { 180 if (row.isSummaryWithChildren()) { 181 sanitizeHeader(row.getNotificationHeader()); 182 return; 183 } 184 final NotificationContentView layout = row.getPrivateLayout(); 185 sanitizeChild(layout.getContractedChild()); 186 sanitizeChild(layout.getHeadsUpChild()); 187 sanitizeChild(layout.getExpandedChild()); 188 } 189 sanitizeChild(View child)190 private void sanitizeChild(View child) { 191 if (child != null) { 192 ViewGroup header = child.findViewById( 193 com.android.internal.R.id.notification_header); 194 sanitizeHeader(header); 195 } 196 } 197 sanitizeHeader(ViewGroup rowHeader)198 private void sanitizeHeader(ViewGroup rowHeader) { 199 if (rowHeader == null) { 200 return; 201 } 202 final int childCount = rowHeader.getChildCount(); 203 View time = rowHeader.findViewById(com.android.internal.R.id.time); 204 boolean hasVisibleText = false; 205 for (int i = 0; i < childCount; i++) { 206 View child = rowHeader.getChildAt(i); 207 if (child instanceof TextView 208 && child.getVisibility() != View.GONE 209 && !mDividers.contains(child.getId()) 210 && child != time) { 211 hasVisibleText = true; 212 break; 213 } 214 } 215 // in case no view is visible we make sure the time is visible 216 int timeVisibility = !hasVisibleText 217 || mRow.getEntry().getSbn().getNotification().showsTime() 218 ? View.VISIBLE : View.GONE; 219 time.setVisibility(timeVisibility); 220 View left = null; 221 View right; 222 for (int i = 0; i < childCount; i++) { 223 View child = rowHeader.getChildAt(i); 224 if (mDividers.contains(child.getId())) { 225 boolean visible = false; 226 // Lets find the item to the right 227 for (i++; i < childCount; i++) { 228 right = rowHeader.getChildAt(i); 229 if (mDividers.contains(right.getId())) { 230 // A divider was found, this needs to be hidden 231 i--; 232 break; 233 } else if (right.getVisibility() != View.GONE && right instanceof TextView) { 234 visible = left != null; 235 left = right; 236 break; 237 } 238 } 239 child.setVisibility(visible ? View.VISIBLE : View.GONE); 240 } else if (child.getVisibility() != View.GONE && child instanceof TextView) { 241 left = child; 242 } 243 } 244 } 245 restoreNotificationHeader(ExpandableNotificationRow row)246 public void restoreNotificationHeader(ExpandableNotificationRow row) { 247 for (int compI = 0; compI < mComparators.size(); compI++) { 248 mComparators.get(compI).apply(row, true /* reset */); 249 } 250 sanitizeHeaderViews(row); 251 } 252 253 private static class HeaderProcessor { 254 private final int mId; 255 private final DataExtractor mExtractor; 256 private final ResultApplicator mApplicator; 257 private final ExpandableNotificationRow mParentRow; 258 private boolean mApply; 259 private View mParentView; 260 private ViewComparator mComparator; 261 private Object mParentData; 262 forTextView(ExpandableNotificationRow row, int id)263 public static HeaderProcessor forTextView(ExpandableNotificationRow row, int id) { 264 return new HeaderProcessor(row, id, null, sTextViewComparator, sVisibilityApplicator); 265 } 266 HeaderProcessor(ExpandableNotificationRow row, int id, DataExtractor extractor, ViewComparator comparator, ResultApplicator applicator)267 HeaderProcessor(ExpandableNotificationRow row, int id, DataExtractor extractor, 268 ViewComparator comparator, 269 ResultApplicator applicator) { 270 mId = id; 271 mExtractor = extractor; 272 mApplicator = applicator; 273 mComparator = comparator; 274 mParentRow = row; 275 } 276 init()277 public void init() { 278 mParentView = mParentRow.getNotificationHeader().findViewById(mId); 279 mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow); 280 mApply = !mComparator.isEmpty(mParentView); 281 } compareToHeader(ExpandableNotificationRow row)282 public void compareToHeader(ExpandableNotificationRow row) { 283 if (!mApply) { 284 return; 285 } 286 View contractedChild = row.getPrivateLayout().getContractedChild(); 287 if (contractedChild == null) { 288 return; 289 } 290 View ownView = contractedChild.findViewById(mId); 291 if (ownView == null) { 292 // No view found. We still consider this to be the same to avoid weird flickering 293 // when for example showing an undo notification 294 return; 295 } 296 Object childData = mExtractor == null ? null : mExtractor.extractData(row); 297 mApply = mComparator.compare(mParentView, ownView, 298 mParentData, childData); 299 } 300 apply(ExpandableNotificationRow row)301 public void apply(ExpandableNotificationRow row) { 302 apply(row, false /* reset */); 303 } 304 apply(ExpandableNotificationRow row, boolean reset)305 public void apply(ExpandableNotificationRow row, boolean reset) { 306 boolean apply = mApply && !reset; 307 if (row.isSummaryWithChildren()) { 308 applyToView(apply, reset, row.getNotificationHeader()); 309 return; 310 } 311 applyToView(apply, reset, row.getPrivateLayout().getContractedChild()); 312 applyToView(apply, reset, row.getPrivateLayout().getHeadsUpChild()); 313 applyToView(apply, reset, row.getPrivateLayout().getExpandedChild()); 314 } 315 applyToView(boolean apply, boolean reset, View parent)316 private void applyToView(boolean apply, boolean reset, View parent) { 317 if (parent != null) { 318 View view = parent.findViewById(mId); 319 if (view != null && !mComparator.isEmpty(view)) { 320 mApplicator.apply(parent, view, apply, reset); 321 } 322 } 323 } 324 } 325 326 private interface ViewComparator { 327 /** 328 * @param parent the parent view 329 * @param child the child view 330 * @param parentData optional data for the parent 331 * @param childData optional data for the child 332 * @return whether to views are the same 333 */ compare(View parent, View child, Object parentData, Object childData)334 boolean compare(View parent, View child, Object parentData, Object childData); isEmpty(View view)335 boolean isEmpty(View view); 336 } 337 338 private interface DataExtractor { extractData(ExpandableNotificationRow row)339 Object extractData(ExpandableNotificationRow row); 340 } 341 342 private static class TextViewComparator implements ViewComparator { 343 @Override compare(View parent, View child, Object parentData, Object childData)344 public boolean compare(View parent, View child, Object parentData, Object childData) { 345 TextView parentView = (TextView) parent; 346 TextView childView = (TextView) child; 347 return parentView.getText().equals(childView.getText()); 348 } 349 350 @Override isEmpty(View view)351 public boolean isEmpty(View view) { 352 return TextUtils.isEmpty(((TextView) view).getText()); 353 } 354 } 355 356 private static abstract class IconComparator implements ViewComparator { 357 @Override compare(View parent, View child, Object parentData, Object childData)358 public boolean compare(View parent, View child, Object parentData, Object childData) { 359 return false; 360 } 361 hasSameIcon(Object parentData, Object childData)362 protected boolean hasSameIcon(Object parentData, Object childData) { 363 Icon parentIcon = ((Notification) parentData).getSmallIcon(); 364 Icon childIcon = ((Notification) childData).getSmallIcon(); 365 return parentIcon.sameAs(childIcon); 366 } 367 368 /** 369 * @return whether two ImageViews have the same colorFilterSet or none at all 370 */ hasSameColor(Object parentData, Object childData)371 protected boolean hasSameColor(Object parentData, Object childData) { 372 int parentColor = ((Notification) parentData).color; 373 int childColor = ((Notification) childData).color; 374 return parentColor == childColor; 375 } 376 377 @Override isEmpty(View view)378 public boolean isEmpty(View view) { 379 return false; 380 } 381 } 382 383 private interface ResultApplicator { apply(View parent, View view, boolean apply, boolean reset)384 void apply(View parent, View view, boolean apply, boolean reset); 385 } 386 387 private static class VisibilityApplicator implements ResultApplicator { 388 389 @Override apply(View parent, View view, boolean apply, boolean reset)390 public void apply(View parent, View view, boolean apply, boolean reset) { 391 view.setVisibility(apply ? View.GONE : View.VISIBLE); 392 } 393 } 394 395 private static class AppNameApplicator extends VisibilityApplicator { 396 397 @Override apply(View parent, View view, boolean apply, boolean reset)398 public void apply(View parent, View view, boolean apply, boolean reset) { 399 if (reset && parent instanceof ConversationLayout) { 400 ConversationLayout layout = (ConversationLayout) parent; 401 apply = layout.shouldHideAppName(); 402 } 403 super.apply(parent, view, apply, reset); 404 } 405 } 406 } 407