1 /*
2  * Copyright (C) 2023 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.google.android.car.kitchensink;
17 
18 import static com.google.android.car.kitchensink.KitchenSinkActivity.MENU_ENTRIES;
19 
20 import android.annotation.Nullable;
21 import android.car.Car;
22 import android.car.CarOccupantZoneManager;
23 import android.car.CarProjectionManager;
24 import android.car.drivingstate.CarUxRestrictions;
25 import android.car.hardware.CarSensorManager;
26 import android.car.hardware.hvac.CarHvacManager;
27 import android.car.hardware.power.CarPowerManager;
28 import android.car.hardware.property.CarPropertyManager;
29 import android.car.os.CarPerformanceManager;
30 import android.car.telemetry.CarTelemetryManager;
31 import android.car.watchdog.CarWatchdogManager;
32 import android.content.Intent;
33 import android.content.SharedPreferences;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.util.Log;
37 import android.util.Pair;
38 import android.view.View;
39 import android.widget.EditText;
40 import android.widget.Toast;
41 
42 import androidx.annotation.NonNull;
43 import androidx.fragment.app.Fragment;
44 import androidx.fragment.app.FragmentActivity;
45 
46 import com.android.car.ui.core.CarUi;
47 import com.android.car.ui.recyclerview.CarUiContentListItem;
48 import com.android.car.ui.recyclerview.CarUiRecyclerView;
49 import com.android.car.ui.toolbar.MenuItem;
50 import com.android.car.ui.toolbar.NavButtonMode;
51 import com.android.car.ui.toolbar.SearchMode;
52 import com.android.car.ui.toolbar.ToolbarController;
53 
54 import java.io.FileDescriptor;
55 import java.io.PrintWriter;
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.List;
59 import java.util.Locale;
60 import java.util.Optional;
61 
62 public class KitchenSink2Activity extends FragmentActivity implements KitchenSinkHelper {
63     static final String TAG = KitchenSink2Activity.class.getName();
64     private static final String LAST_FRAGMENT_TAG = "lastFragmentTag";
65     private static final String PREFERENCES_NAME = "fragment_item_prefs";
66     private static final String KEY_PINNED_ITEMS_LIST = "key_pinned_items_list";
67     private static final String KEY_COLLAPSE_STATE = "key_collapse_state";
68     private static final String DELIMITER = "::"; // A unique delimiter
69     @Nullable
70     private Fragment mLastFragment;
71     private int mNotificationId = 1000;
72     private static final int NO_INDEX = -1;
73     private static final String EMPTY_STRING = "";
74     private HighlightableAdapter mAdapter;
75     private final FragmentItemClickHandler mItemClickHandler = new FragmentItemClickHandler();
76 
77     /**
78      * List of all the original menu items.
79      */
80     private List<FragmentListItem> mData;
81 
82     /**
83      * Dynamic list of menu items that needs to be given to the adapter. Useful while searching.
84      */
85     private List<FragmentListItem> mFilteredData;
86     private boolean mIsSinglePane = false;
87     private ToolbarController mGlobalToolbar, mMiniToolbar;
88     private View mWrapper;
89     private CarUiRecyclerView mRV;
90     private CharSequence mLastFragmentTitle;
91     private MenuItem mFavButton;
92     private MenuItem mCollapsibleButton;
93     private boolean mIsSearching;
94     private int mPinnedItemsCount;
95     private SharedPreferences mSharedPreferences;
96     public static final String DUMP_ARG_CMD = "cmd";
97     public static final String DUMP_ARG_FRAGMENT = "fragment";
98     public static final String DUMP_ARG_QUIET = "quiet";
99 
100     private final KitchenSinkHelperImpl mKsHelperImpl = new KitchenSinkHelperImpl();
101 
102     @Override
getCar()103     public Car getCar() {
104         return mKsHelperImpl.getCar();
105     }
106 
107     @Override
requestRefreshManager(Runnable r, Handler h)108     public void requestRefreshManager(Runnable r, Handler h) {
109         mKsHelperImpl.requestRefreshManager(r, h);
110     }
111 
112     @Override
getPropertyManager()113     public CarPropertyManager getPropertyManager() {
114         return mKsHelperImpl.getPropertyManager();
115     }
116 
117     @Override
getHvacManager()118     public CarHvacManager getHvacManager() {
119         return mKsHelperImpl.getHvacManager();
120     }
121 
122     @Override
getOccupantZoneManager()123     public CarOccupantZoneManager getOccupantZoneManager() {
124         return mKsHelperImpl.getOccupantZoneManager();
125     }
126 
127     @Override
getPowerManager()128     public CarPowerManager getPowerManager() {
129         return mKsHelperImpl.getPowerManager();
130     }
131 
132     @Override
getSensorManager()133     public CarSensorManager getSensorManager() {
134         return mKsHelperImpl.getSensorManager();
135     }
136 
137     @Override
getProjectionManager()138     public CarProjectionManager getProjectionManager() {
139         return mKsHelperImpl.getProjectionManager();
140     }
141 
142     @Override
getCarTelemetryManager()143     public CarTelemetryManager getCarTelemetryManager() {
144         return mKsHelperImpl.getCarTelemetryManager();
145     }
146 
147     @Override
getCarWatchdogManager()148     public CarWatchdogManager getCarWatchdogManager() {
149         return mKsHelperImpl.getCarWatchdogManager();
150     }
151 
152     @Override
getPerformanceManager()153     public CarPerformanceManager getPerformanceManager() {
154         return mKsHelperImpl.getPerformanceManager();
155     }
156 
157     @Override
onPointerCaptureChanged(boolean hasCapture)158     public void onPointerCaptureChanged(boolean hasCapture) {
159         super.onPointerCaptureChanged(hasCapture);
160     }
161 
showDefaultFragment()162     private void showDefaultFragment() {
163         onFragmentItemClick(0);
164     }
165 
166     /**
167      * Searches in dynamic list (Not the whole list)
168      *
169      * @param title - Searches the list whose fragment title matches
170      * @return index of the item
171      */
getFragmentIndexFromTitle(String title)172     private int getFragmentIndexFromTitle(String title) {
173         for (int i = 0; i < mFilteredData.size(); i++) {
174             String targetText = mFilteredData.get(i).getTitle().getPreferredText().toString();
175             if (targetText.equalsIgnoreCase(title)) {
176                 return i;
177             }
178         }
179         return NO_INDEX;
180     }
181 
onFragmentItemClick(int fragIndex)182     public void onFragmentItemClick(int fragIndex) {
183         if (fragIndex < 0 || fragIndex >= mFilteredData.size()) return;
184         FragmentListItem fragmentListItem = mFilteredData.get(fragIndex);
185         Fragment fragment = fragmentListItem.getFragment();
186         if (mLastFragment != fragment) {
187             Log.v(TAG, "onFragmentItemClick(): from " + mLastFragment + " to " + fragment);
188         } else {
189             Log.v(TAG, "onFragmentItemClick(): showing " + fragment + " again");
190         }
191         getSupportFragmentManager()
192                 .beginTransaction()
193                 .replace(R.id.fragment_container, fragment)
194                 .commit();
195         mLastFragment = fragment;
196         mLastFragmentTitle = fragmentListItem.getTitle().getPreferredText();
197         mMiniToolbar.setTitle(mLastFragmentTitle);
198         mAdapter.requestHighlight(mLastFragmentTitle.toString(), fragIndex);
199         mFavButton.setIcon(
200                 fragmentListItem.isFavourite()
201                         ? getDrawable(R.drawable.ic_item_unpin)
202                         : getDrawable(R.drawable.ic_item_pin));
203     }
204 
205     @Override
onCreate(Bundle savedInstanceState)206     public void onCreate(Bundle savedInstanceState) {
207         super.onCreate(savedInstanceState);
208         mSharedPreferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
209 
210         setContentView(R.layout.activity_2pane);
211 
212         mKsHelperImpl.initCarApiIfAutomotive(this);
213 
214         mRV = findViewById(R.id.list_pane);
215         mWrapper = findViewById(R.id.wrapper);
216         mWrapper.setVisibility(mSharedPreferences.getInt(KEY_COLLAPSE_STATE, View.VISIBLE));
217 
218         setUpToolbars();
219 
220         mData = getProcessedData();
221         mFilteredData = new ArrayList<>(mData);
222         mAdapter = new HighlightableAdapter(this, mFilteredData, mRV);
223         mRV.setAdapter(mAdapter);
224 
225         // showing intent or default
226         showDefaultFragment();
227         onNewIntent(getIntent());
228     }
229 
setUpToolbars()230     private void setUpToolbars() {
231         View toolBarView = requireViewById(R.id.top_level_menu_container);
232 
233         mGlobalToolbar = CarUi.installBaseLayoutAround(
234                 toolBarView,
235                 insets -> findViewById(R.id.top_level_menu_container).setPadding(
236                         insets.getLeft(), insets.getTop(), insets.getRight(),
237                         insets.getBottom()), /* hasToolbar= */ true);
238 
239         MenuItem searchButton = new MenuItem.Builder(this)
240                 .setToSearch()
241                 .setOnClickListener(menuItem -> onSearchButtonClicked())
242                 .setUxRestrictions(CarUxRestrictions.UX_RESTRICTIONS_NO_KEYBOARD)
243                 .setId(R.id.toolbar_menu_item_search)
244                 .build();
245 
246         mGlobalToolbar.setMenuItems(List.of(searchButton));
247         mGlobalToolbar.registerBackListener(() -> {
248             mIsSearching = false;
249             mGlobalToolbar.setSearchMode(SearchMode.DISABLED);
250             mGlobalToolbar.setNavButtonMode(NavButtonMode.DISABLED);
251             mGlobalToolbar.unregisterOnSearchListener(this::onQueryChanged);
252             EditText mSearchText = findViewById(R.id.car_ui_toolbar_search_bar);
253             mSearchText.getText().clear();
254             onQueryChanged(EMPTY_STRING);
255             mAdapter.onSearchEnded();
256             return true;
257         });
258 
259         mGlobalToolbar.setTitle(getString(R.string.app_title));
260         mGlobalToolbar.setNavButtonMode(NavButtonMode.DISABLED);
261         mGlobalToolbar.setLogo(R.drawable.ic_launcher);
262 //        if (mIsSinglePane) {
263 //            mGlobalToolbar.setNavButtonMode(NavButtonMode.BACK);
264 //            findViewById(R.id.top_level_menu_container).setVisibility(View.GONE);
265 //            findViewById(R.id.top_level_divider).setVisibility(View.GONE);
266 //            return;
267 //        }
268         mMiniToolbar = CarUi.installBaseLayoutAround(
269                 requireViewById(R.id.fragment_container_wrapper),
270                 insets -> findViewById(R.id.fragment_container_wrapper).setPadding(
271                         insets.getLeft(), insets.getTop(), insets.getRight(),
272                         insets.getBottom()), /* hasToolbar= */ true);
273 
274         mFavButton = new MenuItem.Builder(this)
275                 .setOnClickListener(i -> onFavClicked())
276                 .setUxRestrictions(CarUxRestrictions.UX_RESTRICTIONS_NO_KEYBOARD)
277                 .build();
278 
279         mCollapsibleButton = new MenuItem.Builder(this)
280                 .setOnClickListener(i -> toggleMenuWrapper())
281                 .setUxRestrictions(CarUxRestrictions.UX_RESTRICTIONS_NO_KEYBOARD)
282                 .setIcon(mWrapper.getVisibility() == View.GONE
283                         ? R.drawable.ic_left_panel_open
284                         : R.drawable.ic_left_panel_close)
285                 .build();
286 
287         mMiniToolbar.setMenuItems(List.of(mFavButton, mCollapsibleButton));
288         mMiniToolbar.setNavButtonMode(NavButtonMode.BACK);
289     }
290 
onSearchButtonClicked()291     private void onSearchButtonClicked() {
292         mIsSearching = true;
293         mGlobalToolbar.setSearchMode(SearchMode.SEARCH);
294         mGlobalToolbar.setNavButtonMode(NavButtonMode.BACK);
295         mGlobalToolbar.registerOnSearchListener(this::onQueryChanged);
296     }
297 
onQueryChanged(String query)298     private void onQueryChanged(String query) {
299         mFilteredData.clear();
300         if (query.isEmpty()) {
301             mFilteredData.addAll(mData);
302         } else {
303             for (FragmentListItem item : mData) {
304                 if (item.getTitle().getPreferredText().toString().toLowerCase().contains(
305                         query.toLowerCase())) {
306                     mFilteredData.add(item);
307                 }
308             }
309         }
310         mAdapter.afterTextChanged();
311     }
312 
onFavClicked()313     private void onFavClicked() {
314         int fromIndex = getOriginalIndexFromTitle(mLastFragmentTitle, 0, false);
315         int toIndex;
316         String text;
317 
318         FragmentListItem fragmentListItem = mData.get(fromIndex);
319         if (fragmentListItem.isFavourite()) {
320             // Un-pinning: Moving the item to its lexicographic position
321             toIndex = getOriginalIndexFromTitle(
322                     fragmentListItem.getTitle().getPreferredText(), mPinnedItemsCount,
323                     true);
324             text = getString(R.string.toast_item_unpinned_message, mLastFragmentTitle);
325             mPinnedItemsCount--;
326         } else {
327             // Pinning: Moving the item to the top most position.
328             toIndex = 0;
329             text = getString(R.string.toast_item_pinned_message, mLastFragmentTitle);
330             mPinnedItemsCount++;
331         }
332 
333         moveFragmentItem(fromIndex, toIndex);
334         fragmentListItem.toggleFavourite();
335 
336         Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
337         mFavButton.setIcon(
338                 fragmentListItem.isFavourite()
339                         ? getDrawable(R.drawable.ic_item_unpin)
340                         : getDrawable(R.drawable.ic_item_pin));
341 
342     }
343 
344     /**
345      * Finds index from the original data (Not from the dynamic list)
346      *
347      * @param fragmentTitle   - Finds by comparing the title
348      * @param startFrom       - the starting index it should search from
349      * @param isLexicographic - If set to true, finds lexicographical position by comparing
350      *                        strings. If false, finds the
351      *                        index with exact string match or -1.
352      */
getOriginalIndexFromTitle(CharSequence fragmentTitle, int startFrom, boolean isLexicographic)353     private int getOriginalIndexFromTitle(CharSequence fragmentTitle, int startFrom,
354             boolean isLexicographic) {
355         if (fragmentTitle.toString().equalsIgnoreCase(EMPTY_STRING)) return NO_INDEX;
356         for (int i = startFrom; i < mData.size(); i++) {
357             String targetText = mData.get(i).getTitle().getPreferredText().toString();
358             if (isLexicographic && targetText.compareToIgnoreCase(fragmentTitle.toString()) > 0) {
359                 return i - 1;
360             }
361             if (!isLexicographic && targetText.equalsIgnoreCase(fragmentTitle.toString())) {
362                 return i;
363             }
364         }
365         return isLexicographic ? mData.size() - 1 : NO_INDEX;
366     }
367 
368     /**
369      * Moves the fragmentItem from @param "from" to @param "to"
370      * Used for both pinning and unpinning an item.
371      *
372      * @param from - the current index of the item
373      * @param to   - the target index to move the item
374      */
moveFragmentItem(int from, int to)375     private void moveFragmentItem(int from, int to) {
376         if (from < 0 || from >= mData.size() || to < 0 || to >= mData.size()) return;
377         mData.add(to, mData.remove(from));
378         if (!mIsSearching) {
379             mFilteredData.add(to, mFilteredData.remove(from));
380             mAdapter.afterFavClicked(from, to);
381         }
382     }
383 
toggleMenuWrapper()384     private void toggleMenuWrapper() {
385         if (mWrapper.getVisibility() == View.VISIBLE) {
386             mWrapper.setVisibility(View.GONE);
387             mCollapsibleButton.setIcon(R.drawable.ic_left_panel_open);
388         } else {
389             mWrapper.setVisibility(View.VISIBLE);
390             mCollapsibleButton.setIcon(R.drawable.ic_left_panel_close);
391         }
392     }
393 
saveVisibilityState()394     private void saveVisibilityState() {
395         if (mSharedPreferences == null) {
396             mSharedPreferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
397         }
398         mSharedPreferences.edit()
399                 .putInt(KEY_COLLAPSE_STATE, mWrapper.getVisibility())
400                 .apply();
401     }
402 
getProcessedData()403     List<FragmentListItem> getProcessedData() {
404 
405         List<String> pinnedTitles = getPinnedTitlesFromPrefs();
406 
407         List<FragmentListItem> allItems = new ArrayList<>();
408         ArrayList<FragmentListItem> pinnedItems = new ArrayList<>();
409         mPinnedItemsCount = pinnedTitles.size();
410         for (int i = 0; i < mPinnedItemsCount; i++) {
411             pinnedItems.add(null);
412         }
413 
414         for (Pair<String, Class> entry : MENU_ENTRIES) {
415             // Retrieves the pinned position and preserves it in the same order
416             int pinnedPosition = pinnedTitles.indexOf(entry.first.toLowerCase());
417             if (pinnedPosition >= 0) {
418                 pinnedItems.set(pinnedPosition,
419                         new FragmentListItem(entry.first, true, entry.second, mItemClickHandler));
420             } else {
421                 allItems.add(
422                         new FragmentListItem(entry.first, false, entry.second, mItemClickHandler));
423             }
424         }
425 
426         allItems.sort((o1, o2) -> {
427             String s1 = o1.getTitle().getPreferredText().toString();
428             String s2 = o2.getTitle().getPreferredText().toString();
429             return s1.compareToIgnoreCase(s2);
430         });
431 
432         allItems.addAll(0, pinnedItems);
433         return allItems;
434     }
435 
436     @NonNull
getPinnedTitlesFromPrefs()437     List<String> getPinnedTitlesFromPrefs() {
438         if (mSharedPreferences == null) return new ArrayList<>();
439 
440         String pinnedTitles = mSharedPreferences.getString(KEY_PINNED_ITEMS_LIST, "");
441         if (pinnedTitles.isEmpty()) {
442             return new ArrayList<>();
443         } else {
444             return Arrays.asList(pinnedTitles.split(DELIMITER));
445         }
446     }
447 
448     @Override
onPause()449     protected void onPause() {
450         super.onPause();
451         savePinnedItemsToPreferences();
452         saveVisibilityState();
453     }
454 
455     @Override
onSaveInstanceState(Bundle outState)456     protected void onSaveInstanceState(Bundle outState) {
457         outState.putString(LAST_FRAGMENT_TAG, mLastFragmentTitle.toString());
458         super.onSaveInstanceState(outState);
459     }
460 
461     @Override
onRestoreInstanceState(Bundle savedInstanceState)462     protected void onRestoreInstanceState(Bundle savedInstanceState) {
463         super.onRestoreInstanceState(savedInstanceState);
464         // The app is being started for the first time.
465         if (savedInstanceState == null) {
466             return;
467         }
468 
469         // The app is being reloaded, restores the last fragment UI.
470         mLastFragmentTitle = savedInstanceState.getString(LAST_FRAGMENT_TAG, "");
471         if (mLastFragmentTitle.isEmpty()) {
472             showDefaultFragment();
473         } else {
474             onFragmentItemClick(getFragmentIndexFromTitle(mLastFragmentTitle.toString()));
475         }
476     }
477 
savePinnedItemsToPreferences()478     private void savePinnedItemsToPreferences() {
479         if (mSharedPreferences == null) {
480             mSharedPreferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
481         }
482         SharedPreferences.Editor editor = mSharedPreferences.edit();
483 
484         StringBuilder sb = new StringBuilder();
485         for (int i = 0; i < mPinnedItemsCount; i++) {
486             sb.append(mData.get(i).getTitle().getPreferredText()).append(DELIMITER);
487         }
488 
489         // Remove the last delimiter
490         if (sb.length() > 0) {
491             sb.setLength(sb.length() - DELIMITER.length());
492         }
493 
494         editor.putString(KEY_PINNED_ITEMS_LIST, sb.toString().toLowerCase(Locale.US));
495         editor.apply();
496     }
497 
498     /* Open any tab directly:
499      * adb shell am force-stop com.google.android.car.kitchensink
500      * adb shell am 'start -n com.google.android.car.kitchensink/.KitchenSink2Activity --es select
501      *  "connectivity"'
502      *-ee
503      * Test car watchdog:
504      * adb shell am force-stop com.google.android.car.kitchensink
505      * adb shell am start -n com.google.android.car.kitchensink/.KitchenSink2Activity \
506      *     --es "watchdog" "[timeout] [not_respond_after] [inactive_main_after] [verbose]"
507      * - timeout: critical | moderate | normal
508      * - not_respond_after: after the given seconds, the client will not respond to car watchdog
509      *                      (-1 for making the client respond always)
510      * - inactive_main_after: after the given seconds, the main thread will not be responsive
511      *                        (-1 for making the main thread responsive always)
512      * - verbose: whether to output verbose logs (default: false)
513      */
514     @Override
onNewIntent(Intent intent)515     protected void onNewIntent(Intent intent) {
516         super.onNewIntent(intent);
517         Log.d(TAG, "onNewIntent");
518         Bundle extras = intent.getExtras();
519         if (extras == null) {
520             return;
521         }
522         String watchdog = extras.getString("watchdog");
523         if (watchdog != null) {
524             CarWatchdogClient.start(getCar(), watchdog);
525         }
526         String select = extras.getString("select", "");
527         Log.d(TAG, "Trying to launch entry: " + select);
528         int fragmentItemIndex = getOriginalIndexFromTitle(select, 0, false);
529         onFragmentItemClick(fragmentItemIndex);
530     }
531 
532     @Override
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)533     public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
534         boolean skipParentState = false;
535         if (args != null && args.length > 0) {
536             Log.v(TAG, "dump: args=" + Arrays.toString(args));
537             String arg = args[0];
538             switch (arg) {
539                 case DUMP_ARG_CMD:
540                     String[] cmdArgs = new String[args.length - 1];
541                     System.arraycopy(args, 1, cmdArgs, 0, args.length - 1);
542                     new KitchenSinkShellCommand(this, writer, cmdArgs, mNotificationId++).run();
543                     return;
544                 case DUMP_ARG_FRAGMENT:
545                     if (args.length < 2) {
546                         writer.println("Missing fragment name");
547                         return;
548                     }
549                     String select = args[1];
550                     Optional<FragmentListItem> entry = mData.stream()
551                             .filter(me -> select.equals(
552                                     me.getTitle().getPreferredText().toString())).findAny();
553                     if (entry.isPresent()) {
554                         String[] strippedArgs = new String[args.length - 2];
555                         System.arraycopy(args, 2, strippedArgs, 0, strippedArgs.length);
556                         entry.get().dump(prefix, fd, writer, strippedArgs);
557                     } else {
558                         writer.printf("No entry called '%s'\n", select);
559                     }
560                     return;
561                 case DUMP_ARG_QUIET:
562                     skipParentState = true;
563                     break;
564                 default:
565                     Log.v(TAG, "dump(): unknown arg, calling super(): " + Arrays.toString(args));
566             }
567         }
568         String innerPrefix = prefix;
569         if (!skipParentState) {
570             writer.printf("%sCustom state:\n", prefix);
571             innerPrefix = prefix + prefix;
572         }
573         writer.printf("%smLastFragmentTag: %s\n", innerPrefix, mLastFragmentTitle);
574         writer.printf("%smLastFragment: %s\n", innerPrefix, mLastFragment);
575         writer.printf("%sNext Notification Id: %d\n", innerPrefix, mNotificationId);
576 
577         if (skipParentState) {
578             Log.v(TAG, "dump(): skipping parent state");
579             return;
580         }
581         writer.println();
582 
583         super.dump(prefix, fd, writer, args);
584     }
585 
586     private class FragmentItemClickHandler implements CarUiContentListItem.OnClickListener {
587         @Override
onClick(@onNull CarUiContentListItem carUiContentListItem)588         public void onClick(@NonNull CarUiContentListItem carUiContentListItem) {
589             int fragmentItemIndex = getFragmentIndexFromTitle(
590                     carUiContentListItem.getTitle().getPreferredText().toString());
591             onFragmentItemClick(fragmentItemIndex);
592         }
593     }
594 
595 }
596