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