Add tabs to car-ui-lib shared library

Bug: 175624230
Test: Manually
Change-Id: I63289d5a843a51fd1bdc704eb27b0e465abc9673
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ImageViewListener.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ImageViewListener.java
index 288de5b..106b612 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ImageViewListener.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ImageViewListener.java
@@ -23,11 +23,11 @@
 import java.util.function.Consumer;
 
 @SuppressWarnings("AndroidJdkLibsChecker")
-final class ImageViewListener extends ImageView {
+public final class ImageViewListener extends ImageView {
 
     private Consumer<Drawable> mImageDrawableListener;
 
-    ImageViewListener(Context context) {
+    public ImageViewListener(Context context) {
         super(context);
     }
 
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabAdapterV1.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabAdapterV1.java
index 1f45508..477d42f 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabAdapterV1.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabAdapterV1.java
@@ -35,22 +35,12 @@
 
     TabAdapterV1(Context context, Tab clientTab, Runnable onClickListener) {
         ImageViewListener imageView = new ImageViewListener(context);
-        imageView.setImageDrawableListener(drawable -> {
-            mIcon = drawable;
-            if (mUpdateListener != null) {
-                mUpdateListener.accept(this);
-            }
-        });
-        clientTab.bindIcon(imageView);
+        imageView.setImageDrawableListener(this::setIcon);
+        clientTab.bindIconPublic(imageView);
 
         TextViewListener textView = new TextViewListener(context);
-        textView.setTextListener(text -> {
-            mText = text;
-            if (mUpdateListener != null) {
-                mUpdateListener.accept(this);
-            }
-        });
-        clientTab.bindText(textView);
+        textView.setTextListener(this::setTitle);
+        clientTab.bindTextPublic(textView);
 
         mOnClickListener = onClickListener;
         mClientTab = clientTab;
@@ -70,11 +60,25 @@
         return mText;
     }
 
+    public void setTitle(CharSequence title) {
+        mText = title;
+        if (mUpdateListener != null) {
+            mUpdateListener.accept(this);
+        }
+    }
+
     @Override
     public Drawable getIcon() {
         return mIcon;
     }
 
+    public void setIcon(Drawable icon) {
+        mIcon = icon;
+        if (mUpdateListener != null) {
+            mUpdateListener.accept(this);
+        }
+    }
+
     @Override
     public Runnable getOnClickListener() {
         return mOnClickListener;
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabLayout.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabLayout.java
index 1f973d2..f16d8bd 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabLayout.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabLayout.java
@@ -295,9 +295,25 @@
             textView.setText(mText);
         }
 
+        /**
+         * Do not use, this method is here for the shared library adapters, which cannot
+         * call the protected version due to being in a different classloader.
+         */
+        public final void bindTextPublic(TextView textView) {
+            bindText(textView);
+        }
+
         /** Set icon drawable. TODO(b/139444064): revise this api.*/
         protected void bindIcon(ImageView imageView) {
             imageView.setImageDrawable(mIcon);
         }
+
+        /**
+         * Do not use, this method is here for the shared library adapters, which cannot
+         * call the protected version due to being in a different classloader.
+         */
+        public final void bindIconPublic(ImageView imageView) {
+            bindIcon(imageView);
+        }
     }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TextViewListener.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TextViewListener.java
index 319e0f4..ca6152a 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TextViewListener.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TextViewListener.java
@@ -22,10 +22,10 @@
 import java.util.function.Consumer;
 
 @SuppressWarnings("AndroidJdkLibsChecker")
-final class TextViewListener extends TextView {
+public final class TextViewListener extends TextView {
     private Consumer<CharSequence> mTextListener;
 
-    TextViewListener(Context context) {
+    public TextViewListener(Context context) {
         super(context);
     }
 
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerAdapterV1.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerAdapterV1.java
index 0dcda9a..2d4c62c 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerAdapterV1.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerAdapterV1.java
@@ -19,6 +19,7 @@
 import android.app.Activity;
 import android.content.Context;
 import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
 
@@ -42,6 +43,7 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.Function;
 
@@ -54,15 +56,10 @@
 
     private static final String TAG = ToolbarControllerAdapterV1.class.getName();
 
-    private ToolbarControllerOEMV1 mOemToolbar;
-    private Context mContext;
+    private final ToolbarControllerOEMV1 mOemToolbar;
+    private final Context mContext;
 
-    private final List<TabAdapterV1> mTabs = new ArrayList<>();
-    private State mState = State.HOME;
-    private CharSequence mTitle = null;
-    private CharSequence mSubtitle = null;
-    private Drawable mLogo = null;
-    private boolean mShowTabsInSubpage = false;
+    private ToolbarAdapterState mAdapterState = new ToolbarAdapterState();
     private final Set<OnTabSelectedListener> mOnTabSelectedListeners = new HashSet<>();
     private final Set<OnBackListener> mOnBackListeners = new HashSet<>();
     private final Set<OnSearchListener> mOnSearchListeners = new HashSet<>();
@@ -113,15 +110,12 @@
 
     @Override
     public void setTitle(CharSequence title) {
-        mTitle = title;
-        if (stateHasLogoTitleOrSubtitle(mState)) {
-            mOemToolbar.setTitle(title);
-        }
+        update(mAdapterState.copy().setTitle(title).build());
     }
 
     @Override
     public CharSequence getTitle() {
-        return mTitle;
+        return mAdapterState.getTitle();
     }
 
     @Override
@@ -131,26 +125,24 @@
 
     @Override
     public void setSubtitle(CharSequence subtitle) {
-        mSubtitle = subtitle;
-        if (stateHasLogoTitleOrSubtitle(mState)) {
-            mOemToolbar.setSubtitle(subtitle);
-        }
+        update(mAdapterState.copy().setSubtitle(subtitle).build());
     }
 
     @Override
     public CharSequence getSubtitle() {
-        return mSubtitle;
+        return mAdapterState.getSubtitle();
     }
 
     @Override
     public int getTabCount() {
-        return mTabs.size();
+        return mAdapterState.getTabs().size();
     }
 
     @Override
     public int getTabPosition(Tab tab) {
-        for (int i = 0; i < mTabs.size(); i++) {
-            if (mTabs.get(i).getClientTab() == tab) {
+        List<TabAdapterV1> tabs = mAdapterState.getTabs();
+        for (int i = 0; i < tabs.size(); i++) {
+            if (tabs.get(i).getClientTab() == tab) {
                 return i;
             }
         }
@@ -159,40 +151,64 @@
 
     @Override
     public void addTab(Tab clientTab) {
-        mTabs.add(new TabAdapterV1(mContext, clientTab, () -> {
+        ToolbarAdapterState.Builder newStateBuilder = mAdapterState.copy();
+        newStateBuilder.addTab(new TabAdapterV1(mContext, clientTab, () -> {
+            List<TabAdapterV1> tabs = mAdapterState.getTabs();
+            int selectedIndex = -1;
+            for (int i = 0; i < tabs.size(); i++) {
+                if (tabs.get(i).getClientTab() == clientTab) {
+                    selectedIndex = i;
+                    break;
+                }
+            }
+            // We need to update selectedIndex in our state, but don't need to call update(),
+            // as this change originated in the shared library so it already knows about it.
+            mAdapterState = mAdapterState.copy().setSelectedTab(selectedIndex).build();
+
             for (OnTabSelectedListener listener : mOnTabSelectedListeners) {
                 listener.onTabSelected(clientTab);
             }
         }));
-        setState(getState());
+        if (mAdapterState.getSelectedTab() < 0) {
+            newStateBuilder.setSelectedTab(0);
+        }
+        update(newStateBuilder.build());
     }
 
     @Override
     public void clearAllTabs() {
-        mTabs.clear();
-        setState(getState());
+        update(mAdapterState.copy()
+                .setTabs(Collections.emptyList())
+                .setSelectedTab(-1)
+                .build());
     }
 
     @Override
     public Tab getTab(int position) {
-        TabAdapterV1 tab = mTabs.get(position);
+        List<TabAdapterV1> tabs = mAdapterState.getTabs();
+        if (position < 0 || position >= tabs.size()) {
+            throw new IllegalArgumentException("Tab position is invalid: " + position);
+        }
+        TabAdapterV1 tab = tabs.get(position);
         return tab.getClientTab();
     }
 
     @Override
     public void selectTab(int position) {
-        mOemToolbar.selectTab(position);
+        if (position < 0 || position >= mAdapterState.getTabs().size()) {
+            throw new IllegalArgumentException("Tab position is invalid: " + position);
+        }
+        update(mAdapterState.copy().setSelectedTab(position).build());
     }
 
     @Override
     public void setShowTabsInSubpage(boolean showTabs) {
-        mShowTabsInSubpage = showTabs;
-        setState(getState());
+        update(mAdapterState.copy().setShowTabsInSubpage(showTabs).build());
     }
 
     @Override
     public boolean getShowTabsInSubpage() {
-        return mShowTabsInSubpage;
+        return mAdapterState.getShowTabsInSubpage();
     }
 
     @Override
@@ -206,10 +222,7 @@
 
     @Override
     public void setLogo(Drawable drawable) {
-        mLogo = drawable;
-        if (stateHasLogoTitleOrSubtitle(mState)) {
-            mOemToolbar.setLogo(drawable);
-        }
+        update(mAdapterState.copy().setLogo(drawable).build());
     }
 
     @Override
@@ -282,9 +295,11 @@
 
     @Override
     public List<MenuItem> setMenuItems(int resId) {
-        List<MenuItem> menuItems = MenuItemRenderer.readMenuItemList(mContext, resId);
-        setMenuItemsInternal(menuItems);
-        return menuItems;
+        //TODO(b/175624230) MenuItemRenderer cannot be reached from this classloader
+        //List<MenuItem> menuItems = MenuItemRenderer.readMenuItemList(mContext, resId);
+        //setMenuItemsInternal(menuItems);
+        //return menuItems;
+        return Collections.emptyList();
     }
 
     private void setMenuItemsInternal(@Nullable List<MenuItem> items) {
@@ -337,49 +352,87 @@
 
     @Override
     public void setState(State state) {
-        boolean gainingLogoTitleOrSubtitle =
-                stateHasLogoTitleOrSubtitle(state) && !stateHasLogoTitleOrSubtitle(mState);
-        boolean losingLogoTitleOrSubtitle =
-                !stateHasLogoTitleOrSubtitle(state) && stateHasLogoTitleOrSubtitle(mState);
-        mState = state;
-
-        if (gainingLogoTitleOrSubtitle) {
-            mOemToolbar.setLogo(mLogo);
-            mOemToolbar.setTitle(mTitle);
-            mOemToolbar.setSubtitle(mSubtitle);
-        } else if (losingLogoTitleOrSubtitle) {
-            mOemToolbar.setLogo(null);
-            mOemToolbar.setTitle(null);
-            mOemToolbar.setSubtitle(null);
-        }
-
-        switch (state) {
-            case SEARCH:
-                mOemToolbar.setSearchMode(ToolbarControllerOEMV1.SEARCH_MODE_SEARCH);
-                break;
-            case EDIT:
-                mOemToolbar.setSearchMode(ToolbarControllerOEMV1.SEARCH_MODE_EDIT);
-                break;
-            default:
-                mOemToolbar.setSearchMode(ToolbarControllerOEMV1.SEARCH_MODE_DISABLED);
-        }
-
-        mOemToolbar.setBackButtonVisible(state != State.HOME);
-
-        if (state == State.HOME || (state == State.SUBPAGE && mShowTabsInSubpage)) {
-            mOemToolbar.setTabs(mTabs);
-        } else {
-            mOemToolbar.setTabs(Collections.emptyList());
-        }
+        update(mAdapterState.copy().setState(state).build());
     }
 
-    private boolean stateHasLogoTitleOrSubtitle(State state) {
-        return state == State.HOME || state == State.SUBPAGE;
+    /**
+     * This method takes a new {@link ToolbarAdapterState} and compares it to the current
+     * {@link #mAdapterState}. It then sends any differences it detects to the shared library
+     * toolbar.
+     *
+     * This is also the core of the logic that adapts from the client's toolbar interface to
+     * the OEM apis toolbar interface. For example, when you are in the HOME state and add tabs,
+     * it will call setTitle(null) on the shared library toolbar. This is because the client
+     * interface
+     */
+    private void update(ToolbarAdapterState newAdapterState) {
+        ToolbarAdapterState oldAdapterState = mAdapterState;
+        mAdapterState = newAdapterState;
+
+        boolean gainingTitleOrSubtitle = newAdapterState.hasTitleOrSubtitle()
+                && !oldAdapterState.hasTitleOrSubtitle();
+        boolean losingTitleOrSubtitle = !newAdapterState.hasTitleOrSubtitle()
+                && oldAdapterState.hasTitleOrSubtitle();
+        boolean gainingLogo = newAdapterState.hasLogo() && !oldAdapterState.hasLogo();
+        boolean losingLogo = !newAdapterState.hasLogo() && oldAdapterState.hasLogo();
+
+        if (gainingTitleOrSubtitle) {
+            mOemToolbar.setTitle(newAdapterState.getTitle());
+            mOemToolbar.setSubtitle(newAdapterState.getSubtitle());
+        } else if (losingTitleOrSubtitle) {
+            mOemToolbar.setTitle(null);
+            mOemToolbar.setSubtitle(null);
+        } else if (newAdapterState.hasTitleOrSubtitle()) {
+            if (!TextUtils.equals(newAdapterState.getTitle(), oldAdapterState.getTitle())) {
+                mOemToolbar.setTitle(newAdapterState.getTitle());
+            }
+            if (!TextUtils.equals(newAdapterState.getSubtitle(), oldAdapterState.getSubtitle())) {
+                mOemToolbar.setSubtitle(newAdapterState.getSubtitle());
+            }
+        }
+
+        if (gainingLogo) {
+            mOemToolbar.setLogo(newAdapterState.getLogo());
+        } else if (losingLogo) {
+            mOemToolbar.setLogo(null);
+        } else if (newAdapterState.hasLogo() && newAdapterState.getLogoDirty()) {
+            mOemToolbar.setLogo(newAdapterState.getLogo());
+        }
+
+        if (newAdapterState.getState() != oldAdapterState.getState()) {
+            switch (newAdapterState.getState()) {
+                case SEARCH:
+                    mOemToolbar.setSearchMode(ToolbarControllerOEMV1.SEARCH_MODE_SEARCH);
+                    break;
+                case EDIT:
+                    mOemToolbar.setSearchMode(ToolbarControllerOEMV1.SEARCH_MODE_EDIT);
+                    break;
+                default:
+                    mOemToolbar.setSearchMode(ToolbarControllerOEMV1.SEARCH_MODE_DISABLED);
+            }
+        }
+
+        if (oldAdapterState.hasBackButton() != newAdapterState.hasBackButton()) {
+            mOemToolbar.setBackButtonVisible(newAdapterState.hasBackButton());
+        }
+
+        boolean gainingTabs = newAdapterState.hasTabs() && !oldAdapterState.hasTabs();
+        boolean losingTabs = !newAdapterState.hasTabs() && oldAdapterState.hasTabs();
+        if (gainingTabs) {
+            mOemToolbar.setTabs(newAdapterState.getTabs(), newAdapterState.getSelectedTab());
+        } else if (losingTabs) {
+            mOemToolbar.setTabs(Collections.emptyList(), -1);
+        } else if (newAdapterState.hasTabs() && newAdapterState.getTabsDirty()) {
+            mOemToolbar.setTabs(newAdapterState.getTabs(), newAdapterState.getSelectedTab());
+        } else if (newAdapterState.hasTabs()
+                && newAdapterState.getSelectedTab() != oldAdapterState.getSelectedTab()) {
+            mOemToolbar.selectTab(newAdapterState.getSelectedTab());
+        }
     }
 
     @Override
     public State getState() {
-        return mState;
+        return mAdapterState.getState();
     }
 
     @Override
@@ -475,4 +528,205 @@
         }
         return result;
     }
+
+    private static class ToolbarAdapterState {
+        private final State mState;
+        private final boolean mShowTabsInSubpage;
+        @NonNull
+        private final List<TabAdapterV1> mTabs;
+        private final int mSelectedTab;
+        private final CharSequence mTitle;
+        private final CharSequence mSubtitle;
+        private final Drawable mLogo;
+        private final boolean mTabsDirty;
+        private final boolean mLogoDirty;
+
+        ToolbarAdapterState() {
+            mState = State.HOME;
+            mShowTabsInSubpage = false;
+            mTabs = Collections.emptyList();
+            mSelectedTab = -1;
+            mTitle = null;
+            mSubtitle = null;
+            mLogo = null;
+            mTabsDirty = false;
+            mLogoDirty = false;
+        }
+
+        private ToolbarAdapterState(Builder builder) {
+            mState = builder.mState;
+            mShowTabsInSubpage = builder.mShowTabsInSubpage;
+            mTabs = builder.mTabs;
+            mSelectedTab = builder.mSelectedTab;
+            mTitle = builder.mTitle;
+            mSubtitle = builder.mSubtitle;
+            mLogo = builder.mLogo;
+            mTabsDirty = builder.mTabsDirty;
+            mLogoDirty = builder.mLogoDirty;
+        }
+
+        public State getState() {
+            return mState;
+        }
+
+        public boolean getShowTabsInSubpage() {
+            return mShowTabsInSubpage;
+        }
+
+        @NonNull
+        public List<TabAdapterV1> getTabs() {
+            return mTabs;
+        }
+
+        public int getSelectedTab() {
+            return mSelectedTab;
+        }
+
+        public CharSequence getTitle() {
+            return mTitle;
+        }
+
+        public CharSequence getSubtitle() {
+            return mSubtitle;
+        }
+
+        public Drawable getLogo() {
+            return mLogo;
+        }
+
+        public boolean getTabsDirty() {
+            return mTabsDirty;
+        }
+
+        public boolean getLogoDirty() {
+            return mLogoDirty;
+        }
+
+        private boolean hasLogo() {
+            State state = getState();
+            return (state == State.HOME || state == State.SUBPAGE) && getLogo() != null;
+        }
+
+        private boolean hasTitleOrSubtitle() {
+            State state = getState();
+            return state == State.HOME || state == State.SUBPAGE;
+        }
+
+        private boolean hasTabs() {
+            State state = getState();
+            return (state == State.HOME
+                    || state == State.SUBPAGE && getShowTabsInSubpage())
+                    && getTabs().size() > 0;
+        }
+
+        private boolean hasBackButton() {
+            return getState() != State.HOME;
+        }
+
+        public Builder copy() {
+            return new Builder(this);
+        }
+
+        public static class Builder {
+            private final ToolbarAdapterState mStateClonedFrom;
+            private boolean mWasChanged = false;
+            private State mState;
+            private boolean mShowTabsInSubpage;
+            @NonNull
+            private List<TabAdapterV1> mTabs;
+            private int mSelectedTab;
+            private CharSequence mTitle;
+            private CharSequence mSubtitle;
+            private Drawable mLogo;
+            private boolean mTabsDirty = false;
+            private boolean mLogoDirty = false;
+
+            Builder(ToolbarAdapterState state) {
+                mStateClonedFrom = state;
+                mState = state.getState();
+                mShowTabsInSubpage = state.getShowTabsInSubpage();
+                mTabs = state.getTabs();
+                mSelectedTab = state.getSelectedTab();
+                mTitle = state.getTitle();
+                mSubtitle = state.getSubtitle();
+                mLogo = state.getLogo();
+            }
+
+            public ToolbarAdapterState build() {
+                if (!mWasChanged) {
+                    return mStateClonedFrom;
+                } else {
+                    return new ToolbarAdapterState(this);
+                }
+            }
+
+            public Builder setState(State state) {
+                if (state != mState) {
+                    mState = state;
+                    mWasChanged = true;
+                }
+                return this;
+            }
+
+            public Builder setShowTabsInSubpage(boolean showTabsInSubpage) {
+                if (mShowTabsInSubpage != showTabsInSubpage) {
+                    mShowTabsInSubpage = showTabsInSubpage;
+                    mWasChanged = true;
+                }
+                return this;
+            }
+
+            public Builder setTabs(
+                    @NonNull List<TabAdapterV1> tabs) {
+                if (!Objects.equals(tabs, mTabs)) {
+                    mTabs = Collections.unmodifiableList(tabs);
+                    mWasChanged = true;
+                    mTabsDirty = true;
+                }
+                return this;
+            }
+
+            public Builder addTab(@NonNull TabAdapterV1 tab) {
+                List<TabAdapterV1> newTabs = new ArrayList<>(mTabs);
+                newTabs.add(tab);
+                mTabs = Collections.unmodifiableList(newTabs);
+                mWasChanged = true;
+                mTabsDirty = true;
+                return this;
+            }
+
+            public Builder setSelectedTab(int selectedTab) {
+                if (mSelectedTab != selectedTab) {
+                    mSelectedTab = selectedTab;
+                    mWasChanged = true;
+                }
+                return this;
+            }
+
+            public Builder setTitle(CharSequence title) {
+                if (!Objects.equals(mTitle, title)) {
+                    mTitle = title;
+                    mWasChanged = true;
+                }
+                return this;
+            }
+
+            public Builder setSubtitle(CharSequence subtitle) {
+                if (!Objects.equals(mSubtitle, subtitle)) {
+                    mSubtitle = subtitle;
+                    mWasChanged = true;
+                }
+                return this;
+            }
+
+            public Builder setLogo(Drawable logo) {
+                if (mLogo != logo) {
+                    mLogo = logo;
+                    mWasChanged = true;
+                    mLogoDirty = true;
+                }
+                return this;
+            }
+        }
+    }
 }
diff --git a/car-ui-lib/oem-apis/src/main/java/com/android/car/ui/sharedlibrary/oemapis/toolbar/ToolbarControllerOEMV1.java b/car-ui-lib/oem-apis/src/main/java/com/android/car/ui/sharedlibrary/oemapis/toolbar/ToolbarControllerOEMV1.java
index 17efbf9..d2b4719 100644
--- a/car-ui-lib/oem-apis/src/main/java/com/android/car/ui/sharedlibrary/oemapis/toolbar/ToolbarControllerOEMV1.java
+++ b/car-ui-lib/oem-apis/src/main/java/com/android/car/ui/sharedlibrary/oemapis/toolbar/ToolbarControllerOEMV1.java
@@ -16,7 +16,6 @@
 
 package com.android.car.ui.sharedlibrary.oemapis.toolbar;
 
-import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.view.View;
 
@@ -27,9 +26,6 @@
 @SuppressWarnings("AndroidJdkLibsChecker")
 public interface ToolbarControllerOEMV1 {
 
-    /** Gets the context used by the views of this toolbar */
-    Context getContext();
-
     /**
      * Sets the title of the toolbar to a CharSequence.
      *
@@ -59,8 +55,9 @@
      * or else the list could be modified from the app when the toolbar wasn't expecting it.
      *
      * @param tabs Nullable. Must not be mutated. List of tabs to show.
+     * @param selectedTab The index of the tab that is initially selected.
      */
-    void setTabs(List<? extends TabOEMV1> tabs);
+    void setTabs(List<? extends TabOEMV1> tabs, int selectedTab);
 
     /**
      * Selects a tab added to this toolbar. See
diff --git a/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/BaseLayoutInstaller.java b/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/BaseLayoutInstaller.java
index d28d330..78c1f62 100644
--- a/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/BaseLayoutInstaller.java
+++ b/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/BaseLayoutInstaller.java
@@ -64,7 +64,8 @@
                 ViewGroup.LayoutParams.MATCH_PARENT,
                 ViewGroup.LayoutParams.MATCH_PARENT));
 
-        ToolbarControllerOEMV1 toolbarController = new ToolbarControllerImpl(baseLayout);
+        ToolbarControllerOEMV1 toolbarController = new ToolbarControllerImpl(
+                baseLayout, sharedLibraryContext);
 
         InsetsUpdater updater = new InsetsUpdater(baseLayout, contentView);
         updater.replaceInsetsChangedListenerWith(insetsChangedListener);
diff --git a/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/TabLayout.java b/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/TabLayout.java
new file mode 100644
index 0000000..3a7f358
--- /dev/null
+++ b/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/TabLayout.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2021 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.google.car.ui.sharedlibrary.toolbar;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.ui.sharedlibrary.oemapis.toolbar.TabOEMV1;
+
+import com.google.car.ui.sharedlibrary.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A view that can show tabs via the {@link #setTabs(List, int)} method.
+ */
+public class TabLayout extends LinearLayout {
+    private List<? extends TabOEMV1> mTabs = new ArrayList<>();
+    private TabOEMV1 mSelectedTab;
+    private final Map<TabOEMV1, View> mTabViews = new HashMap<>();
+
+    public TabLayout(Context context) {
+        super(context);
+    }
+
+    public TabLayout(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public TabLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    /**
+     * Sets a list of tabs to show.
+     *
+     * @param tabs The tabs to show.
+     * @param selectedTab Which tab is selected.
+     */
+    public void setTabs(@NonNull List<? extends TabOEMV1> tabs, int selectedTab) {
+        removeAllViews();
+        mTabViews.clear();
+        mSelectedTab = null;
+        mTabs = tabs;
+
+        if (tabs != null && tabs.size() > 0) {
+            mSelectedTab = tabs.get(selectedTab);
+            for (TabOEMV1 tab : tabs) {
+                View view = LayoutInflater.from(getContext())
+                        .inflate(R.layout.toolbar_tab, this, false);
+                addView(view);
+                tab.setUpdateListener(this::bindTab);
+                mTabViews.put(tab, view);
+                bindTab(tab);
+            }
+        }
+    }
+
+    private void bindTab(TabOEMV1 tab) {
+        View view = mTabViews.get(tab);
+        if (view == null) {
+            return;
+        }
+
+        ImageView iconView = view.requireViewById(R.id.car_ui_toolbar_tab_item_icon);
+        TextView textView = view.requireViewById(R.id.car_ui_toolbar_tab_item_text);
+
+        view.setOnClickListener(v -> selectTab(tab));
+        iconView.setImageDrawable(tab.getIcon());
+        textView.setText(tab.getTitle());
+
+        boolean selected = tab == mSelectedTab;
+        view.setActivated(selected);
+        textView.setTextAppearance(selected
+                ? R.style.TextAppearance_CarUi_Widget_Toolbar_Tab_Selected
+                : R.style.TextAppearance_CarUi_Widget_Toolbar_Tab);
+    }
+
+    /**
+     * Selects a particular tab.
+     */
+    private void selectTab(TabOEMV1 tab) {
+        if (mSelectedTab == tab) {
+            return;
+        }
+
+        TabOEMV1 oldSelectedTab = mSelectedTab;
+        mSelectedTab = tab;
+        bindTab(oldSelectedTab);
+        bindTab(mSelectedTab);
+
+        Runnable onClickListener = tab.getOnClickListener();
+        if (onClickListener != null) {
+            onClickListener.run();
+        }
+    }
+
+    /**
+     * Selects a particular tab by its position in the list.
+     */
+    public void selectTab(int position) {
+        selectTab(mTabs.get(position));
+    }
+
+    /**
+     * Returns if this TabLayout has at least one tab.
+     */
+    public boolean hasTabs() {
+        return mTabs != null && mTabs.size() > 0;
+    }
+}
diff --git a/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/ToolbarControllerImpl.java b/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/ToolbarControllerImpl.java
index b96a896..329fd6f 100644
--- a/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/ToolbarControllerImpl.java
+++ b/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/ToolbarControllerImpl.java
@@ -30,19 +30,21 @@
 
 import com.google.car.ui.sharedlibrary.R;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.function.Consumer;
 
 @SuppressWarnings("AndroidJdkLibsChecker")
 class ToolbarControllerImpl implements ToolbarControllerOEMV1 {
 
-    private final Context mContext;
+    private final Context mSharedLibraryContext;
     private final ImageView mBackButtonView;
     private final TextView mTitleView;
     private final TextView mSubtitleView;
     private final ImageView mLogo;
     private final ImageView mLogoInNavIconSpace;
     private final ProgressBarController mProgressBar;
+    private final TabLayout mTabContainer;
 
     private Runnable mBackListener;
     private int mNavButtonMode = ToolbarControllerOEMV1.NAV_BUTTON_MODE_BACK;
@@ -50,9 +52,8 @@
     private boolean mBackButtonVisible = false;
     private boolean mHasLogo = false;
 
-    ToolbarControllerImpl(View view) {
-        mContext = view.getContext();
-
+    ToolbarControllerImpl(View view, Context sharedLibraryContext) {
+        mSharedLibraryContext = sharedLibraryContext;
         mBackButtonView = view.requireViewById(R.id.toolbar_nav_icon);
         mTitleView = view.requireViewById(R.id.toolbar_title);
         mSubtitleView = view.requireViewById(R.id.toolbar_subtitle);
@@ -60,11 +61,7 @@
         mLogoInNavIconSpace = view.requireViewById(R.id.toolbar_logo);
         mProgressBar = new ProgressBarController(
                 view.requireViewById(R.id.toolbar_progress_bar));
-    }
-
-    @Override
-    public Context getContext() {
-        return mContext;
+        mTabContainer = view.requireViewById(R.id.toolbar_tabs);
     }
 
     @Override
@@ -73,8 +70,6 @@
         mTitleView.setText(title);
         boolean hasTitle = !TextUtils.isEmpty(getTitle());
 
-        setVisible(mTitleView, hasTitle);
-
         if (hadTitle != hasTitle) {
             update();
         }
@@ -91,8 +86,6 @@
         mSubtitleView.setText(title);
         boolean hasSubtitle = !TextUtils.isEmpty(getSubtitle());
 
-        setVisible(mSubtitleView, hasSubtitle);
-
         if (hadSubtitle != hasSubtitle) {
             update();
         }
@@ -104,13 +97,23 @@
     }
 
     @Override
-    public void setTabs(List<? extends TabOEMV1> tabs) {
+    public void setTabs(List<? extends TabOEMV1> tabs, int selectedTab) {
+        if (tabs == null) {
+            tabs = Collections.emptyList();
+        }
 
+        boolean hadTabs = mTabContainer.hasTabs();
+        mTabContainer.setTabs(tabs, selectedTab);
+        boolean hasTabs = mTabContainer.hasTabs();
+
+        if (hadTabs != hasTabs) {
+            update();
+        }
     }
 
     @Override
     public void selectTab(int position) {
-
+        mTabContainer.selectTab(position);
     }
 
     @Override
@@ -254,6 +257,10 @@
         setVisible(mBackButtonView, mBackButtonVisible);
         setVisible(mLogoInNavIconSpace, mHasLogo && !mBackButtonVisible);
         setVisible(mLogo, mHasLogo && mBackButtonVisible);
+        boolean hasTabs = mTabContainer.hasTabs();
+        setVisible(mTabContainer, hasTabs);
+        setVisible(mTitleView, !TextUtils.isEmpty(getTitle()) && !hasTabs);
+        setVisible(mSubtitleView, !TextUtils.isEmpty(getSubtitle()) && !hasTabs);
     }
 
     private static void setVisible(View view, boolean visible) {
diff --git a/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/base_layout_toolbar.xml b/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/base_layout_toolbar.xml
index 77c56c0..cbcc9a1 100644
--- a/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/base_layout_toolbar.xml
+++ b/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/base_layout_toolbar.xml
@@ -86,7 +86,7 @@
                 android:textAppearance="?android:attr/textAppearanceSmall"/>
         </LinearLayout>
 
-        <LinearLayout
+        <com.google.car.ui.sharedlibrary.toolbar.TabLayout
             android:id="@+id/toolbar_tabs"
             android:layout_width="wrap_content"
             android:layout_height="0dp"
@@ -94,7 +94,7 @@
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toStartOf="@+id/toolbar_menu_items_container"
             app:layout_constraintHorizontal_bias="0.0"
-            app:layout_constraintStart_toEndOf="@+id/toolbar_title_logo_container"
+            app:layout_constraintStart_toEndOf="@+id/toolbar_title_logo"
             app:layout_constraintTop_toTopOf="parent" />
 
         <LinearLayout
diff --git a/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/toolbar_tab.xml b/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/toolbar_tab.xml
new file mode 100644
index 0000000..7ca84be
--- /dev/null
+++ b/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/toolbar_tab.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:paddingStart="12dp"
+    android:paddingEnd="12dp"
+    android:gravity="center"
+    android:background="?android:attr/selectableItemBackground">
+    <ImageView
+        android:id="@+id/car_ui_toolbar_tab_item_icon"
+        android:layout_width="36dp"
+        android:layout_height="36dp"
+        android:scaleType="fitCenter"
+        android:tint="@color/toolbar_tab_item_selector"
+        android:tintMode="src_in" />
+    <TextView
+        android:id="@+id/car_ui_toolbar_tab_item_text"
+        android:layout_width="135dp"
+        android:layout_height="wrap_content"
+        android:singleLine="true"
+        android:gravity="center"
+        android:textAppearance="@style/TextAppearance.CarUi.Widget.Toolbar.Tab"/>
+</LinearLayout>
diff --git a/car-ui-lib/referencedesign/sharedlibrary/src/main/res/values/values-toolbar.xml b/car-ui-lib/referencedesign/sharedlibrary/src/main/res/values/values-toolbar.xml
index b7c39b7..230f314 100644
--- a/car-ui-lib/referencedesign/sharedlibrary/src/main/res/values/values-toolbar.xml
+++ b/car-ui-lib/referencedesign/sharedlibrary/src/main/res/values/values-toolbar.xml
@@ -3,4 +3,14 @@
     <dimen name="toolbar_margin">112dp</dimen>
     <dimen name="toolbar_row_height">96dp</dimen>
     <dimen name="toolbar_menu_item_icon_ripple_radius">48dp</dimen>
+
+    <style name="TextAppearance.CarUi.Widget.Toolbar.Tab" parent="TextAppearance.CarUi.Body3">
+        <item name="android:textColor">@color/toolbar_tab_item_selector</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:textFontWeight">400</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.Widget.Toolbar.Tab.Selected">
+        <item name="android:textFontWeight">500</item>
+    </style>
 </resources>