• 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 package com.android.car.notification;
17 
18 import android.annotation.NonNull;
19 import android.app.Notification;
20 import android.car.drivingstate.CarUxRestrictions;
21 import android.content.Context;
22 import android.os.Build;
23 import android.os.Bundle;
24 import android.util.Log;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.ViewGroup;
28 
29 import androidx.annotation.Nullable;
30 import androidx.recyclerview.widget.DiffUtil;
31 import androidx.recyclerview.widget.LinearLayoutManager;
32 import androidx.recyclerview.widget.RecyclerView;
33 
34 import com.android.car.notification.template.CarNotificationBaseViewHolder;
35 import com.android.car.notification.template.CarNotificationFooterViewHolder;
36 import com.android.car.notification.template.CarNotificationHeaderViewHolder;
37 import com.android.car.notification.template.CarNotificationOlderViewHolder;
38 import com.android.car.notification.template.CarNotificationRecentsViewHolder;
39 import com.android.car.notification.template.GroupNotificationViewHolder;
40 import com.android.car.notification.template.GroupSummaryNotificationViewHolder;
41 import com.android.car.notification.template.MessageNotificationViewHolder;
42 import com.android.car.ui.recyclerview.ContentLimitingAdapter;
43 
44 import java.util.ArrayList;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.stream.Collectors;
49 
50 /**
51  * Notification data adapter that binds a notification to the corresponding view.
52  */
53 public class CarNotificationViewAdapter extends ContentLimitingAdapter<RecyclerView.ViewHolder>
54         implements PreprocessingManager.CallStateListener {
55     private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG;
56     private static final String TAG = "CarNotificationAdapter";
57     private static final int ID_HEADER = 0;
58     private static final int ID_RECENT_HEADER = 1;
59     private static final int ID_OLDER_HEADER = 2;
60     private static final int ID_FOOTER = 3;
61 
62     private final Context mContext;
63     private final LayoutInflater mInflater;
64     private final int mMaxNumberGroupChildrenShown;
65     private final boolean mIsGroupNotificationAdapter;
66     private final boolean mShowRecentsAndOlderHeaders;
67 
68     // book keeping expanded notification groups
69     private final List<ExpandedNotification> mExpandedNotifications = new ArrayList<>();
70     private final CarNotificationItemController mNotificationItemController;
71 
72     private List<NotificationGroup> mNotifications = new ArrayList<>();
73     private Map<String, Integer> mGroupKeyToCountMap = new HashMap<>();
74     private LinearLayoutManager mLayoutManager;
75     private RecyclerView.RecycledViewPool mViewPool;
76     private CarUxRestrictions mCarUxRestrictions;
77     private NotificationClickHandlerFactory mClickHandlerFactory;
78     private NotificationDataManager mNotificationDataManager;
79     private boolean mIsInCall;
80     private boolean mHasHeaderAndFooter;
81     private boolean mHasUnseenNotifications;
82     private boolean mHasSeenNotifications;
83     private int mMaxItems = ContentLimitingAdapter.UNLIMITED;
84 
85     /**
86      * Constructor for a notification adapter.
87      * Can be used both by the root notification list view, or a grouped notification view.
88      *
89      * @param context the context for resources and inflating views
90      * @param isGroupNotificationAdapter true if this adapter is used by a grouped notification view
91      * @param notificationItemController shared logic to control notification items.
92      */
CarNotificationViewAdapter(Context context, boolean isGroupNotificationAdapter, @Nullable CarNotificationItemController notificationItemController)93     public CarNotificationViewAdapter(Context context, boolean isGroupNotificationAdapter,
94             @Nullable CarNotificationItemController notificationItemController) {
95         mContext = context;
96         mInflater = LayoutInflater.from(context);
97         mMaxNumberGroupChildrenShown =
98                 mContext.getResources().getInteger(R.integer.max_group_children_number);
99         mShowRecentsAndOlderHeaders =
100                 mContext.getResources().getBoolean(R.bool.config_showRecentAndOldHeaders);
101         mIsGroupNotificationAdapter = isGroupNotificationAdapter;
102         mNotificationItemController = notificationItemController;
103         mNotificationDataManager = NotificationDataManager.getInstance();
104         setHasStableIds(true);
105         if (!mIsGroupNotificationAdapter) {
106             mViewPool = new RecyclerView.RecycledViewPool();
107         }
108 
109         PreprocessingManager.getInstance(context).addCallStateListener(this::onCallStateChanged);
110     }
111 
112     @Override
onAttachedToRecyclerView(@onNull RecyclerView recyclerView)113     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
114         super.onAttachedToRecyclerView(recyclerView);
115         mLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
116     }
117 
118     @Override
onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)119     public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
120         super.onDetachedFromRecyclerView(recyclerView);
121         mLayoutManager = null;
122     }
123 
124     @Override
onCreateViewHolderImpl(@onNull ViewGroup parent, int viewType)125     public RecyclerView.ViewHolder onCreateViewHolderImpl(@NonNull ViewGroup parent, int viewType) {
126         RecyclerView.ViewHolder viewHolder;
127         View view;
128         switch (viewType) {
129             case NotificationViewType.HEADER:
130                 view = mInflater.inflate(R.layout.notification_header_template, parent, false);
131                 viewHolder = new CarNotificationHeaderViewHolder(mContext, view,
132                         mNotificationItemController, mClickHandlerFactory);
133                 break;
134             case NotificationViewType.FOOTER:
135                 view = mInflater.inflate(R.layout.notification_footer_template, parent, false);
136                 viewHolder = new CarNotificationFooterViewHolder(mContext, view,
137                         mNotificationItemController, mClickHandlerFactory);
138                 break;
139             case NotificationViewType.RECENTS:
140                 view = mInflater.inflate(R.layout.notification_recents_template, parent, false);
141                 viewHolder = new CarNotificationRecentsViewHolder(mContext, view,
142                         mNotificationItemController);
143                 break;
144             case NotificationViewType.OLDER:
145                 view = mInflater.inflate(R.layout.notification_older_template, parent, false);
146                 viewHolder = new CarNotificationOlderViewHolder(mContext, view,
147                         mNotificationItemController);
148                 break;
149             default:
150                 CarNotificationTypeItem carNotificationTypeItem = CarNotificationTypeItem.of(
151                         viewType);
152                 view = mInflater.inflate(
153                         carNotificationTypeItem.getNotificationCenterTemplate(), parent, false);
154                 viewHolder = carNotificationTypeItem.getViewHolder(view, mClickHandlerFactory);
155         }
156 
157         return viewHolder;
158     }
159 
160     @Override
onBindViewHolderImpl(RecyclerView.ViewHolder holder, int position)161     public void onBindViewHolderImpl(RecyclerView.ViewHolder holder, int position) {
162         NotificationGroup notificationGroup = mNotifications.get(position);
163 
164         int viewType = holder.getItemViewType();
165         switch (viewType) {
166             case NotificationViewType.HEADER:
167                 ((CarNotificationHeaderViewHolder) holder).bind(hasNotifications());
168                 return;
169             case NotificationViewType.FOOTER:
170                 ((CarNotificationFooterViewHolder) holder).bind(hasNotifications());
171                 return;
172             case NotificationViewType.RECENTS:
173                 ((CarNotificationRecentsViewHolder) holder).bind(mHasUnseenNotifications);
174                 return;
175             case NotificationViewType.OLDER:
176                 ((CarNotificationOlderViewHolder) holder)
177                         .bind(mHasSeenNotifications, !mHasUnseenNotifications);
178                 return;
179             case NotificationViewType.GROUP:
180                 ((GroupNotificationViewHolder) holder)
181                         .bind(notificationGroup, this, /* isExpanded= */
182                                 isExpanded(notificationGroup.getGroupKey(),
183                                         notificationGroup.isSeen()));
184                 return;
185             case NotificationViewType.GROUP_SUMMARY:
186                 ((CarNotificationBaseViewHolder) holder).setHideDismissButton(true);
187                 ((GroupSummaryNotificationViewHolder) holder).bind(notificationGroup);
188                 return;
189         }
190 
191         CarNotificationTypeItem carNotificationTypeItem = CarNotificationTypeItem.of(viewType);
192         AlertEntry alertEntry = notificationGroup.getSingleNotification();
193 
194         if (shouldRestrictMessagePreview() && (viewType == NotificationViewType.MESSAGE
195                 || viewType == NotificationViewType.MESSAGE_IN_GROUP)) {
196             ((MessageNotificationViewHolder) holder)
197                     .bindRestricted(alertEntry, /* isInGroup= */ false, /* isHeadsUp= */false);
198         } else {
199             carNotificationTypeItem.bind(alertEntry, false, (CarNotificationBaseViewHolder) holder);
200         }
201     }
202 
203     @Override
getItemViewTypeImpl(int position)204     public int getItemViewTypeImpl(int position) {
205         NotificationGroup notificationGroup = mNotifications.get(position);
206         if (notificationGroup.isHeader()) {
207             return NotificationViewType.HEADER;
208         }
209 
210         if (notificationGroup.isFooter()) {
211             return NotificationViewType.FOOTER;
212         }
213 
214         if (notificationGroup.isRecentsHeader()) {
215             return NotificationViewType.RECENTS;
216         }
217 
218         if (notificationGroup.isOlderHeader()) {
219             return NotificationViewType.OLDER;
220         }
221 
222         ExpandedNotification expandedNotification =
223                 new ExpandedNotification(notificationGroup.getGroupKey(),
224                         notificationGroup.isSeen());
225         if (notificationGroup.isGroup()) {
226             return NotificationViewType.GROUP;
227         } else if (mExpandedNotifications.contains(expandedNotification)) {
228             // when there are 2 notifications left in the expanded notification and one of them is
229             // removed at that time the item type changes from group to normal and hence the
230             // notification should be removed from expanded notifications.
231             setExpanded(expandedNotification.getKey(), expandedNotification.isExpanded(),
232                     /* isExpanded= */ false);
233         }
234 
235         Notification notification =
236                 notificationGroup.getSingleNotification().getNotification();
237         Bundle extras = notification.extras;
238 
239         String category = notification.category;
240         if (category != null) {
241             switch (category) {
242                 case Notification.CATEGORY_CALL:
243                     return NotificationViewType.CALL;
244                 case Notification.CATEGORY_CAR_EMERGENCY:
245                     return NotificationViewType.CAR_EMERGENCY;
246                 case Notification.CATEGORY_CAR_WARNING:
247                     return NotificationViewType.CAR_WARNING;
248                 case Notification.CATEGORY_CAR_INFORMATION:
249                     return mIsGroupNotificationAdapter
250                             ? NotificationViewType.CAR_INFORMATION_IN_GROUP
251                             : NotificationViewType.CAR_INFORMATION;
252                 case Notification.CATEGORY_MESSAGE:
253                     return mIsGroupNotificationAdapter
254                             ? NotificationViewType.MESSAGE_IN_GROUP : NotificationViewType.MESSAGE;
255                 default:
256                     break;
257             }
258         }
259 
260         // progress
261         int progressMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX);
262         boolean isIndeterminate = extras.getBoolean(
263                 Notification.EXTRA_PROGRESS_INDETERMINATE);
264         boolean hasValidProgress = isIndeterminate || progressMax != 0;
265         boolean isProgress = extras.containsKey(Notification.EXTRA_PROGRESS)
266                 && extras.containsKey(Notification.EXTRA_PROGRESS_MAX)
267                 && hasValidProgress
268                 && !notification.hasCompletedProgress();
269         if (isProgress) {
270             return mIsGroupNotificationAdapter
271                     ? NotificationViewType.PROGRESS_IN_GROUP : NotificationViewType.PROGRESS;
272         }
273 
274         // inbox
275         boolean isInbox = extras.containsKey(Notification.EXTRA_TITLE_BIG)
276                 && extras.containsKey(Notification.EXTRA_SUMMARY_TEXT);
277         if (isInbox) {
278             return mIsGroupNotificationAdapter
279                     ? NotificationViewType.INBOX_IN_GROUP : NotificationViewType.INBOX;
280         }
281 
282         // group summary
283         boolean isGroupSummary = notificationGroup.getChildTitles() != null;
284         if (isGroupSummary) {
285             return NotificationViewType.GROUP_SUMMARY;
286         }
287 
288         // the big text and big picture styles are fallen back to basic template in car
289         // i.e. setting the big text and big picture does not have an effect
290         boolean isBigText = extras.containsKey(Notification.EXTRA_BIG_TEXT);
291         if (isBigText) {
292             Log.i(TAG, "Big text style is not supported as a car notification");
293         }
294         boolean isBigPicture = extras.containsKey(Notification.EXTRA_PICTURE);
295         if (isBigPicture) {
296             Log.i(TAG, "Big picture style is not supported as a car notification");
297         }
298 
299         // basic, big text, big picture
300         return mIsGroupNotificationAdapter
301                 ? NotificationViewType.BASIC_IN_GROUP : NotificationViewType.BASIC;
302     }
303 
304     @Override
getUnrestrictedItemCount()305     public int getUnrestrictedItemCount() {
306         return mNotifications.size();
307     }
308 
309     @Override
setMaxItems(int maxItems)310     public void setMaxItems(int maxItems) {
311         if (maxItems == ContentLimitingAdapter.UNLIMITED
312                 || (!mHasHeaderAndFooter && !mHasUnseenNotifications && !mHasSeenNotifications)) {
313             mMaxItems = maxItems;
314         } else {
315             // Adding to max limit of notifications for each header so that they do not count
316             // towards limit.
317             // Footer is not accounted for since it as the end of the list and it doesn't affect the
318             // limit of notifications above it.
319             mMaxItems = maxItems;
320             if (mHasHeaderAndFooter) {
321                 mMaxItems++;
322             }
323             if (mHasSeenNotifications) {
324                 mMaxItems++;
325             }
326             if (mHasUnseenNotifications) {
327                 mMaxItems++;
328             }
329         }
330         super.setMaxItems(mMaxItems);
331     }
332 
333     @Override
getScrollToPositionWhenRestricted()334     protected int getScrollToPositionWhenRestricted() {
335         if (mLayoutManager == null) {
336             return -1;
337         }
338         int firstItem = mLayoutManager.findFirstVisibleItemPosition();
339         if (firstItem >= getItemCount() - 1) {
340             return getItemCount() - 1;
341         }
342         return -1;
343     }
344 
345     @Override
getItemId(int position)346     public long getItemId(int position) {
347         NotificationGroup notificationGroup = mNotifications.get(position);
348         if (notificationGroup.isHeader()) {
349             return ID_HEADER;
350         }
351         if (mShowRecentsAndOlderHeaders && !mIsGroupNotificationAdapter) {
352             if (notificationGroup.isRecentsHeader()) {
353                 return ID_RECENT_HEADER;
354             }
355             if (notificationGroup.isOlderHeader()) {
356                 return ID_OLDER_HEADER;
357             }
358             if (notificationGroup.isFooter()) {
359                 return ID_FOOTER;
360             }
361         }
362         if (notificationGroup.isFooter()) {
363             // We can use recent header's ID when it isn't being used.
364             return ID_RECENT_HEADER;
365         }
366 
367         String key = notificationGroup.isGroup()
368                 ? notificationGroup.getGroupKey()
369                 : notificationGroup.getSingleNotification().getKey();
370 
371         if (mShowRecentsAndOlderHeaders) {
372             key += notificationGroup.isSeen();
373         }
374 
375         return key.hashCode();
376     }
377 
378     /**
379      * Set the expansion state of a group notification given its group key.
380      *
381      * @param groupKey the unique identifier of a {@link NotificationGroup}
382      * @param isSeen whether the {@link NotificationGroup} has been seen by the user
383      * @param isExpanded whether the group notification should be expanded.
384      */
setExpanded(String groupKey, boolean isSeen, boolean isExpanded)385     public void setExpanded(String groupKey, boolean isSeen, boolean isExpanded) {
386         if (isExpanded(groupKey, isSeen) == isExpanded) {
387             return;
388         }
389 
390         ExpandedNotification expandedNotification = new ExpandedNotification(groupKey, isSeen);
391         if (isExpanded) {
392             mExpandedNotifications.add(expandedNotification);
393         } else {
394             mExpandedNotifications.remove(expandedNotification);
395         }
396         if (DEBUG) {
397             Log.d(TAG, "Expanded notification statuses: " + mExpandedNotifications);
398         }
399     }
400 
401     /**
402      * Collapses all expanded groups.
403      */
collapseAllGroups()404     public void collapseAllGroups() {
405         if (!mExpandedNotifications.isEmpty()) {
406             mExpandedNotifications.clear();
407         }
408     }
409 
410     /**
411      * Returns whether the notification is expanded given its group key and it's seen status.
412      *
413      * @param groupKey the unique identifier of a {@link NotificationGroup}
414      * @param isSeen whether the {@link NotificationGroup} has been seen by the user
415      */
isExpanded(String groupKey, boolean isSeen)416     boolean isExpanded(String groupKey, boolean isSeen) {
417         ExpandedNotification expandedNotification = new ExpandedNotification(groupKey, isSeen);
418         return mExpandedNotifications.contains(expandedNotification);
419     }
420 
421     /**
422      * Gets the current {@link CarUxRestrictions}.
423      */
getCarUxRestrictions()424     public CarUxRestrictions getCarUxRestrictions() {
425         return mCarUxRestrictions;
426     }
427 
428     /**
429      * Updates notifications and update views.
430      *
431      * @param setRecyclerViewListHeadersAndFooters sets the header and footer on the entire list of
432      * items within the recycler view. This is NOT the header/footer for the grouped notifications.
433      */
setNotifications(List<NotificationGroup> notifications, boolean setRecyclerViewListHeadersAndFooters)434     public void setNotifications(List<NotificationGroup> notifications,
435             boolean setRecyclerViewListHeadersAndFooters) {
436         mGroupKeyToCountMap.clear();
437         notifications.forEach(notificationGroup -> {
438             if ((mGroupKeyToCountMap.computeIfPresent(notificationGroup.getGroupKey(),
439                     (key, currentValue) -> currentValue + 1)) == null) {
440                 mGroupKeyToCountMap.put(notificationGroup.getGroupKey(), 1);
441             }
442         });
443 
444         if (mShowRecentsAndOlderHeaders && !mIsGroupNotificationAdapter) {
445             List<NotificationGroup> seenNotifications = new ArrayList<>();
446             List<NotificationGroup> unseenNotifications = new ArrayList<>();
447             notifications.forEach(notificationGroup -> {
448                 if (notificationGroup.isSeen()) {
449                     seenNotifications.add(new NotificationGroup(notificationGroup));
450                 } else {
451                     unseenNotifications.add(new NotificationGroup(notificationGroup));
452                 }
453             });
454             setSeenAndUnseenNotifications(unseenNotifications, seenNotifications,
455                     setRecyclerViewListHeadersAndFooters);
456             return;
457         }
458 
459         List<NotificationGroup> notificationGroupList = notifications.stream()
460                 .map(notificationGroup -> new NotificationGroup(notificationGroup))
461                 .collect(Collectors.toList());
462 
463         if (setRecyclerViewListHeadersAndFooters) {
464             // add header as the first item of the list.
465             notificationGroupList.add(0, createNotificationHeader());
466             // add footer as the last item of the list.
467             notificationGroupList.add(createNotificationFooter());
468             mHasHeaderAndFooter = true;
469         } else {
470             mHasHeaderAndFooter = false;
471         }
472 
473         CarNotificationDiff notificationDiff =
474                 new CarNotificationDiff(mContext, mNotifications, notificationGroupList, mMaxItems);
475         notificationDiff.setShowRecentsAndOlderHeaders(false);
476         DiffUtil.DiffResult diffResult =
477                 DiffUtil.calculateDiff(notificationDiff, /* detectMoves= */ false);
478         mNotifications = notificationGroupList;
479         if (DEBUG) {
480             Log.d(TAG, "Updated adapter view holders: " + mNotifications);
481         }
482         updateUnderlyingDataChanged(getUnrestrictedItemCount(), /* newAnchorIndex= */ 0);
483         diffResult.dispatchUpdatesTo(this);
484     }
485 
setSeenAndUnseenNotifications(List<NotificationGroup> unseenNotifications, List<NotificationGroup> seenNotifications, boolean setRecyclerViewListHeadersAndFooters)486     private void setSeenAndUnseenNotifications(List<NotificationGroup> unseenNotifications,
487             List<NotificationGroup> seenNotifications,
488             boolean setRecyclerViewListHeadersAndFooters) {
489         if (DEBUG) {
490             Log.d(TAG, "Seen notifications: " + seenNotifications);
491             Log.d(TAG, "Unseen notifications: " + unseenNotifications);
492         }
493 
494         List<NotificationGroup> notificationGroupList;
495         if (unseenNotifications.isEmpty()) {
496             mHasUnseenNotifications = false;
497 
498             notificationGroupList = new ArrayList<>();
499         } else {
500             mHasUnseenNotifications = true;
501 
502             notificationGroupList = new ArrayList<>(unseenNotifications);
503             if (setRecyclerViewListHeadersAndFooters) {
504                 // Add recents header as the first item of the list.
505                 notificationGroupList.add(/* index= */ 0, createRecentsHeader());
506             }
507         }
508 
509         if (seenNotifications.isEmpty()) {
510             mHasSeenNotifications = false;
511         } else {
512             mHasSeenNotifications = true;
513 
514             if (setRecyclerViewListHeadersAndFooters) {
515                 // Append older header to the list.
516                 notificationGroupList.add(createOlderHeader());
517             }
518             notificationGroupList.addAll(seenNotifications);
519         }
520 
521         if (setRecyclerViewListHeadersAndFooters) {
522             // Add header as the first item of the list.
523             notificationGroupList.add(0, createNotificationHeader());
524             // Add footer as the last item of the list.
525             notificationGroupList.add(createNotificationFooter());
526             mHasHeaderAndFooter = true;
527         } else {
528             mHasHeaderAndFooter = false;
529         }
530 
531         CarNotificationDiff notificationDiff =
532                 new CarNotificationDiff(mContext, mNotifications, notificationGroupList, mMaxItems);
533         notificationDiff.setShowRecentsAndOlderHeaders(true);
534         DiffUtil.DiffResult diffResult =
535                 DiffUtil.calculateDiff(notificationDiff, /* detectMoves= */ false);
536         mNotifications = notificationGroupList;
537         if (DEBUG) {
538             Log.d(TAG, "Updated adapter view holders: " + mNotifications);
539         }
540         updateUnderlyingDataChanged(getUnrestrictedItemCount(), /* newAnchorIndex= */ 0);
541         diffResult.dispatchUpdatesTo(this);
542     }
543 
544     /**
545      * Returns {@code true} if notifications are present in adapter.
546      *
547      * Group notification list doesn't have any headers, hence, if there are any notifications
548      * present the size will be more than zero.
549      *
550      * Non-group notification list has header and footer by default. Therefore the min number of
551      * items in the adapter will always be two. If there are any notifications present the size will
552      * be more than two.
553      *
554      * When recent and older headers are enabled, each header will be accounted for when checking
555      * for the presence of notifications.
556      */
hasNotifications()557     public boolean hasNotifications() {
558         int numberOfHeaders;
559         if (mIsGroupNotificationAdapter) {
560             numberOfHeaders = 0;
561         } else {
562             numberOfHeaders = 2;
563 
564             if (mHasSeenNotifications) {
565                 numberOfHeaders++;
566             }
567 
568             if (mHasUnseenNotifications) {
569                 numberOfHeaders++;
570             }
571         }
572 
573         return getItemCount() > numberOfHeaders;
574     }
575 
createNotificationHeader()576     private NotificationGroup createNotificationHeader() {
577         NotificationGroup notificationGroupWithHeader = new NotificationGroup();
578         notificationGroupWithHeader.setHeader(true);
579         notificationGroupWithHeader.setGroupKey("notification_header");
580         return notificationGroupWithHeader;
581     }
582 
createNotificationFooter()583     private NotificationGroup createNotificationFooter() {
584         NotificationGroup notificationGroupWithFooter = new NotificationGroup();
585         notificationGroupWithFooter.setFooter(true);
586         notificationGroupWithFooter.setGroupKey("notification_footer");
587         return notificationGroupWithFooter;
588     }
589 
createRecentsHeader()590     private NotificationGroup createRecentsHeader() {
591         NotificationGroup notificationGroupWithRecents = new NotificationGroup();
592         notificationGroupWithRecents.setRecentsHeader(true);
593         notificationGroupWithRecents.setGroupKey("notification_recents");
594         notificationGroupWithRecents.setSeen(false);
595         return notificationGroupWithRecents;
596     }
597 
createOlderHeader()598     private NotificationGroup createOlderHeader() {
599         NotificationGroup notificationGroupWithOlder = new NotificationGroup();
600         notificationGroupWithOlder.setOlderHeader(true);
601         notificationGroupWithOlder.setGroupKey("notification_older");
602         notificationGroupWithOlder.setSeen(true);
603         return notificationGroupWithOlder;
604     }
605 
606     /** Implementation of {@link PreprocessingManager.CallStateListener} **/
607     @Override
onCallStateChanged(boolean isInCall)608     public void onCallStateChanged(boolean isInCall) {
609         if (isInCall != mIsInCall) {
610             mIsInCall = isInCall;
611             notifyDataSetChanged();
612         }
613     }
614 
615     /**
616      * Sets the current {@link CarUxRestrictions}.
617      */
setCarUxRestrictions(CarUxRestrictions carUxRestrictions)618     public void setCarUxRestrictions(CarUxRestrictions carUxRestrictions) {
619         Log.d(TAG, "setCarUxRestrictions");
620         mCarUxRestrictions = carUxRestrictions;
621         notifyDataSetChanged();
622     }
623 
624     /**
625      * Helper method that determines whether a notification is a messaging notification and
626      * should have restricted content (no message preview).
627      */
shouldRestrictMessagePreview()628     private boolean shouldRestrictMessagePreview() {
629         return mCarUxRestrictions != null && (mCarUxRestrictions.getActiveRestrictions()
630                 & CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE) != 0;
631     }
632 
633     /**
634      * Get root recycler view's view pool so that the child recycler view can share the same
635      * view pool with the parent.
636      */
getViewPool()637     public RecyclerView.RecycledViewPool getViewPool() {
638         if (mIsGroupNotificationAdapter) {
639             // currently only support one level of expansion.
640             throw new IllegalStateException("CarNotificationViewAdapter is a child adapter; "
641                     + "its view pool should not be reused.");
642         }
643         return mViewPool;
644     }
645 
646     /**
647      * Returns {@code true} if there are multiple groups with the same {@code groupKey}.
648      */
shouldRemoveGroupSummary(String groupKey)649     public boolean shouldRemoveGroupSummary(String groupKey) {
650         return mGroupKeyToCountMap.getOrDefault(groupKey, /* defaultValue= */ 0) <= 1;
651     }
652 
653     /**
654      * Sets the NotificationClickHandlerFactory that allows for a hook to run a block off code
655      * when  the notification is clicked. This is useful to dismiss a screen after
656      * a notification list clicked.
657      */
setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory)658     public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) {
659         mClickHandlerFactory = clickHandlerFactory;
660     }
661 
662     /**
663      * Set notification groups as seen.
664      *
665      * @param start Initial adapter position of the notification groups.
666      * @param end Final adapter position of the notification groups.
667      */
setVisibleNotificationsAsSeen(int start, int end)668     void setVisibleNotificationsAsSeen(int start, int end) {
669         if (mNotificationDataManager == null || mIsGroupNotificationAdapter) {
670             return;
671         }
672 
673         start = Math.max(start, 0);
674         end = Math.min(end, mNotifications.size() - 1);
675 
676         List<AlertEntry> notifications = new ArrayList();
677         for (int i = start; i <= end; i++) {
678             NotificationGroup group = mNotifications.get(i);
679             AlertEntry groupSummary = group.getGroupSummaryNotification();
680             if (groupSummary != null) {
681                 notifications.add(groupSummary);
682             }
683 
684             notifications.addAll(group.getChildNotifications());
685         }
686 
687         mNotificationDataManager.setVisibleNotificationsAsSeen(notifications);
688     }
689 
690     @Override
getConfigurationId()691     public int getConfigurationId() {
692         return R.id.notification_list_uxr_config;
693     }
694 
695     private static class ExpandedNotification {
696         private String mKey;
697         private boolean mIsExpanded;
698 
ExpandedNotification(String key, boolean isExpanded)699         ExpandedNotification(String key, boolean isExpanded) {
700             mKey = key;
701             mIsExpanded = isExpanded;
702         }
703 
704         @Override
equals(Object obj)705         public boolean equals(Object obj) {
706             if (!(obj instanceof ExpandedNotification)) {
707                 return false;
708             }
709             ExpandedNotification other = (ExpandedNotification) obj;
710 
711             return mKey.equals(other.getKey()) && mIsExpanded == other.isExpanded();
712         }
713 
getKey()714         public String getKey() {
715             return mKey;
716         }
717 
isExpanded()718         public boolean isExpanded() {
719             return mIsExpanded;
720         }
721     }
722 }
723