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