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.app.Notification; 19 import android.car.drivingstate.CarUxRestrictions; 20 import android.content.Context; 21 import android.os.Bundle; 22 import android.os.Handler; 23 import android.service.notification.StatusBarNotification; 24 import android.util.Log; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.view.ViewGroup; 28 29 import androidx.recyclerview.widget.RecyclerView; 30 31 import com.android.car.notification.template.BasicNotificationViewHolder; 32 import com.android.car.notification.template.CallNotificationViewHolder; 33 import com.android.car.notification.template.CarNotificationFooterViewHolder; 34 import com.android.car.notification.template.CarNotificationHeaderViewHolder; 35 import com.android.car.notification.template.EmergencyNotificationViewHolder; 36 import com.android.car.notification.template.GroupNotificationViewHolder; 37 import com.android.car.notification.template.GroupSummaryNotificationViewHolder; 38 import com.android.car.notification.template.InboxNotificationViewHolder; 39 import com.android.car.notification.template.MessageNotificationViewHolder; 40 import com.android.car.notification.template.ProgressNotificationViewHolder; 41 42 import java.util.ArrayList; 43 import java.util.List; 44 45 /** 46 * Notification data adapter that binds a notification to the corresponding view. 47 */ 48 public class CarNotificationViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { 49 private static final String TAG = "CarNotificationAdapter"; 50 51 // Delay in posting notifyDataSetChanged for the adapter in milliseconds. 52 private static final int NOTIFY_DATASET_CHANGED_DELAY = 100; 53 54 private final Context mContext; 55 private final LayoutInflater mInflater; 56 private final int mMaxNumberGroupChildrenShown; 57 private final boolean mIsGroupNotificationAdapter; 58 private final Handler mHandler = new Handler(); 59 60 // book keeping expanded notification groups 61 private final List<String> mExpandedNotifications = new ArrayList<>(); 62 63 private List<NotificationGroup> mNotifications = new ArrayList<>(); 64 private RecyclerView.RecycledViewPool mViewPool; 65 private CarUxRestrictions mCarUxRestrictions; 66 private NotificationClickHandlerFactory mClickHandlerFactory; 67 private NotificationDataManager mNotificationDataManager; 68 69 private Runnable mNotifyDataSetChangedRunnable = this::notifyDataSetChanged; 70 71 /** 72 * Constructor for a notification adapter. 73 * Can be used both by the root notification list view, or a grouped notification view. 74 * 75 * @param context the context for resources and inflating views 76 * @param isGroupNotificationAdapter true if this adapter is used by a grouped notification view 77 */ CarNotificationViewAdapter(Context context, boolean isGroupNotificationAdapter)78 public CarNotificationViewAdapter(Context context, boolean isGroupNotificationAdapter) { 79 mContext = context; 80 mInflater = LayoutInflater.from(context); 81 mMaxNumberGroupChildrenShown = 82 mContext.getResources().getInteger(R.integer.max_group_children_number); 83 mIsGroupNotificationAdapter = isGroupNotificationAdapter; 84 setHasStableIds(true); 85 if (!mIsGroupNotificationAdapter) { 86 mViewPool = new RecyclerView.RecycledViewPool(); 87 } 88 } 89 90 @Override onCreateViewHolder(ViewGroup parent, int viewType)91 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 92 RecyclerView.ViewHolder viewHolder; 93 View view; 94 switch (viewType) { 95 case NotificationViewType.HEADER: 96 view = mInflater.inflate(R.layout.notification_header_template, parent, false); 97 viewHolder = new CarNotificationHeaderViewHolder(view, mClickHandlerFactory); 98 break; 99 case NotificationViewType.FOOTER: 100 view = mInflater.inflate(R.layout.notification_footer_template, parent, false); 101 viewHolder = new CarNotificationFooterViewHolder(view, mClickHandlerFactory); 102 break; 103 case NotificationViewType.GROUP_EXPANDED: 104 case NotificationViewType.GROUP_COLLAPSED: 105 view = mInflater.inflate( 106 R.layout.group_notification_template, parent, false); 107 viewHolder = new GroupNotificationViewHolder(view, mClickHandlerFactory); 108 break; 109 case NotificationViewType.GROUP_SUMMARY: 110 view = mInflater 111 .inflate(R.layout.group_summary_notification_template, parent, false); 112 viewHolder = new GroupSummaryNotificationViewHolder(view, mClickHandlerFactory); 113 break; 114 case NotificationViewType.CALL: 115 view = mInflater 116 .inflate(R.layout.call_notification_template, parent, false); 117 viewHolder = new CallNotificationViewHolder(view, mClickHandlerFactory); 118 break; 119 case NotificationViewType.CAR_EMERGENCY: 120 view = mInflater.inflate( 121 R.layout.car_emergency_notification_template, parent, false); 122 viewHolder = new EmergencyNotificationViewHolder(view, mClickHandlerFactory); 123 break; 124 case NotificationViewType.CAR_WARNING: 125 view = mInflater.inflate( 126 R.layout.car_warning_notification_template, parent, false); 127 // Using the basic view holder because they share the same view binding logic 128 // OEMs should create view holders if needed 129 viewHolder = new BasicNotificationViewHolder(view, mClickHandlerFactory); 130 break; 131 case NotificationViewType.CAR_INFORMATION: 132 view = mInflater.inflate( 133 R.layout.car_information_notification_template, parent, false); 134 // Using the basic view holder because they share the same view binding logic 135 // OEMs should create view holders if needed 136 viewHolder = new BasicNotificationViewHolder(view, mClickHandlerFactory); 137 break; 138 case NotificationViewType.CAR_INFORMATION_IN_GROUP: 139 view = mInflater.inflate( 140 R.layout.car_information_notification_template_inner, parent, false); 141 // Using the basic view holder because they share the same view binding logic 142 // OEMs should create view holders if needed 143 viewHolder = new BasicNotificationViewHolder(view, mClickHandlerFactory); 144 break; 145 case NotificationViewType.MESSAGE_IN_GROUP: 146 view = mInflater.inflate( 147 R.layout.message_notification_template_inner, parent, false); 148 viewHolder = new MessageNotificationViewHolder(view, mClickHandlerFactory); 149 break; 150 case NotificationViewType.MESSAGE: 151 view = mInflater.inflate(R.layout.message_notification_template, parent, false); 152 viewHolder = new MessageNotificationViewHolder(view, mClickHandlerFactory); 153 break; 154 case NotificationViewType.PROGRESS_IN_GROUP: 155 view = mInflater.inflate( 156 R.layout.progress_notification_template_inner, parent, false); 157 viewHolder = new ProgressNotificationViewHolder(view, mClickHandlerFactory); 158 break; 159 case NotificationViewType.PROGRESS: 160 view = mInflater 161 .inflate(R.layout.progress_notification_template, parent, false); 162 viewHolder = new ProgressNotificationViewHolder(view, mClickHandlerFactory); 163 break; 164 case NotificationViewType.INBOX_IN_GROUP: 165 view = mInflater 166 .inflate(R.layout.inbox_notification_template_inner, parent, false); 167 viewHolder = new InboxNotificationViewHolder(view, mClickHandlerFactory); 168 break; 169 case NotificationViewType.INBOX: 170 view = mInflater 171 .inflate(R.layout.inbox_notification_template, parent, false); 172 viewHolder = new InboxNotificationViewHolder(view, mClickHandlerFactory); 173 break; 174 case NotificationViewType.BASIC_IN_GROUP: 175 view = mInflater 176 .inflate(R.layout.basic_notification_template_inner, parent, false); 177 viewHolder = new BasicNotificationViewHolder(view, mClickHandlerFactory); 178 break; 179 case NotificationViewType.BASIC: 180 default: 181 view = mInflater 182 .inflate(R.layout.basic_notification_template, parent, false); 183 viewHolder = new BasicNotificationViewHolder(view, mClickHandlerFactory); 184 break; 185 } 186 return viewHolder; 187 } 188 189 @Override onBindViewHolder(RecyclerView.ViewHolder holder, int position)190 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 191 NotificationGroup notificationGroup = mNotifications.get(position); 192 193 switch (holder.getItemViewType()) { 194 case NotificationViewType.HEADER: 195 ((CarNotificationHeaderViewHolder) holder).bind(hasNotifications()); 196 break; 197 case NotificationViewType.FOOTER: 198 ((CarNotificationFooterViewHolder) holder).bind(hasNotifications()); 199 break; 200 case NotificationViewType.GROUP_EXPANDED: 201 ((GroupNotificationViewHolder) holder) 202 .bind(notificationGroup, this, /* isExpanded= */ true); 203 break; 204 case NotificationViewType.GROUP_COLLAPSED: 205 ((GroupNotificationViewHolder) holder) 206 .bind(notificationGroup, this, /* isExpanded= */ false); 207 break; 208 case NotificationViewType.GROUP_SUMMARY: 209 ((GroupSummaryNotificationViewHolder) holder).bind(notificationGroup); 210 break; 211 case NotificationViewType.CALL: { 212 StatusBarNotification notification = notificationGroup.getSingleNotification(); 213 ((CallNotificationViewHolder) holder) 214 .bind(notification, /* isInGroup= */ false, /* isHeadsUp= */ false); 215 break; 216 } 217 case NotificationViewType.CAR_EMERGENCY: { 218 StatusBarNotification notification = notificationGroup.getSingleNotification(); 219 ((EmergencyNotificationViewHolder) holder) 220 .bind(notification, /* isInGroup= */ false, /* isHeadsUp= */ false); 221 break; 222 } 223 case NotificationViewType.MESSAGE: { 224 StatusBarNotification notification = notificationGroup.getSingleNotification(); 225 if (shouldRestrictMessagePreview()) { 226 ((MessageNotificationViewHolder) holder) 227 .bindRestricted(notification, /* isInGroup= */ false, /* isHeadsUp= */ 228 false); 229 } else { 230 ((MessageNotificationViewHolder) holder) 231 .bind(notification, /* isInGroup= */ false, /* isHeadsUp= */ false); 232 } 233 break; 234 } 235 case NotificationViewType.MESSAGE_IN_GROUP: { 236 StatusBarNotification notification = notificationGroup.getSingleNotification(); 237 if (shouldRestrictMessagePreview()) { 238 ((MessageNotificationViewHolder) holder) 239 .bindRestricted(notification, /* isInGroup= */ true, /* isHeadsUp= */ 240 false); 241 } else { 242 ((MessageNotificationViewHolder) holder) 243 .bind(notification, /* isInGroup= */ true, /* isHeadsUp= */ false); 244 } 245 break; 246 } 247 case NotificationViewType.PROGRESS: { 248 StatusBarNotification notification = notificationGroup.getSingleNotification(); 249 ((ProgressNotificationViewHolder) holder) 250 .bind(notification, /* isInGroup= */ false, false); 251 break; 252 } 253 case NotificationViewType.PROGRESS_IN_GROUP: { 254 StatusBarNotification notification = notificationGroup.getSingleNotification(); 255 ((ProgressNotificationViewHolder) holder).bind(notification, /* isInGroup= */ 256 true, false); 257 break; 258 } 259 case NotificationViewType.INBOX: { 260 StatusBarNotification notification = notificationGroup.getSingleNotification(); 261 ((InboxNotificationViewHolder) holder).bind(notification, /* isInGroup= */ false, 262 /* isHeadsUp= */ false); 263 break; 264 } 265 case NotificationViewType.INBOX_IN_GROUP: { 266 StatusBarNotification notification = notificationGroup.getSingleNotification(); 267 ((InboxNotificationViewHolder) holder).bind(notification, /* isInGroup= */ true, 268 /* isHeadsUp= */ false); 269 break; 270 } 271 case NotificationViewType.CAR_INFORMATION_IN_GROUP: 272 case NotificationViewType.BASIC_IN_GROUP: { 273 StatusBarNotification notification = notificationGroup.getSingleNotification(); 274 ((BasicNotificationViewHolder) holder).bind(notification, /* isInGroup= */ true, 275 /* isHeadsUp= */ false); 276 break; 277 } 278 case NotificationViewType.CAR_WARNING: 279 case NotificationViewType.CAR_INFORMATION: 280 case NotificationViewType.BASIC: 281 default: { 282 StatusBarNotification notification = notificationGroup.getSingleNotification(); 283 ((BasicNotificationViewHolder) holder).bind(notification, /* isInGroup= */ false, 284 /* isHeadsUp= */ false); 285 break; 286 } 287 } 288 } 289 290 @Override getItemViewType(int position)291 public int getItemViewType(int position) { 292 NotificationGroup notificationGroup = mNotifications.get(position); 293 294 if (notificationGroup.isHeader()) { 295 return NotificationViewType.HEADER; 296 } 297 298 if (notificationGroup.isFooter()) { 299 return NotificationViewType.FOOTER; 300 } 301 302 if (notificationGroup.isGroup()) { 303 if (mExpandedNotifications.contains(notificationGroup.getGroupKey())) { 304 return NotificationViewType.GROUP_EXPANDED; 305 } else { 306 return NotificationViewType.GROUP_COLLAPSED; 307 } 308 } else if (mExpandedNotifications.contains(notificationGroup.getGroupKey())) { 309 // when there are 2 notifications left in the expanded notification and one of them is 310 // removed at that time the item type changes from group to normal and hence the 311 // notification should be removed from expanded notifications. 312 setExpanded(notificationGroup.getGroupKey(), false); 313 } 314 315 Notification notification = 316 notificationGroup.getSingleNotification().getNotification(); 317 Bundle extras = notification.extras; 318 319 String category = notification.category; 320 if (category != null) { 321 switch (category) { 322 case Notification.CATEGORY_CALL: 323 return NotificationViewType.CALL; 324 case Notification.CATEGORY_CAR_EMERGENCY: 325 return NotificationViewType.CAR_EMERGENCY; 326 case Notification.CATEGORY_CAR_WARNING: 327 return NotificationViewType.CAR_WARNING; 328 case Notification.CATEGORY_CAR_INFORMATION: 329 return mIsGroupNotificationAdapter 330 ? NotificationViewType.CAR_INFORMATION_IN_GROUP 331 : NotificationViewType.CAR_INFORMATION; 332 case Notification.CATEGORY_MESSAGE: 333 return mIsGroupNotificationAdapter 334 ? NotificationViewType.MESSAGE_IN_GROUP : NotificationViewType.MESSAGE; 335 default: 336 break; 337 } 338 } 339 340 // progress 341 int progressMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX); 342 boolean isIndeterminate = extras.getBoolean( 343 Notification.EXTRA_PROGRESS_INDETERMINATE); 344 boolean hasValidProgress = isIndeterminate || progressMax != 0; 345 boolean isProgress = extras.containsKey(Notification.EXTRA_PROGRESS) 346 && extras.containsKey(Notification.EXTRA_PROGRESS_MAX) 347 && hasValidProgress 348 && !notification.hasCompletedProgress(); 349 if (isProgress) { 350 return mIsGroupNotificationAdapter 351 ? NotificationViewType.PROGRESS_IN_GROUP : NotificationViewType.PROGRESS; 352 } 353 354 // inbox 355 boolean isInbox = extras.containsKey(Notification.EXTRA_TITLE_BIG) 356 && extras.containsKey(Notification.EXTRA_SUMMARY_TEXT); 357 if (isInbox) { 358 return mIsGroupNotificationAdapter 359 ? NotificationViewType.INBOX_IN_GROUP : NotificationViewType.INBOX; 360 } 361 362 // group summary 363 boolean isGroupSummary = notificationGroup.getChildTitles() != null; 364 if (isGroupSummary) { 365 return NotificationViewType.GROUP_SUMMARY; 366 } 367 368 // the big text and big picture styles are fallen back to basic template in car 369 // i.e. setting the big text and big picture does not have an effect 370 boolean isBigText = extras.containsKey(Notification.EXTRA_BIG_TEXT); 371 if (isBigText) { 372 Log.i(TAG, "Big text style is not supported as a car notification"); 373 } 374 boolean isBigPicture = extras.containsKey(Notification.EXTRA_PICTURE); 375 if (isBigPicture) { 376 Log.i(TAG, "Big picture style is not supported as a car notification"); 377 } 378 379 // basic, big text, big picture 380 return mIsGroupNotificationAdapter 381 ? NotificationViewType.BASIC_IN_GROUP : NotificationViewType.BASIC; 382 } 383 384 @Override getItemCount()385 public int getItemCount() { 386 int itemCount = mNotifications.size(); 387 388 if (mIsGroupNotificationAdapter && itemCount > mMaxNumberGroupChildrenShown) { 389 return mMaxNumberGroupChildrenShown; 390 } 391 392 if (!mIsGroupNotificationAdapter && mCarUxRestrictions != null 393 && (mCarUxRestrictions.getActiveRestrictions() 394 & CarUxRestrictions.UX_RESTRICTIONS_LIMIT_CONTENT) != 0) { 395 396 int maxItemCount = mCarUxRestrictions.getMaxCumulativeContentItems(); 397 398 return Math.min(itemCount, maxItemCount); 399 } 400 return itemCount; 401 } 402 403 @Override getItemId(int position)404 public long getItemId(int position) { 405 NotificationGroup notificationGroup = mNotifications.get(position); 406 if (notificationGroup.isHeader()) { 407 return 0; 408 } 409 410 if (notificationGroup.isFooter()) { 411 return 1; 412 } 413 414 return notificationGroup.isGroup() 415 ? notificationGroup.getGroupKey().hashCode() 416 : notificationGroup.getSingleNotification().getKey().hashCode(); 417 } 418 419 /** 420 * Set the expansion state of a group notification given its group key. 421 * 422 * @param groupKey the unique identifier of a {@link NotificationGroup} 423 * @param isExpanded whether the group notification should be expanded. 424 */ setExpanded(String groupKey, boolean isExpanded)425 public void setExpanded(String groupKey, boolean isExpanded) { 426 if (isExpanded(groupKey) == isExpanded) { 427 return; 428 } 429 430 if (isExpanded) { 431 mExpandedNotifications.add(groupKey); 432 } else { 433 mExpandedNotifications.remove(groupKey); 434 } 435 } 436 437 /** 438 * Collapses all expanded groups. 439 */ collapseAllGroups()440 public void collapseAllGroups() { 441 if (!mExpandedNotifications.isEmpty()) { 442 mExpandedNotifications.clear(); 443 } 444 } 445 446 /** 447 * Returns whether the notification is expanded given its group key. 448 */ isExpanded(String groupKey)449 boolean isExpanded(String groupKey) { 450 return mExpandedNotifications.contains(groupKey); 451 } 452 453 /** 454 * Gets the current {@link CarUxRestrictions}. 455 */ getCarUxRestrictions()456 public CarUxRestrictions getCarUxRestrictions() { 457 return mCarUxRestrictions; 458 } 459 460 /** 461 * Clear all notifications. 462 */ clearAllNotifications()463 public void clearAllNotifications() { 464 mClickHandlerFactory.clearAllNotifications(); 465 } 466 467 /** 468 * Updates notifications and update views. 469 * 470 * @param setRecyclerViewListHeaderAndFooter sets the header and footer on the entire list of 471 * items within the recycler view. This is NOT the header/footer for the grouped notifications. 472 */ setNotifications(List<NotificationGroup> notifications, boolean setRecyclerViewListHeaderAndFooter)473 public void setNotifications(List<NotificationGroup> notifications, 474 boolean setRecyclerViewListHeaderAndFooter) { 475 476 List<NotificationGroup> notificationGroupList = new ArrayList<>(notifications); 477 478 if (setRecyclerViewListHeaderAndFooter) { 479 // add header as the first item of the list. 480 notificationGroupList.add(0, createNotificationHeader()); 481 // add footer as the last item of the list. 482 notificationGroupList.add(createNotificationFooter()); 483 } 484 485 mNotifications = notificationGroupList; 486 487 mHandler.removeCallbacks(mNotifyDataSetChangedRunnable); 488 mHandler.postDelayed(mNotifyDataSetChangedRunnable, NOTIFY_DATASET_CHANGED_DELAY); 489 } 490 491 /** 492 * Notification list has header and footer by default. Therefore the min number of items in the 493 * adapter will always be two. If there are any notifications present the size will be more than 494 * two. 495 */ hasNotifications()496 private boolean hasNotifications() { 497 return getItemCount() > 2; 498 } 499 createNotificationHeader()500 private NotificationGroup createNotificationHeader() { 501 NotificationGroup notificationGroupWithHeader = new NotificationGroup(); 502 notificationGroupWithHeader.setHeader(true); 503 notificationGroupWithHeader.setGroupKey("notification_header"); 504 return notificationGroupWithHeader; 505 } 506 createNotificationFooter()507 private NotificationGroup createNotificationFooter() { 508 NotificationGroup notificationGroupWithFooter = new NotificationGroup(); 509 notificationGroupWithFooter.setFooter(true); 510 notificationGroupWithFooter.setGroupKey("notification_footer"); 511 return notificationGroupWithFooter; 512 } 513 514 /** 515 * Sets the current {@link CarUxRestrictions}. 516 */ setCarUxRestrictions(CarUxRestrictions carUxRestrictions)517 public void setCarUxRestrictions(CarUxRestrictions carUxRestrictions) { 518 Log.d(TAG, "setCarUxRestrictions"); 519 mCarUxRestrictions = carUxRestrictions; 520 notifyDataSetChanged(); 521 } 522 523 /** 524 * Helper method that determines whether a notification is a messaging notification and 525 * should have restricted content (no message preview). 526 */ shouldRestrictMessagePreview()527 private boolean shouldRestrictMessagePreview() { 528 return mCarUxRestrictions != null && (mCarUxRestrictions.getActiveRestrictions() 529 & CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE) != 0; 530 } 531 532 /** 533 * Get root recycler view's view pool so that the child recycler view can share the same 534 * view pool with the parent. 535 */ getViewPool()536 public RecyclerView.RecycledViewPool getViewPool() { 537 if (mIsGroupNotificationAdapter) { 538 // currently only support one level of expansion. 539 throw new IllegalStateException("CarNotificationViewAdapter is a child adapter; " 540 + "its view pool should not be reused."); 541 } 542 return mViewPool; 543 } 544 545 /** 546 * Sets the NotificationClickHandlerFactory that allows for a hook to run a block off code 547 * when the notification is clicked. This is useful to dismiss a screen after 548 * a notification list clicked. 549 */ setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory)550 public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) { 551 mClickHandlerFactory = clickHandlerFactory; 552 } 553 554 /** 555 * Sets NotificationDataManager that handles additional states for notifications such as "seen", 556 * and muting a messaging type notification. 557 * 558 * @param notificationDataManager An instance of NotificationDataManager. 559 */ setNotificationDataManager(NotificationDataManager notificationDataManager)560 public void setNotificationDataManager(NotificationDataManager notificationDataManager) { 561 mNotificationDataManager = notificationDataManager; 562 } 563 564 /** 565 * Set the notification group as seen. 566 * 567 * @param position Adapter position of the notification group. 568 */ setNotificationAsSeen(int position)569 public void setNotificationAsSeen(int position) { 570 NotificationGroup notificationGroup = null; 571 572 try { 573 notificationGroup = mNotifications.get(position); 574 } catch (IndexOutOfBoundsException e) { 575 Log.e(TAG, "trying to mark none existent notification as seen."); 576 return; 577 } 578 579 if (mNotificationDataManager != null) { 580 for (StatusBarNotification notification : notificationGroup.getChildNotifications()) { 581 mNotificationDataManager.setNotificationAsSeen(notification); 582 } 583 } 584 } 585 } 586