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);