1 /* 2 * Copyright (C) 2018 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.car.notification.template; 18 19 import android.annotation.CallSuper; 20 import android.annotation.ColorInt; 21 import android.annotation.Nullable; 22 import android.app.Notification; 23 import android.content.Context; 24 import android.view.View; 25 import android.view.ViewTreeObserver; 26 import android.widget.ImageButton; 27 28 import androidx.annotation.VisibleForTesting; 29 import androidx.cardview.widget.CardView; 30 import androidx.recyclerview.widget.RecyclerView; 31 32 import com.android.car.notification.AlertEntry; 33 import com.android.car.notification.NotificationClickHandlerFactory; 34 import com.android.car.notification.NotificationUtils; 35 import com.android.car.notification.R; 36 37 /** 38 * The base view holder class that all template view holders should extend. 39 */ 40 public abstract class CarNotificationBaseViewHolder extends RecyclerView.ViewHolder { 41 private final Context mContext; 42 private final NotificationClickHandlerFactory mClickHandlerFactory; 43 44 @Nullable 45 private final CardView mCardView; // can be null for group child or group summary notification 46 @Nullable 47 private final View mInnerView; // can be null for GroupNotificationViewHolder 48 @Nullable 49 private final CarNotificationHeaderView mHeaderView; 50 @Nullable 51 private final CarNotificationBodyView mBodyView; 52 @Nullable 53 private final CarNotificationActionsView mActionsView; 54 @Nullable 55 private final ImageButton mDismissButton; 56 private final float mIsSeenAlpha; 57 private final boolean mUseCustomColorForWarningNotification; 58 private final boolean mUseCustomColorForInformationNotification; 59 60 /** 61 * Focus change listener to make the dismiss button transparent or opaque depending on whether 62 * the card view has focus. 63 */ 64 private final ViewTreeObserver.OnGlobalFocusChangeListener mFocusChangeListener; 65 66 /** 67 * Whether to hide the dismiss button. If the bound {@link AlertEntry} is dismissible, a dismiss 68 * button will normally be shown when card view has focus. If this field is true, no dismiss 69 * button will be shown. This is the case for the group summary notification in a collapsed 70 * group. 71 */ 72 private boolean mHideDismissButton; 73 private boolean mUseLauncherIcon; 74 75 @ColorInt 76 private final int mDefaultBackgroundColor; 77 @ColorInt 78 private final int mDefaultCarAccentColor; 79 @ColorInt 80 private final int mCustomInformationBackgroundColor; 81 @ColorInt 82 private final int mCustomInformationPrimaryColor; 83 @ColorInt 84 private final int mCustomInformationSecondaryColor; 85 @ColorInt 86 private final int mCustomWarningBackgroundColor; 87 @ColorInt 88 private final int mCustomWarningPrimaryColor; 89 @ColorInt 90 private final int mCustomWarningSecondaryColor; 91 @ColorInt 92 private int mDefaultPrimaryForegroundColor; 93 @ColorInt 94 private int mDefaultSecondaryForegroundColor; 95 @ColorInt 96 private int mCalculatedPrimaryForegroundColor; 97 @ColorInt 98 private int mCalculatedSecondaryForegroundColor; 99 @ColorInt 100 private int mSmallIconColor; 101 @ColorInt 102 private int mBackgroundColor; 103 104 private AlertEntry mAlertEntry; 105 private boolean mIsAnimating; 106 private boolean mHasColor; 107 private boolean mIsColorized; 108 private boolean mEnableCardBackgroundColorForCategoryNavigation; 109 private boolean mEnableCardBackgroundColorForSystemApp; 110 private boolean mEnableSmallIconAccentColor; 111 private boolean mAlwaysShowDismissButton; 112 private boolean mIsSeen; 113 114 /** 115 * Tracks if the foreground colors have been calculated for the binding of the view holder. 116 * The colors should only be calculated once per binding. 117 **/ 118 private boolean mInitializedColors; 119 CarNotificationBaseViewHolder(View itemView, NotificationClickHandlerFactory clickHandlerFactory)120 CarNotificationBaseViewHolder(View itemView, 121 NotificationClickHandlerFactory clickHandlerFactory) { 122 super(itemView); 123 mContext = itemView.getContext(); 124 mClickHandlerFactory = clickHandlerFactory; 125 mCardView = itemView.findViewById(R.id.card_view); 126 mInnerView = itemView.findViewById(R.id.inner_template_view); 127 mHeaderView = itemView.findViewById(R.id.notification_header); 128 mBodyView = itemView.findViewById(R.id.notification_body); 129 mActionsView = itemView.findViewById(R.id.notification_actions); 130 mDismissButton = itemView.findViewById(R.id.dismiss_button); 131 mAlwaysShowDismissButton = mContext.getResources().getBoolean( 132 R.bool.config_alwaysShowNotificationDismissButton); 133 mUseLauncherIcon = mContext.getResources().getBoolean(R.bool.config_useLauncherIcon); 134 mFocusChangeListener = (oldFocus, newFocus) -> { 135 if (mDismissButton != null && !mAlwaysShowDismissButton) { 136 // The dismiss button should only be visible when the focus is on this notification 137 // or within it. Use alpha rather than visibility so that focus can move up to the 138 // previous notification's dismiss button when action buttons are not present. 139 mDismissButton.setImageAlpha(itemView.hasFocus() ? 255 : 0); 140 } 141 }; 142 mDefaultBackgroundColor = mContext.getColor(R.color.notification_background_color); 143 mDefaultCarAccentColor = NotificationUtils.getAttrColor(mContext, 144 android.R.attr.colorAccent); 145 mDefaultPrimaryForegroundColor = mContext.getColor(R.color.primary_text_color); 146 mDefaultSecondaryForegroundColor = mContext.getColor(R.color.secondary_text_color); 147 mCustomInformationBackgroundColor = mContext.getColor(R.color.information_background_color); 148 mCustomInformationPrimaryColor = mContext.getColor(R.color.information_primary_text_color); 149 mCustomInformationSecondaryColor = mContext.getColor( 150 R.color.information_secondary_text_color); 151 mCustomWarningBackgroundColor = mContext.getColor(R.color.warning_background_color); 152 mCustomWarningPrimaryColor = mContext.getColor(R.color.warning_primary_text_color); 153 mCustomWarningSecondaryColor = mContext.getColor(R.color.warning_secondary_text_color); 154 mEnableCardBackgroundColorForCategoryNavigation = 155 mContext.getResources().getBoolean( 156 R.bool.config_enableCardBackgroundColorForCategoryNavigation); 157 mEnableCardBackgroundColorForSystemApp = 158 mContext.getResources().getBoolean( 159 R.bool.config_enableCardBackgroundColorForSystemApp); 160 mEnableSmallIconAccentColor = 161 mContext.getResources().getBoolean(R.bool.config_enableSmallIconAccentColor); 162 mIsSeenAlpha = mContext.getResources().getFloat(R.dimen.config_olderNotificationsAlpha); 163 mUseCustomColorForWarningNotification = mContext.getResources().getBoolean( 164 R.color.warning_background_color); 165 mUseCustomColorForInformationNotification = mContext.getResources().getBoolean( 166 R.color.information_background_color); 167 } 168 169 /** 170 * Binds a {@link AlertEntry} to a notification template. Base class sets the 171 * clicking event for the card view and calls recycling methods. 172 * 173 * @param alertEntry the notification to be bound. 174 * @param isInGroup whether this notification is part of a grouped notification. 175 */ 176 @CallSuper bind(AlertEntry alertEntry, boolean isInGroup, boolean isHeadsUp, boolean isSeen)177 public void bind(AlertEntry alertEntry, boolean isInGroup, boolean isHeadsUp, boolean isSeen) { 178 reset(); 179 mIsSeen = isSeen; 180 mAlertEntry = alertEntry; 181 182 if (isInGroup) { 183 mInnerView.setBackgroundColor(mDefaultBackgroundColor); 184 mInnerView.setOnClickListener(mClickHandlerFactory.getClickHandler(alertEntry)); 185 } else if (mCardView != null) { 186 mCardView.setOnClickListener(mClickHandlerFactory.getClickHandler(alertEntry)); 187 } 188 updateDismissButton(alertEntry, isHeadsUp); 189 190 bindCardView(mCardView, isInGroup); 191 bindHeader(mHeaderView, isInGroup); 192 bindBody(mBodyView, isInGroup); 193 194 itemView.setAlpha(mIsSeen ? mIsSeenAlpha : 1); 195 } 196 getContext()197 protected final Context getContext() { 198 return mContext; 199 } 200 201 /** 202 * Binds a {@link AlertEntry} to a notification template's card. 203 * 204 * @param cardView the CardView the notification should be bound to. 205 * @param isInGroup whether this notification is part of a grouped notification. 206 */ bindCardView(CardView cardView, boolean isInGroup)207 void bindCardView(CardView cardView, boolean isInGroup) { 208 initializeColors(isInGroup); 209 210 if (cardView == null) { 211 return; 212 } 213 214 if (canChangeCardBackgroundColor() && ((mHasColor && mIsColorized) 215 || mUseCustomColorForInformationNotification 216 || mUseCustomColorForWarningNotification) && !isInGroup) { 217 cardView.setCardBackgroundColor(mBackgroundColor); 218 } 219 } 220 221 /** 222 * Binds a {@link AlertEntry} to a notification template's header. 223 * 224 * @param headerView the CarNotificationHeaderView the notification should be bound to. 225 * @param isInGroup whether this notification is part of a grouped notification. 226 */ bindHeader(CarNotificationHeaderView headerView, boolean isInGroup)227 void bindHeader(CarNotificationHeaderView headerView, boolean isInGroup) { 228 if (headerView == null) return; 229 initializeColors(isInGroup); 230 231 headerView.setSmallIconColor(mSmallIconColor); 232 headerView.setHeaderTextColor(mCalculatedPrimaryForegroundColor); 233 } 234 235 /** 236 * Binds a {@link AlertEntry} to a notification template's body. 237 * 238 * @param bodyView the CarNotificationBodyView the notification should be bound to. 239 * @param isInGroup whether this notification is part of a grouped notification. 240 */ bindBody(CarNotificationBodyView bodyView, boolean isInGroup)241 void bindBody(CarNotificationBodyView bodyView, 242 boolean isInGroup) { 243 if (bodyView == null) return; 244 initializeColors(isInGroup); 245 246 bodyView.setPrimaryTextColor(mCalculatedPrimaryForegroundColor); 247 bodyView.setSecondaryTextColor(mCalculatedSecondaryForegroundColor); 248 bodyView.setTimeTextColor(mCalculatedPrimaryForegroundColor); 249 } 250 initializeColors(boolean isInGroup)251 private void initializeColors(boolean isInGroup) { 252 if (mInitializedColors) return; 253 Notification notification = getAlertEntry().getNotification(); 254 255 mHasColor = notification.color != Notification.COLOR_DEFAULT; 256 mIsColorized = notification.extras.getBoolean(Notification.EXTRA_COLORIZED, false); 257 258 mCalculatedPrimaryForegroundColor = mDefaultPrimaryForegroundColor; 259 mCalculatedSecondaryForegroundColor = mDefaultSecondaryForegroundColor; 260 if (canChangeCardBackgroundColor() && !isInGroup) { 261 if (mHasColor && mIsColorized) { 262 mBackgroundColor = notification.color; 263 } 264 boolean isWarningCategory = Notification.CATEGORY_CAR_WARNING.equals( 265 notification.category); 266 boolean isInformationCategory = Notification.CATEGORY_CAR_INFORMATION.equals( 267 notification.category); 268 if (mUseCustomColorForWarningNotification && isWarningCategory) { 269 mBackgroundColor = mCustomWarningBackgroundColor; 270 mDefaultPrimaryForegroundColor = mCustomWarningPrimaryColor; 271 mDefaultSecondaryForegroundColor = mCustomWarningSecondaryColor; 272 } else if (mUseCustomColorForInformationNotification && isInformationCategory) { 273 mBackgroundColor = mCustomInformationBackgroundColor; 274 mDefaultPrimaryForegroundColor = mCustomInformationPrimaryColor; 275 mDefaultSecondaryForegroundColor = mCustomInformationSecondaryColor; 276 } 277 mCalculatedPrimaryForegroundColor = NotificationUtils.resolveContrastColor( 278 mDefaultPrimaryForegroundColor, mBackgroundColor); 279 mCalculatedSecondaryForegroundColor = NotificationUtils.resolveContrastColor( 280 mDefaultSecondaryForegroundColor, mBackgroundColor); 281 } 282 mSmallIconColor = 283 hasCustomBackgroundColor() ? mCalculatedPrimaryForegroundColor : getAccentColor(); 284 285 mInitializedColors = true; 286 } 287 288 canChangeCardBackgroundColor()289 private boolean canChangeCardBackgroundColor() { 290 Notification notification = getAlertEntry().getNotification(); 291 292 boolean isSystemApp = mEnableCardBackgroundColorForSystemApp && 293 NotificationUtils.isSystemApp(mContext, getAlertEntry().getStatusBarNotification()); 294 boolean isSignedWithPlatformKey = NotificationUtils.isSignedWithPlatformKey(mContext, 295 getAlertEntry().getStatusBarNotification()); 296 boolean isNavigationCategory = mEnableCardBackgroundColorForCategoryNavigation 297 && Notification.CATEGORY_NAVIGATION.equals(notification.category); 298 boolean isWarningCategory = mUseCustomColorForWarningNotification 299 && Notification.CATEGORY_CAR_WARNING.equals(notification.category); 300 boolean isInformationCategory = mUseCustomColorForInformationNotification 301 && Notification.CATEGORY_CAR_INFORMATION.equals(notification.category); 302 return isSystemApp || isNavigationCategory || isSignedWithPlatformKey || isWarningCategory 303 || isInformationCategory; 304 } 305 306 /** 307 * Returns the accent color for this notification. 308 */ 309 @ColorInt getAccentColor()310 int getAccentColor() { 311 312 int color = getAlertEntry().getNotification().color; 313 if (mEnableSmallIconAccentColor && color != Notification.COLOR_DEFAULT) { 314 return color; 315 } 316 return mDefaultCarAccentColor; 317 } 318 319 /** 320 * Returns whether this card has a custom background color. 321 */ hasCustomBackgroundColor()322 boolean hasCustomBackgroundColor() { 323 return mBackgroundColor != mDefaultBackgroundColor; 324 } 325 326 /** 327 * Child view holders should override and call super to recycle any custom component 328 * that's not handled by {@link CarNotificationHeaderView}, {@link CarNotificationBodyView} and 329 * {@link CarNotificationActionsView}. 330 * Note that any child class that is not calling {@link #bind} has to call this method directly. 331 */ 332 @CallSuper reset()333 void reset() { 334 mAlertEntry = null; 335 mBackgroundColor = mDefaultBackgroundColor; 336 mInitializedColors = false; 337 mIsSeen = false; 338 339 itemView.setTranslationX(0); 340 itemView.setAlpha(1f); 341 342 if (mCardView != null) { 343 mCardView.setOnClickListener(null); 344 mCardView.setCardBackgroundColor(mDefaultBackgroundColor); 345 } 346 347 if (mBodyView != null) { 348 mBodyView.reset(); 349 } 350 351 if (mActionsView != null) { 352 mActionsView.reset(); 353 } 354 355 itemView.getViewTreeObserver().removeOnGlobalFocusChangeListener(mFocusChangeListener); 356 if (mDismissButton != null) { 357 if (!mAlwaysShowDismissButton) { 358 mDismissButton.setImageAlpha(0); 359 } 360 mDismissButton.setVisibility(View.GONE); 361 } 362 } 363 364 /** 365 * Returns the current {@link AlertEntry} that this view holder is holding. 366 * Note that any child class that is not calling {@link #bind} has to override this method. 367 */ getAlertEntry()368 public AlertEntry getAlertEntry() { 369 return mAlertEntry; 370 } 371 372 /** 373 * Returns true if the panel notification contained in this view holder can be swiped away. 374 */ isDismissible()375 public boolean isDismissible() { 376 if (mAlertEntry == null) { 377 return true; 378 } 379 380 return (getAlertEntry().getNotification().flags 381 & (Notification.FLAG_FOREGROUND_SERVICE | Notification.FLAG_ONGOING_EVENT)) == 0; 382 } 383 updateDismissButton(AlertEntry alertEntry, boolean isHeadsUp)384 void updateDismissButton(AlertEntry alertEntry, boolean isHeadsUp) { 385 if (mDismissButton == null) { 386 return; 387 } 388 // isDismissible only applies to panel notifications, not HUNs 389 if ((!isHeadsUp && !isDismissible()) || mHideDismissButton) { 390 hideDismissButton(); 391 return; 392 } 393 if (!mAlwaysShowDismissButton) { 394 mDismissButton.setImageAlpha(0); 395 } 396 mDismissButton.setVisibility(View.VISIBLE); 397 if (!isHeadsUp) { 398 // Only set the click listener here for panel notifications - HUNs already have one 399 // provided from the CarHeadsUpNotificationManager 400 mDismissButton.setOnClickListener(getDismissHandler(alertEntry)); 401 } 402 itemView.getViewTreeObserver().addOnGlobalFocusChangeListener(mFocusChangeListener); 403 } 404 hideDismissButton()405 void hideDismissButton() { 406 if (mDismissButton == null) { 407 return; 408 } 409 mDismissButton.setVisibility(View.GONE); 410 itemView.getViewTreeObserver().removeOnGlobalFocusChangeListener(mFocusChangeListener); 411 } 412 413 /** 414 * Returns the TranslationX of the ItemView. 415 */ getSwipeTranslationX()416 public float getSwipeTranslationX() { 417 return itemView.getTranslationX(); 418 } 419 420 /** 421 * Sets the TranslationX of the ItemView. 422 */ setSwipeTranslationX(float translationX)423 public void setSwipeTranslationX(float translationX) { 424 itemView.setTranslationX(translationX); 425 } 426 427 /** 428 * Sets the alpha of the ItemView. 429 */ setSwipeAlpha(float alpha)430 public void setSwipeAlpha(float alpha) { 431 itemView.setAlpha(alpha); 432 } 433 434 /** 435 * Sets whether this view holder has ongoing animation. 436 */ setIsAnimating(boolean animating)437 public void setIsAnimating(boolean animating) { 438 mIsAnimating = animating; 439 } 440 441 /** 442 * Returns true if this view holder has ongoing animation. 443 */ isAnimating()444 public boolean isAnimating() { 445 return mIsAnimating; 446 } 447 448 /** 449 * Returns is seen alpha if is seen is {@code true}. 450 */ getAlpha()451 public float getAlpha() { 452 return mIsSeen ? mIsSeenAlpha : 1; 453 } 454 455 @VisibleForTesting shouldHideDismissButton()456 public boolean shouldHideDismissButton() { 457 return mHideDismissButton; 458 } 459 setHideDismissButton(boolean hideDismissButton)460 public void setHideDismissButton(boolean hideDismissButton) { 461 mHideDismissButton = hideDismissButton; 462 } 463 getDismissHandler(AlertEntry alertEntry)464 View.OnClickListener getDismissHandler(AlertEntry alertEntry) { 465 return mClickHandlerFactory.getDismissHandler(alertEntry); 466 } 467 } 468