• 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.Bundle;
23 import android.util.Log;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.ViewGroup;
27 
28 import androidx.annotation.Nullable;
29 import androidx.recyclerview.widget.DiffUtil;
30 import androidx.recyclerview.widget.LinearLayoutManager;
31 import androidx.recyclerview.widget.RecyclerView;
32 
33 import com.android.car.notification.template.CarNotificationBaseViewHolder;
34 import com.android.car.notification.template.CarNotificationFooterViewHolder;
35 import com.android.car.notification.template.CarNotificationHeaderViewHolder;
36 import com.android.car.notification.template.GroupNotificationViewHolder;
37 import com.android.car.notification.template.GroupSummaryNotificationViewHolder;
38 import com.android.car.notification.template.MessageNotificationViewHolder;
39 import com.android.car.ui.recyclerview.ContentLimitingAdapter;
40 
41 import java.util.ArrayList;
42 import java.util.HashSet;
43 import java.util.List;
44 import java.util.Set;
45 
46 /**
47  * Notification data adapter that binds a notification to the corresponding view.
48  */
49 public class CarNotificationViewAdapter extends ContentLimitingAdapter<RecyclerView.ViewHolder>
50         implements PreprocessingManager.CallStateListener {
51     private static final String TAG = "CarNotificationAdapter";
52 
53     private final Context mContext;
54     private final LayoutInflater mInflater;
55     private final int mMaxNumberGroupChildrenShown;
56     private final boolean mIsGroupNotificationAdapter;
57 
58     // book keeping expanded notification groups
59     private final List<String> mExpandedNotifications = new ArrayList<>();
60     private final CarNotificationItemController mNotificationItemController;
61 
62     private List<NotificationGroup> mNotifications = new ArrayList<>();
63     private LinearLayoutManager mLayoutManager;
64     private RecyclerView.RecycledViewPool mViewPool;
65     private CarUxRestrictions mCarUxRestrictions;
66     private NotificationClickHandlerFactory mClickHandlerFactory;
67     private NotificationDataManager mNotificationDataManager;
68     private boolean mIsInCall;
69     // Suppress binding views to child notifications in the process of being removed.
70     private Set<AlertEntry> mChildNotificationsBeingCleared = new HashSet<>();
71     private boolean mHasHeaderAndFooter;
72     private int mMaxItems = ContentLimitingAdapter.UNLIMITED;
73 
74     /**
75      * Constructor for a notification adapter.
76      * Can be used both by the root notification list view, or a grouped notification view.
77      *
78      * @param context the context for resources and inflating views
79      * @param isGroupNotificationAdapter true if this adapter is used by a grouped notification view
80      * @param notificationItemController shared logic to control notification items.
81      */
CarNotificationViewAdapter(Context context, boolean isGroupNotificationAdapter, @Nullable CarNotificationItemController notificationItemController)82     public CarNotificationViewAdapter(Context context, boolean isGroupNotificationAdapter,
83             @Nullable CarNotificationItemController notificationItemController) {
84         mContext = context;
85         mInflater = LayoutInflater.from(context);
86         mMaxNumberGroupChildrenShown =
87                 mContext.getResources().getInteger(R.integer.max_group_children_number);
88         mIsGroupNotificationAdapter = isGroupNotificationAdapter;
89         mNotificationItemController = notificationItemController;
90         setHasStableIds(true);
91         if (!mIsGroupNotificationAdapter) {
92             mViewPool = new RecyclerView.RecycledViewPool();
93         }
94 
95         PreprocessingManager.getInstance(context).addCallStateListener(this::onCallStateChanged);
96     }
97 
98     @Override
onAttachedToRecyclerView(@onNull RecyclerView recyclerView)99     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
100         super.onAttachedToRecyclerView(recyclerView);
101         mLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
102     }
103 
104     @Override
onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)105     public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
106         super.onDetachedFromRecyclerView(recyclerView);
107         mLayoutManager = null;
108     }
109 
110     @Override
onCreateViewHolderImpl(@onNull ViewGroup parent, int viewType)111     public RecyclerView.ViewHolder onCreateViewHolderImpl(@NonNull ViewGroup parent, int viewType) {
112         RecyclerView.ViewHolder viewHolder;
113         View view;
114         switch (viewType) {
115             case NotificationViewType.HEADER:
116                 view = mInflater.inflate(R.layout.notification_header_template, parent, false);
117                 viewHolder = new CarNotificationHeaderViewHolder(mContext, view,
118                         mNotificationItemController);
119                 break;
120             case NotificationViewType.FOOTER:
121                 view = mInflater.inflate(R.layout.notification_footer_template, parent, false);
122                 viewHolder = new CarNotificationFooterViewHolder(mContext, view,
123                         mNotificationItemController);
124                 break;
125             default:
126                 CarNotificationTypeItem carNotificationTypeItem = CarNotificationTypeItem.of(
127                         viewType);
128                 view = mInflater.inflate(
129                         carNotificationTypeItem.getNotificationCenterTemplate(), parent, false);
130                 viewHolder = carNotificationTypeItem.getViewHolder(view, mClickHandlerFactory);
131         }
132 
133         return viewHolder;
134     }
135 
136     @Override
onBindViewHolderImpl(RecyclerView.ViewHolder holder, int position)137     public void onBindViewHolderImpl(RecyclerView.ViewHolder holder, int position) {
138         NotificationGroup notificationGroup = mNotifications.get(position);
139         int viewType = holder.getItemViewType();
140         switch (viewType) {
141             case NotificationViewType.HEADER:
142                 ((CarNotificationHeaderViewHolder) holder).bind(hasNotifications());
143                 return;
144             case NotificationViewType.FOOTER:
145                 ((CarNotificationFooterViewHolder) holder).bind(hasNotifications());
146                 return;
147             case NotificationViewType.GROUP_EXPANDED:
148                 ((GroupNotificationViewHolder) holder)
149                         .bind(notificationGroup, this, /* isExpanded= */ true);
150                 return;
151             case NotificationViewType.GROUP_COLLAPSED:
152                 ((GroupNotificationViewHolder) holder)
153                         .bind(notificationGroup, this, /* isExpanded= */ false);
154                 return;
155             case NotificationViewType.GROUP_SUMMARY:
156                 ((CarNotificationBaseViewHolder) holder).setHideDismissButton(true);
157                 ((GroupSummaryNotificationViewHolder) holder).bind(notificationGroup);
158                 return;
159         }
160 
161         CarNotificationTypeItem carNotificationTypeItem = CarNotificationTypeItem.of(viewType);
162         AlertEntry alertEntry = notificationGroup.getSingleNotification();
163 
164         if (shouldRestrictMessagePreview() && (viewType == NotificationViewType.MESSAGE
165                 || viewType == NotificationViewType.MESSAGE_IN_GROUP)) {
166             ((MessageNotificationViewHolder) holder)
167                     .bindRestricted(alertEntry, /* isInGroup= */ false, /* isHeadsUp= */false);
168         } else {
169             carNotificationTypeItem.bind(alertEntry, false, (CarNotificationBaseViewHolder) holder);
170         }
171     }
172 
173     @Override
getItemViewTypeImpl(int position)174     public int getItemViewTypeImpl(int position) {
175         NotificationGroup notificationGroup = mNotifications.get(position);
176 
177         if (notificationGroup.isHeader()) {
178             return NotificationViewType.HEADER;
179         }
180 
181         if (notificationGroup.isFooter()) {
182             return NotificationViewType.FOOTER;
183         }
184 
185         if (notificationGroup.isGroup()) {
186             if (mExpandedNotifications.contains(notificationGroup.getGroupKey())) {
187                 return NotificationViewType.GROUP_EXPANDED;
188             } else {
189                 return NotificationViewType.GROUP_COLLAPSED;
190             }
191         } else if (mExpandedNotifications.contains(notificationGroup.getGroupKey())) {
192             // when there are 2 notifications left in the expanded notification and one of them is
193             // removed at that time the item type changes from group to normal and hence the
194             // notification should be removed from expanded notifications.
195             setExpanded(notificationGroup.getGroupKey(), false);
196         }
197 
198         Notification notification =
199                 notificationGroup.getSingleNotification().getNotification();
200         Bundle extras = notification.extras;
201 
202         String category = notification.category;
203         if (category != null) {
204             switch (category) {
205                 case Notification.CATEGORY_CALL:
206                     return NotificationViewType.CALL;
207                 case Notification.CATEGORY_CAR_EMERGENCY:
208                     return NotificationViewType.CAR_EMERGENCY;
209                 case Notification.CATEGORY_CAR_WARNING:
210                     return NotificationViewType.CAR_WARNING;
211                 case Notification.CATEGORY_CAR_INFORMATION:
212                     return mIsGroupNotificationAdapter
213                             ? NotificationViewType.CAR_INFORMATION_IN_GROUP
214                             : NotificationViewType.CAR_INFORMATION;
215                 case Notification.CATEGORY_MESSAGE:
216                     return mIsGroupNotificationAdapter
217                             ? NotificationViewType.MESSAGE_IN_GROUP : NotificationViewType.MESSAGE;
218                 default:
219                     break;
220             }
221         }
222 
223         // progress
224         int progressMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX);
225         boolean isIndeterminate = extras.getBoolean(
226                 Notification.EXTRA_PROGRESS_INDETERMINATE);
227         boolean hasValidProgress = isIndeterminate || progressMax != 0;
228         boolean isProgress = extras.containsKey(Notification.EXTRA_PROGRESS)
229                 && extras.containsKey(Notification.EXTRA_PROGRESS_MAX)
230                 && hasValidProgress
231                 && !notification.hasCompletedProgress();
232         if (isProgress) {
233             return mIsGroupNotificationAdapter
234                     ? NotificationViewType.PROGRESS_IN_GROUP : NotificationViewType.PROGRESS;
235         }
236 
237         // inbox
238         boolean isInbox = extras.containsKey(Notification.EXTRA_TITLE_BIG)
239                 && extras.containsKey(Notification.EXTRA_SUMMARY_TEXT);
240         if (isInbox) {
241             return mIsGroupNotificationAdapter
242                     ? NotificationViewType.INBOX_IN_GROUP : NotificationViewType.INBOX;
243         }
244 
245         // group summary
246         boolean isGroupSummary = notificationGroup.getChildTitles() != null;
247         if (isGroupSummary) {
248             return NotificationViewType.GROUP_SUMMARY;
249         }
250 
251         // the big text and big picture styles are fallen back to basic template in car
252         // i.e. setting the big text and big picture does not have an effect
253         boolean isBigText = extras.containsKey(Notification.EXTRA_BIG_TEXT);
254         if (isBigText) {
255             Log.i(TAG, "Big text style is not supported as a car notification");
256         }
257         boolean isBigPicture = extras.containsKey(Notification.EXTRA_PICTURE);
258         if (isBigPicture) {
259             Log.i(TAG, "Big picture style is not supported as a car notification");
260         }
261 
262         // basic, big text, big picture
263         return mIsGroupNotificationAdapter
264                 ? NotificationViewType.BASIC_IN_GROUP : NotificationViewType.BASIC;
265     }
266 
267     @Override
getUnrestrictedItemCount()268     public int getUnrestrictedItemCount() {
269         return mNotifications.size();
270     }
271 
272     @Override
setMaxItems(int maxItems)273     public void setMaxItems(int maxItems) {
274         if (maxItems == ContentLimitingAdapter.UNLIMITED || !mHasHeaderAndFooter) {
275             mMaxItems = maxItems;
276         } else {
277             // Adding one so the notification header doesn't count toward the limit.
278             mMaxItems = maxItems + 1;
279         }
280         super.setMaxItems(mMaxItems);
281     }
282 
283     @Override
getScrollToPositionWhenRestricted()284     protected int getScrollToPositionWhenRestricted() {
285         if (mLayoutManager == null) {
286             return -1;
287         }
288         int firstItem = mLayoutManager.findFirstVisibleItemPosition();
289         if (firstItem >= getItemCount() - 1) {
290             return getItemCount() - 1;
291         }
292         return -1;
293     }
294 
295     @Override
getItemId(int position)296     public long getItemId(int position) {
297         NotificationGroup notificationGroup = mNotifications.get(position);
298         if (notificationGroup.isHeader()) {
299             return 0;
300         }
301 
302         if (notificationGroup.isFooter()) {
303             return 1;
304         }
305 
306         return notificationGroup.isGroup()
307                 ? notificationGroup.getGroupKey().hashCode()
308                 : notificationGroup.getSingleNotification().getKey().hashCode();
309     }
310 
311     /**
312      * Set the expansion state of a group notification given its group key.
313      *
314      * @param groupKey the unique identifier of a {@link NotificationGroup}
315      * @param isExpanded whether the group notification should be expanded.
316      */
setExpanded(String groupKey, boolean isExpanded)317     public void setExpanded(String groupKey, boolean isExpanded) {
318         if (isExpanded(groupKey) == isExpanded) {
319             return;
320         }
321 
322         if (isExpanded) {
323             mExpandedNotifications.add(groupKey);
324         } else {
325             mExpandedNotifications.remove(groupKey);
326         }
327     }
328 
329     /**
330      * Collapses all expanded groups.
331      */
collapseAllGroups()332     public void collapseAllGroups() {
333         if (!mExpandedNotifications.isEmpty()) {
334             mExpandedNotifications.clear();
335         }
336     }
337 
338     /**
339      * Returns whether the notification is expanded given its group key.
340      */
isExpanded(String groupKey)341     boolean isExpanded(String groupKey) {
342         return mExpandedNotifications.contains(groupKey);
343     }
344 
345     /**
346      * Gets the current {@link CarUxRestrictions}.
347      */
getCarUxRestrictions()348     public CarUxRestrictions getCarUxRestrictions() {
349         return mCarUxRestrictions;
350     }
351 
352     /**
353      * Updates notifications and update views.
354      *
355      * @param setRecyclerViewListHeaderAndFooter sets the header and footer on the entire list of
356      * items within the recycler view. This is NOT the header/footer for the grouped notifications.
357      */
setNotifications(List<NotificationGroup> notifications, boolean setRecyclerViewListHeaderAndFooter)358     public void setNotifications(List<NotificationGroup> notifications,
359             boolean setRecyclerViewListHeaderAndFooter) {
360 
361         notifications.removeIf(notificationGroup ->
362                 mChildNotificationsBeingCleared.contains(notificationGroup.getSingleNotification())
363         );
364 
365         List<NotificationGroup> notificationGroupList = new ArrayList<>(notifications);
366 
367         if (setRecyclerViewListHeaderAndFooter) {
368             // add header as the first item of the list.
369             notificationGroupList.add(0, createNotificationHeader());
370             // add footer as the last item of the list.
371             notificationGroupList.add(createNotificationFooter());
372             mHasHeaderAndFooter = true;
373         } else {
374             mHasHeaderAndFooter = false;
375         }
376 
377         DiffUtil.DiffResult diffResult =
378                 DiffUtil.calculateDiff(
379                         new CarNotificationDiff(mContext, mNotifications, notificationGroupList, mMaxItems),
380                         /* detectMoves= */ false);
381         mNotifications = notificationGroupList;
382         updateUnderlyingDataChanged(getUnrestrictedItemCount(), /* newAnchorIndex= */ 0);
383         diffResult.dispatchUpdatesTo(this);
384     }
385     /**
386      * Sets child notifications of the group notification that is in the process of being cleared.
387      * This prevents these child notifications from appearing briefly while the clearing process is
388      * running.
389      *
390      * <p>NOTE: To reset mChildNotificationsBeingCleared, pass an empty Set instead of null.</p>
391      *
392      * @param notificationsBeingCleared
393      */
setChildNotificationsBeingCleared(@onNull Set notificationsBeingCleared)394     protected void setChildNotificationsBeingCleared(@NonNull Set notificationsBeingCleared) {
395         mChildNotificationsBeingCleared = notificationsBeingCleared;
396     }
397 
398     /**
399      * Notification list has header and footer by default. Therefore the min number of items in the
400      * adapter will always be two. If there are any notifications present the size will be more than
401      * two.
402      */
hasNotifications()403     private boolean hasNotifications() {
404         return getItemCount() > 2;
405     }
406 
createNotificationHeader()407     private NotificationGroup createNotificationHeader() {
408         NotificationGroup notificationGroupWithHeader = new NotificationGroup();
409         notificationGroupWithHeader.setHeader(true);
410         notificationGroupWithHeader.setGroupKey("notification_header");
411         return notificationGroupWithHeader;
412     }
413 
createNotificationFooter()414     private NotificationGroup createNotificationFooter() {
415         NotificationGroup notificationGroupWithFooter = new NotificationGroup();
416         notificationGroupWithFooter.setFooter(true);
417         notificationGroupWithFooter.setGroupKey("notification_footer");
418         return notificationGroupWithFooter;
419     }
420 
421     /** Implementation of {@link PreprocessingManager.CallStateListener} **/
422     @Override
onCallStateChanged(boolean isInCall)423     public void onCallStateChanged(boolean isInCall) {
424         if (isInCall != mIsInCall) {
425             mIsInCall = isInCall;
426             notifyDataSetChanged();
427         }
428     }
429 
430     /**
431      * Sets the current {@link CarUxRestrictions}.
432      */
setCarUxRestrictions(CarUxRestrictions carUxRestrictions)433     public void setCarUxRestrictions(CarUxRestrictions carUxRestrictions) {
434         Log.d(TAG, "setCarUxRestrictions");
435         mCarUxRestrictions = carUxRestrictions;
436         notifyDataSetChanged();
437     }
438 
439     /**
440      * Helper method that determines whether a notification is a messaging notification and
441      * should have restricted content (no message preview).
442      */
shouldRestrictMessagePreview()443     private boolean shouldRestrictMessagePreview() {
444         return mCarUxRestrictions != null && (mCarUxRestrictions.getActiveRestrictions()
445                 & CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE) != 0;
446     }
447 
448     /**
449      * Get root recycler view's view pool so that the child recycler view can share the same
450      * view pool with the parent.
451      */
getViewPool()452     public RecyclerView.RecycledViewPool getViewPool() {
453         if (mIsGroupNotificationAdapter) {
454             // currently only support one level of expansion.
455             throw new IllegalStateException("CarNotificationViewAdapter is a child adapter; "
456                     + "its view pool should not be reused.");
457         }
458         return mViewPool;
459     }
460 
461     /**
462      * Sets the NotificationClickHandlerFactory that allows for a hook to run a block off code
463      * when  the notification is clicked. This is useful to dismiss a screen after
464      * a notification list clicked.
465      */
setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory)466     public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) {
467         mClickHandlerFactory = clickHandlerFactory;
468     }
469 
470     /**
471      * Sets NotificationDataManager that handles additional states for notifications such as "seen",
472      * and muting a messaging type notification.
473      *
474      * @param notificationDataManager An instance of NotificationDataManager.
475      */
setNotificationDataManager(NotificationDataManager notificationDataManager)476     public void setNotificationDataManager(NotificationDataManager notificationDataManager) {
477         mNotificationDataManager = notificationDataManager;
478     }
479 
480     /**
481      * Set notification groups as seen.
482      *
483      * @param start Initial adapter position of the notification groups.
484      * @param end Final adapter position of the notification groups.
485      */
setNotificationsAsSeen(int start, int end)486     /* package */ void setNotificationsAsSeen(int start, int end) {
487         start = Math.max(start, 0);
488         end = Math.min(end, mNotifications.size() - 1);
489 
490         if (mNotificationDataManager != null) {
491             List<AlertEntry> notifications = new ArrayList();
492             for (int i = start; i <= end; i++) {
493                 notifications.addAll(mNotifications.get(i).getChildNotifications());
494             }
495             mNotificationDataManager.setNotificationsAsSeen(notifications);
496         }
497     }
498 
499     @Override
getConfigurationId()500     public int getConfigurationId() {
501         return R.id.notification_list_uxr_config;
502     }
503 }
504