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