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