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