Add a wrapper ContentLimiting adapter.

Also elevate ScrollingLimitedViewHolder to a more useful standalone public class.

BUG: 160737822
BUG: 159766205
Test: manual with paintbooth app
Change-Id: Ic7cc133a1a8fcb01dedcef22d75d79342960db48
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/ContentLimitingAdapter.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/ContentLimitingAdapter.java
index 9014054..615ac25 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/ContentLimitingAdapter.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/ContentLimitingAdapter.java
@@ -17,18 +17,12 @@
 package com.android.car.ui.recyclerview;
 
 import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
 import android.view.ViewGroup;
-import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
 import androidx.recyclerview.widget.RecyclerView;
 
-import com.android.car.ui.R;
-import com.android.car.ui.utils.CarUiUtils;
-
 /**
  * A {@link RecyclerView.Adapter} that can limit its content based on a given length limit which
  * can change at run-time.
@@ -50,7 +44,7 @@
      * Override this method to provide your own alternative value if {@link Integer#MAX_VALUE} is
      * a viewType value already in-use by your adapter.
      */
-    protected int getScrollingLimitedMessageViewType() {
+    public int getScrollingLimitedMessageViewType() {
         return SCROLLING_LIMITED_MESSAGE_VIEW_TYPE;
     }
 
@@ -59,9 +53,7 @@
     public final RecyclerView.ViewHolder onCreateViewHolder(
             @NonNull ViewGroup parent, int viewType) {
         if (viewType == getScrollingLimitedMessageViewType()) {
-            View rootView = LayoutInflater.from(parent.getContext())
-                    .inflate(R.layout.car_ui_list_limiting_message, parent, false);
-            return new ScrollingLimitedViewHolder(rootView);
+            return ScrollingLimitedViewHolder.create(parent);
         }
 
         return onCreateViewHolderImpl(parent, viewType);
@@ -80,12 +72,7 @@
     @SuppressWarnings("unchecked")
     public final void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
         if (isContentLimited() && position == getScrollingLimitedMessagePosition()) {
-            String message = holder.itemView.getContext()
-                    .getString(R.string.car_ui_scrolling_limited_message);
-            if (mScrollingLimitedMessageResId != null) {
-                message = holder.itemView.getContext().getString(mScrollingLimitedMessageResId);
-            }
-            ((ScrollingLimitedViewHolder) holder).setMessage(message);
+            ((ScrollingLimitedViewHolder) holder).bind(mScrollingLimitedMessageResId);
             return;
         }
         onBindViewHolderImpl((T) holder, position);
@@ -259,24 +246,4 @@
             notifyItemChanged(getScrollingLimitedMessagePosition());
         }
     }
