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