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