1 package com.android.systemui.statusbar.phone; 2 3 import android.content.Context; 4 import android.content.res.Resources; 5 import android.graphics.Color; 6 import android.graphics.Rect; 7 import android.view.LayoutInflater; 8 import android.view.View; 9 import android.view.ViewGroup; 10 import android.widget.FrameLayout; 11 12 import androidx.annotation.NonNull; 13 import androidx.collection.ArrayMap; 14 15 import com.android.internal.annotations.VisibleForTesting; 16 import com.android.internal.statusbar.StatusBarIcon; 17 import com.android.internal.util.ContrastColorUtil; 18 import com.android.systemui.Dependency; 19 import com.android.systemui.R; 20 import com.android.systemui.plugins.DarkIconDispatcher; 21 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; 22 import com.android.systemui.plugins.statusbar.StatusBarStateController; 23 import com.android.systemui.statusbar.NotificationListener; 24 import com.android.systemui.statusbar.NotificationMediaManager; 25 import com.android.systemui.statusbar.NotificationShelf; 26 import com.android.systemui.statusbar.StatusBarIconView; 27 import com.android.systemui.statusbar.StatusBarState; 28 import com.android.systemui.statusbar.notification.NotificationEntryManager; 29 import com.android.systemui.statusbar.notification.NotificationUtils; 30 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 31 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 32 33 import java.util.ArrayList; 34 import java.util.Objects; 35 import java.util.function.Function; 36 37 /** 38 * A controller for the space in the status bar to the left of the system icons. This area is 39 * normally reserved for notifications. 40 */ 41 public class NotificationIconAreaController implements DarkReceiver, 42 StatusBarStateController.StateListener { 43 44 public static final String HIGH_PRIORITY = "high_priority"; 45 46 private final ContrastColorUtil mContrastColorUtil; 47 private final NotificationEntryManager mEntryManager; 48 private final Runnable mUpdateStatusBarIcons = this::updateStatusBarIcons; 49 private final StatusBarStateController mStatusBarStateController; 50 private final NotificationMediaManager mMediaManager; 51 52 private int mIconSize; 53 private int mIconHPadding; 54 private int mIconTint = Color.WHITE; 55 private int mCenteredIconTint = Color.WHITE; 56 57 private StatusBar mStatusBar; 58 protected View mNotificationIconArea; 59 private NotificationIconContainer mNotificationIcons; 60 private NotificationIconContainer mShelfIcons; 61 protected View mCenteredIconArea; 62 private NotificationIconContainer mCenteredIcon; 63 private StatusBarIconView mCenteredIconView; 64 private final Rect mTintArea = new Rect(); 65 private ViewGroup mNotificationScrollLayout; 66 private Context mContext; 67 private boolean mFullyDark; 68 private boolean mAnimationsEnabled; 69 70 /** 71 * Ratio representing being awake or in ambient mode, where 1 is dark and 0 awake. 72 */ 73 private float mDarkAmount; 74 NotificationIconAreaController(Context context, StatusBar statusBar, StatusBarStateController statusBarStateController, NotificationMediaManager notificationMediaManager)75 public NotificationIconAreaController(Context context, StatusBar statusBar, 76 StatusBarStateController statusBarStateController, 77 NotificationMediaManager notificationMediaManager) { 78 mStatusBar = statusBar; 79 mContrastColorUtil = ContrastColorUtil.getInstance(context); 80 mContext = context; 81 mEntryManager = Dependency.get(NotificationEntryManager.class); 82 mStatusBarStateController = statusBarStateController; 83 mStatusBarStateController.addCallback(this); 84 mMediaManager = notificationMediaManager; 85 86 initializeNotificationAreaViews(context); 87 } 88 inflateIconArea(LayoutInflater inflater)89 protected View inflateIconArea(LayoutInflater inflater) { 90 return inflater.inflate(R.layout.notification_icon_area, null); 91 } 92 93 /** 94 * Initializes the views that will represent the notification area. 95 */ initializeNotificationAreaViews(Context context)96 protected void initializeNotificationAreaViews(Context context) { 97 reloadDimens(context); 98 99 LayoutInflater layoutInflater = LayoutInflater.from(context); 100 mNotificationIconArea = inflateIconArea(layoutInflater); 101 mNotificationIcons = mNotificationIconArea.findViewById(R.id.notificationIcons); 102 103 mNotificationScrollLayout = mStatusBar.getNotificationScrollLayout(); 104 105 mCenteredIconArea = layoutInflater.inflate(R.layout.center_icon_area, null); 106 mCenteredIcon = mCenteredIconArea.findViewById(R.id.centeredIcon); 107 } 108 setupShelf(NotificationShelf shelf)109 public void setupShelf(NotificationShelf shelf) { 110 mShelfIcons = shelf.getShelfIcons(); 111 shelf.setCollapsedIcons(mNotificationIcons); 112 } 113 onDensityOrFontScaleChanged(Context context)114 public void onDensityOrFontScaleChanged(Context context) { 115 reloadDimens(context); 116 final FrameLayout.LayoutParams params = generateIconLayoutParams(); 117 for (int i = 0; i < mNotificationIcons.getChildCount(); i++) { 118 View child = mNotificationIcons.getChildAt(i); 119 child.setLayoutParams(params); 120 } 121 for (int i = 0; i < mShelfIcons.getChildCount(); i++) { 122 View child = mShelfIcons.getChildAt(i); 123 child.setLayoutParams(params); 124 } 125 for (int i = 0; i < mCenteredIcon.getChildCount(); i++) { 126 View child = mCenteredIcon.getChildAt(i); 127 child.setLayoutParams(params); 128 } 129 } 130 131 @NonNull generateIconLayoutParams()132 private FrameLayout.LayoutParams generateIconLayoutParams() { 133 return new FrameLayout.LayoutParams( 134 mIconSize + 2 * mIconHPadding, getHeight()); 135 } 136 reloadDimens(Context context)137 private void reloadDimens(Context context) { 138 Resources res = context.getResources(); 139 mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size); 140 mIconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_padding); 141 } 142 143 /** 144 * Returns the view that represents the notification area. 145 */ getNotificationInnerAreaView()146 public View getNotificationInnerAreaView() { 147 return mNotificationIconArea; 148 } 149 150 /** 151 * Returns the view that represents the centered notification area. 152 */ getCenteredNotificationAreaView()153 public View getCenteredNotificationAreaView() { 154 return mCenteredIconArea; 155 } 156 157 /** 158 * See {@link com.android.systemui.statusbar.policy.DarkIconDispatcher#setIconsDarkArea}. 159 * Sets the color that should be used to tint any icons in the notification area. 160 * 161 * @param tintArea the area in which to tint the icons, specified in screen coordinates 162 * @param darkIntensity 163 */ onDarkChanged(Rect tintArea, float darkIntensity, int iconTint)164 public void onDarkChanged(Rect tintArea, float darkIntensity, int iconTint) { 165 if (tintArea == null) { 166 mTintArea.setEmpty(); 167 } else { 168 mTintArea.set(tintArea); 169 } 170 171 if (mNotificationIconArea != null) { 172 if (DarkIconDispatcher.isInArea(tintArea, mNotificationIconArea)) { 173 mIconTint = iconTint; 174 } 175 } else { 176 mIconTint = iconTint; 177 } 178 179 if (mCenteredIconArea != null) { 180 if (DarkIconDispatcher.isInArea(tintArea, mCenteredIconArea)) { 181 mCenteredIconTint = iconTint; 182 } 183 } else { 184 mCenteredIconTint = iconTint; 185 } 186 187 applyNotificationIconsTint(); 188 } 189 getHeight()190 protected int getHeight() { 191 return mStatusBar.getStatusBarHeight(); 192 } 193 shouldShowNotificationIcon(NotificationEntry entry, boolean showAmbient, boolean showLowPriority, boolean hideDismissed, boolean hideRepliedMessages, boolean hideCurrentMedia, boolean hideCenteredIcon)194 protected boolean shouldShowNotificationIcon(NotificationEntry entry, 195 boolean showAmbient, boolean showLowPriority, boolean hideDismissed, 196 boolean hideRepliedMessages, boolean hideCurrentMedia, boolean hideCenteredIcon) { 197 198 final boolean isCenteredNotificationIcon = entry.centeredIcon != null 199 && Objects.equals(entry.centeredIcon, mCenteredIconView); 200 if (hideCenteredIcon == isCenteredNotificationIcon) { 201 return false; 202 } 203 if (mEntryManager.getNotificationData().isAmbient(entry.key) && !showAmbient) { 204 return false; 205 } 206 if (hideCurrentMedia && entry.key.equals(mMediaManager.getMediaNotificationKey())) { 207 return false; 208 } 209 if (!showLowPriority && !entry.isHighPriority()) { 210 return false; 211 } 212 if (!entry.isTopLevelChild()) { 213 return false; 214 } 215 if (entry.getRow().getVisibility() == View.GONE) { 216 return false; 217 } 218 if (entry.isRowDismissed() && hideDismissed) { 219 return false; 220 } 221 if (hideRepliedMessages && entry.isLastMessageFromReply()) { 222 return false; 223 } 224 // showAmbient == show in shade but not shelf 225 if ((!showAmbient || mFullyDark) && entry.shouldSuppressStatusBar()) { 226 return false; 227 } 228 return true; 229 } 230 231 /** 232 * Updates the notifications with the given list of notifications to display. 233 */ updateNotificationIcons()234 public void updateNotificationIcons() { 235 updateStatusBarIcons(); 236 updateShelfIcons(); 237 updateCenterIcon(); 238 239 applyNotificationIconsTint(); 240 } 241 updateShelfIcons()242 private void updateShelfIcons() { 243 updateIconsForLayout(entry -> entry.expandedIcon, mShelfIcons, 244 true /* showAmbient */, 245 true /* showLowPriority */, 246 false /* hideDismissed */, 247 mFullyDark /* hideRepliedMessages */, 248 mFullyDark /* hideCurrentMedia */, 249 true /* hide centered icon */); 250 } 251 updateStatusBarIcons()252 public void updateStatusBarIcons() { 253 updateIconsForLayout(entry -> entry.icon, mNotificationIcons, 254 false /* showAmbient */, 255 true /* showLowPriority */, 256 true /* hideDismissed */, 257 true /* hideRepliedMessages */, 258 false /* hideCurrentMedia */, 259 true /* hide centered icon */); 260 } 261 updateCenterIcon()262 private void updateCenterIcon() { 263 updateIconsForLayout(entry -> entry.centeredIcon, mCenteredIcon, 264 false /* showAmbient */, 265 true /* showLowPriority */, 266 false /* hideDismissed */, 267 false /* hideRepliedMessages */, 268 mFullyDark /* hideCurrentMedia */, 269 false /* hide centered icon */); 270 } 271 272 /** 273 * If icons of the status bar should animate when they are added or removed. 274 */ setAnimationsEnabled(boolean enabled)275 public void setAnimationsEnabled(boolean enabled) { 276 mAnimationsEnabled = enabled; 277 updateAnimations(); 278 } 279 280 @Override onStateChanged(int newState)281 public void onStateChanged(int newState) { 282 updateAnimations(); 283 } 284 updateAnimations()285 private void updateAnimations() { 286 boolean inShade = mStatusBarStateController.getState() == StatusBarState.SHADE; 287 mCenteredIcon.setAnimationsEnabled(mAnimationsEnabled && inShade); 288 mNotificationIcons.setAnimationsEnabled(mAnimationsEnabled && inShade); 289 } 290 291 /** 292 * Updates the notification icons for a host layout. This will ensure that the notification 293 * host layout will have the same icons like the ones in here. 294 * @param function A function to look up an icon view based on an entry 295 * @param hostLayout which layout should be updated 296 * @param showAmbient should ambient notification icons be shown 297 * @param hideDismissed should dismissed icons be hidden 298 * @param hideRepliedMessages should messages that have been replied to be hidden 299 */ updateIconsForLayout(Function<NotificationEntry, StatusBarIconView> function, NotificationIconContainer hostLayout, boolean showAmbient, boolean showLowPriority, boolean hideDismissed, boolean hideRepliedMessages, boolean hideCurrentMedia, boolean hideCenteredIcon)300 private void updateIconsForLayout(Function<NotificationEntry, StatusBarIconView> function, 301 NotificationIconContainer hostLayout, boolean showAmbient, boolean showLowPriority, 302 boolean hideDismissed, boolean hideRepliedMessages, boolean hideCurrentMedia, 303 boolean hideCenteredIcon) { 304 ArrayList<StatusBarIconView> toShow = new ArrayList<>( 305 mNotificationScrollLayout.getChildCount()); 306 307 // Filter out ambient notifications and notification children. 308 for (int i = 0; i < mNotificationScrollLayout.getChildCount(); i++) { 309 View view = mNotificationScrollLayout.getChildAt(i); 310 if (view instanceof ExpandableNotificationRow) { 311 NotificationEntry ent = ((ExpandableNotificationRow) view).getEntry(); 312 if (shouldShowNotificationIcon(ent, showAmbient, showLowPriority, hideDismissed, 313 hideRepliedMessages, hideCurrentMedia, hideCenteredIcon)) { 314 StatusBarIconView iconView = function.apply(ent); 315 if (iconView != null) { 316 toShow.add(iconView); 317 } 318 } 319 } 320 } 321 322 // In case we are changing the suppression of a group, the replacement shouldn't flicker 323 // and it should just be replaced instead. We therefore look for notifications that were 324 // just replaced by the child or vice-versa to suppress this. 325 326 ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons = new ArrayMap<>(); 327 ArrayList<View> toRemove = new ArrayList<>(); 328 for (int i = 0; i < hostLayout.getChildCount(); i++) { 329 View child = hostLayout.getChildAt(i); 330 if (!(child instanceof StatusBarIconView)) { 331 continue; 332 } 333 if (!toShow.contains(child)) { 334 boolean iconWasReplaced = false; 335 StatusBarIconView removedIcon = (StatusBarIconView) child; 336 String removedGroupKey = removedIcon.getNotification().getGroupKey(); 337 for (int j = 0; j < toShow.size(); j++) { 338 StatusBarIconView candidate = toShow.get(j); 339 if (candidate.getSourceIcon().sameAs((removedIcon.getSourceIcon())) 340 && candidate.getNotification().getGroupKey().equals(removedGroupKey)) { 341 if (!iconWasReplaced) { 342 iconWasReplaced = true; 343 } else { 344 iconWasReplaced = false; 345 break; 346 } 347 } 348 } 349 if (iconWasReplaced) { 350 ArrayList<StatusBarIcon> statusBarIcons = replacingIcons.get(removedGroupKey); 351 if (statusBarIcons == null) { 352 statusBarIcons = new ArrayList<>(); 353 replacingIcons.put(removedGroupKey, statusBarIcons); 354 } 355 statusBarIcons.add(removedIcon.getStatusBarIcon()); 356 } 357 toRemove.add(removedIcon); 358 } 359 } 360 // removing all duplicates 361 ArrayList<String> duplicates = new ArrayList<>(); 362 for (String key : replacingIcons.keySet()) { 363 ArrayList<StatusBarIcon> statusBarIcons = replacingIcons.get(key); 364 if (statusBarIcons.size() != 1) { 365 duplicates.add(key); 366 } 367 } 368 replacingIcons.removeAll(duplicates); 369 hostLayout.setReplacingIcons(replacingIcons); 370 371 final int toRemoveCount = toRemove.size(); 372 for (int i = 0; i < toRemoveCount; i++) { 373 hostLayout.removeView(toRemove.get(i)); 374 } 375 376 final FrameLayout.LayoutParams params = generateIconLayoutParams(); 377 for (int i = 0; i < toShow.size(); i++) { 378 StatusBarIconView v = toShow.get(i); 379 // The view might still be transiently added if it was just removed and added again 380 hostLayout.removeTransientView(v); 381 if (v.getParent() == null) { 382 if (hideDismissed) { 383 v.setOnDismissListener(mUpdateStatusBarIcons); 384 } 385 hostLayout.addView(v, i, params); 386 } 387 } 388 389 hostLayout.setChangingViewPositions(true); 390 // Re-sort notification icons 391 final int childCount = hostLayout.getChildCount(); 392 for (int i = 0; i < childCount; i++) { 393 View actual = hostLayout.getChildAt(i); 394 StatusBarIconView expected = toShow.get(i); 395 if (actual == expected) { 396 continue; 397 } 398 hostLayout.removeView(expected); 399 hostLayout.addView(expected, i); 400 } 401 hostLayout.setChangingViewPositions(false); 402 hostLayout.setReplacingIcons(null); 403 } 404 405 /** 406 * Applies {@link #mIconTint} to the notification icons. 407 * Applies {@link #mCenteredIconTint} to the center notification icon. 408 */ applyNotificationIconsTint()409 private void applyNotificationIconsTint() { 410 for (int i = 0; i < mNotificationIcons.getChildCount(); i++) { 411 final StatusBarIconView iv = (StatusBarIconView) mNotificationIcons.getChildAt(i); 412 if (iv.getWidth() != 0) { 413 updateTintForIcon(iv, mIconTint); 414 } else { 415 iv.executeOnLayout(() -> updateTintForIcon(iv, mIconTint)); 416 } 417 } 418 419 for (int i = 0; i < mCenteredIcon.getChildCount(); i++) { 420 final StatusBarIconView iv = (StatusBarIconView) mCenteredIcon.getChildAt(i); 421 if (iv.getWidth() != 0) { 422 updateTintForIcon(iv, mCenteredIconTint); 423 } else { 424 iv.executeOnLayout(() -> updateTintForIcon(iv, mCenteredIconTint)); 425 } 426 } 427 } 428 updateTintForIcon(StatusBarIconView v, int tint)429 private void updateTintForIcon(StatusBarIconView v, int tint) { 430 boolean isPreL = Boolean.TRUE.equals(v.getTag(R.id.icon_is_pre_L)); 431 int color = StatusBarIconView.NO_COLOR; 432 boolean colorize = !isPreL || NotificationUtils.isGrayscale(v, mContrastColorUtil); 433 if (colorize) { 434 color = DarkIconDispatcher.getTint(mTintArea, v, tint); 435 } 436 v.setStaticDrawableColor(color); 437 v.setDecorColor(tint); 438 } 439 setDark(boolean dark)440 public void setDark(boolean dark) { 441 mNotificationIcons.setDark(dark, false, 0); 442 mShelfIcons.setDark(dark, false, 0); 443 mCenteredIcon.setDark(dark, false, 0); 444 } 445 446 /** 447 * Shows the icon view given in the center. 448 */ showIconCentered(NotificationEntry entry)449 public void showIconCentered(NotificationEntry entry) { 450 StatusBarIconView icon = entry == null ? null : entry.centeredIcon; 451 if (!Objects.equals(mCenteredIconView, icon)) { 452 mCenteredIconView = icon; 453 updateNotificationIcons(); 454 } 455 } 456 showIconIsolated(StatusBarIconView icon, boolean animated)457 public void showIconIsolated(StatusBarIconView icon, boolean animated) { 458 mNotificationIcons.showIconIsolated(icon, animated); 459 } 460 setIsolatedIconLocation(Rect iconDrawingRect, boolean requireStateUpdate)461 public void setIsolatedIconLocation(Rect iconDrawingRect, boolean requireStateUpdate) { 462 mNotificationIcons.setIsolatedIconLocation(iconDrawingRect, requireStateUpdate); 463 } 464 465 @Override onDozeAmountChanged(float linear, float eased)466 public void onDozeAmountChanged(float linear, float eased) { 467 mDarkAmount = linear; 468 boolean fullyDark = mDarkAmount == 1f; 469 if (mFullyDark != fullyDark) { 470 mFullyDark = fullyDark; 471 updateShelfIcons(); 472 } 473 } 474 } 475