1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.chassis.car.ui.plugin.recyclerview;
17 
18 import static androidx.recyclerview.widget.RecyclerView.VERTICAL;
19 
20 import static com.android.car.ui.plugin.oemapis.recyclerview.RecyclerViewAttributesOEMV1.SIZE_LARGE;
21 import static com.android.car.ui.plugin.oemapis.recyclerview.RecyclerViewAttributesOEMV1.SIZE_MEDIUM;
22 import static com.android.car.ui.plugin.oemapis.recyclerview.RecyclerViewAttributesOEMV1.SIZE_SMALL;
23 
24 import android.car.drivingstate.CarUxRestrictions;
25 import android.content.Context;
26 import android.view.InputDevice;
27 import android.view.LayoutInflater;
28 import android.view.MotionEvent;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.FrameLayout;
32 import android.widget.LinearLayout;
33 
34 import androidx.annotation.LayoutRes;
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.recyclerview.widget.GridLayoutManager;
38 import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
39 import androidx.recyclerview.widget.LinearLayoutManager;
40 import androidx.recyclerview.widget.OrientationHelper;
41 import androidx.recyclerview.widget.RecyclerView;
42 import androidx.recyclerview.widget.RecyclerView.Adapter;
43 import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
44 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
45 
46 import com.android.car.ui.plugin.oemapis.recyclerview.AdapterOEMV1;
47 import com.android.car.ui.plugin.oemapis.recyclerview.LayoutStyleOEMV1;
48 import com.android.car.ui.plugin.oemapis.recyclerview.OnChildAttachStateChangeListenerOEMV1;
49 import com.android.car.ui.plugin.oemapis.recyclerview.OnScrollListenerOEMV1;
50 import com.android.car.ui.plugin.oemapis.recyclerview.RecyclerViewAttributesOEMV1;
51 import com.android.car.ui.plugin.oemapis.recyclerview.RecyclerViewOEMV1;
52 import com.android.car.ui.plugin.oemapis.recyclerview.ViewHolderOEMV1;
53 
54 import com.chassis.car.ui.plugin.R;
55 import com.chassis.car.ui.plugin.recyclerview.AdapterWrapper.ViewHolderWrapper;
56 import com.chassis.car.ui.plugin.uxr.CarUxRestrictionsUtil;
57 
58 import java.util.ArrayList;
59 import java.util.List;
60 
61 /**
62  * Reference OEM implementation for RecyclerView
63  */
64 public final class RecyclerViewImpl extends FrameLayout implements RecyclerViewOEMV1 {
65 
66     /**
67      * {@link com.android.car.ui.utils.RotaryConstants#ROTARY_CONTAINER}
68      */
69     private static final String ROTARY_CONTAINER =
70             "com.android.car.ui.utils.ROTARY_CONTAINER";
71     /**
72      * {@link com.android.car.ui.utils.RotaryConstants#ROTARY_HORIZONTALLY_SCROLLABLE}
73      */
74     private static final String ROTARY_HORIZONTALLY_SCROLLABLE =
75             "com.android.car.ui.utils.HORIZONTALLY_SCROLLABLE";
76     /**
77      * {@link com.android.car.ui.utils.RotaryConstants#ROTARY_VERTICALLY_SCROLLABLE}
78      */
79     private static final String ROTARY_VERTICALLY_SCROLLABLE =
80             "com.android.car.ui.utils.VERTICALLY_SCROLLABLE";
81 
82     @NonNull
83     private final RecyclerView mRecyclerView;
84 
85     private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener =
86             new UxRestrictionChangedListener();
87     @NonNull
88     private final CarUxRestrictionsUtil mCarUxRestrictionsUtil;
89 
90     @Nullable
91     private final DefaultScrollBar mScrollBar;
92 
93     @NonNull
94     private final List<OnScrollListenerOEMV1> mScrollListeners = new ArrayList<>();
95 
96     @NonNull
97     private final RecyclerView.OnScrollListener mOnScrollListener =
98             new RecyclerView.OnScrollListener() {
99                 @Override
100                 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
101                     for (OnScrollListenerOEMV1 listener : mScrollListeners) {
102                         listener.onScrolled(RecyclerViewImpl.this, dx, dy);
103                     }
104                 }
105 
106                 @Override
107                 public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
108                     for (OnScrollListenerOEMV1 listener : mScrollListeners) {
109                         listener.onScrollStateChanged(RecyclerViewImpl.this,
110                                 toInternalScrollState(newState));
111                     }
112                 }
113             };
114 
115     @NonNull
116     private final List<OnChildAttachStateChangeListenerOEMV1> mOnChildAttachStateChangeListeners =
117             new ArrayList<>();
118 
119     @NonNull
120     private final RecyclerView.OnChildAttachStateChangeListener mOnChildAttachStateChangeListener =
121             new OnChildAttachStateChangeListener() {
122                 @Override
123                 public void onChildViewAttachedToWindow(@NonNull View view) {
124                     for (OnChildAttachStateChangeListenerOEMV1 listener :
125                             mOnChildAttachStateChangeListeners) {
126                         listener.onChildViewAttachedToWindow(view);
127                     }
128                 }
129 
130                 @Override
131                 public void onChildViewDetachedFromWindow(@NonNull View view) {
132                     for (OnChildAttachStateChangeListenerOEMV1 listener :
133                             mOnChildAttachStateChangeListeners) {
134                         listener.onChildViewDetachedFromWindow(view);
135                     }
136                 }
137             };
138 
139     @Nullable
140     private LayoutStyleOEMV1 mLayoutStyle;
141 
RecyclerViewImpl(@onNull Context context)142     public RecyclerViewImpl(@NonNull Context context) {
143         this(context, null);
144     }
145 
RecyclerViewImpl(@onNull Context context, @Nullable RecyclerViewAttributesOEMV1 attrs)146     public RecyclerViewImpl(@NonNull Context context,
147             @Nullable RecyclerViewAttributesOEMV1 attrs) {
148         super(context);
149         boolean scrollBarEnabled = context.getResources().getBoolean(R.bool.scrollbar_enable);
150         @LayoutRes int layout = R.layout.recycler_view_no_scrollbar;
151         if (scrollBarEnabled) {
152             int size = attrs != null ? attrs.getSize() : SIZE_LARGE;
153             switch (size) {
154                 case SIZE_SMALL:
155                     layout = R.layout.recycler_view_small;
156                     break;
157                 case SIZE_MEDIUM:
158                     layout = R.layout.recycler_view_medium;
159                     break;
160                 case SIZE_LARGE:
161                     layout = R.layout.recycler_view;
162             }
163         }
164 
165         LayoutInflater factory = LayoutInflater.from(context);
166         View rootView = factory.inflate(layout, this, true);
167         mRecyclerView = rootView.requireViewById(R.id.recycler_view);
168 
169         // Set to false so the items below the toolbar are visible.
170         mRecyclerView.setClipToPadding(false);
171 
172         mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(context);
173 
174         if (attrs != null) {
175             setLayoutStyle(attrs.getLayoutStyle());
176             setBackground(attrs.getBackground());
177             setPadding(attrs.getPaddingLeft(), attrs.getPaddingTop(), attrs.getPaddingRight(),
178                     attrs.getPaddingBottom());
179             setMinimumHeight(attrs.getMinHeight());
180             setMinimumWidth(attrs.geMinWidth());
181 
182             LayoutParams params = new LayoutParams(attrs.getLayoutWidth(), attrs.getLayoutHeight());
183             params.setMargins(attrs.getMarginLeft(), attrs.getMarginTop(), attrs.getMarginRight(),
184                     attrs.getMarginBottom());
185             setLayoutParams(params);
186 
187             mLayoutStyle = attrs.getLayoutStyle();
188         } else {
189             mLayoutStyle = new LayoutStyleOEMV1() {
190                 @Override
191                 public int getSpanCount() {
192                     return 1;
193                 }
194 
195                 @Override
196                 public int getLayoutType() {
197                     return LayoutStyleOEMV1.LAYOUT_TYPE_LINEAR;
198                 }
199 
200                 @Override
201                 public int getOrientation() {
202                     return LayoutStyleOEMV1.ORIENTATION_VERTICAL;
203                 }
204 
205                 @Override
206                 public boolean getReverseLayout() {
207                     return false;
208                 }
209             };
210         }
211 
212         setLayoutStyle(mLayoutStyle);
213 
214         boolean rotaryScrollEnabled = attrs != null && attrs.isRotaryScrollEnabled();
215         initRotaryScroll(mRecyclerView, rotaryScrollEnabled, getLayoutStyle().getOrientation());
216 
217         if (!scrollBarEnabled) {
218             mScrollBar = null;
219             return;
220         }
221 
222         mRecyclerView.setVerticalScrollBarEnabled(false);
223         mRecyclerView.setHorizontalScrollBarEnabled(false);
224 
225         mScrollBar = new DefaultScrollBar();
226         mScrollBar.initialize(context, mRecyclerView, rootView.requireViewById(R.id.scroll_bar));
227     }
228 
229     @Override
setAdapter(AdapterOEMV1<V> adapterV1)230     public <V extends ViewHolderOEMV1> void setAdapter(AdapterOEMV1<V> adapterV1) {
231         if (adapterV1 == null) {
232             mRecyclerView.setAdapter(null);
233         } else {
234             mRecyclerView.setAdapter(new AdapterWrapper(adapterV1));
235         }
236     }
237 
238     @Override
addOnScrollListener(OnScrollListenerOEMV1 listener)239     public void addOnScrollListener(OnScrollListenerOEMV1 listener) {
240         if (listener == null) {
241             return;
242         }
243         if (mScrollListeners.isEmpty()) {
244             mRecyclerView.addOnScrollListener(mOnScrollListener);
245         }
246         mScrollListeners.add(listener);
247     }
248 
249     @Override
removeOnScrollListener(OnScrollListenerOEMV1 listener)250     public void removeOnScrollListener(OnScrollListenerOEMV1 listener) {
251         if (listener == null) {
252             return;
253         }
254         mScrollListeners.remove(listener);
255         if (mScrollListeners.isEmpty()) {
256             mRecyclerView.removeOnScrollListener(mOnScrollListener);
257         }
258     }
259 
260     @Override
clearOnScrollListeners()261     public void clearOnScrollListeners() {
262         if (!mScrollListeners.isEmpty()) {
263             mScrollListeners.clear();
264             mRecyclerView.clearOnScrollListeners();
265         }
266     }
267 
268     @Override
scrollToPosition(int position)269     public void scrollToPosition(int position) {
270         mRecyclerView.scrollToPosition(position);
271     }
272 
273     @Override
smoothScrollBy(int dx, int dy)274     public void smoothScrollBy(int dx, int dy) {
275         mRecyclerView.smoothScrollBy(dx, dy);
276     }
277 
278     @Override
smoothScrollToPosition(int position)279     public void smoothScrollToPosition(int position) {
280         mRecyclerView.smoothScrollToPosition(position);
281     }
282 
283     @Override
setHasFixedSize(boolean hasFixedSize)284     public void setHasFixedSize(boolean hasFixedSize) {
285         mRecyclerView.setHasFixedSize(hasFixedSize);
286     }
287 
288     @Override
hasFixedSize()289     public boolean hasFixedSize() {
290         return mRecyclerView.hasFixedSize();
291     }
292 
293     @Override
setLayoutStyle(@ullable LayoutStyleOEMV1 layoutStyle)294     public void setLayoutStyle(@Nullable LayoutStyleOEMV1 layoutStyle) {
295         mLayoutStyle = layoutStyle;
296 
297         int orientation = layoutStyle == null ? VERTICAL : layoutStyle.getOrientation();
298         boolean reverseLayout = layoutStyle != null && layoutStyle.getReverseLayout();
299 
300         if (layoutStyle == null
301                 || layoutStyle.getLayoutType() == LayoutStyleOEMV1.LAYOUT_TYPE_LINEAR) {
302             mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext(),
303                     orientation,
304                     reverseLayout));
305         } else {
306             GridLayoutManager glm = new GridLayoutManager(getContext(),
307                     layoutStyle.getSpanCount(),
308                     orientation,
309                     reverseLayout);
310             glm.setSpanSizeLookup(new SpanSizeLookup() {
311                 @Override
312                 public int getSpanSize(int position) {
313                     return layoutStyle.getSpanSize(position);
314                 }
315             });
316             mRecyclerView.setLayoutManager(glm);
317         }
318     }
319 
320     @Override
getLayoutStyle()321     public LayoutStyleOEMV1 getLayoutStyle() {
322         return mLayoutStyle;
323     }
324 
getView()325     public View getView() {
326         return this;
327     }
328 
329     @Override
findFirstCompletelyVisibleItemPosition()330     public int findFirstCompletelyVisibleItemPosition() {
331         RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
332         if (layoutManager instanceof LinearLayoutManager) {
333             return ((LinearLayoutManager) layoutManager)
334                     .findFirstCompletelyVisibleItemPosition();
335         }
336         return 0;
337     }
338 
339     @Override
findFirstVisibleItemPosition()340     public int findFirstVisibleItemPosition() {
341         RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
342         if (layoutManager instanceof LinearLayoutManager) {
343             return ((LinearLayoutManager) layoutManager)
344                     .findFirstVisibleItemPosition();
345         }
346         return 0;
347     }
348 
349     @Override
findLastCompletelyVisibleItemPosition()350     public int findLastCompletelyVisibleItemPosition() {
351         RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
352         if (layoutManager instanceof LinearLayoutManager) {
353             return ((LinearLayoutManager) layoutManager)
354                     .findLastCompletelyVisibleItemPosition();
355         }
356         return 0;
357     }
358 
359     @Override
findLastVisibleItemPosition()360     public int findLastVisibleItemPosition() {
361         RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
362         if (layoutManager instanceof LinearLayoutManager) {
363             return ((LinearLayoutManager) layoutManager)
364                     .findLastVisibleItemPosition();
365         }
366         return 0;
367     }
368 
toInternalScrollState(int state)369     private static int toInternalScrollState(int state) {
370         /* default to RecyclerView.SCROLL_STATE_IDLE */
371         int internalState = RecyclerViewOEMV1.SCROLL_STATE_IDLE;
372         switch (state) {
373             case RecyclerView.SCROLL_STATE_DRAGGING:
374                 internalState = RecyclerViewOEMV1.SCROLL_STATE_DRAGGING;
375                 break;
376             case RecyclerView.SCROLL_STATE_SETTLING:
377                 internalState = RecyclerViewOEMV1.SCROLL_STATE_SETTLING;
378                 break;
379         }
380         return internalState;
381     }
382 
383     @Override
getScrollState()384     public int getScrollState() {
385         return toInternalScrollState(mRecyclerView.getScrollState());
386     }
387 
388     @Override
setContentDescription(CharSequence contentDescription)389     public void setContentDescription(CharSequence contentDescription) {
390         boolean rotaryScrollEnabled = contentDescription != null
391                 && (ROTARY_HORIZONTALLY_SCROLLABLE.contentEquals(contentDescription)
392                 || ROTARY_VERTICALLY_SCROLLABLE.contentEquals(contentDescription));
393         int orientation = getLayoutStyle() == null ? LinearLayout.VERTICAL
394                 : getLayoutStyle().getOrientation();
395         initRotaryScroll(mRecyclerView, rotaryScrollEnabled, orientation);
396         // Only change this view's content description when not related to rotary scroll. Don't
397         // change its content description when related to rotary scroll, because the content
398         // description should be set on its inner recyclerview in this case.
399         if (!rotaryScrollEnabled) {
400             super.setContentDescription(contentDescription);
401         }
402     }
403 
createOrientationHelper()404     private OrientationHelper createOrientationHelper() {
405         if (mLayoutStyle.getOrientation() == LayoutStyleOEMV1.ORIENTATION_VERTICAL) {
406             return OrientationHelper.createVerticalHelper(mRecyclerView.getLayoutManager());
407         } else {
408             return OrientationHelper.createHorizontalHelper(mRecyclerView.getLayoutManager());
409         }
410     }
411 
412     @Override
getEndAfterPadding()413     public int getEndAfterPadding() {
414         if (mLayoutStyle == null) return 0;
415         return createOrientationHelper().getEndAfterPadding();
416     }
417 
418     @Override
getStartAfterPadding()419     public int getStartAfterPadding() {
420         if (mLayoutStyle == null) return 0;
421         return createOrientationHelper().getStartAfterPadding();
422     }
423 
424     @Override
getTotalSpace()425     public int getTotalSpace() {
426         if (mLayoutStyle == null) return 0;
427         return createOrientationHelper().getTotalSpace();
428     }
429 
430     @Override
setPadding(int left, int top, int right, int bottom)431     public void setPadding(int left, int top, int right, int bottom) {
432         if (mScrollBar != null) {
433             int currentPosition = findFirstVisibleItemPosition();
434             setScrollBarPadding(top, bottom);
435             // Maintain same index position after setting padding
436             scrollToPosition(currentPosition);
437         }
438         mRecyclerView.setPadding(mRecyclerView.getPaddingLeft(),
439                 top, mRecyclerView.getPaddingRight(), bottom);
440         super.setPadding(left, 0, right, 0);
441     }
442 
443     @Override
getPaddingTop()444     public int getPaddingTop() {
445         return mRecyclerView.getPaddingTop();
446     }
447 
448     @Override
getPaddingBottom()449     public int getPaddingBottom() {
450         return mRecyclerView.getPaddingBottom();
451     }
452 
453     @Override
setPaddingRelative(int start, int top, int end, int bottom)454     public void setPaddingRelative(int start, int top, int end, int bottom) {
455         if (mScrollBar != null) {
456             int currentPosition = findFirstVisibleItemPosition();
457             setScrollBarPadding(top, bottom);
458             // Maintain same index position after setting padding
459             scrollToPosition(currentPosition);
460         }
461         mRecyclerView.setPaddingRelative(0, top, 0, bottom);
462         super.setPaddingRelative(start, 0, end, 0);
463     }
464 
setScrollBarPadding(int paddingTop, int paddingBottom)465     private void setScrollBarPadding(int paddingTop, int paddingBottom) {
466         if (mScrollBar != null) {
467             mScrollBar.setPadding(paddingTop, paddingBottom);
468         }
469     }
470 
471     @Override
onAttachedToWindow()472     protected void onAttachedToWindow() {
473         super.onAttachedToWindow();
474         mCarUxRestrictionsUtil.register(mListener);
475     }
476 
477     @Override
onDetachedFromWindow()478     protected void onDetachedFromWindow() {
479         super.onDetachedFromWindow();
480         mCarUxRestrictionsUtil.unregister(mListener);
481     }
482 
483     @Override
getRecyclerViewChildCount()484     public int getRecyclerViewChildCount() {
485         if (mRecyclerView.getLayoutManager() != null) {
486             return mRecyclerView.getLayoutManager().getChildCount();
487         } else {
488             return 0;
489         }
490     }
491 
492     @Override
getRecyclerViewChildAt(int index)493     public View getRecyclerViewChildAt(int index) {
494         if (mRecyclerView.getLayoutManager() != null) {
495             return mRecyclerView.getLayoutManager().getChildAt(index);
496         } else {
497             return null;
498         }
499     }
500 
501     @Override
findViewHolderForAdapterPosition(int position)502     public ViewHolderOEMV1 findViewHolderForAdapterPosition(int position) {
503         ViewHolder viewHolder = mRecyclerView.findViewHolderForAdapterPosition(position);
504         if (viewHolder instanceof ViewHolderWrapper) {
505             return ((ViewHolderWrapper) viewHolder).getViewHolder();
506         }
507         return null;
508     }
509 
510     @Override
findViewHolderForLayoutPosition(int position)511     public ViewHolderOEMV1 findViewHolderForLayoutPosition(int position) {
512         ViewHolder viewHolder = mRecyclerView.findViewHolderForLayoutPosition(position);
513         if (viewHolder instanceof ViewHolderWrapper) {
514             return ((ViewHolderWrapper) viewHolder).getViewHolder();
515         }
516         return null;
517     }
518 
519     @Override
canScrollHorizontally(int direction)520     public boolean canScrollHorizontally(int direction) {
521         return mRecyclerView.canScrollHorizontally(direction);
522     }
523 
524     @Override
canScrollVertically(int direction)525     public boolean canScrollVertically(int direction) {
526         return mRecyclerView.canScrollVertically(direction);
527     }
528 
529     @Override
addOnChildAttachStateChangeListener( OnChildAttachStateChangeListenerOEMV1 listener)530     public void addOnChildAttachStateChangeListener(
531             OnChildAttachStateChangeListenerOEMV1 listener) {
532         if (listener == null) {
533             return;
534         }
535         if (mOnChildAttachStateChangeListeners.isEmpty()) {
536             mRecyclerView.addOnChildAttachStateChangeListener(mOnChildAttachStateChangeListener);
537         }
538         mOnChildAttachStateChangeListeners.add(listener);
539     }
540 
541     @Override
removeOnChildAttachStateChangeListener( OnChildAttachStateChangeListenerOEMV1 listener)542     public void removeOnChildAttachStateChangeListener(
543             OnChildAttachStateChangeListenerOEMV1 listener) {
544         if (listener == null) {
545             return;
546         }
547         mOnChildAttachStateChangeListeners.remove(listener);
548         if (mOnChildAttachStateChangeListeners.isEmpty()) {
549             mRecyclerView.removeOnChildAttachStateChangeListener(mOnChildAttachStateChangeListener);
550         }
551     }
552 
553     @Override
clearOnChildAttachStateChangeListener()554     public void clearOnChildAttachStateChangeListener() {
555         if (!mOnChildAttachStateChangeListeners.isEmpty()) {
556             mOnChildAttachStateChangeListeners.clear();
557             mRecyclerView.clearOnChildAttachStateChangeListeners();
558         }
559     }
560 
561     @Override
getChildLayoutPosition(View child)562     public int getChildLayoutPosition(View child) {
563         return mRecyclerView.getChildLayoutPosition(child);
564     }
565 
566     /**
567      * If this view's {@code rotaryScrollEnabled} attribute is set to true, sets the content
568      * description so that the {@code RotaryService} will treat it as a scrollable container and
569      * initializes this view accordingly.
570      */
initRotaryScroll(@onNull ViewGroup recyclerView, boolean rotaryScrollEnabled, int orientation)571     private void initRotaryScroll(@NonNull ViewGroup recyclerView,
572             boolean rotaryScrollEnabled,
573             int orientation) {
574         if (rotaryScrollEnabled) {
575             setRotaryScrollEnabled(
576                     recyclerView, /* isVertical= */ orientation == LinearLayout.VERTICAL);
577         }
578 
579         // If rotary scrolling is enabled, set a generic motion event listener to convert
580         // SOURCE_ROTARY_ENCODER scroll events into SOURCE_MOUSE scroll events that RecyclerView
581         // knows how to handle.
582         recyclerView.setOnGenericMotionListener(rotaryScrollEnabled ? (v, event) -> {
583             if (event.getAction() == MotionEvent.ACTION_SCROLL) {
584                 if (event.getSource() == InputDevice.SOURCE_ROTARY_ENCODER) {
585                     MotionEvent mouseEvent = MotionEvent.obtain(event);
586                     mouseEvent.setSource(InputDevice.SOURCE_MOUSE);
587                     recyclerView.onGenericMotionEvent(mouseEvent);
588                     return true;
589                 }
590             }
591             return false;
592         } : null);
593 
594         // If rotary scrolling is enabled, mark this view as focusable. This view will be focused
595         // when no focusable elements are visible.
596         recyclerView.setFocusable(rotaryScrollEnabled);
597 
598         // Focus this view before descendants so that the RotaryService can focus this view when it
599         // wants to.
600         recyclerView.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
601 
602         // Disable the default focus highlight. No highlight should appear when this view is
603         // focused.
604         recyclerView.setDefaultFocusHighlightEnabled(false);
605 
606         // If rotary scrolling is enabled, set a focus change listener to highlight the scrollbar
607         // thumb when this recycler view is focused, i.e. when no focusable descendant is visible.
608         recyclerView.setOnFocusChangeListener(rotaryScrollEnabled ? (v, hasFocus) -> {
609             if (mScrollBar != null) mScrollBar.setHighlightThumb(hasFocus);
610         } : null);
611 
612         // This recyclerView is a rotary container if it's not a scrollable container.
613         if (!rotaryScrollEnabled) {
614             recyclerView.setContentDescription(ROTARY_CONTAINER);
615         }
616     }
617 
setRotaryScrollEnabled(@onNull View view, boolean isVertical)618     private static void setRotaryScrollEnabled(@NonNull View view, boolean isVertical) {
619         view.setContentDescription(
620                 isVertical ? ROTARY_VERTICALLY_SCROLLABLE : ROTARY_HORIZONTALLY_SCROLLABLE);
621     }
622 
623     private class UxRestrictionChangedListener implements
624             CarUxRestrictionsUtil.OnUxRestrictionsChangedListener {
625 
626         @Override
onRestrictionsChanged(@onNull CarUxRestrictions carUxRestrictions)627         public void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions) {
628             Adapter<?> adapter = mRecyclerView.getAdapter();
629             if (adapter == null) return;
630 
631             int maxItems = AdapterOEMV1.UNLIMITED;
632             if ((carUxRestrictions.getActiveRestrictions()
633                     & CarUxRestrictions.UX_RESTRICTIONS_LIMIT_CONTENT) != 0) {
634                 maxItems = carUxRestrictions.getMaxCumulativeContentItems();
635             }
636 
637             int originalCount = adapter.getItemCount();
638             ((AdapterWrapper) adapter).getOEMAdapter().setMaxItems(maxItems);
639             int newCount = adapter.getItemCount();
640 
641             if (newCount == originalCount) {
642                 return;
643             }
644 
645             if (newCount < originalCount) {
646                 adapter.notifyItemRangeRemoved(newCount, originalCount - newCount);
647             } else {
648                 adapter.notifyItemRangeInserted(originalCount, newCount - originalCount);
649             }
650         }
651     }
652 }
653