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