• 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 
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