• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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