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         int progressMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX);
266         boolean isIndeterminate = extras.getBoolean(
267                 Notification.EXTRA_PROGRESS_INDETERMINATE);
268         boolean hasValidProgress = isIndeterminate || progressMax != 0;
269         boolean isProgress = extras.containsKey(Notification.EXTRA_PROGRESS)
270                 && extras.containsKey(Notification.EXTRA_PROGRESS_MAX)
271                 && hasValidProgress
272                 && !notification.hasCompletedProgress();
273         if (isProgress) {
274             return mIsGroupNotificationAdapter
275                     ? NotificationViewType.PROGRESS_IN_GROUP : NotificationViewType.PROGRESS;
276         }
277 
278         // inbox
279         boolean isInbox = extras.containsKey(Notification.EXTRA_TITLE_BIG)
280                 && extras.containsKey(Notification.EXTRA_SUMMARY_TEXT);
281         if (isInbox) {
282             return mIsGroupNotificationAdapter
283                     ? NotificationViewType.INBOX_IN_GROUP : NotificationViewType.INBOX;
284         }
285 
286         // group summary
287         boolean isGroupSummary = notificationGroup.getChildTitles() != null;
288         if (isGroupSummary) {
289             return NotificationViewType.GROUP_SUMMARY;
290         }
291 
292         // the big text and big picture styles are fallen back to basic template in car
293         // i.e. setting the big text and big picture does not have an effect
294         boolean isBigText = extras.containsKey(Notification.EXTRA_BIG_TEXT);
295         if (isBigText) {
296             Log.i(TAG, "Big text style is not supported as a car notification");
297         }
298         boolean isBigPicture = extras.containsKey(Notification.EXTRA_PICTURE);
299         if (isBigPicture) {
300             Log.i(TAG, "Big picture style is not supported as a car notification");
301         }
302 
303         // basic, big text, big picture
304         return mIsGroupNotificationAdapter
305                 ? NotificationViewType.BASIC_IN_GROUP : NotificationViewType.BASIC;
306     }
307 
308     @Override
getUnrestrictedItemCount()309     public int getUnrestrictedItemCount() {
310         return mNotifications.size();
311     }
312 
313     @Override
setMaxItems(int maxItems)314     public void setMaxItems(int maxItems) {
315         if (maxItems == ContentLimitingAdapter.UNLIMITED
316                 || (!mHasHeaderAndFooter && !mHasUnseenNotifications && !mHasSeenNotifications)) {
317             mMaxItems = maxItems;
318         } else {
319             // Adding to max limit of notifications for each header so that they do not count
320             // towards limit.
321             // Footer is not accounted for since it as the end of the list and it doesn't affect the
322             // limit of notifications above it.
323             mMaxItems = maxItems;
324             if (mHasHeaderAndFooter) {
325                 mMaxItems++;
326             }
327             if (mHasSeenNotifications) {
328                 mMaxItems++;
329             }
330             if (mHasUnseenNotifications) {
331                 mMaxItems++;
332             }
333         }
334         super.setMaxItems(mMaxItems);
335     }
336 
337     @Override
getScrollToPositionWhenRestricted()338     protected int getScrollToPositionWhenRestricted() {
339         if (mLayoutManager == null) {
340             return -1;
341         }
342         int firstItem = mLayoutManager.findFirstVisibleItemPosition();
343         if (firstItem >= getItemCount() - 1) {
344             return getItemCount() - 1;
345         }
346         return -1;
347     }
348 
349     @Override
getItemId(int position)350     public long getItemId(int position) {
351         NotificationGroup notificationGroup = mNotifications.get(position);
352         if (notificationGroup.isHeader()) {
353             return ID_HEADER;
354         }
355         if (mShowRecentsAndOlderHeaders && !mIsGroupNotificationAdapter) {
356             if (notificationGroup.isRecentsHeader()) {
357                 return ID_RECENT_HEADER;
358             }
359             if (notificationGroup.isOlderHeader()) {
360                 return ID_OLDER_HEADER;
361             }
362             if (notificationGroup.isFooter()) {
363                 return ID_FOOTER;
364             }
365         }
366         if (notificationGroup.isFooter()) {
367             // We can use recent header's ID when it isn't being used.
368             return ID_RECENT_HEADER;
369         }
370 
371         String key = notificationGroup.isGroup()
372                 ? notificationGroup.getGroupKey()
373                 : notificationGroup.getSingleNotification().getKey();
374 
375         if (mShowRecentsAndOlderHeaders) {
376             key += notificationGroup.isSeen();
377         }
378 
379         return key.hashCode();
380     }
381 
382     /**
383      * Set the expansion state of a group notification given its group key.
384      *
385      * @param groupKey the unique identifier of a {@link NotificationGroup}
386      * @param isSeen whether the {@link NotificationGroup} has been seen by the user
387      * @param isExpanded whether the group notification should be expanded.
388      */
setExpanded(String groupKey, boolean isSeen, boolean isExpanded)389     public void setExpanded(String groupKey, boolean isSeen, boolean isExpanded) {
390         if (isExpanded(groupKey, isSeen) == isExpanded) {
391             return;
392         }
393 
394         ExpandedNotification expandedNotification = new ExpandedNotification(groupKey, isSeen);
395         if (isExpanded) {
396             mExpandedNotifications.add(expandedNotification);
397         } else {
398             mExpandedNotifications.remove(expandedNotification);
399         }
400         if (DEBUG) {
401             Log.d(TAG, "Expanded notification statuses: " + mExpandedNotifications);
402         }
403     }
404 
405     /**
406      * Collapses all expanded groups.
407      */
collapseAllGroups()408     public void collapseAllGroups() {
409         if (!mExpandedNotifications.isEmpty()) {
410             mExpandedNotifications.clear();
411         }
412     }
413 
414     /**
415      * Returns whether the notification is expanded given its group key and it's seen status.
416      *
417      * @param groupKey the unique identifier of a {@link NotificationGroup}
418      * @param isSeen whether the {@link NotificationGroup} has been seen by the user
419      */
isExpanded(String groupKey, boolean isSeen)420     boolean isExpanded(String groupKey, boolean isSeen) {
421         ExpandedNotification expandedNotification = new ExpandedNotification(groupKey, isSeen);
422         return mExpandedNotifications.contains(expandedNotification);
423     }
424 
425     /**
426      * Gets the current {@link CarUxRestrictions}.
427      */
getCarUxRestrictions()428     public CarUxRestrictions getCarUxRestrictions() {
429         return mCarUxRestrictions;
430     }
431 
432     /**
433      * Updates notifications and update views.
434      *
435      * @param setRecyclerViewListHeadersAndFooters sets the header and footer on the entire list of
436      * items within the recycler view. This is NOT the header/footer for the grouped notifications.
437      */
setNotifications(List<NotificationGroup> notifications, boolean setRecyclerViewListHeadersAndFooters)438     public void setNotifications(List<NotificationGroup> notifications,
439             boolean setRecyclerViewListHeadersAndFooters) {
440         mGroupKeyToCountMap.clear();
441         notifications.forEach(notificationGroup -> {
442             if ((mGroupKeyToCountMap.computeIfPresent(notificationGroup.getGroupKey(),
443                     (key, currentValue) -> currentValue + 1)) == null) {
444                 mGroupKeyToCountMap.put(notificationGroup.getGroupKey(), 1);
445             }
446         });
447 
448         if (mShowRecentsAndOlderHeaders && !mIsGroupNotificationAdapter) {
449             List<NotificationGroup> seenNotifications = new ArrayList<>();
450             List<NotificationGroup> unseenNotifications = new ArrayList<>();
451             notifications.forEach(notificationGroup -> {
452                 if (notificationGroup.isSeen()) {
453                     seenNotifications.add(new NotificationGroup(notificationGroup));
454                 } else {
455                     unseenNotifications.add(new NotificationGroup(notificationGroup));
456                 }
457             });
458             setSeenAndUnseenNotifications(unseenNotifications, seenNotifications,
459                     setRecyclerViewListHeadersAndFooters);
460             return;
461         }
462 
463         List<NotificationGroup> notificationGroupList = notifications.stream()
464                 .map(notificationGroup -> new NotificationGroup(notificationGroup))
465                 .collect(Collectors.toList());
466 
467         if (setRecyclerViewListHeadersAndFooters) {
468             // add header as the first item of the list.
469             notificationGroupList.add(0, createNotificationHeader());
470             // add footer as the last item of the list.
471             notificationGroupList.add(createNotificationFooter());
472             mHasHeaderAndFooter = true;
473         } else {
474             mHasHeaderAndFooter = false;
475         }
476 
477         CarNotificationDiff notificationDiff =
478                 new CarNotificationDiff(mContext, mNotifications, notificationGroupList, mMaxItems);
479         notificationDiff.setShowRecentsAndOlderHeaders(false);
480         DiffUtil.DiffResult diffResult =
481                 DiffUtil.calculateDiff(notificationDiff, /* detectMoves= */ false);
482         mNotifications = notificationGroupList;
483         if (DEBUG) {
484             Log.d(TAG, "Updated adapter view holders: " + mNotifications);
485         }
486         updateUnderlyingDataChanged(getUnrestrictedItemCount(), /* newAnchorIndex= */ 0);
487         diffResult.dispatchUpdatesTo(this);
488     }
489 
setSeenAndUnseenNotifications(List<NotificationGroup> unseenNotifications, List<NotificationGroup> seenNotifications, boolean setRecyclerViewListHeadersAndFooters)490     private void setSeenAndUnseenNotifications(List<NotificationGroup> unseenNotifications,
491             List<NotificationGroup> seenNotifications,
492             boolean setRecyclerViewListHeadersAndFooters) {
493         if (DEBUG) {
494             Log.d(TAG, "Seen notifications: " + seenNotifications);
495             Log.d(TAG, "Unseen notifications: " + unseenNotifications);
496         }
497 
498         List<NotificationGroup> notificationGroupList;
499         if (unseenNotifications.isEmpty()) {
500             mHasUnseenNotifications = false;
501 
502             notificationGroupList = new ArrayList<>();
503         } else {
504             mHasUnseenNotifications = true;
505 
506             notificationGroupList = new ArrayList<>(unseenNotifications);
507             if (setRecyclerViewListHeadersAndFooters) {
508                 // Add recents header as the first item of the list.
509                 notificationGroupList.add(/* index= */ 0, createRecentsHeader());
510             }
511         }
512 
513         if (seenNotifications.isEmpty()) {
514             mHasSeenNotifications = false;
515         } else {
516             mHasSeenNotifications = true;
517 
518             if (setRecyclerViewListHeadersAndFooters) {
519                 // Append older header to the list.
520                 notificationGroupList.add(createOlderHeader());
521             }
522             notificationGroupList.addAll(seenNotifications);
523         }
524 
525         if (setRecyclerViewListHeadersAndFooters) {
526             // Add header as the first item of the list.
527             notificationGroupList.add(0, createNotificationHeader());
528             // Add footer as the last item of the list.
529             notificationGroupList.add(createNotificationFooter());
530             mHasHeaderAndFooter = true;
531         } else {
532             mHasHeaderAndFooter = false;
533         }
534 
535         CarNotificationDiff notificationDiff =
536                 new CarNotificationDiff(mContext, mNotifications, notificationGroupList, mMaxItems);
537         notificationDiff.setShowRecentsAndOlderHeaders(true);
538         DiffUtil.DiffResult diffResult =
539                 DiffUtil.calculateDiff(notificationDiff, /* detectMoves= */ false);
540         mNotifications = notificationGroupList;
541         if (DEBUG) {
542             Log.d(TAG, "Updated adapter view holders: " + mNotifications);
543         }
544         updateUnderlyingDataChanged(getUnrestrictedItemCount(), /* newAnchorIndex= */ 0);
545         diffResult.dispatchUpdatesTo(this);
546     }
547 
548     /**
549      * Returns {@code true} if notifications are present in adapter.
550      *
551      * Group notification list doesn't have any headers, hence, if there are any notifications
552      * present the size will be more than zero.
553      *
554      * Non-group notification list has header and footer by default. Therefore the min number of
555      * items in the adapter will always be two. If there are any notifications present the size will
556      * be more than two.
557      *
558      * When recent and older headers are enabled, each header will be accounted for when checking
559      * for the presence of notifications.
560      */
hasNotifications()561     public boolean hasNotifications() {
562         int numberOfHeaders;
563         if (mIsGroupNotificationAdapter) {
564             numberOfHeaders = 0;
565         } else {
566             numberOfHeaders = 2;
567 
568             if (mHasSeenNotifications) {
569                 numberOfHeaders++;
570             }
571 
572             if (mHasUnseenNotifications) {
573                 numberOfHeaders++;
574             }
575         }
576 
577         return getItemCount() > numberOfHeaders;
578     }
579 
createNotificationHeader()580     private NotificationGroup createNotificationHeader() {
581         NotificationGroup notificationGroupWithHeader = new NotificationGroup();
582         notificationGroupWithHeader.setHeader(true);
583         notificationGroupWithHeader.setGroupKey("notification_header");
584         return notificationGroupWithHeader;
585     }
586 
createNotificationFooter()587     private NotificationGroup createNotificationFooter() {
588         NotificationGroup notificationGroupWithFooter = new NotificationGroup();
589         notificationGroupWithFooter.setFooter(true);
590         notificationGroupWithFooter.setGroupKey("notification_footer");
591         return notificationGroupWithFooter;
592     }
593 
createRecentsHeader()594     private NotificationGroup createRecentsHeader() {
595         NotificationGroup notificationGroupWithRecents = new NotificationGroup();
596         notificationGroupWithRecents.setRecentsHeader(true);
597         notificationGroupWithRecents.setGroupKey("notification_recents");
598         notificationGroupWithRecents.setSeen(false);
599         return notificationGroupWithRecents;
600     }
601 
createOlderHeader()602     private NotificationGroup createOlderHeader() {
603         NotificationGroup notificationGroupWithOlder = new NotificationGroup();
604         notificationGroupWithOlder.setOlderHeader(true);
605         notificationGroupWithOlder.setGroupKey("notification_older");
606         notificationGroupWithOlder.setSeen(true);
607         return notificationGroupWithOlder;
608     }
609 
610     /** Implementation of {@link PreprocessingManager.CallStateListener} **/
611     @Override
onCallStateChanged(boolean isInCall)612     public void onCallStateChanged(boolean isInCall) {
613         if (isInCall != mIsInCall) {
614             mIsInCall = isInCall;
615             notifyDataSetChanged();
616         }
617     }
618 
619     /**
620      * Sets the current {@link CarUxRestrictions}.
621      */
setCarUxRestrictions(CarUxRestrictions carUxRestrictions)622     public void setCarUxRestrictions(CarUxRestrictions carUxRestrictions) {
623         Log.d(TAG, "setCarUxRestrictions");
624         mCarUxRestrictions = carUxRestrictions;
625         notifyDataSetChanged();
626     }
627 
628     /**
629      * Helper method that determines whether a notification is a messaging notification and
630      * should have restricted content (no message preview).
631      */
shouldRestrictMessagePreview()632     private boolean shouldRestrictMessagePreview() {
633         return mCarUxRestrictions != null && (mCarUxRestrictions.getActiveRestrictions()
634                 & CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE) != 0;
635     }
636 
637     /**
638      * Get root recycler view's view pool so that the child recycler view can share the same
639      * view pool with the parent.
640      */
getViewPool()641     public RecyclerView.RecycledViewPool getViewPool() {
642         if (mIsGroupNotificationAdapter) {
643             // currently only support one level of expansion.
644             throw new IllegalStateException("CarNotificationViewAdapter is a child adapter; "
645                     + "its view pool should not be reused.");
646         }
647         return mViewPool;
648     }
649 
650     /**
651      * Returns {@code true} if there are multiple groups with the same {@code groupKey}.
652      */
shouldRemoveGroupSummary(String groupKey)653     public boolean shouldRemoveGroupSummary(String groupKey) {
654         return mGroupKeyToCountMap.getOrDefault(groupKey, /* defaultValue= */ 0) <= 1;
655     }
656 
657     /**
658      * Sets the NotificationClickHandlerFactory that allows for a hook to run a block off code
659      * when  the notification is clicked. This is useful to dismiss a screen after
660      * a notification list clicked.
661      */
setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory)662     public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) {
663         mClickHandlerFactory = clickHandlerFactory;
664     }
665 
666     /**
667      * Set notification groups as seen.
668      *
669      * @param start Initial adapter position of the notification groups.
670      * @param end Final adapter position of the notification groups.
671      */
setVisibleNotificationsAsSeen(int start, int end)672     void setVisibleNotificationsAsSeen(int start, int end) {
673         if (mNotificationDataManager == null || mIsGroupNotificationAdapter) {
674             return;
675         }
676 
677         start = Math.max(start, 0);
678         end = Math.min(end, mNotifications.size() - 1);
679 
680         List<AlertEntry> notifications = new ArrayList();
681         for (int i = start; i <= end; i++) {
682             NotificationGroup group = mNotifications.get(i);
683             AlertEntry groupSummary = group.getGroupSummaryNotification();
684             if (groupSummary != null) {
685                 notifications.add(groupSummary);
686             }
687 
688             notifications.addAll(group.getChildNotifications());
689         }
690 
691         mNotificationDataManager.setVisibleNotificationsAsSeen(notifications);
692     }
693 
694     @Override
getConfigurationId()695     public int getConfigurationId() {
696         return R.id.notification_list_uxr_config;
697     }
698 
699     private static class ExpandedNotification {
700         private String mKey;
701         private boolean mIsExpanded;
702 
ExpandedNotification(String key, boolean isExpanded)703         ExpandedNotification(String key, boolean isExpanded) {
704             mKey = key;
705             mIsExpanded = isExpanded;
706         }
707 
708         @Override
equals(Object obj)709         public boolean equals(Object obj) {
710             if (!(obj instanceof ExpandedNotification)) {
711                 return false;
712             }
713             ExpandedNotification other = (ExpandedNotification) obj;
714 
715             return mKey.equals(other.getKey()) && mIsExpanded == other.isExpanded();
716         }
717 
getKey()718         public String getKey() {
719             return mKey;
720         }
721 
isExpanded()722         public boolean isExpanded() {
723             return mIsExpanded;
724         }
725     }
726 }
727