Add Ux restrictions to notifications
Integrate uxr content limiting into notification panel recyclerview
Bug: 160736993, 160736995
Test: manual, make -j50 RunCarNotificationRoboTests
Change-Id: Iec7498560d921ca28807a67ed51986c5657f12e9
(cherry picked from commit 2b155abf5ade041f71d8e652dbd2a2b28bdd3cd2)
diff --git a/Android.bp b/Android.bp
index 5a9f92d..ed7b1da 100644
--- a/Android.bp
+++ b/Android.bp
@@ -39,7 +39,8 @@
"car-assist-client-lib",
"car-ui-lib",
"android.car.userlib",
- "androidx-constraintlayout_constraintlayout"
+ "androidx-constraintlayout_constraintlayout",
+ "car-uxr-client-lib"
],
libs: ["android.car"],
@@ -82,7 +83,8 @@
"car-assist-client-lib",
"car-ui-lib",
"android.car.userlib",
- "androidx-constraintlayout_constraintlayout"
+ "androidx-constraintlayout_constraintlayout",
+ "car-uxr-client-lib",
],
libs: ["android.car"],
@@ -119,7 +121,8 @@
"androidx.palette_palette",
"car-assist-client-lib",
"android.car.userlib",
- "androidx-constraintlayout_constraintlayout"
+ "androidx-constraintlayout_constraintlayout",
+ "car-uxr-client-lib",
],
libs: ["android.car"],
diff --git a/res/xml/uxr_config.xml b/res/xml/uxr_config.xml
new file mode 100644
index 0000000..e17c0e4
--- /dev/null
+++ b/res/xml/uxr_config.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 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
+ -->
+<Mapping xmlns:app="http://schemas.android.com/apk/res-auto">
+ <!--
+ Uxr list configuration attributes:
+ - app:id - the id of the list, as specified in the getConfigurationId() method of the adapter
+ - app:maxLength - the maximum amount of elements allowed in the list when restricted (-1 for unlimited)
+ - app:message - the message to show at the end of the list when restricted
+ -->
+ <ListConfig
+ app:id="@+id/notification_list_uxr_config"
+ app:maxLength="-1"
+ />
+</Mapping>
diff --git a/src/com/android/car/notification/CarNotificationDiff.java b/src/com/android/car/notification/CarNotificationDiff.java
index b88c82d..f22bee8 100644
--- a/src/com/android/car/notification/CarNotificationDiff.java
+++ b/src/com/android/car/notification/CarNotificationDiff.java
@@ -22,6 +22,8 @@
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
+import com.android.car.ui.recyclerview.ContentLimitingAdapter;
+
import java.util.List;
import java.util.Objects;
@@ -42,6 +44,7 @@
private final Context mContext;
private final List<NotificationGroup> mOldList;
private final List<NotificationGroup> mNewList;
+ private final int mMaxItems;
CarNotificationDiff(
Context context,
@@ -49,19 +52,30 @@
List<NotificationGroup> oldList,
@NonNull
List<NotificationGroup> newList) {
+ this(context, oldList, newList, ContentLimitingAdapter.UNLIMITED);
+ }
+
+ CarNotificationDiff(
+ Context context,
+ @NonNull
+ List<NotificationGroup> oldList,
+ @NonNull
+ List<NotificationGroup> newList,
+ int maxItems) {
mContext = context;
mOldList = oldList;
mNewList = newList;
+ mMaxItems = maxItems;
}
@Override
public int getOldListSize() {
- return mOldList.size();
+ return getContentLimitedListSize(mOldList.size());
}
@Override
public int getNewListSize() {
- return mNewList.size();
+ return getContentLimitedListSize(mNewList.size());
}
@Override
@@ -233,4 +247,13 @@
return true;
}
+
+ private int getContentLimitedListSize(int listSize) {
+ if (mMaxItems != ContentLimitingAdapter.UNLIMITED) {
+ // Add one to mMaxItems to account for the scrolling limited message that is added by
+ // the ContentLimitingAdapter.
+ return Math.min(listSize, mMaxItems + 1);
+ }
+ return listSize;
+ }
}
diff --git a/src/com/android/car/notification/CarNotificationItemTouchListener.java b/src/com/android/car/notification/CarNotificationItemTouchListener.java
index e5b600c..bd14a5e 100644
--- a/src/com/android/car/notification/CarNotificationItemTouchListener.java
+++ b/src/com/android/car/notification/CarNotificationItemTouchListener.java
@@ -36,6 +36,7 @@
import com.android.car.notification.template.CarNotificationFooterViewHolder;
import com.android.car.notification.template.CarNotificationHeaderViewHolder;
import com.android.car.notification.template.GroupNotificationViewHolder;
+import com.android.car.ui.recyclerview.ScrollingLimitedViewHolder;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
@@ -184,7 +185,8 @@
RecyclerView.ViewHolder viewHolderAtPoint =
recyclerView.findContainingViewHolder(viewAtPoint);
if (viewHolderAtPoint instanceof CarNotificationHeaderViewHolder
- || viewHolderAtPoint instanceof CarNotificationFooterViewHolder) {
+ || viewHolderAtPoint instanceof CarNotificationFooterViewHolder
+ || viewHolderAtPoint instanceof ScrollingLimitedViewHolder) {
return false;
}
checkArgument(viewHolderAtPoint instanceof CarNotificationBaseViewHolder);
diff --git a/src/com/android/car/notification/CarNotificationView.java b/src/com/android/car/notification/CarNotificationView.java
index 22ca2ea..b55447b 100644
--- a/src/com/android/car/notification/CarNotificationView.java
+++ b/src/com/android/car/notification/CarNotificationView.java
@@ -22,6 +22,7 @@
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
import com.android.internal.statusbar.IStatusBarService;
+import com.android.car.uxr.UxrContentLimiterImpl;
import java.util.ArrayList;
import java.util.HashSet;
@@ -48,6 +49,7 @@
private NotificationDataManager mNotificationDataManager;
private boolean mIsClearAllActive = false;
private List<NotificationGroup> mNotifications;
+ private UxrContentLimiterImpl mUxrContentLimiter;
public CarNotificationView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -73,6 +75,11 @@
mAdapter = new CarNotificationViewAdapter(mContext, /* isGroupNotificationAdapter= */
false, this::startClearAllNotifications);
listView.setAdapter(mAdapter);
+
+ mUxrContentLimiter = new UxrContentLimiterImpl(mContext, R.xml.uxr_config);
+ mUxrContentLimiter.setAdapter(mAdapter);
+ mUxrContentLimiter.start();
+
listView.addOnItemTouchListener(new CarNotificationItemTouchListener(mContext, mAdapter));
listView.addOnScrollListener(new OnScrollListener() {
diff --git a/src/com/android/car/notification/CarNotificationViewAdapter.java b/src/com/android/car/notification/CarNotificationViewAdapter.java
index 9233e12..474a3e8 100644
--- a/src/com/android/car/notification/CarNotificationViewAdapter.java
+++ b/src/com/android/car/notification/CarNotificationViewAdapter.java
@@ -35,6 +35,7 @@
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.HashSet;
@@ -44,7 +45,7 @@
/**
* Notification data adapter that binds a notification to the corresponding view.
*/
-public class CarNotificationViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
+public class CarNotificationViewAdapter extends ContentLimitingAdapter<RecyclerView.ViewHolder>
implements PreprocessingManager.CallStateListener {
private static final String TAG = "CarNotificationAdapter";
@@ -65,6 +66,8 @@
private boolean mIsInCall;
// Suppress binding views to child notifications in the process of being removed.
private Set<AlertEntry> mChildNotificationsBeingCleared = new HashSet<>();
+ private boolean mHasHeaderAndFooter;
+ private int mMaxItems = ContentLimitingAdapter.UNLIMITED;
/**
* Constructor for a notification adapter.
@@ -91,7 +94,7 @@
}
@Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ public RecyclerView.ViewHolder onCreateViewHolderImpl(@NonNull ViewGroup parent, int viewType) {
RecyclerView.ViewHolder viewHolder;
View view;
switch (viewType) {
@@ -117,16 +120,24 @@
}
@Override
- public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ public void onBindViewHolderImpl(RecyclerView.ViewHolder holder, int position) {
+ if (position == 0 && mHasHeaderAndFooter) {
+ // The first element of the main recyclerview should always be the header
+ ((CarNotificationHeaderViewHolder) holder).bind(hasNotifications());
+ return;
+ }
+
+ if (position == getItemCount() - 1 && mHasHeaderAndFooter) {
+ // The last element shown in the main recyclerview should always be the footer, even
+ // if the element at this position in mNotifications is not the footer due to Uxr
+ // limiting the item count
+ ((CarNotificationFooterViewHolder) holder).bind(hasNotifications());
+ return;
+ }
+
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.GROUP_EXPANDED:
((GroupNotificationViewHolder) holder)
.bind(notificationGroup, this, /* isExpanded= */ true);
@@ -153,17 +164,21 @@
}
@Override
- public int getItemViewType(int position) {
- NotificationGroup notificationGroup = mNotifications.get(position);
-
- if (notificationGroup.isHeader()) {
+ public int getItemViewTypeImpl(int position) {
+ if (position == 0 && mHasHeaderAndFooter) {
+ // The first element of the main recyclerview should always be the header
return NotificationViewType.HEADER;
}
- if (notificationGroup.isFooter()) {
+ if (position == getItemCount() - 1 && mHasHeaderAndFooter) {
+ // The last element shown in the main recyclerview should always be the footer, even
+ // if the element at this position in mNotifications is not the footer due to Uxr
+ // limiting the item count
return NotificationViewType.FOOTER;
}
+ NotificationGroup notificationGroup = mNotifications.get(position);
+
if (notificationGroup.isGroup()) {
if (mExpandedNotifications.contains(notificationGroup.getGroupKey())) {
return NotificationViewType.GROUP_EXPANDED;
@@ -247,22 +262,28 @@
}
@Override
- public int getItemCount() {
- int itemCount = mNotifications.size();
+ public int getUnrestrictedItemCount() {
+ return mNotifications.size();
+ }
- if (mIsGroupNotificationAdapter && itemCount > mMaxNumberGroupChildrenShown) {
- return mMaxNumberGroupChildrenShown;
+ @Override
+ public void setMaxItems(int maxItems) {
+ if (maxItems == ContentLimitingAdapter.UNLIMITED || !mHasHeaderAndFooter) {
+ mMaxItems = maxItems;
+ } else {
+ // Adding two so the notification header and footer don't count toward the limit.
+ mMaxItems = maxItems + 2;
}
+ super.setMaxItems(mMaxItems);
+ }
- if (!mIsGroupNotificationAdapter && mCarUxRestrictions != null
- && (mCarUxRestrictions.getActiveRestrictions()
- & CarUxRestrictions.UX_RESTRICTIONS_LIMIT_CONTENT) != 0) {
-
- int maxItemCount = mCarUxRestrictions.getMaxCumulativeContentItems();
-
- return Math.min(itemCount, maxItemCount);
+ @Override
+ protected int getScrollingLimitedMessagePosition() {
+ if (mHasHeaderAndFooter) {
+ // Place the message as the second to last element so it is above the footer
+ return getItemCount() - 2;
}
- return itemCount;
+ return getItemCount() - 1;
}
@Override
@@ -342,15 +363,18 @@
notificationGroupList.add(0, createNotificationHeader());
// add footer as the last item of the list.
notificationGroupList.add(createNotificationFooter());
+ mHasHeaderAndFooter = true;
+ } else {
+ mHasHeaderAndFooter = false;
}
+
DiffUtil.DiffResult diffResult =
DiffUtil.calculateDiff(
- new CarNotificationDiff(mContext, mNotifications, notificationGroupList),
+ new CarNotificationDiff(mContext, mNotifications, notificationGroupList, mMaxItems),
/* detectMoves= */ false);
mNotifications = notificationGroupList;
diffResult.dispatchUpdatesTo(this);
}
-
/**
* Sets child notifications of the group notification that is in the process of being cleared.
* This prevents these child notifications from appearing briefly while the clearing process is
@@ -464,4 +488,9 @@
mNotificationDataManager.setNotificationsAsSeen(notifications);
}
}
+
+ @Override
+ public int getConfigurationId() {
+ return R.id.notification_list_uxr_config;
+ }
}
diff --git a/tests/robotests/src/com/android/car/notification/CarNotificationDiffTest.java b/tests/robotests/src/com/android/car/notification/CarNotificationDiffTest.java
index e2a790a..c6b441b 100644
--- a/tests/robotests/src/com/android/car/notification/CarNotificationDiffTest.java
+++ b/tests/robotests/src/com/android/car/notification/CarNotificationDiffTest.java
@@ -264,6 +264,30 @@
}
@Test
+ public void maxItemsSet_getOldListSize_shouldReturnTwo() {
+ mNotificationGroupList4 = new ArrayList<>();
+ mNotificationGroupList4.add(mNotificationGroup1);
+ mNotificationGroupList4.add(mNotificationGroup2);
+ mNotificationGroupList4.add(mNotificationGroup3);
+ CarNotificationDiff carNotificationDiff = new CarNotificationDiff(mContext,
+ mNotificationGroupList4, mNotificationGroupList3, /* maxItems= */ 1);
+ // Should return two - one for the notification and one for the limited message
+ assertThat(carNotificationDiff.getOldListSize()).isEqualTo(2);
+ }
+
+ @Test
+ public void maxItemsSet_getNewListSize_shouldReturnTwo() {
+ mNotificationGroupList4 = new ArrayList<>();
+ mNotificationGroupList4.add(mNotificationGroup1);
+ mNotificationGroupList4.add(mNotificationGroup2);
+ mNotificationGroupList4.add(mNotificationGroup3);
+ CarNotificationDiff carNotificationDiff = new CarNotificationDiff(mContext,
+ mNotificationGroupList1, mNotificationGroupList4, /* maxItems= */ 1);
+ // Should return two - one for the notification and one for the limited message
+ assertThat(carNotificationDiff.getNewListSize()).isEqualTo(2);
+ }
+
+ @Test
public void areBundleEqual_sameSize_shouldReturnTrue() {
Notification.Builder oldNotification = new Notification.Builder(mContext,
CHANNEL_ID)
diff --git a/tests/robotests/src/com/android/car/notification/CarNotificationViewAdapterTest.java b/tests/robotests/src/com/android/car/notification/CarNotificationViewAdapterTest.java
index c7543b9..20ddccf 100644
--- a/tests/robotests/src/com/android/car/notification/CarNotificationViewAdapterTest.java
+++ b/tests/robotests/src/com/android/car/notification/CarNotificationViewAdapterTest.java
@@ -728,6 +728,73 @@
}
@Test
+ public void setMaxItems_headerShouldBeFirstVisibleElement() {
+ initializeWithFactory(true);
+ NotificationGroup notificationGroup = new NotificationGroup();
+ notificationGroup.addNotification(mNotification1);
+ mNotificationGroupList1.add(notificationGroup);
+
+ mCarNotificationViewAdapter.setNotifications(
+ mNotificationGroupList1, /* setRecyclerViewListHeaderAndFooter= */ true);
+
+ mCarNotificationViewAdapter.setMaxItems(1);
+
+ assertThat(mCarNotificationViewAdapter.getItemViewType(0)).isEqualTo(
+ NotificationViewType.HEADER);
+ }
+
+ @Test
+ public void setMaxItems_footerShouldBeLastVisibleElement() {
+ initializeWithFactory(true);
+ NotificationGroup notificationGroup = new NotificationGroup();
+ notificationGroup.addNotification(mNotification1);
+ mNotificationGroupList1.add(notificationGroup);
+
+ mCarNotificationViewAdapter.setNotifications(
+ mNotificationGroupList1, /* setRecyclerViewListHeaderAndFooter= */ true);
+
+ mCarNotificationViewAdapter.setMaxItems(1);
+
+ assertThat(mCarNotificationViewAdapter.getItemViewType(
+ mCarNotificationViewAdapter.getItemCount() - 1)).isEqualTo(
+ NotificationViewType.FOOTER);
+ }
+
+ @Test
+ public void setMaxItems_noHeaderAndFooter_getItemCount_shouldReturnTwo() {
+ initializeWithFactory(true);
+ NotificationGroup notificationGroup = new NotificationGroup();
+ notificationGroup.addNotification(mNotification1);
+ mNotificationGroupList1.add(notificationGroup);
+
+ mCarNotificationViewAdapter.setNotifications(
+ mNotificationGroupList1, /* setRecyclerViewListHeaderAndFooter= */ false);
+
+ mCarNotificationViewAdapter.setMaxItems(1);
+
+ // Count should be two - one for the allotted notification and one for the limited message
+ assertThat(mCarNotificationViewAdapter.getItemCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void setMaxItems_hasHeaderAndFooter_getItemCount_shouldReturnFour() {
+ initializeWithFactory(true);
+ NotificationGroup notificationGroup = new NotificationGroup();
+ notificationGroup.addNotification(mNotification1);
+ mNotificationGroupList1.add(notificationGroup);
+
+ mCarNotificationViewAdapter.setNotifications(
+ mNotificationGroupList1, /* setRecyclerViewListHeaderAndFooter= */ true);
+
+ mCarNotificationViewAdapter.setMaxItems(1);
+
+ // Count should be four - one for the allotted notification, one for the limited message,
+ // and two for the header and footer
+ assertThat(mCarNotificationViewAdapter.getItemCount()).isEqualTo(4);
+ }
+
+
+ @Test
public void getViewPool_shouldReturnNotNull() {
initializeWithFactory(false);