blob: 2b417f590a89e37a50e446dd62a731ddd577bcbb [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.notification;
import android.app.Notification;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.android.car.notification.template.BasicNotificationViewHolder;
import com.android.car.notification.template.CallNotificationViewHolder;
import com.android.car.notification.template.CarNotificationFooterViewHolder;
import com.android.car.notification.template.CarNotificationHeaderViewHolder;
import com.android.car.notification.template.EmergencyNotificationViewHolder;
import com.android.car.notification.template.GroupNotificationViewHolder;
import com.android.car.notification.template.GroupSummaryNotificationViewHolder;
import com.android.car.notification.template.InboxNotificationViewHolder;
import com.android.car.notification.template.MessageNotificationViewHolder;
import com.android.car.notification.template.ProgressNotificationViewHolder;
import java.util.ArrayList;
import java.util.List;
/**
* Notification data adapter that binds a notification to the corresponding view.
*/
public class CarNotificationViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = "CarNotificationAdapter";
// Delay in posting notifyDataSetChanged for the adapter in milliseconds.
private static final int NOTIFY_DATASET_CHANGED_DELAY = 100;
private final Context mContext;
private final LayoutInflater mInflater;
private final int mMaxNumberGroupChildrenShown;
private final boolean mIsGroupNotificationAdapter;
private final Handler mHandler = new Handler();
// book keeping expanded notification groups
private final List<String> mExpandedNotifications = new ArrayList<>();
private List<NotificationGroup> mNotifications = new ArrayList<>();
private RecyclerView.RecycledViewPool mViewPool;
private CarUxRestrictions mCarUxRestrictions;
private NotificationClickHandlerFactory mClickHandlerFactory;
private NotificationDataManager mNotificationDataManager;
private Runnable mNotifyDataSetChangedRunnable = this::notifyDataSetChanged;
/**
* Constructor for a notification adapter.
* Can be used both by the root notification list view, or a grouped notification view.
*
* @param context the context for resources and inflating views
* @param isGroupNotificationAdapter true if this adapter is used by a grouped notification view
*/
public CarNotificationViewAdapter(Context context, boolean isGroupNotificationAdapter) {
mContext = context;
mInflater = LayoutInflater.from(context);
mMaxNumberGroupChildrenShown =
mContext.getResources().getInteger(R.integer.max_group_children_number);
mIsGroupNotificationAdapter = isGroupNotificationAdapter;
setHasStableIds(true);
if (!mIsGroupNotificationAdapter) {
mViewPool = new RecyclerView.RecycledViewPool();
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RecyclerView.ViewHolder viewHolder;
View view;
switch (viewType) {
case NotificationViewType.HEADER:
view = mInflater.inflate(R.layout.notification_header_template, parent, false);
viewHolder = new CarNotificationHeaderViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.FOOTER:
view = mInflater.inflate(R.layout.notification_footer_template, parent, false);
viewHolder = new CarNotificationFooterViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.GROUP_EXPANDED:
case NotificationViewType.GROUP_COLLAPSED:
view = mInflater.inflate(
R.layout.group_notification_template, parent, false);
viewHolder = new GroupNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.GROUP_SUMMARY:
view = mInflater
.inflate(R.layout.group_summary_notification_template, parent, false);
viewHolder = new GroupSummaryNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.CALL:
view = mInflater
.inflate(R.layout.call_notification_template, parent, false);
viewHolder = new CallNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.CAR_EMERGENCY:
view = mInflater.inflate(
R.layout.car_emergency_notification_template, parent, false);
viewHolder = new EmergencyNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.CAR_WARNING:
view = mInflater.inflate(
R.layout.car_warning_notification_template, parent, false);
// Using the basic view holder because they share the same view binding logic
// OEMs should create view holders if needed
viewHolder = new BasicNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.CAR_INFORMATION:
view = mInflater.inflate(
R.layout.car_information_notification_template, parent, false);
// Using the basic view holder because they share the same view binding logic
// OEMs should create view holders if needed
viewHolder = new BasicNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.CAR_INFORMATION_IN_GROUP:
view = mInflater.inflate(
R.layout.car_information_notification_template_inner, parent, false);
// Using the basic view holder because they share the same view binding logic
// OEMs should create view holders if needed
viewHolder = new BasicNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.MESSAGE_IN_GROUP:
view = mInflater.inflate(
R.layout.message_notification_template_inner, parent, false);
viewHolder = new MessageNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.MESSAGE:
view = mInflater.inflate(R.layout.message_notification_template, parent, false);
viewHolder = new MessageNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.PROGRESS_IN_GROUP:
view = mInflater.inflate(
R.layout.progress_notification_template_inner, parent, false);
viewHolder = new ProgressNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.PROGRESS:
view = mInflater
.inflate(R.layout.progress_notification_template, parent, false);
viewHolder = new ProgressNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.INBOX_IN_GROUP:
view = mInflater
.inflate(R.layout.inbox_notification_template_inner, parent, false);
viewHolder = new InboxNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.INBOX:
view = mInflater
.inflate(R.layout.inbox_notification_template, parent, false);
viewHolder = new InboxNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.BASIC_IN_GROUP:
view = mInflater
.inflate(R.layout.basic_notification_template_inner, parent, false);
viewHolder = new BasicNotificationViewHolder(view, mClickHandlerFactory);
break;
case NotificationViewType.BASIC:
default:
view = mInflater
.inflate(R.layout.basic_notification_template, parent, false);
viewHolder = new BasicNotificationViewHolder(view, mClickHandlerFactory);
break;
}
return viewHolder;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
NotificationGroup notificationGroup = mNotifications.get(position);
switch (holder.getItemViewType()) {
case NotificationViewType.HEADER:
((CarNotificationHeaderViewHolder) holder).bind(hasNotifications());
break;
case NotificationViewType.FOOTER:
((CarNotificationFooterViewHolder) holder).bind(hasNotifications());
break;
case NotificationViewType.GROUP_EXPANDED:
((GroupNotificationViewHolder) holder)
.bind(notificationGroup, this, /* isExpanded= */ true);
break;
case NotificationViewType.GROUP_COLLAPSED:
((GroupNotificationViewHolder) holder)
.bind(notificationGroup, this, /* isExpanded= */ false);
break;
case NotificationViewType.GROUP_SUMMARY:
((GroupSummaryNotificationViewHolder) holder).bind(notificationGroup);
break;
case NotificationViewType.CALL: {
StatusBarNotification notification = notificationGroup.getSingleNotification();
((CallNotificationViewHolder) holder)
.bind(notification, /* isInGroup= */ false, /* isHeadsUp= */ false);
break;
}
case NotificationViewType.CAR_EMERGENCY: {
StatusBarNotification notification = notificationGroup.getSingleNotification();
((EmergencyNotificationViewHolder) holder)
.bind(notification, /* isInGroup= */ false, /* isHeadsUp= */ false);
break;
}
case NotificationViewType.MESSAGE: {
StatusBarNotification notification = notificationGroup.getSingleNotification();
if (shouldRestrictMessagePreview()) {
((MessageNotificationViewHolder) holder)
.bindRestricted(notification, /* isInGroup= */ false, /* isHeadsUp= */
false);
} else {
((MessageNotificationViewHolder) holder)
.bind(notification, /* isInGroup= */ false, /* isHeadsUp= */ false);
}
break;
}
case NotificationViewType.MESSAGE_IN_GROUP: {
StatusBarNotification notification = notificationGroup.getSingleNotification();
if (shouldRestrictMessagePreview()) {
((MessageNotificationViewHolder) holder)
.bindRestricted(notification, /* isInGroup= */ true, /* isHeadsUp= */
false);
} else {
((MessageNotificationViewHolder) holder)
.bind(notification, /* isInGroup= */ true, /* isHeadsUp= */ false);
}
break;
}
case NotificationViewType.PROGRESS: {
StatusBarNotification notification = notificationGroup.getSingleNotification();
((ProgressNotificationViewHolder) holder)
.bind(notification, /* isInGroup= */ false, false);
break;
}
case NotificationViewType.PROGRESS_IN_GROUP: {
StatusBarNotification notification = notificationGroup.getSingleNotification();
((ProgressNotificationViewHolder) holder).bind(notification, /* isInGroup= */
true, false);
break;
}
case NotificationViewType.INBOX: {
StatusBarNotification notification = notificationGroup.getSingleNotification();
((InboxNotificationViewHolder) holder).bind(notification, /* isInGroup= */ false,
/* isHeadsUp= */ false);
break;
}
case NotificationViewType.INBOX_IN_GROUP: {
StatusBarNotification notification = notificationGroup.getSingleNotification();
((InboxNotificationViewHolder) holder).bind(notification, /* isInGroup= */ true,
/* isHeadsUp= */ false);
break;
}
case NotificationViewType.CAR_INFORMATION_IN_GROUP:
case NotificationViewType.BASIC_IN_GROUP: {
StatusBarNotification notification = notificationGroup.getSingleNotification();
((BasicNotificationViewHolder) holder).bind(notification, /* isInGroup= */ true,
/* isHeadsUp= */ false);
break;
}
case NotificationViewType.CAR_WARNING:
case NotificationViewType.CAR_INFORMATION:
case NotificationViewType.BASIC:
default: {
StatusBarNotification notification = notificationGroup.getSingleNotification();
((BasicNotificationViewHolder) holder).bind(notification, /* isInGroup= */ false,
/* isHeadsUp= */ false);
break;
}
}
}
@Override
public int getItemViewType(int position) {
NotificationGroup notificationGroup = mNotifications.get(position);
if (notificationGroup.isHeader()) {
return NotificationViewType.HEADER;
}
if (notificationGroup.isFooter()) {
return NotificationViewType.FOOTER;
}
if (notificationGroup.isGroup()) {
if (mExpandedNotifications.contains(notificationGroup.getGroupKey())) {
return NotificationViewType.GROUP_EXPANDED;
} else {
return NotificationViewType.GROUP_COLLAPSED;
}
} else if (mExpandedNotifications.contains(notificationGroup.getGroupKey())) {
// when there are 2 notifications left in the expanded notification and one of them is
// removed at that time the item type changes from group to normal and hence the
// notification should be removed from expanded notifications.
setExpanded(notificationGroup.getGroupKey(), false);
}
Notification notification =
notificationGroup.getSingleNotification().getNotification();
Bundle extras = notification.extras;
String category = notification.category;
if (category != null) {
switch (category) {
case Notification.CATEGORY_CALL:
return NotificationViewType.CALL;
case Notification.CATEGORY_CAR_EMERGENCY:
return NotificationViewType.CAR_EMERGENCY;
case Notification.CATEGORY_CAR_WARNING:
return NotificationViewType.CAR_WARNING;
case Notification.CATEGORY_CAR_INFORMATION:
return mIsGroupNotificationAdapter
? NotificationViewType.CAR_INFORMATION_IN_GROUP
: NotificationViewType.CAR_INFORMATION;
case Notification.CATEGORY_MESSAGE:
return mIsGroupNotificationAdapter
? NotificationViewType.MESSAGE_IN_GROUP : NotificationViewType.MESSAGE;
default:
break;
}
}
// progress
int progressMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX);
boolean isIndeterminate = extras.getBoolean(
Notification.EXTRA_PROGRESS_INDETERMINATE);
boolean hasValidProgress = isIndeterminate || progressMax != 0;
boolean isProgress = extras.containsKey(Notification.EXTRA_PROGRESS)
&& extras.containsKey(Notification.EXTRA_PROGRESS_MAX)
&& hasValidProgress
&& !notification.hasCompletedProgress();
if (isProgress) {
return mIsGroupNotificationAdapter
? NotificationViewType.PROGRESS_IN_GROUP : NotificationViewType.PROGRESS;
}
// inbox
boolean isInbox = extras.containsKey(Notification.EXTRA_TITLE_BIG)
&& extras.containsKey(Notification.EXTRA_SUMMARY_TEXT);
if (isInbox) {
return mIsGroupNotificationAdapter
? NotificationViewType.INBOX_IN_GROUP : NotificationViewType.INBOX;
}
// group summary
boolean isGroupSummary = notificationGroup.getChildTitles() != null;
if (isGroupSummary) {
return NotificationViewType.GROUP_SUMMARY;
}
// the big text and big picture styles are fallen back to basic template in car
// i.e. setting the big text and big picture does not have an effect
boolean isBigText = extras.containsKey(Notification.EXTRA_BIG_TEXT);
if (isBigText) {
Log.i(TAG, "Big text style is not supported as a car notification");
}
boolean isBigPicture = extras.containsKey(Notification.EXTRA_PICTURE);
if (isBigPicture) {
Log.i(TAG, "Big picture style is not supported as a car notification");
}
// basic, big text, big picture
return mIsGroupNotificationAdapter
? NotificationViewType.BASIC_IN_GROUP : NotificationViewType.BASIC;
}
@Override
public int getItemCount() {
int itemCount = mNotifications.size();
if (mIsGroupNotificationAdapter && itemCount > mMaxNumberGroupChildrenShown) {
return mMaxNumberGroupChildrenShown;
}
if (!mIsGroupNotificationAdapter && mCarUxRestrictions != null
&& (mCarUxRestrictions.getActiveRestrictions()
& CarUxRestrictions.UX_RESTRICTIONS_LIMIT_CONTENT) != 0) {
int maxItemCount = mCarUxRestrictions.getMaxCumulativeContentItems();
return Math.min(itemCount, maxItemCount);
}
return itemCount;
}
@Override
public long getItemId(int position) {
NotificationGroup notificationGroup = mNotifications.get(position);
if (notificationGroup.isHeader()) {
return 0;
}
if (notificationGroup.isFooter()) {
return 1;
}
return notificationGroup.isGroup()
? notificationGroup.getGroupKey().hashCode()
: notificationGroup.getSingleNotification().getKey().hashCode();
}
/**
* Set the expansion state of a group notification given its group key.
*
* @param groupKey the unique identifier of a {@link NotificationGroup}
* @param isExpanded whether the group notification should be expanded.
*/
public void setExpanded(String groupKey, boolean isExpanded) {
if (isExpanded(groupKey) == isExpanded) {
return;
}
if (isExpanded) {
mExpandedNotifications.add(groupKey);
} else {
mExpandedNotifications.remove(groupKey);
}
}
/**
* Collapses all expanded groups.
*/
public void collapseAllGroups() {
if (!mExpandedNotifications.isEmpty()) {
mExpandedNotifications.clear();
}
}
/**
* Returns whether the notification is expanded given its group key.
*/
boolean isExpanded(String groupKey) {
return mExpandedNotifications.contains(groupKey);
}
/**
* Gets the current {@link CarUxRestrictions}.
*/
public CarUxRestrictions getCarUxRestrictions() {
return mCarUxRestrictions;
}
/**
* Clear all notifications.
*/
public void clearAllNotifications() {
mClickHandlerFactory.clearAllNotifications();
}
/**
* Updates notifications and update views.
*
* @param setRecyclerViewListHeaderAndFooter sets the header and footer on the entire list of
* items within the recycler view. This is NOT the header/footer for the grouped notifications.
*/
public void setNotifications(List<NotificationGroup> notifications,
boolean setRecyclerViewListHeaderAndFooter) {
List<NotificationGroup> notificationGroupList = new ArrayList<>(notifications);
if (setRecyclerViewListHeaderAndFooter) {
// add header as the first item of the list.
notificationGroupList.add(0, createNotificationHeader());
// add footer as the last item of the list.
notificationGroupList.add(createNotificationFooter());
}
mNotifications = notificationGroupList;
mHandler.removeCallbacks(mNotifyDataSetChangedRunnable);
mHandler.postDelayed(mNotifyDataSetChangedRunnable, NOTIFY_DATASET_CHANGED_DELAY);
}
/**
* Notification list has header and footer by default. Therefore the min number of items in the
* adapter will always be two. If there are any notifications present the size will be more than
* two.
*/
private boolean hasNotifications() {
return getItemCount() > 2;
}
private NotificationGroup createNotificationHeader() {
NotificationGroup notificationGroupWithHeader = new NotificationGroup();
notificationGroupWithHeader.setHeader(true);
notificationGroupWithHeader.setGroupKey("notification_header");
return notificationGroupWithHeader;
}
private NotificationGroup createNotificationFooter() {
NotificationGroup notificationGroupWithFooter = new NotificationGroup();
notificationGroupWithFooter.setFooter(true);
notificationGroupWithFooter.setGroupKey("notification_footer");
return notificationGroupWithFooter;
}
/**
* Sets the current {@link CarUxRestrictions}.
*/
public void setCarUxRestrictions(CarUxRestrictions carUxRestrictions) {
Log.d(TAG, "setCarUxRestrictions");
mCarUxRestrictions = carUxRestrictions;
notifyDataSetChanged();
}
/**
* Helper method that determines whether a notification is a messaging notification and
* should have restricted content (no message preview).
*/
private boolean shouldRestrictMessagePreview() {
return mCarUxRestrictions != null && (mCarUxRestrictions.getActiveRestrictions()
& CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE) != 0;
}
/**
* Get root recycler view's view pool so that the child recycler view can share the same
* view pool with the parent.
*/
public RecyclerView.RecycledViewPool getViewPool() {
if (mIsGroupNotificationAdapter) {
// currently only support one level of expansion.
throw new IllegalStateException("CarNotificationViewAdapter is a child adapter; "
+ "its view pool should not be reused.");
}
return mViewPool;
}
/**
* Sets the NotificationClickHandlerFactory that allows for a hook to run a block off code
* when the notification is clicked. This is useful to dismiss a screen after
* a notification list clicked.
*/
public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) {
mClickHandlerFactory = clickHandlerFactory;
}
/**
* Sets NotificationDataManager that handles additional states for notifications such as "seen",
* and muting a messaging type notification.
*
* @param notificationDataManager An instance of NotificationDataManager.
*/
public void setNotificationDataManager(NotificationDataManager notificationDataManager) {
mNotificationDataManager = notificationDataManager;
}
/**
* Set the notification group as seen.
*
* @param position Adapter position of the notification group.
*/
public void setNotificationAsSeen(int position) {
NotificationGroup notificationGroup = null;
try {
notificationGroup = mNotifications.get(position);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "trying to mark none existent notification as seen.");
return;
}
if (mNotificationDataManager != null) {
for (StatusBarNotification notification : notificationGroup.getChildNotifications()) {
mNotificationDataManager.setNotificationAsSeen(notification);
}
}
}
}