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