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 
26 import androidx.cardview.widget.CardView;
27 import androidx.recyclerview.widget.RecyclerView;
28 
29 import com.android.car.notification.AlertEntry;
30 import com.android.car.notification.NotificationClickHandlerFactory;
31 import com.android.car.notification.NotificationUtils;
32 import com.android.car.notification.R;
33 
34 /**
35  * The base view holder class that all template view holders should extend.
36  */
37 public abstract class CarNotificationBaseViewHolder extends RecyclerView.ViewHolder {
38     private final Context mContext;
39     private final NotificationClickHandlerFactory mClickHandlerFactory;
40 
41     @Nullable
42     private final CardView mCardView; // can be null for group child or group summary notification
43     @Nullable
44     private final View mInnerView; // can be null for GroupNotificationViewHolder
45     @Nullable
46     private final CarNotificationHeaderView mHeaderView;
47     @Nullable
48     private final CarNotificationBodyView mBodyView;
49     @Nullable
50     private final CarNotificationActionsView mActionsView;
51 
52     @ColorInt
53     private final int mDefaultBackgroundColor;
54     @ColorInt
55     private final int mDefaultCarAccentColor;
56     @ColorInt
57     private final int mDefaultPrimaryForegroundColor;
58     @ColorInt
59     private final int mDefaultSecondaryForegroundColor;
60     @ColorInt
61     private int mCalculatedPrimaryForegroundColor;
62     @ColorInt
63     private int mCalculatedSecondaryForegroundColor;
64     @ColorInt
65     private int mSmallIconColor;
66     @ColorInt
67     private int mBackgroundColor;
68 
69     private AlertEntry mAlertEntry;
70     private boolean mIsAnimating;
71     private boolean mHasColor;
72     private boolean mIsColorized;
73     private boolean mEnableCardBackgroundColorForCategoryNavigation;
74     private boolean mEnableCardBackgroundColorForSystemApp;
75     private boolean mEnableSmallIconAccentColor;
76 
77     /**
78      * Tracks if the foreground colors have been calculated for the binding of the view holder.
79      * The colors should only be calculated once per binding.
80      **/
81     private boolean mInitializedColors;
82 
CarNotificationBaseViewHolder(View itemView, NotificationClickHandlerFactory clickHandlerFactory)83     CarNotificationBaseViewHolder(View itemView,
84             NotificationClickHandlerFactory clickHandlerFactory) {
85         super(itemView);
86         mContext = itemView.getContext();
87         mClickHandlerFactory = clickHandlerFactory;
88         mCardView = itemView.findViewById(R.id.card_view);
89         mInnerView = itemView.findViewById(R.id.inner_template_view);
90         mHeaderView = itemView.findViewById(R.id.notification_header);
91         mBodyView = itemView.findViewById(R.id.notification_body);
92         mActionsView = itemView.findViewById(R.id.notification_actions);
93         mDefaultBackgroundColor = NotificationUtils.getAttrColor(mContext,
94                 android.R.attr.colorPrimary);
95         mDefaultCarAccentColor = NotificationUtils.getAttrColor(mContext,
96                 android.R.attr.colorAccent);
97         mDefaultPrimaryForegroundColor = mContext.getColor(R.color.primary_text_color);
98         mDefaultSecondaryForegroundColor = mContext.getColor(R.color.secondary_text_color);
99         mEnableCardBackgroundColorForCategoryNavigation =
100                 mContext.getResources().getBoolean(
101                         R.bool.config_enableCardBackgroundColorForCategoryNavigation);
102         mEnableCardBackgroundColorForSystemApp =
103                 mContext.getResources().getBoolean(
104                         R.bool.config_enableCardBackgroundColorForSystemApp);
105         mEnableSmallIconAccentColor =
106                 mContext.getResources().getBoolean(R.bool.config_enableSmallIconAccentColor);
107     }
108 
109     /**
110      * Binds a {@link AlertEntry} to a notification template. Base class sets the
111      * clicking event for the card view and calls recycling methods.
112      *
113      * @param alertEntry the notification to be bound.
114      * @param isInGroup whether this notification is part of a grouped notification.
115      */
116     @CallSuper
bind(AlertEntry alertEntry, boolean isInGroup, boolean isHeadsUp)117     public void bind(AlertEntry alertEntry, boolean isInGroup, boolean isHeadsUp) {
118         reset();
119         mAlertEntry = alertEntry;
120 
121         if (isInGroup) {
122             mInnerView.setBackgroundColor(mDefaultBackgroundColor);
123             mInnerView.setOnClickListener(mClickHandlerFactory.getClickHandler(alertEntry));
124         } else if (mCardView != null) {
125             mCardView.setOnClickListener(mClickHandlerFactory.getClickHandler(alertEntry));
126         }
127 
128         bindCardView(mCardView, isInGroup);
129         bindHeader(mHeaderView, isInGroup);
130         bindBody(mBodyView, isInGroup);
131     }
132 
133     /**
134      * Binds a {@link AlertEntry} to a notification template's card.
135      *
136      * @param cardView the CardView the notification should be bound to.
137      * @param isInGroup whether this notification is part of a grouped notification.
138      */
bindCardView(CardView cardView, boolean isInGroup)139     void bindCardView(CardView cardView, boolean isInGroup) {
140         initializeColors(isInGroup);
141 
142         if (cardView == null) {
143             return;
144         }
145 
146         if (canChangeCardBackgroundColor() && mHasColor && mIsColorized && !isInGroup) {
147             cardView.setCardBackgroundColor(mBackgroundColor);
148         }
149     }
150 
151     /**
152      * Binds a {@link AlertEntry} to a notification template's header.
153      *
154      * @param headerView the CarNotificationHeaderView the notification should be bound to.
155      * @param isInGroup whether this notification is part of a grouped notification.
156      */
bindHeader(CarNotificationHeaderView headerView, boolean isInGroup)157     void bindHeader(CarNotificationHeaderView headerView, boolean isInGroup) {
158         if (headerView == null) return;
159         initializeColors(isInGroup);
160 
161         headerView.setSmallIconColor(mSmallIconColor);
162         headerView.setHeaderTextColor(mCalculatedPrimaryForegroundColor);
163         headerView.setTimeTextColor(mCalculatedPrimaryForegroundColor);
164     }
165 
166     /**
167      * Binds a {@link AlertEntry} to a notification template's body.
168      *
169      * @param bodyView the CarNotificationBodyView the notification should be bound to.
170      * @param isInGroup whether this notification is part of a grouped notification.
171      */
bindBody(CarNotificationBodyView bodyView, boolean isInGroup)172     void bindBody(CarNotificationBodyView bodyView,
173             boolean isInGroup) {
174         if (bodyView == null) return;
175         initializeColors(isInGroup);
176 
177         bodyView.setPrimaryTextColor(mCalculatedPrimaryForegroundColor);
178         bodyView.setSecondaryTextColor(mCalculatedSecondaryForegroundColor);
179     }
180 
initializeColors(boolean isInGroup)181     private void initializeColors(boolean isInGroup) {
182         if (mInitializedColors) return;
183         Notification notification = getAlertEntry().getNotification();
184 
185         mHasColor = notification.color != Notification.COLOR_DEFAULT;
186         mIsColorized = notification.extras.getBoolean(Notification.EXTRA_COLORIZED, false);
187 
188         mCalculatedPrimaryForegroundColor = mDefaultPrimaryForegroundColor;
189         mCalculatedSecondaryForegroundColor = mDefaultSecondaryForegroundColor;
190         if (canChangeCardBackgroundColor() && mHasColor && mIsColorized && !isInGroup) {
191             mBackgroundColor = notification.color;
192             mCalculatedPrimaryForegroundColor = NotificationUtils.resolveContrastColor(
193                     mDefaultPrimaryForegroundColor, mBackgroundColor);
194             mCalculatedSecondaryForegroundColor = NotificationUtils.resolveContrastColor(
195                     mDefaultSecondaryForegroundColor, mBackgroundColor);
196         }
197         mSmallIconColor =
198                 hasCustomBackgroundColor() ? mCalculatedPrimaryForegroundColor : getAccentColor();
199 
200         mInitializedColors = true;
201     }
202 
203 
canChangeCardBackgroundColor()204     private boolean canChangeCardBackgroundColor() {
205         Notification notification = getAlertEntry().getNotification();
206 
207         boolean isSystemApp = mEnableCardBackgroundColorForSystemApp &&
208                 NotificationUtils.isSystemApp(mContext, getAlertEntry().getStatusBarNotification());
209         boolean isSignedWithPlatformKey = NotificationUtils.isSignedWithPlatformKey(mContext,
210                 getAlertEntry().getStatusBarNotification());
211         boolean isNavigationCategory = mEnableCardBackgroundColorForCategoryNavigation &&
212                 Notification.CATEGORY_NAVIGATION.equals(notification.category);
213         return isSystemApp || isNavigationCategory || isSignedWithPlatformKey;
214     }
215 
216     /**
217      * Returns the accent color for this notification.
218      */
219     @ColorInt
getAccentColor()220     int getAccentColor() {
221 
222         int color = getAlertEntry().getNotification().color;
223         if (mEnableSmallIconAccentColor && color != Notification.COLOR_DEFAULT) {
224             return color;
225         }
226         return mDefaultCarAccentColor;
227     }
228 
229     /**
230      * Returns whether this card has a custom background color.
231      */
hasCustomBackgroundColor()232     boolean hasCustomBackgroundColor() {
233         return mBackgroundColor != mDefaultBackgroundColor;
234     }
235 
236     /**
237      * Child view holders should override and call super to recycle any custom component
238      * that's not handled by {@link CarNotificationHeaderView}, {@link CarNotificationBodyView} and
239      * {@link CarNotificationActionsView}.
240      * Note that any child class that is not calling {@link #bind} has to call this method directly.
241      */
242     @CallSuper
reset()243     void reset() {
244         mAlertEntry = null;
245         mBackgroundColor = mDefaultBackgroundColor;
246         mInitializedColors = false;
247 
248         itemView.setTranslationX(0);
249         itemView.setAlpha(1f);
250 
251         if (mCardView != null) {
252             mCardView.setOnClickListener(null);
253             mCardView.setCardBackgroundColor(mDefaultBackgroundColor);
254         }
255 
256         if (mHeaderView != null) {
257             mHeaderView.reset();
258         }
259 
260         if (mBodyView != null) {
261             mBodyView.reset();
262         }
263 
264         if (mActionsView != null) {
265             mActionsView.reset();
266         }
267     }
268 
269     /**
270      * Returns the current {@link AlertEntry} that this view holder is holding.
271      * Note that any child class that is not calling {@link #bind} has to override this method.
272      */
getAlertEntry()273     public AlertEntry getAlertEntry() {
274         return mAlertEntry;
275     }
276 
277     /**
278      * Returns true if the notification contained in this view holder can be swiped away.
279      */
isDismissible()280     public boolean isDismissible() {
281         if (mAlertEntry == null) {
282             return true;
283         }
284 
285         return (getAlertEntry().getNotification().flags
286                 & (Notification.FLAG_FOREGROUND_SERVICE | Notification.FLAG_ONGOING_EVENT)) == 0;
287     }
288 
289     /**
290      * Returns the TranslationX of the ItemView.
291      */
getSwipeTranslationX()292     public float getSwipeTranslationX() {
293         return itemView.getTranslationX();
294     }
295 
296     /**
297      * Sets the TranslationX of the ItemView.
298      */
setSwipeTranslationX(float translationX)299     public void setSwipeTranslationX(float translationX) {
300         itemView.setTranslationX(translationX);
301     }
302 
303     /**
304      * Sets the alpha of the ItemView.
305      */
setSwipeAlpha(float alpha)306     public void setSwipeAlpha(float alpha) {
307         itemView.setAlpha(alpha);
308     }
309 
310     /**
311      * Sets whether this view holder has ongoing animation.
312      */
setIsAnimating(boolean animating)313     public void setIsAnimating(boolean animating) {
314         mIsAnimating = animating;
315     }
316 
317     /**
318      * Returns true if this view holder has ongoing animation.
319      */
isAnimating()320     public boolean isAnimating() {
321         return mIsAnimating;
322     }
323 }
324