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