blob: 57e650beb8c9f931ebdac38716a313afc712a4ff [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.annotation.NonNull;
import android.app.Notification;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.car.notification.template.CarNotificationBaseViewHolder;
import com.android.car.notification.template.CarNotificationFooterViewHolder;
import com.android.car.notification.template.CarNotificationHeaderViewHolder;
import com.android.car.notification.template.CarNotificationOlderViewHolder;
import com.android.car.notification.template.CarNotificationRecentsViewHolder;
import com.android.car.notification.template.GroupNotificationViewHolder;
import com.android.car.notification.template.GroupSummaryNotificationViewHolder;
import com.android.car.notification.template.MessageNotificationViewHolder;
import com.android.car.ui.recyclerview.ContentLimitingAdapter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Notification data adapter that binds a notification to the corresponding view.
*/
public class CarNotificationViewAdapter extends ContentLimitingAdapter<RecyclerView.ViewHolder>
implements PreprocessingManager.CallStateListener {
private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG;
private static final String TAG = "CarNotificationAdapter";
private static final int ID_HEADER = 0;
private static final int ID_RECENT_HEADER = 1;
private static final int ID_OLDER_HEADER = 2;
private static final int ID_FOOTER = 3;
private final Context mContext;
private final LayoutInflater mInflater;
private final int mMaxNumberGroupChildrenShown;
private final boolean mIsGroupNotificationAdapter;
private final boolean mShowRecentsAndOlderHeaders;
// book keeping expanded notification groups
private final List<ExpandedNotification> mExpandedNotifications = new ArrayList<>();
private final CarNotificationItemController mNotificationItemController;
private List<NotificationGroup> mNotifications = new ArrayList<>();
private Map<String, Integer> mGroupKeyToCountMap = new HashMap<>();
private LinearLayoutManager mLayoutManager;
private RecyclerView.RecycledViewPool mViewPool;
private CarUxRestrictions mCarUxRestrictions;
private NotificationClickHandlerFactory mClickHandlerFactory;
private NotificationDataManager mNotificationDataManager;
private boolean mIsInCall;
private boolean mHasHeaderAndFooter;
private boolean mHasUnseenNotifications;
private boolean mHasSeenNotifications;
private int mMaxItems = ContentLimitingAdapter.UNLIMITED;
/**
* 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
* @param notificationItemController shared logic to control notification items.
*/
public CarNotificationViewAdapter(Context context, boolean isGroupNotificationAdapter,
@Nullable CarNotificationItemController notificationItemController) {
mContext = context;
mInflater = LayoutInflater.from(context);
mMaxNumberGroupChildrenShown =
mContext.getResources().getInteger(R.integer.max_group_children_number);
mShowRecentsAndOlderHeaders =
mContext.getResources().getBoolean(R.bool.config_showRecentAndOldHeaders);
mIsGroupNotificationAdapter = isGroupNotificationAdapter;
mNotificationItemController = notificationItemController;
mNotificationDataManager = NotificationDataManager.getInstance();
setHasStableIds(true);
if (!mIsGroupNotificationAdapter) {
mViewPool = new RecyclerView.RecycledViewPool();
}
PreprocessingManager.getInstance(context).addCallStateListener(this::onCallStateChanged);
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
mLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
}
@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
mLayoutManager = null;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolderImpl(@NonNull 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(mContext, view,
mNotificationItemController, mClickHandlerFactory);
break;
case NotificationViewType.FOOTER:
view = mInflater.inflate(R.layout.notification_footer_template, parent, false);
viewHolder = new CarNotificationFooterViewHolder(mContext, view,
mNotificationItemController, mClickHandlerFactory);
break;
case NotificationViewType.RECENTS:
view = mInflater.inflate(R.layout.notification_recents_template, parent, false);
viewHolder = new CarNotificationRecentsViewHolder(mContext, view,
mNotificationItemController);
break;
case NotificationViewType.OLDER:
view = mInflater.inflate(R.layout.notification_older_template, parent, false);
viewHolder = new CarNotificationOlderViewHolder(mContext, view,
mNotificationItemController);
break;
default:
CarNotificationTypeItem carNotificationTypeItem = CarNotificationTypeItem.of(
viewType);
view = mInflater.inflate(
carNotificationTypeItem.getNotificationCenterTemplate(), parent, false);
viewHolder = carNotificationTypeItem.getViewHolder(view, mClickHandlerFactory);
}
return viewHolder;
}
@Override
public void onBindViewHolderImpl(RecyclerView.ViewHolder holder, int position) {
NotificationGroup notificationGroup = mNotifications.get(position);
int viewType = holder.getItemViewType();
switch (viewType) {
case NotificationViewType.HEADER:
((CarNotificationHeaderViewHolder) holder).bind(hasNotifications());
return;
case NotificationViewType.FOOTER:
((CarNotificationFooterViewHolder) holder).bind(hasNotifications());
return;
case NotificationViewType.RECENTS:
((CarNotificationRecentsViewHolder) holder).bind(mHasUnseenNotifications);
return;
case NotificationViewType.OLDER:
((CarNotificationOlderViewHolder) holder)
.bind(mHasSeenNotifications, !mHasUnseenNotifications);
return;
case NotificationViewType.GROUP:
((GroupNotificationViewHolder) holder)
.bind(notificationGroup, this, /* isExpanded= */
isExpanded(notificationGroup.getGroupKey(),
notificationGroup.isSeen()));
return;
case NotificationViewType.GROUP_SUMMARY:
((CarNotificationBaseViewHolder) holder).setHideDismissButton(true);
((GroupSummaryNotificationViewHolder) holder).bind(notificationGroup);
return;
}
CarNotificationTypeItem carNotificationTypeItem = CarNotificationTypeItem.of(viewType);
AlertEntry alertEntry = notificationGroup.getSingleNotification();
if (shouldRestrictMessagePreview() && (viewType == NotificationViewType.MESSAGE
|| viewType == NotificationViewType.MESSAGE_IN_GROUP)) {
((MessageNotificationViewHolder) holder)
.bindRestricted(alertEntry, /* isInGroup= */ false, /* isHeadsUp= */false);
} else {
carNotificationTypeItem.bind(alertEntry, false, (CarNotificationBaseViewHolder) holder);
}
}
@Override
public int getItemViewTypeImpl(int position) {
NotificationGroup notificationGroup = mNotifications.get(position);
if (notificationGroup.isHeader()) {
return NotificationViewType.HEADER;
}
if (notificationGroup.isFooter()) {
return NotificationViewType.FOOTER;
}
if (notificationGroup.isRecentsHeader()) {
return NotificationViewType.RECENTS;
}
if (notificationGroup.isOlderHeader()) {
return NotificationViewType.OLDER;
}
ExpandedNotification expandedNotification =
new ExpandedNotification(notificationGroup.getGroupKey(),
notificationGroup.isSeen());
if (notificationGroup.isGroup()) {
return NotificationViewType.GROUP;
} else if (mExpandedNotifications.contains(expandedNotification)) {
// 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(expandedNotification.getKey(), expandedNotification.isExpanded(),
/* isExpanded= */ 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 getUnrestrictedItemCount() {
return mNotifications.size();
}
@Override
public void setMaxItems(int maxItems) {
if (maxItems == ContentLimitingAdapter.UNLIMITED
|| (!mHasHeaderAndFooter && !mHasUnseenNotifications && !mHasSeenNotifications)) {
mMaxItems = maxItems;
} else {
// Adding to max limit of notifications for each header so that they do not count
// towards limit.
// Footer is not accounted for since it as the end of the list and it doesn't affect the
// limit of notifications above it.
mMaxItems = maxItems;
if (mHasHeaderAndFooter) {
mMaxItems++;
}
if (mHasSeenNotifications) {
mMaxItems++;
}
if (mHasUnseenNotifications) {
mMaxItems++;
}
}
super.setMaxItems(mMaxItems);
}
@Override
protected int getScrollToPositionWhenRestricted() {
if (mLayoutManager == null) {
return -1;
}
int firstItem = mLayoutManager.findFirstVisibleItemPosition();
if (firstItem >= getItemCount() - 1) {
return getItemCount() - 1;
}
return -1;
}
@Override
public long getItemId(int position) {
NotificationGroup notificationGroup = mNotifications.get(position);
if (notificationGroup.isHeader()) {
return ID_HEADER;
}
if (mShowRecentsAndOlderHeaders && !mIsGroupNotificationAdapter) {
if (notificationGroup.isRecentsHeader()) {
return ID_RECENT_HEADER;
}
if (notificationGroup.isOlderHeader()) {
return ID_OLDER_HEADER;
}
if (notificationGroup.isFooter()) {
return ID_FOOTER;
}
}
if (notificationGroup.isFooter()) {
// We can use recent header's ID when it isn't being used.
return ID_RECENT_HEADER;
}
String key = notificationGroup.isGroup()
? notificationGroup.getGroupKey()
: notificationGroup.getSingleNotification().getKey();
if (mShowRecentsAndOlderHeaders) {
key += notificationGroup.isSeen();
}
return key.hashCode();
}
/**
* Set the expansion state of a group notification given its group key.
*
* @param groupKey the unique identifier of a {@link NotificationGroup}
* @param isSeen whether the {@link NotificationGroup} has been seen by the user
* @param isExpanded whether the group notification should be expanded.
*/
public void setExpanded(String groupKey, boolean isSeen, boolean isExpanded) {
if (isExpanded(groupKey, isSeen) == isExpanded) {
return;
}
ExpandedNotification expandedNotification = new ExpandedNotification(groupKey, isSeen);
if (isExpanded) {
mExpandedNotifications.add(expandedNotification);
} else {
mExpandedNotifications.remove(expandedNotification);
}
if (DEBUG) {
Log.d(TAG, "Expanded notification statuses: " + mExpandedNotifications);
}
}
/**
* Collapses all expanded groups.
*/
public void collapseAllGroups() {
if (!mExpandedNotifications.isEmpty()) {
mExpandedNotifications.clear();
}
}
/**
* Returns whether the notification is expanded given its group key and it's seen status.
*
* @param groupKey the unique identifier of a {@link NotificationGroup}
* @param isSeen whether the {@link NotificationGroup} has been seen by the user
*/
boolean isExpanded(String groupKey, boolean isSeen) {
ExpandedNotification expandedNotification = new ExpandedNotification(groupKey, isSeen);
return mExpandedNotifications.contains(expandedNotification);
}
/**
* Gets the current {@link CarUxRestrictions}.
*/
public CarUxRestrictions getCarUxRestrictions() {
return mCarUxRestrictions;
}
/**
* Updates notifications and update views.
*
* @param setRecyclerViewListHeadersAndFooters 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 setRecyclerViewListHeadersAndFooters) {
mGroupKeyToCountMap.clear();
notifications.forEach(notificationGroup -> {
if ((mGroupKeyToCountMap.computeIfPresent(notificationGroup.getGroupKey(),
(key, currentValue) -> currentValue + 1)) == null) {
mGroupKeyToCountMap.put(notificationGroup.getGroupKey(), 1);
}
});
if (mShowRecentsAndOlderHeaders && !mIsGroupNotificationAdapter) {
List<NotificationGroup> seenNotifications = new ArrayList<>();
List<NotificationGroup> unseenNotifications = new ArrayList<>();
notifications.forEach(notificationGroup -> {
if (notificationGroup.isSeen()) {
seenNotifications.add(new NotificationGroup(notificationGroup));
} else {
unseenNotifications.add(new NotificationGroup(notificationGroup));
}
});
setSeenAndUnseenNotifications(unseenNotifications, seenNotifications,
setRecyclerViewListHeadersAndFooters);
return;
}
List<NotificationGroup> notificationGroupList = notifications.stream()
.map(notificationGroup -> new NotificationGroup(notificationGroup))
.collect(Collectors.toList());
if (setRecyclerViewListHeadersAndFooters) {
// 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());
mHasHeaderAndFooter = true;
} else {
mHasHeaderAndFooter = false;
}
CarNotificationDiff notificationDiff =
new CarNotificationDiff(mContext, mNotifications, notificationGroupList, mMaxItems);
notificationDiff.setShowRecentsAndOlderHeaders(false);
DiffUtil.DiffResult diffResult =
DiffUtil.calculateDiff(notificationDiff, /* detectMoves= */ false);
mNotifications = notificationGroupList;
if (DEBUG) {
Log.d(TAG, "Updated adapter view holders: " + mNotifications);
}
updateUnderlyingDataChanged(getUnrestrictedItemCount(), /* newAnchorIndex= */ 0);
diffResult.dispatchUpdatesTo(this);
}
private void setSeenAndUnseenNotifications(List<NotificationGroup> unseenNotifications,
List<NotificationGroup> seenNotifications,
boolean setRecyclerViewListHeadersAndFooters) {
if (DEBUG) {
Log.d(TAG, "Seen notifications: " + seenNotifications);
Log.d(TAG, "Unseen notifications: " + unseenNotifications);
}
List<NotificationGroup> notificationGroupList;
if (unseenNotifications.isEmpty()) {
mHasUnseenNotifications = false;
notificationGroupList = new ArrayList<>();
} else {
mHasUnseenNotifications = true;
notificationGroupList = new ArrayList<>(unseenNotifications);
if (setRecyclerViewListHeadersAndFooters) {
// Add recents header as the first item of the list.
notificationGroupList.add(/* index= */ 0, createRecentsHeader());
}
}
if (seenNotifications.isEmpty()) {
mHasSeenNotifications = false;
} else {
mHasSeenNotifications = true;
if (setRecyclerViewListHeadersAndFooters) {
// Append older header to the list.
notificationGroupList.add(createOlderHeader());
}
notificationGroupList.addAll(seenNotifications);
}
if (setRecyclerViewListHeadersAndFooters) {
// 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());
mHasHeaderAndFooter = true;
} else {
mHasHeaderAndFooter = false;
}
CarNotificationDiff notificationDiff =
new CarNotificationDiff(mContext, mNotifications, notificationGroupList, mMaxItems);
notificationDiff.setShowRecentsAndOlderHeaders(true);
DiffUtil.DiffResult diffResult =
DiffUtil.calculateDiff(notificationDiff, /* detectMoves= */ false);
mNotifications = notificationGroupList;
if (DEBUG) {
Log.d(TAG, "Updated adapter view holders: " + mNotifications);
}
updateUnderlyingDataChanged(getUnrestrictedItemCount(), /* newAnchorIndex= */ 0);
diffResult.dispatchUpdatesTo(this);
}
/**
* Returns {@code true} if notifications are present in adapter.
*
* Group notification list doesn't have any headers, hence, if there are any notifications
* present the size will be more than zero.
*
* Non-group 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.
*
* When recent and older headers are enabled, each header will be accounted for when checking
* for the presence of notifications.
*/
public boolean hasNotifications() {
int numberOfHeaders;
if (mIsGroupNotificationAdapter) {
numberOfHeaders = 0;
} else {
numberOfHeaders = 2;
if (mHasSeenNotifications) {
numberOfHeaders++;
}
if (mHasUnseenNotifications) {
numberOfHeaders++;
}
}
return getItemCount() > numberOfHeaders;
}
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;
}
private NotificationGroup createRecentsHeader() {
NotificationGroup notificationGroupWithRecents = new NotificationGroup();
notificationGroupWithRecents.setRecentsHeader(true);
notificationGroupWithRecents.setGroupKey("notification_recents");
notificationGroupWithRecents.setSeen(false);
return notificationGroupWithRecents;
}
private NotificationGroup createOlderHeader() {
NotificationGroup notificationGroupWithOlder = new NotificationGroup();
notificationGroupWithOlder.setOlderHeader(true);
notificationGroupWithOlder.setGroupKey("notification_older");
notificationGroupWithOlder.setSeen(true);
return notificationGroupWithOlder;
}
/** Implementation of {@link PreprocessingManager.CallStateListener} **/
@Override
public void onCallStateChanged(boolean isInCall) {
if (isInCall != mIsInCall) {
mIsInCall = isInCall;
notifyDataSetChanged();
}
}
/**
* 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;
}
/**
* Returns {@code true} if there are multiple groups with the same {@code groupKey}.
*/
public boolean shouldRemoveGroupSummary(String groupKey) {
return mGroupKeyToCountMap.getOrDefault(groupKey, /* defaultValue= */ 0) <= 1;
}
/**
* 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;
}
/**
* Set notification groups as seen.
*
* @param start Initial adapter position of the notification groups.
* @param end Final adapter position of the notification groups.
*/
void setVisibleNotificationsAsSeen(int start, int end) {
if (mNotificationDataManager == null || mIsGroupNotificationAdapter) {
return;
}
start = Math.max(start, 0);
end = Math.min(end, mNotifications.size() - 1);
List<AlertEntry> notifications = new ArrayList();
for (int i = start; i <= end; i++) {
NotificationGroup group = mNotifications.get(i);
AlertEntry groupSummary = group.getGroupSummaryNotification();
if (groupSummary != null) {
notifications.add(groupSummary);
}
notifications.addAll(group.getChildNotifications());
}
mNotificationDataManager.setVisibleNotificationsAsSeen(notifications);
}
@Override
public int getConfigurationId() {
return R.id.notification_list_uxr_config;
}
private static class ExpandedNotification {
private String mKey;
private boolean mIsExpanded;
ExpandedNotification(String key, boolean isExpanded) {
mKey = key;
mIsExpanded = isExpanded;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ExpandedNotification)) {
return false;
}
ExpandedNotification other = (ExpandedNotification) obj;
return mKey.equals(other.getKey()) && mIsExpanded == other.isExpanded();
}
public String getKey() {
return mKey;
}
public boolean isExpanded() {
return mIsExpanded;
}
}
}