-
-    /**
-     * {@link RecyclerView.ViewHolder} for the last item in a scrolling limited list.
-     */
-    public static class ScrollingLimitedViewHolder extends RecyclerView.ViewHolder {
-
-        private final TextView mMessage;
-
-        ScrollingLimitedViewHolder(@NonNull View itemView) {
-            super(itemView);
-            mMessage = CarUiUtils.findViewByRefId(itemView, R.id.car_ui_list_limiting_message);
-        }
-
-        /**
-         * Sets the display message.
-         */
-        public void setMessage(String message) {
-            mMessage.setText(message);
-        }
-    }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DelegatingContentLimitingAdapter.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DelegatingContentLimitingAdapter.java
new file mode 100644
index 0000000..9a21641
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DelegatingContentLimitingAdapter.java
@@ -0,0 +1,210 @@
+/*
+ * 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.
+ */
+
+package com.android.car.ui.recyclerview;
+
+import android.view.ViewGroup;
+
+import androidx.annotation.IdRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * A delegating implementation of {@link ContentLimiting} interface.
+ *
+ * <p>This class will provide content limiting capability to any {@link RecyclerView.Adapter} that
+ * is wrapped in it.
+ *
+ * @param <T> type of the {@link RecyclerView.ViewHolder} objects used by the delegate.
+ */
+public final class DelegatingContentLimitingAdapter<T extends RecyclerView.ViewHolder>
+        extends ContentLimitingAdapter<T> {
+    private static final int SCROLLING_LIMITED_MESSAGE_VIEW_TYPE = Integer.MAX_VALUE;
+    private static final int SCROLLING_LIMITED_MESSAGE_DEFAULT_POSITION_OFFSET = -1;
+
+    private final RecyclerView.Adapter<T> mDelegate;
+    private final int mScrollingLimitedMessageViewType;
+    private final int mScrollingLimitedMessagePositionOffset;
+    @IdRes
+    private final int mConfigId;
+
+    /**
+     * Constructs a {@link DelegatingContentLimitingAdapter} that uses {@link Integer#MAX_VALUE}
+     * for the scrolling limited message viewType and positions it at the very bottom of the list
+     * being content limited.
+     *
+     * <p>Use {@link #DelegatingContentLimitingAdapter(RecyclerView.Adapter, int, int, int)} if you
+     * need to customize any of the two default values above.
+     *
+     * @param delegate - the {@link RecyclerView.Adapter} whose content needs to be limited.
+     * @param configId - an Id Resource that can be used to identify said adapter.
+     */
+    public DelegatingContentLimitingAdapter(
+            RecyclerView.Adapter<T> delegate,
+            @IdRes int configId) {
+        this(delegate,
+                configId,
+                SCROLLING_LIMITED_MESSAGE_VIEW_TYPE,
+                SCROLLING_LIMITED_MESSAGE_DEFAULT_POSITION_OFFSET);
+    }
+
+    /**
+     * Constructs a {@link DelegatingContentLimitingAdapter}.
+     *
+     * @param delegate - the {@link RecyclerView.Adapter} whose content needs to be limited.
+     * @param configId - an Id Resource that can be used to identify said adapter.
+     * @param viewType - viewType value for the scrolling limited message
+     * @param offset   - offset of the position of the scrolling limited message. Negative values
+     *                 will be treated as a "bottom offset", i.e. they represent the value to
+     *                 subtract from {@link #getItemCount()} to get to the actual position of the
+     *                 message. For example, by default the offset is -1, meaning the position of
+     *                 the scrolling limited message will be getItemCount() - 1, which in a list
+     *                 indexed at 0 means the very last item. Positive values will be treated as
+     *                 "top offset", so an offset of 0 will put the scrolling limited message at the
+     *                 very top of the list.
+     */
+    public DelegatingContentLimitingAdapter(RecyclerView.Adapter<T> delegate,
+            @IdRes int configId,
+            int viewType,
+            int offset) {
+        mDelegate = delegate;
+        mConfigId = configId;
+        mScrollingLimitedMessageViewType = viewType;
+        mScrollingLimitedMessagePositionOffset = offset;
+        mDelegate.registerAdapterDataObserver(new Observer());
+    }
+
+    private class Observer extends RecyclerView.AdapterDataObserver {
+
+        @Override
+        public void onChanged() {
+            DelegatingContentLimitingAdapter.this.notifyDataSetChanged();
+        }
+
+        @Override
+        public void onItemRangeChanged(int positionStart, int itemCount) {
+            DelegatingContentLimitingAdapter.this
+                    .notifyItemRangeChanged(positionStart, itemCount);
+        }
+
+        @Override
+        public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
+            DelegatingContentLimitingAdapter.this
+                    .notifyItemRangeChanged(positionStart, itemCount, payload);
+        }
+
+        @Override
+        public void onItemRangeInserted(int positionStart, int itemCount) {
+            DelegatingContentLimitingAdapter.this
+                    .notifyItemRangeInserted(positionStart, itemCount);
+        }
+
+        @Override
+        public void onItemRangeRemoved(int positionStart, int itemCount) {
+            DelegatingContentLimitingAdapter.this
+                    .notifyItemRangeRemoved(positionStart, itemCount);
+        }
+
+        @Override
+        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+            DelegatingContentLimitingAdapter.this.notifyDataSetChanged();
+        }
+    }
+
+    @Override
+    @NonNull
+    public T onCreateViewHolderImpl(@NonNull ViewGroup parent, int viewType) {
+        return mDelegate.onCreateViewHolder(parent, viewType);
+    }
+
+    @Override
+    public void onBindViewHolderImpl(T holder, int position) {
+        mDelegate.onBindViewHolder(holder, position);
+    }
+
+    @Override
+    public int getItemViewTypeImpl(int position) {
+        return mDelegate.getItemViewType(position);
+    }
+
+    @Override
+    protected void onViewRecycledImpl(@NonNull T holder) {
+        mDelegate.onViewRecycled(holder);
+    }
+
+    @Override
+    protected boolean onFailedToRecycleViewImpl(@NonNull T holder) {
+        return mDelegate.onFailedToRecycleView(holder);
+    }
+
+    @Override
+    protected void onViewAttachedToWindowImpl(@NonNull T holder) {
+        mDelegate.onViewAttachedToWindow(holder);
+    }
+
+    @Override
+    protected void onViewDetachedFromWindowImpl(@NonNull T holder) {
+        mDelegate.onViewDetachedFromWindow(holder);
+    }
+
+    @Override
+    public void setHasStableIds(boolean hasStableIds) {
+        mDelegate.setHasStableIds(hasStableIds);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return mDelegate.getItemId(position);
+    }
+
+    @Override
+    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+        mDelegate.onAttachedToRecyclerView(recyclerView);
+    }
+
+    @Override
+    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+        mDelegate.onDetachedFromRecyclerView(recyclerView);
+    }
+
+    @Override
+    public int getUnrestrictedItemCount() {
+        return mDelegate.getItemCount();
+    }
+
+    @Override
+    @IdRes
+    public int getConfigurationId() {
+        return mConfigId;
+    }
+
+    @Override
+    public int getScrollingLimitedMessageViewType() {
+        return mScrollingLimitedMessageViewType;
+    }
+
+    @Override
+    protected int getScrollingLimitedMessagePosition() {
+        if (mScrollingLimitedMessagePositionOffset < 0) {
+            // For negative values, treat them as a bottom offset.
+            return getItemCount() + mScrollingLimitedMessagePositionOffset;
+        } else {
+            // For positive values, treat them like a top offset.
+            return mScrollingLimitedMessagePositionOffset;
+        }
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/ScrollingLimitedViewHolder.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/ScrollingLimitedViewHolder.java
new file mode 100644
index 0000000..63b8692
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/ScrollingLimitedViewHolder.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+package com.android.car.ui.recyclerview;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.R;
+import com.android.car.ui.utils.CarUiUtils;
+
+/**
+ * {@link RecyclerView.ViewHolder} for the last item in a scrolling limited list.
+ */
+public final class ScrollingLimitedViewHolder extends RecyclerView.ViewHolder {
+
+    private final TextView mMessage;
+
+    /**
+     * Return an instance of {@link ScrollingLimitedViewHolder} with an already inflated root view.
+     * @param parent - the parent {@link ViewGroup} to use during inflation of the root view.
+     */
+    public static ScrollingLimitedViewHolder create(@NonNull ViewGroup parent) {
+        View rootView = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.car_ui_list_limiting_message, parent, false);
+        return new ScrollingLimitedViewHolder(rootView);
+    }
+
+    ScrollingLimitedViewHolder(@NonNull View itemView) {
+        super(itemView);
+        mMessage = CarUiUtils.requireViewByRefId(itemView, R.id.car_ui_list_limiting_message);
+    }
+
+    /**
+     * Update the content of this {@link ScrollingLimitedViewHolder} object using the provided
+     * message String resource id.
+     * @param messageId
+     */
+    public void bind(@StringRes @Nullable Integer messageId) {
+        int resId = (messageId != null) ? messageId : R.string.car_ui_scrolling_limited_message;
+        mMessage.setText(mMessage.getContext().getString(resId));
+    }
+}